@arcforgelabs/flow 2026.10.42 → 2026.10.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -0
  2. package/npm-shrinkwrap.json +4 -3
  3. package/package.json +5 -3
  4. package/server/auth/people.js +40 -2
  5. package/server/auth/routes.js +11 -0
  6. package/server/auth/session.js +22 -3
  7. package/server/core/custom-fields.js +4 -3
  8. package/server/core/invoice-rules.js +8 -3
  9. package/server/core/job-match.js +6 -2
  10. package/server/core/sending.js +4 -3
  11. package/server/integrations/sentry/index.js +9 -1
  12. package/server/release-channel.js +5 -2
  13. package/server/ui/app-page.js +1 -1
  14. package/server/ui/app.js +46 -162
  15. package/server/ui/automations.js +9 -3
  16. package/server/ui/columns.js +268 -0
  17. package/server/ui/custom-fields.js +17 -16
  18. package/server/ui/flow.css +268 -55
  19. package/server/ui/invoice-notes.js +7 -5
  20. package/server/ui/invoice-rules.js +3 -3
  21. package/server/ui/invoicing-settings.js +92 -35
  22. package/server/ui/invoicing.js +206 -119
  23. package/server/ui/job-board.js +20 -8
  24. package/server/ui/job-change.js +36 -0
  25. package/server/ui/job-view.js +255 -60
  26. package/server/ui/jobs.js +197 -145
  27. package/server/ui/kit.js +278 -0
  28. package/server/ui/list-views.js +73 -0
  29. package/server/ui/monitoring.js +1 -0
  30. package/server/ui/person.js +29 -0
  31. package/server/ui/price-list.js +2 -3
  32. package/server/ui/price-reconcile.js +7 -8
  33. package/server/ui/reconcile.js +114 -61
  34. package/server/ui/routes.js +1 -1
  35. package/server/ui/saved-views.js +141 -0
  36. package/server/ui/search.js +8 -0
  37. package/server/ui/sidecar.js +72 -0
  38. package/server/ui/tip.js +99 -0
  39. package/server/ui/view-bar.js +176 -70
  40. package/server/ui/xero-import.js +7 -10
@@ -7,6 +7,10 @@
7
7
 
8
8
  :root {
9
9
  --fx-line: color-mix(in srgb, var(--t-text) 12%, transparent);
10
+ /* The side space of every page. Pages use the whole width with this slim
11
+ edge, as ClickUp and HubSpot do; the title, toolbar and content start at
12
+ one place on every page. */
13
+ --fx-gutter: clamp(16px, 2vw, 24px);
10
14
  --fx-line-soft: color-mix(in srgb, var(--t-text) 7%, transparent);
11
15
  --fx-hover: color-mix(in srgb, var(--t-text) 5%, transparent);
12
16
  --fx-focus: 0 0 0 3px color-mix(in srgb, var(--t-ring) 40%, transparent);
@@ -90,6 +94,9 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
90
94
  }
91
95
  .fx-input:hover, .fx-select:hover { border-color: var(--t-text); }
92
96
  .fx-input:focus, .fx-select:focus { outline: none; border-color: var(--t-text); box-shadow: var(--fx-focus); }
97
+ .fx-input[aria-invalid="true"], .fx-select[aria-invalid="true"] {
98
+ border-color: var(--t-danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--t-danger) 28%, transparent);
99
+ }
93
100
  .fx-select {
94
101
  width: auto; min-width: 112px; padding-right: 30px; appearance: none; cursor: pointer;
95
102
  background-image: linear-gradient(45deg, transparent 50%, var(--t-dim) 50%), linear-gradient(135deg, var(--t-dim) 50%, transparent 50%);
@@ -167,18 +174,33 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
167
174
  .fx-shell[data-folded] #user-button .cl-userButtonTrigger { width: auto; padding: 0; border-radius: 50%; background: transparent; box-shadow: none; }
168
175
  .fx-shell[data-folded] #user-button .cl-userButtonOuterIdentifier, .fx-shell[data-folded] #user-button .cl-userButtonBox::before { display: none; }
169
176
  }
170
- .fx-main { flex: 1; min-width: 0; max-width: 1180px; margin: 0 auto; padding: 32px clamp(16px, 4vw, 48px) 64px; display: grid; gap: 20px; align-content: start; }
177
+ .fx-main { flex: 1; min-width: 0; padding: 32px var(--fx-gutter) 64px; display: grid; gap: 20px; align-content: start; }
171
178
  .fx-main:focus { outline: none; box-shadow: none; }
172
179
 
173
- /* Page header: title and one line, then the actions, primary last. */
174
- .fx-ph { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 16px; }
175
- .fx-ph__text { display: grid; gap: 6px; max-width: 64ch; }
180
+ /* Page header: the title only (no line under it), then the actions,
181
+ primary last. One height on every page, with or without a button. */
182
+ .fx-ph { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px; min-height: 36px; }
183
+ .fx-ph__text { display: flex; align-items: center; gap: 10px; min-width: 0; }
176
184
  .fx-title {
177
185
  color: var(--t-strong);
178
- font: var(--t-display-weight) 28px/1.15 var(--t-display); letter-spacing: -0.015em;
186
+ font: var(--t-display-weight) 24px/1.15 var(--t-display); letter-spacing: -0.015em;
179
187
  }
188
+ .fx-ph__count { color: var(--t-dim); font-size: 14px; font-variant-numeric: tabular-nums; }
189
+ /* A record's facts beside its title (Reconcile: client · date · total). */
180
190
  .fx-ph__sub { color: var(--t-dim); }
181
- .fx-ph__actions { display: flex; gap: 8px; }
191
+ /* A docs link as a "?" icon beside a title (helpLink in app.js). */
192
+ .fx-help { display: inline-flex; vertical-align: middle; margin-left: 2px; padding: 4px; border-radius: var(--t-radius); color: var(--t-dim); }
193
+ .fx-help:hover, .fx-help:focus-visible { background: var(--fx-hover); color: var(--t-strong); }
194
+ .fx-help .fx-lucide { width: 16px; height: 16px; }
195
+ .fx-ph__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
196
+
197
+ /* The tip an icon-only control shows on hover and keyboard focus (tip.js). */
198
+ .fx-tip {
199
+ position: fixed; top: 0; left: 0; z-index: 1000; max-width: 260px; padding: 4px 8px;
200
+ border: 1px solid var(--fx-line); border-radius: var(--t-radius); background: var(--t-paper); color: var(--t-strong);
201
+ font: 500 12px/1.3 var(--t-body); pointer-events: none; white-space: nowrap;
202
+ box-shadow: var(--t-shadow), 0 4px 12px rgba(0, 0, 0, .12);
203
+ }
182
204
 
183
205
  /* ── Notices, pills, tags, toasts ──────────────────────────────────────── */
