@arcforgelabs/flow 2026.10.25 → 2026.10.26

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 +2 -1
  4. package/server/api/invoicing.js +375 -0
  5. package/server/app.js +11 -0
  6. package/server/auth/people.js +26 -0
  7. package/server/auth/routes.js +13 -0
  8. package/server/core/invoices.js +7 -0
  9. package/server/core/ledger.js +6 -3
  10. package/server/core/links.js +3 -3
  11. package/server/core/pipeline.js +488 -0
  12. package/server/db/migrations/0008_people_appearance.sql +1 -0
  13. package/server/db/migrations/0009_pipeline_matcher.sql +46 -0
  14. package/server/db/migrations/meta/0008_snapshot.json +2479 -0
  15. package/server/db/migrations/meta/0009_snapshot.json +2809 -0
  16. package/server/db/migrations/meta/_journal.json +14 -0
  17. package/server/db/schema.js +71 -2
  18. package/server/index.js +9 -0
  19. package/server/integrations/port.js +3 -0
  20. package/server/integrations/seazona/feed.js +11 -0
  21. package/server/integrations/seazona/index.js +7 -1
  22. package/server/integrations/seazona/ledger.js +2 -0
  23. package/server/integrations/xero/import.js +22 -0
  24. package/server/integrations/xero/index.js +10 -2
  25. package/server/integrations/xero/lookup.js +94 -0
  26. package/server/theme/color-contrast.js +39 -2
  27. package/server/theme/contrast-audit.js +63 -0
  28. package/server/theme/themes.js +10 -2
  29. package/server/ui/app-page.js +5 -10
  30. package/server/ui/app.js +351 -105
  31. package/server/ui/appearance.js +3 -2
  32. package/server/ui/clerk-appearance.js +18 -2
  33. package/server/ui/flow.css +246 -22
  34. package/server/ui/invoicing.js +451 -0
  35. package/server/ui/jobs.js +153 -0
  36. package/server/ui/logos/seazona.svg +1 -0
  37. package/server/ui/logos/xero.svg +1 -0
  38. package/server/ui/routes.js +5 -1
@@ -1,7 +1,8 @@
1
1
  // Appearance in the browser: which design system (theme family), light or
2
2
  // dark, and an optional accent colour. The families and their tokens live in
3
- // server/theme/themes.js. The choice is per browser and is a convenience, so
4
- // it lives in localStorage. server/ui/page.js runs the same family and mode
3
+ // server/theme/themes.js. The choice belongs to the person's account
4
+ // (server/ui/app.js saves it); localStorage keeps this browser's copy so a
5
+ // page paints in it before sign-in is known. server/ui/page.js runs the same family and mode
5
6
  // resolution inline before first paint so there is no flash; keep the two in
6
7
  // step. Ported from Arc Forge Labs' Field (client/src/lib/color-mode.ts).
7
8
  import { accentOverrides, normalizeAccent, normalizeFamily, tokensFor } from '/assets/theme/themes.js';
@@ -3,6 +3,14 @@
3
3
  // --t-* tokens at render time, so Clerk changes with the theme and mode.
4
4
  // Ported from Arc Forge Labs' Field (client/src/lib/clerk-appearance.ts).
5
5
 
