@visns-studio/visns-components 6.6.4 → 6.15.1

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 (98) hide show
  1. package/README.md +1623 -18
  2. package/package.json +10 -4
  3. package/src/components/DataGrid.jsx +181 -38
  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/ColumnRenderers.jsx +53 -7
  32. package/src/components/generic/GenericAuth.jsx +163 -96
  33. package/src/components/generic/GenericDetail.jsx +215 -90
  34. package/src/components/generic/GenericIndex.jsx +20 -47
  35. package/src/components/generic/GenericMain.jsx +5 -0
  36. package/src/components/generic/GroupedReportRenderer.jsx +1 -5
  37. package/src/components/generic/StandardModal.jsx +17 -5
  38. package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
  39. package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
  40. package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
  41. package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
  42. package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
  43. package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
  44. package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
  45. package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
  46. package/src/components/navActive.js +60 -0
  47. package/src/components/notify/desktopNotifications.js +256 -0
  48. package/src/components/sketch/SketchField.jsx +12 -2
  49. package/src/components/sms/SmsComposeModal.jsx +495 -0
  50. package/src/components/sms/SmsInbox.jsx +596 -0
  51. package/src/components/sms/SmsInboxBadge.jsx +468 -0
  52. package/src/components/sms/SmsLineSettings.jsx +854 -0
  53. package/src/components/sms/SmsThreadPanel.jsx +1184 -0
  54. package/src/components/sms/smsEndpoints.js +56 -0
  55. package/src/components/sms/smsHelpers.js +1075 -0
  56. package/src/components/sms/smsLiveState.js +132 -0
  57. package/src/components/sms/useSmsLive.js +307 -0
  58. package/src/components/styles/CallQueuePop.module.scss +646 -0
  59. package/src/components/styles/CallQueueSettings.module.scss +460 -0
  60. package/src/components/styles/ClientAuth.module.scss +229 -0
  61. package/src/components/styles/GenericDetail.module.scss +153 -26
  62. package/src/components/styles/GenericIndex.module.scss +13 -0
  63. package/src/components/styles/GenericMain.module.scss +19 -2
  64. package/src/components/styles/ImpersonateGate.module.scss +85 -0
  65. package/src/components/styles/Login.module.scss +13 -122
  66. package/src/components/styles/LogoutScreen.module.scss +89 -0
  67. package/src/components/styles/Navigation.module.scss +509 -223
  68. package/src/components/styles/Notification.module.scss +103 -0
  69. package/src/components/styles/Reset.module.scss +52 -186
  70. package/src/components/styles/Sms.module.scss +2382 -0
  71. package/src/components/styles/TableFilter.module.scss +118 -98
  72. package/src/components/styles/TwoFactorAuth.module.scss +115 -176
  73. package/src/components/styles/Vault.module.scss +1983 -0
  74. package/src/components/styles/Verify.module.scss +57 -180
  75. package/src/components/styles/_authBrandPanel.scss +163 -0
  76. package/src/components/styles/_authShell.scss +369 -0
  77. package/src/components/styles/global-datagrid.css +27 -2
  78. package/src/components/utils/buildEnv.js +67 -0
  79. package/src/components/utils/displayValue.js +94 -0
  80. package/src/components/utils/useDensity.js +345 -9
  81. package/src/components/vault/OtpChip.jsx +208 -0
  82. package/src/components/vault/PasswordGenerator.jsx +145 -0
  83. package/src/components/vault/QrScanner.jsx +326 -0
  84. package/src/components/vault/VaultAccessLog.jsx +225 -0
  85. package/src/components/vault/VaultConfirmPanel.jsx +113 -0
  86. package/src/components/vault/VaultEntryForm.jsx +740 -0
  87. package/src/components/vault/VaultManager.jsx +1000 -0
  88. package/src/components/vault/VaultQuickSearch.jsx +681 -0
  89. package/src/components/vault/qrDecode.js +257 -0
  90. package/src/components/vault/useDebouncedValue.js +22 -0
  91. package/src/components/vault/useVaultReveal.js +160 -0
  92. package/src/components/vault/vaultClipboard.js +33 -0
  93. package/src/components/vault/vaultEndpoints.js +40 -0
  94. package/src/components/vault/vaultFit.js +116 -0
  95. package/src/components/vault/vaultHelpers.js +547 -0
  96. package/src/components/vault/vaultNavigation.jsx +58 -0
  97. package/src/components/vault/vaultOtp.js +105 -0
  98. package/src/index.js +233 -1
