@visns-studio/visns-components 6.3.3 → 6.3.4

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.
package/package.json CHANGED
@@ -91,7 +91,7 @@
91
91
  "react-dom": "^17.0.0 || ^18.0.0"
92
92
  },
93
93
  "name": "@visns-studio/visns-components",
94
- "version": "6.3.3",
94
+ "version": "6.3.4",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -2306,6 +2306,11 @@ function Field({
2306
2306
  data-name={settings.id}
2307
2307
  type="radio"
2308
2308
  className={`${styles.fi__customcheckbox} radio-input`}
2309
+ // Radio renderers previously ignored the
2310
+ // field's read-only state, leaving
2311
+ // `readOnlyWhen` configs claiming a lock
2312
+ // the UI didn't enforce.
2313
+ disabled={isFieldReadOnly}
2309
2314
  value={option.id}
2310
2315
  checked={
2311
2316
  inputValue !== undefined &&
@@ -2336,6 +2341,11 @@ function Field({
2336
2341
  data-name={settings.id}
2337
2342
  type="radio"
2338
2343
  className={`${styles.fi__customcheckbox} radio-input`}
2344
+ // Radio renderers previously ignored the
2345
+ // field's read-only state, leaving
2346
+ // `readOnlyWhen` configs claiming a lock
2347
+ // the UI didn't enforce.
2348
+ disabled={isFieldReadOnly}
2339
2349
  value={option.id}
2340
2350
  checked={
2341
2351
  inputValue !== undefined &&
@@ -115,6 +115,12 @@ function Form({
115
115
  const [inputClass, setInputClass] = useState({});
116
116
  const [tableData, setTableData] = useState({ columns: [], dataSource: [] });
117
117
  const [tableProps, setTableProps] = useState({});
118
+ // A `save.return.type: 'table'` form (the report filter rows in
119
+ // GenericDetail) renders nothing at all when the query comes back empty,
120
+ // so "no rows matched" and "you have not run it yet" looked identical: an
121
+ // unchanged blank panel under a "generated successfully" toast. This
122
+ // separates the two so the empty result can say so.
123
+ const [hasQueried, setHasQueried] = useState(false);
118
124
  const [uploadProgress, setUploadProgress] = useState(0);
119
125
 
120
126
  // Group selection state management
@@ -2091,6 +2097,11 @@ function Form({
2091
2097
  );
2092
2098
  }
2093
2099
 
2100
+ // Set before the branch: an empty result is
2101
+ // still a result, and that is the case the
2102
+ // empty state exists for.
2103
+ setHasQueried(true);
2104
+
2094
2105
  if (
2095
2106
  res.data[formSettings.save.return.param]
2096
2107
  ?.length > 0
@@ -3300,6 +3311,27 @@ function Form({
3300
3311
  />
3301
3312
  </div>
3302
3313
  )}
3314
+
3315
+ {/* Empty result. Only ever after a run — before that the
3316
+ panel is a filter row waiting to be used, and telling
3317
+ the user there is nothing there would be wrong. Copy is
3318
+ overridable per report via `save.return.empty`. */}
3319
+ {hasQueried &&
3320
+ formSettings?.save?.return?.type === 'table' &&
3321
+ !(tableData?.dataSource?.length > 0) && (
3322
+ <div
3323
+ className={`${styles.formItem} ${styles.fwItem} ${styles.emptyResult}`}
3324
+ >
3325
+ <p>
3326
+ {formSettings.save.return.empty?.title ||
3327
+ 'No records matched'}
3328
+ </p>
3329
+ <span>
3330
+ {formSettings.save.return.empty?.message ||
3331
+ 'Nothing came back for these filters. Widen them and run the report again.'}
3332
+ </span>
3333
+ </div>
3334
+ )}
3303
3335
  </form>
3304
3336
  <div className={styles.polActions}>
3305
3337
  {formSettings.hasOwnProperty('customButton') &&
@@ -2679,6 +2679,22 @@ function GenericDetail({
2679
2679
  >
2680
2680
  <span>{formConfig.title}</span>
2681
2681
  </div>
2682
+ {/* Optional one-line subtitle. A
2683
+ report tab is a name and a set
2684
+ of controls with nothing saying
2685
+ what it returns; a tab that has
2686
+ something to say can now say
2687
+ it, and one that has not is
2688
+ unchanged. */}
2689
+ {formConfig.description && (
2690
+ <p
2691
+ className={
2692
+ styles.gridtxt__description
2693
+ }
2694
+ >
2695
+ {formConfig.description}
2696
+ </p>
2697
+ )}
2682
2698
  <Form
2683
2699
  key={`form-${activeTabConfig.id}`}
2684
2700
  columnId={routeParams[urlParam]}
@@ -335,43 +335,54 @@ input[type='file'] {
335
335
 
336
336
 
337
337
 
338
+ /* The form's primary action (Save / View Report / Refresh).
339
+
340
+ This was a bespoke button — 1.25em type on 0.65rem padding, its own radius
341
+ token, its own transition curve, and a hover that swapped to pale blue on
342
+ blue. Sat next to `.btn` (the shared primary) in the same action bar, so the
343
+ two controls that are meant to read as a pair were a different height, a
344
+ different type size and a different hover. It is the same button as every
345
+ other primary in the library; it just needed to say so. */
338
346
  .saveBtn {
339
- width: max-content;
340
- display: inline-block;
341
- position: relative;
342
- padding: 0.65rem 1rem;
343
- cursor: pointer;
344
- font-size: 1rem;
345
- color: var(--tertiary-color);
346
- text-decoration: none;
347
- overflow: hidden;
348
- background: var(--primary-color);
349
- border: 1px solid rgba(var(--primary-color--rgb), 1.1);
350
- border-radius: var(--br);
351
- outline: none;
352
- transition: all 0.2s cubic-bezier(0.85, 0, 0.15, 1) 0s;
353
- font-size: 1.25em;
354
-
355
- &:hover {
356
- color: var(--primary-color);
357
- background: var(--highlight-color);
358
- border: 1px solid rgba(var(--highlight-rgb), 1.05);
359
- }
347
+ @include button-primary;
360
348
  }
361
349
 
350
+ /* The inline form's action bar.
351
+
352
+ Was `position: fixed; bottom: 15px; right: 20px` with `float: left`
353
+ children: the buttons left the form entirely and parked in the corner of
354
+ the *viewport*. On a short form (the report filter rows in GenericDetail —
355
+ one row of dropdowns) that put "View Report" nowhere near the filters it
356
+ runs; on a form that returns a results grid it floated on top of the grid's
357
+ last rows and its horizontal scrollbar.
358
+
359
+ It is a footer to the form it belongs to, so it lives at the end of the
360
+ form. `sticky` keeps what the fixed positioning was actually for — the
361
+ action stays reachable while a long form or a 550px results grid is
362
+ scrolled — without detaching it from its panel. */
362
363
  .polActions {
363
- position: fixed;
364
- bottom: 15px;
365
- right: 20px;
366
- width: auto;
367
- z-index: 100;
364
+ position: sticky;
365
+ bottom: 0;
366
+ z-index: 5;
367
+ display: flex;
368
+ align-items: center;
369
+ justify-content: flex-end;
370
+ gap: var(--spacing-sm, 0.5rem);
371
+ width: 100%;
372
+ margin-top: var(--spacing-lg, 1.5rem);
373
+ padding: var(--spacing-sm, 0.5rem) 0;
374
+ /* Opaque, or the form scrolls through the bar while it is stuck. */
375
+ background: var(--surface-color, #fff);
376
+ border-top: 1px solid var(--border-color, #e1e1e1);
368
377
  }
369
378
 
370
- .polActions button {
371
- display: block;
372
- float: left;
373
- padding: 0.35em 1em;
374
- margin: 0 0.15em;
379
+ /* One primary per bar. Download / Sort / Save & Exit are alternatives to the
380
+ main action, not competitors: identical geometry, no fill. Previously every
381
+ button in here was a filled brand-blue block, so a filter row offering
382
+ "Download Report" and "View Report" gave no clue which one was the thing
383
+ you came to do. */
384
+ .polActions .btn {
385
+ @include button-secondary;
375
386
  }
376
387
 
377
388
  /* For in-place editing mode */
@@ -421,6 +432,37 @@ input[type='file'] {
421
432
  display: none;
422
433
  }
423
434
 
435
+ /* Empty result for `save.return.type: 'table'` forms. Deliberately not a
436
+ card and not an alert: an empty report is an ordinary outcome, so it is a
437
+ quiet block of centred text on the panel it would have filled, drawn with
438
+ the same hairline the rest of the page separates things with. */
439
+ .emptyResult {
440
+ display: flex;
441
+ flex-direction: column;
442
+ align-items: center;
443
+ justify-content: center;
444
+ gap: var(--spacing-xs, 0.25rem);
445
+ padding: var(--spacing-2xl, 3rem) var(--spacing-md, 1rem);
446
+ text-align: center;
447
+ border: 1px dashed var(--border-color, #e1e1e1);
448
+ border-radius: var(--radius-md, 10px);
449
+ background: var(--alternate-color, transparent);
450
+
451
+ p {
452
+ margin: 0;
453
+ font-size: var(--font-size-md, 0.9375rem);
454
+ font-weight: var(--font-weight-semibold, 600);
455
+ color: var(--heading-color, inherit);
456
+ }
457
+
458
+ span {
459
+ max-width: 42ch;
460
+ font-size: var(--font-size-sm, 0.8125rem);
461
+ line-height: 1.5;
462
+ color: var(--muted-color, #6e7276);
463
+ }
464
+ }
465
+
424
466
  .buttonContainer {
425
467
  display: flex;
426
468
  justify-content: center;
@@ -86,15 +86,31 @@
86
86
  gap: 0.5rem;
87
87
  }
88
88
 
89
+ /* The tab rail. Identical spec to `.grid__subnav` in GenericIndex.module.scss
90
+ — the two pages share one rail, so keep them in step.
91
+
92
+ It was a white panel under a 50px brand-tinted blur, holding one tinted blue
93
+ pill per tab. Every item therefore carried the same weight as the selected
94
+ one, and a seven-tab rail (the reports page) read as a stack of seven
95
+ buttons rather than as navigation with one thing chosen. 6.3's answer
96
+ everywhere else on the page is a flat surface, a hairline and quiet ink, so
97
+ that is what the rail gets: unselected tabs are plain text on the panel, and
98
+ the brand blue is spent once, on the tab you are actually on. */
89
99
  .grid__subnav {
100
+ /* `flex: 1` (basis 0) against `.grid__subcontent`'s `flex: 0 1 80%` is
101
+ left exactly as it was: `.grid__subrow` is `flex-wrap: wrap`, so a
102
+ flex-basis here large enough to overflow the row would wrap the content
103
+ panel onto its own line rather than shrink it. Only the treatment
104
+ changes below. */
90
105
  flex: 1;
91
- background: white;
106
+ background: var(--surface-color, #fff);
107
+ border: 1px solid var(--border-color, #e1e1e1);
92
108
  border-radius: var(--br);
93
109
  box-sizing: border-box;
94
- padding: 5px;
110
+ padding: var(--spacing-xs, 0.25rem);
95
111
  margin: 8px 0;
96
112
  height: auto;
97
- box-shadow: 0 10px 50px rgba(var(--primary-rgb), 0.05);
113
+ box-shadow: none;
98
114
  }
99
115
 
100
116
  .grid__subnav > ul {
@@ -108,32 +124,54 @@
108
124
 
109
125
  .grid__subnav > ul li {
110
126
  width: 100%;
111
- margin-bottom: 3px;
127
+ margin-bottom: var(--spacing-xs, 0.25rem);
128
+ }
129
+
130
+ .grid__subnav > ul li:last-child {
131
+ margin-bottom: 0;
112
132
  }
113
133
 
114
134
  .grid__subnav > ul li a {
115
135
  width: 100%;
116
- padding: 0.35rem 1rem;
136
+ /* 0.35rem was a 5px-tall hit area for a control people click all day. */
137
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
117
138
  box-sizing: border-box;
118
139
  display: block;
119
140
  text-decoration: none;
120
- background: rgba(var(--primary-rgb), 0.05);
121
- color: var(--primary-color);
122
- border-radius: 5px;
123
- transition: background 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
124
- outline: none;
141
+ background: none;
142
+ color: var(--text-color, #374151);
143
+ font-size: var(--font-size-sm, 0.8125rem);
144
+ font-weight: var(--font-weight-medium, 500);
145
+ line-height: 1.35;
146
+ border-radius: var(--radius-sm, 6px);
147
+ transition:
148
+ background-color var(--speed, 0.18s) var(--ease, ease),
149
+ color var(--speed, 0.18s) var(--ease, ease);
125
150
  cursor: pointer;
126
151
  }
127
152
 
128
153
  .grid__subnav > ul li:hover a {
129
- background: var(--primary-color);
130
- color: var(--background-color);
154
+ /* Hover is a hint, not a commitment. It used to fill solid brand blue —
155
+ indistinguishable from the selected tab, so passing the mouse across the
156
+ rail made it look as though the selection was following the cursor. */
157
+ background: var(--hover-color, rgba(0, 0, 0, 0.04));
158
+ color: var(--heading-color, inherit);
131
159
  }
132
160
 
133
161
  .grid__subnav > ul li .subactive {
134
- font-weight: 700;
162
+ font-weight: var(--font-weight-semibold, 600);
135
163
  background: var(--primary-color);
136
- color: var(--background-color);
164
+ /* Was --background-color, the page's off-white; white is what the button
165
+ and nav-pill treatments elsewhere put on this blue. */
166
+ color: #fff;
167
+ }
168
+
169
+ /* `outline: none` used to sit on the link with nothing put back, so the rail
170
+ was unusable by keyboard. The app's own ring token, same as every other
171
+ control. */
172
+ .grid__subnav > ul li a:focus-visible {
173
+ outline: none;
174
+ box-shadow: var(--focus-ring, 0 0 0 3px rgba(0, 118, 192, 0.32));
137
175
  }
138
176
 
139
177
  .grid__minheight {
@@ -146,9 +184,12 @@
146
184
 
147
185
  .grid__subcontent {
148
186
  flex: 0 1 80%;
149
- background: var(--tertiary-color);
187
+ background: var(--surface-color, #fff);
150
188
  border-radius: var(--br);
151
- border: 1px solid rgba(var(--primary-rgb), 0.05);
189
+ /* Was the brand blue at 5% — a tint so faint the panel had no edge at all
190
+ while the rail beside it did. The shared hairline, same as every other
191
+ surface on the page. */
192
+ border: 1px solid var(--border-color, #e1e1e1);
152
193
  box-sizing: border-box;
153
194
  padding: 2px;
154
195
  margin: 8px 0;
@@ -163,6 +204,24 @@
163
204
  margin: 0 0 30px 0;
164
205
  }
165
206
 
207
+ /* The tab panel's own inset.
208
+
209
+ `.gridtxt` had no rule at all, and its container (`.grid__subcontent`,
210
+ `.grid__full`) carries `padding: 2px` — so a panel's section heading and any
211
+ form inside it sat 2px from the panel border. Only the overview `<ul>` was
212
+ readable, because it padded itself. On the reports page that meant the
213
+ filter row's dropdowns and date pickers were flush against the panel edge.
214
+
215
+ The inset belongs to the panel, not to one child of it, so it is stated
216
+ here once and the `<ul>` below gives up its horizontal padding to match.
217
+ The `<ul>`'s vertical padding stays: with several heading/list pairs in one
218
+ `.gridtxt` (the overview tabs) that is what separates one section from the
219
+ next. */
220
+ .gridtxt {
221
+ padding: var(--spacing-md, 1rem) var(--spacing-lg, 1.5rem)
222
+ var(--spacing-lg, 1.5rem);
223
+ }
224
+
166
225
  .gridtxt__header {
167
226
  width: 100%;
168
227
  display: block;
@@ -192,6 +251,21 @@
192
251
  font-weight: 700;
193
252
  }
194
253
 
254
+ /* Optional one-line subtitle under a section heading (GenericDetail renders it
255
+ from a tab's `description`). Sits in the heading's own space — it belongs to
256
+ the label, not to the content — so it takes over the heading's bottom margin
257
+ rather than adding a third gap. */
258
+ .gridtxt__description {
259
+ /* Pulls back half of the heading's 1rem bottom margin, so the subtitle sits
260
+ close under its heading and the whole block is separated from the
261
+ content below rather than floating between the two. */
262
+ margin: calc(-1 * var(--spacing-sm, 0.5rem)) 0 var(--spacing-lg, 1.5rem);
263
+ max-width: 70ch;
264
+ font-size: var(--font-size-sm, 0.8125rem);
265
+ line-height: 1.55;
266
+ color: var(--muted-color, #6e7276);
267
+ }
268
+
195
269
  .gridtxt > ul {
196
270
  width: 100%;
197
271
  display: flex;
@@ -200,7 +274,9 @@
200
274
  flex-wrap: wrap;
201
275
  list-style: none;
202
276
  box-sizing: border-box;
203
- padding: 0.85rem;
277
+ /* Horizontal padding removed — `.gridtxt` owns the panel inset now (see
278
+ the note there). The vertical padding separates stacked sections. */
279
+ padding: 0.85rem 0;
204
280
  margin: 0;
205
281
  }
206
282
 
@@ -114,15 +114,19 @@
114
114
  gap: 0.5rem;
115
115
  }
116
116
 
117
+ /* The tab rail. Identical spec to `.grid__subnav` in GenericDetail.module.scss
118
+ — the two pages share one rail, so keep them in step; the reasoning for the
119
+ treatment is written out there. */
117
120
  .grid__subnav {
118
121
  flex: 1;
119
- background: white;
122
+ background: var(--surface-color, #fff);
123
+ border: 1px solid var(--border-color, #e1e1e1);
120
124
  border-radius: var(--br);
121
125
  box-sizing: border-box;
122
- padding: 5px;
126
+ padding: var(--spacing-xs, 0.25rem);
123
127
  margin: 8px 0;
124
128
  height: auto;
125
- box-shadow: 0 10px 50px rgba(var(--primary-rgb), 0.05);
129
+ box-shadow: none;
126
130
  }
127
131
 
128
132
  .grid__subnav > ul {
@@ -136,32 +140,45 @@
136
140
 
137
141
  .grid__subnav > ul li {
138
142
  width: 100%;
139
- margin-bottom: 3px;
143
+ margin-bottom: var(--spacing-xs, 0.25rem);
144
+ }
145
+
146
+ .grid__subnav > ul li:last-child {
147
+ margin-bottom: 0;
140
148
  }
141
149
 
142
150
  .grid__subnav > ul li a {
143
151
  width: 100%;
144
- padding: 0.35rem 1rem;
152
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
145
153
  box-sizing: border-box;
146
154
  display: block;
147
155
  text-decoration: none;
148
- background: rgba(var(--primary-rgb), 0.05);
149
- color: var(--primary-color);
150
- border-radius: 5px;
151
- transition: background 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
152
- outline: none;
156
+ background: none;
157
+ color: var(--text-color, #374151);
158
+ font-size: var(--font-size-sm, 0.8125rem);
159
+ font-weight: var(--font-weight-medium, 500);
160
+ line-height: 1.35;
161
+ border-radius: var(--radius-sm, 6px);
162
+ transition:
163
+ background-color var(--speed, 0.18s) var(--ease, ease),
164
+ color var(--speed, 0.18s) var(--ease, ease);
153
165
  cursor: pointer;
154
166
  }
155
167
 
156
168
  .grid__subnav > ul li:hover a {
157
- background: var(--primary-color);
158
- color: var(--tertiary-color);
169
+ background: var(--hover-color, rgba(0, 0, 0, 0.04));
170
+ color: var(--heading-color, inherit);
159
171
  }
160
172
 
161
173
  .grid__subnav > ul li .subactive {
162
- font-weight: 700;
174
+ font-weight: var(--font-weight-semibold, 600);
163
175
  background: var(--primary-color);
164
- color: var(--tertiary-color);
176
+ color: #fff;
177
+ }
178
+
179
+ .grid__subnav > ul li a:focus-visible {
180
+ outline: none;
181
+ box-shadow: var(--focus-ring, 0 0 0 3px rgba(0, 118, 192, 0.32));
165
182
  }
166
183
 
167
184
  .grid__subnav__alternate {
@@ -1,3 +1,17 @@
1
+ /**
2
+ * The tab rail's own stylesheet, which predates the 6.3 token language: it was
3
+ * still drawing its own card (a hardcoded rgba drop shadow and radius) and its
4
+ * own type scale (0.85/0.9rem literals) around items that the host surface —
5
+ * `.grid__subnav` in GenericIndex/GenericDetail — already frames. Two nested
6
+ * cards, two shadows, and a rail whose type did not match anything else on the
7
+ * page. Everything here now reads the same tokens as the rest of the library,
8
+ * and the framing is left to the surface it sits on.
9
+ *
10
+ * Literal fallbacks throughout: a bare `var()` for a token a consuming app has
11
+ * not declared is invalid at computed-value time and the whole declaration is
12
+ * dropped, which is exactly how a rail like this loses its padding.
13
+ */
14
+
1
15
  .tableFilterContainer {
2
16
  width: 100%;
3
17
  position: relative;
@@ -9,11 +23,11 @@
9
23
  list-style: none;
10
24
  padding: 0;
11
25
  margin: 0;
12
- font-size: 0.9rem;
26
+ font-size: var(--font-size-sm, 0.8125rem);
13
27
  width: 100%;
14
- border-radius: 8px;
15
- overflow: hidden;
16
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
28
+ /* No radius, no shadow, no `overflow: hidden`. The panel around this list
29
+ supplies the surface; the clipping additionally cut the focus ring off
30
+ the first and last items. */
17
31
  }
18
32
 
19
33
  /* Parent category styling */
@@ -29,19 +43,23 @@
29
43
  display: flex;
30
44
  justify-content: space-between;
31
45
  align-items: center;
32
- padding: 8px 16px;
33
- font-weight: 600;
34
- font-size: 0.9rem;
46
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
47
+ border-radius: var(--radius-sm, 6px);
48
+ font-weight: var(--font-weight-semibold, 600);
49
+ font-size: var(--font-size-sm, 0.8125rem);
35
50
  background-color: var(--primary-color, #000);
36
- color: var(--tertiary-color, #fff);
37
- transition: all 0.2s ease;
51
+ color: #fff;
52
+ /* Was `all 0.2s`, which animates layout properties too. */
53
+ transition:
54
+ background-color var(--speed, 0.18s) var(--ease, ease),
55
+ color var(--speed, 0.18s) var(--ease, ease);
38
56
  position: relative;
39
57
  text-decoration: none;
40
58
  }
41
59
 
42
60
  .parentLink:hover {
43
- background-color: var(--secondary-color, #222) !important;
44
- color: var(--tertiary-color, #fff) !important;
61
+ background-color: var(--primary-color-darker, #222) !important;
62
+ color: #fff !important;
45
63
  text-decoration: none;
46
64
  }
47
65
 
@@ -63,7 +81,9 @@
63
81
  overflow: hidden;
64
82
  transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
65
83
  opacity: 0;
66
- background-color: var(--bg-color, #f5f5f5);
84
+ /* Flush with the rail. The child links carry their own hover surface now,
85
+ so a tint behind the whole submenu only made a second panel edge. */
86
+ background: none;
67
87
  }
68
88
 
69
89
  .visibleMenu {
@@ -74,7 +94,7 @@
74
94
  .childItem {
75
95
  margin: 0;
76
96
  position: relative;
77
- border-bottom: 1px solid rgba(var(--paragraph-color-rgb, 0, 0, 0), 0.03);
97
+ border-bottom: 1px solid var(--border-color, #e1e1e1);
78
98
  }
79
99
 
80
100
  .childItem:last-child {
@@ -82,18 +102,20 @@
82
102
  }
83
103
 
84
104
  .childLink {
85
- padding: 6px 16px 6px 26px;
86
- background-color: var(--bg-color, #f5f5f5);
87
- color: var(--paragraph-color, #333);
88
- font-weight: normal;
89
- font-size: 0.85rem;
105
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem) var(--spacing-sm, 0.5rem)
106
+ var(--spacing-xl, 1.75rem);
107
+ background: none;
108
+ color: var(--text-color, #333);
109
+ font-weight: var(--font-weight-regular, 400);
110
+ font-size: var(--font-size-sm, 0.8125rem);
111
+ border-radius: var(--radius-sm, 6px);
90
112
  display: block;
91
113
  text-decoration: none;
92
114
  }
93
115
 
94
116
  .childLink:hover {
95
117
  background-color: var(--hover-color, #e8e8e8) !important;
96
- color: var(--primary-color, #000) !important;
118
+ color: var(--heading-color, #000) !important;
97
119
  text-decoration: none;
98
120
  }
99
121
 
@@ -104,37 +126,55 @@
104
126
  color: rgba(var(--paragraph-color-rgb, 153, 153, 153), 0.6);
105
127
  }
106
128
 
107
- /* Active states */
129
+ /* Active states. The brand fill is the one loud thing in the rail, and it
130
+ marks exactly one item — which is what makes it readable as a selection
131
+ rather than as decoration. */
108
132
  .subactive {
109
- font-weight: 600;
133
+ font-weight: var(--font-weight-semibold, 600);
110
134
  background: var(--primary-color, #000) !important;
111
- color: var(--tertiary-color, #fff) !important;
135
+ /* Was --tertiary-color, which several projects set to their page
136
+ off-white; white is what sits on this blue elsewhere. */
137
+ color: #fff !important;
112
138
  }
113
139
 
114
140
  .subactivechildren {
115
- font-weight: 500;
116
- background-color: var(--hover-color, #f0f0f0) !important;
141
+ font-weight: var(--font-weight-medium, 500);
142
+ background-color: var(--primary-color-light, #f0f0f0) !important;
117
143
  color: var(--primary-color, #000) !important;
118
144
  }
119
145
 
120
- /* Base link styling */
146
+ /* Base link styling. Unselected tabs are text on the panel, not tinted
147
+ blocks — see the note on `.grid__subnav` in GenericDetail.module.scss. */
121
148
  .link {
122
149
  display: block;
123
- padding: 6px 16px;
150
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
124
151
  text-decoration: none;
125
- background: var(--bg-color, #f5f5f5);
126
- color: var(--paragraph-color, #333);
127
- font-size: 0.85rem;
128
- transition: all 0.2s ease;
152
+ background: none;
153
+ color: var(--text-color, #333);
154
+ font-size: var(--font-size-sm, 0.8125rem);
155
+ font-weight: var(--font-weight-medium, 500);
156
+ border-radius: var(--radius-sm, 6px);
157
+ transition:
158
+ background-color var(--speed, 0.18s) var(--ease, ease),
159
+ color var(--speed, 0.18s) var(--ease, ease);
129
160
  cursor: pointer;
130
161
  }
131
162
 
132
163
  .link:hover {
133
164
  background: var(--hover-color, #e8e8e8) !important;
134
- color: var(--primary-color, #000) !important;
165
+ /* Was the brand blue, which on hover made an unselected tab look like the
166
+ selected one. */
167
+ color: var(--heading-color, #000) !important;
135
168
  text-decoration: none;
136
169
  }
137
170
 
171
+ .link:focus-visible,
172
+ .childLink:focus-visible,
173
+ .parentLink:focus-visible {
174
+ outline: none;
175
+ box-shadow: var(--focus-ring, 0 0 0 3px rgba(0, 118, 192, 0.32));
176
+ }
177
+
138
178
  .activetab button {
139
179
  font-weight: bold;
140
180
  background: var(--hover-color, #e0e0e0) !important;
@@ -146,19 +186,20 @@
146
186
  display: none;
147
187
  align-items: center;
148
188
  justify-content: space-between;
149
- padding: 12px 15px;
189
+ padding: var(--spacing-sm, 0.75rem) var(--spacing-md, 1rem);
150
190
  background: var(--primary-color, #000);
151
- color: var(--tertiary-color, white);
152
- border-radius: 8px;
191
+ color: #fff;
192
+ border-radius: var(--radius-sm, 6px);
153
193
  cursor: pointer;
154
- margin-bottom: 10px;
194
+ margin-bottom: var(--spacing-sm, 0.5rem);
155
195
  border: none;
156
- box-shadow: 0 1px 3px rgba(var(--paragraph-color-rgb, 0, 0, 0), 0.2);
196
+ box-shadow: var(--shadow-sm, 0 1px 3px rgba(0, 0, 0, 0.2));
157
197
  }
158
198
 
159
199
  .activeFilterLabel {
160
- font-weight: 600;
161
- color: var(--tertiary-color, white);
200
+ font-weight: var(--font-weight-semibold, 600);
201
+ font-size: var(--font-size-sm, 0.8125rem);
202
+ color: #fff;
162
203
  }
163
204
 
164
205
  .mobileMenuIcon {
@@ -175,7 +216,7 @@
175
216
  display: block;
176
217
  height: 2px;
177
218
  width: 100%;
178
- background-color: var(--tertiary-color, white);
219
+ background-color: #fff;
179
220
  border-radius: 2px;
180
221
  transition: all 0.3s ease;
181
222
  }
@@ -206,20 +247,17 @@
206
247
  display: none; /* Hide hamburger menu on tablets */
207
248
  }
208
249
 
209
- /* Slightly adjust padding for better tablet experience */
210
- .parentLink {
211
- padding: 9px 16px;
212
- font-size: 0.9rem;
213
- }
214
-
250
+ /* Roomier tap targets. Only the padding moves: the literal 0.85/0.9rem
251
+ sizes that used to sit here re-declared the rail's type at a scale
252
+ nothing else on the page uses, so the labels changed size at 1024px for
253
+ no reason. Density is a padding question. */
254
+ .parentLink,
215
255
  .link {
216
- padding: 9px 16px;
217
- font-size: 0.85rem;
256
+ padding: 0.65rem var(--spacing-md, 1rem);
218
257
  }
219
258
 
220
259
  .childLink {
221
- padding: 7px 16px 7px 26px;
222
- font-size: 0.85rem;
260
+ padding: 0.65rem var(--spacing-md, 1rem) 0.65rem var(--spacing-xl, 1.75rem);
223
261
  }
224
262
  }
225
263
 
@@ -240,25 +278,21 @@
240
278
  display: block;
241
279
  max-height: 1000px;
242
280
  overflow-y: auto;
243
- border: 1px solid rgba(var(--paragraph-color-rgb, 0, 0, 0), 0.1);
244
- border-radius: 8px;
245
- margin-bottom: 10px;
246
- box-shadow: 0 2px 5px rgba(var(--paragraph-color-rgb, 0, 0, 0), 0.1);
247
- }
248
-
249
- .parentLink {
250
- padding: 10px 16px; /* Slightly larger touch targets for mobile */
251
- font-size: 0.9rem;
281
+ padding: var(--spacing-xs, 0.25rem);
282
+ border: 1px solid var(--border-color, #e1e1e1);
283
+ border-radius: var(--br, 8px);
284
+ margin-bottom: var(--spacing-sm, 0.5rem);
285
+ box-shadow: var(--shadow-sm, 0 2px 5px rgba(0, 0, 0, 0.1));
252
286
  }
253
287
 
288
+ /* Larger tap targets on a phone; the type scale is left alone, as above. */
289
+ .parentLink,
254
290
  .link {
255
- padding: 10px 16px; /* Slightly larger touch targets for mobile */
256
- font-size: 0.85rem;
291
+ padding: 0.75rem var(--spacing-md, 1rem);
257
292
  }
258
293
 
259
294
  .childLink {
260
- padding: 8px 16px 8px 26px;
261
- font-size: 0.85rem;
295
+ padding: 0.75rem var(--spacing-md, 1rem) 0.75rem var(--spacing-xl, 1.75rem);
262
296
  }
263
297
 
264
298
  .collapsibleMenu {