@kurtbruns/kestrel 1.7.0 → 1.9.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.
@@ -5,26 +5,28 @@
5
5
  different values on purpose; don't assume they share meaning. */
6
6
  :root {
7
7
  color-scheme: light dark;
8
- /* Layering — one named z-index ladder (DESIGN §4). Every fixed or floating surface
8
+ /* Layering — one named z-index ladder (DESIGN §9). Every fixed or floating surface
9
9
  references a rung by token, never a bare literal; the order encodes the intent.
10
10
  z-index is theme-independent, so it lives only here (not the dark block). */
11
11
  --z-content: 1;
12
12
  --z-header: 10;
13
13
  --z-menu: 20;
14
14
  --z-bar: 30;
15
- --z-toast: 40;
16
- --z-modal: 50;
15
+ --z-modal: 40;
16
+ /* Toasts above modals: a toast raised while a dialog is open (an autosave that failed,
17
+ a send that started) must not sit dimmed beneath its backdrop. */
18
+ --z-toast: 50;
17
19
  --fg: #18181b;
18
20
  --muted: #52525b;
19
21
  --line: #e4e4e7;
20
22
  --bg: #fbfbfa;
21
23
  --card: #fff;
22
24
  --hover: #efefef;
23
- /* Neutral ink — the strong near-black/near-white neutral (DESIGN §3). A text token
25
+ /* Neutral ink — the strong near-black/near-white neutral (DESIGN §8). A text token
24
26
  for headings and strong labels; never a button fill (Primary is --accent). */
25
27
  --ink: #18181b;
26
28
  --ink-on: #ffffff;
27
- /* Semantic feedback — one {fg, bg, line} scale each for danger / warn / ok (DESIGN §3).
29
+ /* Semantic feedback — one {fg, bg, line} scale each for danger / warn / ok (DESIGN §8).
28
30
  fg is the saturated hue (text on the soft bg, or a solid fill paired with an on-color);
29
31
  bg is the soft tint behind a notice; line is its border. There is one red, not two —
30
32
  the old --danger vs --b-bad-* split is collapsed here. Kept distinct from the
@@ -40,13 +42,11 @@
40
42
  --ok-bg: #dcfce7;
41
43
  --ok-line: #bbf7d0;
42
44
  --ring: #2563eb;
43
- --banner-bg: #eff6ff;
44
- --banner-line: #bfdbfe;
45
45
  --status-draft-bg: #f4f4f5;
46
46
  --status-draft-fg: #52525b;
47
47
  --status-draft-line: #d4d4d8;
48
48
  /* Scheduled is a violet, deliberately off the slate-blue action accent (--accent)
49
- so the "scheduled" status never reads as the primary action (DESIGN §3); the fill
49
+ so the "scheduled" status never reads as the primary action (DESIGN §8); the fill
50
50
  backs the scheduled pill, banner, and cards, and the line borders them. */
51
51
  --status-scheduled-bg: #ece6f9;
52
52
  --status-scheduled-fg: #6743ac;
@@ -65,7 +65,7 @@
65
65
  --accent: #3355cc;
66
66
  --accent-on: #ffffff;
67
67
  --accent-soft: rgba(51, 85, 204, 0.12);
68
- /* Syntax-highlight palette, shared by the template (issue #123) and post (issue #138)
68
+ /* Syntax-highlight palette, shared by the template and post
69
69
  editors — the HTML/CSS .cx-* tokens and the Markdown .cx-md-* tokens map onto it. */
70
70
  --syntax-bg: #fbfbfc;
71
71
  --syntax-fg: #3f3f46;
@@ -79,7 +79,7 @@
79
79
  --syntax-punctuation: #a1a1aa;
80
80
  --syntax-variable: #2d47b8;
81
81
  --syntax-variable-bg: rgba(51, 85, 204, 0.11);
82
- /* Post editor's minimal scheme (issue #138): marks get the color, content keeps --syntax-fg.
82
+ /* Post editor's minimal scheme: marks get the color, content keeps --syntax-fg.
83
83
  Heading # is crimson; the emphasis asterisks are a magenta-pink kept clear of the heading
84
84
  crimson; block markers (>, list bullets, fence language) are a saturated, bright orange. */
85
85
  --syntax-heading: #be123c;
@@ -88,7 +88,7 @@
88
88
  /* Code surface — inline `code` chip and the fenced-block band. A hair off the editor well
89
89
  (--syntax-bg), lighter than the neutral --chip, so code reads as code without a heavy box. */
90
90
  --syntax-code-bg: #f2f2f4;
91
- /* HTTP method badges in the API reference (DESIGN §3). A self-contained palette like
91
+ /* HTTP method badges in the API reference (DESIGN §8). A self-contained palette like
92
92
  --syntax-*: verb colors are a naming convention, not feedback or lifecycle, so they
93
93
  borrow no semantic scale. The badge tints each over a wash of itself (.api-method). */
94
94
  --method-get: #166534;
@@ -122,8 +122,6 @@
122
122
  --ok-bg: #14532d;
123
123
  --ok-line: #15803d;
124
124
  --ring: #60a5fa;
125
- --banner-bg: #1e293b;
126
- --banner-line: #334155;
127
125
  --status-draft-bg: #2e2e33;
128
126
  --status-draft-fg: #a1a1aa;
129
127
  --status-draft-line: #52525b;
@@ -157,7 +155,7 @@
157
155
  --syntax-punctuation: #6a6a72;
158
156
  --syntax-variable: #a9bbf2;
159
157
  --syntax-variable-bg: rgba(120, 150, 240, 0.18);
160
- /* Post editor minimal scheme (issue #138) — near-black/saturated hues lift to legible
158
+ /* Post editor minimal scheme — near-black/saturated hues lift to legible
161
159
  tints on the dark well; the heading crimson becomes a rose, emphasis a lighter pink. */
162
160
  --syntax-heading: #fb7185;
163
161
  --syntax-emphasis: #f472b6;
@@ -593,7 +591,7 @@ body.nav-resizing .sidebar {
593
591
  margin: 10px 2px 0;
594
592
  line-height: 1.5;
595
593
  }
596
- /* The connected state: an inline status line reusing the --ok feedback scale (DESIGN §3),
594
+ /* The connected state: an inline status line reusing the --ok feedback scale (DESIGN §8),
597
595
  not a notification — a quiet confirmation the agent has edited here. */
598
596
  .conn-status {
599
597
  display: flex;
@@ -900,8 +898,10 @@ body.nav-resizing .sidebar {
900
898
  margin-top: 12px;
901
899
  }
902
900
  /* The refused-configuration wall says more than the sign-in wall, so it reads as text: left
903
- aligned, a little wider, with the variable and the server's words set apart. */
904
- .config-wall {
901
+ aligned, a little wider, with the variable and the server's words set apart. The locked
902
+ editor, before Access is set up, reads the same way. */
903
+ .config-wall,
904
+ .access-wall {
905
905
  max-width: 560px;
906
906
  text-align: left;
907
907
  }
@@ -918,7 +918,8 @@ body.nav-resizing .sidebar {
918
918
  white-space: nowrap;
919
919
  border: 0;
920
920
  }
921
- .config-wall h2:focus {
921
+ .config-wall h2:focus,
922
+ .access-wall h2:focus {
922
923
  outline: none;
923
924
  }
924
925
  .config-wall-problem {
@@ -949,8 +950,8 @@ body.tool-mode .content {
949
950
  }
950
951
 
951
952
  /* --- Collapsed icon rail. Driven by data-nav="rail" on <html> (set from the viewport
952
- width and the persisted collapse preference — see app.js and the <head> guard in
953
- index.html), so the collapse toggle can pin it at any width in range. The rail is the
953
+ width and the collapse toggle's override for this load — see client/shell.ts and the
954
+ <head> guard in index.html), so the collapse toggle can pin it at any width in range. The rail is the
954
955
  default across the tablet range and small desktops; the nav keeps its one-click
955
956
  reach, labels move to hover/focus tooltips, and the secondary chrome (brand text,
956
957
  account, tool footer) folds down to its marks. The whole block is gated at ≥540 so a
@@ -1180,7 +1181,7 @@ button:hover {
1180
1181
  button:active {
1181
1182
  transform: translateY(0.5px);
1182
1183
  }
1183
- /* --- Buttons: five roles (DESIGN §1), chosen by importance and consequence, one
1184
+ /* --- Buttons: five roles (DESIGN §7), chosen by importance and consequence, one
1184
1185
  class each. The bare `button` above is the neutral default (Ghost's card-filled
1185
1186
  end); the classes below step up to Primary / Secondary or down to the quiet Ghost,
1186
1187
  plus Danger and the square Icon. Exactly one solid brand-blue action — Primary — is
@@ -1189,7 +1190,7 @@ button:active {
1189
1190
 
1190
1191
  /* One box height across the label roles, so a row that mixes them (the editor's
1191
1192
  Save draft / Send test email / Schedule, a modal's Cancel + confirm) aligns:
1192
- roles differ by treatment — fill, weight, font-size — never by height (DESIGN §1).
1193
+ roles differ by treatment — fill, weight, font-size — never by height (DESIGN §7).
1193
1194
  A smaller-font role (Secondary, Ghost) grows to this floor with its label centered
1194
1195
  rather than sitting a few pixels short of the Primary beside it. The bare `button`
1195
1196
  default already meets it; the square Icon role and the segmented toggle Control keep
@@ -1518,16 +1519,10 @@ td a:hover {
1518
1519
  color: var(--status-sending-fg);
1519
1520
  border-color: var(--status-sending-line);
1520
1521
  }
1521
- .badge.canceled,
1522
- .badge.failed {
1523
- background: var(--danger-bg);
1524
- color: var(--danger-fg);
1525
- border-color: var(--danger-line);
1526
- }
1527
1522
  /* Subscriber consent states borrow a hue that matches the state's meaning: confirmed →
1528
1523
  the sent green, unsubscribed → the draft grey, suppressed → danger red (below). Pending
1529
1524
  awaits double-opt-in — a waiting/attention state — so it takes the amber `warn` scale,
1530
- NOT `--status-sending`: the send "sending" badge is blue (DESIGN §3), and a pending subscriber
1525
+ NOT `--status-sending`: the send "sending" badge is blue (DESIGN §8), and a pending subscriber
1531
1526
  must never read as a send in flight. (The two coincided only while `--status-sending` was
1532
1527
  amber; when it went blue for the dispatch treatment, this pill must stay amber.) */
1533
1528
  .badge.confirmed {
@@ -1749,7 +1744,7 @@ body.web-mode .content {
1749
1744
  margin: 0;
1750
1745
  padding: 0;
1751
1746
  }
1752
- /* The page sits under one strip that says where the post stands (DESIGN §7). The strip is
1747
+ /* The page sits under one strip that says where the post stands (DESIGN §3). The strip is
1753
1748
  the editor's, above the frame, and the frame is as tall as the post, so the window scrolls
1754
1749
  and the strip goes up with the page like the test pages' strip. It wears the brand accent
1755
1750
  like the reader surface's dev pill, its tag as the pill's inverted chip, and its content
@@ -1815,8 +1810,8 @@ body.web-mode .content {
1815
1810
  margin: 48px auto;
1816
1811
  padding: 0 16px;
1817
1812
  }
1818
- /* Sent record view (#148): a read-only delivery record. The outcome tiles reuse the
1819
- semantic feedback scales (--ok / --warn / --danger, DESIGN §3); a status badge, not
1813
+ /* Sent record view: a read-only delivery record. The outcome tiles reuse the
1814
+ semantic feedback scales (--ok / --warn / --danger, DESIGN §8); a status badge, not
1820
1815
  a feedback color, still labels the send's lifecycle. */
1821
1816
  .rec-card {
1822
1817
  display: flex;
@@ -1916,7 +1911,7 @@ body.web-mode .content {
1916
1911
  }
1917
1912
  }
1918
1913
 
1919
- /* ---- per-recipient record (#164): the rows behind the outcome tiles ---- */
1914
+ /* ---- per-recipient record: the rows behind the outcome tiles ---- */
1920
1915
  .rec-people {
1921
1916
  margin-top: 26px;
1922
1917
  }
@@ -1931,7 +1926,7 @@ body.web-mode .content {
1931
1926
  margin: 0;
1932
1927
  font-size: 15px;
1933
1928
  }
1934
- /* The view toggle (Failures / Delivered / All) is a Control, not a button (DESIGN §1):
1929
+ /* The view toggle (Failures / Delivered / All) is a Control, not a button (DESIGN §7):
1935
1930
  the same segmented, aria-pressed pill group as the Template preview-width toggle, its
1936
1931
  active segment in the Secondary palette. It switches what you're looking at; it never
1937
1932
  commits an action. */
@@ -1983,8 +1978,8 @@ body.web-mode .content {
1983
1978
  font-size: 13px;
1984
1979
  }
1985
1980
 
1986
- /* ---- live in-flight watch (#154): the `sending` state of the record view ---- */
1987
- /* The sending treatment is blue (--status-sending-*, DESIGN §3) — distinct from the
1981
+ /* ---- live in-flight watch: the `sending` state of the record view ---- */
1982
+ /* The sending treatment is blue (--status-sending-*, DESIGN §8) — distinct from the
1988
1983
  action slate-blue — carried by the phase pill, the dispatch bar, and the swatch. */
1989
1984
  .rec-sw.sw-sending {
1990
1985
  background: var(--status-sending-fg);
@@ -2150,7 +2145,7 @@ body.web-mode .content {
2150
2145
  }
2151
2146
  }
2152
2147
 
2153
- /* ---- active-send row (Sent page "In progress") + dashboard widget (#154) ---- */
2148
+ /* ---- active-send row (Sent page "In progress") + dashboard widget ---- */
2154
2149
  /* An in-flight send as a card with a live mini dispatch bar; blue, so it reads as
2155
2150
  "sending" — distinct from the scheduled violet cards and the neutral sent rows. */
2156
2151
  .card.active-card {
@@ -2194,6 +2189,27 @@ body.web-mode .content {
2194
2189
  font-size: 12.5px;
2195
2190
  color: var(--warn-fg);
2196
2191
  }
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. */
2194
+ .sched-held {
2195
+ margin-top: 4px;
2196
+ font-size: 12.5px;
2197
+ font-weight: 500;
2198
+ color: var(--warn-fg);
2199
+ }
2200
+ /* The editor's scheduled banner stacks its line and the held line on the left. */
2201
+ .banner-scheduled .sched-main {
2202
+ display: flex;
2203
+ flex-direction: column;
2204
+ /* Takes the room and wraps its own lines (the re-made line is a sentence), so Reschedule
2205
+ and Cancel stay at the right rather than dropping below; on a phone the row still wraps. */
2206
+ flex: 1 1 18rem;
2207
+ min-width: 0;
2208
+ }
2209
+ /* A draft's not-connected line sits above its actions bar, which keeps its own top gap. */
2210
+ .health.editor-not-connected {
2211
+ margin: 16px 0 0;
2212
+ }
2197
2213
 
2198
2214
  /* Composer: GitHub-style Write/Preview tabs + a formatting toolbar */
2199
2215
  .composer {
@@ -2327,7 +2343,7 @@ body.web-mode .content {
2327
2343
  .composer-body {
2328
2344
  position: relative;
2329
2345
  }
2330
- /* Post editor syntax-highlight overlay (issue #138): a transparent <textarea> layered over
2346
+ /* Post editor syntax-highlight overlay: a transparent <textarea> layered over
2331
2347
  a highlighted <pre> that shows the colored Markdown beneath the caret. The two share
2332
2348
  identical box + font metrics so the caret sits exactly on the colored text; the textarea
2333
2349
  is the only scroller and the <pre> is synced to it in script. Unlike the template editor
@@ -2402,7 +2418,7 @@ body.web-mode .content {
2402
2418
  background: #fff;
2403
2419
  display: block;
2404
2420
  }
2405
- /* A scheduled post's Edit tab (DESIGN §7): the text stays fully legible, selectable, and
2421
+ /* A scheduled post's Edit tab (DESIGN §3): the text stays fully legible, selectable, and
2406
2422
  copyable (the fields are readonly, not disabled, since a disabled textarea cannot even be
2407
2423
  selected), on the recessed surface Settings uses for a read-only card (`.set-card.sunken`);
2408
2424
  the caret is hidden because the field is not for typing. The way out is the composer's
@@ -2462,7 +2478,7 @@ body.web-mode .content {
2462
2478
  }
2463
2479
  /* The locked composer's foot: where a draft's foot offers images, a scheduled post's says
2464
2480
  the way out, "Cancel the schedule to edit", with the read-only lock Settings uses. It is
2465
- a hint, not a control (Cancel stays on the banner, DESIGN §7), so no pointer and no hover
2481
+ a hint, not a control (Cancel stays on the banner, DESIGN §3), so no pointer and no hover
2466
2482
  lift. `.nudge` pulses it once, in the read-only note's tint, when an edit is attempted,
2467
2483
  so the refusal is seen where the keystroke landed; reduced-motion holds the tint instead. */
2468
2484
  .composer-foot-lock {
@@ -2501,7 +2517,14 @@ body.web-mode .content {
2501
2517
  .actions-bar > .row:only-child {
2502
2518
  margin-left: auto;
2503
2519
  }
2504
- /* The autosave cue (DESIGN §2, home ⑤), beside Save draft. Model B's save lifecycle:
2520
+ /* The save side takes the room left over and lets a long cue (why a save failed) wrap
2521
+ inside it, so the test and schedule buttons keep their place at the right. */
2522
+ .actions-bar > .row:first-child:not(:only-child) {
2523
+ flex: 1 1 16rem;
2524
+ min-width: 0;
2525
+ flex-wrap: nowrap;
2526
+ }
2527
+ /* The autosave cue (DESIGN §5, §6), beside Save draft. The editor's save lifecycle:
2505
2528
  Unsaved (amber --warn dot) · Saving… (pulsing --accent dot) · Saved (green --ok
2506
2529
  dot). The dot is a reinforcement, never the whole signal — the text label carries
2507
2530
  each state too, so the cue survives a reader who can't tell the dot's color. The
@@ -2510,6 +2533,7 @@ body.web-mode .content {
2510
2533
  .save-status {
2511
2534
  display: inline-flex;
2512
2535
  align-items: center;
2536
+ min-width: 0;
2513
2537
  gap: 7px;
2514
2538
  font-size: 13px;
2515
2539
  color: var(--muted);
@@ -2520,9 +2544,13 @@ body.web-mode .content {
2520
2544
  .save-status.is-dirty {
2521
2545
  color: var(--fg);
2522
2546
  }
2547
+ .save-status.is-error {
2548
+ color: var(--danger-fg);
2549
+ }
2523
2550
  .save-status.is-dirty::before,
2524
2551
  .save-status.is-saving::before,
2525
- .save-status.is-saved::before {
2552
+ .save-status.is-saved::before,
2553
+ .save-status.is-error::before {
2526
2554
  content: "";
2527
2555
  width: 8px;
2528
2556
  height: 8px;
@@ -2539,6 +2567,9 @@ body.web-mode .content {
2539
2567
  .save-status.is-saved::before {
2540
2568
  background: var(--ok-fg);
2541
2569
  }
2570
+ .save-status.is-error::before {
2571
+ background: var(--danger-fg);
2572
+ }
2542
2573
  @keyframes save-pulse {
2543
2574
  0%,
2544
2575
  100% {
@@ -2637,7 +2668,7 @@ body.web-mode .content {
2637
2668
  margin: 0 0 14px;
2638
2669
  line-height: 1.5;
2639
2670
  }
2640
- /* A dialog's not-connected line (DESIGN §2) takes the health block's amber warning, so it
2671
+ /* A dialog's not-connected line (DESIGN §4) takes the health block's amber warning, so it
2641
2672
  reads as the reason the dialog's action is off rather than as one more hint. */
2642
2673
  .modal .hint.not-connected {
2643
2674
  color: var(--warn-fg);
@@ -2649,6 +2680,15 @@ body.web-mode .content {
2649
2680
  .modal .hint.not-connected a {
2650
2681
  color: inherit;
2651
2682
  }
2683
+ /* On Workers Free's limits, a send too large for D1's daily writes (DESIGN §4): the same amber
2684
+ box, since it is a warning to read before scheduling, though it disables nothing. */
2685
+ .modal .hint.daily-writes {
2686
+ color: var(--warn-fg);
2687
+ background: var(--warn-bg);
2688
+ border: 1px solid var(--warn-line);
2689
+ border-radius: 8px;
2690
+ padding: 10px 12px;
2691
+ }
2652
2692
  .modal .actions {
2653
2693
  display: flex;
2654
2694
  justify-content: flex-end;
@@ -2666,10 +2706,16 @@ body.web-mode .content {
2666
2706
  padding-top: 14px;
2667
2707
  border-top: 1px solid var(--line);
2668
2708
  }
2669
- /* The alternate-path row can carry a plain note beside its link. */
2709
+ /* The alternate-path row can carry a plain note beside its link; the note wraps and the
2710
+ link stays whole on its line. */
2670
2711
  .modal .altrow .altrow-note {
2671
2712
  font-size: 12.5px;
2672
2713
  color: var(--muted);
2714
+ min-width: 0;
2715
+ }
2716
+ .modal .altrow .linkbtn {
2717
+ flex: none;
2718
+ white-space: nowrap;
2673
2719
  }
2674
2720
  /* The back link at the top of the Send-now view needs no divider — it returns from,
2675
2721
  rather than demotes below, the Schedule view. */
@@ -2679,7 +2725,7 @@ body.web-mode .content {
2679
2725
  border-top: 0;
2680
2726
  justify-content: flex-start;
2681
2727
  }
2682
- /* Link — a text-link-styled action (DESIGN §1), not one of the button roles. Reserved
2728
+ /* Link — a text-link-styled action (DESIGN §7), not one of the button roles. Reserved
2683
2729
  for a deliberately subordinate choice, like the in-modal Send now that sits below
2684
2730
  the Schedule Primary: it must read as clearly demoted, so it is a link, not a Ghost
2685
2731
  button that would compete with Cancel. Still a real <button>, so it's keyboard- and
@@ -2700,12 +2746,12 @@ button.linkbtn:hover {
2700
2746
 
2701
2747
  .countdown {
2702
2748
  font-variant-numeric: tabular-nums;
2703
- /* The scheduled violet (DESIGN §3), so the fire-time countdown on the scheduled
2749
+ /* The scheduled violet (DESIGN §8), so the fire-time countdown on the scheduled
2704
2750
  cards (Sent page + dashboard) carries the same "scheduled" cue as the pill. */
2705
2751
  color: var(--status-scheduled-fg);
2706
2752
  }
2707
2753
  /* Past the server's missed tolerance the countdown says how late the send is, in the
2708
- danger tone: a missed fire time needs a look (DESIGN §9). */
2754
+ danger tone: a missed fire time needs a look (DESIGN §4). */
2709
2755
  .countdown.countdown-missed {
2710
2756
  color: var(--danger-fg);
2711
2757
  }
@@ -2829,6 +2875,23 @@ td.act {
2829
2875
  .menu-item.danger-item {
2830
2876
  color: var(--danger-fg);
2831
2877
  }
2878
+ /* A menu whose items say what each pick does: a name over a muted line. */
2879
+ .menu-described {
2880
+ width: 280px;
2881
+ max-width: min(280px, 78vw);
2882
+ }
2883
+ .menu-item-name {
2884
+ display: block;
2885
+ font-size: 13px;
2886
+ font-weight: 600;
2887
+ }
2888
+ .menu-item-desc {
2889
+ display: block;
2890
+ margin-top: 1px;
2891
+ font-size: 11.5px;
2892
+ color: var(--muted);
2893
+ line-height: 1.35;
2894
+ }
2832
2895
 
2833
2896
  /* editor field hints + scheduled banner */
2834
2897
  .field-hint {
@@ -2836,7 +2899,7 @@ td.act {
2836
2899
  color: var(--muted);
2837
2900
  margin-top: 4px;
2838
2901
  }
2839
- /* --- Field validation (DESIGN §2, home ④): an error about one field lives on that
2902
+ /* --- Field validation (DESIGN §5): an error about one field lives on that
2840
2903
  field — the input goes to an invalid state and the reason sits directly beneath it,
2841
2904
  tied by aria-describedby. This replaces the old disable-the-button + `title` tooltip,
2842
2905
  which never reached keyboard, touch, or screen-reader users. --- */
@@ -2856,6 +2919,11 @@ textarea.is-invalid {
2856
2919
  .field-error .field-error-ico {
2857
2920
  font-weight: 700;
2858
2921
  }
2922
+ /* A dialog's own error, when no one field is at fault: just above its actions, where the
2923
+ reader is looking rather than in a toast at the foot of the screen (DESIGN §5). */
2924
+ .modal .dialog-error {
2925
+ margin-top: 12px;
2926
+ }
2859
2927
  /* Label + info icon grouped at the top of the slug field. */
2860
2928
  .label-row {
2861
2929
  display: flex;
@@ -2964,11 +3032,10 @@ textarea.is-invalid {
2964
3032
  #f-slug.slug-auto {
2965
3033
  color: var(--muted);
2966
3034
  }
2967
- /* --- Editor-state banner (DESIGN §2, home ③): one persistent, in-flow notice at the
2968
- top of the editor, in three variants. `scheduled` marks a frozen/soft-locked post
2969
- (SPEC §6); `conflict` warns the draft changed elsewhere (another tab, or Claude) —
2970
- its two actions are equal-weight Ghost, since both choices lose something; `info` is
2971
- a neutral notice. It replaces the old `.sched-banner` + `.fresh-banner` pair. --- */
3035
+ /* --- Editor-state banner (DESIGN §5): a standing, in-flow line at the top of the
3036
+ editor, in two variants. `scheduled` marks a frozen, soft-locked post (SPEC §6);
3037
+ `conflict` warns the draft changed elsewhere (another tab, or Claude), and its two
3038
+ actions are equal-weight Ghost, since both choices lose something. --- */
2972
3039
  .banner {
2973
3040
  display: flex;
2974
3041
  align-items: center;
@@ -2997,59 +3064,35 @@ textarea.is-invalid {
2997
3064
  color: var(--warn-fg);
2998
3065
  border-color: var(--warn-line);
2999
3066
  }
3000
- .banner-info {
3001
- background: var(--banner-bg);
3002
- color: var(--fg);
3003
- border-color: var(--banner-line);
3004
- }
3005
3067
 
3006
- /* --- Notice (DESIGN §2, home ⑥): an event the reader could not otherwise know happened,
3007
- shown once and cleared by the reader, asking nothing. It wears the banner's `info` tone
3008
- (the same tokens as .banner-info, so the tint has one definition) but is its own class:
3009
- its layout is prose plus one control, not a status line with an actions row, so it never
3010
- wraps the dismiss beneath the text the way the banner wraps its buttons. Padding is a
3011
- full 1rem because it is a sentence or two, not a status line. Removed on dismiss with
3012
- no animation. Both themes come through the existing tokens. --- */
3013
- .notice {
3068
+ /* Look changed since your last test (SPEC §8): the server's condition, a line in the scheduled
3069
+ banner that says to send a new test, and a mark on the dashboard's scheduled card. */
3070
+ .sched-remade {
3014
3071
  display: flex;
3015
- align-items: flex-start;
3016
- gap: 12px;
3017
- border: 1px solid var(--banner-line);
3018
- border-radius: 8px;
3019
- background: var(--banner-bg);
3020
- color: var(--fg);
3021
- padding: 1rem;
3022
- margin-bottom: 14px;
3023
- font-size: 14px;
3024
- line-height: 1.45;
3025
- }
3026
- .notice-text {
3027
- flex: 1 1 auto;
3028
- min-width: 0;
3072
+ flex-wrap: wrap;
3073
+ align-items: center;
3074
+ gap: 4px 10px;
3075
+ margin-top: 6px;
3076
+ font-size: 13px;
3029
3077
  }
3030
- /* Links are allowed (buttons are not); underlined so they stay findable on the tint. */
3031
- .notice a {
3032
- color: var(--accent);
3033
- font-weight: 550;
3034
- text-decoration: underline;
3035
- text-underline-offset: 2px;
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 {
3082
+ display: flex;
3083
+ gap: 6px;
3084
+ align-items: baseline;
3085
+ margin-top: 6px;
3086
+ font-size: 12.5px;
3087
+ color: var(--status-scheduled-fg);
3036
3088
  }
3037
- /* The dismiss is the Icon role (button.icon) carrying the app's `x` glyph: a square hit
3038
- target pinned to the right, pulled 4px into the padding on three sides so it centers on
3039
- the first line of text without making a one-line notice taller than its text. */
3040
- button.notice-dismiss {
3041
- flex: 0 0 auto;
3042
- display: inline-flex;
3043
- align-items: center;
3044
- justify-content: center;
3045
- width: 28px;
3046
- height: 28px;
3047
- padding: 0;
3048
- margin: -4px -4px -4px 0;
3089
+ .sched-note-mark {
3090
+ flex: none;
3049
3091
  }
3050
- button.notice-dismiss svg {
3051
- width: 14px;
3052
- height: 14px;
3092
+ .sched-remade-mark {
3093
+ margin-top: 4px;
3094
+ font-size: 12.5px;
3095
+ color: var(--status-scheduled-fg);
3053
3096
  }
3054
3097
 
3055
3098
  .grid2 {
@@ -3931,7 +3974,7 @@ button.api-filter-clear svg {
3931
3974
  text-decoration: underline;
3932
3975
  }
3933
3976
 
3934
- /* Publication card: the live-site + archive URLs with copy buttons. */
3977
+ /* Publication card: the home page's address with Copy, and the way out to it. */
3935
3978
  .pub-card {
3936
3979
  display: flex;
3937
3980
  flex-direction: column;
@@ -3942,10 +3985,6 @@ button.api-filter-clear svg {
3942
3985
  align-items: center;
3943
3986
  gap: 12px;
3944
3987
  }
3945
- .pub-key {
3946
- flex: 0 0 84px;
3947
- font-size: 12px;
3948
- }
3949
3988
  .pub-val {
3950
3989
  flex: 1 1 auto;
3951
3990
  min-width: 0;
@@ -4052,10 +4091,9 @@ button.api-filter-clear svg {
4052
4091
  deploy / Copy-out), editable surfaces sit on cards and read-only
4053
4092
  facts on a recessed "sunken" card. Kestrel's own accents use the
4054
4093
  fixed --accent (a slate-blue), independent of the reader surface.
4055
- A sticky save bar collects the unsaved, persisted changes
4056
- (identity + test recipients); the logo and the email-template
4057
- mock are handled on their own and don't feed it. Theme follows
4058
- the OS via the tokens at the top of this file.
4094
+ A sticky save bar collects every editable section's unsaved
4095
+ changes; the logo saves on its own and doesn't feed it. Theme
4096
+ follows the OS via the tokens at the top of this file.
4059
4097
  ============================================================ */
4060
4098
  .settings {
4061
4099
  max-width: 940px;
@@ -4081,7 +4119,7 @@ button.api-filter-clear svg {
4081
4119
  margin-bottom: 34px;
4082
4120
  }
4083
4121
 
4084
- /* --- copying text out of the app (DESIGN §1, ui/copy.ts) ---
4122
+ /* --- copying text out of the app (DESIGN §7, ui/copy.ts) ---
4085
4123
  A labelled block (a code block, or the API reference's query and content-type tables):
4086
4124
  its label line, then the block. The Copy is a control outside the five roles: the word
4087
4125
  and its glyph, no fill or border, quiet on the line it shares; the glyph turns to a
@@ -4143,7 +4181,7 @@ button.copied svg {
4143
4181
  flex: 0 0 auto;
4144
4182
  }
4145
4183
 
4146
- /* --- groups: the sections kept together by whom they serve (DESIGN §5). A group has no
4184
+ /* --- groups: the sections kept together by whom they serve (DESIGN §3). A group has no
4147
4185
  heading, since each section's title already says what it is; a wider gap than the one
4148
4186
  between sections is all that separates one group from the next. --- */
4149
4187
  .set-group + .set-group {
@@ -4194,7 +4232,7 @@ button.copied svg {
4194
4232
  background: var(--accent-soft);
4195
4233
  }
4196
4234
  /* In use by scheduled posts (SPEC §9): the scheduled violet, a lifecycle fact and not a
4197
- warning, the same reading as the scheduled badge (DESIGN §3). Adds no token. */
4235
+ warning, the same reading as the scheduled badge (DESIGN §8). Adds no token. */
4198
4236
  .set-chip.inuse {
4199
4237
  color: var(--status-scheduled-fg);
4200
4238
  border-color: var(--status-scheduled-line);
@@ -4207,22 +4245,12 @@ button.copied svg {
4207
4245
  background: currentColor;
4208
4246
  opacity: 0.85;
4209
4247
  }
4248
+ /* Set at deploy: a fact about where the value lives, not a warning, so it stays neutral
4249
+ rather than borrowing the amber `warn` scale (DESIGN §8). */
4210
4250
  .set-chip.readonly {
4211
- color: var(--warn-fg);
4212
- border-color: color-mix(in srgb, var(--warn-fg) 30%, var(--line-soft));
4213
- background: color-mix(in srgb, var(--warn-fg) 9%, transparent);
4214
- }
4215
- /* A dashed "Mock" tag beside the Email-template chip — says the section is a
4216
- preview of a feature not wired to the backend yet. */
4217
- .set-tag-mock {
4218
- font-size: 10px;
4219
- font-weight: 650;
4220
- letter-spacing: 0.06em;
4221
- text-transform: uppercase;
4222
- padding: 3px 7px;
4223
- border-radius: 6px;
4224
- border: 1px dashed var(--line-soft);
4225
4251
  color: var(--muted);
4252
+ border-color: var(--line);
4253
+ background: var(--chip);
4226
4254
  }
4227
4255
 
4228
4256
  /* --- cards: editable surfaces raised, read-only facts recessed --- */
@@ -4411,7 +4439,7 @@ button.set-field-remove {
4411
4439
  align-items: flex-start;
4412
4440
  padding: 11px 16px;
4413
4441
  border-bottom: 1px solid var(--line-soft);
4414
- background: color-mix(in srgb, var(--warn-fg) 6%, transparent);
4442
+ background: var(--chip);
4415
4443
  font-size: 12.5px;
4416
4444
  color: var(--muted);
4417
4445
  line-height: 1.5;
@@ -4421,7 +4449,7 @@ button.set-field-remove {
4421
4449
  height: 15px;
4422
4450
  flex: 0 0 auto;
4423
4451
  margin-top: 1px;
4424
- color: var(--warn-fg);
4452
+ color: var(--muted);
4425
4453
  }
4426
4454
  .set-kv {
4427
4455
  display: grid;
@@ -4758,14 +4786,21 @@ button.set-field-remove {
4758
4786
  }
4759
4787
  .set-recip-add {
4760
4788
  display: flex;
4789
+ flex-wrap: wrap;
4761
4790
  gap: 8px;
4762
4791
  max-width: 420px;
4763
4792
  }
4764
4793
  .set-recip-add input {
4765
4794
  flex: 1;
4766
4795
  }
4796
+ /* A refused address says why on its own line under the field and its button (DESIGN §5). */
4797
+ .set-recip-add .field-error {
4798
+ flex-basis: 100%;
4799
+ order: 1;
4800
+ margin-top: 0;
4801
+ }
4767
4802
 
4768
- /* --- ways to subscribe (DESIGN §5): one card, the two ways as bands in reading order,
4803
+ /* --- ways to subscribe (DESIGN §3): one card, the two ways as bands in reading order,
4769
4804
  the page then the form, split by a full-width rule like the other cards' bands. Each
4770
4805
  band has the same head (a glyph, a title, one line) so they read as two options. The
4771
4806
  card folds by the section's own width, since the sidebar decides how wide Settings is. --- */
@@ -4956,7 +4991,7 @@ button.set-embed-copy svg {
4956
4991
  color: var(--muted);
4957
4992
  }
4958
4993
 
4959
- /* --- email template (mock): a standalone sample-email frame, then a card holding
4994
+ /* --- email template: a standalone sample-email frame, then a card holding
4960
4995
  the HTML template editor and a collapsible variable reference. The example
4961
4996
  templates are authored with a <style> block; the preview renders them in an
4962
4997
  isolated iframe, so a template's own styles apply as a mail client would show
@@ -4991,7 +5026,7 @@ button.set-embed-copy svg {
4991
5026
  font-size: 12px;
4992
5027
  color: var(--muted);
4993
5028
  }
4994
- /* Syntax-highlight overlay editor (issue #123): a transparent <textarea> over a
5029
+ /* Syntax-highlight overlay editor: a transparent <textarea> over a
4995
5030
  highlighted <pre>, plus a line-number gutter. All three share identical font
4996
5031
  metrics so the caret sits exactly over the colored text; the textarea is the only
4997
5032
  scroller and the gutter + overlay are synced to it in script. */
@@ -5067,7 +5102,7 @@ button.set-embed-copy svg {
5067
5102
  .set-tpl-editor-wrap.show-lines .set-tpl-editor {
5068
5103
  left: 47px;
5069
5104
  }
5070
- /* Syntax colors (issue #123). */
5105
+ /* Syntax colors. */
5071
5106
  .cx-tag {
5072
5107
  color: var(--syntax-tag);
5073
5108
  }
@@ -5097,7 +5132,7 @@ button.set-embed-copy svg {
5097
5132
  padding: 0 2px;
5098
5133
  font-weight: 600;
5099
5134
  }
5100
- /* Markdown token classes (issue #138) — the minimal scheme: the MARK carries the color, the
5135
+ /* Markdown token classes — the minimal scheme: the MARK carries the color, the
5101
5136
  CONTENT keeps --syntax-fg and carries only weight/slant. So a bold word reads as weight, not
5102
5137
  as a hue that competes with a heading. Heading text is bold with a crimson # mark; emphasis
5103
5138
  asterisks are pink around plain weighted/slanted text; block markers (>, bullets, the fence
@@ -5309,7 +5344,7 @@ button.set-embed-copy svg {
5309
5344
  background: var(--accent-soft);
5310
5345
  border-color: color-mix(in srgb, var(--accent) 30%, var(--line-soft));
5311
5346
  }
5312
- /* Required-token pills (issue #123 revisit): live green/red for the two tokens whose
5347
+ /* Required-token pills: live green/red for the two tokens whose
5313
5348
  absence blocks a save, shown upper-right of the editor toolbar. */
5314
5349
  .set-tpl-required {
5315
5350
  display: flex;
@@ -5373,10 +5408,7 @@ button.set-embed-copy svg {
5373
5408
  box-shadow: 0 1px 4px rgba(15, 18, 25, 0.1);
5374
5409
  transition: max-width 0.16s ease;
5375
5410
  }
5376
- /* "Start from example" dropdown menu — a compact, secondary action. */
5377
- .set-menu {
5378
- position: relative;
5379
- }
5411
+ /* "Start from example": the trigger of a shared menu (openMenu). */
5380
5412
  .set-menu-btn {
5381
5413
  display: inline-flex;
5382
5414
  align-items: center;
@@ -5390,45 +5422,6 @@ button.set-embed-copy svg {
5390
5422
  transform: translateY(-1px) rotate(45deg);
5391
5423
  opacity: 0.6;
5392
5424
  }
5393
- .set-menu-list {
5394
- position: absolute;
5395
- left: 0;
5396
- top: calc(100% + 6px);
5397
- z-index: var(--z-menu);
5398
- width: 280px;
5399
- max-width: min(280px, 78vw);
5400
- padding: 5px;
5401
- background: var(--card);
5402
- border: 1px solid var(--line);
5403
- border-radius: 10px;
5404
- box-shadow: 0 10px 28px rgba(15, 18, 25, 0.16);
5405
- }
5406
- .set-menu-list button {
5407
- display: block;
5408
- width: 100%;
5409
- text-align: left;
5410
- border: 0;
5411
- background: transparent;
5412
- border-radius: 7px;
5413
- padding: 8px 10px;
5414
- cursor: pointer;
5415
- }
5416
- .set-menu-list button:hover {
5417
- background: var(--sunken);
5418
- }
5419
- .set-menu-name {
5420
- display: block;
5421
- font-size: 13px;
5422
- font-weight: 600;
5423
- color: var(--fg);
5424
- }
5425
- .set-menu-desc {
5426
- display: block;
5427
- margin-top: 1px;
5428
- font-size: 11.5px;
5429
- color: var(--muted);
5430
- line-height: 1.35;
5431
- }
5432
5425
  /* Variables card heading. */
5433
5426
  .set-tpl-vartitle {
5434
5427
  margin: 0 0 2px;
@@ -5442,7 +5435,7 @@ button.set-embed-copy svg {
5442
5435
  .set-tpl-varcard .set-tpl-vars-body {
5443
5436
  padding: 0;
5444
5437
  }
5445
- /* The "this is a mock" note under the editor. */
5438
+ /* The note at the foot of a card: what the section is for, or where it is set. */
5446
5439
  .set-note {
5447
5440
  display: flex;
5448
5441
  align-items: flex-start;
@@ -5707,7 +5700,7 @@ button.set-embed-copy svg {
5707
5700
  icon rail — and collapses to edge-to-edge once the sidebar becomes the off-canvas
5708
5701
  mobile drawer. The post editor keeps its own autosave + conflict model, so it does
5709
5702
  NOT use this bar. Its Save is a Primary button — now brand-blue --accent app-wide
5710
- (DESIGN §1), so the bar needs no primary override of its own. --- */
5703
+ (DESIGN §7), so the bar needs no primary override of its own. --- */
5711
5704
  .savebar {
5712
5705
  position: fixed;
5713
5706
  /* Clears the full 240px sidebar; narrowed to the rail / drawer in the rules below. */