@visns-studio/visns-components 6.3.17 → 6.3.18

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.
@@ -1,24 +1,36 @@
1
1
  @use 'controls' as *;
2
2
 
3
+ /* ============================================================================
4
+ Profile — density hooks
5
+ ----------------------------------------------------------------------------
6
+ The screen is an account-settings page: a tab rail on the left, and a panel
7
+ whose tabs are a stack of titled sections rather than one flat form. Each
8
+ section is the classic settings two-column — what the section is on the
9
+ left, the controls on the right — so the panel's right half is used rather
10
+ than left empty.
11
+
12
+ Every measurement below is written `var(--hook, <the value it always had>)`,
13
+ so an unset hook is a no-op and a project retunes the page from `:root`
14
+ without forking the component:
15
+
16
+ --profile-rail-width the tab rail column. default: 14rem
17
+ --profile-content-width the measure the sections and the footer share,
18
+ so Save lands under the right edge of the
19
+ fields. default: 68rem
20
+ --profile-aside-width the section's title/description column.
21
+ default: 18rem
22
+ --profile-signature-width the signature pad. default: 29rem
23
+
24
+ Literal fallbacks throughout: `--spacing-*`, `--font-size-*`, `--radius-*`,
25
+ `--muted-color` and `--surface-color` are not in every consuming project's
26
+ token set, and a bare `var()` for a token that does not exist is invalid at
27
+ computed-value time — the whole declaration is dropped, which is how a form
28
+ like this silently loses its padding.
29
+ ========================================================================= */
30
+
3
31
  .btn {
4
32
  @include button-primary;
5
33
  }
6
- /* Profile.module.scss */
7
-
8
- /* Form field label animation */
9
- input:not(:placeholder-shown) + .fi__span,
10
- textarea:not(:placeholder-shown) + .fi__span,
11
- select:not(:placeholder-shown) + .fi__span,
12
- input:focus + .fi__span,
13
- textarea:focus + .fi__span,
14
- select:focus + .fi__span {
15
- transform: translateY(-40%) translateX(10px) scale(0.9);
16
- opacity: 1;
17
- padding: 0 0.25rem;
18
- background: var(--tertiary-color);
19
- font-weight: 400;
20
- border-radius: var(--br);
21
- }
22
34
 
