@kurtbruns/kestrel 1.6.0 → 1.8.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;
@@ -899,6 +897,37 @@ body.nav-resizing .sidebar {
899
897
  .auth-wall button {
900
898
  margin-top: 12px;
901
899
  }
900
+ /* The refused-configuration wall says more than the sign-in wall, so it reads as text: left
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
+ max-width: 560px;
906
+ text-align: left;
907
+ }
908
+ /* Text for a screen reader alone, such as a link's "(opens in a new tab)" beside a ↗ that is
909
+ hidden from it. */
910
+ .visually-hidden {
911
+ position: absolute;
912
+ width: 1px;
913
+ height: 1px;
914
+ margin: -1px;
915
+ padding: 0;
916
+ overflow: hidden;
917
+ clip-path: inset(50%);
918
+ white-space: nowrap;
919
+ border: 0;
920
+ }
921
+ .config-wall h2:focus,
922
+ .access-wall h2:focus {
923
+ outline: none;
924
+ }
925
+ .config-wall-problem {
926
+ padding: 10px 12px;
927
+ border-radius: 8px;
928
+ background: var(--bg);
929
+ overflow-wrap: anywhere;
930
+ }
902
931
 
903
932
  /* The routed view. Capped for readability and centered in the space beside the
904
933
  sidebar; the editor gets a slightly narrower canvas to match its old width. */
@@ -921,8 +950,8 @@ body.tool-mode .content {
921
950
  }
922
951
 
923
952
  /* --- Collapsed icon rail. Driven by data-nav="rail" on <html> (set from the viewport
924
- width and the persisted collapse preference — see app.js and the <head> guard in
925
- 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
926
955
  default across the tablet range and small desktops; the nav keeps its one-click
927
956
  reach, labels move to hover/focus tooltips, and the secondary chrome (brand text,
928
957
  account, tool footer) folds down to its marks. The whole block is gated at ≥540 so a
@@ -1152,7 +1181,7 @@ button:hover {
1152
1181
  button:active {
1153
1182
  transform: translateY(0.5px);
1154
1183
  }
1155
- /* --- Buttons: five roles (DESIGN §1), chosen by importance and consequence, one
1184
+ /* --- Buttons: five roles (DESIGN §7), chosen by importance and consequence, one
1156
1185
  class each. The bare `button` above is the neutral default (Ghost's card-filled
1157
1186
  end); the classes below step up to Primary / Secondary or down to the quiet Ghost,
1158
1187
  plus Danger and the square Icon. Exactly one solid brand-blue action — Primary — is
@@ -1161,7 +1190,7 @@ button:active {
1161
1190
 
1162
1191
  /* One box height across the label roles, so a row that mixes them (the editor's
1163
1192
  Save draft / Send test email / Schedule, a modal's Cancel + confirm) aligns:
1164
- 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).
1165
1194
  A smaller-font role (Secondary, Ghost) grows to this floor with its label centered
1166
1195
  rather than sitting a few pixels short of the Primary beside it. The bare `button`
1167
1196
  default already meets it; the square Icon role and the segmented toggle Control keep
@@ -1490,16 +1519,10 @@ td a:hover {
1490
1519
  color: var(--status-sending-fg);
1491
1520
  border-color: var(--status-sending-line);
1492
1521
  }
1493
- .badge.canceled,
1494
- .badge.failed {
1495
- background: var(--danger-bg);
1496
- color: var(--danger-fg);
1497
- border-color: var(--danger-line);
1498
- }
1499
1522
  /* Subscriber consent states borrow a hue that matches the state's meaning: confirmed →
1500
1523
  the sent green, unsubscribed → the draft grey, suppressed → danger red (below). Pending
1501
1524
  awaits double-opt-in — a waiting/attention state — so it takes the amber `warn` scale,
1502
- 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
1503
1526
  must never read as a send in flight. (The two coincided only while `--status-sending` was
1504
1527
  amber; when it went blue for the dispatch treatment, this pill must stay amber.) */
1505
1528
  .badge.confirmed {
@@ -1721,7 +1744,7 @@ body.web-mode .content {
1721
1744
  margin: 0;
1722
1745
  padding: 0;
1723
1746
  }
1724
- /* 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
1725
1748
  the editor's, above the frame, and the frame is as tall as the post, so the window scrolls
1726
1749
  and the strip goes up with the page like the test pages' strip. It wears the brand accent
1727
1750
  like the reader surface's dev pill, its tag as the pill's inverted chip, and its content
@@ -1787,8 +1810,8 @@ body.web-mode .content {
1787
1810
  margin: 48px auto;
1788
1811
  padding: 0 16px;
1789
1812
  }
1790
- /* Sent record view (#148): a read-only delivery record. The outcome tiles reuse the
1791
- 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
1792
1815
  a feedback color, still labels the send's lifecycle. */
1793
1816
  .rec-card {
1794
1817
  display: flex;
@@ -1888,7 +1911,7 @@ body.web-mode .content {
1888
1911
  }
1889
1912
  }
1890
1913
 
1891
- /* ---- per-recipient record (#164): the rows behind the outcome tiles ---- */
1914
+ /* ---- per-recipient record: the rows behind the outcome tiles ---- */
1892
1915
  .rec-people {
1893
1916
  margin-top: 26px;
1894
1917
  }
@@ -1903,7 +1926,7 @@ body.web-mode .content {
1903
1926
  margin: 0;
1904
1927
  font-size: 15px;
1905
1928
  }
1906
- /* 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):
1907
1930
  the same segmented, aria-pressed pill group as the Template preview-width toggle, its
1908
1931
  active segment in the Secondary palette. It switches what you're looking at; it never
1909
1932
  commits an action. */
@@ -1955,8 +1978,8 @@ body.web-mode .content {
1955
1978
  font-size: 13px;
1956
1979
  }
1957
1980
 
1958
- /* ---- live in-flight watch (#154): the `sending` state of the record view ---- */
1959
- /* 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
1960
1983
  action slate-blue — carried by the phase pill, the dispatch bar, and the swatch. */
1961
1984
  .rec-sw.sw-sending {
1962
1985
  background: var(--status-sending-fg);
@@ -2122,7 +2145,7 @@ body.web-mode .content {
2122
2145
  }
2123
2146
  }
2124
2147
 
2125
- /* ---- active-send row (Sent page "In progress") + dashboard widget (#154) ---- */
2148
+ /* ---- active-send row (Sent page "In progress") + dashboard widget ---- */
2126
2149
  /* An in-flight send as a card with a live mini dispatch bar; blue, so it reads as
2127
2150
  "sending" — distinct from the scheduled violet cards and the neutral sent rows. */
2128
2151
  .card.active-card {
@@ -2166,6 +2189,27 @@ body.web-mode .content {
2166
2189
  font-size: 12.5px;
2167
2190
  color: var(--warn-fg);
2168
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
+ }
2169
2213
 
2170
2214
  /* Composer: GitHub-style Write/Preview tabs + a formatting toolbar */
2171
2215
  .composer {
@@ -2299,7 +2343,7 @@ body.web-mode .content {
2299
2343
  .composer-body {
2300
2344
  position: relative;
2301
2345
  }
2302
- /* Post editor syntax-highlight overlay (issue #138): a transparent <textarea> layered over
2346
+ /* Post editor syntax-highlight overlay: a transparent <textarea> layered over
2303
2347
  a highlighted <pre> that shows the colored Markdown beneath the caret. The two share
2304
2348
  identical box + font metrics so the caret sits exactly on the colored text; the textarea
2305
2349
  is the only scroller and the <pre> is synced to it in script. Unlike the template editor
@@ -2374,7 +2418,7 @@ body.web-mode .content {
2374
2418
  background: #fff;
2375
2419
  display: block;
2376
2420
  }
2377
- /* 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
2378
2422
  copyable (the fields are readonly, not disabled, since a disabled textarea cannot even be
2379
2423
  selected), on the recessed surface Settings uses for a read-only card (`.set-card.sunken`);
2380
2424
  the caret is hidden because the field is not for typing. The way out is the composer's
@@ -2434,7 +2478,7 @@ body.web-mode .content {
2434
2478
  }
2435
2479
  /* The locked composer's foot: where a draft's foot offers images, a scheduled post's says
2436
2480
  the way out, "Cancel the schedule to edit", with the read-only lock Settings uses. It is
2437
- 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
2438
2482
  lift. `.nudge` pulses it once, in the read-only note's tint, when an edit is attempted,
2439
2483
  so the refusal is seen where the keystroke landed; reduced-motion holds the tint instead. */
2440
2484
  .composer-foot-lock {
@@ -2473,7 +2517,14 @@ body.web-mode .content {
2473
2517
  .actions-bar > .row:only-child {
2474
2518
  margin-left: auto;
2475
2519
  }
2476
- /* 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:
2477
2528
  Unsaved (amber --warn dot) · Saving… (pulsing --accent dot) · Saved (green --ok
2478
2529
  dot). The dot is a reinforcement, never the whole signal — the text label carries
2479
2530
  each state too, so the cue survives a reader who can't tell the dot's color. The
@@ -2482,6 +2533,7 @@ body.web-mode .content {
2482
2533
  .save-status {
2483
2534
  display: inline-flex;
2484
2535
  align-items: center;
2536
+ min-width: 0;
2485
2537
  gap: 7px;
2486
2538
  font-size: 13px;
2487
2539
  color: var(--muted);
@@ -2492,9 +2544,13 @@ body.web-mode .content {
2492
2544
  .save-status.is-dirty {
2493
2545
  color: var(--fg);
2494
2546
  }
2547
+ .save-status.is-error {
2548
+ color: var(--danger-fg);
2549
+ }
2495
2550
  .save-status.is-dirty::before,
2496
2551
  .save-status.is-saving::before,
2497
- .save-status.is-saved::before {
2552
+ .save-status.is-saved::before,
2553
+ .save-status.is-error::before {
2498
2554
  content: "";
2499
2555
  width: 8px;
2500
2556
  height: 8px;
@@ -2511,6 +2567,9 @@ body.web-mode .content {
2511
2567
  .save-status.is-saved::before {
2512
2568
  background: var(--ok-fg);
2513
2569
  }
2570
+ .save-status.is-error::before {
2571
+ background: var(--danger-fg);
2572
+ }
2514
2573
  @keyframes save-pulse {
2515
2574
  0%,
2516
2575
  100% {
@@ -2609,6 +2668,18 @@ body.web-mode .content {
2609
2668
  margin: 0 0 14px;
2610
2669
  line-height: 1.5;
2611
2670
  }
2671
+ /* A dialog's not-connected line (DESIGN §4) takes the health block's amber warning, so it
2672
+ reads as the reason the dialog's action is off rather than as one more hint. */
2673
+ .modal .hint.not-connected {
2674
+ color: var(--warn-fg);
2675
+ background: var(--warn-bg);
2676
+ border: 1px solid var(--warn-line);
2677
+ border-radius: 8px;
2678
+ padding: 10px 12px;
2679
+ }
2680
+ .modal .hint.not-connected a {
2681
+ color: inherit;
2682
+ }
2612
2683
  .modal .actions {
2613
2684
  display: flex;
2614
2685
  justify-content: flex-end;
@@ -2626,10 +2697,16 @@ body.web-mode .content {
2626
2697
  padding-top: 14px;
2627
2698
  border-top: 1px solid var(--line);
2628
2699
  }
2629
- /* The alternate-path row can carry a plain note beside its link. */
2700
+ /* The alternate-path row can carry a plain note beside its link; the note wraps and the
2701
+ link stays whole on its line. */
2630
2702
  .modal .altrow .altrow-note {
2631
2703
  font-size: 12.5px;
2632
2704
  color: var(--muted);
2705
+ min-width: 0;
2706
+ }
2707
+ .modal .altrow .linkbtn {
2708
+ flex: none;
2709
+ white-space: nowrap;
2633
2710
  }
2634
2711
  /* The back link at the top of the Send-now view needs no divider — it returns from,
2635
2712
  rather than demotes below, the Schedule view. */
@@ -2639,7 +2716,7 @@ body.web-mode .content {
2639
2716
  border-top: 0;
2640
2717
  justify-content: flex-start;
2641
2718
  }
2642
- /* Link — a text-link-styled action (DESIGN §1), not one of the button roles. Reserved
2719
+ /* Link — a text-link-styled action (DESIGN §7), not one of the button roles. Reserved
2643
2720
  for a deliberately subordinate choice, like the in-modal Send now that sits below
2644
2721
  the Schedule Primary: it must read as clearly demoted, so it is a link, not a Ghost
2645
2722
  button that would compete with Cancel. Still a real <button>, so it's keyboard- and
@@ -2660,12 +2737,12 @@ button.linkbtn:hover {
2660
2737
 
2661
2738
  .countdown {
2662
2739
  font-variant-numeric: tabular-nums;
2663
- /* The scheduled violet (DESIGN §3), so the fire-time countdown on the scheduled
2740
+ /* The scheduled violet (DESIGN §8), so the fire-time countdown on the scheduled
2664
2741
  cards (Sent page + dashboard) carries the same "scheduled" cue as the pill. */
2665
2742
  color: var(--status-scheduled-fg);
2666
2743
  }
2667
2744
  /* Past the server's missed tolerance the countdown says how late the send is, in the
2668
- danger tone: a missed fire time needs a look (DESIGN §9). */
2745
+ danger tone: a missed fire time needs a look (DESIGN §4). */
2669
2746
  .countdown.countdown-missed {
2670
2747
  color: var(--danger-fg);
2671
2748
  }
@@ -2789,6 +2866,23 @@ td.act {
2789
2866
  .menu-item.danger-item {
2790
2867
  color: var(--danger-fg);
2791
2868
  }
2869
+ /* A menu whose items say what each pick does: a name over a muted line. */
2870
+ .menu-described {
2871
+ width: 280px;
2872
+ max-width: min(280px, 78vw);
2873
+ }
2874
+ .menu-item-name {
2875
+ display: block;
2876
+ font-size: 13px;
2877
+ font-weight: 600;
2878
+ }
2879
+ .menu-item-desc {
2880
+ display: block;
2881
+ margin-top: 1px;
2882
+ font-size: 11.5px;
2883
+ color: var(--muted);
2884
+ line-height: 1.35;
2885
+ }
2792
2886
 
2793
2887
  /* editor field hints + scheduled banner */
2794
2888
  .field-hint {
@@ -2796,7 +2890,7 @@ td.act {
2796
2890
  color: var(--muted);
2797
2891
  margin-top: 4px;
2798
2892
  }
2799
- /* --- Field validation (DESIGN §2, home ④): an error about one field lives on that
2893
+ /* --- Field validation (DESIGN §5): an error about one field lives on that
2800
2894
  field — the input goes to an invalid state and the reason sits directly beneath it,
2801
2895
  tied by aria-describedby. This replaces the old disable-the-button + `title` tooltip,
2802
2896
  which never reached keyboard, touch, or screen-reader users. --- */
@@ -2816,6 +2910,11 @@ textarea.is-invalid {
2816
2910
  .field-error .field-error-ico {
2817
2911
  font-weight: 700;
2818
2912
  }
2913
+ /* A dialog's own error, when no one field is at fault: just above its actions, where the
2914
+ reader is looking rather than in a toast at the foot of the screen (DESIGN §5). */
2915
+ .modal .dialog-error {
2916
+ margin-top: 12px;
2917
+ }
2819
2918
  /* Label + info icon grouped at the top of the slug field. */
2820
2919
  .label-row {
2821
2920
  display: flex;
@@ -2924,11 +3023,10 @@ textarea.is-invalid {
2924
3023
  #f-slug.slug-auto {
2925
3024
  color: var(--muted);
2926
3025
  }
2927
- /* --- Editor-state banner (DESIGN §2, home ③): one persistent, in-flow notice at the
2928
- top of the editor, in three variants. `scheduled` marks a frozen/soft-locked post
2929
- (SPEC §6); `conflict` warns the draft changed elsewhere (another tab, or Claude) —
2930
- its two actions are equal-weight Ghost, since both choices lose something; `info` is
2931
- a neutral notice. It replaces the old `.sched-banner` + `.fresh-banner` pair. --- */
3026
+ /* --- Editor-state banner (DESIGN §5): a standing, in-flow line at the top of the
3027
+ editor, in two variants. `scheduled` marks a frozen, soft-locked post (SPEC §6);
3028
+ `conflict` warns the draft changed elsewhere (another tab, or Claude), and its two
3029
+ actions are equal-weight Ghost, since both choices lose something. --- */
2932
3030
  .banner {
2933
3031
  display: flex;
2934
3032
  align-items: center;
@@ -2957,59 +3055,21 @@ textarea.is-invalid {
2957
3055
  color: var(--warn-fg);
2958
3056
  border-color: var(--warn-line);
2959
3057
  }
2960
- .banner-info {
2961
- background: var(--banner-bg);
2962
- color: var(--fg);
2963
- border-color: var(--banner-line);
2964
- }
2965
3058
 
2966
- /* --- Notice (DESIGN §2, home ⑥): an event the reader could not otherwise know happened,
2967
- shown once and cleared by the reader, asking nothing. It wears the banner's `info` tone
2968
- (the same tokens as .banner-info, so the tint has one definition) but is its own class:
2969
- its layout is prose plus one control, not a status line with an actions row, so it never
2970
- wraps the dismiss beneath the text the way the banner wraps its buttons. Padding is a
2971
- full 1rem because it is a sentence or two, not a status line. Removed on dismiss with
2972
- no animation. Both themes come through the existing tokens. --- */
2973
- .notice {
3059
+ /* Look changed since your last test (SPEC §8): the server's condition, a line in the scheduled
3060
+ banner that says to send a new test, and a mark on the dashboard's scheduled card. */
3061
+ .sched-remade {
2974
3062
  display: flex;
2975
- align-items: flex-start;
2976
- gap: 12px;
2977
- border: 1px solid var(--banner-line);
2978
- border-radius: 8px;
2979
- background: var(--banner-bg);
2980
- color: var(--fg);
2981
- padding: 1rem;
2982
- margin-bottom: 14px;
2983
- font-size: 14px;
2984
- line-height: 1.45;
2985
- }
2986
- .notice-text {
2987
- flex: 1 1 auto;
2988
- min-width: 0;
2989
- }
2990
- /* Links are allowed (buttons are not); underlined so they stay findable on the tint. */
2991
- .notice a {
2992
- color: var(--accent);
2993
- font-weight: 550;
2994
- text-decoration: underline;
2995
- text-underline-offset: 2px;
2996
- }
2997
- /* The dismiss is the Icon role (button.icon) carrying the app's `x` glyph: a square hit
2998
- target pinned to the right, pulled 4px into the padding on three sides so it centers on
2999
- the first line of text without making a one-line notice taller than its text. */
3000
- button.notice-dismiss {
3001
- flex: 0 0 auto;
3002
- display: inline-flex;
3063
+ flex-wrap: wrap;
3003
3064
  align-items: center;
3004
- justify-content: center;
3005
- width: 28px;
3006
- height: 28px;
3007
- padding: 0;
3008
- margin: -4px -4px -4px 0;
3065
+ gap: 4px 10px;
3066
+ margin-top: 6px;
3067
+ font-size: 13px;
3009
3068
  }
3010
- button.notice-dismiss svg {
3011
- width: 14px;
3012
- height: 14px;
3069
+ .sched-remade-mark {
3070
+ margin-top: 4px;
3071
+ font-size: 12.5px;
3072
+ color: var(--status-scheduled-fg);
3013
3073
  }
3014
3074
 
3015
3075
  .grid2 {
@@ -3891,7 +3951,7 @@ button.api-filter-clear svg {
3891
3951
  text-decoration: underline;
3892
3952
  }
3893
3953
 
3894
- /* Publication card: the live-site + archive URLs with copy buttons. */
3954
+ /* Publication card: the home page's address with Copy, and the way out to it. */
3895
3955
  .pub-card {
3896
3956
  display: flex;
3897
3957
  flex-direction: column;
@@ -3902,10 +3962,6 @@ button.api-filter-clear svg {
3902
3962
  align-items: center;
3903
3963
  gap: 12px;
3904
3964
  }
3905
- .pub-key {
3906
- flex: 0 0 84px;
3907
- font-size: 12px;
3908
- }
3909
3965
  .pub-val {
3910
3966
  flex: 1 1 auto;
3911
3967
  min-width: 0;
@@ -4012,10 +4068,9 @@ button.api-filter-clear svg {
4012
4068
  deploy / Copy-out), editable surfaces sit on cards and read-only
4013
4069
  facts on a recessed "sunken" card. Kestrel's own accents use the
4014
4070
  fixed --accent (a slate-blue), independent of the reader surface.
4015
- A sticky save bar collects the unsaved, persisted changes
4016
- (identity + test recipients); the logo and the email-template
4017
- mock are handled on their own and don't feed it. Theme follows
4018
- the OS via the tokens at the top of this file.
4071
+ A sticky save bar collects every editable section's unsaved
4072
+ changes; the logo saves on its own and doesn't feed it. Theme
4073
+ follows the OS via the tokens at the top of this file.
4019
4074
  ============================================================ */
4020
4075
  .settings {
4021
4076
  max-width: 940px;
@@ -4041,7 +4096,7 @@ button.api-filter-clear svg {
4041
4096
  margin-bottom: 34px;
4042
4097
  }
4043
4098
 
4044
- /* --- copying text out of the app (DESIGN §1, ui/copy.ts) ---
4099
+ /* --- copying text out of the app (DESIGN §7, ui/copy.ts) ---
4045
4100
  A labelled block (a code block, or the API reference's query and content-type tables):
4046
4101
  its label line, then the block. The Copy is a control outside the five roles: the word
4047
4102
  and its glyph, no fill or border, quiet on the line it shares; the glyph turns to a
@@ -4103,7 +4158,7 @@ button.copied svg {
4103
4158
  flex: 0 0 auto;
4104
4159
  }
4105
4160
 
4106
- /* --- groups: the sections kept together by whom they serve (DESIGN §5). A group has no
4161
+ /* --- groups: the sections kept together by whom they serve (DESIGN §3). A group has no
4107
4162
  heading, since each section's title already says what it is; a wider gap than the one
4108
4163
  between sections is all that separates one group from the next. --- */
4109
4164
  .set-group + .set-group {
@@ -4154,7 +4209,7 @@ button.copied svg {
4154
4209
  background: var(--accent-soft);
4155
4210
  }
4156
4211
  /* In use by scheduled posts (SPEC §9): the scheduled violet, a lifecycle fact and not a
4157
- warning, the same reading as the scheduled badge (DESIGN §3). Adds no token. */
4212
+ warning, the same reading as the scheduled badge (DESIGN §8). Adds no token. */
4158
4213
  .set-chip.inuse {
4159
4214
  color: var(--status-scheduled-fg);
4160
4215
  border-color: var(--status-scheduled-line);
@@ -4167,22 +4222,12 @@ button.copied svg {
4167
4222
  background: currentColor;
4168
4223
  opacity: 0.85;
4169
4224
  }
4225
+ /* Set at deploy: a fact about where the value lives, not a warning, so it stays neutral
4226
+ rather than borrowing the amber `warn` scale (DESIGN §8). */
4170
4227
  .set-chip.readonly {
4171
- color: var(--warn-fg);
4172
- border-color: color-mix(in srgb, var(--warn-fg) 30%, var(--line-soft));
4173
- background: color-mix(in srgb, var(--warn-fg) 9%, transparent);
4174
- }
4175
- /* A dashed "Mock" tag beside the Email-template chip — says the section is a
4176
- preview of a feature not wired to the backend yet. */
4177
- .set-tag-mock {
4178
- font-size: 10px;
4179
- font-weight: 650;
4180
- letter-spacing: 0.06em;
4181
- text-transform: uppercase;
4182
- padding: 3px 7px;
4183
- border-radius: 6px;
4184
- border: 1px dashed var(--line-soft);
4185
4228
  color: var(--muted);
4229
+ border-color: var(--line);
4230
+ background: var(--chip);
4186
4231
  }
4187
4232
 
4188
4233
  /* --- cards: editable surfaces raised, read-only facts recessed --- */
@@ -4371,7 +4416,7 @@ button.set-field-remove {
4371
4416
  align-items: flex-start;
4372
4417
  padding: 11px 16px;
4373
4418
  border-bottom: 1px solid var(--line-soft);
4374
- background: color-mix(in srgb, var(--warn-fg) 6%, transparent);
4419
+ background: var(--chip);
4375
4420
  font-size: 12.5px;
4376
4421
  color: var(--muted);
4377
4422
  line-height: 1.5;
@@ -4381,7 +4426,7 @@ button.set-field-remove {
4381
4426
  height: 15px;
4382
4427
  flex: 0 0 auto;
4383
4428
  margin-top: 1px;
4384
- color: var(--warn-fg);
4429
+ color: var(--muted);
4385
4430
  }
4386
4431
  .set-kv {
4387
4432
  display: grid;
@@ -4718,14 +4763,21 @@ button.set-field-remove {
4718
4763
  }
4719
4764
  .set-recip-add {
4720
4765
  display: flex;
4766
+ flex-wrap: wrap;
4721
4767
  gap: 8px;
4722
4768
  max-width: 420px;
4723
4769
  }
4724
4770
  .set-recip-add input {
4725
4771
  flex: 1;
4726
4772
  }
4773
+ /* A refused address says why on its own line under the field and its button (DESIGN §5). */
4774
+ .set-recip-add .field-error {
4775
+ flex-basis: 100%;
4776
+ order: 1;
4777
+ margin-top: 0;
4778
+ }
4727
4779
 
4728
- /* --- ways to subscribe (DESIGN §5): one card, the two ways as bands in reading order,
4780
+ /* --- ways to subscribe (DESIGN §3): one card, the two ways as bands in reading order,
4729
4781
  the page then the form, split by a full-width rule like the other cards' bands. Each
4730
4782
  band has the same head (a glyph, a title, one line) so they read as two options. The
4731
4783
  card folds by the section's own width, since the sidebar decides how wide Settings is. --- */
@@ -4916,7 +4968,7 @@ button.set-embed-copy svg {
4916
4968
  color: var(--muted);
4917
4969
  }
4918
4970
 
4919
- /* --- email template (mock): a standalone sample-email frame, then a card holding
4971
+ /* --- email template: a standalone sample-email frame, then a card holding
4920
4972
  the HTML template editor and a collapsible variable reference. The example
4921
4973
  templates are authored with a <style> block; the preview renders them in an
4922
4974
  isolated iframe, so a template's own styles apply as a mail client would show
@@ -4951,7 +5003,7 @@ button.set-embed-copy svg {
4951
5003
  font-size: 12px;
4952
5004
  color: var(--muted);
4953
5005
  }
4954
- /* Syntax-highlight overlay editor (issue #123): a transparent <textarea> over a
5006
+ /* Syntax-highlight overlay editor: a transparent <textarea> over a
4955
5007
  highlighted <pre>, plus a line-number gutter. All three share identical font
4956
5008
  metrics so the caret sits exactly over the colored text; the textarea is the only
4957
5009
  scroller and the gutter + overlay are synced to it in script. */
@@ -5027,7 +5079,7 @@ button.set-embed-copy svg {
5027
5079
  .set-tpl-editor-wrap.show-lines .set-tpl-editor {
5028
5080
  left: 47px;
5029
5081
  }
5030
- /* Syntax colors (issue #123). */
5082
+ /* Syntax colors. */
5031
5083
  .cx-tag {
5032
5084
  color: var(--syntax-tag);
5033
5085
  }
@@ -5057,7 +5109,7 @@ button.set-embed-copy svg {
5057
5109
  padding: 0 2px;
5058
5110
  font-weight: 600;
5059
5111
  }
5060
- /* Markdown token classes (issue #138) — the minimal scheme: the MARK carries the color, the
5112
+ /* Markdown token classes — the minimal scheme: the MARK carries the color, the
5061
5113
  CONTENT keeps --syntax-fg and carries only weight/slant. So a bold word reads as weight, not
5062
5114
  as a hue that competes with a heading. Heading text is bold with a crimson # mark; emphasis
5063
5115
  asterisks are pink around plain weighted/slanted text; block markers (>, bullets, the fence
@@ -5269,7 +5321,7 @@ button.set-embed-copy svg {
5269
5321
  background: var(--accent-soft);
5270
5322
  border-color: color-mix(in srgb, var(--accent) 30%, var(--line-soft));
5271
5323
  }
5272
- /* Required-token pills (issue #123 revisit): live green/red for the two tokens whose
5324
+ /* Required-token pills: live green/red for the two tokens whose
5273
5325
  absence blocks a save, shown upper-right of the editor toolbar. */
5274
5326
  .set-tpl-required {
5275
5327
  display: flex;
@@ -5333,10 +5385,7 @@ button.set-embed-copy svg {
5333
5385
  box-shadow: 0 1px 4px rgba(15, 18, 25, 0.1);
5334
5386
  transition: max-width 0.16s ease;
5335
5387
  }
5336
- /* "Start from example" dropdown menu — a compact, secondary action. */
5337
- .set-menu {
5338
- position: relative;
5339
- }
5388
+ /* "Start from example": the trigger of a shared menu (openMenu). */
5340
5389
  .set-menu-btn {
5341
5390
  display: inline-flex;
5342
5391
  align-items: center;
@@ -5350,45 +5399,6 @@ button.set-embed-copy svg {
5350
5399
  transform: translateY(-1px) rotate(45deg);
5351
5400
  opacity: 0.6;
5352
5401
  }
5353
- .set-menu-list {
5354
- position: absolute;
5355
- left: 0;
5356
- top: calc(100% + 6px);
5357
- z-index: var(--z-menu);
5358
- width: 280px;
5359
- max-width: min(280px, 78vw);
5360
- padding: 5px;
5361
- background: var(--card);
5362
- border: 1px solid var(--line);
5363
- border-radius: 10px;
5364
- box-shadow: 0 10px 28px rgba(15, 18, 25, 0.16);
5365
- }
5366
- .set-menu-list button {
5367
- display: block;
5368
- width: 100%;
5369
- text-align: left;
5370
- border: 0;
5371
- background: transparent;
5372
- border-radius: 7px;
5373
- padding: 8px 10px;
5374
- cursor: pointer;
5375
- }
5376
- .set-menu-list button:hover {
5377
- background: var(--sunken);
5378
- }
5379
- .set-menu-name {
5380
- display: block;
5381
- font-size: 13px;
5382
- font-weight: 600;
5383
- color: var(--fg);
5384
- }
5385
- .set-menu-desc {
5386
- display: block;
5387
- margin-top: 1px;
5388
- font-size: 11.5px;
5389
- color: var(--muted);
5390
- line-height: 1.35;
5391
- }
5392
5402
  /* Variables card heading. */
5393
5403
  .set-tpl-vartitle {
5394
5404
  margin: 0 0 2px;
@@ -5402,7 +5412,7 @@ button.set-embed-copy svg {
5402
5412
  .set-tpl-varcard .set-tpl-vars-body {
5403
5413
  padding: 0;
5404
5414
  }
5405
- /* The "this is a mock" note under the editor. */
5415
+ /* The note at the foot of a card: what the section is for, or where it is set. */
5406
5416
  .set-note {
5407
5417
  display: flex;
5408
5418
  align-items: flex-start;
@@ -5667,7 +5677,7 @@ button.set-embed-copy svg {
5667
5677
  icon rail — and collapses to edge-to-edge once the sidebar becomes the off-canvas
5668
5678
  mobile drawer. The post editor keeps its own autosave + conflict model, so it does
5669
5679
  NOT use this bar. Its Save is a Primary button — now brand-blue --accent app-wide
5670
- (DESIGN §1), so the bar needs no primary override of its own. --- */
5680
+ (DESIGN §7), so the bar needs no primary override of its own. --- */
5671
5681
  .savebar {
5672
5682
  position: fixed;
5673
5683
  /* Clears the full 240px sidebar; narrowed to the rail / drawer in the rules below. */