184
206
  .fx-notice {
@@ -287,10 +309,12 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
287
309
  .fx-chip { border: 1px solid var(--fx-line); border-radius: 999px; background: var(--t-paper); color: var(--t-text); }
288
310
  .fx-int-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
289
311
  .fx-int-bar .fx-search { margin: 0; }
312
+ .fx-int-bar .fx-row { flex: 1 1 auto; flex-wrap: nowrap; justify-content: flex-end; min-width: 0; }
313
+ .fx-int-bar .fx-row .fx-search { flex: 0 1 320px; width: auto; min-width: 0; }
314
+ .fx-int-bar .fx-row .fx-select { flex: none; }
290
315
  .fx-int-sections { display: grid; gap: 32px; }
291
316
  .fx-int-section-list { display: grid; gap: 16px; }
292
317
  .fx-int-heading { color: var(--t-strong); font: 600 19px/1.3 var(--t-body); }
293
- .fx-int-heading-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
294
318
  .fx-int-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
295
319
  .fx-int-card {
296
320
  display: grid; grid-template-rows: auto 1fr auto; gap: 14px; padding: 18px;
@@ -298,6 +322,7 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
298
322
  color: var(--t-text); text-decoration: none;
299
323
  transition: transform 140ms var(--fx-ease), box-shadow 140ms var(--fx-ease), border-color 140ms var(--fx-ease);
300
324
  }
325
+ .fx-int-card--bare { grid-template-rows: auto auto; }
301
326
  .fx-int-card:hover, .fx-int-card:focus-visible { transform: translateY(-2px); border-color: var(--t-brd-hi, var(--t-control)); box-shadow: 0 8px 24px rgba(0, 0, 0, .1); }
302
327
  .fx-int-card__head { display: flex; align-items: center; gap: 12px; }
303
328
  .fx-int-card__head h3 { flex: 1; color: var(--t-strong); font: 650 16px/1.25 var(--t-body); }
@@ -312,6 +337,26 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
312
337
  display: grid; place-items: center; flex: none; width: 44px; height: 44px; border-radius: 10px;
313
338
  background: #fff; box-shadow: inset 0 0 0 1px var(--fx-line);
314
339
  }
340
+ .fx-logo--icon { background: var(--t-accent-bg); color: var(--t-accent); box-shadow: none; }
341
+ .fx-tiles { display: grid; gap: 12px; }
342
+ .fx-tiles + .fx-tiles { margin-top: 16px; }
343
+ .fx-tiles__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
344
+ .fx-tiles__head h2 { color: var(--t-strong); font: 600 15px/1.3 var(--t-body); }
345
+ .fx-tiles__more { display: inline-flex; align-items: center; gap: 4px; color: var(--t-dim); font-size: 13.5px; text-decoration: none; }
346
+ .fx-tiles__more:hover, .fx-tiles__more:focus-visible { color: var(--t-strong); }
347
+ .fx-tiles__more .fx-lucide { width: 15px; height: 15px; }
348
+ .fx-tiles__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 12px; }
349
+ .fx-tile {
350
+ display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 12px 14px 12px 12px;
351
+ border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); color: var(--t-text); text-decoration: none;
352
+ transition: border-color 140ms var(--fx-ease), background-color 140ms var(--fx-ease);
353
+ }
354
+ .fx-tile:hover, .fx-tile:focus-visible { border-color: var(--t-control); background: var(--fx-hover); }
355
+ .fx-tile .fx-logo { width: 48px; height: 48px; border-radius: var(--t-radius); }
356
+ .fx-tile .fx-logo--icon { background: var(--fx-hover); color: var(--t-strong); box-shadow: inset 0 0 0 1px var(--fx-line); }
357
+ .fx-tile h3 { overflow: hidden; color: var(--t-strong); font: 600 14.5px/1.35 var(--t-body); text-overflow: ellipsis; white-space: nowrap; }
358
+ .fx-tile__meta { color: var(--t-dim); font-size: 12.5px; white-space: nowrap; }
359
+ .fx-logo--icon .fx-lucide { width: 22px; height: 22px; }
315
360
  .fx-logo img { width: 28px; height: 28px; object-fit: contain; }
316
361
  .fx-logo--lg { width: 52px; height: 52px; }
317
362
  .fx-logo--lg img { width: 34px; height: 34px; }
@@ -388,6 +433,28 @@ a.fx-strong:hover { text-decoration: underline; }
388
433
  .fx-tabs__count { padding: 0 7px; border-radius: 999px; background: var(--fx-hover); color: var(--t-dim); font-size: 12px; line-height: 20px; }
389
434
  .fx-tabs button[data-tone="danger"] .fx-tabs__count { background: var(--t-danger-bg); color: var(--t-danger); }
390
435
 
436
+ /* Saved views (saved-views.js): the row of named view tabs over a list, as
437
+ ClickUp's. The chosen tab is underlined; its ⋯ shows on hover and when
438
+ chosen. Save and Revert sit at the right while the list differs. */
439
+ .fx-views { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; min-height: 32px; margin-bottom: -8px; }
440
+ .fx-views:empty { display: none; }
441
+ .fx-views__item { display: inline-flex; align-items: center; border-bottom: 2px solid transparent; }
442
+ .fx-views__item[data-on="true"] { border-bottom-color: var(--t-brand); }
443
+ .fx-views__tab { display: inline-flex; align-items: center; gap: 6px; max-width: 220px; padding: 6px 4px 6px 8px; border: 0; background: none; color: var(--t-dim); font: 600 13px var(--t-body); cursor: pointer; }
444
+ .fx-views__tab:hover, .fx-views__item[data-on="true"] .fx-views__tab { color: var(--t-strong); }
445
+ .fx-views__tab:focus-visible { outline: 2px solid var(--t-ring, var(--t-accent)); outline-offset: 1px; }
446
+ .fx-views__tab .fx-lucide { width: 14px; height: 14px; flex: none; }
447
+ .fx-views__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
448
+ .fx-views__dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--t-accent); }
449
+ .fx-views__item .fx-cfm__more { width: 22px; height: 22px; opacity: 0; }
450
+ .fx-views__item:hover .fx-cfm__more, .fx-views__item[data-on="true"] .fx-cfm__more, .fx-views__item .fx-cfm__more[aria-expanded="true"], .fx-views__item .fx-cfm__more:focus-visible { opacity: 1; }
451
+ .fx-views__item .fx-cfm__more .fx-lucide { width: 14px; height: 14px; }
452
+ .fx-views__add { display: inline-flex; align-items: center; gap: 4px; padding: 6px 8px; border: 0; background: none; color: var(--t-dim); font: 500 13px var(--t-body); cursor: pointer; }
453
+ .fx-views__add:hover { color: var(--t-strong); }
454
+ .fx-views__add .fx-lucide { width: 14px; height: 14px; }
455
+ .fx-views__end { display: inline-flex; gap: 6px; margin-left: auto; }
456
+ @media (hover: none) { .fx-views__item .fx-cfm__more { opacity: 1; } }
457
+
391
458
  /* Quiet alerts (server/ui/alerts.js): one slim line under the page header,
392
459
  only while something on the page needs this person. */
393
460
  .fx-alerts { display: flex; align-items: center; gap: 12px; margin: 0 0 12px; padding: 6px 6px 6px 14px; border: 1px solid var(--fx-line); border-left: 3px solid var(--t-warn); border-radius: var(--t-radius); background: var(--t-paper); font-size: 14px; }
@@ -473,7 +540,6 @@ a.fx-strong:hover { text-decoration: underline; }
473
540
  }
474
541
 
475
542
  /* ── Jobs board ─────────────────────────────────────────────────────────── */
