@arcforgelabs/flow 2026.10.36 → 2026.10.38

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 (38) hide show
  1. package/README.md +1 -0
  2. package/npm-shrinkwrap.json +2 -2
  3. package/package.json +1 -1
  4. package/server/api/custom-fields.js +2 -1
  5. package/server/api/invoicing.js +61 -3
  6. package/server/auth/clerk.js +35 -0
  7. package/server/auth/routes.js +45 -6
  8. package/server/core/custom-fields.js +18 -0
  9. package/server/core/host.js +24 -0
  10. package/server/core/job-board.js +2 -1
  11. package/server/core/job-match.js +241 -0
  12. package/server/core/ledger.js +80 -1
  13. package/server/index.js +9 -3
  14. package/server/integrations/seazona/feed.js +4 -1
  15. package/server/integrations/seazona/index.js +1 -1
  16. package/server/integrations/seazona/settings.js +5 -2
  17. package/server/integrations/sentry/index.js +5 -2
  18. package/server/integrations/xero/fit.js +108 -0
  19. package/server/integrations/xero/import.js +6 -1
  20. package/server/integrations/xero/index.js +4 -4
  21. package/server/integrations/xero/ledger.js +3 -0
  22. package/server/integrations/xero/links.js +293 -0
  23. package/server/integrations/xero/migrations/0008_xero_invoice_links.sql +26 -0
  24. package/server/integrations/xero/migrations/meta/_journal.json +7 -0
  25. package/server/integrations/xero/reconcile.js +17 -11
  26. package/server/integrations/xero/routes.js +42 -0
  27. package/server/integrations/xero/settings.js +12 -5
  28. package/server/ui/app-page.js +1 -1
  29. package/server/ui/app.js +142 -23
  30. package/server/ui/bulk-bar.js +100 -0
  31. package/server/ui/flow.css +225 -11
  32. package/server/ui/invoicing.js +234 -30
  33. package/server/ui/job-board.js +60 -34
  34. package/server/ui/job-view.js +30 -7
  35. package/server/ui/jobs.js +469 -38
  36. package/server/ui/reconcile.js +275 -0
  37. package/server/ui/routes.js +1 -1
  38. package/server/ui/xero-import.js +329 -0
@@ -155,7 +155,8 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
155
155
  /* Folded (wide screens only): the sidebar keeps its icons, the menu button
156
156
  and the account photo; each page's name is still read out. */