6
+ // Clerk's "Manage account" cog has no hub, so at 16px it reads as a blob.
7
+ // This gear has the same 1.5 stroke as Clerk's sign-out icon beside it.
8
+ const GEAR = `url("data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linejoin="round"><path d="M7.06 3.45L7.20 2.05L8.80 2.05L8.94 3.45L10.55 4.11L11.64 3.23L12.77 4.36L11.89 5.45L12.55 7.06L13.95 7.20L13.95 8.80L12.55 8.94L11.89 10.55L12.77 11.64L11.64 12.77L10.55 11.89L8.94 12.55L8.80 13.95L7.20 13.95L7.06 12.55L5.45 11.89L4.36 12.77L3.23 11.64L4.11 10.55L3.45 8.94L2.05 8.80L2.05 7.20L3.45 7.06L4.11 5.45L3.23 4.36L4.36 3.23L5.45 4.11Z"/><circle cx="8" cy="8" r="2"/></svg>')}")`;
9
+ const gearIcon = {
10
+ '&&': { background: 'currentColor', mask: `${GEAR} center / contain no-repeat`, WebkitMask: `${GEAR} center / contain no-repeat` },
11
+ '&& > *': { visibility: 'hidden' },
12
+ };
13
+
6
14
  function token(name, fallback) {
7
15
  const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
8
16
  return value || fallback;
@@ -33,13 +41,14 @@ export function flowClerkAppearance({ embedded = false, publicSignUp = false } =
33
41
  const danger = token('--t-danger', '#b8442a');
34
42
  const inputEdge = { ...controlEdge, '&&[data-feedback="error"]': { boxShadow: edge(danger) } };
35
43
  const display = token('--t-display', 'ui-serif, Georgia, serif');
44
+ const dim = token('--t-dim', '#5b6773');
36
45
  return {
37
46
  variables: {
38
47
  colorPrimary: token('--t-primary', '#1f6f8f'),
39
48
  colorPrimaryForeground: token('--t-on-primary', '#ffffff'),
40
49
  colorBackground: token('--t-paper', '#ffffff'),
41
50
  colorForeground: text,
42
- colorMutedForeground: token('--t-dim', '#5b6773'),
51
+ colorMutedForeground: dim,
43
52
  colorMuted: token('--t-s1', '#f7f9fb'),
44
53
  colorInput: token('--t-paper', '#ffffff'),
45
54
  colorInputForeground: text,
@@ -47,7 +56,10 @@ export function flowClerkAppearance({ embedded = false, publicSignUp = false } =
47
56
  colorSuccess: token('--t-success', '#3f6b2a'),
48
57
  colorWarning: token('--t-warn', '#8a5a00'),
49
58
  colorNeutral: text,
50
- colorBorder: line,
59
+ // Clerk builds its border shades (dividers, hover edges, the badge text)
60
+ // as fractions of colorBorder, so it takes the opaque text colour: a
61
+ // colour already faded to 12% left the "Primary" badge near 1:1.
62
+ colorBorder: text,
51
63
  colorRing: token('--t-ring', '#1f6f8f'),
52
64
  fontFamily: token('--t-body', 'ui-sans-serif, system-ui, sans-serif'),
53
65
  fontSize: '14px',
@@ -67,9 +79,13 @@ export function flowClerkAppearance({ embedded = false, publicSignUp = false } =
67
79
  scrollBox: embedded ? { borderRadius: 0 } : undefined,
68
80
  formFieldInput: inputEdge,
69
81
  otpCodeFieldInput: inputEdge,
82
+ // Clerk's default badge ("Primary" by an email address) draws its text
83
+ // in a 62% border shade, under 4.5:1 on light papers; --t-dim reads.
84
+ badge: { '&&': { color: dim, background: `color-mix(in srgb, ${text} 5%, transparent)`, borderColor: line } },
70
85
  formButtonPrimary: { boxShadow: 'none', textTransform: 'none', fontWeight: 650 },
71
86
  socialButtonsBlockButton: controlEdge,
72
87
  userButtonPopoverCard: { border: `1px solid ${line}`, boxShadow: shadow },
88
+ userButtonPopoverActionButtonIcon__manageAccount: gearIcon,
73
89
  footer: embedded ? { display: 'none' } : undefined,
74
90
  },
75
91
  };
@@ -106,25 +106,32 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
106
106
  }
107
107
  .fx-panel { overflow: hidden; }
108
108
 
109
- .fx-shell { flex-direction: column; min-height: 100vh; }
110
- .fx-top {
109
+ /* The signed-in app: a standard left sidebar, then the view. */
110
+ .fx-shell { min-height: 100vh; }
111
+ .fx-side {
111
112
  position: sticky; top: 0; z-index: 10;
112
- display: flex; align-items: center; gap: 24px;
113
- height: 60px; padding: 0 max(16px, calc((100vw - 1120px) / 2));
114
- background: color-mix(in srgb, var(--t-bg) 92%, transparent);
115
- border-bottom: 1px solid var(--fx-line);
116
- backdrop-filter: saturate(1.2) blur(8px);
113
+ display: flex; flex-direction: column; gap: 4px; flex: none;
114
+ width: 232px; height: 100vh; padding: 16px 12px 12px;
115
+ background: var(--t-s1); border-right: 1px solid var(--fx-line);
117
116
  }
118
- .fx-nav { display: flex; gap: 4px; flex: 1; overflow-x: auto; }
117
+ .fx-side > .fx-brand { padding: 4px 8px 18px; font-size: 19px; }
118
+ .fx-side > .fx-brand .fx-mark { width: 26px; height: 26px; }
119
+ .fx-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; overflow-y: auto; }
120
+ .fx-nav__group { padding: 16px 10px 6px; color: var(--t-dim); font: var(--t-label-weight) var(--t-label-size)/1.3 var(--t-body); letter-spacing: var(--t-label-spacing); text-transform: var(--t-label-case); }
121
+ .fx-nav__group:first-child { padding-top: 4px; }
119
122
  .fx-nav a {
120
- padding: 7px 12px; border-radius: var(--t-radius);
121
- color: var(--t-dim); font-weight: 600; font-size: 14px; text-decoration: none; white-space: nowrap;
123
+ display: flex; align-items: center; gap: 10px;
124
+ padding: 8px 10px; border-radius: var(--t-radius);
125
+ color: var(--t-text); font-weight: 500; font-size: 14px; text-decoration: none; white-space: nowrap;
122
126
  }
123
127
  .fx-nav a:hover { color: var(--t-strong); background: var(--fx-hover); }
124
- .fx-nav a[aria-current="page"] { color: var(--t-strong); background: var(--t-accent-bg); box-shadow: inset 0 0 0 1px var(--t-accent-brd); }
125
- .fx-top__end { display: flex; align-items: center; gap: 6px; }
126
- .fx-top__user { display: flex; min-width: 32px; min-height: 32px; align-items: center; }
127
- .fx-main { width: 100%; max-width: 1120px; margin: 0 auto; padding: 32px 16px 64px; display: grid; gap: 20px; align-content: start; }
128
+ .fx-nav a[aria-current="page"] { color: var(--t-accent); background: var(--t-accent-bg); font-weight: 650; box-shadow: inset 2px 0 0 var(--t-brand); }
129
+ .fx-nav__count { margin-left: auto; color: var(--t-dim); font-size: 12px; font-weight: 700; }
130
+ .fx-ico { display: inline-flex; flex: none; }
131
+ .fx-ico svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
132
+ .fx-side__end { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px 6px 0; border-top: 1px solid var(--fx-line); }
133
+ .fx-side__user { display: flex; min-width: 32px; min-height: 32px; align-items: center; }
134
+ .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; }
128
135
  .fx-main:focus { outline: none; box-shadow: none; }
129
136
 
130
137
  /* Page header: title and one line, then the actions, primary last. */
@@ -156,6 +163,7 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
156
163
  .fx-status[data-tone="success"] { color: var(--t-success); background: var(--t-success-bg); }
157
164
  .fx-status[data-tone="warn"] { color: var(--t-warn); background: var(--t-warn-bg); }
158
165
  .fx-status[data-tone="mute"] { color: var(--t-dim); background: var(--fx-hover); }
166
+ .fx-status[data-tone="danger"] { color: var(--t-danger); background: var(--t-danger-bg); }
159
167
  .fx-tag {
160
168
  margin-left: 8px; padding: 1px 7px; border: 1px solid var(--fx-line); border-radius: 999px;
161
169
  color: var(--t-dim); font-size: 11.5px; font-weight: 600;
@@ -195,6 +203,196 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
195
203
  .fx-roles dt { color: var(--t-strong); font-weight: 600; }
196
204
  .fx-roles dd { margin: 0; }
197
205
 
206
+ /* ── Integrations ───────────────────────────────────────────────────────── */
207
+ .fx-chip { border: 1px solid var(--fx-line); border-radius: 999px; background: var(--t-paper); color: var(--t-text); }
208
+ .fx-int-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
209
+ .fx-int-bar .fx-search { margin: 0; }
210
+ .fx-int-sections { display: grid; gap: 32px; }
211
+ .fx-int-section-list { display: grid; gap: 16px; }
212
+ .fx-int-heading { color: var(--t-strong); font: 600 19px/1.3 var(--t-body); }
213
+ .fx-int-heading-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
214
+ .fx-int-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
215
+ .fx-int-card {
216
+ display: grid; grid-template-rows: auto 1fr auto; gap: 14px; padding: 18px;
217
+ border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper);
218
+ color: var(--t-text); text-decoration: none;
219
+ transition: transform 140ms var(--fx-ease), box-shadow 140ms var(--fx-ease), border-color 140ms var(--fx-ease);
220
+ }
221
+ .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); }
222
+ .fx-int-card__head { display: flex; align-items: center; gap: 12px; }
223
+ .fx-int-card__head h3 { flex: 1; color: var(--t-strong); font: 650 16px/1.25 var(--t-body); }
224
+ .fx-int-card__blurb { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 2.9em; color: var(--t-text); font-size: 14px; line-height: 1.45; }
225
+ .fx-int-card__btn {
226
+ display: flex; align-items: center; justify-content: center; min-height: 34px;
227
+ border: 1px solid var(--t-control); border-radius: var(--t-radius); color: var(--t-strong); font-weight: 600; font-size: 14px;
228
+ transition: background-color 140ms var(--fx-ease), color 140ms var(--fx-ease), border-color 140ms var(--fx-ease);
229
+ }
230
+ .fx-int-card:hover .fx-int-card__btn, .fx-int-card:focus-visible .fx-int-card__btn { background: var(--t-primary); border-color: var(--t-primary); color: var(--t-on-primary); }
231
+ .fx-logo {
232
+ display: grid; place-items: center; flex: none; width: 44px; height: 44px; border-radius: 10px;
233
+ background: #fff; box-shadow: inset 0 0 0 1px var(--fx-line);
234
+ }
235
+ .fx-logo img { width: 28px; height: 28px; object-fit: contain; }
236
+ .fx-logo--lg { width: 52px; height: 52px; }
237
+ .fx-logo--lg img { width: 34px; height: 34px; }
238
+
239
+ .fx-logo--xl { width: 112px; height: 112px; border-radius: 22px; }
240
+ .fx-logo--xl img { width: 68px; height: 68px; }
241
+ .fx-chip--static { display: inline-flex; align-items: center; min-height: 26px; padding: 0 10px; font-size: 12.5px; cursor: default; }
242
+
243
+ /* One integration's page: hero, then the setup beside its facts. */
244
+ .fx-int-page { display: grid; gap: 24px; }
245
+ .fx-int-hero { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px; }
246
+ .fx-int-hero__text { display: grid; gap: 8px; flex: 1; min-width: 260px; }
247
+ .fx-int-hero__text .fx-title { font-size: 34px; }
248
+ .fx-int-hero__maker { display: flex; gap: 10px; align-items: baseline; color: var(--t-strong); font-weight: 650; }
249
+ .fx-int-hero__blurb { max-width: 64ch; color: var(--t-text); font-size: 15.5px; }
250
+ .fx-int-hero__actions { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 6px; }
251
+ .fx-int-page__grid { display: grid; grid-template-columns: minmax(0, 1fr) 240px; gap: 32px; align-items: start; }
252
+ .fx-int-page__main { display: grid; gap: 28px; }
253
+ .fx-int-section { display: grid; gap: 14px; padding-top: 20px; border-top: 1px solid var(--fx-line); }
254
+ .fx-int-section__title { color: var(--t-strong); font: 650 17px/1.3 var(--t-body); }
255
+ .fx-int-section .fx-btn { justify-self: start; }
256
+ .fx-steps { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; counter-reset: step; }
257
+ .fx-steps li { position: relative; padding-left: 38px; min-height: 26px; counter-increment: step; font-size: 14.5px; line-height: 1.55; }
258
+ .fx-steps li::before {
259
+ content: counter(step); position: absolute; left: 0; top: 0; display: grid; place-items: center; width: 26px; height: 26px;
260
+ border-radius: 50%; background: var(--t-accent-bg); color: var(--t-accent); box-shadow: inset 0 0 0 1px var(--t-accent-brd); font-size: 12.5px; font-weight: 700;
261
+ }
262
+ .fx-int-page__meta { position: sticky; top: 24px; }
263
+ .fx-meta { display: grid; gap: 0; margin: 0; }
264
+ .fx-meta > div { display: grid; gap: 6px; padding: 14px 0; border-bottom: 1px solid var(--fx-line-soft); }
265
+ .fx-meta > div:first-child { padding-top: 0; }
266
+ .fx-meta dt { color: var(--t-dim); font-size: 12.5px; font-weight: 600; }
267
+ .fx-meta dd { margin: 0; color: var(--t-strong); font-size: 14px; justify-self: start; }
268
+ .fx-progress {
269
+ position: absolute; z-index: 15; top: calc(100% + 8px); left: 0; display: grid; gap: 10px; width: min(360px, 90vw); padding: 14px 16px;
270
+ border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); box-shadow: 0 16px 40px rgba(0, 0, 0, .18);
271
+ animation: fx-rise 140ms var(--fx-ease);
272
+ }
273
+ .fx-progress__head { display: flex; justify-content: space-between; }
274
+ .fx-progress ol { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; font-size: 14px; }
275
+ .fx-progress li { display: flex; align-items: center; gap: 10px; color: var(--t-strong); }
276
+ .fx-progress li[data-state="waiting"] { color: var(--t-dim); }
277
+ .fx-progress li[data-state="failed"] { color: var(--t-danger); }
278
+ .fx-progress__mark { display: grid; place-items: center; flex: none; width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--fx-line); color: #fff; font-size: 12px; font-weight: 800; }
279
+ .fx-progress li[data-state="done"] .fx-progress__mark { background: var(--t-success); border-color: var(--t-success); }
280
+ .fx-progress li[data-state="failed"] .fx-progress__mark { background: var(--t-danger); border-color: var(--t-danger); }
281
+ .fx-progress li[data-state="running"] .fx-progress__mark { border-color: var(--fx-line); border-top-color: var(--t-brand); animation: fx-spin 800ms linear infinite; }
282
+ .fx-spinner--sm { width: 14px; height: 14px; border-width: 2px; border-color: color-mix(in srgb, currentColor 35%, transparent); border-top-color: currentColor; }
283
+ @media (max-width: 900px) {
284
+ .fx-int-page__grid { grid-template-columns: minmax(0, 1fr); }
285
+ .fx-int-page__meta { position: static; }
286
+ .fx-logo--xl { width: 72px; height: 72px; border-radius: 16px; }
287
+ .fx-logo--xl img { width: 44px; height: 44px; }
288
+ }
289
+ .fx-drawer .fx-select { width: 100%; }
290
+
291
+ /* ── Invoices ───────────────────────────────────────────────────────────── */
292
+ .fx-num { text-align: right; font-family: var(--t-data, var(--t-body)); font-variant-numeric: tabular-nums; }
293
+ .fx-strong { color: var(--t-strong); font-weight: 600; }
294
+ a.fx-strong { text-decoration: none; }
295
+ a.fx-strong:hover { text-decoration: underline; }
296
+ .fx-toolbar { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--fx-line); }
297
+ .fx-tabs { display: flex; gap: 2px; }
298
+ .fx-tabs button {
299
+ display: inline-flex; align-items: center; gap: 8px; padding: 10px 14px; margin-bottom: -1px;
300
+ border: 0; border-bottom: 2px solid transparent; background: transparent; cursor: pointer;
301
+ color: var(--t-dim); font: 600 14px var(--t-body);
302
+ }
303
+ .fx-tabs button:hover { color: var(--t-strong); }
304
+ .fx-tabs button[aria-selected="true"] { color: var(--t-strong); border-bottom-color: var(--t-brand); }
305
+ .fx-tabs__count { padding: 0 7px; border-radius: 999px; background: var(--fx-hover); color: var(--t-dim); font-size: 12px; line-height: 20px; }
306
+ .fx-search { width: min(320px, 100%); margin-bottom: 8px; }
307
+ .fx-table--rows tbody tr { cursor: pointer; }
308
+ .fx-table th.fx-num { text-align: right; }
309
+
310
+ .fx-crumbs { display: flex; gap: 8px; color: var(--t-dim); font-size: 13.5px; }
311
+ .fx-crumbs a { color: var(--t-dim); text-decoration: none; }
312
+ .fx-crumbs a:hover { color: var(--t-strong); text-decoration: underline; }
313
+ .fx-invoice { display: grid; gap: 20px; }
314
+ .fx-invoice__grid { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 20px; align-items: start; }
315
+ .fx-invoice__main, .fx-invoice__aside { display: grid; gap: 16px; }
316
+ .fx-invoice__aside:empty { display: none; }
317
+ .fx-invoice__grid:has(.fx-invoice__aside:empty) { grid-template-columns: minmax(0, 1fr); }
318
+ .fx-invoice__head { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 16px; padding: 18px 20px; }
319
+ .fx-side-card { display: grid; gap: 10px; padding: 16px; }
320
+ .fx-facts { display: grid; grid-template-columns: max-content 1fr; gap: 6px 12px; margin: 0; font-size: 13.5px; }
321
+ .fx-facts dt { color: var(--t-dim); }
322
+ .fx-facts dd { margin: 0; color: var(--t-strong); overflow-wrap: anywhere; }
323
+ .fx-timeline { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; font-size: 13.5px; }
324
+ .fx-timeline li { display: grid; gap: 1px; padding-left: 14px; border-left: 2px solid var(--t-brand); }
325
+
326
+ .fx-suggest-host { position: relative; }
327
+ .fx-suggest {
328
+ position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; right: 0; display: grid; padding: 4px; max-height: 280px; overflow-y: auto;
329
+ border: 1px solid var(--fx-line); border-radius: var(--t-radius); background: var(--t-paper); box-shadow: 0 12px 32px rgba(0, 0, 0, .16);
330
+ }
331
+ .fx-suggest button { display: grid; gap: 1px; padding: 8px 10px; border: 0; border-radius: var(--t-radius); background: transparent; color: var(--t-strong); text-align: left; font: 500 14px var(--t-body); cursor: pointer; }
332
+ .fx-suggest button:hover { background: var(--fx-hover); }
333
+ .fx-suggest__none { padding: 8px 10px; }
334
+
335
+ .fx-lines { overflow: visible; }
336
+ .fx-lines__table { width: 100%; border-collapse: collapse; font-size: 14px; }
337
+ .fx-lines__table th {
338
+ padding: 10px 8px; text-align: left; border-bottom: 1px solid var(--fx-line); background: var(--t-s1);
339
+ color: var(--t-dim); font: var(--t-label-weight) var(--t-label-size)/1.3 var(--t-body); letter-spacing: var(--t-label-spacing); text-transform: var(--t-label-case);
340
+ }
341
+ .fx-lines__table th:first-child, .fx-lines__table td:first-child { padding-left: 16px; }
342
+ .fx-lines__table th:last-child, .fx-lines__table td:last-child { padding-right: 16px; }
343
+ .fx-lines__table th.fx-num { text-align: right; }
344
+ .fx-lines__table td { padding: 8px; border-bottom: 1px solid var(--fx-line-soft); vertical-align: middle; }
345
+ .fx-lines__table .fx-input, .fx-lines__table .fx-select { min-height: 34px; }
346
+ .fx-lines__table .fx-select { width: 100%; min-width: 0; }
347
+ .fx-lines__item { width: 130px; }
348
+ .fx-lines__qty { width: 76px; }
349
+ .fx-lines__price { width: 112px; }
350
+ .fx-lines__gst { width: 116px; }
351
+ .fx-lines__table--read td { padding-top: 12px; padding-bottom: 12px; }
352
+ .fx-lines__foot { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: start; gap: 16px; padding: 12px 16px 18px; }
353
+ .fx-totals { display: grid; grid-template-columns: auto minmax(120px, auto); gap: 6px 24px; margin: 0; font-size: 14px; }
354
+ .fx-totals dt { color: var(--t-dim); }
355
+ .fx-totals dd { margin: 0; text-align: right; font-family: var(--t-data, var(--t-body)); font-variant-numeric: tabular-nums; color: var(--t-strong); }
356
+ .fx-totals__grand { padding-top: 8px; border-top: 1px solid var(--fx-line); font-size: 17px; font-weight: 700; color: var(--t-strong) !important; }
357
+ .fx-doc__meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin: 0; padding: 20px; border-bottom: 1px solid var(--fx-line-soft); }
358
+ .fx-doc__meta dd { display: grid; margin: 4px 0 0; color: var(--t-strong); font-weight: 600; }
359
+ @media (max-width: 1320px) {
360
+ .fx-invoice__grid { grid-template-columns: minmax(0, 1fr); }
361
+ .fx-invoice__aside { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
362
+ }
363
+ @media (max-width: 960px) {
364
+ .fx-invoice__head, .fx-doc__meta { grid-template-columns: 1fr 1fr; }
365
+ .fx-lines { overflow-x: auto; }
366
+ .fx-lines__table { min-width: 680px; }
367
+ }
368
+
369
+ /* ── Jobs board ─────────────────────────────────────────────────────────── */
370
+ .fx-main--wide { max-width: none; }
371
+ .fx-main > * { min-width: 0; }
372
+ .fx-toolbar--plain { border-bottom: 0; align-items: center; }
373
+ .fx-toolbar--plain .fx-search { margin-bottom: 0; }
374
+ .fx-empty .fx-btn { margin-top: 12px; }
375
+ .fx-panel.fx-empty { display: grid; justify-items: center; }
376
+ .fx-board { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(240px, 1fr); gap: 12px; overflow-x: auto; padding-bottom: 8px; align-items: start; }
377
+ .fx-board__col { display: grid; gap: 8px; align-content: start; min-height: 240px; padding: 10px; border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-s1); }
378
+ .fx-board__head { display: flex; align-items: center; gap: 8px; padding: 2px 4px 4px; font-size: 14px; }
379
+ .fx-board__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--t-accent); flex: none; }
380
+ .fx-board__dot[data-tone="warn"] { background: var(--t-warn); }
381
+ .fx-board__dot[data-tone="success"] { background: var(--t-success); }
382
+ .fx-board__dot[data-tone="mute"] { background: var(--t-dim); }
383
+ .fx-board__count { padding: 0 7px; border-radius: 999px; background: var(--fx-hover); color: var(--t-dim); font-size: 12px; font-weight: 700; line-height: 20px; }
384
+ .fx-board__sum { margin-left: auto; color: var(--t-dim); font-size: 12.5px; font-variant-numeric: tabular-nums; }
385
+ .fx-board__cards { display: grid; gap: 8px; }
386
+ .fx-board__none { padding: 12px 4px; color: var(--t-dim); font-size: 13px; }
387
+ .fx-job { display: grid; gap: 4px; padding: 10px 12px; border: 1px solid var(--fx-line); border-radius: var(--t-radius); background: var(--t-paper); box-shadow: var(--t-shadow); font-size: 13.5px; }
388
+ .fx-job--flag { border-left: 3px solid var(--t-warn); }
389
+ .fx-job__top { display: flex; align-items: baseline; gap: 8px; }
390
+ .fx-job__amount { margin-left: auto; color: var(--t-strong); font-weight: 600; font-family: var(--t-data, var(--t-body)); font-variant-numeric: tabular-nums; }
391
+ .fx-job__client { color: var(--t-text); }
392
+ .fx-job__meta { display: flex; flex-wrap: wrap; gap: 4px 10px; color: var(--t-dim); font-size: 12.5px; }
393
+ .fx-job__why { color: var(--t-warn); font-size: 12.5px; }
394
+ .fx-job .fx-btn { justify-self: start; margin-top: 4px; }
395
+
198
396
  /* ── Dialog ─────────────────────────────────────────────────────────────── */