476
- .fx-main--wide { max-width: none; }
477
543
  .fx-main > * { min-width: 0; }
478
544
  .fx-toolbar--plain { border-bottom: 0; align-items: center; }
479
545
  .fx-toolbar--plain .fx-search { margin-bottom: 0; }
@@ -524,14 +590,16 @@ a.fx-strong:hover { text-decoration: underline; }
524
590
  .fx-chipset--on .fx-chip__clear { border-color: var(--t-accent-brd, var(--t-accent)); background: var(--t-accent-bg); color: var(--t-accent); }
525
591
  /* Group by, Sort and Filters: rows of selects with a bin at the end, as
526
592
  ClickUp lays them out, in Flow's own look. */
527
- .fx-pop.fx-pop--rows { width: max-content; min-width: 360px; max-width: min(680px, calc(100vw - 16px)); max-height: min(520px, calc(100vh - 32px)); padding: 12px; }
593
+ .fx-pop.fx-pop--rows { width: max-content; min-width: 360px; max-width: min(760px, calc(100vw - 16px)); max-height: min(520px, calc(100vh - 32px)); padding: 12px; }
528
594
  .fx-rows__title { margin: 0 0 10px; color: var(--t-strong); font: 600 14px/1.3 var(--t-body); }
529
595
  .fx-rows { display: grid; gap: 8px; }
530
- .fx-rows__row { display: grid; grid-template-columns: 78px minmax(130px, 1fr) minmax(120px, auto) minmax(150px, 1.2fr) 32px; gap: 8px; align-items: center; }
596
+ .fx-rows__row { display: grid; grid-template-columns: 64px minmax(140px, 1fr) minmax(120px, auto) minmax(170px, 1.3fr) 32px; gap: 8px; align-items: start; }
531
597
  .fx-rows__row--sort { grid-template-columns: 78px minmax(150px, 1fr) minmax(140px, auto) 32px; }
532
598
  .fx-rows__row--group { grid-template-columns: minmax(170px, 1fr) minmax(130px, auto) 32px; }
533
599
  .fx-rows .fx-select, .fx-rows .fx-input { width: 100%; min-width: 0; min-height: 32px; font-size: 13px; }
534
- .fx-rows__lead { color: var(--t-dim); font: 500 13px/1.2 var(--t-body); }
600
+ /* Rows align at the top, so a value on two lines (a date range) keeps the
601
+ bin beside its first line. */
602
+ .fx-rows__lead { display: flex; align-items: center; min-height: 32px; color: var(--t-dim); font: 500 13px/1.2 var(--t-body); }
535
603
  .fx-rows__value { overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
536
604
  .fx-rows__gone { grid-column: span 3; color: var(--t-dim); font-size: 13px; font-style: italic; }
537
605
  .fx-rows__bin {
@@ -545,13 +613,51 @@ a.fx-strong:hover { text-decoration: underline; }
545
613
  .fx-rows__foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 12px; }
546
614
  .fx-rows__add { display: inline-flex; align-items: center; gap: 6px; }
547
615
  .fx-rows__add .fx-lucide { width: 14px; height: 14px; }
616
+ /* The filter panel's groups, as ClickUp's: each group a shaded box with its
617
+ own rows and "Add nested filter"; groups stack with And / Or at the left. */
618
+ .fx-fgroup { display: grid; gap: 8px; padding: 10px; border-radius: var(--t-radius-lg); background: var(--fx-hover); }
619
+ .fx-fgroups { display: grid; gap: 8px; }
620
+ .fx-fgroups__row { display: grid; grid-template-columns: 64px minmax(0, 1fr); gap: 8px; align-items: start; }
621
+ .fx-fgroups__row > .fx-rows__lead, .fx-fgroups__row > span { padding-top: 16px; }
622
+ .fx-fgroup__add { justify-self: start; padding: 2px 4px; border: 0; background: none; color: var(--t-dim); font: 500 12.5px/1.3 var(--t-body); cursor: pointer; }
623
+ .fx-fgroup__add:hover, .fx-fgroup__add:focus-visible { color: var(--t-strong); text-decoration: underline; outline: none; }
624
+ .fx-choice { display: inline-flex; align-items: center; gap: 6px; min-width: 0; text-align: left; }
625
+ .fx-choice .fx-lucide { flex: none; width: 14px; height: 14px; color: var(--t-dim); }
626
+ .fx-choice__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
627
+ .fx-rows .fx-rows__join { min-width: 0; width: 100%; padding-left: 8px; font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; }
628
+ .fx-rows__word { font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; padding-left: 9px; }
629
+ .fx-fgroups__row .fx-rows__join { min-width: 0; width: 64px; padding-left: 8px; font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; min-height: 32px; }
630
+ .fx-rows__date { display: flex; flex-wrap: wrap; gap: 6px; overflow: visible; }
631
+ .fx-rows__date > .fx-choice { flex: 1 1 130px; }
632
+ .fx-rows__date .fx-input { width: auto; flex: 1 1 130px; }
633
+ .fx-rows__days { display: inline-flex; flex: 1 1 130px; align-items: center; gap: 6px; }
634
+ .fx-rows__days .fx-input[type="number"] { width: 72px; flex: none; }
635
+ .fx-pop__bar { display: flex; align-items: center; justify-content: space-between; }
636
+ .fx-pop__link { padding: 4px 8px; border: 0; background: none; color: var(--t-accent); font: 500 12.5px/1.3 var(--t-body); cursor: pointer; }
637
+ .fx-pop__link:hover, .fx-pop__link:focus-visible { text-decoration: underline; outline: none; }
548
638
  .fx-pop.fx-pop--choices { min-width: 200px; max-width: 300px; padding: 6px; }
639
+ /* A value list row, as ClickUp's: mark (when there is one), name, tick box
640
+ at the right. No empty space at the left. */
641
+ .fx-pop__item--check { gap: 8px; }
642
+ .fx-pop__item--check .fx-pop__text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
643
+ .fx-pop__box { display: inline-grid; place-items: center; flex: none; width: 16px; height: 16px; margin-left: auto; border: 1px solid var(--t-control); border-radius: var(--t-radius); color: var(--t-on-primary); }
644
+ .fx-pop__item--check[aria-checked="true"] .fx-pop__box { border-color: var(--t-primary); background: var(--t-primary); }
645
+ .fx-pop__box svg { width: 12px; height: 12px; }
646
+ .fx-dot { flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--cf, var(--t-dim)); }
647
+ .fx-dot--stage { --stage: var(--t-accent); background: var(--stage); }
648
+ .fx-dot--stage[data-rank="0"] { --stage: var(--t-mute); }
649
+ .fx-dot--stage[data-rank="3"], .fx-dot--stage[data-rank="4"] { --stage: color-mix(in srgb, var(--t-accent) 45%, var(--t-success)); }
650
+ .fx-dot--stage[data-rank="5"], .fx-dot--stage[data-rank="6"] { --stage: var(--t-success); }
651
+ .fx-dot--stage[data-rank="7"] { --stage: var(--t-dim); }
549
652
  .fx-pop--choices .fx-input { min-height: 32px; margin-bottom: 4px; font-size: 13px; }
550
653
  .fx-pop__end { display: inline-grid; margin-left: auto; color: var(--t-accent); }
