@visns-studio/visns-components 6.6.3 → 6.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +1658 -18
  2. package/package.json +10 -4
  3. package/src/components/DataGrid.jsx +619 -158
  4. package/src/components/Fetch.jsx +80 -3
  5. package/src/components/Form.jsx +9 -0
  6. package/src/components/Navigation.jsx +109 -50
  7. package/src/components/Notification.jsx +279 -9
  8. package/src/components/TableFilter.jsx +14 -1
  9. package/src/components/auth/AuthBrandPanel.jsx +43 -0
  10. package/src/components/auth/AuthLoading.jsx +78 -0
  11. package/src/components/auth/AuthShell.jsx +59 -0
  12. package/src/components/auth/ClientAuth.jsx +161 -0
  13. package/src/components/auth/ClientAuthFrame.jsx +59 -0
  14. package/src/components/auth/ClientLogin.jsx +266 -55
  15. package/src/components/auth/ClientOTPVerify.jsx +587 -115
  16. package/src/components/auth/ImpersonateGate.jsx +254 -0
  17. package/src/components/auth/Login.jsx +134 -41
  18. package/src/components/auth/LogoutScreen.jsx +112 -0
  19. package/src/components/auth/Reset.jsx +134 -77
  20. package/src/components/auth/TwoFactorAuth.jsx +475 -297
  21. package/src/components/auth/Verify.jsx +237 -126
  22. package/src/components/auth/authEndpoints.js +105 -0
  23. package/src/components/auth/authFont.js +23 -0
  24. package/src/components/auth/authHelpers.js +465 -0
  25. package/src/components/auth/clientAuthProtocols.js +240 -0
  26. package/src/components/auth/useOptionalRouter.js +49 -0
  27. package/src/components/callQueue/CallQueuePop.jsx +1502 -0
  28. package/src/components/callQueue/CallQueueSettings.jsx +508 -0
  29. package/src/components/callQueue/callQueueHelpers.js +280 -0
  30. package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
  31. package/src/components/columns/AutoGrowCell.jsx +141 -0
  32. package/src/components/columns/ColumnRenderers.jsx +53 -7
  33. package/src/components/generic/GenericAuth.jsx +163 -96
  34. package/src/components/generic/GenericDetail.jsx +215 -90
  35. package/src/components/generic/GenericIndex.jsx +20 -47
  36. package/src/components/generic/GenericMain.jsx +5 -0
  37. package/src/components/generic/GroupedReportRenderer.jsx +1 -5
  38. package/src/components/generic/StandardModal.jsx +17 -5
  39. package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
  40. package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
  41. package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
  42. package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
  43. package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
  44. package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
  45. package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
  46. package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
  47. package/src/components/navActive.js +60 -0
  48. package/src/components/notify/desktopNotifications.js +256 -0
  49. package/src/components/sketch/SketchField.jsx +12 -2
  50. package/src/components/sms/SmsComposeModal.jsx +495 -0
  51. package/src/components/sms/SmsInbox.jsx +596 -0
  52. package/src/components/sms/SmsInboxBadge.jsx +468 -0
  53. package/src/components/sms/SmsLineSettings.jsx +854 -0
  54. package/src/components/sms/SmsThreadPanel.jsx +1184 -0
  55. package/src/components/sms/smsEndpoints.js +56 -0
  56. package/src/components/sms/smsHelpers.js +1075 -0
  57. package/src/components/sms/smsLiveState.js +132 -0
  58. package/src/components/sms/useSmsLive.js +307 -0
  59. package/src/components/styles/CallQueuePop.module.scss +646 -0
  60. package/src/components/styles/CallQueueSettings.module.scss +460 -0
  61. package/src/components/styles/ClientAuth.module.scss +229 -0
  62. package/src/components/styles/DataGrid.module.scss +10 -0
  63. package/src/components/styles/GenericDetail.module.scss +153 -26
  64. package/src/components/styles/GenericIndex.module.scss +13 -0
  65. package/src/components/styles/GenericMain.module.scss +19 -2
  66. package/src/components/styles/ImpersonateGate.module.scss +85 -0
  67. package/src/components/styles/Login.module.scss +13 -122
  68. package/src/components/styles/LogoutScreen.module.scss +89 -0
  69. package/src/components/styles/Navigation.module.scss +509 -223
  70. package/src/components/styles/Notification.module.scss +103 -0
  71. package/src/components/styles/Reset.module.scss +52 -186
  72. package/src/components/styles/Sms.module.scss +2382 -0
  73. package/src/components/styles/TableFilter.module.scss +118 -98
  74. package/src/components/styles/TwoFactorAuth.module.scss +115 -176
  75. package/src/components/styles/Vault.module.scss +1983 -0
  76. package/src/components/styles/Verify.module.scss +57 -180
  77. package/src/components/styles/_authBrandPanel.scss +163 -0
  78. package/src/components/styles/_authShell.scss +369 -0
  79. package/src/components/styles/global-datagrid.css +29 -0
  80. package/src/components/utils/buildEnv.js +67 -0
  81. package/src/components/utils/displayValue.js +94 -0
  82. package/src/components/utils/useDensity.js +345 -9
  83. package/src/components/vault/OtpChip.jsx +208 -0
  84. package/src/components/vault/PasswordGenerator.jsx +145 -0
  85. package/src/components/vault/QrScanner.jsx +326 -0
  86. package/src/components/vault/VaultAccessLog.jsx +225 -0
  87. package/src/components/vault/VaultConfirmPanel.jsx +113 -0
  88. package/src/components/vault/VaultEntryForm.jsx +740 -0
  89. package/src/components/vault/VaultManager.jsx +1000 -0
  90. package/src/components/vault/VaultQuickSearch.jsx +681 -0
  91. package/src/components/vault/qrDecode.js +257 -0
  92. package/src/components/vault/useDebouncedValue.js +22 -0
  93. package/src/components/vault/useVaultReveal.js +160 -0
  94. package/src/components/vault/vaultClipboard.js +33 -0
  95. package/src/components/vault/vaultEndpoints.js +40 -0
  96. package/src/components/vault/vaultFit.js +116 -0
  97. package/src/components/vault/vaultHelpers.js +547 -0
  98. package/src/components/vault/vaultNavigation.jsx +58 -0
  99. package/src/components/vault/vaultOtp.js +105 -0
  100. package/src/index.js +233 -1