157
157
  @media (min-width: 821px) {
158
- .fx-shell[data-folded] .fx-side { width: 64px; padding-inline: 10px; }
158
+ .fx-shell[data-folded] .fx-side { width: 64px; padding-inline: 10px; cursor: e-resize; }
159
+ .fx-shell[data-folded] .fx-side :is(a, button, #user-button) { cursor: pointer; }
159
160
  .fx-shell[data-folded] .fx-side__head { flex-direction: column; gap: 10px; padding: 0 0 14px; }
160
161
  .fx-shell[data-folded] .fx-side__head .fx-brand { display: none; }
161
162
  .fx-shell[data-folded] .fx-nav__group { height: 1px; margin: 10px 6px; padding: 0; overflow: hidden; background: var(--fx-line); color: transparent; }
@@ -234,6 +235,12 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
234
235
  .fx-link__dot[data-tone="success"] { background: var(--t-success); }
235
236
  .fx-link__dot[data-tone="warn"] { background: var(--t-warn); }
236
237
  .fx-link__dot[data-tone="danger"] { background: var(--t-danger); }
238
+ .fx-linked { display: grid; }
239
+ .fx-linked__row { display: flex; align-items: center; gap: 8px; white-space: nowrap; min-height: 44px; padding: 6px 0; border-top: 1px solid var(--fx-line-soft); }
240
+ .fx-linked__row:first-child { border-top: 0; }
241
+ .fx-linked__ref { font-family: var(--t-data); font-variant-numeric: tabular-nums; color: var(--t-strong); }
242
+ .fx-linked__open { margin-left: auto; font-size: 12.5px; color: var(--t-dim); white-space: nowrap; }
243
+ .fx-linked__open:hover { color: var(--t-strong); }
237
244
  .fx-tag {
238
245
  margin-left: 8px; padding: 1px 7px; border: 1px solid var(--fx-line); border-radius: 999px;
239
246
  color: var(--t-dim); font-size: 11.5px; font-weight: 600;
@@ -269,9 +276,6 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
269
276
  font-size: 12.5px; font-weight: 700; letter-spacing: .02em;
270
277
  }
271
278
  .fx-empty { padding: 28px 16px; color: var(--t-dim); text-align: center; }
272
- .fx-roles { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; font-size: 13.5px; color: var(--t-dim); }
273
- .fx-roles dt { color: var(--t-strong); font-weight: 600; }
274
- .fx-roles dd { margin: 0; }
275
279
 
276
280
  /* ── Integrations ───────────────────────────────────────────────────────── */
277
281
  .fx-chip { border: 1px solid var(--fx-line); border-radius: 999px; background: var(--t-paper); color: var(--t-text); }
@@ -508,13 +512,32 @@ a.fx-strong:hover { text-decoration: underline; }
508
512
  .fx-table tbody tr.fx-table__group:hover td { background: var(--t-s1); }
509
513
  .fx-table__group-row { display: flex; align-items: center; gap: 12px; }
510
514
  .fx-table__group-row .fx-board__sum { margin-left: 0; }
515
+ /* The Jobs list's group head folds its rows (jobs.js, list): a click on the
516
+ head or its chevron. */
517
+ .fx-table--jobs .fx-table__group { cursor: pointer; }
518
+ .fx-table--jobs .fx-table__group td.fx-tick-cell { padding-right: 0; }
519
+ .fx-table__fold { display: inline-flex; align-items: center; gap: 8px; min-width: 0; padding: 2px 4px 2px 0; border: 0; background: none; color: var(--t-dim); font: inherit; cursor: pointer; }
520
+ .fx-table__fold > svg { flex: none; transition: transform .15s; }
521
+ .fx-table__fold[aria-expanded="true"] > svg { transform: rotate(90deg); }
522
+ .fx-table__fold:hover, .fx-table__fold:focus-visible { color: var(--t-strong); outline: none; }
523
+ .fx-table__fold:focus-visible .fx-stage, .fx-table__fold:focus-visible .fx-cf__opt { box-shadow: 0 0 0 2px var(--t-accent); }
524
+ .fx-table__group--folded td { padding-bottom: 14px; }
525
+ /* A Jobs list column head opens the column's menu (a click or a
526
+ right-click); + at the end shows a hidden column. */
527
+ .fx-th { margin: -4px -6px; padding: 4px 6px; border: 0; border-radius: var(--t-radius); background: none; color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }
528
+ .fx-th:hover, .fx-th:focus-visible { background: var(--fx-hover); color: var(--t-strong); outline: none; }
529
+ .fx-th-add { width: 36px; text-align: right; }
530
+ .fx-th--add { font-size: 16px; line-height: 1; }
531
+ .fx-avatar--sm { width: 24px; height: 24px; font-size: 10px; }
532
+ .fx-c-date time { font-variant-numeric: tabular-nums; }
511
533
  .fx-stage[data-rank="plain"] { --stage: var(--t-dim); }
512
- /* The board never scrolls sideways: its columns (one per work system status)
513
- are about a phone wide, wrap onto more rows when the page is narrow, and
514
- stack on a phone. On a wide, tall screen the board fills the page down to
515
- its bottom edge, and each column scrolls inside itself (jobs.js sets
516
- fx-main--board while the board shows). */
517
- .fx-board { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 256px), 1fr)); gap: 12px; align-items: start; }
534
+ /* The board is one row of lanes (one per group), each about a phone wide.
535
+ Lanes share the width when they fit; more lanes scroll sideways inside
536
+ the board, never as a second row. On a wide, tall screen the board fills
537
+ the page down to its bottom edge, the page itself does not scroll, and
538
+ each lane scrolls inside itself (jobs.js sets fx-main--board while the
539
+ board shows). */
540
+ .fx-board { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(min(85vw, 272px), 1fr); gap: 12px; align-items: start; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; padding-bottom: 4px; }
518
541
  .fx-board__col { display: flex; flex-direction: column; gap: 8px; min-width: 0; min-height: 0; padding: 10px 6px 6px 8px; border-radius: var(--t-radius-lg); background: var(--t-s1); }
519
542
  .fx-board__head { display: flex; flex: none; align-items: center; gap: 8px; min-width: 0; padding: 2px 4px 4px 2px; }
520
543
  .fx-board__sum { flex: none; margin-left: auto; color: var(--t-dim); font-size: 12px; font-variant-numeric: tabular-nums; }