551
654
  @media (max-width: 640px) {
552
655
  .fx-pop.fx-pop--rows { min-width: 0; width: calc(100vw - 16px); }
553
656
  .fx-rows__row, .fx-rows__row--sort { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 32px; }
554
- .fx-rows__row > .fx-rows__lead, .fx-rows__row > .fx-select:first-child { grid-column: 1 / -1; }
657
+ .fx-rows__row > .fx-rows__lead, .fx-rows__row > .fx-select:first-child, .fx-rows__row > .fx-rows__join { grid-column: 1 / -1; }
658
+ .fx-rows__row > .fx-rows__join { width: 72px; }
659
+ .fx-fgroups__row { grid-template-columns: minmax(0, 1fr); }
660
+ .fx-fgroups__row > .fx-rows__lead, .fx-fgroups__row > span { padding-top: 0; }
555
661
  .fx-rows__row > .fx-rows__value { grid-column: 1 / 3; }
556
662
  .fx-rows__row > .fx-rows__value--none { display: none; }
557
663
  }
@@ -590,6 +696,42 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
590
696
  .fx-th-sort .fx-lucide { width: 13px; height: 13px; }
591
697
  .fx-th-sort__n { font: 700 10px/1 var(--t-body); }
592
698
  @media (hover: none) { .fx-th-sort { opacity: 1; } }
699
+ /* A sized list (columns.js): each column has its width, the table is as
700
+ wide as its columns (never stretched; space left over stays empty) and
701
+ scrolls sideways past the page. A head's right
702
+ edge is a grip that resizes the column; a head drags to move it, and a
703
+ line shows where it lands. */
704
+ .fx-table-scroll { overflow-x: auto; scrollbar-width: thin; }
705
+ .fx-table.fx-table--sized { table-layout: fixed; }
706
+ .fx-table--sized th, .fx-table--sized td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
707
+ .fx-table--sized th { position: relative; }
708
+ /* Every cell is one line: a long value ends in "…" and the row keeps its
709
+ height, as HubSpot's. A wrapper in a cell (a client with its warning
710
+ mark, a stage pill) shrinks its text, never its mark. */
711
+ .fx-table--sized td > * { max-width: 100%; }
712
+ .fx-table--sized .fx-job__cell { display: flex; min-width: 0; }
713
+ .fx-table--sized .fx-job__cell > * { flex: none; }
714
+ .fx-table--sized .fx-job__cell > .fx-strong,
715
+ .fx-table--sized .fx-status { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
716
+ .fx-table--sized .fx-job__cell > .fx-strong { flex: 0 1 auto; }
717
+ .fx-table--sized .fx-th-cell { max-width: 100%; }
718
+ .fx-table--sized .fx-th { overflow: hidden; text-overflow: ellipsis; }
719
+ .fx-th--drag .fx-th { cursor: grab; touch-action: none; }
720
+ .fx-th--drag[data-dragging] { opacity: .5; }
721
+ .fx-th--drag[data-dragging] .fx-th { cursor: grabbing; }
722
+ .fx-table--sized th[data-drop]::after { content: ""; position: absolute; top: 6px; bottom: 6px; width: 2px; border-radius: 1px; background: var(--t-accent); }
723
+ .fx-table--sized th[data-drop="before"]::after { left: 0; }
724
+ .fx-table--sized th[data-drop="after"]::after { right: 0; }
725
+ .fx-th-grip { position: absolute; top: 0; right: 0; bottom: 0; width: 8px; cursor: col-resize; touch-action: none; }
726
+ .fx-th-grip::after { content: ""; position: absolute; top: 25%; bottom: 25%; right: 3px; width: 2px; border-radius: 1px; background: transparent; }
727
+ th:hover > .fx-th-grip::after { background: var(--t-brd-hi); }
728
+ .fx-th-grip:hover::after, .fx-th-grip:focus-visible::after, .fx-table--sized[data-resizing] .fx-th-grip:active::after { background: var(--t-accent); }
729
+ .fx-th-grip:focus-visible { outline: none; }
730
+ .fx-table--sized[data-resizing] { cursor: col-resize; user-select: none; }
731
+ .fx-table--sized .fx-tick-cell, .fx-table--sized .fx-th-add, .fx-table--sized .fx-c-end { overflow: visible; text-overflow: clip; }
732
+ .fx-table--sized .fx-tick-cell { padding-right: 0; }
733
+ /* + stays at the right edge while the table scrolls sideways. */
734
+ .fx-table--sized .fx-th-add { position: sticky; right: 0; z-index: 1; padding-left: 0; padding-right: 12px; }
593
735
  .fx-avatar--sm { width: 24px; height: 24px; font-size: 10px; }
594
736
  .fx-c-date time { font-variant-numeric: tabular-nums; }
595
737
  .fx-stage[data-rank="plain"] { --stage: var(--t-dim); }
@@ -604,9 +746,41 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
604
746
  .fx-board__head { display: flex; flex: none; align-items: center; gap: 8px; min-width: 0; padding: 2px 4px 4px 2px; }
605
747
  .fx-board__sum { flex: none; margin-left: auto; color: var(--t-dim); font-size: 12px; font-variant-numeric: tabular-nums; }
606
748
  .fx-board__cards { display: grid; gap: 8px; align-content: start; min-height: 0; max-height: 70vh; padding-right: 2px; overflow-y: auto; scrollbar-width: thin; }
749
+ /* listFrame() in kit.js: the window of a list or board (Jobs, Invoices,
750
+ Reconcile). Its body scrolls both ways; the foot holds the count. A table
751
+ or panel inside drops its own frame. On a wide, tall screen the page is
752
+ the window's height and does not scroll itself: the frame fills it, and
753
+ on the board each lane scrolls inside itself. */
754
+ .fx-frame { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); box-shadow: var(--t-shadow); overflow: hidden; }
755
+ .fx-frame__view { display: flex; flex-direction: column; min-height: 0; }
756
+ .fx-frame__body { min-height: 0; overflow: auto; scrollbar-width: thin; }
757
+ /* edgeFade() in kit.js: a soft fade over each edge with more content past
758
+ it, in the colour under the rows (--fx-fade). */
759
+ .fx-fades { position: relative; --fx-fade: var(--t-paper); }
760
+ .fx-fade { position: absolute; z-index: 3; pointer-events: none; opacity: 0; transition: opacity .15s; }
761
+ .fx-fade[data-edge="top"] { inset: 0 0 auto; height: 24px; background: linear-gradient(to bottom, var(--fx-fade), transparent); }
762
+ .fx-fade[data-edge="bottom"] { inset: auto 0 0; height: 24px; background: linear-gradient(to top, var(--fx-fade), transparent); }
763
+ .fx-fade[data-edge="left"] { inset: 0 auto 0 0; width: 24px; background: linear-gradient(to right, var(--fx-fade), transparent); }
764
+ .fx-fade[data-edge="right"] { inset: 0 0 0 auto; width: 24px; background: linear-gradient(to left, var(--fx-fade), transparent); }
765
+ .fx-fades[data-fade~="top"] > .fx-fade[data-edge="top"], .fx-fades[data-fade~="bottom"] > .fx-fade[data-edge="bottom"],
766
+ .fx-fades[data-fade~="left"] > .fx-fade[data-edge="left"], .fx-fades[data-fade~="right"] > .fx-fade[data-edge="right"] { opacity: 1; }
767
+ .fx-main--board .fx-frame__view { --fx-fade: var(--t-bg); }
768
+ .fx-board__col.fx-fades { --fx-fade: var(--t-s1); }
769
+ .fx-board__col > .fx-fade[data-edge="bottom"] { border-radius: 0 0 var(--t-radius-lg) var(--t-radius-lg); }
770
+ .fx-frame__body .fx-panel { border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
771
+ .fx-frame__body .fx-empty { padding: 24px; }
772
+ .fx-frame .fx-table-scroll { overflow: visible; }
773
+ .fx-frame thead th { position: sticky; top: 0; z-index: 2; }
774
+ .fx-frame__foot { display: flex; flex: none; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 6px 12px; border-top: 1px solid var(--fx-line); color: var(--t-dim); font-size: 13px; font-variant-numeric: tabular-nums; }
775
+ .fx-frame__tools { display: flex; align-items: center; gap: 8px; }
776
+ .fx-main--board .fx-frame__body { padding: 12px; background: var(--t-bg); }
607
777
  @media (min-width: 821px) and (min-height: 560px) {
608
- .fx-main--board { height: 100vh; height: 100dvh; padding: 24px 24px 16px; grid-template-rows: auto auto minmax(0, 1fr); align-content: stretch; }
609
- .fx-main--board > .fx-jobs { min-height: 0; }
778
+ .fx-main--frame { height: 100vh; height: 100dvh; padding-bottom: 16px; display: flex; flex-direction: column; }
779
+ .fx-main--frame > * { flex: none; }
780
+ .fx-main--frame > .fx-frame { flex: 1; min-height: 0; }
781
+ .fx-main--frame .fx-frame__view, .fx-main--frame .fx-frame__body { flex: 1; }
782
+ .fx-main--board .fx-frame__body { display: flex; overflow: hidden; }
783
+ .fx-main--board .fx-jobs { flex: 1; min-width: 0; min-height: 0; }
610
784
  .fx-main--board .fx-board { height: 100%; overflow-y: hidden; align-items: stretch; }
611
785
  .fx-main--board .fx-board__cards { flex: 1; max-height: none; }
612
786
  }
@@ -674,7 +848,14 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
674
848
  the activity at the side. It stacks on a phone. */
675
849
  .fx-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; color: var(--t-dim); overflow-wrap: anywhere; }