@@ -0,0 +1,460 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Call Queue Settings (.cqsettings)
3
+ // ---------------------------------------------------------------------------
4
+ // The admin table of every Zoom call queue with its pickup code and pop
5
+ // exclusion (components/callQueue/CallQueueSettings.jsx). Sibling of
6
+ // CallQueuePop.module.scss because they are two ends of one feature — the
7
+ // codes edited here are what the pop's Pick up button dials.
8
+ //
9
+ // Ported from the `.cqsettings` half of the CRM's resources/css/_call-queue.scss.
10
+ // The sass `config` variables it used do not exist in this library, so every
11
+ // brand value is a CSS custom property with a neutral fallback.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ $cqsettings-mono: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
15
+
16
+ .cqsettings {
17
+ // Local aliases only — every brand value comes from the consuming app's
18
+ // token layer so this library stays reusable.
19
+ --cq-primary: var(--primary-color, #00386c);
20
+ --cq-secondary: var(--secondary-color, #404040);
21
+ --cq-tertiary: var(--tertiary-color, #fff);
22
+ --cq-paragraph: var(--paragraph-color, #2b2b2b);
23
+ --cq-background: var(--background-color, #fff);
24
+ --cq-br: var(--br, 5px);
25
+ --cq-btn-br: var(--btn-br, 5px);
26
+
27
+ // Reused from the pop so the two ends of the feature agree on what "good"
28
+ // and "wrong" look like.
29
+ --cq-ok: rgb(43, 145, 85);
30
+ --cq-bad: rgb(214, 100, 100);
31
+
32
+ // Full-width layout: the queue-name column absorbs the slack so the
33
+ // control cluster keeps its size and sits at the row's end.
34
+ width: 100%;
35
+ padding: 1rem 1.5rem 2rem;
36
+
37
+ // The CRM relied on a global `spin` keyframe from main.scss; this library
38
+ // has none, so the animation is defined at the bottom of this module and
39
+ // the helper class is a module class the JSX references as styles.spin.
40
+ .spin {
41
+ animation: spin 1s linear infinite;
42
+ }
43
+
44
+ // --- the copy ----------------------------------------------------------
45
+ // One muted line saying what the page is, then one callout for the single
46
+ // caveat that actually changes what an admin has to do. Nothing else.
47
+ .cqsettingsIntro {
48
+ margin: 0 0 0.75rem;
49
+ color: color-mix(in srgb, var(--cq-paragraph) 70%, transparent);
50
+ font-size: 0.82rem;
51
+ line-height: 1.5;
52
+ }
53
+
54
+ .cqsettingsInfo,
55
+ .cqsettingsWarning {
56
+ display: flex;
57
+ gap: 0.5rem;
58
+ align-items: flex-start;
59
+ // Full container width so the callout's edges line up with the table
60
+ // panel below it instead of stopping mid-page.
61
+ margin: 0 0 1rem;
62
+ padding: 0.55rem 0.75rem;
63
+ border-radius: var(--cq-br);
64
+ font-size: 0.76rem;
65
+ line-height: 1.45;
66
+
67
+ svg {
68
+ flex: none;
69
+ margin-top: 0.12rem;
70
+ }
71
+ }
72
+
73
+ .cqsettingsInfo {
74
+ border: 1px solid color-mix(in srgb, var(--cq-primary) 18%, transparent);
75
+ background-color: color-mix(in srgb, var(--cq-primary) 5%, transparent);
76
+ color: color-mix(in srgb, var(--cq-paragraph) 80%, transparent);
77
+
78
+ svg {
79
+ color: var(--cq-primary);
80
+ }
81
+ }
82
+
83
+ .cqsettingsWarning {
84
+ border: 1px solid color-mix(in srgb, var(--cq-bad) 35%, transparent);
85
+ background-color: color-mix(in srgb, var(--cq-bad) 8%, transparent);
86
+ color: color-mix(in srgb, var(--cq-bad) 67%, #000);
87
+ }
88
+
89
+ // --- the table ---------------------------------------------------------
90
+ // The panel is the scroll container: on a narrow viewport the table slides
91
+ // inside its own frame instead of pushing the page body sideways.
92
+ .cqsettingsPanel {
93
+ border: 1px solid color-mix(in srgb, var(--cq-primary) 14%, transparent);
94
+ border-radius: var(--cq-br);
95
+ background: var(--cq-background);
96
+ overflow-x: auto;
97
+ }
98
+
99
+ .cqsettingsState {
100
+ display: flex;
101
+ gap: 0.5rem;
102
+ align-items: center;
103
+ padding: 1.25rem;
104
+ color: color-mix(in srgb, var(--cq-paragraph) 60%, transparent);
105
+ font-size: 0.82rem;
106
+ }
107
+
108
+ .cqsettingsTable {
109
+ width: 100%;
110
+ // Below this the controls start colliding — the panel scrolls instead.
111
+ min-width: 940px;
112
+ border-collapse: collapse;
113
+ table-layout: fixed;
114
+ font-size: 0.82rem;
115
+
116
+ th,
117
+ td {
118
+ padding: 0.6rem 1rem;
119
+ text-align: left;
120
+ vertical-align: middle;
121
+ }
122
+
123
+ .isCentred {
124
+ text-align: center;
125
+ }
126
+
127
+ thead th {
128
+ border-bottom: 1px solid
129
+ color-mix(in srgb, var(--cq-primary) 18%, transparent);
130
+ background: #f0f0f0;
131
+ color: var(--cq-primary);
132
+ font-size: 0.68rem;
133
+ font-weight: 700;
134
+ letter-spacing: 0.06em;
135
+ text-transform: uppercase;
136
+ white-space: nowrap;
137
+ }
138
+
139
+ tbody tr {
140
+ border-bottom: 1px solid
141
+ color-mix(in srgb, var(--cq-secondary) 12%, transparent);
142
+ transition: background 0.12s ease;
143
+
144
+ &:last-child {
145
+ border-bottom: none;
146
+ }
147
+
148
+ &:hover {
149
+ background: color-mix(in srgb, var(--cq-primary) 3.5%, transparent);
150
+ }
151
+
152
+ // An excluded queue never pops; the row reads as switched off.
153
+ &.isExcluded {
154
+ background: color-mix(in srgb, var(--cq-secondary) 3.5%, transparent);
155
+
156
+ .cqsettingsName,
157
+ .cqsettingsNumber {
158
+ color: color-mix(in srgb, var(--cq-paragraph) 55%, transparent);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ // The first column has no width: with `table-layout: fixed` it takes
165
+ // whatever the sized columns leave, so the queue name is the only thing
166
+ // that flexes.
167
+ .cqsettingsColNumber {
168
+ width: 170px;
169
+ }
170
+
171
+ .cqsettingsColStatus {
172
+ width: 120px;
173
+ }
174
+
175
+ .cqsettingsColCode {
176
+ width: 190px;
177
+ }
178
+
179
+ .cqsettingsColPop {
180
+ width: 170px;
181
+ }
182
+
183
+ .cqsettingsColExclude {
184
+ width: 110px;
185
+ }
186
+
187
+ .cqsettingsColSave {
188
+ width: 130px;
189
+ }
190
+
191
+ // --- the queue cell ----------------------------------------------------
192
+ .cqsettingsName {
193
+ display: flex;
194
+ gap: 0.4rem;
195
+ align-items: center;
196
+ min-width: 0;
197
+ color: var(--cq-paragraph);
198
+ font-weight: 600;
199
+
200
+ // Queue names come from Zoom admin and can run long; one line, clipped.
201
+ > span {
202
+ overflow: hidden;
203
+ text-overflow: ellipsis;
204
+ white-space: nowrap;
205
+ }
206
+ }
207
+
208
+ // Gives the phone glyph something to sit in so the name has a left edge to
209
+ // hang off rather than floating in the cell.
210
+ .cqsettingsIcon {
211
+ display: inline-flex;
212
+ flex: none;
213
+ align-items: center;
214
+ justify-content: center;
215
+ width: 22px;
216
+ height: 22px;
217
+ border-radius: var(--cq-br);
218
+ background: color-mix(in srgb, var(--cq-primary) 10%, transparent);
219
+ color: var(--cq-primary);
220
+ }
221
+
222
+ // Extension (and the "gone from Zoom" flag) as a quiet second line, indented
223
+ // to the name's own left edge — 22px badge + the 0.4rem gap.
224
+ .cqsettingsMeta {
225
+ display: block;
226
+ margin-top: 0.1rem;
227
+ padding-left: 1.775rem;
228
+ color: color-mix(in srgb, var(--cq-paragraph) 50%, transparent);
229
+ font-size: 0.68rem;
230
+ line-height: 1.4;
231
+ }
232
+
233
+ .cqsettingsFlag {
234
+ color: color-mix(in srgb, var(--cq-bad) 82%, #000);
235
+ font-weight: 600;
236
+
237
+ &::before {
238
+ content: '·';
239
+ margin: 0 0.3rem;
240
+ color: color-mix(in srgb, var(--cq-paragraph) 35%, transparent);
241
+ font-weight: 400;
242
+ }
243
+ }
244
+
245
+ .cqsettingsNumber {
246
+ color: var(--cq-paragraph);
247
+ font-variant-numeric: tabular-nums;
248
+ white-space: nowrap;
249
+ }
250
+
251
+ .cqsettingsNone {
252
+ color: color-mix(in srgb, var(--cq-paragraph) 30%, transparent);
253
+ }
254
+
255
+ // --- pills -------------------------------------------------------------
256
+ // One shape for both status columns: muted by default, tinted green only
257
+ // when the answer is "yes, this is working".
258
+ .cqsettingsPill {
259
+ display: inline-flex;
260
+ align-items: center;
261
+ padding: 0.15rem 0.5rem;
262
+ border-radius: 999px;
263
+ background: color-mix(in srgb, var(--cq-secondary) 12%, transparent);
264
+ color: color-mix(in srgb, var(--cq-paragraph) 60%, transparent);
265
+ font-size: 0.68rem;
266
+ font-weight: 700;
267
+ letter-spacing: 0.02em;
268
+ line-height: 1.5;
269
+ white-space: nowrap;
270
+ }
271
+
272
+ .cqsettingsPillOk {
273
+ background: color-mix(in srgb, var(--cq-ok) 15%, transparent);
274
+ color: color-mix(in srgb, var(--cq-ok) 88%, #000);
275
+ }
276
+
277
+ // --- pickup code -------------------------------------------------------
278
+ // An input group, not a text box: the `*` adornment IS the dialled form, so
279
+ // the row no longer needs a "Dialled as *6439" line under it.
280
+ .cqsettingsCode {
281
+ display: inline-flex;
282
+ align-items: stretch;
283
+ // Wide enough for the *99 prefix cell plus four spaced monospace
284
+ // digits without clipping.
285
+ width: 150px;
286
+ border: 1px solid color-mix(in srgb, var(--cq-secondary) 35%, transparent);
287
+ border-radius: var(--cq-br);
288
+ background: var(--cq-background);
289
+ overflow: hidden;
290
+ transition:
291
+ border-color 0.15s ease,
292
+ box-shadow 0.15s ease;
293
+
294
+ &:focus-within {
295
+ border-color: var(--cq-primary);
296
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--cq-primary) 12%, transparent);
297
+ }
298
+
299
+ &.hasError {
300
+ border-color: var(--cq-bad);
301
+
302
+ &:focus-within {
303
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--cq-bad) 15%, transparent);
304
+ }
305
+ }
306
+ }
307
+
308
+ .cqsettingsCodePrefix {
309
+ display: flex;
310
+ flex: none;
311
+ align-items: center;
312
+ padding: 0 0.45rem;
313
+ border-right: 1px solid
314
+ color-mix(in srgb, var(--cq-secondary) 22%, transparent);
315
+ background: color-mix(in srgb, var(--cq-secondary) 7%, transparent);
316
+ color: color-mix(in srgb, var(--cq-paragraph) 60%, transparent);
317
+ font-family: $cqsettings-mono;
318
+ font-size: 0.85rem;
319
+ font-weight: 700;
320
+ line-height: 1;
321
+ }
322
+
323
+ .cqsettingsCodeInput {
324
+ width: 100%;
325
+ min-width: 0;
326
+ padding: 0.32rem 0.45rem;
327
+ border: none;
328
+ background: none;
329
+ color: var(--cq-paragraph);
330
+ font-family: $cqsettings-mono;
331
+ font-size: 0.85rem;
332
+ font-weight: 600;
333
+ letter-spacing: 0.12em;
334
+
335
+ &:focus {
336
+ outline: none;
337
+ }
338
+
339
+ &::placeholder {
340
+ color: color-mix(in srgb, var(--cq-paragraph) 28%, transparent);
341
+ font-weight: 400;
342
+ }
343
+ }
344
+
345
+ // Only rendered while the typed code breaks a Zoom rule, so it can afford to
346
+ // add a line to the row.
347
+ .cqsettingsError {
348
+ display: block;
349
+ max-width: 120px;
350
+ margin-top: 0.2rem;
351
+ color: color-mix(in srgb, var(--cq-bad) 73%, #000);
352
+ font-size: 0.66rem;
353
+ line-height: 1.3;
354
+ }
355
+
356
+ // --- exclude switch ----------------------------------------------------
357
+ // react-toggle is the switch the rest of the CRM uses; scaled down to table
358
+ // size and recoloured to the house navy, because "on" here means "a setting
359
+ // is applied", not "all good". react-toggle owns those class names, so they
360
+ // stay global inside this module.
361
+ .cqsettingsToggle {
362
+ display: inline-flex;
363
+ align-items: center;
364
+ justify-content: center;
365
+ height: 24px;
366
+
367
+ :global(.react-toggle) {
368
+ transform: scale(0.75);
369
+ }
370
+
371
+ // react-toggle's default unchecked track is near-black (#4D4D4D),
372
+ // which reads as "on". Off must be unmistakably light.
373
+ :global(.react-toggle .react-toggle-track) {
374
+ background-color: color-mix(in srgb, var(--cq-secondary) 35%, transparent);
375
+ }
376
+
377
+ :global(
378
+ .react-toggle:hover:not(.react-toggle--disabled):not(.react-toggle--checked)
379
+ .react-toggle-track
380
+ ) {
381
+ background-color: color-mix(in srgb, var(--cq-secondary) 50%, transparent);
382
+ }
383
+
384
+ :global(.react-toggle--checked .react-toggle-track) {
385
+ background-color: var(--cq-primary);
386
+ }
387
+
388
+ :global(
389
+ .react-toggle--checked:hover:not(.react-toggle--disabled)
390
+ .react-toggle-track
391
+ ) {
392
+ background-color: color-mix(in srgb, var(--cq-primary) 91%, #000);
393
+ }
394
+
395
+ :global(.react-toggle--checked .react-toggle-thumb) {
396
+ border-color: var(--cq-primary);
397
+ }
398
+
399
+ :global(.react-toggle--focus .react-toggle-thumb),
400
+ :global(.react-toggle:active:not(.react-toggle--disabled) .react-toggle-thumb) {
401
+ box-shadow: 0 0 2px 3px color-mix(in srgb, var(--cq-primary) 35%, transparent);
402
+ }
403
+ }
404
+
405
+ // --- save --------------------------------------------------------------
406
+ // Primary colour while there is something to save, visibly muted (not just
407
+ // faded) when the row is clean, so "nothing to do" never reads as "broken".
408
+ .cqsettingsSave {
409
+ display: inline-flex;
410
+ align-items: center;
411
+ justify-content: center;
412
+ gap: 0.35rem;
413
+ padding: 0.34rem 0.65rem;
414
+ border: 1px solid color-mix(in srgb, var(--cq-primary) 91%, #000);
415
+ border-radius: var(--cq-btn-br);
416
+ background: var(--cq-primary);
417
+ color: var(--cq-tertiary);
418
+ font-family: inherit;
419
+ font-size: 0.74rem;
420
+ font-weight: 700;
421
+ line-height: 1.4;
422
+ cursor: pointer;
423
+ transition:
424
+ background 0.15s ease,
425
+ border-color 0.15s ease,
426
+ color 0.15s ease;
427
+
428
+ svg {
429
+ flex: none;
430
+ }
431
+
432
+ &:hover:not(:disabled) {
433
+ background: color-mix(in srgb, var(--cq-primary) 88%, #000);
434
+ }
435
+
436
+ &:focus-visible {
437
+ outline: 2px solid color-mix(in srgb, var(--cq-primary) 45%, transparent);
438
+ outline-offset: 1px;
439
+ }
440
+
441
+ &:disabled {
442
+ border-color: color-mix(in srgb, var(--cq-secondary) 20%, transparent);
443
+ background: color-mix(in srgb, var(--cq-secondary) 7%, transparent);
444
+ color: color-mix(in srgb, var(--cq-paragraph) 40%, transparent);
445
+ cursor: default;
446
+ }
447
+ }
448
+ }
449
+
450
+ // The CRM had this globally in main.scss; the library ships it with the module
451
+ // that needs it so `.spin` is never an animation-less no-op.
452
+ @keyframes spin {
453
+ from {
454
+ transform: rotate(0deg);
455
+ }
456
+
457
+ to {
458
+ transform: rotate(360deg);
459
+ }
460
+ }
@@ -1,3 +1,5 @@
1
+ @use './authBrandPanel' as brand;
2
+
1
3
  // Client Auth styles
2
4
  .container {
3
5
  min-height: 100vh;
@@ -165,3 +167,230 @@
165
167
  color: var(--secondary-color, #3cbf7d);
166
168
  }
167
169
  }
170
+
171
+ // --- 6.7.0 additions, all rendered only behind an opt-in prop ---------------
172
+
173
+ // The live "Email address detected" line under the contact field. Quiet: it
174
+ // confirms what the field already shows, it does not instruct.
175
+ .inputHint {
176
+ margin: 0.4rem 0 0;
177
+ font-size: 0.8rem;
178
+ line-height: 1.4;
179
+ color: #888;
180
+ }
181
+
182
+ // Development-only OTP panel. Deliberately loud — it must be impossible to
183
+ // mistake a build showing this for a production one.
184
+ .devOtp {
185
+ display: flex;
186
+ flex-wrap: wrap;
187
+ gap: 0.5rem;
188
+ align-items: center;
189
+ justify-content: space-between;
190
+ margin-top: 0.6rem;
191
+ padding: 0.5rem 0.7rem;
192
+ border: 1px dashed #e0a800;
193
+ border-radius: 4px;
194
+ background-color: #fff8e1;
195
+ color: #7a5b00;
196
+ font-size: 0.82rem;
197
+
198
+ strong {
199
+ font-family: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
200
+ letter-spacing: 0.04em;
201
+ }
202
+ }
203
+
204
+ .devOtpFill {
205
+ padding: 0.25rem 0.6rem;
206
+ border: 1px solid #e0a800;
207
+ border-radius: 4px;
208
+ background-color: #ffe9a8;
209
+ color: #7a5b00;
210
+ font-family: inherit;
211
+ font-size: 0.78rem;
212
+ font-weight: 600;
213
+ cursor: pointer;
214
+ transition: background-color 0.2s ease;
215
+
216
+ &:hover {
217
+ background-color: #ffdf85;
218
+ }
219
+ }
220
+
221
+ // The read-only contact echo above the code field (`showContactField`), and
222
+ // its masked caption. Muted rather than greyed to unreadability: it is there to
223
+ // be read, it is simply not there to be edited.
224
+ .disabledInput {
225
+ background-color: #f4f4f5;
226
+ color: #666;
227
+ cursor: default;
228
+
229
+ &:disabled {
230
+ // Safari and Firefox each fade a disabled input differently; stating
231
+ // the colour keeps the value legible in both.
232
+ opacity: 1;
233
+ -webkit-text-fill-color: #666;
234
+ }
235
+ }
236
+
237
+ .contactInfo {
238
+ margin: 0.4rem 0 0;
239
+ font-size: 0.8rem;
240
+ line-height: 1.4;
241
+ color: #888;
242
+ }
243
+
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Split layout (`layout="split"`)
247
+ // ---------------------------------------------------------------------------
248
+ // The card layout above floats a small box in whatever vertical space the host
249
+ // page gives it, which on the portal meant a login card marooned in a tall
250
+ // empty band. This is the staff sign-in's design language brought across: the
251
+ // brand on one side, the task on the other.
252
+ //
253
+ // It is a SECTION, not a screen. The staff `.auth` is `position: fixed;
254
+ // inset: 0` because it owns the whole viewport; this one cannot be, because the
255
+ // consumer keeps its own header and footer and hands this the space between.
256
+ // Hence `height: 100%` with a `flex: 1` fallback and no `100vh` anywhere — the
257
+ // parent decides how tall this is.
258
+ // ---------------------------------------------------------------------------
259
+
260
+ .split {
261
+ // Local aliases, same names and same neutral fallbacks as the staff
262
+ // screens, so one token layer themes the whole product. The brand panel
263
+ // mixin below reads --navy-deep and --flag from here.
264
+ --ink: var(--header-color, #10192a);
265
+ --navy: var(--primary-color, #26364a);
266
+ --navy-deep: var(--primary-color-darker, #16202e);
267
+ --flag: var(--accent-color, #8a8f98);
268
+ --paper: var(--surface-color, #ffffff);
269
+ --warm: var(--hover-color, #f4f4f2);
270
+ --line: var(--border-color, #e4e1dc);
271
+ --muted: var(--muted-color, #6b7688);
272
+
273
+ display: grid;
274
+ // Asymmetric on purpose. A 50/50 split reads as a template; giving the
275
+ // photograph the larger share makes it the subject and the form the task.
276
+ grid-template-columns: 52fr 48fr;
277
+ width: 100%;
278
+ // Fill the parent, however the parent is laid out: `height` for a sized
279
+ // block, `flex` for a flex column, `align-self` so a stretch container does
280
+ // not centre it. `min-height` keeps it honest when the parent has no
281
+ // height of its own to give.
282
+ height: 100%;
283
+ min-height: 24rem;
284
+ flex: 1 1 auto;
285
+ align-self: stretch;
286
+ background: var(--paper);
287
+ font-family: var(--auth-font-family, 'Barlow', system-ui, sans-serif);
288
+ color: var(--ink);
289
+
290
+ @media (max-width: 900px) {
291
+ // The brand becomes a header band rather than disappearing: the portal
292
+ // is a public front door, and on a phone it is the only thing telling
293
+ // the visitor whose front door it is.
294
+ grid-template-columns: 1fr;
295
+ grid-template-rows: auto 1fr;
296
+ }
297
+ }
298
+
299
+ @include brand.auth-brand-panel(band);
300
+ @include brand.auth-rise-keyframes;
301
+
302
+ // The band treatment the mixin leaves to the consumer: the eyebrow and the
303
+ // tagline are the first things to go, because at that width they are two more
304
+ // lines between the visitor and the field they came to fill in.
305
+ @media (max-width: 900px) {
306
+ .brandInk {
307
+ left: 1.5rem;
308
+ right: 1.5rem;
309
+ bottom: 1.25rem;
310
+ }
311
+
312
+ .eyebrow,
313
+ .brandSub {
314
+ display: none;
315
+ }
316
+
317
+ .brandLine {
318
+ margin: 0;
319
+ font-size: clamp(1.5rem, 6vw, 2rem);
320
+ }
321
+ }
322
+
323
+ // The form side. Scrolls internally rather than growing the section, so a tall
324
+ // step-two — contact field, code field, dev panel, two links — cannot push the
325
+ // consumer's footer off the bottom of the page.
326
+ .splitPane {
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: center;
330
+ padding: 2rem clamp(1.5rem, 5vw, 4.5rem);
331
+ background: var(--paper);
332
+ overflow-y: auto;
333
+ min-width: 0;
334
+
335
+ @media (max-width: 900px) {
336
+ // The warm neutral stops the stacked screen being a blank white sheet.
337
+ background: linear-gradient(var(--warm), var(--paper) 22rem);
338
+ padding: 2rem 1.5rem 2.5rem;
339
+ }
340
+ }
341
+
342
+ // Holds exactly what the card layout holds — same children, same props, same
343
+ // form. Only the frame around them changed.
344
+ .splitCard {
345
+ width: 100%;
346
+ max-width: 24rem;
347
+ // Left aligned, not centred. Centred text over a form is a template look,
348
+ // and the eye has to travel back and forth to read labels and fields.
349
+ text-align: left;
350
+ animation: rise 0.55s cubic-bezier(0.2, 0.7, 0.3, 1) both;
351
+ animation-delay: 0.08s;
352
+
353
+ // The card layout centres its logo, title and subtitle; this one does not,
354
+ // so those three are re-aligned here rather than being made conditional in
355
+ // half a dozen places in the JSX.
356
+ .logo {
357
+ max-width: 190px;
358
+ margin: 0 0 2rem;
359
+ }
360
+
361
+ .title {
362
+ text-align: left;
363
+ font-size: 1.9rem;
364
+ letter-spacing: -0.02em;
365
+ line-height: 1.1;
366
+ color: var(--ink);
367
+ }
368
+
369
+ .subtitle {
370
+ text-align: left;
371
+ color: var(--muted);
372
+ }
373
+
374
+ // The masked-destination line under a disabled contact field, and the live
375
+ // contact-type hint, both read as captions here rather than centred notes.
376
+ .inputHint,
377
+ .contactInfo {
378
+ text-align: left;
379
+ }
380
+ }
381
+
382
+ // The sign-off, when `branding.footer` is set. Same treatment as the staff
383
+ // panel's, at the foot of the form column.
384
+ .splitFoot {
385
+ margin: 2.5rem 0 0;
386
+ font-size: 0.78rem;
387
+ line-height: 1.5;
388
+ color: var(--muted);
389
+ }
390
+
391
+ @media (prefers-reduced-motion: reduce) {
392
+ .brandInk,
393
+ .splitCard {
394
+ animation: none;
395
+ }
396
+ }