@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
@@ -8,6 +8,14 @@
8
8
  .btn {
9
9
  @include button-primary;
10
10
  }
11
+
12
+ /* The secondary half of the pair. A detail page's action bar is one primary
13
+ action and, when editing, one way back out of it — the way out was painted
14
+ in error red, the loudest colour in the palette, sitting next to the Save it
15
+ competes with. */
16
+ .btnSecondary {
17
+ @include button-secondary;
18
+ }
11
19
  .grid {
12
20
  width: 100%;
13
21
  display: flex;
@@ -86,6 +94,29 @@
86
94
  gap: 0.5rem;
87
95
  }
88
96
 
97
+ /* `page.tabLayout: "underline"` — tabs above the panel rather than beside it.
98
+ See the note on `tabsUnderlined` in GenericDetail.jsx for when a page wants
99
+ this. The row becomes a column and both children take the full width; the
100
+ rail gives up its card, because an underline strip is not a panel. */
101
+ .grid__subrow--stacked {
102
+ flex-direction: column;
103
+ flex-wrap: nowrap;
104
+ gap: 0;
105
+ }
106
+
107
+ .grid__subnav--underline {
108
+ flex: 0 0 auto;
109
+ width: 100%;
110
+ padding: 0;
111
+ margin: 8px 0 0;
112
+ background: none;
113
+ border: none;
114
+ border-radius: 0;
115
+ /* The one hairline under the whole strip, which the selected tab's accent
116
+ edge sits on top of. The strip's own items draw no border. */
117
+ border-bottom: 1px solid var(--border-color, #e1e1e1);
118
+ }
119
+
89
120
  /* The tab rail. Identical spec to `.grid__subnav` in GenericIndex.module.scss
90
121
  — the two pages share one rail, so keep them in step.
91
122
 
@@ -224,7 +255,14 @@
224
255
 
225
256
  .gridtxt__header {
226
257
  width: 100%;
227
- display: block;
258
+ /* Flex so an optional right-hand meta line (`section.meta`) can sit on the
259
+ heading's baseline at the far edge — the card-header shape used across
260
+ the dashboards: title left, quiet facts right. */
261
+ display: flex;
262
+ align-items: baseline;
263
+ justify-content: space-between;
264
+ gap: var(--spacing-md, 1rem);
265
+ flex-wrap: wrap;
228
266
  background: none;
229
267
  box-sizing: border-box;
230
268
  /* Was a centred, tinted bar. A section heading is read at the start of
@@ -239,16 +277,36 @@
239
277
  border-bottom: 1px solid var(--border-color, #e1e1e1);
240
278
  border-radius: 0;
241
279
  margin-bottom: var(--spacing-md, 1rem);
242
- font-size: var(--font-size-sm, 0.8rem);
280
+ /* Promoted from a 0.8rem uppercase eyebrow to a plain 0.9rem heading in
281
+ the page's ink. The eyebrow treatment now belongs to the field labels
282
+ inside the card (`.overviewLabel`), and two levels of hierarchy cannot
283
+ both be small-caps-muted-0.8rem without collapsing into one. */
284
+ font-size: var(--font-size-md, 0.9rem);
243
285
  font-weight: var(--font-weight-semibold, 600);
244
- letter-spacing: 0.06em;
245
- text-transform: uppercase;
286
+ letter-spacing: 0;
287
+ text-transform: none;
246
288
  text-align: left;
247
- color: var(--muted-color, rgba(var(--paragraph-color-rgb, 34, 30, 51), 0.6));
289
+ color: var(--heading-color, var(--paragraph-color, #221e33));
248
290
  }
249
291
 
250
- .gridtxt__header span {
251
- font-weight: 700;
292
+ .gridtxt__header > span:first-child {
293
+ font-weight: var(--font-weight-semibold, 600);
294
+ min-width: 0;
295
+ }
296
+
297
+ /* The quiet facts on the right of a card header — a count, a last-updated
298
+ stamp. Deliberately weaker than the heading it shares a line with: it
299
+ describes the card, it does not name it. */
300
+ .gridtxt__meta {
301
+ display: inline-flex;
302
+ align-items: baseline;
303
+ flex-wrap: wrap;
304
+ gap: 0 var(--spacing-md, 1rem);
305
+ font-size: var(--font-size-sm, 0.8rem);
306
+ font-weight: var(--font-weight-regular, 400);
307
+ letter-spacing: 0;
308
+ text-transform: none;
309
+ color: var(--muted-color, rgba(var(--paragraph-color-rgb, 34, 30, 51), 0.6));
252
310
  }
253
311
 
254
312
  /* Optional one-line subtitle under a section heading (GenericDetail renders it
@@ -321,31 +379,97 @@
321
379
  }
322
380
  }
323
381
 
324
- .customer__overview {
382
+ /* The overview field list.
383
+
384
+ Every field used to be its own bordered, tinted box, so a summary of six
385
+ facts arrived as six competing containers and the card around them counted
386
+ for nothing. The card is the container; the fields inside it are label/value
387
+ pairs on the card's own surface, laid on a two-column grid so the second
388
+ column's labels line up with the first's instead of drifting with whatever
389
+ the row above happened to be tall enough to need.
390
+
391
+ Selectors are `.gridtxt > ul.customer__overview` rather than
392
+ `.customer__overview`, because `.gridtxt > ul li` above is more specific
393
+ than a single class and would otherwise keep the hairline box. */
394
+ .gridtxt > ul.customer__overview {
325
395
  width: 100%;
326
- display: flex;
327
- justify-content: flex-start;
328
- flex-direction: row;
329
- flex-wrap: wrap;
396
+ display: grid;
397
+ grid-template-columns: repeat(
398
+ var(--detail-overview-columns, 2),
399
+ minmax(0, 1fr)
400
+ );
401
+ gap: var(--spacing-md, 1rem) var(--spacing-xl, 1.75rem);
330
402
  list-style: none;
331
403
  box-sizing: border-box;
332
- padding: 1em;
404
+ padding: 0.35rem 0 0.85rem;
333
405
  margin: 0;
334
406
 
335
407
  li {
336
- width: 49%;
337
- padding: 1em;
408
+ width: auto;
409
+ min-width: 0;
410
+ display: flex;
411
+ flex-direction: column;
412
+ gap: 0.15rem;
413
+ padding: 0;
414
+ margin: 0;
338
415
  box-sizing: border-box;
339
- border: 1px solid rgba(var(--primary-rgb), 0.15);
416
+ border: none;
417
+ border-radius: 0;
418
+ background: none;
340
419
  color: var(--paragraph-color);
341
- background: rgba(#f4f4f4, 0.65);
342
- margin: 5px;
343
- border-radius: 5px;
420
+ font-size: 0.85rem;
421
+ line-height: 1.45;
344
422
 
345
423
  p {
346
424
  font-size: 0.85rem;
347
425
  }
348
426
  }
427
+
428
+ /* `size: "full"` fields — a description, a note — run the width of the
429
+ card rather than being squeezed into one column. */
430
+ .fw-grid-item {
431
+ grid-column: 1 / -1;
432
+ }
433
+ }
434
+
435
+ @media (max-width: 759px) {
436
+ .gridtxt > ul.customer__overview {
437
+ grid-template-columns: minmax(0, 1fr);
438
+ }
439
+ }
440
+
441
+ /* The label above a value. The eyebrow treatment the section heading used to
442
+ wear: small, muted, letter-spaced — enough to read as a label and not enough
443
+ to compete with the value under it. */
444
+ .overviewLabel {
445
+ display: block;
446
+ font-size: var(--font-size-xs, 0.72rem);
447
+ font-weight: var(--font-weight-semibold, 600);
448
+ letter-spacing: 0.06em;
449
+ text-transform: uppercase;
450
+ line-height: 1.3;
451
+ color: var(--muted-color, rgba(var(--paragraph-color-rgb, 34, 30, 51), 0.6));
452
+ }
453
+
454
+ .overviewValue {
455
+ display: block;
456
+ min-width: 0;
457
+ font-size: 0.85rem;
458
+ line-height: 1.5;
459
+ color: var(--paragraph-color, #221e33);
460
+ /* A long unbroken value (a URL, a reference) must not push the grid
461
+ column wider than its share. */
462
+ overflow-wrap: anywhere;
463
+
464
+ a {
465
+ color: var(--primary-color);
466
+ }
467
+ }
468
+
469
+ /* "Not set", said out loud. A label followed by nothing reads as a page that
470
+ failed to load rather than as a field nobody has filled in. */
471
+ .overviewEmpty {
472
+ color: var(--muted-color, rgba(var(--paragraph-color-rgb, 34, 30, 51), 0.45));
349
473
  }
350
474
 
351
475
  .progress {
@@ -577,18 +701,21 @@
577
701
  }
578
702
  }
579
703
 
704
+ /* The floating action bar. It stays pinned bottom-right — every detail page
705
+ in every project is laid out around it — but its buttons are the shared
706
+ controls now: `display: block; float: left` overrode the button mixin's own
707
+ inline-flex centring, which is why an icon and its label sat off-centre, and
708
+ the 0.35em padding undercut the shared control height so these buttons were
709
+ visibly shorter than every other button in the app. */
580
710
  .polActions {
581
711
  position: fixed;
582
712
  bottom: 15px;
583
713
  right: 20px;
584
714
  width: auto;
585
- }
586
-
587
- .polActions button {
588
- display: block;
589
- float: left;
590
- padding: 0.35em 1em;
591
- margin: 0 0.15em;
715
+ display: flex;
716
+ align-items: center;
717
+ gap: var(--spacing-sm, 0.5rem);
718
+ z-index: 20;
592
719
  }
593
720
 
594
721
  .oppoverview {
@@ -112,6 +112,12 @@
112
112
  flex-wrap: wrap;
113
113
  height: auto;
114
114
  gap: 0.5rem;
115
+ /* Not `stretch`. The grid card is the one that decides how tall this row
116
+ is — it has measured itself against the window. A tab rail with more
117
+ entries than that is taller than the window allows, and stretching the
118
+ grid card to match it pushes the pager below the fold. The rail scrolls
119
+ inside itself instead (below). */
120
+ align-items: flex-start;
115
121
  }
116
122
 
117
123
  /* The tab rail. Identical spec to `.grid__subnav` in GenericDetail.module.scss
@@ -126,6 +132,13 @@
126
132
  padding: var(--spacing-xs, 0.25rem);
127
133
  margin: 8px 0;
128
134
  height: auto;
135
+ /* A long rail scrolls itself rather than the page. The offset is the app
136
+ chrome stacked above it; a consuming project with a different shell can
137
+ retune it with `--index-subnav-offset`. It only decides where the rail
138
+ starts scrolling, so it does not have to be exact. */
139
+ max-height: calc(100vh - var(--index-subnav-offset, 200px));
140
+ overflow-y: auto;
141
+ overscroll-behavior: contain;
129
142
  box-shadow: none;
130
143
  }
131
144
 
@@ -64,8 +64,19 @@
64
64
  — the density hook for the bar's own height — belongs here rather than
65
65
  in Navigation.module.scss, where the rest of the set is documented. */
66
66
  padding: var(--nav-bar-padding, var(--spacing-sm) var(--spacing-md));
67
- /* Separates the bar from the page when content scrolls under it. */
68
- box-shadow: var(--shadow-sm);
67
+ /* Where the bar stops. Without it the navy ran straight into the page's
68
+ off-white with nothing to mark the edge, which is most of why the bar
69
+ read as flat. */
70
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
71
+ /* Separates the bar from the page when content scrolls under it.
72
+ `var(--shadow-sm)` carried no fallback, so in a project that does not
73
+ declare that token the declaration was invalid and thrown away — there
74
+ was no shadow at all. */
75
+ box-shadow: var(
76
+ --shadow-sm,
77
+ 0 1px 2px rgba(0, 0, 0, 0.12),
78
+ 0 6px 16px rgba(0, 0, 0, 0.06)
79
+ );
69
80
  }
70
81
 
71
82
  .headerAlternate {
@@ -76,6 +87,12 @@
76
87
  width: 100%;
77
88
  z-index: 995;
78
89
  padding: var(--nav-bar-padding, 0.35rem 1rem);
90
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
91
+ box-shadow: var(
92
+ --shadow-sm,
93
+ 0 1px 2px rgba(0, 0, 0, 0.06),
94
+ 0 6px 16px rgba(0, 0, 0, 0.04)
95
+ );
79
96
  }
80
97
 
81
98
  .content {
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Impersonation hand-off.
3
+ *
4
+ * Ported from the Next.js portal's styles/impersonate.css. Two states, one
5
+ * card: the spinner while the token is being validated, and the error card if
6
+ * it is not accepted. Colours come from the consuming app's token layer with
7
+ * the portal's own values as fallbacks, so it looks the same there and
8
+ * sensible everywhere else.
9
+ */
10
+
11
+ .loading,
12
+ .error {
13
+ display: flex;
14
+ justify-content: center;
15
+ align-items: center;
16
+ min-height: 100vh;
17
+ background-color: var(--background-color, #f8f9fa);
18
+ }
19
+
20
+ .card {
21
+ width: 100%;
22
+ max-width: 500px;
23
+ padding: 2rem;
24
+ border-radius: 10px;
25
+ background-color: var(--surface-color, #fff);
26
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
27
+ text-align: center;
28
+ }
29
+
30
+ .title {
31
+ margin: 0 0 1.5rem;
32
+ color: var(--primary-color, #0276ba);
33
+ font-size: clamp(1.5rem, 2.5vw, 2rem);
34
+ font-weight: 600;
35
+ line-height: 1.25;
36
+ }
37
+
38
+ .message {
39
+ margin: 0 0 1.5rem;
40
+ color: var(--paragraph-color, #58585a);
41
+ line-height: 1.5;
42
+ /* An error message can arrive from the server as one long unbroken token
43
+ (a JWT complaint, a URL); it must not push the card wider than the
44
+ viewport. */
45
+ overflow-wrap: anywhere;
46
+ }
47
+
48
+ .spinner {
49
+ display: inline-block;
50
+ width: 50px;
51
+ height: 50px;
52
+ border: 5px solid rgba(2, 118, 186, 0.2);
53
+ border-top-color: var(--primary-color, #0276ba);
54
+ border-radius: 50%;
55
+ animation: impersonate-spin 1s ease-in-out infinite;
56
+ }
57
+
58
+ .button {
59
+ padding: 0.75rem 1.5rem;
60
+ border: none;
61
+ border-radius: 10px;
62
+ background-color: var(--primary-color, #0276ba);
63
+ color: #fff;
64
+ font-family: inherit;
65
+ font-size: 1rem;
66
+ text-decoration: none;
67
+ cursor: pointer;
68
+ transition: background-color 0.3s;
69
+
70
+ &:hover {
71
+ background-color: var(--primary-color-darker, #025d94);
72
+ }
73
+ }
74
+
75
+ @keyframes impersonate-spin {
76
+ to {
77
+ transform: rotate(360deg);
78
+ }
79
+ }
80
+
81
+ @media (prefers-reduced-motion: reduce) {
82
+ .spinner {
83
+ animation-duration: 3s;
84
+ }
85
+ }
@@ -11,6 +11,8 @@
11
11
  * @fontsource, so there is no webfont request to fail.
12
12
  */
13
13
 
14
+ @use './authBrandPanel' as brand;
15
+
14
16
  .auth {
15
17
  /* Local aliases only — every brand value comes from the consuming app's
16
18
  token layer so this library stays reusable. The fallbacks are neutral
@@ -32,7 +34,11 @@
32
34
  // photograph the larger share makes it the subject and the form the task.
33
35
  grid-template-columns: 52fr 48fr;
34
36
  background: var(--paper);
35
- font-family: 'Barlow', system-ui, sans-serif;
37
+ // Read from a token first, so an app can put its own face on the whole
38
+ // auth journey without forking this file. 'Barlow' itself is imported by
39
+ // the screens (see auth/authFont.js) — declaring it here without loading
40
+ // it anywhere was why this screen quietly fell back to system-ui.
41
+ font-family: var(--auth-font-family, 'Barlow', system-ui, sans-serif);
36
42
  color: var(--ink);
37
43
 
38
44
  @media (max-width: 900px) {
@@ -42,115 +48,10 @@
42
48
 
43
49
  /* --- Brand panel --------------------------------------------------------- */
44
50
 
45
- .brand {
46
- position: relative;
47
- background-color: var(--navy-deep);
48
- background-size: cover;
49
- background-position: center 62%;
50
- overflow: hidden;
51
-
52
- // A scrim anchored to the bottom-left, where the copy sits — not a flat
53
- // wash over the whole image, which would dull the dusk light that makes
54
- // the photograph worth using.
55
- //
56
- // The foot is a near-solid plate rather than a fade. The brand line has to
57
- // hold against whatever the photograph is doing behind it, and this one is
58
- // bright exactly where the type sits, which left the brand line washing
59
- // into the photograph. Held high — 0.88 still at 18%, which is where the ink
60
- // block ends — so the whole block reads on one ground, then released
61
- // quickly to 0.06 at the top, so the sky above is still a photograph and
62
- // not a tint.
63
- //
64
- // Shared verbatim with the supervisor PWA
65
- // (prime-web-nextjs/app/login/Login.module.scss). If one moves, move both.
66
- &::after {
67
- content: '';
68
- position: absolute;
69
- inset: 0;
70
- background:
71
- linear-gradient(
72
- to top,
73
- rgba(16, 25, 42, 0.94) 0%,
74
- rgba(16, 25, 42, 0.88) 18%,
75
- rgba(16, 25, 42, 0.62) 42%,
76
- rgba(16, 25, 42, 0.22) 70%,
77
- rgba(16, 25, 42, 0.06) 100%
78
- ),
79
- linear-gradient(
80
- to right,
81
- rgba(16, 25, 42, 0.5) 0%,
82
- rgba(16, 25, 42, 0) 55%
83
- );
84
- }
85
-
86
- @media (max-width: 900px) {
87
- display: none;
88
- }
89
- }
90
-
91
- .brandInk {
92
- position: absolute;
93
- left: clamp(2rem, 4vw, 3.5rem);
94
- right: clamp(2rem, 4vw, 3.5rem);
95
- bottom: clamp(2rem, 5vh, 3.5rem);
96
- z-index: 1;
97
- color: #fff;
98
- animation: rise 0.7s cubic-bezier(0.2, 0.7, 0.3, 1) both;
99
- }
100
-
101
- .eyebrow {
102
- margin: 0 0 0.75rem;
103
- font-size: 0.72rem;
104
- font-weight: 700;
105
- letter-spacing: 0.22em;
106
- // Stated, not left to the cascade: global.css sets
107
- // `p { line-height: var(--para-height) }`, which is 1.6 here. That put the
108
- // kicker on a taller line than the same kicker in the PWA, so the red dash
109
- // sat at a different height and the gap beneath it read as wider.
110
- line-height: 1.4;
111
- text-transform: uppercase;
112
- color: rgba(255, 255, 255, 0.75);
113
- // The single use of the logo's red in the whole screen.
114
- padding-left: 2.1rem;
115
- position: relative;
116
-
117
- &::before {
118
- content: '';
119
- position: absolute;
120
- left: 0;
121
- top: 50%;
122
- width: 1.5rem;
123
- height: 2px;
124
- background: var(--flag);
125
- }
126
- }
127
-
128
- .brandLine {
129
- margin: 0 0 0.6rem;
130
- font-size: clamp(2.4rem, 4.2vw, 3.6rem);
131
- font-weight: 700;
132
- line-height: 1;
133
- // Tight tracking at display size — Barlow opens up as it scales.
134
- letter-spacing: -0.02em;
135
- // Stated rather than inherited from .brandInk: this is the one line on the
136
- // screen that has to be white, and finding that out should not mean
137
- // reading the parent.
138
- color: #fff;
139
- text-wrap: balance;
140
- }
141
-
142
- .brandSub {
143
- margin: 0;
144
- max-width: 34ch;
145
- font-size: 1rem;
146
- font-weight: 300;
147
- line-height: 1.5;
148
- // No-op against this app's globals, but the PWA's own `p` rule tracks body
149
- // copy at 0.015rem, so the value is pinned in both files rather than left
150
- // to whichever global layer the screen happens to be rendered under.
151
- letter-spacing: 0;
152
- color: rgba(255, 255, 255, 0.82);
153
- }
51
+ // Shared with AuthShell and the client portal's split layout — see
52
+ // _authBrandPanel.scss. It was copied into each of them before 6.8.3, which is
53
+ // the arrangement where one copy quietly stops matching the others.
54
+ @include brand.auth-brand-panel(hide);
154
55
 
155
56
  /* --- Form panel ---------------------------------------------------------- */
156
57
 
@@ -493,18 +394,8 @@
493
394
  color: var(--muted);
494
395
  }
495
396
 
496
- /* One entrance, staggered between the two panels. Anything more would be
497
- decoration on a screen people pass through several times a day. */
498
- @keyframes rise {
499
- from {
500
- opacity: 0;
501
- transform: translateY(0.75rem);
502
- }
503
- to {
504
- opacity: 1;
505
- transform: none;
506
- }
507
- }
397
+ // One entrance, staggered between the two panels — see _authBrandPanel.scss.
398
+ @include brand.auth-rise-keyframes;
508
399
 
509
400
  @media (prefers-reduced-motion: reduce) {
510
401
  .brandInk,
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Sign out.
3
+ *
4
+ * Ported from the Next.js portal's styles/logout-page.css. One card, one
5
+ * spinner, one and a half seconds — long enough to read "logging out" and be
6
+ * sure it happened, short enough not to feel like a wait.
7
+ */
8
+
9
+ .page {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ min-height: 60vh;
14
+ }
15
+
16
+ .card {
17
+ max-width: 400px;
18
+ margin: 0 auto;
19
+ padding: 3rem;
20
+ border-radius: 10px;
21
+ background-color: var(--surface-color, #fff);
22
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
23
+ text-align: center;
24
+ }
25
+
26
+ .icon {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ width: 80px;
31
+ height: 80px;
32
+ margin: 0 auto 1.5rem;
33
+ border-radius: 50%;
34
+ background-color: rgba(2, 118, 186, 0.1);
35
+ color: var(--primary-color, #0276ba);
36
+ }
37
+
38
+ .title {
39
+ margin: 0 0 0.5rem;
40
+ color: var(--primary-color, #0276ba);
41
+ font-size: 1.8rem;
42
+ font-weight: 600;
43
+ line-height: 1.2;
44
+ }
45
+
46
+ .message {
47
+ margin: 0;
48
+ color: var(--paragraph-color, #58585a);
49
+ font-size: 1rem;
50
+ line-height: 1.5;
51
+ }
52
+
53
+ .spinner {
54
+ width: 40px;
55
+ height: 40px;
56
+ margin: 2rem auto 0;
57
+ border: 3px solid rgba(2, 118, 186, 0.2);
58
+ border-top-color: var(--primary-color, #0276ba);
59
+ border-radius: 50%;
60
+ animation: logout-spin 1s linear infinite;
61
+ }
62
+
63
+ @keyframes logout-spin {
64
+ to {
65
+ transform: rotate(360deg);
66
+ }
67
+ }
68
+
69
+ @media (max-width: 576px) {
70
+ .card {
71
+ margin: 0 1rem;
72
+ padding: 2rem;
73
+ }
74
+
75
+ .icon {
76
+ width: 60px;
77
+ height: 60px;
78
+ }
79
+
80
+ .title {
81
+ font-size: 1.5rem;
82
+ }
83
+ }
84
+
85
+ @media (prefers-reduced-motion: reduce) {
86
+ .spinner {
87
+ animation-duration: 3s;
88
+ }
89
+ }