676
850
  .fx-dialog.fx-task { width: min(1040px, calc(100vw - 32px)); max-height: calc(100vh - 48px); overflow-y: auto; }
677
- .fx-task__grid { display: grid; grid-template-columns: minmax(0, 1fr) 280px; }
851
+ .fx-task__grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; }
852
+ /* No sidecar: the main area takes the room. Fix is wider for the change table. */
853
+ .fx-task__grid:has(> .fx-sidecar[hidden]) { grid-template-columns: minmax(0, 1fr); }
854
+ .fx-task__grid:has(> .fx-sidecar [data-tab="fix"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr) 400px; }
855
+ .fx-task__side { background: var(--t-s1); }
856
+ .fx-task__tools { display: flex; align-items: center; gap: 4px; }
857
+ .fx-task__tools .fx-lucide { width: 16px; height: 16px; }
858
+ .fx-task__tools [aria-pressed="true"] { background: var(--fx-hover); color: var(--t-strong); }
678
859
  .fx-task__main { min-width: 0; padding: 20px 28px 28px; }
679
860
  .fx-task__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
680
861
  .fx-task__crumb { display: flex; flex-wrap: wrap; gap: 6px; color: var(--t-dim); font-size: 12.5px; }
@@ -688,11 +869,22 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
688
869
  .fx-task__late { color: var(--t-danger) !important; }
689
870
  .fx-task__link { color: var(--t-accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--t-accent) 40%, transparent); }
690
871
  .fx-task__link:hover { border-bottom-color: currentColor; }