199
397
  .fx-dialog {
200
398
  width: min(440px, calc(100vw - 32px)); padding: 0;
@@ -222,16 +420,12 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
222
420
  [data-mode="light"] .fx-auth__brand .fx-mark rect { fill: var(--t-on-primary); }
223
421
  [data-mode="light"] .fx-auth__brand .fx-mark path { stroke: var(--t-primary); }
224
422
  .fx-auth__title { max-width: 16ch; font: var(--t-display-weight) clamp(30px, 3.6vw, 46px)/1.08 var(--t-display); letter-spacing: -0.02em; }
225
- .fx-auth__points { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; font-size: 16px; opacity: .88; }
226
- .fx-auth__points li { display: flex; gap: 12px; align-items: baseline; }
227
- .fx-auth__points li::before { content: ""; flex: none; width: 18px; height: 2px; border-radius: 2px; background: currentColor; transform: translateY(-4px); opacity: .7; }
228
423
  .fx-auth__form { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; padding: 40px 16px; }
229
424
  .fx-auth__note { color: var(--t-dim); text-align: center; }
230
425
  .fx-auth__widget { display: flex; justify-content: center; min-height: 360px; width: 100%; }
231
426
  @media (max-width: 820px) {
232
427
  .fx-auth { grid-template-columns: 1fr; }
233
428
  .fx-auth__brand { gap: 16px; padding: 28px 16px; }
234
- .fx-auth__points { display: none; }
235
429
  .fx-auth__title { font-size: 26px; max-width: none; }
236
430
  }
237
431
 
@@ -254,7 +448,27 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
254
448
 
255
449
  /* ── Appearance ─────────────────────────────────────────────────────────── */
256
450
  .fx-appearance { display: grid; gap: 28px; }
257
- .fx-group { display: grid; gap: 12px; }
451
+
452
+ /* ── Settings rows: name, one line under it, the control on the right ──── */
453
+ .fx-settings { display: grid; }
454
+ .fx-setting { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 32px; align-items: center; padding: 14px 0; }
455
+ .fx-setting + .fx-setting { border-top: 1px solid var(--fx-line-soft); }
456
+ .fx-setting__text { display: grid; gap: 3px; min-width: 0; }
457
+ .fx-setting__name { color: var(--t-strong); font: 500 14.5px/1.35 var(--t-body); }
458
+ .fx-setting__hint { margin: 0; color: var(--t-dim); font-size: 13px; line-height: 1.45; }
459
+ .fx-setting__control { display: flex; justify-content: flex-end; min-width: 0; }
460
+ .fx-setting__control > .fx-input, .fx-setting__control > .fx-select, .fx-setting__stack { width: 320px; max-width: 100%; }
461
+ .fx-setting__control .fx-swatches { flex-wrap: nowrap; }
462
+ .fx-setting__stack { display: grid; gap: 6px; justify-items: end; }
463
+ .fx-setting__value { font-size: 12.5px; color: var(--t-strong); overflow-wrap: anywhere; text-align: right; }
464
+ .fx-setting--wide { grid-template-columns: minmax(0, 1fr); }
465
+ .fx-setting--wide .fx-setting__control { display: block; }
466
+ @media (max-width: 640px) {
467
+ .fx-setting { grid-template-columns: minmax(0, 1fr); }
468
+ .fx-setting__control { justify-content: flex-start; }
469
+ .fx-setting__stack { justify-items: start; }
470
+ .fx-setting__control .fx-swatches { flex-wrap: wrap; }
471
+ }
258
472
  .fx-seg { display: inline-flex; justify-self: start; padding: 3px; gap: 2px; border-radius: calc(var(--t-radius) + 3px); background: var(--t-s2); }
259
473
  .fx-seg__item {
260
474
  min-height: 32px; padding: 0 16px; border: 0; border-radius: var(--t-radius);
@@ -288,11 +502,21 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
288
502
  .fx-swatch-custom { display: inline-flex; align-items: center; gap: 8px; color: var(--t-dim); font-size: 13px; font-weight: 600; }
289
503
  .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; }
290
504
 
505
+ /* ── Narrow screens: the sidebar becomes a bar across the top. ──────────── */
506
+ @media (max-width: 820px) {
507
+ .fx-shell { flex-direction: column; }
508
+ .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); }
509
+ .fx-side > .fx-brand { padding: 0; }
510
+ .fx-side > .fx-brand span { display: none; }
511
+ .fx-nav { flex-direction: row; overflow-x: auto; overflow-y: hidden; }
512
+ .fx-nav__group { display: none; }
513
+ .fx-nav a { padding: 7px 9px; }
514
+ .fx-nav a .fx-ico { display: none; }
515
+ .fx-side__end { padding: 0; border-top: 0; }
516
+ }
517
+
291
518
  /* ── Phones ─────────────────────────────────────────────────────────────── */
292
519
  @media (max-width: 640px) {
293
- .fx-top { gap: 12px; }
294
- .fx-top .fx-brand span { display: none; }
295
- .fx-nav a { padding: 7px 9px; }
296
520
  .fx-main { padding-top: 20px; }
297
521
  .fx-title { font-size: 24px; }
298
522
  .fx-table thead { display: none; }