@@ -522,7 +545,7 @@ a.fx-strong:hover { text-decoration: underline; }
522
545
  @media (min-width: 821px) and (min-height: 560px) {
523
546
  .fx-main--board { height: 100vh; height: 100dvh; padding: 24px 24px 16px; grid-template-rows: auto auto minmax(0, 1fr); align-content: stretch; }
524
547
  .fx-main--board > .fx-jobs { min-height: 0; }
525
- .fx-main--board .fx-board { height: 100%; overflow-y: auto; grid-auto-rows: 100%; align-items: stretch; }
548
+ .fx-main--board .fx-board { height: 100%; overflow-y: hidden; align-items: stretch; }
526
549
  .fx-main--board .fx-board__cards { flex: 1; max-height: none; }
527
550
  }
528
551
  .fx-board__none { padding: 8px 4px; color: var(--t-dim); font-size: 13px; }
@@ -608,6 +631,70 @@ a.fx-strong:hover { text-decoration: underline; }
608
631
  .fx-task__fix h3 { padding: 12px 14px 4px; color: var(--t-warn); }
609
632
  .fx-task__fixrow { padding: 9px 14px; border-top: 1px solid var(--fx-line-soft); color: var(--t-strong); font-size: 13.5px; }
610
633
  .fx-task__fixrow:first-of-type { border-top: 0; }