691
- .fx-task__fix { scroll-margin-top: 16px; margin-top: 22px; border: 1px solid var(--t-warn-brd, var(--t-warn)); border-radius: var(--t-radius); background: var(--t-warn-bg); }
692
- .fx-task__fix h3, .fx-task__lines h3, .fx-task__activity h3 { margin: 0; color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
693
- .fx-task__fix h3 { padding: 12px 14px 4px; color: var(--t-warn); }
694
- .fx-task__fixrow { padding: 9px 14px; border-top: 1px solid var(--fx-line-soft); color: var(--t-strong); font-size: 13.5px; }
695
- .fx-task__fixrow:first-of-type { border-top: 0; }
872
+ .fx-task__lines h3 { margin: 0; color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
873
+ /* What to fix: one toggle chip a problem beside the stage; its problem opens
874
+ in the sidecar's Fix tab (jobs.js openJob). One layout with or without. */
875
+ .fx-task__stage { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
876
+ .fx-task__fixchip { all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px; border: 1px solid var(--t-warn-brd, var(--t-warn)); border-radius: 999px; background: var(--t-warn-bg); color: var(--t-warn); font-size: 12.5px; font-weight: 600; }
877
+ .fx-task__fixchip .fx-lucide { width: 14px; height: 14px; }
878
+ .fx-task__fixchip:hover { border-color: var(--t-warn); }
879
+ .fx-task__fixchip:focus-visible { box-shadow: var(--fx-focus); }
880
+ /* Pressed: its problem is open in the sidecar's Fix tab. */
881
+ .fx-task__fixchip[aria-pressed="true"] { border-color: var(--t-warn); background: var(--t-warn); color: var(--t-paper); }
882
+ .fx-task__fixitem { display: grid; gap: 8px; justify-items: start; font-size: 13.5px; }
883
+ .fx-task__fixitem > strong { color: var(--t-strong); }
884
+ .fx-task__fixitem > p { margin: 0; }
885
+ .fx-task__fixitem > .fx-task__review { justify-self: stretch; }
886
+ .fx-task__fixgo .fx-lucide { width: 14px; height: 14px; }
887
+ .fx-task__fixgo { display: inline-flex; align-items: center; gap: 6px; }
696
888
  .fx-task__choose { scroll-margin-top: 16px; margin-top: 22px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 12px 14px; border: 1px solid var(--t-warn-brd, var(--t-warn)); border-radius: var(--t-radius); background: var(--t-warn-bg); color: var(--t-strong); font-size: 13.5px; }
697
889
  .fx-task__choose > svg { flex: none; width: 20px; height: 20px; color: var(--t-warn); }
698
890
  .fx-task__choose > span { flex: 1 1 200px; }
@@ -704,7 +896,6 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
704
896
  invoice opened from Invoices (the pager on top). */
705
897
  .fx-rec { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
706
898
  .fx-rec .fx-lucide { width: 16px; height: 16px; }
707
- .fx-rec__gap { max-width: 1180px; }
708
899
  .fx-rec__gap .fx-toolbar .fx-rows__title { margin: 0; }
709
900
  .fx-rec__gap .fx-ph__actions .fx-btn, .fx-rec__acts .fx-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
710
901
  .fx-rec__results { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
@@ -758,8 +949,9 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
758
949
  .fx-task__miss { color: var(--t-warn); }
759
950
  .fx-task__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; }
760
951
  .fx-task__actions:empty { display: none; }
761
- .fx-task__activity { min-width: 0; padding: 24px 20px; border-left: 1px solid var(--fx-line); background: var(--t-s1); }
762
- .fx-task__activity h3 { margin-bottom: 14px; }
952
+ .fx-task__activity { min-width: 0; }
953
+ /* Review in the sidecar: the change banner, without its own box. */
954
+ .fx-task__review .fx-change { margin: 0; padding: 0; border: 0; background: none; }
763
955
  .fx-task__activity ol { margin: 0; padding: 0; list-style: none; }
764
956
  .fx-task__activity li { position: relative; padding: 0 0 16px 18px; color: var(--t-text); font-size: 13px; }
765
957
  .fx-task__activity li::before { content: ""; position: absolute; left: 3px; top: 6px; width: 6px; height: 6px; border-radius: 50%; background: var(--t-mute); }
@@ -771,7 +963,8 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
771
963
  .fx-task__grid { grid-template-columns: minmax(0, 1fr); }
772
964
  .fx-task__main { padding: 16px; }
773
965
  .fx-task__fields { grid-template-columns: minmax(0, 1fr); }
774
- .fx-task__activity { padding: 16px; border-top: 1px solid var(--fx-line); border-left: 0; }
966
+ .fx-task__grid:has(> .fx-sidecar [data-tab="fix"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr); }
967
+ .fx-task__side { border-top: 1px solid var(--fx-line); border-left: 0; }
775
968
  }
776
969
 
777
970
  /* ── Dialog ─────────────────────────────────────────────────────────────── */
@@ -898,14 +1091,21 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
898
1091
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .15);
899
1092
  }
900
1093
  .fx-swatch[aria-checked="true"] { box-shadow: 0 0 0 2px var(--t-paper), 0 0 0 4px var(--t-text); }
901
- .fx-swatch--default { overflow: hidden; background: var(--t-paper); }
902
- .fx-swatch--default span { display: block; width: 100%; height: 100%; }
1094
+ /* The default accent: a pill that says Default, before the colours. */
1095
+ .fx-swatch--default {
1096
+ display: inline-flex; align-items: center; gap: 6px; width: auto; padding: 0 10px 0 4px; border-radius: 999px;
1097
+ background: var(--t-paper); color: var(--t-text); font: 600 12.5px/1 var(--t-body); box-shadow: inset 0 0 0 1px var(--t-control);
1098
+ }
1099
+ .fx-swatch--default[aria-checked="true"] { box-shadow: inset 0 0 0 1px var(--t-control), 0 0 0 2px var(--t-paper), 0 0 0 4px var(--t-text); }
1100
+ .fx-swatch__dot { flex: none; width: 22px; height: 22px; border-radius: 50%; }
903
1101
  .fx-swatch-custom { display: inline-flex; align-items: center; gap: 8px; color: var(--t-dim); font-size: 13px; font-weight: 600; }
904
1102
  .fx-swatch-custom input { width: 34px; height: 30px; padding: 0; border: 1px solid var(--t-control); border-radius: var(--t-radius); background: var(--t-paper); cursor: pointer; }
905
1103
 
906
1104
  /* ── Narrow screens: the sidebar becomes a bar across the top. ──────────── */
907
1105
  @media (max-width: 820px) {
908
1106
  .fx-shell { flex-direction: column; }
1107
+ /* In a column, the page takes the full width, not its content's. */
1108
+ .fx-main { width: 100%; }
909
1109
  .fx-side { flex-direction: row; align-items: center; width: auto; height: 60px; padding: 0 12px; border-right: 0; border-bottom: 1px solid var(--fx-line); }
910
1110
  .fx-side__head { flex-direction: row-reverse; gap: 6px; padding: 0; }
911
1111
  .fx-side__head .fx-brand { display: none; }
@@ -946,8 +1146,11 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
946
1146
  /* ── Phones ─────────────────────────────────────────────────────────────── */
947
1147
  @media (max-width: 640px) {
948
1148
  .fx-main { padding-top: 20px; }
949
- .fx-title { font-size: 24px; }
1149
+ .fx-title { font-size: 22px; }
950
1150
  .fx-table thead { display: none; }
1151
+ .fx-table.fx-table--sized { width: 100% !important; min-width: 0; table-layout: auto; }
1152
+ .fx-table--sized td { white-space: normal; }
1153
+ .fx-table--sized colgroup { display: none; }
951
1154
  .fx-table, .fx-table tbody, .fx-table tr, .fx-table td { display: block; width: 100%; }
952
1155
  .fx-table tr { padding: 12px 14px; border-bottom: 1px solid var(--fx-line-soft); }
953
1156
  .fx-table td { padding: 4px 0; border: 0; }
@@ -955,7 +1158,6 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
955
1158
  .fx-table tbody tr:hover td { background: none; }
956
1159
  .fx-table__actions { text-align: left; }
957
1160
  .fx-table__actions .fx-row--end { justify-content: flex-start; }
958
- .fx-ph__actions, .fx-ph__actions .fx-btn { width: 100%; }
959
1161
  /* People: the name with its actions (⋯, Approve) on one line, then the
960
1162
  role select full width, then the status pill when there is one. */
961
1163
  .fx-people tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px 10px; }
@@ -976,7 +1178,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
976
1178
  .fx-table--jobs tr.fx-row-link > td.fx-c-client { grid-area: 1 / 2; }
977
1179
  .fx-table--jobs tr.fx-row-link > td.fx-c-total { grid-area: 1 / 3; }
978
1180
  .fx-table--jobs td.fx-c-end { display: none; }
979
- .fx-table--jobs tr.fx-row-link > td[data-label]:not(.fx-c-total, .fx-c-stage)::before { content: attr(data-label) " "; color: var(--t-dim); font-size: 12px; }
1181
+ .fx-table--jobs tr.fx-row-link > td[data-label]:not(.fx-c-client, .fx-c-total, .fx-c-stage)::before { content: attr(data-label) " "; color: var(--t-dim); font-size: 12px; }
980
1182
  .fx-table--jobs tr.fx-table__group { grid-template-columns: 28px minmax(0, 1fr); background: var(--t-s1); }
981
1183
  .fx-table--jobs tr.fx-table__group > td.fx-tick-cell { grid-row: 1; align-self: center; padding: 0; }
982
1184
  .fx-table--jobs tr.fx-table__group > td { background: none; }
@@ -1118,10 +1320,22 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1118
1320
  in from the right of the manager and leaves the table usable. */
1119
1321
  .fx-cfm__card { position: relative; }
1120
1322
  .fx-cfm__card:has(> .fx-cfs:not([hidden])) { min-height: min(640px, 100vh); }
1121
- .fx-cfs { position: absolute; inset: 0 0 0 auto; z-index: 5; display: flex; width: min(420px, 100%); border-left: 1px solid var(--fx-line); background: var(--t-paper); color: var(--t-text); box-shadow: -16px 0 40px rgba(0, 0, 0, .22); animation: fx-cfs-in .18s var(--fx-ease); }
1122
- .fx-cfs[hidden] { display: none; }
1123
- @keyframes fx-cfs-in { from { transform: translateX(24px); opacity: 0; } }
1124
- @media (prefers-reduced-motion: reduce) { .fx-cfs { animation: none; } }
1323
+ /* The shared sidecar (sidecar.js): a right column, or over the page with
1324
+ --over. Tabs are an icon and one word; × closes it. */
1325
+ .fx-sidecar { display: flex; flex-direction: column; min-width: 0; border-left: 1px solid var(--fx-line); background: var(--t-paper); color: var(--t-text); }
1326
+ .fx-sidecar[hidden] { display: none; }
1327
+ .fx-sidecar--over { position: absolute; inset: 0 0 0 auto; z-index: 5; width: min(420px, 100%); box-shadow: -16px 0 40px rgba(0, 0, 0, .22); animation: fx-sidecar-in .18s var(--fx-ease); }
1328
+ @keyframes fx-sidecar-in { from { transform: translateX(24px); opacity: 0; } }
1329
+ @media (prefers-reduced-motion: reduce) { .fx-sidecar--over { animation: none; } }
1330
+ .fx-sidecar__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 52px; padding: 0 12px 0 8px; border-bottom: 1px solid var(--fx-line-soft); }
1331
+ .fx-sidecar__tabs { display: flex; gap: 2px; min-width: 0; }
1332
+ .fx-sidecar__tab { all: unset; cursor: pointer; position: relative; display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 0 10px; border-radius: var(--t-radius); color: var(--t-dim); font-size: 13px; font-weight: 500; }
1333
+ .fx-sidecar__tab .fx-lucide { width: 15px; height: 15px; }
1334
+ .fx-sidecar__tab:hover { background: var(--fx-hover); color: var(--t-strong); }
1335
+ .fx-sidecar__tab[aria-selected="true"] { background: var(--fx-hover); color: var(--t-strong); }
1336
+ .fx-sidecar__tab:focus-visible { box-shadow: var(--fx-focus); }
1337
+ .fx-sidecar__mark { width: 6px; height: 6px; border-radius: 50%; background: var(--t-warn); }
1338
+ .fx-sidecar__body { flex: 1; min-height: 0; overflow-y: auto; padding: 16px; }
1125
1339
  .fx-cfs__form { position: sticky; top: 0; display: flex; flex-direction: column; width: 100%; height: min(100%, 100vh); max-height: 100vh; min-height: 0; }
1126
1340
  .fx-cfs__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 52px; padding: 0 12px 0 16px; border-bottom: 1px solid var(--fx-line-soft); }