23
35
  .grid {
24
36
  width: 100%;
@@ -50,96 +62,84 @@ select:focus + .fi__span {
50
62
  box-shadow: 0 10px 50px rgba(var(--primary-rgb), 0.05);
51
63
  }
52
64
 
65
+ /* The rail/panel row.
66
+
67
+ It was a wrapping flex row of `flex: 1` against `flex: 0 1 80%` — two
68
+ percentage bases plus a gap, which is the arrangement that puts the panel's
69
+ right edge past the page's right padding while the rail keeps its left one,
70
+ and the reason the page read as lopsided. Two grid tracks always sum to the
71
+ container, gap included, so the two cards sit inside the same gutters. */
53
72
  .grid__subrow {
54
73
  width: 100%;
55
- display: flex;
56
- flex-wrap: wrap;
57
- height: auto;
58
- gap: 0.35rem;
74
+ display: grid;
75
+ grid-template-columns:
76
+ minmax(0, var(--profile-rail-width, 14rem))
77
+ minmax(0, 1fr);
78
+ /* The rail is as tall as its two tabs. `stretch` — the flex and grid
79
+ default — ran it the full height of the panel beside it, so a card
80
+ holding two links was most of a screen of empty white. */
81
+ align-items: start;
82
+ gap: var(--spacing-md, 1rem);
83
+
84
+ @media (max-width: 900px) {
85
+ grid-template-columns: minmax(0, 1fr);
86
+ }
59
87
  }
60
88
 
89
+ /* The tab rail. Same spec as `.grid__subnav` in GenericIndex/GenericDetail —
90
+ a flat surface, a hairline, and the framing of the items left to
91
+ TableFilter's own stylesheet, which is what actually draws them.
92
+
93
+ Deliberately not sticky: neither of the two pages that share this rail is,
94
+ and the app's nav bar is itself `position: sticky` at `top: 0` on a higher
95
+ z-index, so a rail stuck to the top of the viewport would slide underneath
96
+ it. */
61
97
  .grid__subnav {
62
- flex: 1;
63
- background: white;
98
+ background: var(--surface-color, #fff);
99
+ border: 1px solid var(--border-color, #e1e1e1);
64
100
  border-radius: var(--br);
65
- border: 1px solid var(--border-color);
66
101
  box-sizing: border-box;
67
- padding: 5px;
102
+ padding: var(--spacing-xs, 0.25rem);
68
103
  margin: 8px 0;
69
104
  height: auto;
70
- box-shadow: 0 10px 50px rgba(var(--primary-rgb), 0.05);
71
- }
72
-
73
- .grid__subnav > ul {
74
- width: 100%;
75
- list-style: none;
76
- padding: 0;
77
- margin: 0;
78
- display: flex;
79
- flex-wrap: wrap;
80
- }
81
-
82
- .grid__subnav > ul li {
83
- width: 100%;
84
- margin-bottom: 3px;
105
+ box-shadow: none;
85
106
  }
86
107
 
87
- .grid__subnav > ul li a {
88
- width: 100%;
89
- padding: 0.35rem 1rem;
90
- box-sizing: border-box;
91
- display: block;
92
- text-decoration: none;
93
- background: rgba(var(--primary-rgb), 0.05);
94
- color: var(--primary-color);
95
- border-radius: 5px;
96
- transition: background 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
97
- outline: none;
98
- cursor: pointer;
99
- }
100
-
101
- .grid__subnav > ul li:hover a {
102
- background: var(--primary-color);
103
- color: var(--tertiary-color);
104
- }
105
-
106
- .grid__subnav > ul li .subactive {
107
- font-weight: 700;
108
- background: var(--primary-color);
109
- color: var(--tertiary-color);
108
+ /* `.tableFilterContainer` carries a 20px bottom margin, which is the right
109
+ amount of room under a rail that runs the height of the page and dead space
110
+ under one that hugs its two items. */
111
+ .grid__subnav > div {
112
+ margin-bottom: 0;
110
113
  }
111
114
 
112
115
  .grid__subcontent {
113
- flex: 0 1 80%;
114
- background: var(--tertiary-color);
116
+ background: var(--surface-color, #fff);
115
117
  border-radius: var(--br);
116
- border: 1px solid rgba(var(--primary-rgb), 0.05);
118
+ border: 1px solid var(--border-color, #e1e1e1);
117
119
  box-sizing: border-box;
118
- /* Was 2px, which left the section header's hairline floating a couple of
119
- pixels short of the panel edge. The panel is a card; its header, body and
120
- footer own their own padding, so the card itself has none. */
120
+ /* The card has none of its own: its header, sections and footer each own
121
+ their padding, and a couple of pixels here left the header's hairline
122
+ floating short of the card edge. */
121
123
  padding: 0;
122
124
  margin: 8px 0;
125
+ min-width: 0;
123
126
  height: auto;
124
127
  position: relative;
125
128
  }
126
129
 
127
- .grid__subcontent h2 {
128
- color: var(--primary-color);
129
- font-size: 1.35em;
130
- padding: 0;
131
- margin: 0 0 30px 0;
130
+ /* The tab panel: header, sections, footer. The card around it has no padding
131
+ of its own, so each of those three owns its inset — which is what lets the
132
+ header's and the footer's hairlines run the full width of the card while the
133
+ sections in between stay on a measure. */
134
+ .gridtxt {
135
+ display: flex;
136
+ flex-direction: column;
132
137
  }
133
138
 
134
- /* The panel's section header. It follows the same spec as the unified page
135
- header documented at the top of GenericIndex.module.scss — the panel's own
136
- surface, one bottom hairline, the title left-aligned on the same padding as
137
- the body beneath it — a step down in type size because this names a section
138
- inside a page rather than the page.
139
-
140
- It used to be a pale centred band with its own fill and radius: a second
141
- card sitting on top of the card, and the only centred heading in the
142
- library. */
139
+ /* The panel header. Same spec as the unified page header documented at the top
140
+ of GenericIndex.module.scss — the panel's own surface, one bottom hairline,
141
+ the title left-aligned on the padding the body below it uses, one step down
142
+ in type size because this names a tab inside a page rather than the page. */
143
143
  .gridtxt__header {
144
144
  width: 100%;
145
145
  display: flex;
@@ -147,7 +147,7 @@ select:focus + .fi__span {
147
147
  justify-content: space-between;
148
148
  gap: var(--spacing-md, 1rem);
149
149
  box-sizing: border-box;
150
- padding: var(--spacing-md, 1rem);
150
+ padding: var(--spacing-md, 1rem) var(--spacing-lg, 1.5rem);
151
151
  background: none;
152
152
  border-bottom: 1px solid var(--border-color, #e1e1e1);
153
153
  border-radius: 0;
@@ -158,146 +158,175 @@ select:focus + .fi__span {
158
158
 
159
159
  .gridtxt__header span {
160
160
  font-weight: 700;
161
- /* One step under the page title above it, so the two read as a hierarchy
162
- rather than as two competing headings. */
163
161
  font-size: var(--font-size-lg, 1.125rem);
164
162
  line-height: 1.25;
165
163
  }
166
164
 
167
- .gridtxt {
168
- /* One column width, shared by the form body and the footer under it, so
169
- Save lands beneath the right edge of the fields instead of out at the
170
- card's edge. A project can retune it from `:root` like the rest of the
171
- library's density hooks. */
172
- --profile-form-width: 56rem;
173
- }
174
-
175
- .gridtxt > ul {
176
- width: 100%;
177
- display: flex;
178
- justify-content: flex-start;
179
- flex-direction: row;
180
- flex-wrap: wrap;
181
- list-style: none;
182
- box-sizing: border-box;
183
- padding: 0.85rem;
184
- margin: 0;
185
- }
186
-
187
- .gridtxt > ul li {
188
- flex: 0 0 calc(50% - 10px);
189
- padding: 0.85rem;
190
- box-sizing: border-box;
191
- border: 1px solid rgba(var(--primary-rgb), 0.095);
192
- color: var(--paragraph-color);
193
- background: rgba(var(--primary-rgb), 0.015);
194
- margin: 5px;
195
- border-radius: var(--br);
196
- line-height: 1.45;
197
- font-size: 1rem;
198
- }
199
-
200
- .gridtxt > ul .fw-grid-item {
201
- flex: 0 0 calc(100% - 10px);
202
- }
203
-
204
- .gridtxt > ul .notecolor {
205
- border: 1px solid var(--secondary-color);
206
- }
165
+ /* --- Sections -------------------------------------------------------------
166
+ A settings tab is a list of decisions, not one continuous form. Each of them
167
+ gets a title, a line saying what it is for, and only the controls that
168
+ belong to it; a hairline separates one from the next.
207
169
 
208
- .formcontainer {
209
- width: 100%;
210
- /* Two columns across the full width of a 1600px panel gave every field a
211
- ~600px line — far past the point where a name box reads as a box. The
212
- form takes the width it needs and the rest of the panel stays quiet.
213
- The 2FA tab is a document rather than a form and opts out with its own
214
- `max-width: 100%`. */
215
- max-width: var(--profile-form-width, 56rem);
170
+ The two columns are what keeps the panel from having an empty right half:
171
+ the prose that used to have nowhere to live now occupies the left third, and
172
+ the fields take a measure a name actually wants rather than 600px of empty
173
+ box. */
174
+ .form {
216
175
  box-sizing: border-box;
176
+ max-width: var(--profile-content-width, 68rem);
177
+ padding: 0 var(--spacing-lg, 1.5rem);
217
178
  }
218
179
 
219
- .modalForm {
220
- width: 100%;
221
- box-sizing: border-box;
222
- /* Was flex-wrap with 50%-wide children, so a field that needed the full
223
- row had no way to say so and the row it started stayed half empty. */
180
+ .section {
224
181
  display: grid;
225
- grid-template-columns: repeat(2, minmax(0, 1fr));
182
+ grid-template-columns:
183
+ minmax(0, var(--profile-aside-width, 18rem))
184
+ minmax(0, 1fr);
226
185
  align-items: start;
227
- gap: var(--spacing-lg, 1.5rem) var(--spacing-md, 1rem);
228
- padding: var(--spacing-md, 1rem);
186
+ gap: var(--spacing-md, 1rem) var(--spacing-xl, 2rem);
187
+ padding: var(--spacing-lg, 1.5rem) 0;
229
188
 
230
- @media (max-width: 768px) {
231
- grid-template-columns: 1fr;
189
+ @media (max-width: 900px) {
190
+ grid-template-columns: minmax(0, 1fr);
191
+ gap: var(--spacing-sm, 0.5rem);
232
192
  }
233
193
  }
234
194
 
235
- .formItem {
236
- width: auto;
237
- padding: 0;
238
- position: relative;
239
- box-sizing: border-box;
195
+ .section + .section {
196
+ border-top: 1px solid var(--border-color, #e1e1e1);
240
197
  }
241
198
 
242
- /* A field that spans the row — the signature, which is a canvas rather than a
243
- line of text and does not pair with anything beside it. */
244
- .formItemFull {
245
- grid-column: 1 / -1;
199
+ .sectionAside {
200
+ min-width: 0;
246
201
  }
247
202
 
248
- .fi__label {
249
- position: relative;
250
- display: block;
203
+ .sectionTitle {
204
+ margin: 0;
205
+ font-size: var(--font-size-md, 0.9375rem);
206
+ font-weight: var(--font-weight-semibold, 600);
207
+ line-height: 1.3;
208
+ letter-spacing: 0;
209
+ color: var(--heading-color, #10192a);
251
210
  }
252
211
 
253
- .fi__span {
254
- position: absolute;
255
- transition: all 200ms;
256
- opacity: 1;
257
- left: 0;
258
- padding: 19px 20px;
259
- transform-origin: top left;
260
- cursor: text;
212
+ .sectionDescription {
213
+ margin: 0.35rem 0 0;
214
+ max-width: 42ch;
215
+ font-size: var(--font-size-sm, 0.8125rem);
216
+ line-height: 1.55;
217
+ color: var(--muted-color, #6b7285);
261
218
  }
262
219
 
263
- .prefocus {
264
- transform: translateY(-40%) translateX(10px) scale(0.75);
265
- opacity: 1;
266
- padding: 0 4px;
267
- background: var(--tertiary-color);
268
- font-weight: 300;
269
- border-radius: var(--br);
270
- z-index: 500;
220
+ .sectionFields {
221
+ display: flex;
222
+ flex-direction: column;
223
+ align-items: flex-start;
224
+ gap: var(--spacing-md, 1rem);
225
+ min-width: 0;
271
226
  }
272
227
 
273
- .input,
274
- .textarea,
275
- .select {
276
- background: var(--item-color);
277
- border-radius: var(--br);
278
- /* Was transparent: on a white panel the fields had no edge at all and the
279
- form read as text floating in space. The width stays 1px so the floating
280
- label, which is positioned against this box, does not move. */
281
- border: 1px solid var(--border-color, #e1e1e1);
282
- padding: 1rem;
283
- outline: none;
228
+ /* Two fields on a line where the column is wide enough for two, one where it
229
+ is not. `auto-fit` measures the column it is actually in, which a media
230
+ query on the viewport cannot do — the fields column is two-thirds of the
231
+ panel, and the panel is a fraction of the page. */
232
+ .fieldPair {
284
233
  width: 100%;
234
+ display: grid;
235
+ grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
236
+ align-items: start;
237
+ gap: var(--spacing-md, 1rem);
238
+ }
239
+
240
+ /* --- Fields ---------------------------------------------------------------
241
+ The notched floating label — a `<span>` absolutely positioned inside the
242
+ box, sliding up into a gap in the border once the field held something —
243
+ was the last of that treatment in the library, and it read a decade older
244
+ than every other control on the page. The label is a line of text above the
245
+ box now, on the same uppercase/semibold/muted spec the library gives a small
246
+ label everywhere else (`h5`/`h6` in global.css, the section headings in
247
+ GenericDetail).
248
+
249
+ Profile owns these classes outright: nothing here reaches the shared
250
+ `Field`/`Form` components, whose floating treatment is left exactly as it
251
+ is. */
252
+ .formItem {
253
+ display: flex;
254
+ flex-direction: column;
255
+ gap: 0.35rem;
256
+ min-width: 0;
285
257
  box-sizing: border-box;
286
- color: var(--paragraph-color);
287
- transition: box-shadow 0.15s linear;
288
258
  }
289
259
 
290
- .input:hover,
291
- .textarea:hover,
292
- .select:hover {
293
- border: 1px solid rgba(var(--highlight-color-rgb), 0.85);
294
- transition: border 0.15s linear;
260
+ .fieldLabel {
261
+ font-size: var(--font-size-xs, 0.75rem);
262
+ font-weight: var(--font-weight-semibold, 600);
263
+ letter-spacing: 0.06em;
264
+ text-transform: uppercase;
265
+ line-height: 1.3;
266
+ color: var(--muted-color, #6b7285);
267
+ }
268
+
269
+ /* Why the `!important`s.
270
+ ----------------------------------------------------------------------------
271
+ `global.css` — which this component imports — dresses every text input on
272
+ the page through
273
+
274
+ textarea, select,
275
+ input[type]:not([type='search'])…:not(.react-datepicker__year-select)
276
+
277
+ Thirteen `:not()`s put that selector at specificity (0, 14, 1). A CSS module
278
+ can only ever produce one hashed class, (0, 1, 0), so *every* property that
279
+ rule sets — background, border, radius, padding, font-size, line-height,
280
+ box-shadow — lands on these fields no matter what is written here, and the
281
+ only way a scoped stylesheet can restate one is `!important`. That is the
282
+ sole reason for them below; a property the global rule does not touch
283
+ (`min-height`, `font-family`, `::placeholder`) is left plain and comes from
284
+ the shared `input-base` mixin as usual.
285
+
286
+ This is also why the previous pass's field styling never showed: it was
287
+ correct CSS that never won. Fixing it at the source means rewriting a
288
+ selector every component in the library is sitting under, which is a
289
+ separate job. */
290
+ .input {
291
+ /* The shared control shape: one height, one radius, one border, the same
292
+ as every other input in 6.3. */
293
+ @include input-base;
294
+
295
+ padding: 0 0.65rem !important;
296
+ border: 1px solid var(--border-color) !important;
297
+ border-radius: var(--radius-sm) !important;
298
+ background: var(--paper) !important;
299
+ font-size: var(--font-size-md) !important;
300
+ line-height: 1.25 !important;
301
+
302
+ &:hover:not(:disabled):not([readonly]):not(.inputError) {
303
+ border-color: var(--border-color-dark) !important;
304
+ }
305
+
306
+ &:focus {
307
+ outline: none;
308
+ border-color: var(--primary-color) !important;
309
+ /* A ring rather than a thicker border, so nothing shifts on focus. */
310
+ box-shadow: var(--focus-ring, 0 0 0 3px rgba(0, 118, 192, 0.32)) !important;
311
+ }
312
+
313
+ /* Email is filled from the profile and only an administrator can change
314
+ it. Same surface the shared mixin gives a disabled control, so it reads
315
+ as "not yours to edit" rather than as a field that has gone wrong — but
316
+ the value keeps full-strength ink, because it is still information the
317
+ user came here to read. */
318
+ &[readonly] {
319
+ background: var(--bg-color-secondary, #f4f5f7) !important;
320
+ cursor: default;
321
+ }
295
322
  }
296
323
 
297
- .input:focus,
298
- .textarea:focus,
299
- .select:focus {
300
- border: 1px solid rgba(var(--highlight-color-rgb), 0.85);
324
+ /* Set from JS as `styles.inputError`, appended to the input's class list. It is
325
+ written as a compound so it outranks `.input:focus` above — an invalid field
326
+ that has been focused to correct it should still look invalid. */
327
+ .input.inputError {
328
+ border-color: var(--danger-color, #e74c3c) !important;
329
+ box-shadow: 0 0 0 3px var(--danger-light, rgba(231, 76, 60, 0.14)) !important;
301
330
  }
302
331
 
303
332
  /* --- Password strength ---------------------------------------------------
@@ -311,8 +340,10 @@ select:focus + .fi__span {
311
340
  empty password box telling the user off before they had typed. */
312
341
  .strengthMeter {
313
342
  /* Sits directly under the input it measures, close enough to read as part
314
- of the field rather than as a rule floating beneath it. */
315
- margin-top: 0.35rem;
343
+ of the field rather than as a rule floating beneath it. The distance is
344
+ `.formItem`'s own gap — this only cancels the library's default so the
345
+ two do not stack. */
346
+ margin-top: 0 !important;
316
347
 
317
348
  /* The segments: 2px hairlines with 4px of white between them read as four
318
349
  unrelated underlines. A little more weight and a rounded cap makes them
@@ -332,68 +363,100 @@ select:focus + .fi__span {
332
363
  }
333
364
 
334
365
  /* --- Signature ------------------------------------------------------------
335
- The canvas geometry (2px border, 10px padding) is deliberately unchanged:
336
- signature_pad maps pointer coordinates through the element's bounding rect,
337
- so border and padding shift where the ink lands. Only the colour and radius
338
- are tokenised, and the frame moved off the inline `canvasProps.style` onto
339
- this class where a project can reach it. */
340
- .sigcanvas {
341
- border: 2px solid var(--border-color, #ccc);
342
- border-radius: var(--radius-md, 10px);
343
- padding: 10px;
344
- box-sizing: border-box;
345
- }
346
-
366
+ signature_pad maps a pointer to a canvas coordinate through the element's
367
+ bounding rect and assumes that rect is the drawing surface. A border and
368
+ padding on the canvas itself sit inside the rect and outside the surface, so
369
+ every stroke landed up and to the left of the cursor by exactly the border
370
+ plus the padding. The frame belongs on the wrapper; the canvas keeps its own
371
+ box model and nothing else.
372
+
373
+ The pad's size comes from `--profile-signature-width` and the `height` prop
374
+ in Profile.jsx together. The wrapper library writes `width`/`height`
375
+ attributes of `offsetWidth * dpr` by `height prop * dpr` and scales the
376
+ context by `dpr`; with `width: 100%` and `height: auto` the canvas's own
377
+ intrinsic ratio then renders it at exactly `height` CSS pixels, so the
378
+ attribute geometry and the CSS geometry agree and a stroke lands under the
379
+ cursor. Change one of the two and check the other.
380
+
381
+ Neither may be made smaller. Profile loads a stored signature into the pad
382
+ on mount and `handleSubmitProfile` re-exports the canvas on any save, so a
383
+ smaller pad writes back a cropped signature without anyone asking it to —
384
+ the full reasoning is at the `height` prop in Profile.jsx. */
347
385
  .signatureField {
386
+ width: 100%;
348
387
  display: flex;
349
388
  flex-direction: column;
350
389
  gap: 0.4rem;
351
- /* A signature is a fixed-size mark, not a field that benefits from more
352
- room. Left to fill the row it became the largest thing on the page. */
353
- max-width: 32rem;
390
+ /* A signature is a fixed-size mark, not a field that gains anything from
391
+ more room. Left to fill the row it became the largest thing on the
392
+ page. */
393
+ max-width: var(--profile-signature-width, 29rem);
354
394
  }
355
395
 
356
- /* The label was a floating `.fi__span`, absolutely positioned so it landed on
357
- top of the canvas border — the treatment belongs to inputs that have a value
358
- to sit above. A canvas has none, so this is an ordinary label. */
359
- .signatureLabel {
360
- font-size: var(--font-size-sm, 0.8125rem);
396
+ /* The caption and the Clear action share the line above the pad. Clear used to
397
+ be promoted into the page footer beside Save, where a control that wipes one
398
+ field sat at the same rank as the one that saves the screen. */
399
+ .signatureHeader {
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: space-between;
403
+ gap: var(--spacing-sm, 0.5rem);
404
+ }
405
+
406
+ .signatureCaption {
407
+ font-size: var(--font-size-xs, 0.75rem);
361
408
  font-weight: var(--font-weight-semibold, 600);
362
- color: var(--paragraph-color);
409
+ letter-spacing: 0.06em;
410
+ text-transform: uppercase;
411
+ line-height: 1.3;
412
+ color: var(--muted-color, #6b7285);
363
413
  }
364
414
 
365
415
  .signaturePadWrapper {
366
416
  width: 100%;
367
417
  position: relative;
368
418
  z-index: 1;
419
+ box-sizing: border-box;
420
+ padding: var(--spacing-xs, 0.25rem);
421
+ border: 1px solid var(--border-color, #e1e1e1);
422
+ border-radius: var(--radius-sm, 6px);
423
+ background: var(--paper, #fff);
369
424
 
370
425
  canvas {
426
+ display: block;
427
+ width: 100%;
371
428
  max-width: 100%;
372
429
  height: auto !important;
373
- display: block;
374
430
  }
375
431
  }
376
432
 
377
- .signatureHint {
378
- font-size: var(--font-size-xs, 0.75rem);
379
- color: var(--muted-color, #666);
433
+ /* Nothing here changes the canvas's box model — see the note above. */
434
+ .sigcanvas {
435
+ cursor: crosshair;
436
+ touch-action: none;
380
437
  }
381
438
 
382
- /* The form card's footer. This was `position: fixed` in the bottom-right
383
- corner of the viewport: the page's Save button floated over whatever the
384
- user had scrolled to, unattached to the form it saved. It is now the bottom
385
- edge of the card, on the same hairline-and-padding spec as the header at the
386
- top of it. Secondary before primary, so the destructive-ish action is not
387
- the one under the thumb. */
439
+ /* A small, quiet button for an action that belongs to one field rather than to
440
+ the page. */
441
+ .btnQuiet {
442
+ @include button-secondary;
443
+ @include button-small;
444
+ }
445
+
446
+ /* The panel's footer. Save was `position: fixed` in the bottom-right corner of
447
+ the viewport, floating over whatever the user had scrolled to. It is the
448
+ bottom edge of the card now, on the same hairline-and-padding spec as the
449
+ header, and it shares the sections' measure so it lands under the right edge
450
+ of the fields rather than out at the card's edge. */
388
451
  .polActions {
389
452
  display: flex;
390
453
  align-items: center;
391
454
  justify-content: flex-end;
392
455
  gap: var(--spacing-sm, 0.5rem);
393
456
  width: 100%;
394
- max-width: var(--profile-form-width, 56rem);
457
+ max-width: var(--profile-content-width, 68rem);
395
458
  box-sizing: border-box;
396
- padding: var(--spacing-md, 1rem);
459
+ padding: var(--spacing-md, 1rem) var(--spacing-lg, 1.5rem);
397
460
  border-top: 1px solid var(--border-color, #e1e1e1);
398
461
  }
399
462
 
@@ -401,12 +464,6 @@ select:focus + .fi__span {
401
464
  margin: 0;
402
465
  }
403
466
 
404
-
405
-
406
- .inputError {
407
- border-color: red !important;
408
- }
409
-
410
467
  .loading {
411
468
  position: relative;
412
469
  cursor: wait;
@@ -432,64 +489,67 @@ select:focus + .fi__span {
432
489
  }
433
490
  }
434
491
 
435
- /* Two-Factor Authentication Styles */
436
- .twoFactorSection {
437
- padding: var(--spacing-md, 1rem);
438
- width: 100%;
492
+ /* --- Two-Factor Authentication -------------------------------------------
493
+ The tab is sectioned exactly like Overview — the status in one section, the
494
+ pairing walkthrough in the next — so moving between the two tabs does not
495
+ feel like moving between two different pages. */
496
+ .twoFactorStatus {
439
497
  display: flex;
440
498
  flex-direction: column;
441
- }
442
-
443
- .twoFactorStatus {
444
- margin-bottom: 1.5rem;
499
+ align-items: flex-start;
500
+ gap: var(--spacing-md, 1rem);
445
501
 
446
- h3 {
447
- margin-bottom: 0.75rem;
448
- font-size: 1rem;
502
+ h4 {
503
+ margin: 0;
449
504
  display: flex;
450
505
  align-items: center;
451
506
  gap: 0.5rem;
452
- font-weight: 500;
453
- }
454
-
455
- p {
456
- margin-bottom: 1.25rem;
457
- line-height: 1.4;
458
- color: var(--paragraph-color);
459
- font-size: 0.9rem;
460
- /* This tab is deliberately full width — it holds a QR code and a grid
461
- of recovery codes — but the prose explaining 2FA ran the whole way
462
- across a 1600px panel. Only the paragraphs are capped. */
463
- max-width: 68ch;
507
+ font-size: var(--font-size-xs, 0.75rem);
508
+ font-weight: var(--font-weight-semibold, 600);
509
+ letter-spacing: 0.06em;
510
+ text-transform: uppercase;
511
+ color: var(--muted-color, #6b7285);
464
512
  }
465
513
  }
466
514
 
467
515
  .statusEnabled {
468
516
  color: var(--success-color, #2ecc71);
469
- font-weight: 500;
470
- padding: 0.2rem 0.4rem;
517
+ font-weight: var(--font-weight-semibold, 600);
518
+ padding: 0.15rem 0.45rem;
471
519
  background-color: var(--success-color-light, rgba(46, 204, 113, 0.1));
472
- border-radius: var(--radius-sm, 4px);
473
- font-size: 0.85rem;
520
+ border-radius: var(--radius-sm, 6px);
521
+ font-size: var(--font-size-xs, 0.75rem);
522
+ letter-spacing: 0.04em;
474
523
  }
475
524
 
476
525
  .statusDisabled {
477
526
  color: var(--danger-color, #e74c3c);
478
- font-weight: 500;
479
- padding: 0.2rem 0.4rem;
527
+ font-weight: var(--font-weight-semibold, 600);
528
+ padding: 0.15rem 0.45rem;
480
529
  background-color: var(--danger-light, rgba(231, 76, 60, 0.1));
481
- border-radius: var(--radius-sm, 4px);
482
- font-size: 0.85rem;
530
+ border-radius: var(--radius-sm, 6px);
531
+ font-size: var(--font-size-xs, 0.75rem);
532
+ letter-spacing: 0.04em;
483
533
  }
484
534
 
485
535
  .confirmedMessage {
486
- margin: 1rem 0;
536
+ margin: 0;
487
537
  padding: 0.75rem;
488
538
  background-color: var(--success-color-light, rgba(46, 204, 113, 0.1));
489
539
  border-left: 3px solid var(--success-color, #2ecc71);
490
- border-radius: var(--br);
491
- color: #333;
492
- font-size: 0.9rem;
540
+ border-radius: var(--radius-sm, 6px);
541
+ color: var(--paragraph-color, #333);
542
+ font-size: var(--font-size-sm, 0.8125rem);
543
+ line-height: 1.5;
544
+ }
545
+
546
+ /* The confirmed message and the button that undoes it, stacked and left-hung
547
+ under the status line. */
548
+ .statusActions {
549
+ display: flex;
550
+ flex-direction: column;
551
+ align-items: flex-start;
552
+ gap: var(--spacing-md, 1rem);
493
553
  }
494
554
 
495
555
  /* The three variants each carried their own font size and padding, so a row of
@@ -520,75 +580,67 @@ select:focus + .fi__span {
520
580
  }
521
581
  }
522
582
 
583
+ /* The setup walkthrough. It used to be a bordered, shadowed card inside the
584
+ panel card, carrying a heading that repeated the heading above it. The
585
+ section owns the title and the separation now, so this is only the steps. */
523
586
  .twoFactorSetup {
524
- margin-top: 1.5rem;
525
- padding: 1.5rem;
526
- background-color: var(--paper, #fff);
527
- border-radius: var(--br);
528
- border: 1px solid var(--border-color, #e0e0e0);
529
- box-shadow: var(--shadow-sm, 0 3px 8px rgba(0, 0, 0, 0.05));
530
587
  width: 100%;
531
-
532
- h3 {
533
- margin-bottom: 1.25rem;
534
- font-size: 1rem;
535
- font-weight: 500;
536
- color: var(--primary-color);
537
- padding-bottom: 0.75rem;
538
- border-bottom: 1px solid var(--border-color, #f0f0f0);
539
- }
588
+ min-width: 0;
540
589
  }
541
590
 
542
591
  .setupSteps {
543
- margin-left: 0;
544
- margin-bottom: 1.5rem;
545
- font-size: 0.9rem;
592
+ margin: 0;
593
+ padding: 0;
594
+ font-size: var(--font-size-sm, 0.8125rem);
546
595
  counter-reset: step-counter;
547
596
  list-style-type: none;
548
597
 
549
598
  li {
550
- margin-bottom: 1.5rem;
551
- padding-left: 2.5rem;
599
+ margin-bottom: var(--spacing-lg, 1.5rem);
600
+ padding-left: 2.25rem;
552
601
  position: relative;
553
602
 
603
+ &:last-child {
604
+ margin-bottom: 0;
605
+ }
606
+
554
607
  &:before {
555
608
  content: counter(step-counter);
556
609
  counter-increment: step-counter;
557
610
  position: absolute;
558
611
  left: 0;
559
612
  top: 0;
560
- width: 1.75rem;
561
- height: 1.75rem;
613
+ width: 1.5rem;
614
+ height: 1.5rem;
562
615
  background-color: var(--primary-color);
563
- color: white;
616
+ color: #fff;
564
617
  border-radius: 50%;
565
618
  display: flex;
566
619
  align-items: center;
567
620
  justify-content: center;
568
- font-size: 0.85rem;
569
- font-weight: 500;
621
+ font-size: var(--font-size-xs, 0.75rem);
622
+ font-weight: var(--font-weight-semibold, 600);
570
623
  }
571
624
 
572
625
  p {
573
- margin-bottom: 0.75rem;
574
- font-weight: 500;
575
- font-size: 0.9rem;
576
- color: #333;
626
+ margin-bottom: var(--spacing-sm, 0.5rem);
627
+ font-weight: var(--font-weight-medium, 500);
628
+ font-size: var(--font-size-sm, 0.8125rem);
629
+ line-height: 1.5;
630
+ color: var(--text-color, #333);
577
631
  }
578
632
 
579
633
  ul {
580
- margin-left: 0;
581
- margin-bottom: 1rem;
634
+ margin: 0 0 var(--spacing-md, 1rem);
582
635
  list-style-type: none;
583
- background-color: #f9f9f9;
584
- padding: 0.75rem 1rem;
585
- border-radius: var(--br);
636
+ background: var(--bg-color-secondary, #f9f9f9);
637
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
638
+ border-radius: var(--radius-sm, 6px);
586
639
  border-left: 3px solid var(--primary-color);
587
640
 
588
641
  li {
589
- margin-bottom: 0.5rem;
590
- font-size: 0.85rem;
591
- padding-left: 0;
642
+ margin-bottom: 0.4rem;
643
+ font-size: var(--font-size-sm, 0.8125rem);
592
644
  position: relative;
593
645
  padding-left: 1rem;
594
646
 
@@ -613,7 +665,7 @@ select:focus + .fi__span {
613
665
  a {
614
666
  color: var(--primary-color);
615
667
  text-decoration: none;
616
- font-weight: 500;
668
+ font-weight: var(--font-weight-medium, 500);
617
669
 
618
670
  &:hover {
619
671
  text-decoration: underline;
@@ -626,17 +678,20 @@ select:focus + .fi__span {
626
678
 
627
679
  .qrCodeContainer {
628
680
  display: flex;
629
- justify-content: center;
630
- margin: 0.75rem 0;
631
- padding: 1rem;
632
- background-color: var(--paper, #fff);
633
- border-radius: var(--br);
681
+ justify-content: flex-start;
682
+ margin: 0 0 var(--spacing-sm, 0.5rem);
683
+ padding: var(--spacing-md, 1rem);
684
+ background: var(--paper, #fff);
685
+ border-radius: var(--radius-sm, 6px);
634
686
  border: 1px solid var(--border-color, #e0e0e0);
635
- width: 100%;
636
- /* The panel used to lift and deepen its shadow on hover. Nothing here is
687
+ /* Was full width with the code centred in it, which drew a wide empty
688
+ panel around a 180px square. It is the size of what it holds. */
689
+ width: max-content;
690
+ max-width: 100%;
691
+ /* The panel used to lift and deepen its shadow on hover. Nothing in it is
637
692
  clickable — it is a QR code to point a phone at — so the affordance was
638
693
  promising an interaction that does not exist. */
639
- box-shadow: var(--shadow-sm, 0 2px 6px rgba(0, 0, 0, 0.08));
694
+ box-shadow: none;
640
695
  }
641
696
 
642
697
  .qrCodeSvg {
@@ -656,9 +711,9 @@ select:focus + .fi__span {
656
711
  display: flex;
657
712
  align-items: center;
658
713
  justify-content: center;
659
- background-color: #f5f5f5;
660
- color: #666;
661
- font-size: 0.85rem;
714
+ background-color: var(--bg-color-secondary, #f5f5f5);
715
+ color: var(--muted-color, #666);
716
+ font-size: var(--font-size-sm, 0.8125rem);
662
717
  border-radius: 4px;
663
718
  animation: pulse 1.5s infinite ease-in-out;
664
719
  }
@@ -678,146 +733,131 @@ select:focus + .fi__span {
678
733
  .secretKeyContainer {
679
734
  display: flex;
680
735
  align-items: center;
681
- margin: 0.75rem 0;
682
- gap: 0.5rem;
683
- background-color: #f9f9f9;
684
- padding: 0.75rem;
685
- border-radius: var(--br);
736
+ margin: 0 0 var(--spacing-sm, 0.5rem);
737
+ gap: var(--spacing-sm, 0.5rem);
738
+ background: var(--bg-color-secondary, #f9f9f9);
739
+ padding: var(--spacing-sm, 0.5rem);
740
+ border-radius: var(--radius-sm, 6px);
686
741
  border: 1px solid var(--border-color, #e0e0e0);
687
742
  width: 100%;
743
+ box-sizing: border-box;
688
744
  }
689
745
 
690
746
  .secretKey {
691
- padding: 0.5rem 0.75rem;
692
- background-color: white;
693
- border-radius: var(--br);
747
+ padding: 0.4rem 0.65rem;
748
+ background: var(--paper, #fff);
749
+ border-radius: var(--radius-sm, 6px);
694
750
  font-family: monospace;
695
- font-size: 0.85rem;
751
+ font-size: var(--font-size-sm, 0.8125rem);
696
752
  letter-spacing: 1px;
697
- color: #333;
753
+ color: var(--text-color, #333);
698
754
  user-select: all;
699
- border: 1px solid #ddd;
755
+ border: 1px solid var(--border-color, #ddd);
700
756
  flex-grow: 1;
701
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
757
+ min-width: 0;
758
+ overflow-wrap: anywhere;
702
759
  }
703
760
 
704
761
  .copyButton {
705
- padding: 0.4rem 0.75rem;
706
- background-color: var(--primary-color);
707
- color: white;
708
- border: none;
709
- border-radius: var(--br);
710
- cursor: pointer;
711
- font-size: 0.8rem;
712
- transition: all 0.2s ease;
713
- display: flex;
714
- align-items: center;
715
-
716
- &:hover {
717
- filter: brightness(90%);
718
- }
719
-
720
- &:active {
721
- transform: translateY(1px);
722
- }
762
+ @include button-secondary;
763
+ @include button-small;
723
764
  }
724
765
 
725
766
  .verificationCodeContainer {
726
767
  display: flex;
727
768
  align-items: center;
728
- gap: 0.75rem;
729
- margin: 0.75rem 0;
730
- background-color: #f9f9f9;
731
- padding: 1rem;
732
- border-radius: var(--br);
733
- border: 1px solid var(--border-color, #e0e0e0);
734
- width: 100%;
769
+ flex-wrap: wrap;
770
+ gap: var(--spacing-sm, 0.5rem);
771
+ /* Was a tinted, bordered strip around an input and a button — a third
772
+ surface for two controls that already have their own edges. */
773
+ margin: 0;
735
774
  }
736
775
 
776
+ /* Same fight with `global.css` as `.input` — see the note there. */
737
777
  .verificationCodeInput {
738
- padding: 0.75rem;
739
- font-size: 1.1rem;
778
+ @include input-base;
779
+
780
+ width: 9rem !important;
781
+ flex: 0 0 auto;
782
+ padding: 0 0.65rem !important;
783
+ border: 1px solid var(--border-color) !important;
784
+ border-radius: var(--radius-sm) !important;
785
+ background: var(--paper) !important;
786
+ font-size: 1.05rem !important;
787
+ line-height: 1.25 !important;
740
788
  letter-spacing: 4px;
741
- width: 140px;
742
789
  text-align: center;
743
- border: 1px solid #ddd;
744
- border-radius: var(--br);
745
790
  font-family: monospace;
746
- font-weight: 600;
747
- color: #333;
748
- background-color: white;
749
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
791
+ font-weight: var(--font-weight-semibold, 600);
750
792
 
751
793
  &:focus {
752
- border-color: var(--primary-color);
753
794
  outline: none;
754
- box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.1);
795
+ border-color: var(--primary-color) !important;
796
+ box-shadow: var(--focus-ring, 0 0 0 3px rgba(0, 118, 192, 0.32)) !important;
755
797
  }
756
798
 
757
799
  &::placeholder {
758
- color: #bbb;
800
+ color: var(--muted-color, #bbb);
759
801
  letter-spacing: 2px;
760
802
  }
761
803
  }
762
804
 
763
805
  .recoveryCodes {
764
- margin: 0.75rem 0;
765
- padding: 1rem;
766
- background-color: #f9f9f9;
767
- border-radius: var(--br);
806
+ margin: 0 0 var(--spacing-sm, 0.5rem);
807
+ padding: var(--spacing-md, 1rem);
808
+ background: var(--bg-color-secondary, #f9f9f9);
809
+ border-radius: var(--radius-sm, 6px);
768
810
  border: 1px solid var(--border-color, #e0e0e0);
769
811
  width: 100%;
812
+ box-sizing: border-box;
770
813
  }
771
814
 
772
815
  .recoveryCodesContainer {
773
816
  display: grid;
774
- grid-template-columns: repeat(2, 1fr);
775
- gap: 0.75rem;
776
- margin-bottom: 1rem;
777
-
778
- @media (max-width: 768px) {
779
- grid-template-columns: 1fr;
780
- }
817
+ /* Was two fixed columns, which on the narrower fields column left the
818
+ codes stretched to twice the width of the text in them. */
819
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
820
+ gap: var(--spacing-sm, 0.5rem);
821
+ margin-bottom: var(--spacing-md, 1rem);
781
822
  }
782
823
 
783
824
  .recoveryCode {
784
- padding: 0.5rem 0.75rem;
785
- background-color: white;
786
- border-radius: var(--br);
787
- border: 1px solid #ddd;
825
+ padding: 0.4rem 0.65rem;
826
+ background: var(--paper, #fff);
827
+ border-radius: var(--radius-sm, 6px);
828
+ border: 1px solid var(--border-color, #ddd);
788
829
  font-family: monospace;
789
- font-size: 0.85rem;
830
+ font-size: var(--font-size-sm, 0.8125rem);
790
831
  letter-spacing: 1px;
791
- color: #333;
832
+ color: var(--text-color, #333);
792
833
  user-select: all;
793
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
794
834
  text-align: center;
795
835
  }
796
836
 
797
837
  .noRecoveryCodes {
798
- padding: 1rem;
838
+ padding: var(--spacing-md, 1rem);
799
839
  text-align: center;
800
- color: #666;
840
+ color: var(--muted-color, #666);
801
841
  font-style: italic;
802
- font-size: 0.9rem;
842
+ font-size: var(--font-size-sm, 0.8125rem);
803
843
  }
804
844
 
805
845
  .recoveryCodesWarning {
806
- margin-top: 0.75rem;
846
+ margin: 0;
807
847
  padding: 0.75rem;
808
848
  background-color: var(--danger-light, rgba(231, 76, 60, 0.1));
809
849
  border-left: 3px solid var(--danger-color, #e74c3c);
810
- border-radius: var(--br);
811
- color: #333;
812
- font-size: 0.85rem;
850
+ border-radius: var(--radius-sm, 6px);
851
+ color: var(--paragraph-color, #333);
852
+ font-size: var(--font-size-sm, 0.8125rem);
853
+ line-height: 1.5;
813
854
  }
814
855
 
815
856
  .setupActions {
816
857
  display: flex;
817
- justify-content: flex-end;
818
- margin-top: 1.5rem;
819
- padding-top: 1rem;
820
- border-top: 1px solid var(--border-color, #f0f0f0);
858
+ /* The steps hang off the left margin; so does the way out of them. */
859
+ justify-content: flex-start;
860
+ margin-top: var(--spacing-lg, 1.5rem);
821
861
  }
822
862
 
823
863
  .crmtitle {