634
+ .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; }
635
+ .fx-task__choose > svg { flex: none; width: 20px; height: 20px; color: var(--t-warn); }
636
+ .fx-task__choose > span { flex: 1 1 200px; }
637
+ .fx-task__choices { display: flex; flex-wrap: wrap; gap: 10px; }
638
+ .fx-task__none { display: inline-flex; align-items: center; gap: 6px; color: var(--t-warn); }
639
+ .fx-task__none svg { width: 16px; height: 16px; }
640
+
641
+ /* Reconcile: the queue left, one job and its possible invoices right. */
642
+ .fx-rec { display: grid; grid-template-columns: 280px minmax(0, 1fr); border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); min-height: 420px; }
643
+ .fx-rec > .fx-empty, .fx-rec > .fx-dim { grid-column: 1 / -1; }
644
+ .fx-rec__queue { display: flex; flex-direction: column; border-right: 1px solid var(--fx-line); max-height: 75vh; overflow-y: auto; }
645
+ .fx-rec__tabs { display: flex; flex-wrap: wrap; gap: 6px; padding: 12px 16px; border-bottom: 1px solid var(--fx-line); }
646
+ .fx-rec__tab { display: inline-flex; align-items: center; min-height: 32px; padding: 0 10px; border: 1px solid var(--fx-line); border-radius: 999px; color: var(--t-text); font-size: 12.5px; font-weight: 600; text-decoration: none; }
647
+ .fx-rec__tab[aria-selected="true"] { border-color: var(--t-primary); background: var(--t-primary); color: var(--t-on-primary); }
648
+ .fx-rec__search { width: 100%; min-width: 0; }
649
+ .fx-rec__results { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
650
+ .fx-rec__item { display: grid; gap: 3px; min-height: 44px; padding: 12px 16px; border-bottom: 1px solid var(--fx-line-soft); border-left: 3px solid transparent; color: var(--t-text); text-decoration: none; }
651
+ .fx-rec__item:hover { background: var(--fx-hover); }
652
+ .fx-rec__item[aria-current="true"] { border-left-color: var(--t-accent); background: var(--t-s1); }
653
+ .fx-rec__item-top { display: flex; justify-content: space-between; gap: 8px; }
654
+ .fx-rec__item .fx-num { font-family: var(--t-data); font-variant-numeric: tabular-nums; }
655
+ .fx-rec__detail { display: flex; flex-direction: column; gap: 16px; min-width: 0; padding: 20px 24px; }
656
+ .fx-rec__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; flex-wrap: wrap; }
657
+ .fx-rec__head > div { display: grid; gap: 2px; }
658
+ .fx-rec__back { display: none; color: var(--t-accent); text-decoration: none; font-size: 13px; }
659
+ .fx-rec__title { margin: 0; font-family: var(--t-display); font-weight: var(--t-display-weight); color: var(--t-strong); font-size: 24px; }
660
+ .fx-rec__table { display: grid; grid-template-columns: 110px minmax(150px, 1fr) repeat(var(--cols), minmax(170px, 1fr)); border: 1px solid var(--fx-line); border-radius: var(--t-radius); overflow-x: auto; }
661
+ .fx-rec__row { display: contents; }
662
+ .fx-rec__row > span { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border-top: 1px solid var(--fx-line-soft); font-size: 13.5px; color: var(--t-strong); }
663
+ .fx-rec__row:first-child > span { border-top: 0; background: var(--t-s1); }
664
+ .fx-rec__row > span[role="cell"]:not(.fx-rec__job) { flex-direction: row; align-items: flex-start; gap: 6px; }
665
+ .fx-rec__row > span[role="cell"].fx-rec__acts { flex-direction: column; align-items: stretch; gap: 8px; }
666
+ .fx-rec__acts > .fx-task__link, .fx-rec__card > .fx-task__link { align-self: flex-start; }
667
+ .fx-rec__job { background: color-mix(in srgb, var(--t-accent) 5%, transparent); }
668
+ .fx-rec__row:first-child > .fx-rec__job { background: color-mix(in srgb, var(--t-accent) 10%, var(--t-s1)); }
669
+ .fx-rec__lines { display: flex; flex-direction: column; font-family: var(--t-data); font-variant-numeric: tabular-nums; font-size: 13px; line-height: 1.6; }
670
+ .fx-rec__mark { flex: none; font-weight: 800; }
671
+ [data-mark="same"] > .fx-rec__mark { color: var(--t-success); }
672
+ [data-mark="differs"], [data-mark="differs"] > .fx-rec__mark { color: var(--t-warn); }
673
+ .fx-rec__best { color: var(--t-success); font-size: 12px; font-weight: 700; }
674
+ .fx-rec__foot { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
675
+ .fx-rec__foot-acts { display: flex; gap: 10px; flex-wrap: wrap; }
676
+ .fx-rec__cards { display: none; }
677
+ .fx-rec__card { display: flex; flex-direction: column; gap: 6px; padding: 12px; border: 1px solid var(--fx-line); border-radius: var(--t-radius); font-size: 13.5px; color: var(--t-strong); }
678
+ .fx-rec__card.fx-rec__job { border-color: transparent; }
679
+ .fx-rec__card--best { border-color: var(--t-success); }
680
+ .fx-rec__card-top { display: flex; justify-content: space-between; gap: 8px; }
681
+ .fx-rec__label { color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
682
+ .fx-rec__checks { display: flex; flex-wrap: wrap; gap: 4px 12px; }
683
+ @media (max-width: 1100px) {
684
+ .fx-rec { grid-template-columns: minmax(0, 1fr); min-height: 0; }
685
+ .fx-rec__queue { border-right: 0; max-height: none; }
686
+ .fx-rec--open .fx-rec__queue { display: none; }
687
+ .fx-rec:not(.fx-rec--open) .fx-rec__detail { display: none; }
688
+ .fx-rec__back { display: inline; }
689
+ }
690
+ @media (max-width: 820px) {
691
+ .fx-rec__detail { padding: 16px; }
692
+ .fx-rec__table { display: none; }
693
+ .fx-rec__cards { display: flex; flex-direction: column; gap: 12px; }
694
+ .fx-rec__cards .fx-btn, .fx-rec__foot .fx-btn { min-height: 44px; }
695
+ .fx-rec__foot-acts { width: 100%; }
696
+ .fx-rec__foot-acts .fx-btn { flex: 1 1 auto; }
697
+ }
611
698
  .fx-task__custom { margin-top: 24px; }
612
699
  .fx-task__custom h3 { margin: 0 0 4px; color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
613
700
  .fx-task__input { width: 100%; min-width: 0; }
@@ -823,6 +910,30 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
823
910
  .fx-table__actions { text-align: left; }
824
911
  .fx-table__actions .fx-row--end { justify-content: flex-start; }
825
912
  .fx-ph__actions, .fx-ph__actions .fx-btn { width: 100%; }
913
+ /* People: the name with its actions (⋯, Approve) on one line, then the
914
+ role select full width, then the status pill when there is one. */
915
+ .fx-people tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px 10px; }
916
+ .fx-people td, .fx-people td:nth-child(2), .fx-people td:nth-child(3) { display: block; width: auto; margin: 0; padding: 0; }
917
+ .fx-people td:nth-child(1) { grid-area: 1 / 1; }
918
+ .fx-people td:nth-child(4) { grid-area: 1 / 2; }
919
+ .fx-people td:nth-child(2) { grid-area: 2 / 1 / 3 / -1; }
920
+ .fx-people td:nth-child(2) .fx-select { width: 100%; }
921
+ .fx-people td:nth-child(3) { grid-area: 3 / 1 / 4 / -1; }
922
+ .fx-people td:nth-child(3):empty { display: none; }
923
+ /* Jobs list: the tick at the left of each row and group head; the client
924
+ and total on the first line, then each other column on its own line,
925
+ with its name. */
926
+ .fx-table--jobs tr { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: center; gap: 4px 8px; }
927
+ .fx-table--jobs td, .fx-table--jobs td:nth-child(2), .fx-table--jobs td:nth-child(3) { display: block; width: auto; margin: 0; padding: 0; }
928
+ .fx-table--jobs tr > td.fx-tick-cell { grid-column: 1; grid-row: 1; align-self: start; padding-top: 2px; }
929
+ .fx-table--jobs tr.fx-row-link > td:not(.fx-tick-cell) { grid-column: 2 / -1; }
930
+ .fx-table--jobs tr.fx-row-link > td.fx-c-client { grid-area: 1 / 2; }
931
+ .fx-table--jobs tr.fx-row-link > td.fx-c-total { grid-area: 1 / 3; }
932
+ .fx-table--jobs td.fx-c-end { display: none; }
933
+ .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; }
934
+ .fx-table--jobs tr.fx-table__group { grid-template-columns: 28px minmax(0, 1fr); background: var(--t-s1); }
935
+ .fx-table--jobs tr.fx-table__group > td.fx-tick-cell { grid-row: 1; align-self: center; padding: 0; }
936
+ .fx-table--jobs tr.fx-table__group > td { background: none; }
826
937
  }
