chart-factory 0.1.2

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.
@@ -0,0 +1,899 @@
1
+ /**
2
+ * D3Table Component Styles
3
+ *
4
+ * Import with core and table tokens:
5
+ *
6
+ * @import 'chart-factory/core/tokens.css';
7
+ * @import 'chart-factory/core/base.css';
8
+ * @import 'chart-factory/table/tokens.css';
9
+ * @import 'chart-factory/table/table.css';
10
+ */
11
+
12
+ /* ===========================
13
+ TABLE CONTAINER (extends .d3-container)
14
+ =========================== */
15
+
16
+ .d3-table-container {
17
+ display: inline-block;
18
+ background: var(--color-bg-container);
19
+ padding: var(--space-8);
20
+ box-shadow: var(--shadow-container);
21
+ text-align: left;
22
+ }
23
+
24
+ /* Wrapper for table + footer ensures footer matches table width */
25
+ .d3-table-wrapper {
26
+ display: inline-flex;
27
+ flex-direction: column;
28
+ }
29
+
30
+ /* Fill layout: the wrapper must be block-level so the scroll container
31
+ is bounded by the page container, not shrink-wrapped to the table.
32
+ (The footer still matches the table — it's 100% width too.)
33
+ Applied by D3Table.prepareDom(). */
34
+ .d3-table-wrapper--fill {
35
+ display: block;
36
+ width: 100%;
37
+ }
38
+
39
+ .d3-table-title {
40
+ font-family: var(--font-family);
41
+ font-size: var(--font-size-title);
42
+ font-weight: var(--font-weight-bold);
43
+ margin: 0 0 var(--space-2) 0;
44
+ color: var(--color-black);
45
+ letter-spacing: var(--letter-spacing-tight);
46
+ }
47
+
48
+ .d3-table-subtitle {
49
+ font-family: var(--font-family);
50
+ font-size: var(--font-size-subtitle);
51
+ color: var(--color-text-subtle);
52
+ margin: 0 0 var(--space-6) 0;
53
+ line-height: var(--line-height-normal);
54
+ font-weight: var(--font-weight-normal);
55
+ }
56
+
57
+ .d3-table-footer {
58
+ margin-top: 0;
59
+ padding-top: var(--space-4);
60
+ border-top: 1px solid var(--color-border-footer);
61
+ font-family: var(--font-family);
62
+ font-size: var(--font-size-xs);
63
+ color: var(--color-text-faint);
64
+ text-transform: uppercase;
65
+ letter-spacing: var(--letter-spacing-wider);
66
+ font-weight: var(--font-weight-semibold);
67
+ }
68
+
69
+ /* ===========================
70
+ TABLE BASE
71
+ =========================== */
72
+
73
+ table {
74
+ width: auto;
75
+ border-collapse: collapse;
76
+ font-family: var(--font-family);
77
+ font-size: var(--table-font-size);
78
+ margin-top: var(--space-3);
79
+ margin-bottom: 0;
80
+ }
81
+
82
+ /* ===========================
83
+ FILL LAYOUT (default)
84
+ The table fills its container; headers and text cells wrap;
85
+ numeric cells never wrap. Below the JS-computed min-width
86
+ floor, .d3-table-scroll takes over with horizontal scroll.
87
+ =========================== */
88
+
89
+ .d3-table-scroll {
90
+ overflow-x: auto;
91
+ max-width: 100%;
92
+ -webkit-overflow-scrolling: touch;
93
+ }
94
+
95
+ table.d3-table--fill {
96
+ /* Fit content, up to the container, never below the computed floor:
97
+ columns take their natural widths and leftover card width stays
98
+ OUTSIDE the table as plain whitespace instead of pooling inside a
99
+ stretched column. Bar columns then grow floor -> cap from measured
100
+ container slack (adjustBarColumnFlex). */
101
+ width: fit-content;
102
+ max-width: 100%;
103
+ }
104
+
105
+ table.d3-table--fill th {
106
+ white-space: normal;
107
+ }
108
+
109
+ table.d3-table--fill td {
110
+ white-space: normal;
111
+ overflow-wrap: break-word;
112
+ }
113
+
114
+ table.d3-table--fill td.align-right,
115
+ table.d3-table--fill td.align-center,
116
+ table.d3-table--fill td.numeric-cell,
117
+ table.d3-table--fill td.secondary-cell,
118
+ table.d3-table--fill td.rank-cell {
119
+ white-space: nowrap;
120
+ overflow-wrap: normal;
121
+ }
122
+
123
+ /* ===========================
124
+ HEADER
125
+ =========================== */
126
+
127
+ thead {
128
+ border-bottom: var(--table-header-border);
129
+ }
130
+
131
+ th {
132
+ text-align: left;
133
+ padding: var(--table-header-padding-y) var(--table-cell-padding-x) var(--table-header-padding-bottom) var(--table-cell-padding-near);
134
+ font-weight: var(--font-weight-normal);
135
+ font-size: var(--table-header-font-size);
136
+ text-transform: uppercase;
137
+ letter-spacing: var(--letter-spacing-wide);
138
+ color: var(--color-text-muted);
139
+ white-space: nowrap;
140
+ vertical-align: baseline;
141
+ box-sizing: border-box;
142
+ }
143
+
144
+ th:first-child {
145
+ padding-left: 0;
146
+ }
147
+
148
+ th:last-child {
149
+ padding-right: 0;
150
+ }
151
+
152
+ th.align-right {
153
+ text-align: right;
154
+ padding: var(--table-header-padding-y) var(--table-cell-padding-near) var(--table-header-padding-bottom) var(--table-cell-padding-x);
155
+ }
156
+
157
+ th.align-center {
158
+ text-align: center;
159
+ }
160
+
161
+ /* ===========================
162
+ BODY
163
+ =========================== */
164
+
165
+ tbody tr {
166
+ border-bottom: var(--table-row-border);
167
+ }
168
+
169
+ tbody tr:last-child {
170
+ border-bottom: none;
171
+ }
172
+
173
+ tbody tr:hover {
174
+ background-color: var(--color-bg-hover);
175
+ }
176
+
177
+ td {
178
+ padding: var(--table-cell-padding-y) var(--table-cell-padding-x) var(--table-cell-padding-y) var(--table-cell-padding-near);
179
+ color: var(--color-text-primary);
180
+ font-weight: var(--font-weight-normal);
181
+ height: var(--table-cell-height);
182
+ box-sizing: border-box;
183
+ vertical-align: middle;
184
+ white-space: nowrap;
185
+ overflow: hidden;
186
+ text-overflow: ellipsis;
187
+ }
188
+
189
+ td:first-child {
190
+ padding-left: 0;
191
+ }
192
+
193
+ td:last-child {
194
+ padding-right: 0;
195
+ }
196
+
197
+ td.align-right {
198
+ text-align: right;
199
+ padding: var(--table-cell-padding-y) var(--table-cell-padding-near) var(--table-cell-padding-y) var(--table-cell-padding-x);
200
+ }
201
+
202
+ td.align-center {
203
+ text-align: center;
204
+ }
205
+
206
+ /* ===========================
207
+ CELL TYPES
208
+ =========================== */
209
+
210
+ /*
211
+ * Primary cells - bold, larger font
212
+ * Use for: names, titles, key identifiers
213
+ * Generic: primary-cell | Legacy: team-cell
214
+ */
215
+ td.primary-cell,
216
+ td.team-cell {
217
+ font-family: var(--table-primary-font, inherit);
218
+ font-weight: var(--font-weight-bold);
219
+ font-size: var(--table-primary-font-size, var(--table-team-font-size));
220
+ color: var(--color-black);
221
+ white-space: normal;
222
+ }
223
+
224
+ /*
225
+ * Secondary cells - subdued, smaller
226
+ * Use for: ranks, indices, row numbers
227
+ * Generic: secondary-cell | Legacy: rank-cell
228
+ */
229
+ td.secondary-cell,
230
+ td.rank-cell {
231
+ color: var(--color-text-disabled);
232
+ font-weight: var(--font-weight-semibold);
233
+ font-size: var(--table-secondary-font-size, var(--table-rank-font-size));
234
+ }
235
+
236
+ th.auto-width.secondary-cell,
237
+ td.auto-width.secondary-cell,
238
+ th.auto-width.rank-cell,
239
+ td.auto-width.rank-cell {
240
+ /* Compact padding for narrow rank/index columns */
241
+ padding-left: 0;
242
+ padding-right: var(--space-2);
243
+ min-width: var(--col-secondary-min, var(--col-rank-min));
244
+ max-width: var(--col-secondary-max, var(--col-rank-max));
245
+ text-align: left;
246
+ }
247
+
248
+
249
+ /*
250
+ * Muted cells - de-emphasized text
251
+ * Use for: supplementary info, metadata
252
+ */
253
+ td.muted-cell {
254
+ color: var(--color-text-disabled);
255
+ font-size: var(--table-font-size);
256
+ }
257
+
258
+ /*
259
+ * Numeric cells - right-aligned, tabular figures
260
+ * Use for: numbers, statistics, amounts
261
+ */
262
+ td.numeric-cell {
263
+ text-align: right;
264
+ font-variant-numeric: tabular-nums;
265
+ }
266
+
267
+ /* ===========================
268
+ SUBTITLE CELLS
269
+ =========================== */
270
+
271
+ td.has-subtitle {
272
+ height: auto;
273
+ padding-top: var(--space-2);
274
+ padding-bottom: var(--space-2);
275
+ }
276
+
277
+ .cell-with-subtitle {
278
+ display: flex;
279
+ flex-direction: column;
280
+ gap: 1px;
281
+ }
282
+
283
+ .cell-main-text {
284
+ font-weight: var(--font-weight-bold);
285
+ font-size: var(--table-primary-font-size);
286
+ color: var(--color-black);
287
+ line-height: 1.2;
288
+ }
289
+
290
+ .cell-subtitle-text {
291
+ font-weight: var(--font-weight-normal);
292
+ font-size: var(--font-size-md);
293
+ color: var(--color-text-disabled);
294
+ line-height: 1.2;
295
+ }
296
+
297
+ /* ===========================
298
+ STICKY HEADER / STICKY FIRST COLUMN
299
+ =========================== */
300
+
301
+ /* Sticky header. border-collapse drops borders on stuck cells, so the
302
+ header's bottom rule is redrawn as an inset shadow (on the last
303
+ header row only — a group row above it carries no rule). */
304
+ table.d3-table--sticky-header thead th {
305
+ position: sticky;
306
+ top: 0;
307
+ z-index: 2;
308
+ background: var(--table-sticky-bg, #fff);
309
+ }
310
+
311
+ table.d3-table--sticky-header thead tr:last-child th {
312
+ box-shadow: inset 0 -2px 0 var(--color-black, #000);
313
+ }
314
+
315
+ table.d3-table--sticky-header thead {
316
+ border-bottom: none;
317
+ }
318
+
319
+ /* In-wrapper vertical scrolling (config.maxHeight) */
320
+ .d3-table-scroll--vertical {
321
+ overflow-y: auto;
322
+ }
323
+
324
+ /* Floating header: sticky emulation on horizontally-scrolling tables.
325
+ JS translates the thead with the page scroll (position: sticky can't
326
+ escape the horizontal scroll container). will-change lives ONLY on the
327
+ floating state: on the base thead it would create a z-index:auto
328
+ stacking context that traps the th's z-index, letting tbody bar fills
329
+ and heatmap chips paint over the page-stuck header. */
330
+ table.d3-table--sticky-header thead.d3-table-thead--floating {
331
+ position: relative;
332
+ z-index: 2;
333
+ will-change: transform;
334
+ }
335
+
336
+ table.d3-table--sticky-header thead.d3-table-thead--floating th {
337
+ background: var(--table-sticky-bg, #fff);
338
+ }
339
+
340
+ /* Page-level sticky: overflow-x clip does not create a scroll container,
341
+ applied by JS only while the table fits its container horizontally */
342
+ .d3-table-scroll--clip {
343
+ overflow-x: clip;
344
+ }
345
+
346
+ /* Sticky first column */
347
+ table.d3-table--sticky-first-col th:first-child,
348
+ table.d3-table--sticky-first-col td:first-child {
349
+ position: sticky;
350
+ left: 0;
351
+ z-index: 1;
352
+ background: var(--table-sticky-bg, #fff);
353
+ }
354
+
355
+ table.d3-table--sticky-first-col thead th:first-child {
356
+ z-index: 3;
357
+ }
358
+
359
+ table.d3-table--sticky-first-col tbody tr:hover td:first-child {
360
+ background-color: var(--color-bg-hover);
361
+ }
362
+
363
+ /* Edge shadow appears only once the container is actually scrolled */
364
+ .d3-table-scroll.is-h-scrolled table.d3-table--sticky-first-col th:first-child,
365
+ .d3-table-scroll.is-h-scrolled table.d3-table--sticky-first-col td:first-child {
366
+ box-shadow: var(--table-sticky-edge-shadow);
367
+ }
368
+
369
+ /* Both stickies on one table: the corner header cell needs both shadows */
370
+ table.d3-table--sticky-header.d3-table--sticky-first-col thead tr:last-child th:first-child {
371
+ box-shadow: inset 0 -2px 0 var(--color-black, #000);
372
+ }
373
+
374
+ .d3-table-scroll.is-h-scrolled table.d3-table--sticky-header.d3-table--sticky-first-col thead tr:last-child th:first-child {
375
+ box-shadow: inset 0 -2px 0 var(--color-black, #000), var(--table-sticky-edge-shadow);
376
+ }
377
+
378
+ /* ===========================
379
+ COLUMN GROUP HEADERS
380
+ =========================== */
381
+
382
+ .d3-table-group-row th {
383
+ padding-bottom: var(--space-1);
384
+ vertical-align: bottom;
385
+ }
386
+
387
+ .d3-table-group-row th.group-header {
388
+ text-align: center;
389
+ color: var(--color-text-muted);
390
+ /* The rule is a bottom-anchored background (not border-bottom) so it
391
+ can be inset from the cell edges — adjacent groups get a visible
392
+ gap that marks where one group ends and the next begins. */
393
+ background-image: linear-gradient(
394
+ var(--table-group-rule-color, var(--color-border-row, #e5e7eb)),
395
+ var(--table-group-rule-color, var(--color-border-row, #e5e7eb)));
396
+ background-repeat: no-repeat;
397
+ background-position: bottom center;
398
+ background-size: calc(100% - 2 * var(--table-group-rule-inset, 10px)) var(--table-group-rule-width, 1px);
399
+ }
400
+
401
+ /* ===========================
402
+ ROW CAP (maxRows + Show all)
403
+ =========================== */
404
+
405
+ tr.row-capped {
406
+ display: none !important;
407
+ }
408
+
409
+ .d3-table-showmore {
410
+ display: block;
411
+ background: none;
412
+ border: 0;
413
+ padding: var(--space-2) 0;
414
+ margin-top: var(--space-1);
415
+ font-family: var(--font-family);
416
+ font-size: var(--table-header-font-size);
417
+ text-transform: uppercase;
418
+ letter-spacing: var(--letter-spacing-wide);
419
+ color: var(--color-text-muted);
420
+ cursor: pointer;
421
+ text-align: left;
422
+ }
423
+
424
+ .d3-table-showmore:hover {
425
+ color: var(--color-text-primary, var(--color-black));
426
+ }
427
+
428
+ .d3-table-showmore:focus-visible {
429
+ outline: 2px solid var(--chart-primary, #3b82f6);
430
+ outline-offset: 2px;
431
+ }
432
+
433
+ /* ===========================
434
+ FOOTER / TOTAL ROW
435
+ =========================== */
436
+
437
+ tfoot td {
438
+ font-weight: var(--font-weight-bold);
439
+ }
440
+
441
+ /* First summary row carries the heavy total rule; later rows (e.g. a
442
+ per-game average under the totals) separate with the normal border */
443
+ tfoot tr:first-child td {
444
+ border-top: var(--table-total-border);
445
+ }
446
+
447
+ tfoot tr + tr td {
448
+ border-top: var(--table-row-border);
449
+ }
450
+
451
+ /* ===========================
452
+ EXPANDABLE SUB-ROWS
453
+ =========================== */
454
+
455
+ /* Chevron toggle at the start of an expandable parent row's first cell.
456
+ Same drawn-border chevron as sort indicators: points right collapsed,
457
+ rotates to point down when expanded. */
458
+ .row-toggle {
459
+ background: none;
460
+ border: 0;
461
+ padding: 0;
462
+ margin-right: var(--space-2);
463
+ width: var(--table-toggle-width, 14px);
464
+ cursor: pointer;
465
+ color: var(--color-text-muted);
466
+ display: inline-block;
467
+ vertical-align: middle;
468
+ line-height: 0;
469
+ }
470
+
471
+ .row-toggle::after {
472
+ content: '';
473
+ display: inline-block;
474
+ width: var(--sort-chevron-size, 5px);
475
+ height: var(--sort-chevron-size, 5px);
476
+ border-right: var(--sort-chevron-stroke, 1.5px) solid currentColor;
477
+ border-bottom: var(--sort-chevron-stroke, 1.5px) solid currentColor;
478
+ transform: rotate(-45deg);
479
+ transition: transform var(--sort-transition-duration) ease;
480
+ }
481
+
482
+ .row-toggle:hover {
483
+ color: var(--color-text-primary, var(--color-black));
484
+ }
485
+
486
+ .row-toggle:focus-visible {
487
+ outline: 2px solid var(--chart-primary, #3b82f6);
488
+ outline-offset: 2px;
489
+ }
490
+
491
+ tr.expandable-row.expanded .row-toggle::after {
492
+ transform: rotate(45deg);
493
+ }
494
+
495
+ tr.expandable-row td {
496
+ cursor: default;
497
+ }
498
+
499
+ /* Same-width spacer keeps childless parent rows aligned with toggled ones */
500
+ .row-toggle-spacer {
501
+ display: inline-block;
502
+ width: var(--table-toggle-width, 14px);
503
+ margin-right: var(--space-2);
504
+ }
505
+
506
+ /* Sub-rows: indented under the parent, quieter type */
507
+ tr.sub-row td:first-child {
508
+ padding-left: var(--table-subrow-indent, 22px);
509
+ }
510
+
511
+ tr.sub-row td.primary-cell,
512
+ tr.sub-row td.team-cell {
513
+ font-size: var(--table-font-size);
514
+ font-weight: var(--font-weight-semibold);
515
+ }
516
+
517
+ /* ===========================
518
+ IMAGE CELLS (logos / flags / headshots)
519
+ =========================== */
520
+
521
+ .cell-with-image {
522
+ display: flex;
523
+ align-items: center;
524
+ gap: var(--table-image-gap);
525
+ }
526
+
527
+ td.align-right .cell-with-image {
528
+ justify-content: flex-end;
529
+ }
530
+
531
+ td.align-center .cell-with-image {
532
+ justify-content: center;
533
+ }
534
+
535
+ .d3-table-img {
536
+ width: var(--table-image-size);
537
+ height: var(--table-image-size);
538
+ flex-shrink: 0;
539
+ display: block;
540
+ }
541
+
542
+ img.d3-table-img.img-fit-cover { object-fit: cover; }
543
+ img.d3-table-img.img-fit-contain { object-fit: contain; }
544
+
545
+ .d3-table-img.img-circle { border-radius: 50%; }
546
+ .d3-table-img.img-rounded { border-radius: var(--table-image-radius); }
547
+ .d3-table-img.img-square { border-radius: 0; }
548
+
549
+ /* Rows without an image keep an invisible slot so text stays aligned */
550
+ .d3-table-img-placeholder {
551
+ visibility: hidden;
552
+ }
553
+
554
+ /* ===========================
555
+ AUTO WIDTH UTILITY
556
+ =========================== */
557
+
558
+ .auto-width {
559
+ white-space: nowrap;
560
+ /* width handled by JavaScript */
561
+ }
562
+
563
+ /* Opt-in wrapping for long text columns (column config `wrap: true`).
564
+ Beats the auto-width nowrap; pair with a column maxWidth (applied as an
565
+ inline style) to control where the wrap engages. Data cells only —
566
+ headers keep their single-line layout. */
567
+ td.wrap-cell,
568
+ td.auto-width.wrap-cell {
569
+ white-space: normal;
570
+ overflow-wrap: break-word;
571
+ line-height: var(--table-wrap-line-height, 1.35);
572
+ }
573
+
574
+ /* ===========================
575
+ BAR CHART CELLS
576
+ =========================== */
577
+
578
+ td.bar-cell {
579
+ position: relative;
580
+ min-width: var(--bar-column-min-width);
581
+ overflow: visible;
582
+ padding: var(--table-cell-padding-y) var(--table-cell-padding-x);
583
+ }
584
+
585
+ th.bar-cell {
586
+ text-align: center;
587
+ padding: var(--table-header-padding-y) var(--table-cell-padding-x) var(--table-header-padding-bottom) var(--table-cell-padding-x);
588
+ }
589
+
590
+ .bar-background {
591
+ position: absolute;
592
+ top: var(--bar-margin-y);
593
+ left: var(--table-cell-padding-x);
594
+ bottom: var(--bar-margin-y);
595
+ height: auto;
596
+ opacity: var(--bar-opacity);
597
+ z-index: 0;
598
+ border-radius: var(--table-bar-border-radius, 2px);
599
+ }
600
+
601
+ /* Positioning context spanning the cell's padded interior: bar widths and
602
+ label positions are percentages OF THE TRACK, so they can never bleed
603
+ past the cell padding (the old cell-relative percentages overflowed
604
+ tight columns by the padding width). */
605
+ .bar-track {
606
+ position: absolute;
607
+ top: var(--bar-margin-y);
608
+ bottom: var(--bar-margin-y);
609
+ left: var(--table-cell-padding-x);
610
+ right: var(--table-cell-padding-x);
611
+ }
612
+
613
+ .bar-track .bar-background {
614
+ top: 0;
615
+ bottom: 0;
616
+ left: 0;
617
+ }
618
+
619
+ .bar-track .bar-value {
620
+ margin-left: 0;
621
+ }
622
+
623
+ /* Scoped to table cells: the ChartFactory bar builders also emit an SVG
624
+ <text class="bar-value"> for their end-of-bar labels, and an unscoped rule
625
+ here (position/top/transform) leaks onto those and floats them off their
626
+ bars. Table bar-values are always a <div> inside a <td>, so `td` scoping
627
+ fixes the collision without changing table appearance. */
628
+ td .bar-value {
629
+ position: absolute;
630
+ z-index: 1;
631
+ font-weight: var(--font-weight-semibold);
632
+ text-align: right;
633
+ color: var(--color-black);
634
+ padding: 0 var(--bar-text-padding);
635
+ white-space: nowrap;
636
+ top: 50%;
637
+ transform: translateY(-50%);
638
+ margin-left: var(--table-cell-padding-x);
639
+ }
640
+
641
+ /* ===========================
642
+ HEATMAP CELLS
643
+ =========================== */
644
+
645
+ .heatmap-cell-padded {
646
+ position: relative;
647
+ }
648
+
649
+ .heatmap-cell-padded.align-right {
650
+ text-align: right;
651
+ }
652
+
653
+ .heatmap-box {
654
+ display: inline-flex;
655
+ align-items: center;
656
+ justify-content: center;
657
+ padding: 0 var(--heatmap-box-padding);
658
+ border-radius: var(--heatmap-box-border-radius);
659
+ font-weight: var(--font-weight-semibold);
660
+ text-align: center;
661
+ min-width: var(--heatmap-box-min-width);
662
+ height: var(--heatmap-box-height);
663
+ box-sizing: border-box;
664
+ }
665
+
666
+ .heatmap-value {
667
+ position: relative;
668
+ z-index: 1;
669
+ }
670
+
671
+ /* Missing value in a heatmap column: quiet em dash, no fill */
672
+ .heatmap-box-empty {
673
+ color: var(--color-text-faint);
674
+ font-weight: var(--font-weight-normal);
675
+ }
676
+
677
+ /* ===========================
678
+ HIGHLIGHTING
679
+ =========================== */
680
+
681
+ .highlight-column {
682
+ background-color: var(--color-bg-highlight);
683
+ }
684
+
685
+ .highlight-column .bar-background {
686
+ left: var(--space-3);
687
+ }
688
+
689
+ .highlight-column .bar-value {
690
+ margin-left: var(--space-3);
691
+ }
692
+
693
+ .highlight-header {
694
+ background-color: var(--color-bg-highlight);
695
+ }
696
+
697
+ .highlight-row {
698
+ background-color: var(--color-bg-highlight);
699
+ }
700
+
701
+ .highlight-row:hover {
702
+ background-color: var(--color-bg-highlight);
703
+ }
704
+
705
+ /* ===========================
706
+ SORTING
707
+ =========================== */
708
+
709
+ .sortable-header {
710
+ cursor: pointer;
711
+ position: relative;
712
+ user-select: none;
713
+ transition: background-color var(--sort-transition-duration) ease;
714
+ }
715
+
716
+ /* Sortability is signaled ambiently: pointer cursor + hover/focus fill.
717
+ Idle sortable columns show no glyph. */
718
+ .sortable-header:hover,
719
+ .sortable-header:focus-within {
720
+ background-color: var(--sort-hover-bg);
721
+ }
722
+
723
+ /* The header's click/keyboard surface — a real button for aria-sort,
724
+ Tab focus, and Enter/Space activation. Styled to be invisible. */
725
+ .sort-button {
726
+ background: none;
727
+ border: 0;
728
+ padding: 0;
729
+ margin: 0;
730
+ font: inherit;
731
+ color: inherit;
732
+ letter-spacing: inherit;
733
+ text-transform: inherit;
734
+ text-align: inherit;
735
+ cursor: pointer;
736
+ display: block;
737
+ width: 100%;
738
+ }
739
+
740
+ .sort-button:focus-visible {
741
+ outline: 2px solid var(--chart-primary, #3b82f6);
742
+ outline-offset: -2px;
743
+ }
744
+
745
+ /* Glue wrappers keep the indicator on the same line as the adjacent
746
+ word — the chevron can never wrap onto a line by itself. */
747
+ .sort-glue,
748
+ .sort-glue-lead {
749
+ white-space: nowrap;
750
+ }
751
+
752
+ .sort-indicator {
753
+ display: inline-block;
754
+ color: var(--sort-indicator-color);
755
+ opacity: var(--sort-indicator-opacity, 0.5);
756
+ }
757
+
758
+ .sort-indicator.inactive {
759
+ display: none;
760
+ }
761
+
762
+ .sort-glue .sort-indicator.active {
763
+ margin-left: var(--space-1);
764
+ }
765
+
766
+ .sort-glue-lead .sort-indicator.active {
767
+ margin-right: var(--space-1);
768
+ }
769
+
770
+ /* CSS-drawn chevron: the same bordered box for both directions, rotated
771
+ about its center — size, stroke, and baseline are exactly consistent.
772
+ The rotation leaves the visible strokes entirely on one side of the
773
+ box center (below it for ▾, above it for ▴), so each direction gets a
774
+ compensating translate of size × √2/4 that puts the glyph's visual
775
+ center exactly on the box center — up and down share one center point. */
776
+ .sort-indicator.asc::after,
777
+ .sort-indicator.desc::after {
778
+ content: '';
779
+ display: inline-block;
780
+ width: var(--sort-chevron-size, 5px);
781
+ height: var(--sort-chevron-size, 5px);
782
+ border-right: var(--sort-chevron-stroke, 1.5px) solid currentColor;
783
+ border-bottom: var(--sort-chevron-stroke, 1.5px) solid currentColor;
784
+ vertical-align: 1px;
785
+ }
786
+
787
+ .sort-indicator.desc::after {
788
+ transform: translateY(calc(var(--sort-chevron-size, 5px) * -0.354)) rotate(45deg);
789
+ }
790
+
791
+ .sort-indicator.asc::after {
792
+ transform: translateY(calc(var(--sort-chevron-size, 5px) * 0.354)) rotate(225deg);
793
+ }
794
+
795
+ /* Sorted column highlighting — quiet neutral wash over the full column,
796
+ header cell included, applied when the user sorts manually. */
797
+ .sorted-column {
798
+ background-color: var(--table-sorted-column-bg, rgba(0, 0, 0, 0.03));
799
+ }
800
+
801
+ /* A first column is flush-left by default; when it carries a column
802
+ wash (sorted tint, permanent highlight) the text would hug the
803
+ shaded edge, so those cells get the small near-edge padding. Row-
804
+ level shading (hover, highlight-row) is deliberately excluded —
805
+ per-row padding would misalign first-column text across rows. */
806
+ th:first-child.sorted-column,
807
+ td:first-child.sorted-column,
808
+ th:first-child.highlight-header,
809
+ td:first-child.highlight-column {
810
+ padding-left: var(--table-cell-padding-near);
811
+ }
812
+
813
+ .sorted-column:hover {
814
+ background-color: var(--table-sorted-column-bg-hover, rgba(0, 0, 0, 0.05));
815
+ }
816
+
817
+ th.sorted-column {
818
+ color: var(--color-text-primary, var(--color-black));
819
+ }
820
+
821
+ /* ===========================
822
+ TABLE TOOLTIPS
823
+ =========================== */
824
+
825
+ .d3-table-tooltip {
826
+ position: absolute;
827
+ background: var(--color-bg-container);
828
+ border: 1px solid var(--color-border-tooltip);
829
+ border-radius: var(--tooltip-border-radius);
830
+ padding: var(--tooltip-padding);
831
+ box-shadow: var(--shadow-tooltip);
832
+ z-index: 1000;
833
+ pointer-events: none;
834
+ font-family: var(--font-family);
835
+ font-size: var(--font-size-md);
836
+ min-width: var(--tooltip-min-width);
837
+ overflow: hidden;
838
+ }
839
+
840
+ .d3-table-tooltip-chart {
841
+ display: flex;
842
+ align-items: flex-end;
843
+ gap: var(--tooltip-bar-gap);
844
+ height: var(--tooltip-chart-height);
845
+ position: relative;
846
+ box-sizing: border-box;
847
+ }
848
+
849
+ .d3-table-tooltip-bar {
850
+ flex: 1;
851
+ background-color: var(--color-text-muted);
852
+ border-radius: var(--tooltip-bar-border-radius);
853
+ min-width: var(--tooltip-bar-min-width);
854
+ position: relative;
855
+ max-height: var(--tooltip-chart-height);
856
+ }
857
+
858
+ .d3-table-tooltip-bar-label {
859
+ position: absolute;
860
+ bottom: calc(-1 * var(--tooltip-label-offset-y));
861
+ left: 50%;
862
+ transform: translateX(-50%);
863
+ font-size: var(--font-size-xs);
864
+ color: var(--color-text-muted);
865
+ white-space: nowrap;
866
+ }
867
+
868
+ .d3-table-tooltip-bar-value {
869
+ position: absolute;
870
+ top: calc(-1 * var(--tooltip-value-offset-y));
871
+ left: 50%;
872
+ transform: translateX(-50%);
873
+ font-size: var(--font-size-sm);
874
+ font-weight: var(--font-weight-semibold);
875
+ color: var(--color-black);
876
+ white-space: nowrap;
877
+ }
878
+
879
+ /* ===========================
880
+ RESPONSIVE
881
+ =========================== */
882
+
883
+ @media (max-width: 768px) {
884
+ .d3-table-container {
885
+ padding: var(--container-padding-mobile);
886
+ }
887
+
888
+ .d3-table-title {
889
+ font-size: var(--title-font-size-mobile);
890
+ }
891
+
892
+ table {
893
+ font-size: var(--table-font-size-mobile);
894
+ }
895
+
896
+ th {
897
+ font-size: var(--table-header-font-size-mobile);
898
+ }
899
+ }