1127
1341
  .fx-cfs__typename, .fx-cfs__typepick { display: inline-flex; align-items: center; gap: 8px; color: var(--t-text); font-weight: 500; font-size: 14px; }
@@ -1129,9 +1343,9 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1129
1343
  .fx-cfs__typepick:hover, .fx-cfs__typepick:focus-within { background: var(--fx-hover); }
1130
1344
  .fx-cfs__typepick .fx-lucide { width: 15px; height: 15px; }
1131
1345
  .fx-cfs__type { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
1132
- .fx-cfs__close, .fx-cfs__delete, .fx-cfs__tool { all: unset; cursor: pointer; display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 6px; color: var(--t-dim); }
1133
- .fx-cfs__close:hover, .fx-cfs__tool:hover:not(:disabled) { background: var(--fx-hover); color: var(--t-strong); }
1134
- .fx-cfs__close:focus-visible, .fx-cfs__delete:focus-visible, .fx-cfs__tool:focus-visible { box-shadow: var(--fx-focus); }
1346
+ .fx-sidecar__close, .fx-cfs__delete, .fx-cfs__tool { all: unset; cursor: pointer; display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 6px; color: var(--t-dim); }
1347
+ .fx-sidecar__close:hover, .fx-cfs__tool:hover:not(:disabled) { background: var(--fx-hover); color: var(--t-strong); }
1348
+ .fx-sidecar__close:focus-visible, .fx-cfs__delete:focus-visible, .fx-cfs__tool:focus-visible { box-shadow: var(--fx-focus); }
1135
1349
  .fx-cfs__body { flex: 1; min-height: 0; overflow-y: auto; display: grid; align-content: start; gap: 0; }
1136
1350
  .fx-cfs__section { display: grid; gap: 8px; padding: 14px 16px 4px; }
1137
1351
  .fx-cfs__section:last-child { margin-top: 14px; padding-top: 16px; border-top: 1px solid var(--fx-line-soft); }
@@ -1167,7 +1381,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1167
1381
  .fx-cfs__where .fx-lucide { width: 16px; height: 16px; color: var(--t-dim); }
1168
1382
  .fx-cfs__foot { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--fx-line-soft); }
1169
1383
  .fx-cfs__delete { color: var(--t-danger); background: color-mix(in srgb, var(--t-danger) 12%, transparent); }
1170
- .fx-cfs__delete:hover { background: color-mix(in srgb, var(--t-danger) 22%, transparent); }
1384
+ .fx-cfs__delete:hover { background: color-mix(in srgb, var(--t-danger) 28%, transparent); }
1171
1385
  .fx-cfs__delete .fx-lucide { width: 16px; height: 16px; }