827
938
 
828
939
  @keyframes fx-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@@ -921,6 +1032,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
921
1032
  .fx-cfm > .fx-notice { margin: 12px 16px 0; }
922
1033
  .fx-cfm__foot { display: flex; justify-content: space-between; gap: 12px; padding: 12px 16px; border-top: 1px solid var(--fx-line-soft); color: var(--t-dim); font-size: 13px; }
923
1034
  .fx-pop__item:disabled { opacity: .5; cursor: default; }
1035
+ .fx-pop__item[data-tone="danger"], .fx-pop__item[data-tone="danger"] .fx-pop__tick { color: var(--t-danger); }
924
1036
  .fx-pop__item .fx-lucide { width: 15px; height: 15px; }
925
1037
  @media (max-width: 720px) {
926
1038
  .fx-cfm { grid-template-columns: minmax(0, 1fr) 40px 40px; }
@@ -1019,3 +1131,105 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1019
1131
  .fx-pick__row[hidden] { display: none; }
1020
1132
  .fx-pick__main { display: grid; gap: 2px; min-width: 0; }
1021
1133
  .fx-pick > .fx-dim { padding: 14px; }
1134
+
1135
+ /* The Invoices list head (invoicing.js): tabs, search and the send bar stay
1136
+ at the top while rows scroll up under them; a shadow shows once they do.
1137
+ Narrow, the app bar (60px) sits above it. */
1138
+ .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; }
1139
+ .fx-listhead[data-stuck] { box-shadow: 0 10px 12px -12px rgba(0, 0, 0, .45); }
1140
+ .fx-listhead .fx-sendbar { margin-bottom: 12px; }
1141
+ .fx-listhead__edge { height: 0; margin-bottom: -20px; }
1142
+ @media (max-width: 820px) { .fx-listhead { top: 60px; } }
1143
+ /* On a phone the head would hide too many rows: it scrolls away. */
1144
+ @media (max-width: 560px) { .fx-listhead { position: static; } }
1145
+
1146
+ /* The bulk action bar (bulk-bar.js): floats at the foot while rows are
1147
+ ticked. "N selected ×", then icon actions in groups, then More. It takes
1148
+ the theme's own surface, line, corners and primary button, like a menu. */
1149
+ .fx-bulk {
1150
+ position: sticky; bottom: 16px; z-index: 6; justify-self: center; display: flex; align-items: center; gap: 2px; max-width: 100%;
1151
+ padding: 5px; border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); color: var(--t-text);
1152
+ box-shadow: var(--t-shadow), 0 12px 32px rgba(0, 0, 0, .16);
1153
+ font: 500 13.5px/1 var(--t-body); white-space: nowrap;
1154
+ }
1155
+ .fx-bulk[hidden] { display: none; }
1156
+ .fx-bulk[aria-busy="true"] { opacity: .7; pointer-events: none; }
1157
+ .fx-bulk__acts { display: flex; align-items: center; gap: 2px; min-width: 0; }
1158
+ .fx-bulk__count { display: inline-flex; align-items: center; gap: 6px; flex: none; margin-right: 6px; padding: 3px 4px 3px 10px; border: 1px solid var(--t-accent-brd, var(--fx-line)); border-radius: var(--t-radius); background: var(--t-accent-bg); color: var(--t-accent); font-weight: 600; }
1159
+ .fx-bulk__clear, .fx-bulk__act { position: relative; display: inline-flex; align-items: center; gap: 6px; padding: 7px 9px; border: 1px solid transparent; border-radius: var(--t-radius); background: none; color: inherit; font: inherit; cursor: pointer; }
1160
+ .fx-bulk__clear { padding: 3px; color: inherit; }
1161
+ .fx-bulk__act:hover, .fx-bulk__act:focus-visible, .fx-bulk__act[aria-expanded="true"] { background: var(--fx-hover); color: var(--t-strong); outline: none; }
1162
+ .fx-bulk__clear:hover, .fx-bulk__clear:focus-visible { background: color-mix(in srgb, var(--t-accent) 14%, transparent); outline: none; }
1163
+ .fx-bulk__act--primary, .fx-bulk__act--primary:hover, .fx-bulk__act--primary:focus-visible { background: var(--t-primary); border-color: var(--t-primary); color: var(--t-on-primary); font-weight: 600; }
1164
+ .fx-bulk__act--primary:hover { background: color-mix(in srgb, var(--t-primary) 86%, var(--t-text)); }
1165
+ .fx-bulk__act--icon { padding: 7px; }
1166
+ .fx-bulk__act[data-tone="danger"] { color: var(--t-danger); }
1167
+ .fx-bulk__act[data-tone="danger"]:hover { background: var(--t-danger-bg); }
1168
+ .fx-bulk__sep { flex: none; width: 1px; height: 18px; margin: 0 4px; background: var(--fx-line); }
1169
+ /* An icon-only action says its name on hover, in a tip above it. */
1170
+ .fx-bulk [data-tip]:hover::after, .fx-bulk [data-tip]:focus-visible::after {
1171
+ content: attr(data-tip); position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%);
1172
+ padding: 4px 8px; border: 1px solid var(--fx-line); border-radius: var(--t-radius); background: var(--t-paper); color: var(--t-strong);
1173
+ font: 500 12px/1.2 var(--t-body); pointer-events: none; box-shadow: var(--t-shadow), 0 4px 12px rgba(0, 0, 0, .12);
1174
+ }
1175
+ /* More: a compact list, one row per action, like a dropdown. */
1176
+ .fx-pop.fx-bulk__more { width: 248px; min-width: 0; padding: 4px; }
1177
+ .fx-bulk__search { width: 100%; height: 30px; margin-bottom: 2px; padding: 4px 8px; font-size: 13px; }
1178
+ .fx-bulk__more .fx-pop__head { padding: 6px 8px 2px; font-size: 10.5px; }
1179
+ .fx-bulk__more .fx-pop__item { gap: 8px; padding: 4px 8px; font-size: 13px; line-height: 1.25; }
1180
+ .fx-bulk__more .fx-pop__item .fx-lucide { width: 14px; height: 14px; }
1181
+ .fx-bulk__grid { display: grid; }
1182
+ .fx-bulk__grid .fx-pop__item[data-tone="danger"] { color: var(--t-danger); }
1183
+ .fx-bulk__none { margin: 8px 10px; color: var(--t-dim); font-size: 13.5px; }
1184
+ @media (max-width: 560px) {
1185
+ .fx-bulk { bottom: 10px; padding: 4px; }
1186
+ .fx-bulk__count { margin-right: 2px; padding-left: 8px; }
1187
+ .fx-bulk__act { padding: 7px 6px; }
1188
+ .fx-bulk__sep { margin: 0 2px; }
1189
+ .fx-bulk__act:not(.fx-bulk__act--primary) .fx-bulk__word { display: none; }
1190
+ }
1191
+
1192
+ /* Xero › Import (server/ui/xero-import.js): Review compares one fact per row
1193
+ with ✓ or ≠ between the sides; the list and history tick rows for the bulk
1194
+ bar. */
1195
+ .fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0; max-width: 980px; }
1196
+ .fx-xi .fx-lucide { width: 16px; height: 16px; }
1197
+ .fx-xi__views { display: inline-flex; border: 1px solid var(--fx-line); }
1198
+ .fx-xi__view { display: inline-grid; place-items: center; width: 40px; height: 36px; border: 0; background: transparent; color: var(--t-dim); cursor: pointer; }
1199
+ .fx-xi__view[aria-selected="true"] { background: var(--t-primary); color: var(--t-on-primary); }
1200
+ .fx-xi__count { display: inline-flex; align-items: center; gap: 10px; font-variant-numeric: tabular-nums; }
1201
+ .fx-xi__progress { display: inline-block; width: 120px; height: 4px; background: var(--fx-line); }
1202
+ .fx-xi__progress > span { display: block; height: 100%; background: var(--t-primary); }
1203
+ .fx-xi__bulk { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 14px; border: 1px dashed var(--t-primary); background: color-mix(in srgb, var(--t-primary) 6%, transparent); color: var(--t-strong); font-size: 13.5px; }
1204
+ .fx-xi__bulk-text { margin-right: auto; }
1205
+ .fx-xi__bulk .fx-btn { display: inline-flex; align-items: center; gap: 6px; }
1206
+ .fx-xi__bulk .fx-xi__search { flex: 1; min-width: 0; margin-right: 0; }
1207
+ .fx-xi-cmp { width: 100%; border-collapse: collapse; border: 1px solid var(--fx-line); background: var(--t-s1); }
1208
+ .fx-xi-cmp th, .fx-xi-cmp td { padding: 11px 16px; border-top: 1px solid var(--fx-line-soft); text-align: left; font-size: 14px; color: var(--t-strong); }
1209
+ .fx-xi-cmp thead th { border-top: 0; padding: 14px 16px; }
1210
+ .fx-xi-cmp tbody th { width: 44px; color: var(--t-dim); }
1211
+ .fx-xi-cmp td:nth-child(2), .fx-xi-cmp th:nth-child(2) { width: 42%; }
1212
+ .fx-xi-cmp__mid { width: 48px; text-align: center !important; }
1213
+ .fx-xi__mark { height: 22px; width: auto; vertical-align: middle; }
1214
+ .fx-xi-cmp thead th:last-child { display: flex; align-items: center; gap: 10px; color: var(--t-dim); }
1215
+ .fx-xi__agree { display: inline-grid; place-items: center; width: 24px; height: 24px; }
1216
+ .fx-xi__agree[data-state="same"] { color: var(--t-success); }
1217
+ .fx-xi__agree[data-state="differs"] { color: var(--t-warn); }
1218
+ .fx-xi__acts { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
1219
+ .fx-xi__acts .fx-btn { display: inline-flex; align-items: center; gap: 6px; }
1220
+ .fx-xi__done { display: flex; align-items: center; gap: 12px; padding: 24px; border: 1px solid var(--fx-line); color: var(--t-success); }
1221
+ .fx-xi__done p { margin: 0; color: var(--t-strong); }
1222
+ .fx-xi__scroll { overflow-x: auto; }
1223
+ .fx-xi-list .fx-xi__mark { height: 18px; }
1224
+ .fx-xi-list__mid { width: 56px; text-align: center; color: var(--t-dim); white-space: nowrap; }
1225
+ .fx-xi-list__mid .fx-dim { margin-left: 4px; font-size: 12px; }
1226
+ .fx-xi-list tr[data-state="linked"] .fx-xi-list__mid { color: var(--t-success); }
1227
+ .fx-xi-list tr[data-state="invoice_fits"] .fx-xi-list__mid a, .fx-xi-list tr[data-state="job_fits"] .fx-xi-list__mid a { color: var(--t-warn); }
1228
+ @media (max-width: 820px) {
1229
+ .fx-xi-cmp th, .fx-xi-cmp td { padding: 10px 8px; font-size: 13px; }
1230
+ .fx-xi-cmp tbody th { width: 28px; }
1231
+ .fx-xi-cmp__mid { width: 28px; }
1232
+ .fx-xi__acts .fx-btn { flex: 1 1 auto; min-height: 44px; justify-content: center; }
1233
+ }
1234
+ .fx-xi-count { font-size: 18px; }
1235
+ .fx-xi-list td.fx-num:not(.fx-strong) { text-align: left; }