@@ -0,0 +1,2382 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Messaging (components/sms/*)
3
+ // ---------------------------------------------------------------------------
4
+ // The SMS inbox: a popover in the navigation's action pill (SmsInboxBadge),
5
+ // the two-pane page behind it (SmsInbox), the conversation pane on its own
6
+ // (SmsThreadPanel), the compose modal, and the settings screen.
7
+ //
8
+ // Every brand value is a CSS custom property with a neutral fallback, exactly
9
+ // as in Vault.module.scss, so the library carries no palette of its own.
10
+ //
11
+ // IMPORTANT — why every control below restates its colour, background,
12
+ // border, font-size AND box size:
13
+ //
14
+ // The popover mounts INSIDE `.hactions > ul`, whose chip rules reach every
15
+ // descendant `a` and `button` and hand it white text, a fixed 36px box and a
16
+ // pill radius. On a white panel that renders as blank boxes. Navigation.module
17
+ // .scss excludes `[data-nav-overlay] *` from those rules, which is the real
18
+ // fix, but a component that survives only because its host is well behaved is
19
+ // a component that breaks the next time it is mounted somewhere new. So
20
+ // nothing here inherits anything it actually needs, and no `!important` was
21
+ // required to do it.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ $sms-mono: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
25
+
26
+ @mixin sms-tokens {
27
+ --s-primary: var(--primary-color, #00386c);
28
+ --s-secondary: var(--secondary-color, #404040);
29
+ --s-paragraph: var(--paragraph-color, #2b2b2b);
30
+ --s-background: var(--background-color, #fff);
31
+ --s-br: var(--br, 6px);
32
+ --s-btn-br: var(--btn-br, 6px);
33
+
34
+ --s-line: color-mix(in srgb, var(--s-paragraph) 18%, transparent);
35
+ // 76% of the body colour over white lands around #5b5b5b — roughly 6:1,
36
+ // comfortably past 4.5:1 on the small type this file is full of.
37
+ --s-muted: color-mix(in srgb, var(--s-paragraph) 76%, transparent);
38
+ --s-surface: color-mix(in srgb, var(--s-paragraph) 4%, var(--s-background));
39
+ --s-hover: color-mix(in srgb, var(--s-primary) 9%, var(--s-background));
40
+ --s-tint: color-mix(in srgb, var(--s-primary) 12%, var(--s-background));
41
+ --s-ok: rgb(31, 122, 71);
42
+ --s-warn: rgb(163, 106, 20);
43
+ --s-bad: rgb(190, 62, 62);
44
+ // A selected row. Deliberately fainter than `--s-tint`: on a list where
45
+ // one row is always selected, a strong fill reads as an alert.
46
+ --s-select: color-mix(in srgb, var(--s-primary) 8%, var(--s-background));
47
+ // One height for every control that sits beside another control.
48
+ --s-control: 34px;
49
+
50
+ // The two bubble skins. Incoming is a neutral card; outgoing is the brand
51
+ // colour, which is the only place in the module white text is correct.
52
+ --s-in-bg: color-mix(in srgb, var(--s-paragraph) 7%, var(--s-background));
53
+ --s-in-fg: var(--s-paragraph);
54
+ --s-out-bg: var(--s-primary);
55
+ --s-out-fg: #fff;
56
+ }
57
+
58
+ // Undo anything a host may have imposed on a generic element. Applied to every
59
+ // interactive element in the module — see the note at the top of the file.
60
+ @mixin sms-reset {
61
+ box-sizing: border-box;
62
+ width: auto;
63
+ height: auto;
64
+ min-width: 0;
65
+ min-height: 0;
66
+ max-width: none;
67
+ margin: 0;
68
+ font-family: inherit;
69
+ font-weight: 400;
70
+ line-height: 1.3;
71
+ letter-spacing: normal;
72
+ text-transform: none;
73
+ text-decoration: none;
74
+ text-align: left;
75
+ appearance: none;
76
+ outline: none;
77
+ transform: none;
78
+ box-shadow: none;
79
+ // The host's chip rules transition `all`, which makes a control visibly
80
+ // resize on hover once its box is no longer fixed.
81
+ transition: background-color 0.15s ease, color 0.15s ease,
82
+ border-color 0.15s ease;
83
+ }
84
+
85
+ @mixin sms-focus {
86
+ &:focus-visible {
87
+ outline: 2px solid var(--s-primary);
88
+ outline-offset: 2px;
89
+ }
90
+ }
91
+
92
+ // A checkbox or a radio, and nothing else.
93
+ //
94
+ // The host CRM styles bare `input` — padding, a min-width, a text-field
95
+ // height, a pill radius — none of it typed, so it lands on `input[type=radio]`
96
+ // too and renders a choice as a pair of wide ovals with a dot floating in
97
+ // them. `sms-reset` cannot help: it deliberately unsets `appearance`, which
98
+ // for these two is what draws the control at all. So this is the inverse of
99
+ // the reset — the native box restated, and the native painting asked for back.
100
+ @mixin sms-check {
101
+ @include sms-focus;
102
+
103
+ flex: 0 0 auto;
104
+ box-sizing: border-box;
105
+ width: 16px;
106
+ height: 16px;
107
+ min-width: 16px;
108
+ min-height: 16px;
109
+ max-width: 16px;
110
+ padding: 0;
111
+ margin: 0;
112
+ border: 0;
113
+ border-radius: 0;
114
+ background: none;
115
+ box-shadow: none;
116
+ appearance: auto;
117
+ -webkit-appearance: auto;
118
+ accent-color: var(--s-primary);
119
+ cursor: pointer;
120
+ vertical-align: middle;
121
+ }
122
+
123
+ @keyframes smsSpin {
124
+ to {
125
+ transform: rotate(360deg);
126
+ }
127
+ }
128
+
129
+ @keyframes smsPopIn {
130
+ from {
131
+ opacity: 0;
132
+ transform: translateY(-4px);
133
+ }
134
+ to {
135
+ opacity: 1;
136
+ transform: translateY(0);
137
+ }
138
+ }
139
+
140
+ .spin {
141
+ animation: smsSpin 1s linear infinite;
142
+ flex-shrink: 0;
143
+ }
144
+
145
+ .muted {
146
+ color: var(--s-muted);
147
+ }
148
+
149
+ .visuallyHidden {
150
+ position: absolute;
151
+ width: 1px;
152
+ height: 1px;
153
+ padding: 0;
154
+ margin: -1px;
155
+ overflow: hidden;
156
+ clip: rect(0, 0, 0, 0);
157
+ white-space: nowrap;
158
+ border: 0;
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Shared controls
163
+ // ---------------------------------------------------------------------------
164
+
165
+ .primaryButton {
166
+ @include sms-reset;
167
+ @include sms-focus;
168
+
169
+ display: inline-flex;
170
+ flex-shrink: 0;
171
+ gap: 0.4rem;
172
+ align-items: center;
173
+ justify-content: center;
174
+ padding: 0.5rem 0.85rem;
175
+ border: 1px solid var(--s-primary);
176
+ border-radius: var(--s-btn-br);
177
+ background: var(--s-primary);
178
+ color: #fff;
179
+ font-size: 0.82rem;
180
+ font-weight: 600;
181
+ white-space: nowrap;
182
+ cursor: pointer;
183
+
184
+ svg {
185
+ flex-shrink: 0;
186
+ color: currentColor;
187
+ }
188
+
189
+ &:hover:not(:disabled) {
190
+ border-color: color-mix(in srgb, var(--s-primary) 85%, #000);
191
+ background: color-mix(in srgb, var(--s-primary) 85%, #000);
192
+ color: #fff;
193
+ }
194
+
195
+ &:disabled {
196
+ opacity: 0.55;
197
+ cursor: default;
198
+ }
199
+ }
200
+
201
+ .ghostButton {
202
+ @include sms-reset;
203
+ @include sms-focus;
204
+
205
+ display: inline-flex;
206
+ flex-shrink: 0;
207
+ gap: 0.4rem;
208
+ align-items: center;
209
+ justify-content: center;
210
+ padding: 0.45rem 0.75rem;
211
+ border: 1px solid var(--s-line);
212
+ border-radius: var(--s-btn-br);
213
+ background: var(--s-background);
214
+ color: var(--s-paragraph);
215
+ font-size: 0.8rem;
216
+ font-weight: 500;
217
+ white-space: nowrap;
218
+ cursor: pointer;
219
+
220
+ svg {
221
+ flex-shrink: 0;
222
+ color: currentColor;
223
+ }
224
+
225
+ &:hover:not(:disabled) {
226
+ border-color: var(--s-primary);
227
+ background: var(--s-hover);
228
+ color: var(--s-primary);
229
+ }
230
+
231
+ &:disabled {
232
+ opacity: 0.5;
233
+ cursor: default;
234
+ }
235
+ }
236
+
237
+ .iconButton {
238
+ @include sms-reset;
239
+ @include sms-focus;
240
+
241
+ display: inline-flex;
242
+ flex: 0 0 auto;
243
+ align-items: center;
244
+ justify-content: center;
245
+ width: 30px;
246
+ height: 30px;
247
+ min-width: 30px;
248
+ padding: 0;
249
+ border: 1px solid var(--s-line);
250
+ border-radius: var(--s-btn-br);
251
+ background: var(--s-background);
252
+ color: var(--s-secondary);
253
+ cursor: pointer;
254
+
255
+ svg {
256
+ flex-shrink: 0;
257
+ color: currentColor;
258
+ }
259
+
260
+ &:hover:not(:disabled) {
261
+ border-color: var(--s-primary);
262
+ background: var(--s-tint);
263
+ color: var(--s-primary);
264
+ }
265
+
266
+ &:disabled {
267
+ opacity: 0.55;
268
+ cursor: default;
269
+ }
270
+ }
271
+
272
+ .linkButton {
273
+ @include sms-reset;
274
+ @include sms-focus;
275
+
276
+ display: inline-flex;
277
+ gap: 0.3rem;
278
+ align-items: center;
279
+ padding: 0;
280
+ border: none;
281
+ background: none;
282
+ color: var(--s-primary);
283
+ font-size: 0.8rem;
284
+ font-weight: 600;
285
+ cursor: pointer;
286
+
287
+ svg {
288
+ flex-shrink: 0;
289
+ color: currentColor;
290
+ }
291
+
292
+ &:hover:not(:disabled) {
293
+ color: var(--s-primary);
294
+ text-decoration: underline;
295
+ }
296
+
297
+ &:disabled {
298
+ opacity: 0.5;
299
+ cursor: default;
300
+ }
301
+ }
302
+
303
+ .dangerLink {
304
+ @extend .linkButton;
305
+
306
+ color: var(--s-bad);
307
+
308
+ &:hover:not(:disabled) {
309
+ color: var(--s-bad);
310
+ }
311
+ }
312
+
313
+ .input,
314
+ .textarea,
315
+ .select {
316
+ @include sms-reset;
317
+ @include sms-focus;
318
+
319
+ width: 100%;
320
+ padding: 0.5rem 0.6rem;
321
+ border: 1px solid var(--s-line);
322
+ border-radius: var(--s-btn-br);
323
+ background: var(--s-background);
324
+ color: var(--s-paragraph);
325
+ font-size: 0.87rem;
326
+
327
+ &::placeholder {
328
+ color: var(--s-muted);
329
+ opacity: 1;
330
+ }
331
+
332
+ &:focus {
333
+ border-color: var(--s-primary);
334
+ }
335
+
336
+ &:disabled {
337
+ background: var(--s-surface);
338
+ opacity: 0.7;
339
+ }
340
+ }
341
+
342
+ .textarea {
343
+ line-height: 1.5;
344
+ resize: vertical;
345
+ }
346
+
347
+ .select {
348
+ height: var(--s-control);
349
+ min-height: var(--s-control);
350
+ padding: 0 1.8rem 0 0.6rem;
351
+ // The native arrow goes with `appearance: none` in the reset, so it is
352
+ // drawn back on rather than left missing.
353
+ background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
354
+ linear-gradient(135deg, currentColor 50%, transparent 50%);
355
+ background-position: calc(100% - 15px) 52%, calc(100% - 10px) 52%;
356
+ background-size: 5px 5px, 5px 5px;
357
+ background-repeat: no-repeat;
358
+ cursor: pointer;
359
+ }
360
+
361
+ .field {
362
+ display: flex;
363
+ flex-direction: column;
364
+ gap: 0.3rem;
365
+ }
366
+
367
+ .label {
368
+ color: var(--s-muted);
369
+ font-size: 0.74rem;
370
+ font-weight: 700;
371
+ letter-spacing: 0.04em;
372
+ text-transform: uppercase;
373
+ }
374
+
375
+ .help {
376
+ margin: 0;
377
+ color: var(--s-muted);
378
+ font-size: 0.75rem;
379
+ line-height: 1.5;
380
+ }
381
+
382
+ .fieldError {
383
+ display: flex;
384
+ gap: 0.3rem;
385
+ align-items: center;
386
+ margin: 0;
387
+ color: var(--s-bad);
388
+ font-size: 0.76rem;
389
+ font-weight: 500;
390
+ }
391
+
392
+ .checkRow {
393
+ display: flex;
394
+ flex-wrap: wrap;
395
+ gap: 1rem;
396
+
397
+ label {
398
+ display: inline-flex;
399
+ gap: 0.5rem;
400
+ align-items: center;
401
+ color: var(--s-paragraph);
402
+ font-size: 0.84rem;
403
+ line-height: 1.2;
404
+ cursor: pointer;
405
+ }
406
+
407
+ input[type='radio'],
408
+ input[type='checkbox'] {
409
+ @include sms-check;
410
+ }
411
+
412
+ // Square for a checkbox, round for a radio — restated because the reset
413
+ // above zeroes the radius, and a radio with a 6px radius is an oval.
414
+ input[type='radio'] {
415
+ border-radius: 50%;
416
+ }
417
+ }
418
+
419
+ .inlineCheck {
420
+ display: inline-flex;
421
+ gap: 0.45rem;
422
+ align-items: center;
423
+ color: var(--s-paragraph);
424
+ font-size: 0.82rem;
425
+ line-height: 1.2;
426
+ cursor: pointer;
427
+
428
+ input[type='checkbox'],
429
+ input[type='radio'] {
430
+ @include sms-check;
431
+ }
432
+
433
+ input[type='radio'] {
434
+ border-radius: 50%;
435
+ }
436
+ }
437
+
438
+ // ONE field with the icon inside it — not a bordered strip wrapped around a
439
+ // bordered input. The input below carries no border of its own, and the height
440
+ // is the same `--s-control` every select and button beside it uses, so a row
441
+ // of controls lines up on both edges.
442
+ .searchBar {
443
+ @include sms-reset;
444
+
445
+ display: flex;
446
+ gap: 0.45rem;
447
+ align-items: center;
448
+ box-sizing: border-box;
449
+ width: 100%;
450
+ height: var(--s-control);
451
+ min-height: var(--s-control);
452
+ padding: 0 0.55rem;
453
+ border: 1px solid var(--s-line);
454
+ border-radius: var(--s-btn-br);
455
+ background: var(--s-background);
456
+ color: var(--s-muted);
457
+
458
+ &:focus-within {
459
+ border-color: var(--s-primary);
460
+ }
461
+
462
+ svg {
463
+ flex-shrink: 0;
464
+ color: var(--s-muted);
465
+ }
466
+ }
467
+
468
+ .searchInput {
469
+ @include sms-reset;
470
+
471
+ flex: 1;
472
+ // The host styles bare `input` with a border, a padding and a height of
473
+ // its own; every one of those is restated away here, which is what keeps
474
+ // this a field rather than a field inside a field.
475
+ height: 100%;
476
+ min-width: 0;
477
+ padding: 0;
478
+ border: none;
479
+ border-radius: 0;
480
+ background: transparent;
481
+ color: var(--s-paragraph);
482
+ font-size: 0.86rem;
483
+
484
+ &::placeholder {
485
+ color: var(--s-muted);
486
+ opacity: 1;
487
+ }
488
+ }
489
+
490
+ .pill {
491
+ display: inline-flex;
492
+ gap: 0.25rem;
493
+ align-items: center;
494
+ padding: 0.12rem 0.5rem;
495
+ border-radius: 999px;
496
+ font-size: 0.72rem;
497
+ font-weight: 700;
498
+ white-space: nowrap;
499
+ }
500
+
501
+ .pillOk {
502
+ background: color-mix(in srgb, var(--s-ok) 15%, var(--s-background));
503
+ color: var(--s-ok);
504
+ }
505
+
506
+ .pillWarn {
507
+ background: color-mix(in srgb, var(--s-warn) 16%, var(--s-background));
508
+ color: var(--s-warn);
509
+ }
510
+
511
+ .pillBad {
512
+ background: color-mix(in srgb, var(--s-bad) 15%, var(--s-background));
513
+ color: var(--s-bad);
514
+ }
515
+
516
+ .pillMuted {
517
+ background: color-mix(in srgb, var(--s-paragraph) 12%, var(--s-background));
518
+ color: var(--s-muted);
519
+ }
520
+
521
+ // A person, or a number, as at most two letters in a circle.
522
+ //
523
+ // One fixed size wherever it appears: the avatar is the left edge of every
524
+ // list in this module, and a circle that resizes with its contents is what
525
+ // makes a column of rows zig-zag.
526
+ .avatar {
527
+ display: inline-flex;
528
+ flex: 0 0 auto;
529
+ align-items: center;
530
+ justify-content: center;
531
+ box-sizing: border-box;
532
+ width: 32px;
533
+ height: 32px;
534
+ min-width: 32px;
535
+ max-width: 32px;
536
+ border-radius: 50%;
537
+ background: var(--s-tint);
538
+ color: var(--s-primary);
539
+ font-size: 0.72rem;
540
+ font-weight: 700;
541
+ letter-spacing: 0.02em;
542
+ // Two digits and two letters must occupy the same width, or a list of
543
+ // mixed rows looks ragged.
544
+ font-variant-numeric: tabular-nums;
545
+ text-transform: uppercase;
546
+ // A helper that ever returned three characters must not widen the circle:
547
+ // the layout is the contract, not the string.
548
+ overflow: hidden;
549
+ white-space: nowrap;
550
+ }
551
+
552
+ .avatarLg {
553
+ width: 36px;
554
+ height: 36px;
555
+ font-size: 0.8rem;
556
+ }
557
+
558
+ // ---------------------------------------------------------------------------
559
+ // Status notes and banners
560
+ // ---------------------------------------------------------------------------
561
+
562
+ // One line, and it stays one line.
563
+ //
564
+ // This was a two-line block with a heading and a paragraph, and on a page
565
+ // whose whole job is a list of conversations it dominated the screen — a
566
+ // standing notice about a connection nobody can do anything about from here.
567
+ // Icon, the fact, a short note that ellipses. Nothing wraps.
568
+ .banner {
569
+ display: flex;
570
+ gap: 0.5rem;
571
+ align-items: center;
572
+ box-sizing: border-box;
573
+ width: 100%;
574
+ padding: 0.4rem 0.7rem;
575
+ border: 1px solid var(--s-line);
576
+ border-radius: var(--s-br);
577
+ background: var(--s-surface);
578
+ color: var(--s-paragraph);
579
+ font-size: 0.78rem;
580
+ line-height: 1.4;
581
+
582
+ svg {
583
+ flex-shrink: 0;
584
+ }
585
+
586
+ strong {
587
+ flex-shrink: 0;
588
+ color: inherit;
589
+ font-weight: 700;
590
+ }
591
+ }
592
+
593
+ .bannerNote {
594
+ min-width: 0;
595
+ overflow: hidden;
596
+ color: color-mix(in srgb, currentColor 72%, transparent);
597
+ text-overflow: ellipsis;
598
+ white-space: nowrap;
599
+ }
600
+
601
+ .bannerWarn {
602
+ border-color: color-mix(in srgb, var(--s-warn) 45%, transparent);
603
+ background: color-mix(in srgb, var(--s-warn) 10%, var(--s-background));
604
+ color: color-mix(in srgb, var(--s-warn) 80%, var(--s-paragraph));
605
+
606
+ svg {
607
+ color: var(--s-warn);
608
+ }
609
+ }
610
+
611
+ .bannerBad {
612
+ border-color: color-mix(in srgb, var(--s-bad) 45%, transparent);
613
+ background: color-mix(in srgb, var(--s-bad) 9%, var(--s-background));
614
+ color: color-mix(in srgb, var(--s-bad) 80%, var(--s-paragraph));
615
+
616
+ svg {
617
+ color: var(--s-bad);
618
+ }
619
+ }
620
+
621
+ .bannerOk {
622
+ border-color: color-mix(in srgb, var(--s-ok) 40%, transparent);
623
+ background: color-mix(in srgb, var(--s-ok) 9%, var(--s-background));
624
+ color: color-mix(in srgb, var(--s-ok) 80%, var(--s-paragraph));
625
+
626
+ svg {
627
+ color: var(--s-ok);
628
+ }
629
+ }
630
+
631
+ // The one-line version, for the popover.
632
+ .statusNote {
633
+ display: flex;
634
+ gap: 0.4rem;
635
+ align-items: center;
636
+ padding: 0.45rem 0.85rem;
637
+ border-top: 1px solid var(--s-line);
638
+ background: color-mix(in srgb, var(--s-warn) 8%, var(--s-background));
639
+ color: color-mix(in srgb, var(--s-warn) 85%, var(--s-paragraph));
640
+ font-size: 0.75rem;
641
+ line-height: 1.4;
642
+
643
+ svg {
644
+ flex-shrink: 0;
645
+ color: var(--s-warn);
646
+ }
647
+ }
648
+
649
+ // ---------------------------------------------------------------------------
650
+ // SmsInboxBadge — the header trigger and its popover
651
+ // ---------------------------------------------------------------------------
652
+
653
+ .quickSms {
654
+ @include sms-tokens;
655
+
656
+ position: relative;
657
+ align-items: center;
658
+ }
659
+
660
+ // Deliberately the same shape as Notification's and the vault's triggers:
661
+ // these sit side by side in the action pill and must read as one row of chips,
662
+ // not three designs. This one DOES inherit the header's colour, which is
663
+ // correct — it is a chip.
664
+ .trigger {
665
+ @include sms-focus;
666
+
667
+ position: relative;
668
+ display: flex;
669
+ align-items: center;
670
+ justify-content: center;
671
+ width: 40px;
672
+ height: 40px;
673
+ padding: 8px;
674
+ border: none;
675
+ border-radius: 50%;
676
+ background: transparent;
677
+ color: var(--paragraph-color, #333);
678
+ cursor: pointer;
679
+ transition: all 0.2s ease;
680
+
681
+ &:hover {
682
+ background-color: rgba(var(--primary-color-rgb, 0, 86, 179), 0.1);
683
+ color: var(--primary-color, #0056b3);
684
+ transform: scale(1.05);
685
+ }
686
+
687
+ &:active {
688
+ transform: scale(0.95);
689
+ }
690
+ }
691
+
692
+ // Matches Notification's badge, down to the offsets: two unread counts side by
693
+ // side in the header have to sit on the same line as each other.
694
+ .badge {
695
+ position: absolute;
696
+ top: -2px;
697
+ right: -2px;
698
+ display: flex;
699
+ align-items: center;
700
+ justify-content: center;
701
+ box-sizing: border-box;
702
+ min-width: 16px;
703
+ height: 16px;
704
+ padding: 0 4px;
705
+ border-radius: 8px;
706
+ background-color: var(--secondary-color, #c1272d);
707
+ color: #fff;
708
+ font-size: 10px;
709
+ font-weight: 600;
710
+ font-variant-numeric: tabular-nums;
711
+ line-height: 1;
712
+ }
713
+
714
+ .popoverWrapper {
715
+ position: absolute;
716
+ top: 100%;
717
+ right: 0;
718
+ z-index: 1100;
719
+ margin-top: 10px;
720
+ // The host's chip `li` is a centring flex container. The popover is
721
+ // absolutely positioned so it escapes the flexbox, but `text-align` is
722
+ // inherited and would centre every line of prose inside it.
723
+ text-align: left;
724
+ }
725
+
726
+ .popover {
727
+ @include sms-tokens;
728
+
729
+ display: flex;
730
+ flex-direction: column;
731
+ box-sizing: border-box;
732
+ width: 420px;
733
+ max-width: calc(100vw - 2rem);
734
+ border: 1px solid var(--s-line);
735
+ border-radius: 10px;
736
+ // Opaque, and stated: the header behind this is a dark brand bar, and a
737
+ // panel that inherits any of it is unreadable.
738
+ background: var(--s-background);
739
+ color: var(--s-paragraph);
740
+ font-size: 0.85rem;
741
+ text-align: left;
742
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22), 0 2px 6px rgba(0, 0, 0, 0.12);
743
+ overflow: hidden;
744
+ animation: smsPopIn 0.18s ease-out;
745
+ }
746
+
747
+ .popoverHead {
748
+ display: flex;
749
+ gap: 0.75rem;
750
+ align-items: center;
751
+ justify-content: space-between;
752
+ padding: 0.7rem 0.85rem;
753
+ border-bottom: 1px solid var(--s-line);
754
+ background: var(--s-surface);
755
+ }
756
+
757
+ .popoverTitle {
758
+ display: inline-flex;
759
+ gap: 0.4rem;
760
+ align-items: center;
761
+ color: var(--s-paragraph);
762
+ font-size: 0.95rem;
763
+ font-weight: 700;
764
+
765
+ svg {
766
+ color: var(--s-primary);
767
+ }
768
+ }
769
+
770
+ .rows {
771
+ max-height: 360px;
772
+ margin: 0;
773
+ padding: 0;
774
+ list-style: none;
775
+ overflow-y: auto;
776
+
777
+ // Two hosts reach in here with bare element selectors: Navigation's
778
+ // `.hactions > ul li` (now scoped to `> li`, but this file does not get
779
+ // to assume its host has been fixed) and the CRM's own
780
+ // `ul { padding-left: … }` in main.scss. Restated so the list is a list
781
+ // wherever it is mounted.
782
+ > li {
783
+ min-width: 0;
784
+ min-height: 0;
785
+ margin: 0;
786
+ padding: 0;
787
+ position: static;
788
+ list-style: none;
789
+ }
790
+ }
791
+
792
+ // Specificity, deliberately.
793
+ //
794
+ // `.hactions > ul li` was 0,1,2 — two element selectors and a class — and no
795
+ // single class can outrank it. Everything below is the row's BOX, restated at
796
+ // 0,2,0 and 0,3,0 where it wins outright: the grid that a stray `display:
797
+ // flex` was flattening, and the text cell whose `display: block` is what makes
798
+ // the ellipsis engage. Paint stays in `.row` below; only geometry is repeated.
799
+ .rows > .row {
800
+ display: grid;
801
+ grid-template-columns: 3px 32px minmax(0, 1fr) auto;
802
+ column-gap: 0.6rem;
803
+ align-items: center;
804
+ justify-content: normal;
805
+ box-sizing: border-box;
806
+ min-width: 0;
807
+ min-height: 0;
808
+ margin: 0;
809
+ padding: 0.5rem 0.85rem 0.5rem 0;
810
+ position: static;
811
+ }
812
+
813
+ .rows > .row > .rowMain {
814
+ display: block;
815
+ }
816
+
817
+ // One thread, one line: accent, avatar, name + preview, time.
818
+ //
819
+ // FOUR columns, and the first one is the whole point. The unread marker used
820
+ // to be a `border-left`, and a border is layout: an unread row was three
821
+ // pixels narrower than a read one and its avatar sat three pixels further
822
+ // right, so a list of mixed rows zig-zagged down the left edge. The accent is
823
+ // now a fixed 3px track that is simply transparent when there is nothing to
824
+ // say, so every row in the list is measured identically whatever its state.
825
+ //
826
+ // The avatar track is fixed too, for the same reason. Only the text column
827
+ // gives way — it is the only part of the row that can be any length.
828
+ .row {
829
+ display: grid;
830
+ grid-template-columns: 3px 32px minmax(0, 1fr) auto;
831
+ column-gap: 0.6rem;
832
+ align-items: center;
833
+ // No left padding: the accent sits flush against the edge of the panel,
834
+ // and the gap after it lands the avatar on the same 13px the popover
835
+ // header's title starts at.
836
+ padding: 0.5rem 0.85rem 0.5rem 0;
837
+ border: none;
838
+ border-bottom: 1px solid color-mix(in srgb, var(--s-line) 55%, transparent);
839
+ background: var(--s-background);
840
+ cursor: pointer;
841
+
842
+ &:last-child {
843
+ border-bottom: none;
844
+ }
845
+ }
846
+
847
+ // The 3px track. Always present, coloured only when the row is unread.
848
+ .rowAccent {
849
+ align-self: stretch;
850
+ width: 3px;
851
+ border-radius: 0 2px 2px 0;
852
+ background: transparent;
853
+ }
854
+
855
+ .rowUnread .rowAccent {
856
+ background: var(--s-primary);
857
+ }
858
+
859
+ // Highlight is a background and nothing else — anything that touches the box
860
+ // model here is what the four-column grid above exists to prevent.
861
+ .rowActive {
862
+ background: var(--s-hover);
863
+ }
864
+
865
+ .rowMain {
866
+ // `min-width: 0` is the load-bearing declaration: without it a grid item
867
+ // refuses to shrink below its content and the ellipsis never engages.
868
+ min-width: 0;
869
+ // `stretch`, not `start`: the content is `nowrap`, so under `start` the
870
+ // cell would size to its content, overflow its track and slide under the
871
+ // time instead of ellipsing.
872
+ justify-self: stretch;
873
+ overflow: hidden;
874
+ color: var(--s-paragraph);
875
+ font-size: 0.85rem;
876
+ line-height: 1.35;
877
+ text-align: left;
878
+ text-overflow: ellipsis;
879
+ white-space: nowrap;
880
+ }
881
+
882
+ // The dot between the name and the preview.
883
+ //
884
+ // Its own element with its own margins, because a literal " · " in JSX is at
885
+ // the mercy of whitespace trimming — which is how "(Mrs)· Who" reached a
886
+ // screenshot.
887
+ .rowSep {
888
+ margin: 0 0.35rem;
889
+ color: var(--s-muted);
890
+ }
891
+
892
+ .rowName {
893
+ color: var(--s-paragraph);
894
+ font-size: 0.87rem;
895
+ font-weight: 600;
896
+ }
897
+
898
+ .rowPreview {
899
+ color: var(--s-muted);
900
+ font-size: 0.82rem;
901
+ }
902
+
903
+ .rowUnread {
904
+ .rowName {
905
+ font-weight: 800;
906
+ }
907
+
908
+ .rowPreview {
909
+ color: var(--s-paragraph);
910
+ font-weight: 600;
911
+ }
912
+ }
913
+
914
+ .rowTime {
915
+ justify-self: end;
916
+ display: inline-flex;
917
+ gap: 0.35rem;
918
+ align-items: center;
919
+ color: var(--s-muted);
920
+ font-size: 0.74rem;
921
+ font-variant-numeric: tabular-nums;
922
+ white-space: nowrap;
923
+ }
924
+
925
+ .unreadDot {
926
+ display: inline-block;
927
+ flex: 0 0 auto;
928
+ width: 8px;
929
+ height: 8px;
930
+ border-radius: 50%;
931
+ background: var(--s-primary);
932
+ }
933
+
934
+ .empty {
935
+ padding: 1.75rem 1rem;
936
+ text-align: center;
937
+
938
+ p {
939
+ margin: 0 0 0.85rem;
940
+ color: var(--s-muted);
941
+ font-size: 0.85rem;
942
+ line-height: 1.5;
943
+ }
944
+ }
945
+
946
+ .footer {
947
+ display: flex;
948
+ gap: 0.5rem;
949
+ align-items: center;
950
+ justify-content: center;
951
+ padding: 0.55rem 0.85rem;
952
+ border-top: 1px solid var(--s-line);
953
+ background: var(--s-surface);
954
+ }
955
+
956
+ .footerHint {
957
+ color: var(--s-muted);
958
+ font-size: 0.75rem; // 12px at a 16px root
959
+ }
960
+
961
+ // ---------------------------------------------------------------------------
962
+ // SmsInbox — the page
963
+ // ---------------------------------------------------------------------------
964
+
965
+ .inbox {
966
+ @include sms-tokens;
967
+
968
+ display: flex;
969
+ flex-direction: column;
970
+ gap: 0.75rem;
971
+ box-sizing: border-box;
972
+ padding: 1rem 1.25rem 1.25rem;
973
+ color: var(--s-paragraph);
974
+ font-size: 0.85rem;
975
+ }
976
+
977
+ .inboxHead {
978
+ display: flex;
979
+ flex-wrap: wrap;
980
+ gap: 0.75rem 1rem;
981
+ align-items: flex-start;
982
+ justify-content: space-between;
983
+ }
984
+
985
+ .inboxHeading {
986
+ min-width: 0;
987
+ }
988
+
989
+ .pageTitle {
990
+ display: flex;
991
+ gap: 0.5rem;
992
+ align-items: center;
993
+ margin: 0;
994
+ color: var(--s-paragraph);
995
+ font-size: 1.5rem;
996
+ font-weight: 700;
997
+ line-height: 1.2;
998
+
999
+ svg {
1000
+ color: var(--s-primary);
1001
+ }
1002
+ }
1003
+
1004
+ .headActions {
1005
+ display: flex;
1006
+ flex-wrap: wrap;
1007
+ gap: 0.5rem;
1008
+ align-items: center;
1009
+ }
1010
+
1011
+ // Two panes, ONE card.
1012
+ //
1013
+ // They were two bordered cards with a gap between them, which reads as two
1014
+ // unrelated tables that happen to sit side by side. A list and the thing the
1015
+ // list opens are one object; the divider between them is a hairline, not a
1016
+ // margin.
1017
+ //
1018
+ // The height is pinned so the panes scroll inside the card rather than the
1019
+ // page scrolling underneath them — a timeline that scrolls the document takes
1020
+ // the composer off the bottom of an iPad with it. `--s-inbox-offset` is what
1021
+ // a host subtracts for its own chrome: this module cannot know how tall the
1022
+ // CRM's header is.
1023
+ .panes {
1024
+ display: grid;
1025
+ grid-template-columns: 340px minmax(0, 1fr);
1026
+ box-sizing: border-box;
1027
+ height: calc(100vh - var(--s-inbox-offset, 230px));
1028
+ min-height: 420px;
1029
+ border: 1px solid var(--s-line);
1030
+ border-radius: var(--s-br);
1031
+ background: var(--s-background);
1032
+ overflow: hidden;
1033
+ }
1034
+
1035
+ .listPane {
1036
+ display: flex;
1037
+ flex-direction: column;
1038
+ box-sizing: border-box;
1039
+ min-height: 0;
1040
+ border: 1px solid var(--s-line);
1041
+ border-radius: var(--s-br);
1042
+ background: var(--s-background);
1043
+ overflow: hidden;
1044
+ }
1045
+
1046
+ // Inside the card the panes lose their own frames and share one divider. Kept
1047
+ // as a descendant rule so a pane mounted on its own — the empty state, an
1048
+ // embedded conversation — still comes with a border of its own.
1049
+ .panes .listPane {
1050
+ border: none;
1051
+ border-right: 1px solid var(--s-line);
1052
+ border-radius: 0;
1053
+ }
1054
+
1055
+ .panes .convPane {
1056
+ border: none;
1057
+ border-radius: 0;
1058
+ }
1059
+
1060
+ .listHead {
1061
+ display: flex;
1062
+ flex-direction: column;
1063
+ gap: 0.45rem;
1064
+ padding: 0.6rem;
1065
+ border-bottom: 1px solid var(--s-line);
1066
+ // Flat. A filled strip wrapped around a bordered field is the "double
1067
+ // frame" from the review: a box inside a box, neither of which is the
1068
+ // control.
1069
+ background: var(--s-background);
1070
+
1071
+ // The select's label wrapper adds nothing here — the option text carries
1072
+ // the meaning.
1073
+ .field {
1074
+ gap: 0;
1075
+ }
1076
+ }
1077
+
1078
+ .chips {
1079
+ display: flex;
1080
+ flex-wrap: wrap;
1081
+ gap: 0.35rem;
1082
+ }
1083
+
1084
+ .chip {
1085
+ @include sms-reset;
1086
+ @include sms-focus;
1087
+
1088
+ display: inline-flex;
1089
+ gap: 0.3rem;
1090
+ align-items: center;
1091
+ padding: 0.25rem 0.6rem;
1092
+ border: 1px solid var(--s-line);
1093
+ border-radius: 999px;
1094
+ background: var(--s-background);
1095
+ color: var(--s-muted);
1096
+ font-size: 0.75rem;
1097
+ font-weight: 600;
1098
+ cursor: pointer;
1099
+
1100
+ &:hover:not(:disabled) {
1101
+ border-color: var(--s-primary);
1102
+ color: var(--s-primary);
1103
+ }
1104
+ }
1105
+
1106
+ .chipActive {
1107
+ border-color: var(--s-primary);
1108
+ background: var(--s-primary);
1109
+ color: #fff;
1110
+
1111
+ &:hover:not(:disabled) {
1112
+ border-color: color-mix(in srgb, var(--s-primary) 85%, #000);
1113
+ background: color-mix(in srgb, var(--s-primary) 85%, #000);
1114
+ color: #fff;
1115
+ }
1116
+ }
1117
+
1118
+ .chipCount {
1119
+ font-variant-numeric: tabular-nums;
1120
+ }
1121
+
1122
+ .threadList {
1123
+ flex: 1;
1124
+ // `min-height: 0` is what lets this scroll instead of growing the flex
1125
+ // column past the bottom of the card.
1126
+ min-height: 0;
1127
+ margin: 0;
1128
+ padding: 0;
1129
+ list-style: none;
1130
+ overflow-y: auto;
1131
+
1132
+ // The same host-proofing as `.rows` — the CRM styles bare `ul` and `li`.
1133
+ > li {
1134
+ min-width: 0;
1135
+ min-height: 0;
1136
+ margin: 0;
1137
+ padding: 0;
1138
+ position: static;
1139
+ list-style: none;
1140
+ }
1141
+ }
1142
+
1143
+ // Two lines: name + time, then the preview. Wider than the popover row, so it
1144
+ // can afford the second line the popover cannot.
1145
+ //
1146
+ // Same three-column contract as `.row`, and for the same reason: the accent is
1147
+ // a fixed track rather than a border, so an unread row and a read row are
1148
+ // measured identically and the avatars line up.
1149
+ .threadRow {
1150
+ @include sms-reset;
1151
+ @include sms-focus;
1152
+
1153
+ display: grid;
1154
+ grid-template-columns: 3px 32px minmax(0, 1fr);
1155
+ column-gap: 0.6rem;
1156
+ align-items: center;
1157
+ box-sizing: border-box;
1158
+ width: 100%;
1159
+ padding: 0.55rem 0.7rem 0.55rem 0;
1160
+ border: none;
1161
+ border-bottom: 1px solid color-mix(in srgb, var(--s-line) 55%, transparent);
1162
+ border-radius: 0;
1163
+ background: var(--s-background);
1164
+ color: var(--s-paragraph);
1165
+ font-size: 0.85rem;
1166
+ cursor: pointer;
1167
+
1168
+ &:hover {
1169
+ background: var(--s-hover);
1170
+ }
1171
+ }
1172
+
1173
+ .threadAccent {
1174
+ align-self: stretch;
1175
+ width: 3px;
1176
+ border-radius: 0 2px 2px 0;
1177
+ background: transparent;
1178
+ }
1179
+
1180
+ .threadUnread .threadAccent {
1181
+ background: var(--s-primary);
1182
+ }
1183
+
1184
+ // Selection is a background, never a border — and a faint one: exactly one
1185
+ // row in this list is always selected, so a strong fill reads as an alert.
1186
+ .threadRowActive {
1187
+ background: var(--s-select);
1188
+
1189
+ &:hover {
1190
+ background: var(--s-select);
1191
+ }
1192
+
1193
+ .threadAccent {
1194
+ background: var(--s-primary);
1195
+ }
1196
+ }
1197
+
1198
+ .threadBody {
1199
+ min-width: 0;
1200
+ overflow: hidden;
1201
+ }
1202
+
1203
+ .threadTop {
1204
+ display: flex;
1205
+ gap: 0.5rem;
1206
+ align-items: baseline;
1207
+ justify-content: space-between;
1208
+ min-width: 0;
1209
+ }
1210
+
1211
+ // Regular by default. Bold is information here — it means unread — so a list
1212
+ // where every name is bold says nothing at all.
1213
+ .threadName {
1214
+ overflow: hidden;
1215
+ color: var(--s-paragraph);
1216
+ font-size: 0.87rem;
1217
+ font-weight: 500;
1218
+ text-overflow: ellipsis;
1219
+ white-space: nowrap;
1220
+ }
1221
+
1222
+ .threadTime {
1223
+ flex: 0 0 auto;
1224
+ color: var(--s-muted);
1225
+ font-size: 0.72rem;
1226
+ font-variant-numeric: tabular-nums;
1227
+ white-space: nowrap;
1228
+ }
1229
+
1230
+ .threadPreview {
1231
+ display: flex;
1232
+ gap: 0.4rem;
1233
+ align-items: center;
1234
+ min-width: 0;
1235
+ margin-top: 0.15rem;
1236
+ color: var(--s-muted);
1237
+ font-size: 0.79rem;
1238
+
1239
+ span {
1240
+ overflow: hidden;
1241
+ text-overflow: ellipsis;
1242
+ white-space: nowrap;
1243
+ }
1244
+ }
1245
+
1246
+ .threadUnread {
1247
+ .threadName {
1248
+ font-weight: 700;
1249
+ }
1250
+
1251
+ .threadPreview {
1252
+ color: var(--s-paragraph);
1253
+ font-weight: 600;
1254
+ }
1255
+ }
1256
+
1257
+ .threadCount {
1258
+ display: inline-flex;
1259
+ flex: 0 0 auto;
1260
+ align-items: center;
1261
+ justify-content: center;
1262
+ box-sizing: border-box;
1263
+ min-width: 18px;
1264
+ height: 18px;
1265
+ padding: 0 5px;
1266
+ border-radius: 9px;
1267
+ background: var(--s-primary);
1268
+ color: #fff;
1269
+ font-size: 0.66rem;
1270
+ font-weight: 700;
1271
+ font-variant-numeric: tabular-nums;
1272
+ }
1273
+
1274
+ .listFoot {
1275
+ display: flex;
1276
+ justify-content: center;
1277
+ padding: 0.5rem;
1278
+ border-top: 1px solid var(--s-line);
1279
+ background: var(--s-surface);
1280
+ }
1281
+
1282
+ // ---------------------------------------------------------------------------
1283
+ // SmsThreadPanel — the conversation
1284
+ // ---------------------------------------------------------------------------
1285
+
1286
+ .convPane {
1287
+ @include sms-tokens;
1288
+
1289
+ display: flex;
1290
+ flex-direction: column;
1291
+ box-sizing: border-box;
1292
+ min-width: 0;
1293
+ min-height: 0;
1294
+ border: 1px solid var(--s-line);
1295
+ border-radius: var(--s-br);
1296
+ background: var(--s-background);
1297
+ color: var(--s-paragraph);
1298
+ overflow: hidden;
1299
+ }
1300
+
1301
+ .convHead {
1302
+ display: flex;
1303
+ flex-wrap: wrap;
1304
+ gap: 0.5rem;
1305
+ align-items: center;
1306
+ padding: 0.5rem 0.7rem;
1307
+ border-bottom: 1px solid var(--s-line);
1308
+ background: var(--s-surface);
1309
+
1310
+ // Every action in this row is the same height, whatever kind of control
1311
+ // it is. A chip, a worded button and an icon button at three heights is
1312
+ // what made the header look unfinished.
1313
+ .ghostButton,
1314
+ .iconButton,
1315
+ .clientChip {
1316
+ box-sizing: border-box;
1317
+ height: 30px;
1318
+ min-height: 30px;
1319
+ }
1320
+ }
1321
+
1322
+ .convIdentity {
1323
+ display: flex;
1324
+ gap: 0.5rem;
1325
+ align-items: center;
1326
+ min-width: 0;
1327
+ // Takes the slack, so the actions sit hard right.
1328
+ margin-right: auto;
1329
+ }
1330
+
1331
+ .convName {
1332
+ display: block;
1333
+ overflow: hidden;
1334
+ color: var(--s-paragraph);
1335
+ font-size: 0.95rem;
1336
+ font-weight: 700;
1337
+ text-overflow: ellipsis;
1338
+ white-space: nowrap;
1339
+ }
1340
+
1341
+ .convNumber {
1342
+ display: block;
1343
+ color: var(--s-muted);
1344
+ font-size: 0.76rem;
1345
+ font-variant-numeric: tabular-nums;
1346
+ }
1347
+
1348
+ .convActions {
1349
+ display: flex;
1350
+ flex-wrap: wrap;
1351
+ gap: 0.35rem;
1352
+ align-items: center;
1353
+ // Hard right, as one group.
1354
+ justify-content: flex-end;
1355
+ margin-left: auto;
1356
+ }
1357
+
1358
+ .clientChip {
1359
+ display: inline-flex;
1360
+ gap: 0.3rem;
1361
+ align-items: center;
1362
+ padding: 0 0.6rem;
1363
+ border: 1px solid color-mix(in srgb, var(--s-primary) 35%, transparent);
1364
+ border-radius: 999px;
1365
+ background: var(--s-tint);
1366
+ color: var(--s-primary);
1367
+ font-size: 0.74rem;
1368
+ font-weight: 700;
1369
+ text-decoration: none;
1370
+
1371
+ &:hover {
1372
+ border-color: var(--s-primary);
1373
+ color: var(--s-primary);
1374
+ text-decoration: underline;
1375
+ }
1376
+
1377
+ svg {
1378
+ flex-shrink: 0;
1379
+ color: currentColor;
1380
+ }
1381
+ }
1382
+
1383
+ .timeline {
1384
+ display: flex;
1385
+ flex: 1;
1386
+ flex-direction: column;
1387
+ gap: 0.15rem;
1388
+ box-sizing: border-box;
1389
+ min-height: 0;
1390
+ padding: 0.85rem 0.9rem;
1391
+ background: var(--s-background);
1392
+ overflow-y: auto;
1393
+ }
1394
+
1395
+ .dayHeading {
1396
+ display: flex;
1397
+ gap: 0.6rem;
1398
+ align-items: center;
1399
+ margin: 0.7rem 0 0.5rem;
1400
+ color: var(--s-muted);
1401
+ font-size: 0.72rem;
1402
+ font-weight: 700;
1403
+ letter-spacing: 0.04em;
1404
+ text-transform: uppercase;
1405
+
1406
+ &::before,
1407
+ &::after {
1408
+ flex: 1;
1409
+ height: 1px;
1410
+ background: color-mix(in srgb, var(--s-line) 70%, transparent);
1411
+ content: '';
1412
+ }
1413
+
1414
+ &:first-child {
1415
+ margin-top: 0;
1416
+ }
1417
+ }
1418
+
1419
+ // One message. Consecutive messages from the same side sit 4px apart and read
1420
+ // as one utterance; a change of side opens the gap up. The timestamp is drawn
1421
+ // only under the last of a run, because six identical times stacked down the
1422
+ // margin is noise, not information.
1423
+ .bubbleRow {
1424
+ display: flex;
1425
+ flex-direction: column;
1426
+ max-width: 100%;
1427
+ margin-bottom: 4px;
1428
+ }
1429
+
1430
+ .bubbleRunEnd {
1431
+ margin-bottom: 0.7rem;
1432
+ }
1433
+
1434
+ .bubbleIn {
1435
+ align-items: flex-start;
1436
+ }
1437
+
1438
+ .bubbleOut {
1439
+ align-items: flex-end;
1440
+ }
1441
+
1442
+ .bubble {
1443
+ box-sizing: border-box;
1444
+ // 64% — wide enough for a paragraph, narrow enough that the two sides of
1445
+ // the conversation stay visibly two sides.
1446
+ max-width: min(60ch, 64%);
1447
+ padding: 0.5rem 0.7rem;
1448
+ border: 1px solid transparent;
1449
+ border-radius: 12px;
1450
+ font-size: 0.875rem; // 14px at a 16px root
1451
+ line-height: 1.5;
1452
+ // A pasted account number has no spaces in it and must not push the pane
1453
+ // wider than the pane.
1454
+ overflow-wrap: anywhere;
1455
+ white-space: pre-wrap;
1456
+ }
1457
+
1458
+ .bubbleBodyIn {
1459
+ border-color: color-mix(in srgb, var(--s-paragraph) 12%, transparent);
1460
+ border-bottom-left-radius: 4px;
1461
+ background: var(--s-in-bg);
1462
+ color: var(--s-in-fg);
1463
+ }
1464
+
1465
+ .bubbleBodyOut {
1466
+ border-color: var(--s-out-bg);
1467
+ border-bottom-right-radius: 4px;
1468
+ background: var(--s-out-bg);
1469
+ color: var(--s-out-fg);
1470
+ }
1471
+
1472
+ // A message that is queued behind a transport that cannot send it yet. Not an
1473
+ // error skin: dashed and pale, because the message is genuinely saved.
1474
+ .bubbleHeld {
1475
+ border-style: dashed;
1476
+ border-color: color-mix(in srgb, var(--s-warn) 60%, transparent);
1477
+ background: color-mix(in srgb, var(--s-warn) 12%, var(--s-background));
1478
+ color: var(--s-paragraph);
1479
+ }
1480
+
1481
+ .bubbleFailed {
1482
+ border-color: color-mix(in srgb, var(--s-bad) 60%, transparent);
1483
+ background: color-mix(in srgb, var(--s-bad) 10%, var(--s-background));
1484
+ color: var(--s-paragraph);
1485
+ }
1486
+
1487
+ .bubblePending {
1488
+ opacity: 0.72;
1489
+ }
1490
+
1491
+ // Time, status and sender on ONE small muted line, under the bubble and on
1492
+ // the same side as it.
1493
+ .bubbleMeta {
1494
+ display: flex;
1495
+ flex-wrap: wrap;
1496
+ gap: 0.3rem;
1497
+ align-items: center;
1498
+ margin-top: 0.2rem;
1499
+ padding: 0 0.25rem;
1500
+ color: var(--s-muted);
1501
+ font-size: 0.7rem;
1502
+ font-variant-numeric: tabular-nums;
1503
+
1504
+ svg {
1505
+ flex-shrink: 0;
1506
+ }
1507
+ }
1508
+
1509
+ .bubbleOut .bubbleMeta {
1510
+ justify-content: flex-end;
1511
+ text-align: right;
1512
+ }
1513
+
1514
+ .metaFailed {
1515
+ color: var(--s-bad);
1516
+ font-weight: 600;
1517
+ }
1518
+
1519
+ .metaHeld {
1520
+ color: var(--s-warn);
1521
+ font-weight: 600;
1522
+ }
1523
+
1524
+ .retryButton {
1525
+ @include sms-reset;
1526
+ @include sms-focus;
1527
+
1528
+ padding: 0;
1529
+ border: none;
1530
+ background: none;
1531
+ color: var(--s-bad);
1532
+ font-size: 0.7rem;
1533
+ font-weight: 700;
1534
+ text-decoration: underline;
1535
+ cursor: pointer;
1536
+ }
1537
+
1538
+ .attachments {
1539
+ display: flex;
1540
+ flex-wrap: wrap;
1541
+ gap: 0.3rem;
1542
+ margin-top: 0.35rem;
1543
+ }
1544
+
1545
+ .attachment {
1546
+ display: inline-flex;
1547
+ gap: 0.3rem;
1548
+ align-items: center;
1549
+ max-width: 100%;
1550
+ padding: 0.15rem 0.5rem;
1551
+ border: 1px solid color-mix(in srgb, currentColor 40%, transparent);
1552
+ border-radius: 999px;
1553
+ color: inherit;
1554
+ font-size: 0.72rem;
1555
+ font-weight: 600;
1556
+ text-decoration: none;
1557
+
1558
+ &:hover {
1559
+ color: inherit;
1560
+ text-decoration: underline;
1561
+ }
1562
+
1563
+ svg {
1564
+ flex-shrink: 0;
1565
+ }
1566
+
1567
+ span {
1568
+ overflow: hidden;
1569
+ text-overflow: ellipsis;
1570
+ white-space: nowrap;
1571
+ }
1572
+ }
1573
+
1574
+ .timelineTop {
1575
+ display: flex;
1576
+ justify-content: center;
1577
+ padding-bottom: 0.5rem;
1578
+ }
1579
+
1580
+ .timelineEmpty {
1581
+ display: flex;
1582
+ flex: 1;
1583
+ flex-direction: column;
1584
+ gap: 0.4rem;
1585
+ align-items: center;
1586
+ justify-content: center;
1587
+ padding: 2rem 1rem;
1588
+ color: var(--s-muted);
1589
+ font-size: 0.85rem;
1590
+ text-align: center;
1591
+ }
1592
+
1593
+ // ------------------------------------------------------------- the composer
1594
+
1595
+ .composer {
1596
+ box-sizing: border-box;
1597
+ padding: 0.6rem 0.7rem 0.7rem;
1598
+ border-top: 1px solid var(--s-line);
1599
+ background: var(--s-surface);
1600
+ }
1601
+
1602
+ // One card: the text on top, every control in a bar along its bottom edge.
1603
+ // Buttons beside a textarea line up with nothing once the box grows; a bar
1604
+ // inside the same border always reads as part of the message being written.
1605
+ .composerBox {
1606
+ display: flex;
1607
+ flex-direction: column;
1608
+ box-sizing: border-box;
1609
+ border: 1px solid var(--s-line);
1610
+ border-radius: calc(var(--s-btn-br) + 4px);
1611
+ background: var(--s-background);
1612
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
1613
+
1614
+ &:focus-within {
1615
+ border-color: var(--s-primary);
1616
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--s-primary) 14%, transparent);
1617
+ }
1618
+ }
1619
+
1620
+ .composerInput {
1621
+ @include sms-reset;
1622
+
1623
+ flex: 1;
1624
+ box-sizing: border-box;
1625
+ min-width: 0;
1626
+ max-height: 180px;
1627
+ padding: 0.7rem 0.85rem 0.4rem;
1628
+ border: 0;
1629
+ border-radius: inherit;
1630
+ background: transparent;
1631
+ color: var(--s-paragraph);
1632
+ font-size: 0.9rem;
1633
+ line-height: 1.5;
1634
+ // Auto-grow is driven from JS; this stops the manual handle fighting it.
1635
+ resize: none;
1636
+ overflow-y: auto;
1637
+ outline: none;
1638
+
1639
+ &::placeholder {
1640
+ color: var(--s-muted);
1641
+ opacity: 1;
1642
+ }
1643
+
1644
+ &:disabled {
1645
+ opacity: 0.7;
1646
+ }
1647
+ }
1648
+
1649
+ .composerBar {
1650
+ display: flex;
1651
+ flex-wrap: wrap;
1652
+ gap: 0.5rem 0.75rem;
1653
+ align-items: center;
1654
+ justify-content: space-between;
1655
+ padding: 0.4rem 0.5rem 0.5rem 0.6rem;
1656
+ }
1657
+
1658
+ .composerTools,
1659
+ .composerActions {
1660
+ display: inline-flex;
1661
+ gap: 0.6rem;
1662
+ align-items: center;
1663
+ min-width: 0;
1664
+ }
1665
+
1666
+ // The templates button sits at the LEFT of the bar, so its menu opens to the
1667
+ // right of it: a right-anchored menu would spill past the pane's left edge
1668
+ // and be clipped by .convPane's overflow.
1669
+ .composerTools .menu {
1670
+ right: auto;
1671
+ left: 0;
1672
+ }
1673
+
1674
+ .composerTools .ghostButton {
1675
+ padding: 0.3rem 0.6rem;
1676
+ border-color: transparent;
1677
+ background: transparent;
1678
+ color: var(--s-secondary);
1679
+ font-weight: 500;
1680
+
1681
+ &:hover:not(:disabled) {
1682
+ border-color: transparent;
1683
+ background: var(--s-hover);
1684
+ color: var(--s-primary);
1685
+ }
1686
+ }
1687
+
1688
+ // Send is the one filled control in the bar, and a pill so it reads as the
1689
+ // action rather than as another field.
1690
+ .sendButton {
1691
+ @include sms-reset;
1692
+ @include sms-focus;
1693
+
1694
+ display: inline-flex;
1695
+ flex-shrink: 0;
1696
+ gap: 0.45rem;
1697
+ align-items: center;
1698
+ justify-content: center;
1699
+ padding: 0.45rem 1rem 0.45rem 0.85rem;
1700
+ border: 1px solid var(--s-primary);
1701
+ border-radius: 999px;
1702
+ background: var(--s-primary);
1703
+ color: #fff;
1704
+ font-size: 0.82rem;
1705
+ font-weight: 600;
1706
+ white-space: nowrap;
1707
+ cursor: pointer;
1708
+ transition: background-color 0.15s ease, transform 0.1s ease, opacity 0.15s ease;
1709
+
1710
+ svg {
1711
+ flex-shrink: 0;
1712
+ color: currentColor;
1713
+ }
1714
+
1715
+ &:hover:not(:disabled) {
1716
+ border-color: color-mix(in srgb, var(--s-primary) 85%, #000);
1717
+ background: color-mix(in srgb, var(--s-primary) 85%, #000);
1718
+ }
1719
+
1720
+ &:active:not(:disabled) {
1721
+ transform: translateY(1px);
1722
+ }
1723
+
1724
+ &:disabled {
1725
+ border-color: transparent;
1726
+ background: color-mix(in srgb, var(--s-paragraph) 18%, transparent);
1727
+ color: var(--s-background);
1728
+ cursor: default;
1729
+ }
1730
+ }
1731
+
1732
+ .counter {
1733
+ display: inline-flex;
1734
+ gap: 0.4rem;
1735
+ align-items: center;
1736
+ color: var(--s-muted);
1737
+ font-size: 0.72rem;
1738
+ // The whole point of a counter is that it does not jitter as it counts.
1739
+ font-variant-numeric: tabular-nums;
1740
+ }
1741
+
1742
+ .counterOver {
1743
+ color: var(--s-bad);
1744
+ font-weight: 700;
1745
+ }
1746
+
1747
+ // A small switch, not a checkbox with a sentence beside it.
1748
+ //
1749
+ // It is a preference about one key, read at a glance and flipped rarely; a
1750
+ // labelled checkbox plus an explanation of what the other state does took
1751
+ // more room than the counter it sits beside.
1752
+ .toggle {
1753
+ @include sms-reset;
1754
+ @include sms-focus;
1755
+
1756
+ display: inline-flex;
1757
+ flex-shrink: 0;
1758
+ gap: 0.4rem;
1759
+ align-items: center;
1760
+ padding: 0.15rem 0.45rem 0.15rem 0.3rem;
1761
+ border: 1px solid var(--s-line);
1762
+ border-radius: 999px;
1763
+ background: var(--s-background);
1764
+ color: var(--s-muted);
1765
+ font-size: 0.7rem;
1766
+ font-weight: 600;
1767
+ white-space: nowrap;
1768
+ cursor: pointer;
1769
+
1770
+ &:hover {
1771
+ border-color: var(--s-primary);
1772
+ color: var(--s-primary);
1773
+ }
1774
+ }
1775
+
1776
+ .toggleTrack {
1777
+ position: relative;
1778
+ display: inline-block;
1779
+ flex: 0 0 auto;
1780
+ width: 22px;
1781
+ height: 12px;
1782
+ border-radius: 999px;
1783
+ background: color-mix(in srgb, var(--s-paragraph) 28%, transparent);
1784
+ transition: background-color 0.15s ease;
1785
+ }
1786
+
1787
+ .toggleKnob {
1788
+ position: absolute;
1789
+ top: 1px;
1790
+ left: 1px;
1791
+ width: 10px;
1792
+ height: 10px;
1793
+ border-radius: 50%;
1794
+ background: #fff;
1795
+ transition: transform 0.15s ease;
1796
+ }
1797
+
1798
+ .toggleOn {
1799
+ border-color: var(--s-primary);
1800
+ color: var(--s-primary);
1801
+
1802
+ .toggleTrack {
1803
+ background: var(--s-primary);
1804
+ }
1805
+
1806
+ .toggleKnob {
1807
+ transform: translateX(10px);
1808
+ }
1809
+ }
1810
+
1811
+ @media (prefers-reduced-motion: reduce) {
1812
+ .toggleTrack,
1813
+ .toggleKnob {
1814
+ transition: none;
1815
+ }
1816
+ }
1817
+
1818
+ // The templates menu, anchored to its button.
1819
+ .menuWrap {
1820
+ position: relative;
1821
+ display: inline-flex;
1822
+ }
1823
+
1824
+ .menu {
1825
+ position: absolute;
1826
+ right: 0;
1827
+ bottom: calc(100% + 6px);
1828
+ z-index: 40;
1829
+ box-sizing: border-box;
1830
+ width: 300px;
1831
+ max-width: calc(100vw - 2rem);
1832
+ max-height: 260px;
1833
+ padding: 0.25rem;
1834
+ border: 1px solid var(--s-line);
1835
+ border-radius: var(--s-br);
1836
+ background: var(--s-background);
1837
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.18);
1838
+ overflow-y: auto;
1839
+ animation: smsPopIn 0.14s ease-out;
1840
+ }
1841
+
1842
+ .menuItem {
1843
+ @include sms-reset;
1844
+ @include sms-focus;
1845
+
1846
+ display: block;
1847
+ box-sizing: border-box;
1848
+ width: 100%;
1849
+ padding: 0.4rem 0.5rem;
1850
+ border: none;
1851
+ border-radius: var(--s-btn-br);
1852
+ background: transparent;
1853
+ color: var(--s-paragraph);
1854
+ font-size: 0.82rem;
1855
+ cursor: pointer;
1856
+
1857
+ strong {
1858
+ display: block;
1859
+ color: var(--s-paragraph);
1860
+ font-size: 0.82rem;
1861
+ font-weight: 700;
1862
+ }
1863
+
1864
+ small {
1865
+ display: block;
1866
+ overflow: hidden;
1867
+ color: var(--s-muted);
1868
+ font-size: 0.74rem;
1869
+ text-overflow: ellipsis;
1870
+ white-space: nowrap;
1871
+ }
1872
+
1873
+ &:hover {
1874
+ background: var(--s-hover);
1875
+ }
1876
+ }
1877
+
1878
+ .menuEmpty {
1879
+ padding: 0.6rem 0.5rem;
1880
+ color: var(--s-muted);
1881
+ font-size: 0.78rem;
1882
+ text-align: center;
1883
+ }
1884
+
1885
+ // ---------------------------------------------------------------------------
1886
+ // Compose modal and the client typeahead
1887
+ // ---------------------------------------------------------------------------
1888
+
1889
+ .modalForm {
1890
+ @include sms-tokens;
1891
+
1892
+ display: flex;
1893
+ flex-direction: column;
1894
+ gap: 0.85rem;
1895
+ box-sizing: border-box;
1896
+ padding: 1rem;
1897
+ color: var(--s-paragraph);
1898
+ font-size: 0.85rem;
1899
+ text-align: left;
1900
+ }
1901
+
1902
+ .modalHead {
1903
+ display: flex;
1904
+ gap: 0.6rem;
1905
+ align-items: center;
1906
+ justify-content: space-between;
1907
+ }
1908
+
1909
+ .modalTitle {
1910
+ display: flex;
1911
+ gap: 0.45rem;
1912
+ align-items: center;
1913
+ margin: 0;
1914
+ color: var(--s-paragraph);
1915
+ font-size: 1.05rem;
1916
+ font-weight: 700;
1917
+
1918
+ svg {
1919
+ color: var(--s-primary);
1920
+ }
1921
+ }
1922
+
1923
+ .modalActions {
1924
+ display: flex;
1925
+ gap: 0.6rem;
1926
+ align-items: center;
1927
+ justify-content: flex-end;
1928
+ padding-top: 0.6rem;
1929
+ border-top: 1px solid var(--s-line);
1930
+ }
1931
+
1932
+ .typeahead {
1933
+ position: relative;
1934
+ }
1935
+
1936
+ .typeaheadList {
1937
+ position: absolute;
1938
+ top: calc(100% + 4px);
1939
+ right: 0;
1940
+ left: 0;
1941
+ z-index: 30;
1942
+ box-sizing: border-box;
1943
+ max-height: 240px;
1944
+ margin: 0;
1945
+ padding: 0.2rem;
1946
+ border: 1px solid var(--s-line);
1947
+ border-radius: var(--s-br);
1948
+ background: var(--s-background);
1949
+ list-style: none;
1950
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.18);
1951
+ overflow-y: auto;
1952
+ }
1953
+
1954
+ .typeaheadRow {
1955
+ @include sms-reset;
1956
+ @include sms-focus;
1957
+
1958
+ display: flex;
1959
+ gap: 0.5rem;
1960
+ align-items: center;
1961
+ box-sizing: border-box;
1962
+ width: 100%;
1963
+ padding: 0.4rem 0.5rem;
1964
+ border: none;
1965
+ border-radius: var(--s-btn-br);
1966
+ background: transparent;
1967
+ color: var(--s-paragraph);
1968
+ font-size: 0.83rem;
1969
+ cursor: pointer;
1970
+
1971
+ &:hover {
1972
+ background: var(--s-hover);
1973
+ }
1974
+
1975
+ strong {
1976
+ color: var(--s-paragraph);
1977
+ font-weight: 700;
1978
+ }
1979
+
1980
+ small {
1981
+ color: var(--s-muted);
1982
+ font-size: 0.75rem;
1983
+ font-variant-numeric: tabular-nums;
1984
+ }
1985
+ }
1986
+
1987
+ .typeaheadActive {
1988
+ background: var(--s-tint);
1989
+ }
1990
+
1991
+ .numberChoices {
1992
+ display: flex;
1993
+ flex-wrap: wrap;
1994
+ gap: 0.35rem;
1995
+ margin-top: 0.3rem;
1996
+ }
1997
+
1998
+ // ---------------------------------------------------------------------------
1999
+ // SmsLineSettings
2000
+ // ---------------------------------------------------------------------------
2001
+
2002
+ .settings {
2003
+ @include sms-tokens;
2004
+
2005
+ box-sizing: border-box;
2006
+ padding: 1.25rem 1.5rem 2rem;
2007
+ color: var(--s-paragraph);
2008
+ font-size: 0.85rem;
2009
+ }
2010
+
2011
+ .settingsHead {
2012
+ display: flex;
2013
+ flex-wrap: wrap;
2014
+ gap: 1rem;
2015
+ align-items: flex-start;
2016
+ justify-content: space-between;
2017
+ margin-bottom: 1rem;
2018
+ }
2019
+
2020
+ .pageIntro {
2021
+ max-width: 62ch;
2022
+ margin: 0.35rem 0 0;
2023
+ color: var(--s-muted);
2024
+ font-size: 0.83rem;
2025
+ line-height: 1.5;
2026
+ }
2027
+
2028
+ /* The practice's texting numbers, always in view: a client asking "what number
2029
+ do I text?" is answered from the page header, not from Settings. */
2030
+ .lineNumbers {
2031
+ display: flex;
2032
+ flex-wrap: wrap;
2033
+ gap: 0.4rem 0.6rem;
2034
+ margin: 0.55rem 0 0;
2035
+ padding: 0;
2036
+ list-style: none;
2037
+ }
2038
+
2039
+ .lineNumber {
2040
+ display: inline-flex;
2041
+ align-items: baseline;
2042
+ gap: 0.4rem;
2043
+ padding: 0.22rem 0.6rem;
2044
+ border: 1px solid var(--s-line);
2045
+ border-radius: 999px;
2046
+ background: var(--s-surface);
2047
+ font-size: 0.8rem;
2048
+ line-height: 1.3;
2049
+ }
2050
+
2051
+ .lineNumberLabel {
2052
+ color: var(--s-muted);
2053
+ }
2054
+
2055
+ .lineNumberValue {
2056
+ font-variant-numeric: tabular-nums;
2057
+ letter-spacing: 0.02em;
2058
+ color: var(--s-paragraph);
2059
+ user-select: all;
2060
+ }
2061
+
2062
+ .statusCard {
2063
+ display: flex;
2064
+ flex-wrap: wrap;
2065
+ gap: 0.75rem 1.5rem;
2066
+ box-sizing: border-box;
2067
+ margin-bottom: 1.25rem;
2068
+ padding: 0.85rem 1rem;
2069
+ border: 1px solid var(--s-line);
2070
+ border-radius: var(--s-br);
2071
+ background: var(--s-surface);
2072
+ }
2073
+
2074
+ .statusItem {
2075
+ display: flex;
2076
+ flex-direction: column;
2077
+ gap: 0.2rem;
2078
+ min-width: 140px;
2079
+ }
2080
+
2081
+ .statusLabel {
2082
+ color: var(--s-muted);
2083
+ font-size: 0.72rem;
2084
+ font-weight: 700;
2085
+ letter-spacing: 0.04em;
2086
+ text-transform: uppercase;
2087
+ }
2088
+
2089
+ .statusValue {
2090
+ margin: 0;
2091
+ color: var(--s-paragraph);
2092
+ font-size: 0.88rem;
2093
+ font-weight: 600;
2094
+ }
2095
+
2096
+ .statusMeaning {
2097
+ flex: 1 1 260px;
2098
+ min-width: 220px;
2099
+ color: var(--s-muted);
2100
+ font-size: 0.8rem;
2101
+ line-height: 1.5;
2102
+ }
2103
+
2104
+ .section {
2105
+ margin-bottom: 1.75rem;
2106
+ }
2107
+
2108
+ .sectionHead {
2109
+ display: flex;
2110
+ flex-wrap: wrap;
2111
+ gap: 0.75rem;
2112
+ align-items: center;
2113
+ justify-content: space-between;
2114
+ margin-bottom: 0.6rem;
2115
+ }
2116
+
2117
+ .sectionTitle {
2118
+ margin: 0;
2119
+ color: var(--s-paragraph);
2120
+ font-size: 1.05rem;
2121
+ font-weight: 700;
2122
+ }
2123
+
2124
+ .tableWrap {
2125
+ overflow-x: auto;
2126
+ border: 1px solid var(--s-line);
2127
+ border-radius: var(--s-br);
2128
+ background: var(--s-background);
2129
+ }
2130
+
2131
+ .table {
2132
+ width: 100%;
2133
+ border-collapse: collapse;
2134
+ color: var(--s-paragraph);
2135
+ font-size: 0.84rem;
2136
+ text-align: left;
2137
+
2138
+ th,
2139
+ td {
2140
+ padding: 0.6rem 0.7rem;
2141
+ text-align: left;
2142
+ vertical-align: middle;
2143
+ }
2144
+
2145
+ thead th {
2146
+ border-bottom: 1px solid var(--s-line);
2147
+ background: var(--s-surface);
2148
+ color: var(--s-muted);
2149
+ font-size: 0.72rem;
2150
+ font-weight: 700;
2151
+ letter-spacing: 0.05em;
2152
+ text-transform: uppercase;
2153
+ white-space: nowrap;
2154
+ }
2155
+
2156
+ tbody tr {
2157
+ border-bottom: 1px solid color-mix(in srgb, var(--s-line) 55%, transparent);
2158
+
2159
+ &:last-child {
2160
+ border-bottom: none;
2161
+ }
2162
+
2163
+ &:hover {
2164
+ background: var(--s-hover);
2165
+ }
2166
+ }
2167
+
2168
+ tbody td strong {
2169
+ color: var(--s-paragraph);
2170
+ font-size: 0.88rem;
2171
+ font-weight: 700;
2172
+ }
2173
+ }
2174
+
2175
+ .numberCell {
2176
+ font-variant-numeric: tabular-nums;
2177
+ white-space: nowrap;
2178
+ }
2179
+
2180
+ .actionsHead,
2181
+ .actionsCell {
2182
+ text-align: right;
2183
+ white-space: nowrap;
2184
+ }
2185
+
2186
+ .actionsCell > * {
2187
+ margin-left: 0.5rem;
2188
+ }
2189
+
2190
+ .chipRow {
2191
+ display: flex;
2192
+ flex-wrap: wrap;
2193
+ gap: 0.25rem;
2194
+ }
2195
+
2196
+ .staffChip {
2197
+ display: inline-flex;
2198
+ align-items: center;
2199
+ padding: 0.1rem 0.45rem;
2200
+ border-radius: 999px;
2201
+ background: var(--s-tint);
2202
+ color: var(--s-primary);
2203
+ font-size: 0.7rem;
2204
+ font-weight: 700;
2205
+ }
2206
+
2207
+ .tableEmpty {
2208
+ padding: 2.25rem 1rem;
2209
+ color: var(--s-muted);
2210
+ text-align: center;
2211
+
2212
+ p {
2213
+ margin: 0 0 0.9rem;
2214
+ color: var(--s-muted);
2215
+ font-size: 0.88rem;
2216
+ }
2217
+ }
2218
+
2219
+ .staffPicker {
2220
+ display: flex;
2221
+ flex-direction: column;
2222
+ gap: 0.3rem;
2223
+ box-sizing: border-box;
2224
+ max-height: 190px;
2225
+ padding: 0.5rem 0.6rem;
2226
+ border: 1px solid var(--s-line);
2227
+ border-radius: var(--s-btn-br);
2228
+ background: var(--s-background);
2229
+ overflow-y: auto;
2230
+ }
2231
+
2232
+ .deletedRow {
2233
+ opacity: 0.65;
2234
+
2235
+ strong {
2236
+ text-decoration: line-through;
2237
+ }
2238
+ }
2239
+
2240
+ // ---------------------------------------------------------------------------
2241
+ // Narrow screens
2242
+ // ---------------------------------------------------------------------------
2243
+
2244
+ // An iPad in landscape is 1024px and the CRM's chrome takes some of that: the
2245
+ // list pane loses 60px rather than the conversation losing its bubbles.
2246
+ @media (max-width: 1180px) {
2247
+ .panes {
2248
+ grid-template-columns: 280px minmax(0, 1fr);
2249
+ }
2250
+ }
2251
+
2252
+ // Below 900 the two panes stop being two panes: one is on screen at a time,
2253
+ // and the conversation carries a back button. Which one shows is a class the
2254
+ // component sets, not a media query guess.
2255
+ @media (max-width: 900px) {
2256
+ .inbox {
2257
+ padding: 0.85rem 0.85rem 1rem;
2258
+ }
2259
+
2260
+ .panes {
2261
+ grid-template-columns: minmax(0, 1fr);
2262
+ height: calc(100vh - 170px);
2263
+ }
2264
+
2265
+ .paneHiddenNarrow {
2266
+ display: none;
2267
+ }
2268
+
2269
+ .bubble {
2270
+ max-width: 88%;
2271
+ }
2272
+
2273
+ .settings {
2274
+ padding: 1rem 0.85rem 2rem;
2275
+ }
2276
+
2277
+ .actionsCell {
2278
+ text-align: left;
2279
+
2280
+ > * {
2281
+ margin: 0.15rem 0.5rem 0.15rem 0;
2282
+ }
2283
+ }
2284
+ }
2285
+
2286
+ // The back button only exists in the collapsed layout.
2287
+ .backButton {
2288
+ display: none;
2289
+ }
2290
+
2291
+ @media (max-width: 900px) {
2292
+ .backButton {
2293
+ display: inline-flex;
2294
+ }
2295
+ }
2296
+
2297
+ // ---------------------------------------------------------------------------
2298
+ // The compose modal's header row
2299
+ // ---------------------------------------------------------------------------
2300
+
2301
+ // From and To on one line. Two full-width blocks stacked above the message
2302
+ // pushed the thing anyone actually came here to write below the fold, and
2303
+ // "From" is not a decision on a one-line system - it is a fact.
2304
+ .composeRow {
2305
+ display: grid;
2306
+ gap: 0.5rem 0.75rem;
2307
+ grid-template-columns: minmax(0, 220px) 1fr;
2308
+ align-items: start;
2309
+ }
2310
+
2311
+ .composeField {
2312
+ display: flex;
2313
+ flex-direction: column;
2314
+ gap: 0.2rem;
2315
+ min-width: 0;
2316
+ }
2317
+
2318
+ // Quieter and smaller than .label: at this density the labels are wayfinding,
2319
+ // not headings.
2320
+ .labelSmall {
2321
+ color: var(--s-muted);
2322
+ font-size: 0.68rem;
2323
+ font-weight: 700;
2324
+ letter-spacing: 0.05em;
2325
+ text-transform: uppercase;
2326
+ }
2327
+
2328
+ // The single-line case: a select with one option is a question with one
2329
+ // answer, so the line is stated rather than asked.
2330
+ .fromChip {
2331
+ display: flex;
2332
+ gap: 0.4rem;
2333
+ align-items: baseline;
2334
+ box-sizing: border-box;
2335
+ min-height: var(--s-control);
2336
+ padding: 0.35rem 0.6rem;
2337
+ border: 1px solid var(--s-line);
2338
+ border-radius: var(--s-btn-br);
2339
+ background: var(--s-surface);
2340
+ color: var(--s-paragraph);
2341
+ font-size: 0.83rem;
2342
+ line-height: 1.3;
2343
+
2344
+ strong {
2345
+ overflow: hidden;
2346
+ font-weight: 700;
2347
+ text-overflow: ellipsis;
2348
+ white-space: nowrap;
2349
+ }
2350
+
2351
+ small {
2352
+ color: var(--s-muted);
2353
+ font-size: 0.75rem;
2354
+ font-variant-numeric: tabular-nums;
2355
+ white-space: nowrap;
2356
+ }
2357
+ }
2358
+
2359
+ // The modal's own copy of the thread composer's card, which wants a little
2360
+ // more room to write in than the bar at the bottom of a conversation.
2361
+ .composeBody {
2362
+ min-height: 92px;
2363
+ max-height: 220px;
2364
+ padding-bottom: 0.5rem;
2365
+ resize: vertical;
2366
+ }
2367
+
2368
+ .composeForm {
2369
+ gap: 0.7rem;
2370
+ }
2371
+
2372
+ // The counter sits beside Templates in the bar rather than under the box, so
2373
+ // it needs to stop looking like a field label and start looking like a note.
2374
+ .composeBar {
2375
+ justify-content: flex-start;
2376
+ }
2377
+
2378
+ @media (max-width: 639px) {
2379
+ .composeRow {
2380
+ grid-template-columns: 1fr;
2381
+ }
2382
+ }