1172
1386
  @media (max-width: 720px) {
1173
1387
  .fx-cfm { grid-template-columns: minmax(0, 1fr) 36px 32px; column-gap: 8px; }
@@ -1250,13 +1464,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1250
1464
  .fx-pager__step:hover:not(:disabled) { background: var(--fx-hover); border-color: var(--fx-line); }
1251
1465
  .fx-pager__step:disabled { opacity: 0.35; cursor: default; }
1252
1466
 
1253
- /* New invoice from a job: a quiet strip above the form, and a searchable
1254
- list of jobs with no invoice. */
1255
- .fx-fromjob {
1256
- display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px;
1257
- padding: 12px 16px; border: 1px dashed var(--fx-line); border-radius: var(--t-radius);
1258
- }
1259
- .fx-fromjob__text { display: grid; gap: 2px; font-size: 14px; }
1467
+ /* New invoice from a job: a searchable list of jobs with no invoice. */
1260
1468
  .fx-dialog:has(.fx-pick__body) { width: min(620px, calc(100vw - 32px)); }
1261
1469
  .fx-pick__body { display: grid; gap: 12px; }
1262
1470
  .fx-pick { display: grid; max-height: min(420px, 55vh); overflow-y: auto; border: 1px solid var(--fx-line); border-radius: var(--t-radius); }
@@ -1270,12 +1478,21 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1270
1478
  /* The Invoices list head (invoicing.js): tabs, search and the send bar stay
1271
1479
  at the top while rows scroll up under them; a shadow shows once they do.
1272
1480
  Narrow, the app bar (60px) sits above it. */
1273
- .fx-listhead { position: sticky; top: 0; z-index: 5; margin: -12px calc(-1 * clamp(16px, 4vw, 48px)) 0; padding: 12px clamp(16px, 4vw, 48px) 0; background: var(--t-bg); transition: box-shadow .15s; }
1481
+ /* listHead() in kit.js: the toolbar row of every list. One height, with
1482
+ chips, tabs or a Board/List switch in it, so the rows start at one place. */
1483
+ .fx-listhead > .fx-toolbar { min-height: 40px; }
1484
+ .fx-listhead .fx-toolbar__end { flex: 1 1 auto; }
1485
+ .fx-listhead > .fx-toolbar--plain { align-items: center; }
1486
+ .fx-listhead[data-sticky] { position: sticky; top: 0; z-index: 5; margin: -12px calc(-1 * var(--fx-gutter)) 0; padding: 12px var(--fx-gutter) 0; background: var(--t-bg); transition: box-shadow .15s; }
1274
1487
  .fx-listhead[data-stuck] { box-shadow: 0 10px 12px -12px rgba(0, 0, 0, .45); }
1275
- .fx-listhead__edge { height: 0; margin-bottom: -20px; }
1276
- @media (max-width: 820px) { .fx-listhead { top: 60px; } }
1488
+ /* The edge tells listHead when the rows reach the toolbar. In the page's
1489
+ grid it takes a row and a gap of its own, so the toolbar moves up by both. */
1490
+ .fx-listhead__edge { height: 0; }
1491
+ .fx-listhead__edge + .fx-listhead[data-sticky] { margin-top: -32px; }
1492
+ .fx-listhead > .fx-toolbar > .fx-tabs button { padding-block: 9px; }
1493
+ @media (max-width: 820px) { .fx-listhead[data-sticky] { top: 60px; } }
1277
1494
  /* On a phone the head would hide too many rows: it scrolls away. */
1278
- @media (max-width: 560px) { .fx-listhead { position: static; } }
1495
+ @media (max-width: 560px) { .fx-listhead[data-sticky] { position: static; } }
1279
1496
 
1280
1497
  /* The bulk action bar (bulk-bar.js): floats at the foot while rows are
1281
1498
  ticked. "N selected ×", then icon actions in groups, then More. It takes
@@ -1300,12 +1517,6 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1300
1517
  .fx-bulk__act[data-tone="danger"] { color: var(--t-danger); }
1301
1518
  .fx-bulk__act[data-tone="danger"]:hover { background: var(--t-danger-bg); }
1302
1519
  .fx-bulk__sep { flex: none; width: 1px; height: 18px; margin: 0 4px; background: var(--fx-line); }
1303
- /* An icon-only action says its name on hover, in a tip above it. */
1304
- .fx-bulk [data-tip]:hover::after, .fx-bulk [data-tip]:focus-visible::after {
1305
- content: attr(data-tip); position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%);
1306
- padding: 4px 8px; border: 1px solid var(--fx-line); border-radius: var(--t-radius); background: var(--t-paper); color: var(--t-strong);
1307
- font: 500 12px/1.2 var(--t-body); pointer-events: none; box-shadow: var(--t-shadow), 0 4px 12px rgba(0, 0, 0, .12);
1308
- }
1309
1520
  /* More: a compact list, one row per action, like a dropdown. */
1310
1521
  .fx-pop.fx-bulk__more { width: 248px; min-width: 0; padding: 4px; }
1311
1522
  .fx-bulk__search { width: 100%; height: 30px; margin-bottom: 2px; padding: 4px 8px; font-size: 13px; }
@@ -1326,7 +1537,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1326
1537
  /* Xero › Import (server/ui/xero-import.js): Review compares one fact per row
1327
1538
  with ✓ or ≠ between the sides; the list and history tick rows for the bulk
1328
1539
  bar. */
1329
- .fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0; max-width: 980px; }
1540
+ .fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
1330
1541
  .fx-xi .fx-lucide { width: 16px; height: 16px; }
1331
1542
  .fx-xi__views { display: inline-flex; border: 1px solid var(--fx-line); }
1332
1543
  .fx-xi__view { display: inline-grid; place-items: center; width: 40px; height: 36px; border: 0; background: transparent; color: var(--t-dim); cursor: pointer; }
@@ -1370,6 +1581,8 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1370
1581
 
1371
1582
  /* A list's own search (view-bar.js findBox): an icon that opens into a box. */
1372
1583
  .fx-find { display: inline-flex; align-items: center; }
1584
+ /* On a tab bar the icon sits level with the tab names, not on the line. */
1585
+ .fx-toolbar > .fx-find { align-self: center; }
1373
1586
  .fx-find__button { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0; }
1374
1587
  .fx-find__button .fx-lucide { width: 15px; height: 15px; }
1375
1588
  .fx-find__input { display: none; width: min(240px, 60vw); height: 32px; padding: 0 10px; }
@@ -18,9 +18,11 @@ export function sendModeNote(to, sendMode, what = 'each invoice') {
18
18
  return text.join(' ');
19
19
  }
20
20
 
21
- // The line under a draft's title, one fact per sentence.
22
- export function draftNote(to, invoice, ready) {
23
- if (!to) return ready ? 'It has no problem. Approve gives it a number.' : 'Fix the problem, then approve it.';
24
- if (!ready) return `Fix the problem, then send it to ${to}.`;
25
- return sendModeNote(to, invoice?.sendMode, 'this invoice');
21
+ // A warning above a draft, only when its Send cannot work: the accounting
22
+ // system is off or takes no invoices. Otherwise nothing: the problems show
23
+ // on their own, and the Send dialog says what Send does.
24
+ export function draftNote(to, invoice) {
25
+ const sendMode = invoice?.sendMode;
26
+ if (!to || !sendMode || (sendMode.connected && sendMode.writesAllowed)) return null;
27
+ return sendModeNote(to, sendMode);
26
28
  }
@@ -12,10 +12,10 @@ const TEST_LABELS = {
12
12
  more_than: 'is more than', at_most: 'is at most', is: 'is', is_not: 'is not', is_one_of: 'is one of',
13
13
  is_empty: 'is empty', is_not_empty: 'is not empty', has: 'has', has_not: 'does not have',
14
14
  is_not_one_of: 'is not one of', contains: 'contains', does_not_contain: 'does not contain',
15
- starts_with: 'starts with', does_not_start_with: 'does not start with', has_number: 'has a number', one_word: 'is one word',
15
+ starts_with: 'starts with', does_not_start_with: 'does not start with', has_number: 'has a number', number_in_words: 'has a number beside words', one_word: 'is one word',
16
16
  };
17
17
  const ACTION_LABELS = { wait: 'Wait for a person', skip: 'Never draft', pending: 'Pending, with a note' };
18
- const NO_VALUE = ['is_empty', 'is_not_empty', 'has_number', 'one_word'];
18
+ const NO_VALUE = ['is_empty', 'is_not_empty', 'has_number', 'number_in_words', 'one_word'];
19
19
  const MANY = ['is_one_of', 'is_not_one_of'];
20
20
  const TYPED = ['contains', 'does_not_contain', 'starts_with', 'does_not_start_with'];
21
21
 
@@ -158,7 +158,7 @@ export function rulesSection(ui, title = 'Rules') {
158
158
  // A pending rule's note: what a person must fix. It shows on the draft.
159
159
  const note = h('input', {
160
160
  class: 'fx-input', maxlength: '200', value: draft?.action.note ?? '', 'aria-label': 'Note',
161
- placeholder: 'For example: Put the practitioner in the job, then check the Reference.',
161
+ placeholder: 'Type a note',
162
162
  });
163
163
  const noteField = h('div', { class: 'fx-field' }, h('span', { class: 'fx-label' }, 'Note on the draft'), note);
164
164
  const showNote = () => { noteField.hidden = action.value !== 'pending'; };