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,2368 @@
1
+ /* chart-factory — generated CSS barrel (tokens + base + table). Do not edit. */
2
+
3
+ /* ==== src/core/tokens.css ==== */
4
+ /**
5
+ * Chart Factory - Shared Design Tokens
6
+ *
7
+ * These CSS custom properties define the visual foundation for all D3 components.
8
+ * Override these in your own stylesheet to customize the appearance.
9
+ *
10
+ * Usage:
11
+ * @import 'chart-factory/core/tokens.css';
12
+ *
13
+ * :root {
14
+ * --font-family: 'Your Font', sans-serif;
15
+ * --color-text-primary: #222;
16
+ * }
17
+ */
18
+
19
+ :root {
20
+ /* ===========================
21
+ SPACING
22
+ =========================== */
23
+ --space-0: 0;
24
+ --space-1: 4px;
25
+ --space-2: 8px;
26
+ --space-3: 12px;
27
+ --space-4: 15px;
28
+ --space-5: 20px;
29
+ --space-6: 25px;
30
+ --space-7: 35px;
31
+ --space-8: 40px;
32
+
33
+ /* ===========================
34
+ TYPOGRAPHY
35
+ =========================== */
36
+ /* Default: System font stack */
37
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
38
+ /* Alternative: Open Sans (uncomment and add Google Font import to use)
39
+ * Add to <head>: <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
40
+ * --font-family: 'Open Sans', sans-serif;
41
+ */
42
+
43
+ /* Font Sizes */
44
+ --font-size-xs: 9px;
45
+ --font-size-sm: 10px;
46
+ --font-size-base: 11px;
47
+ --font-size-md: 12px;
48
+ --font-size-lg: 13px;
49
+ --font-size-xl: 14px;
50
+ --font-size-title: 28px;
51
+ --font-size-subtitle: 13px;
52
+ /* Chart-shell text hierarchy (decided from live mockups 2026-07-18):
53
+ card tier 15/11 (ratio 1.36), featured tier 20/14 (ratio 1.43).
54
+ Title size tracks reading context, not chart size. */
55
+ --shell-title-size: 15px;
56
+ --shell-subtitle-size: var(--font-size-base);
57
+ --shell-featured-title-size: 20px;
58
+ --shell-featured-subtitle-size: 14px;
59
+
60
+ /* Font Weights */
61
+ --font-weight-normal: 400;
62
+ --font-weight-semibold: 600;
63
+ --font-weight-bold: 700;
64
+
65
+ /* Letter Spacing */
66
+ --letter-spacing-tight: -0.3px;
67
+ --letter-spacing-wide: 0.8px;
68
+ --letter-spacing-wider: 1px;
69
+
70
+ /* Line Height */
71
+ --line-height-normal: 1.6;
72
+
73
+ /* ===========================
74
+ COLORS
75
+ =========================== */
76
+
77
+ /* Text Colors */
78
+ --color-black: #000;
79
+ --color-text-primary: #333;
80
+ --color-text-muted: #666;
81
+ --color-text-subtle: #767676;
82
+ --color-text-faint: #999;
83
+ --color-text-disabled: #aaa;
84
+
85
+ /* Background Colors */
86
+ --color-bg-body: #f8f8f8;
87
+ --color-bg-container: white;
88
+ --color-bg-hover: #fafafa;
89
+ --color-bg-highlight: #f0f0f0;
90
+
91
+ /* Border Colors */
92
+ --color-border-header: #000;
93
+ --color-border-row: #f0f0f0;
94
+ --color-border-light: #e5e5e5; /* light axis/grid strokes */
95
+ --color-border-footer: #e8e8e8;
96
+ --color-border-tooltip: #ddd;
97
+
98
+ /* Shadows */
99
+ --shadow-container: 0 1px 3px rgba(0, 0, 0, 0.08);
100
+ --shadow-tooltip: 0 2px 8px rgba(0, 0, 0, 0.15);
101
+
102
+ /* ===========================
103
+ ANIMATIONS
104
+ =========================== */
105
+ --animation-duration-fast: 200ms;
106
+ --animation-duration-normal: 500ms;
107
+ --animation-duration-slow: 1000ms;
108
+ --animation-stagger-delay: 50ms;
109
+
110
+ /* ===========================
111
+ TOOLTIPS
112
+ =========================== */
113
+ --tooltip-z-index: 9999; /* above typical host-page modals/sticky chrome */
114
+ --tooltip-font-size: 12px; /* tooltip body (independent of chart axis text) */
115
+ --tooltip-title-font-size: 15px; /* ~1.25x body for clear title dominance */
116
+ --tooltip-row-gap: 4px; /* vertical rhythm between shell rows */
117
+ --tooltip-padding: 10px 13px;
118
+ --tooltip-border-radius: 6px;
119
+ --tooltip-min-width: 130px;
120
+ --tooltip-chart-height: 50px;
121
+ --tooltip-bar-gap: var(--space-1);
122
+ --tooltip-bar-min-width: var(--space-2);
123
+ --tooltip-bar-border-radius: 2px;
124
+ --tooltip-label-offset-y: 18px;
125
+ --tooltip-value-offset-y: 18px;
126
+
127
+ /* ===========================
128
+ CHART STORY (story: config — narrative summary module under the chart)
129
+ =========================== */
130
+ --story-font-size: var(--font-size-lg); /* body text of all three variants */
131
+ --story-line-height: 1.6;
132
+ /* Accent drives the panel tint/border, the bold lead-in label, and band
133
+ <b>/<strong> emphasis. The per-chart `accent` option overrides it via an
134
+ inline custom property on the module. */
135
+ --story-accent: var(--chart-primary);
136
+ /* Accent share in the panel wash / hairline (color-mix percentages) */
137
+ --story-panel-bg-alpha: 7%;
138
+ --story-panel-border-alpha: 25%;
139
+ /* Band variant: full-width dark lede strip */
140
+ --story-band-bg: #1f2937;
141
+ --story-band-text: #e5e7eb;
142
+ --story-band-label: #9ca3af; /* overline label tint on the band */
143
+
144
+ /* ===========================
145
+ RESPONSIVE
146
+ =========================== */
147
+ --container-padding: var(--space-8); /* .d3-container inner padding — the one knob for card breathing room */
148
+ --container-padding-mobile: var(--space-6) var(--space-5);
149
+ --title-font-size-mobile: 22px;
150
+
151
+ /* ===========================
152
+ CHART SHARED
153
+ =========================== */
154
+
155
+ /* Primary chart color — single-series default (line/area/bar/scatter,
156
+ forecast/actual lines, series-3). Brand blue, kept distinct from the Set2
157
+ categorical palette used for multi-series charts. */
158
+ --chart-primary: #3b82f6;
159
+ --chart-primary-light: rgba(59, 130, 246, 0.3);
160
+ --chart-primary-faint: rgba(59, 130, 246, 0.1);
161
+
162
+ /* Secondary chart colors */
163
+ --chart-secondary: #22c55e; /* line palette slot 2 (getLineColors) */
164
+ --chart-tertiary: #f97316; /* line palette slot 3 (getLineColors) */
165
+ --chart-green: #22c55e;
166
+ --chart-lime: #84cc16; /* mid-tier threshold color (progress bars) */
167
+ --chart-orange: #f97316;
168
+ --chart-purple: #a855f7;
169
+ --chart-purple-light: rgba(168, 85, 247, 0.4);
170
+ --chart-purple-faint: rgba(168, 85, 247, 0.05);
171
+ --chart-red: #ef4444;
172
+
173
+ /* Multi-series palette (for YoY, comparisons) */
174
+ --chart-series-1: #94a3b8;
175
+ --chart-series-2: #64748b;
176
+ --chart-series-3: var(--chart-primary);
177
+
178
+ /* Semantic chart colors */
179
+ --chart-trend-up: var(--chart-green);
180
+ --chart-trend-down: var(--chart-red);
181
+ --chart-anomaly: var(--chart-red);
182
+ --chart-forecast: var(--chart-primary);
183
+ --chart-actual: var(--chart-primary);
184
+ --chart-budget: var(--chart-green);
185
+
186
+ /* Neutral/gray for cumulative/baseline lines */
187
+ --chart-neutral: #6b7280;
188
+ --chart-neutral-light: rgba(107, 114, 128, 0.4);
189
+ --chart-neutral-faint: rgba(107, 114, 128, 0.05);
190
+
191
+ /* Axes */
192
+ --axis-color: var(--color-text-muted);
193
+ --axis-font-size: var(--font-size-xs);
194
+ --axis-label-font-size: var(--font-size-sm);
195
+ --axis-tick-size: 5px;
196
+ --axis-tick-padding: 3px;
197
+
198
+ /* Grid */
199
+ --grid-color: var(--color-border-row);
200
+ --grid-opacity: 0.5;
201
+ --grid-stroke-opacity: 0.5;
202
+
203
+ /* Hover states */
204
+ --hover-stroke-color: var(--color-text-primary);
205
+ --hover-stroke-width: 1;
206
+ --hover-dash-pattern: 3, 3;
207
+ --link-dot-radius: 3.5; /* snapped dot size on linked-crosshair (linkGroup) charts */
208
+ --brush-selection-opacity: 0.08; /* fill opacity of the brush selection window */
209
+ --brush-veil-opacity: 0.65; /* opacity of the outside-selection veil on linked charts */
210
+
211
+ /* Lines */
212
+ --line-stroke-width: 2;
213
+ --line-stroke-width-thin: 1;
214
+ --line-stroke-width-hover: 3;
215
+ --line-opacity: 1;
216
+ --line-opacity-muted: 0.3;
217
+ --line-dot-stroke-width: 2;
218
+ --line-transition-duration: 150;
219
+
220
+ /* Areas */
221
+ --area-opacity: 0.15;
222
+ --area-opacity-light: 0.1;
223
+ --area-gradient-start: 0.3;
224
+ --area-gradient-end: 0.02;
225
+
226
+ /* Points */
227
+ --point-radius: 4px;
228
+ --point-radius-small: 3px;
229
+ --point-radius-hover: 6px;
230
+ --point-stroke-width: 2px;
231
+
232
+ /* Confidence bands */
233
+ --band-opacity: 0.15;
234
+ --band-border-opacity: 0.3;
235
+
236
+ /* Anomaly indicators */
237
+ --anomaly-pulse-radius: 4;
238
+ --anomaly-dot-radius: 3;
239
+
240
+ /* Legend */
241
+ --legend-swatch-size: 12px;
242
+ --legend-line-width: 24px;
243
+ --legend-line-height: 2px;
244
+ --legend-gap: var(--space-4);
245
+ --legend-font-size: var(--font-size-sm);
246
+ /* Interactive legend: opacity of non-highlighted series when one is
247
+ hovered/isolated (chosen treatment: dim others, highlighted unchanged). */
248
+ --legend-dim-opacity: 0.18;
249
+
250
+ /* Trend indicators (legacy aliases) */
251
+ --trend-up-color: var(--chart-trend-up);
252
+ --trend-down-color: var(--chart-trend-down);
253
+ --trend-neutral-color: var(--color-text-muted);
254
+
255
+ /* Animation */
256
+ --chart-animation-duration: 2s;
257
+ --chart-pulse-min-opacity: 0;
258
+ --chart-pulse-max-opacity: 0.6;
259
+
260
+ /* Annotations: fade-in (ms) when an annotation waits for its anchor
261
+ mark's entrance animation before appearing */
262
+ --annotation-fade-duration: 200;
263
+
264
+ /* Event annotations (type: 'event'): tick + label band below the x-axis */
265
+ --annotation-event-band-offset: 22; /* px from plot bottom to the event ticks */
266
+ --annotation-event-tick-height: 8; /* event tick length */
267
+ --annotation-event-font-size: 10px; /* event label size */
268
+ --annotation-event-guide-color: #d1d5db; /* dotted in-plot guide (guide: true) */
269
+
270
+ /* Point/callout annotations: editorial two-line callouts + leader lines */
271
+ --annotation-title-size: 13px; /* titled callout value line (`title`) */
272
+ --annotation-leader-threshold: 14; /* px dot-to-label gap before an auto leader draws */
273
+
274
+ /* ===========================
275
+ SLOPE CHART SPECIFIC
276
+ =========================== */
277
+
278
+ /* Slope line widths */
279
+ --slope-line-width: var(--line-stroke-width);
280
+ --slope-line-width-hover: 3;
281
+
282
+ /* Slope point sizing */
283
+ --slope-point-radius: var(--point-radius);
284
+ --slope-point-radius-hover: 7px;
285
+ --slope-gap-radius: 6px;
286
+
287
+ /* Slope interaction states */
288
+ --slope-dimmed-opacity: 0.2;
289
+ --slope-transition-duration: 0.2s;
290
+
291
+ /* Slope animation */
292
+ --slope-animation-duration: 800ms;
293
+ --slope-stagger-delay: 80ms;
294
+ --slope-fade-duration: 300ms;
295
+
296
+ /* Slope highlight mode */
297
+ --slope-highlight-color: var(--chart-orange);
298
+ --slope-muted-color: #94a3b8;
299
+
300
+ /* Slope label dodge: minimum vertical gap between stacked side labels */
301
+ --slope-label-min-gap: 15;
302
+ /* Two-line right labels (createIndexed) need roughly double the gap */
303
+ --slope-label-min-gap-stacked: 34;
304
+
305
+ /* Indexed slope: hairline at the origin/no-change value */
306
+ --slope-baseline-color: var(--color-border-light);
307
+
308
+ /* Dodged-label leader lines (defaults to the hairline border color) */
309
+ --slope-leader-color: var(--color-border-light);
310
+
311
+ /* Slope band colors (quartiles, from bottom to top) */
312
+ --slope-band-1: #fef2f2;
313
+ --slope-band-2: #fefce8;
314
+ --slope-band-3: #f0fdf4;
315
+ --slope-band-4: #eff6ff;
316
+
317
+ /* ===========================
318
+ AREA CHART SPECIFIC
319
+ =========================== */
320
+
321
+ /* Area fill opacity */
322
+ --area-fill-opacity: 0.35;
323
+ --area-fill-opacity-hover: 0.5;
324
+ --area-fill-opacity-muted: 0.15;
325
+
326
+ /* Area stroke */
327
+ --area-stroke-width: 1.5;
328
+ --area-stroke-opacity: 1;
329
+
330
+ /* Categorical series palette — Set2 (house default, CVD-safe, muted).
331
+ Drives getAreaColors()/getLineColors(): stacked areas, multi-line, grouped/
332
+ stacked bars, donut, dot, scatter categories. Single-series charts use
333
+ --chart-primary, not these. Override any of these to restyle every
334
+ multi-series chart at once. */
335
+ --area-color-1: #66c2a5;
336
+ --area-color-2: #fc8d62;
337
+ --area-color-3: #8da0cb;
338
+ --area-color-4: #e78ac3;
339
+ --area-color-5: #a6d854;
340
+ --area-color-6: #ffd92f;
341
+ --area-color-7: #e5c494;
342
+ --area-color-8: #b3b3b3;
343
+
344
+ /* Area interaction */
345
+ --area-transition-duration: 0.2s;
346
+ --area-dimmed-opacity: 0.2;
347
+
348
+ /* Area animation */
349
+ --area-animation-duration: 1000ms;
350
+ --area-stagger-delay: 100ms;
351
+
352
+ /* Area gradient (for single-series with gradient fill) */
353
+ --area-gradient-opacity-top: 0.3;
354
+ --area-gradient-opacity-bottom: 0.02;
355
+
356
+ /* Smoothing curve tension (0 = linear, 1 = very smooth) */
357
+ --area-curve-tension: 0.4;
358
+
359
+ /* ===========================
360
+ STREAMGRAPH SPECIFIC
361
+ =========================== */
362
+
363
+ /* Stream layer fill */
364
+ --stream-fill-opacity: 1;
365
+ --stream-stroke-color: #fff;
366
+ --stream-stroke-width: 1;
367
+
368
+ /* Stream interaction */
369
+ --stream-dimmed-opacity: 0.25;
370
+
371
+ /* Stream labels */
372
+ --stream-label-size: 11;
373
+
374
+ /* ===========================
375
+ DOT CHART SPECIFIC
376
+ =========================== */
377
+
378
+ /* Dot sizing */
379
+ --dot-radius: 5px;
380
+ --dot-radius-hover: 7px;
381
+
382
+ /* Connector line (dumbbell) */
383
+ --dot-connector-width: 2.5;
384
+ --dot-connector-color: var(--color-text-disabled);
385
+
386
+ /* Row spacing */
387
+ --dot-row-height: 36px;
388
+ --dot-row-padding: 0.3;
389
+
390
+ /* Header positioning */
391
+ --dot-header-offset: 12px;
392
+
393
+ /* ===========================
394
+ COLOR TEMPERATURE PALETTE
395
+ =========================== */
396
+ /* Based on Kelvin color temperature scale
397
+ * Cold (blue) → Warm (amber)
398
+ * Useful for percentile bands, heat maps, diverging scales */
399
+
400
+ --temp-cold-10: #7AB8FF; /* 10000K - deep cold blue */
401
+ --temp-cold-9: #9CCAFF; /* 9000K - cold blue */
402
+ --temp-cold-8: #B8D9FF; /* 8000K - sky blue */
403
+ --temp-cold-7: #D6E8FF; /* 7000K - pale blue */
404
+ --temp-neutral: #F2F7FF; /* 6000K - cool white */
405
+ --temp-warm-5: #FFF1C1; /* 5000K - neutral daylight */
406
+ --temp-warm-4: #FFD27F; /* 4000K - warm white */
407
+ --temp-warm-3: #FFB347; /* 3000K - tungsten */
408
+ --temp-warm-2: #FF8C1A; /* 2000K - warm orange */
409
+ --temp-warm-1: #FF6A00; /* 1000K - deep amber */
410
+
411
+ /* ===========================
412
+ SCATTER PLOT / TRAJECTORY
413
+ =========================== */
414
+
415
+ /* Trail length (number of time units to show in trail) */
416
+ --scatter-trail-length: 5;
417
+
418
+ /* Trail styling */
419
+ --scatter-trail-width: 3;
420
+ --scatter-trail-width-hover: 4;
421
+ --scatter-trail-opacity: 0.7;
422
+ --scatter-trail-opacity-dimmed: 0.08;
423
+
424
+ /* Trail gradient (fade from start to end) */
425
+ --scatter-trail-gradient-start: 0.1;
426
+ --scatter-trail-gradient-end: 0.9;
427
+
428
+ /* Dot sizing */
429
+ --scatter-dot-radius: 6;
430
+ --scatter-dot-radius-hover: 10;
431
+ --scatter-dot-stroke-width: 2;
432
+
433
+ /* Start dot (trail origin marker) */
434
+ --scatter-start-dot-radius: 4;
435
+ --scatter-start-dot-stroke-width: 1.5;
436
+ --scatter-start-dot-opacity: 0.8;
437
+
438
+ /* Animation timing */
439
+ --scatter-animation-speed: 1000;
440
+
441
+ /* Quadrant chart */
442
+ --quadrant-line-width: 1.5;
443
+ --quadrant-bg-opacity: 0.08;
444
+
445
+ /* Density contour / KDE outlines (dark theme flips these to white-based) */
446
+ --contour-line-color: rgba(0, 0, 0, 0.15);
447
+ --kde-contour-line-color: rgba(0, 0, 0, 0.1);
448
+
449
+ /* ===========================
450
+ VOCABULARY FAMILIES (Release C)
451
+ =========================== */
452
+
453
+ /* Histogram */
454
+ --histogram-bar-gap: 1; /* px gap between bin bars */
455
+ --histogram-overlay-opacity: 0.55; /* overlay-series fill opacity */
456
+
457
+ /* Donut */
458
+ --donut-inner-ratio: 0.62; /* inner radius share; 0 = pie */
459
+ --donut-pad-angle: 0.008; /* radians between slices */
460
+ --donut-corner-radius: 2; /* px slice corner rounding */
461
+
462
+ /* Box plot */
463
+ --box-fill-opacity: 0.35; /* box body fill vs stroke */
464
+ --box-median-width: 2.5; /* median line stroke width */
465
+ --box-outlier-radius: 3; /* px outlier ring radius */
466
+
467
+ /* Calendar heatmap */
468
+ --calendar-cell-radius: 2; /* px cell corner rounding */
469
+ --calendar-month-gap: 3; /* px extra gutter at month starts */
470
+ --calendar-max-cell: 22; /* px step cap for the 7-col vertical layout */
471
+
472
+ /* Sankey */
473
+ --sankey-node-width: 12; /* px node bar width */
474
+ --sankey-node-padding: 12; /* px vertical gap between nodes */
475
+ --sankey-link-opacity: 0.3; /* link ribbon opacity (hover +0.25) */
476
+ --sankey-anim-duration: 900; /* ms each column's links take to draw */
477
+ --sankey-anim-pause: 300; /* ms the flow rests at a node before flowing on */
478
+
479
+ /* ===========================
480
+ BUMP CHART SPECIFIC
481
+ =========================== */
482
+
483
+ /* Layout */
484
+ --bump-margin-top: 30;
485
+ --bump-margin-bottom: 40;
486
+ --bump-label-gap: 10;
487
+ --bump-label-padding: 5;
488
+ --bump-axis-offset: 15;
489
+
490
+ /* Line styling */
491
+ --bump-line-width: 2.5;
492
+ --bump-line-width-hover: 3.5;
493
+
494
+ /* Dot styling */
495
+ --bump-dot-radius: 8;
496
+ --bump-dot-radius-hover: 10;
497
+ --bump-dot-stroke-width: 2;
498
+ --bump-rank-font-size: 10px;
499
+
500
+ /* Interaction states */
501
+ --bump-dimmed-opacity: 0.15;
502
+ --bump-transition-duration: 0.2s;
503
+
504
+ /* Animation */
505
+ --bump-animation-duration: 800;
506
+ --bump-stagger-delay: 100;
507
+ --bump-dot-fade-duration: 300;
508
+
509
+ /* ===========================
510
+ SEQUENTIAL COLOR SCALES (bars/heatmaps — createColorScale)
511
+ =========================== */
512
+ --scale-orange-lo: #fff5eb;
513
+ --scale-orange-hi: #ff6b35;
514
+ --scale-orange-red-hi: #d7191c;
515
+ --scale-blue-lo: #e0f3f8;
516
+ --scale-blue-hi: #4575b4;
517
+ --scale-teal-hi: #1a9988;
518
+ --scale-aqua-1: #a4d7e1;
519
+ --scale-aqua-2: #4ec1d4;
520
+ --scale-aqua-3: #0095b7;
521
+ --scale-aqua-4: #005e8b;
522
+ --scale-gray-lo: #f5f5f5;
523
+ --scale-gray-hi: #333333;
524
+ --scale-purple-1: #f3e5f5;
525
+ --scale-purple-2: #ce93d8;
526
+ --scale-purple-3: #ab47bc;
527
+ --scale-purple-4: #7b1fa2;
528
+ --scale-green-1: #e8f5e9;
529
+ --scale-green-2: #a5d6a7;
530
+ --scale-green-3: #66bb6a;
531
+ --scale-green-4: #2e7d32;
532
+ /* Diagonal band scales (Scatter.createDiagonal) — white center outward */
533
+ --scale-diag-green-1: #ffffff;
534
+ --scale-diag-green-2: #c6f6d5;
535
+ --scale-diag-green-3: #68d391;
536
+ --scale-diag-green-4: #38a169;
537
+ --scale-diag-red-1: #ffffff;
538
+ --scale-diag-red-2: #fed7d7;
539
+ --scale-diag-red-3: #fc8181;
540
+ --scale-diag-red-4: #e53e3e;
541
+
542
+ /* ===========================
543
+ BAR CHART SPECIFIC
544
+ =========================== */
545
+
546
+ /* Bar sizing */
547
+ --bar-height: 24px;
548
+ --bar-height-mini: 18px;
549
+ --bar-gap: 8px;
550
+ --bar-border-radius: 3px;
551
+
552
+ /* Zero baseline on mixed-sign bar charts (tracks the dark theme via the
553
+ underlying text token) */
554
+ --bar-zero-line-color: var(--color-text-primary);
555
+ --bar-zero-line-width: 1.5;
556
+
557
+ /* Gantt */
558
+ --gantt-bar-height: 18px;
559
+ --gantt-bar-gap: 6px;
560
+ --gantt-border-radius: 2px;
561
+
562
+ /* Waterfall connector hairlines (between consecutive bars) */
563
+ --waterfall-connector-color: rgba(0, 0, 0, 0.18);
564
+
565
+ /* Container */
566
+ --container-max-width: 660px;
567
+
568
+ /* Bar track (background) */
569
+ --bar-track-color: var(--color-border-row);
570
+ --bar-track-opacity: 0.5;
571
+
572
+ /* Label positioning */
573
+ --bar-label-gap: 8px;
574
+ --bar-value-padding: 6px;
575
+ --bar-inline-threshold: 0.35;
576
+
577
+ /* Lollipop variant */
578
+ --lollipop-stem-width: 2px;
579
+ --lollipop-dot-radius: 6px;
580
+ --lollipop-dot-radius-hover: 8px;
581
+
582
+ /* Bullet chart */
583
+ --bullet-range-opacity: 0.25;
584
+ --bullet-measure-height: 8px;
585
+ --bullet-marker-width: 3px;
586
+ --bullet-marker-height: 16px;
587
+
588
+ /* Animation */
589
+ --bar-animation-duration: 800ms;
590
+ --bar-stagger-delay: 50ms;
591
+
592
+ /* Row layout */
593
+ --bar-row-height: 32px;
594
+ --bar-category-width: 100px;
595
+
596
+ /* ===========================
597
+ STAT FORMATTING OPTIONS
598
+ =========================== */
599
+ /* Baseball batting average format: .345 instead of 0.345
600
+ * Use formatBaseballAvg() helper or set data-format="baseball-avg"
601
+ *
602
+ * JavaScript helper:
603
+ * function formatBaseballAvg(value) {
604
+ * return value.toFixed(3).replace(/^0\./, '.');
605
+ * }
606
+ */
607
+ }
608
+
609
+ /* ===========================
610
+ INSTAGRAM EXPORT TIER
611
+ ===========================
612
+ Opt-in via <html data-size="instagram">, paired with `size: 'instagram'`
613
+ in chart config (the SIZE_PRESETS geometry). Scales the text, stroke,
614
+ marker, and annotation tokens for 1080px social-feed posters that are
615
+ viewed at phone size — roughly 2.4x the screen-reading defaults. Colors
616
+ are untouched, so it composes with the dark theme. Same runtime contract
617
+ as dark mode: builders resolve tokens at render, so flipping the
618
+ attribute needs a re-render (ChartFactory.rerenderAll()). */
619
+ :root[data-size="instagram"] {
620
+ /* Typography */
621
+ --font-size-xs: 22px;
622
+ --font-size-sm: 25px;
623
+ --font-size-base: 27px;
624
+ --font-size-md: 29px;
625
+ --font-size-lg: 32px;
626
+ --font-size-xl: 35px;
627
+ --font-size-title: 64px;
628
+ --font-size-subtitle: 32px;
629
+ --shell-title-size: 38px;
630
+
631
+ /* Marks */
632
+ --line-stroke-width: 5;
633
+ --area-stroke-width: 4;
634
+ --line-dot-stroke-width: 4;
635
+ --bar-height: 56;
636
+ --bar-gap: 18;
637
+
638
+ /* Annotations */
639
+ --annotation-title-size: 34px;
640
+ --annotation-event-font-size: 24px;
641
+ --annotation-event-band-offset: 48;
642
+ --annotation-event-tick-height: 18;
643
+ --annotation-leader-threshold: 30;
644
+
645
+ /* Slope label dodge gaps track the scaled label text */
646
+ --slope-label-min-gap: 34;
647
+ --slope-label-min-gap-stacked: 72;
648
+
649
+ /* Bump chart marks at poster weight */
650
+ --bump-line-width: 6;
651
+ --bump-line-width-hover: 8;
652
+ --bump-dot-radius: 17;
653
+ --bump-dot-radius-hover: 20;
654
+ --bump-dot-stroke-width: 4;
655
+ --bump-rank-font-size: 20px;
656
+ --bump-label-gap: 16;
657
+ --bump-axis-offset: 26;
658
+ --bump-margin-top: 40;
659
+ --bump-margin-bottom: 60;
660
+ }
661
+
662
+ /* ===========================
663
+ DARK THEME
664
+ ===========================
665
+ Opt-in via <html data-theme="dark">. Only color-bearing tokens are
666
+ overridden; spacing, sizing, and opacity tokens carry over.
667
+
668
+ Builders resolve tokens live at render time (getCSSVar reads
669
+ documentElement), so switching themes at runtime requires a re-render:
670
+ call chart.rerender() on each chart facade (tables: table.update(data)).
671
+ CSS-styled surfaces (table chrome, tooltip shell, containers) restyle
672
+ immediately without one. */
673
+ :root[data-theme="dark"] {
674
+ /* Text — --color-black is the strongest ink, not literal black */
675
+ --color-black: #f4f4f5;
676
+ --color-text-primary: #e4e4e7;
677
+ --color-text-muted: #a1a1aa;
678
+ --color-text-subtle: #8b8b93;
679
+ --color-text-faint: #71717a;
680
+ --color-text-disabled: #52525b;
681
+
682
+ /* Backgrounds */
683
+ --color-bg-body: #101012;
684
+ --color-bg-container: #1b1b1e;
685
+ --color-bg-hover: #232327;
686
+ --color-bg-highlight: #2a2a2f;
687
+
688
+ /* Borders */
689
+ --color-border-header: #e4e4e7;
690
+ --color-border-row: #2a2a2e;
691
+ --color-border-light: #333338;
692
+ --color-border-footer: #2f2f34;
693
+ --color-border-tooltip: #3f3f46;
694
+
695
+ /* Shadows */
696
+ --shadow-container: 0 1px 3px rgba(0, 0, 0, 0.6);
697
+ --shadow-tooltip: 0 4px 14px rgba(0, 0, 0, 0.6);
698
+
699
+ /* Chart palette — brightened for dark surfaces */
700
+ --chart-primary: #60a5fa;
701
+ --chart-primary-light: rgba(96, 165, 250, 0.35);
702
+ --chart-primary-faint: rgba(96, 165, 250, 0.12);
703
+ --chart-secondary: #4ade80;
704
+ --chart-tertiary: #fb923c;
705
+ --chart-green: #4ade80;
706
+ --chart-lime: #a3e635;
707
+ --chart-orange: #fb923c;
708
+ --chart-purple: #c084fc;
709
+ --chart-purple-light: rgba(192, 132, 252, 0.4);
710
+ --chart-purple-faint: rgba(192, 132, 252, 0.08);
711
+ --chart-red: #f87171;
712
+
713
+ /* Multi-series palette — lighter gray is the more prominent one on dark */
714
+ --chart-series-1: #64748b;
715
+ --chart-series-2: #94a3b8;
716
+
717
+ /* Neutral/gray for cumulative/baseline lines */
718
+ --chart-neutral: #9ca3af;
719
+ --chart-neutral-light: rgba(156, 163, 175, 0.4);
720
+ --chart-neutral-faint: rgba(156, 163, 175, 0.08);
721
+
722
+ /* Slope */
723
+ --slope-muted-color: #52525b;
724
+ --slope-band-1: #2c1d1d;
725
+ --slope-band-2: #2b2717;
726
+ --slope-band-3: #16281c;
727
+ --slope-band-4: #17222f;
728
+
729
+ /* Misc structural strokes */
730
+ --stream-stroke-color: #1b1b1e;
731
+ --waterfall-connector-color: #52525b;
732
+ --dot-connector-color: #3f3f46;
733
+ --annotation-event-guide-color: #52525b;
734
+ --contour-line-color: rgba(255, 255, 255, 0.15);
735
+ --kde-contour-line-color: rgba(255, 255, 255, 0.12);
736
+
737
+ /* Categorical series palette — Set2, lifted for dark backgrounds */
738
+ --area-color-1: #74d0b3;
739
+ --area-color-2: #ff9e78;
740
+ --area-color-3: #a2b3dc;
741
+ --area-color-4: #ef9dd0;
742
+ --area-color-5: #b6e268;
743
+ --area-color-6: #ffe45c;
744
+ --area-color-7: #ecd2a8;
745
+ --area-color-8: #c6c6c6;
746
+
747
+ /* Temperature palette — extremes stay bright, middle fades to surface */
748
+ --temp-cold-10: #3f7fd1;
749
+ --temp-cold-9: #38689f;
750
+ --temp-cold-8: #2f5377;
751
+ --temp-cold-7: #263e52;
752
+ --temp-neutral: #26262b;
753
+ --temp-warm-5: #4a3a20;
754
+ --temp-warm-4: #6e4f22;
755
+ --temp-warm-3: #9c6a25;
756
+ --temp-warm-2: #d07f1f;
757
+ --temp-warm-1: #ff8a3d;
758
+
759
+ /* Sequential scales — lo ends re-anchor from near-white to surface-tinted
760
+ darks so low values fade into the background, high values stay vivid */
761
+ --scale-orange-lo: #2e2018;
762
+ --scale-orange-hi: #ff8a5c;
763
+ --scale-orange-red-hi: #e35d5d;
764
+ --scale-blue-lo: #16202c;
765
+ --scale-blue-hi: #6ea8dc;
766
+ --scale-teal-hi: #34b3a0;
767
+ --scale-aqua-1: #1a3a42;
768
+ --scale-aqua-2: #16606e;
769
+ --scale-aqua-3: #1d92ab;
770
+ --scale-aqua-4: #5cc6da;
771
+ --scale-gray-lo: #232326;
772
+ --scale-gray-hi: #d4d4d8;
773
+ --scale-purple-1: #241a2b;
774
+ --scale-purple-2: #4a3357;
775
+ --scale-purple-3: #7b57a0;
776
+ --scale-purple-4: #b58ae0;
777
+ --scale-green-1: #16241a;
778
+ --scale-green-2: #2f5138;
779
+ --scale-green-3: #4f8c58;
780
+ --scale-green-4: #7ac47f;
781
+ --scale-diag-green-1: #1b1b1e;
782
+ --scale-diag-green-2: #1d3524;
783
+ --scale-diag-green-3: #2f6b40;
784
+ --scale-diag-green-4: #4caf6d;
785
+ --scale-diag-red-1: #1b1b1e;
786
+ --scale-diag-red-2: #3a2023;
787
+ --scale-diag-red-3: #7a3b3b;
788
+ --scale-diag-red-4: #c25959;
789
+
790
+ /* Chart story — band lifts off the dark container surface; accent follows
791
+ --chart-primary automatically, and the panel wash re-mixes from it */
792
+ --story-band-bg: #2a2a2f;
793
+ --story-band-text: #e4e4e7;
794
+ --story-band-label: #a1a1aa;
795
+ }
796
+
797
+
798
+ /* ==== src/core/base.css ==== */
799
+ /**
800
+ * Chart Factory - Shared Base Styles
801
+ *
802
+ * Common styles used across all D3 visualization components.
803
+ * Import after tokens.css:
804
+ *
805
+ * @import 'chart-factory/core/tokens.css';
806
+ * @import 'chart-factory/core/base.css';
807
+ */
808
+
809
+ /* ===========================
810
+ CONTAINER
811
+ =========================== */
812
+
813
+ .d3-container {
814
+ display: block;
815
+ width: 100%;
816
+ max-width: var(--container-max-width, 660px);
817
+ box-sizing: border-box;
818
+ background: var(--color-bg-container);
819
+ padding: var(--container-padding, var(--space-8));
820
+ box-shadow: var(--shadow-container);
821
+ text-align: left;
822
+ }
823
+
824
+ /* Charts fill their container; svg viewBox scaling is applied by the factory */
825
+ .d3-container svg,
826
+ .chart-container svg {
827
+ display: block;
828
+ }
829
+
830
+ /* ===========================
831
+ CHART ELEMENT DEFAULTS
832
+ ===========================
833
+ The canonical look for factory-generated SVG elements. Historically these
834
+ rules were copy-pasted into every example page's <style> block, which meant
835
+ package consumers loading only tokens.css + base.css got browser-default
836
+ black axes and gridlines. They belong here. Scoped under `svg` so generic
837
+ class names (.line, .dot) don't leak onto host-page HTML. */
838
+
839
+ svg .line {
840
+ fill: none;
841
+ stroke-width: var(--line-stroke-width);
842
+ }
843
+
844
+ svg .axis-x,
845
+ svg .axis-y {
846
+ font-family: var(--font-family);
847
+ font-size: var(--font-size-sm);
848
+ color: var(--axis-color);
849
+ }
850
+
851
+ svg .axis-x path,
852
+ svg .axis-y path,
853
+ svg .axis-x line,
854
+ svg .axis-y line {
855
+ stroke: var(--color-border-light, #e5e5e5);
856
+ }
857
+
858
+ svg .axis-y .domain {
859
+ display: none;
860
+ }
861
+
862
+ svg .grid line {
863
+ stroke: var(--color-border-light, #e5e5e5);
864
+ stroke-opacity: var(--grid-stroke-opacity);
865
+ }
866
+
867
+ svg .grid path {
868
+ stroke-width: 0;
869
+ }
870
+
871
+ /* Fallback fill only — dots that set their own fill attribute (per-category
872
+ coloring in the Dot family) keep it. */
873
+ svg .dot:not([fill]) {
874
+ fill: var(--chart-primary);
875
+ }
876
+
877
+ /* Legend swatches must never be squished by a tight flex row (would turn a
878
+ circular dot into an oval) — pin their box regardless of available space. */
879
+ .legend-dot,
880
+ .legend-swatch {
881
+ flex: none;
882
+ }
883
+
884
+ /* ===========================
885
+ TITLE / SUBTITLE / FOOTER
886
+ =========================== */
887
+
888
+ .d3-title {
889
+ font-family: var(--font-family);
890
+ font-size: var(--font-size-title);
891
+ font-weight: var(--font-weight-bold);
892
+ margin: 0 0 var(--space-2) 0;
893
+ color: var(--color-black);
894
+ letter-spacing: var(--letter-spacing-tight);
895
+ }
896
+
897
+ .d3-subtitle {
898
+ font-family: var(--font-family);
899
+ font-size: var(--font-size-subtitle);
900
+ color: var(--color-text-subtle);
901
+ margin: 0 0 var(--space-6) 0;
902
+ line-height: var(--line-height-normal);
903
+ font-weight: var(--font-weight-normal);
904
+ }
905
+
906
+ .d3-footer {
907
+ margin-top: 0;
908
+ padding-top: var(--space-4);
909
+ border-top: 1px solid var(--color-border-footer);
910
+ font-family: var(--font-family);
911
+ font-size: var(--font-size-xs);
912
+ color: var(--color-text-faint);
913
+ text-transform: uppercase;
914
+ letter-spacing: var(--letter-spacing-wider);
915
+ font-weight: var(--font-weight-semibold);
916
+ }
917
+
918
+ /* ===========================
919
+ TOOLTIPS (Shared)
920
+ =========================== */
921
+
922
+ /* Chart hover tooltip — the node TooltipManager creates ('.d3-tooltip').
923
+ Positioning (fixed/left/top/opacity/z-index) is managed by JS; this class
924
+ owns the appearance. */
925
+ .d3-tooltip {
926
+ position: fixed;
927
+ background: var(--color-bg-container);
928
+ border: 1px solid var(--color-border-tooltip);
929
+ border-radius: 6px;
930
+ padding: var(--tooltip-padding, 10px 13px);
931
+ box-shadow: var(--shadow-tooltip);
932
+ z-index: var(--tooltip-z-index, 9999);
933
+ pointer-events: none;
934
+ font-family: var(--font-family);
935
+ font-size: var(--tooltip-font-size, 12px);
936
+ line-height: 1.45;
937
+ min-width: var(--tooltip-min-width, 130px);
938
+ max-width: 320px;
939
+ display: flex;
940
+ flex-direction: column;
941
+ gap: var(--tooltip-row-gap, 4px);
942
+ }
943
+
944
+ /* Standard tooltip shell rows (TooltipManager.rows).
945
+ Hierarchy: title > values > labels > notes.
946
+ Alignment discipline: text labels flush LEFT, numeric values flush RIGHT. */
947
+ .d3-tooltip .d3-tooltip-title {
948
+ font-size: var(--tooltip-title-font-size, 15px);
949
+ font-weight: var(--font-weight-semibold);
950
+ color: var(--color-black);
951
+ letter-spacing: var(--letter-spacing-tight);
952
+ }
953
+
954
+ .d3-tooltip .d3-tooltip-label {
955
+ color: var(--color-text-subtle);
956
+ }
957
+
958
+ .d3-tooltip .d3-tooltip-value {
959
+ font-weight: var(--font-weight-semibold);
960
+ color: var(--color-black);
961
+ font-variant-numeric: tabular-nums;
962
+ }
963
+
964
+ .d3-tooltip .d3-tooltip-row {
965
+ display: flex;
966
+ align-items: baseline;
967
+ justify-content: space-between;
968
+ }
969
+
970
+ /* In swatched rows the label is the middle of three flex children —
971
+ grow it so it hugs the swatch on the left instead of centering.
972
+ (No flex gap: the swatch's own margin spaces it from the label; the
973
+ value's margin guarantees the minimum label-to-value separation.) */
974
+ .d3-tooltip .d3-tooltip-row .d3-tooltip-label {
975
+ flex: 1 1 auto;
976
+ text-align: left;
977
+ }
978
+
979
+ .d3-tooltip .d3-tooltip-row .d3-tooltip-value {
980
+ text-align: right;
981
+ margin-left: var(--space-4);
982
+ }
983
+
984
+ .d3-tooltip .d3-tooltip-note {
985
+ font-size: var(--font-size-sm);
986
+ color: var(--color-text-muted);
987
+ }
988
+
989
+ .d3-tooltip .d3-tooltip-footer {
990
+ border-top: 1px solid var(--color-border-row);
991
+ padding-top: var(--tooltip-row-gap, 3px);
992
+ margin-top: 1px;
993
+ }
994
+
995
+ .d3-tooltip .d3-tooltip-bar {
996
+ height: 3px;
997
+ border-radius: 2px;
998
+ margin-top: 2px;
999
+ }
1000
+
1001
+ .d3-tooltip .d3-tooltip-swatch {
1002
+ display: inline-block;
1003
+ width: 8px;
1004
+ height: 8px;
1005
+ border-radius: 50%;
1006
+ margin-right: var(--space-1);
1007
+ flex: none;
1008
+ }
1009
+
1010
+ .d3-tooltip-chart {
1011
+ display: flex;
1012
+ align-items: flex-end;
1013
+ gap: var(--tooltip-bar-gap);
1014
+ height: var(--tooltip-chart-height);
1015
+ position: relative;
1016
+ box-sizing: border-box;
1017
+ }
1018
+
1019
+ .d3-tooltip-bar {
1020
+ flex: 1;
1021
+ background-color: var(--color-text-muted);
1022
+ border-radius: var(--tooltip-bar-border-radius);
1023
+ min-width: var(--tooltip-bar-min-width);
1024
+ position: relative;
1025
+ max-height: var(--tooltip-chart-height);
1026
+ }
1027
+
1028
+ .d3-tooltip-bar-label {
1029
+ position: absolute;
1030
+ bottom: calc(-1 * var(--tooltip-label-offset-y));
1031
+ left: 50%;
1032
+ transform: translateX(-50%);
1033
+ font-size: var(--font-size-xs);
1034
+ color: var(--color-text-muted);
1035
+ white-space: nowrap;
1036
+ }
1037
+
1038
+ .d3-tooltip-bar-value {
1039
+ position: absolute;
1040
+ top: calc(-1 * var(--tooltip-value-offset-y));
1041
+ left: 50%;
1042
+ transform: translateX(-50%);
1043
+ font-size: var(--font-size-sm);
1044
+ font-weight: var(--font-weight-semibold);
1045
+ color: var(--color-black);
1046
+ white-space: nowrap;
1047
+ }
1048
+
1049
+ /* ===========================
1050
+ RESPONSIVE
1051
+ =========================== */
1052
+
1053
+ @media (max-width: 768px) {
1054
+ .d3-container {
1055
+ padding: var(--container-padding-mobile);
1056
+ }
1057
+
1058
+ .d3-title {
1059
+ font-size: var(--title-font-size-mobile);
1060
+ }
1061
+ }
1062
+
1063
+ /* ===========================
1064
+ CHART SHELL (universal config: title / subtitle / legend / footer)
1065
+ Rendered by the responsive wrapper around the chart svg — one
1066
+ implementation for every builder, token-driven, dark-mode-live.
1067
+ =========================== */
1068
+ .d3-shell-title {
1069
+ font-family: var(--font-family);
1070
+ font-size: var(--shell-title-size, 15px);
1071
+ font-weight: var(--font-weight-bold);
1072
+ color: var(--color-black);
1073
+ letter-spacing: var(--letter-spacing-tight);
1074
+ margin: 0 0 2px 0;
1075
+ }
1076
+
1077
+ .d3-shell-subtitle {
1078
+ font-family: var(--font-family);
1079
+ font-size: var(--shell-subtitle-size, 11px);
1080
+ color: var(--color-text-subtle);
1081
+ margin: 0 0 var(--space-3) 0;
1082
+ }
1083
+
1084
+ .d3-shell--featured.d3-shell-title {
1085
+ font-size: var(--shell-featured-title-size, 20px);
1086
+ margin-bottom: var(--space-1);
1087
+ }
1088
+
1089
+ .d3-shell--featured.d3-shell-subtitle {
1090
+ font-size: var(--shell-featured-subtitle-size, 14px);
1091
+ margin-bottom: var(--space-4);
1092
+ }
1093
+
1094
+ .d3-shell-legend {
1095
+ margin-bottom: var(--space-3);
1096
+ }
1097
+
1098
+ .d3-shell-footer {
1099
+ font-family: var(--font-family);
1100
+ font-size: var(--font-size-xs);
1101
+ color: var(--color-text-faint);
1102
+ text-transform: uppercase;
1103
+ letter-spacing: var(--letter-spacing-wide);
1104
+ margin-top: var(--space-4);
1105
+ padding-top: var(--space-3);
1106
+ border-top: 1px solid var(--color-border-row);
1107
+ display: flex;
1108
+ justify-content: space-between;
1109
+ align-items: center;
1110
+ }
1111
+
1112
+ /* Chart story (story: config): narrative summary module rendered by the
1113
+ wrapper below the svg, above the footer. Variants replicate the picked
1114
+ story-module mockups: panel (accent-tinted takeaway), bubble (card with
1115
+ an up-pointing tail — the chart is speaking), band (full-width dark
1116
+ stat lede). All colors flow through the --story-* tokens; the `accent`
1117
+ option overrides --story-accent inline on the module. */
1118
+ .chart-story {
1119
+ font-family: var(--font-family);
1120
+ font-size: var(--story-font-size);
1121
+ line-height: var(--story-line-height);
1122
+ color: var(--color-text-primary);
1123
+ margin-top: var(--space-3);
1124
+ }
1125
+
1126
+ .chart-story-body { margin: 0; }
1127
+
1128
+ /* Optional uppercase lead-in (bubble and band variants) */
1129
+ .chart-story-overline {
1130
+ display: flex;
1131
+ align-items: center;
1132
+ gap: var(--space-1);
1133
+ font-size: var(--font-size-sm);
1134
+ font-weight: var(--font-weight-bold);
1135
+ letter-spacing: var(--letter-spacing-wider);
1136
+ text-transform: uppercase;
1137
+ color: var(--color-text-subtle);
1138
+ margin-bottom: 3px;
1139
+ }
1140
+
1141
+ .chart-story-icon { display: inline-flex; flex: none; }
1142
+ .chart-story-icon svg { display: block; }
1143
+
1144
+ /* panel — soft accent wash + accent hairline; icon and bold accent label
1145
+ lead the prose inline */
1146
+ .chart-story--panel {
1147
+ display: flex;
1148
+ gap: var(--space-2);
1149
+ align-items: flex-start;
1150
+ background: color-mix(in srgb, var(--story-accent) var(--story-panel-bg-alpha), transparent);
1151
+ border: 1px solid color-mix(in srgb, var(--story-accent) var(--story-panel-border-alpha), transparent);
1152
+ border-radius: 8px;
1153
+ padding: 11px 14px;
1154
+ }
1155
+ .chart-story--panel .chart-story-icon { margin-top: 2px; }
1156
+ .chart-story-label {
1157
+ color: var(--story-accent);
1158
+ font-weight: var(--font-weight-bold);
1159
+ }
1160
+
1161
+ /* bubble — container-background card whose rotated-square tail points up
1162
+ at the chart */
1163
+ .chart-story--bubble {
1164
+ position: relative;
1165
+ margin-top: calc(var(--space-3) + 7px); /* room for the tail */
1166
+ background: var(--color-bg-container);
1167
+ border: 1px solid var(--color-border-light);
1168
+ border-radius: 10px;
1169
+ padding: 11px 15px;
1170
+ box-shadow: var(--shadow-tooltip);
1171
+ }
1172
+ .chart-story--bubble::before {
1173
+ content: "";
1174
+ position: absolute;
1175
+ top: -7px;
1176
+ left: 34px;
1177
+ width: 12px;
1178
+ height: 12px;
1179
+ background: var(--color-bg-container);
1180
+ border-left: 1px solid var(--color-border-light);
1181
+ border-top: 1px solid var(--color-border-light);
1182
+ transform: rotate(45deg);
1183
+ }
1184
+
1185
+ /* band — full-width dark lede strip; spans the CHART CONTAINER (not any
1186
+ outer card the page may have); <b>/<strong> pick up the accent */
1187
+ .chart-story--band {
1188
+ background: var(--story-band-bg);
1189
+ color: var(--story-band-text);
1190
+ border-radius: 6px;
1191
+ padding: 13px 16px 14px;
1192
+ }
1193
+ .chart-story--band .chart-story-overline { color: var(--story-band-label); }
1194
+ .chart-story--band .chart-story-body b,
1195
+ .chart-story--band .chart-story-body strong { color: var(--story-accent); }
1196
+
1197
+ /* ===========================
1198
+ CHART STATES (state: 'loading' | 'empty' | 'error')
1199
+ ===========================
1200
+ The state box takes the chart's exact footprint (height set inline by
1201
+ the wrapper), so the real chart swaps in with zero layout jump. Loading
1202
+ skeletons breathe on a constant 5s cadence with one straight shimmer
1203
+ band (9s cycle: ~3.3s crossing + ~5.7s rest) clipped to the shapes. */
1204
+
1205
+ .d3-state {
1206
+ position: relative;
1207
+ width: 100%;
1208
+ overflow: hidden;
1209
+ display: flex;
1210
+ flex-direction: column;
1211
+ align-items: center;
1212
+ justify-content: center;
1213
+ gap: var(--space-2);
1214
+ text-align: center;
1215
+ }
1216
+
1217
+ .d3-state-msg {
1218
+ font-family: var(--font-family);
1219
+ font-size: var(--font-size-md);
1220
+ color: var(--color-text-muted);
1221
+ }
1222
+
1223
+ .d3-state-glyph {
1224
+ font-size: 22px;
1225
+ line-height: 1;
1226
+ color: var(--color-text-faint);
1227
+ }
1228
+
1229
+ .d3-state--error .d3-state-glyph { color: var(--chart-red); opacity: 0.7; }
1230
+ .d3-state--error .d3-state-msg { color: var(--color-text-primary); }
1231
+
1232
+ /* Loading fallback: three pulsing dots */
1233
+ .d3-state-dots { display: flex; gap: 5px; }
1234
+ .d3-state-dots span {
1235
+ width: 7px;
1236
+ height: 7px;
1237
+ border-radius: 50%;
1238
+ background: var(--color-text-faint);
1239
+ animation: d3-state-pulse 1.2s ease-in-out infinite;
1240
+ }
1241
+ .d3-state-dots span:nth-child(2) { animation-delay: 0.2s; }
1242
+ .d3-state-dots span:nth-child(3) { animation-delay: 0.4s; }
1243
+ @keyframes d3-state-pulse {
1244
+ 0%, 80%, 100% { opacity: 0.25; transform: scale(0.85); }
1245
+ 40% { opacity: 1; transform: scale(1); }
1246
+ }
1247
+
1248
+ /* Loading skeleton: breathing columns. The shimmer is ONE band swept
1249
+ across the whole set — each column's ::after is sized/offset in
1250
+ container coordinates (via the --sw-* custom properties the wrapper
1251
+ sets) so the gradient lines up seamlessly across all columns. */
1252
+ .d3-state--columns { display: block; }
1253
+ .d3-skel-col {
1254
+ position: absolute;
1255
+ bottom: 8%;
1256
+ border-radius: 3px 3px 0 0;
1257
+ background: var(--color-bg-highlight);
1258
+ overflow: hidden;
1259
+ transition: height 5s ease-in-out;
1260
+ }
1261
+ .d3-skel-col::after {
1262
+ content: '';
1263
+ position: absolute;
1264
+ top: 0;
1265
+ bottom: 0;
1266
+ left: var(--sw-left);
1267
+ width: var(--sw-width);
1268
+ background: linear-gradient(90deg, transparent 42%,
1269
+ color-mix(in srgb, var(--color-text-faint) 16%, transparent) 50%,
1270
+ transparent 58%);
1271
+ animation: d3-skel-sweep 9s linear infinite;
1272
+ transform: translateX(-100%);
1273
+ }
1274
+ @keyframes d3-skel-sweep {
1275
+ 0% { transform: translateX(-100%); }
1276
+ 37% { transform: translateX(100%); }
1277
+ 100% { transform: translateX(100%); }
1278
+ }
1279
+
1280
+ /* Rows/trend skeletons are SVG (shimmer clipped to the shapes via SMIL);
1281
+ the svg just needs to fill the state box. */
1282
+ .d3-state--rows svg, .d3-state--trend svg { display: block; width: 100%; height: 100%; }
1283
+
1284
+ /* ============================================
1285
+ Facet helper (ChartFactory.facet) — minimal small-multiples chrome.
1286
+ The grid itself is inline (display:grid on the caller's container);
1287
+ these style the panels the helper creates inside it.
1288
+ ============================================ */
1289
+ .d3-facet-panel { min-width: 0; }
1290
+ .d3-facet-chart { display: block; width: 100%; }
1291
+
1292
+ /* Pulsing anomaly rings (Line.createPulseAnomalies) — library-owned so npm
1293
+ consumers get the pulse without copying example CSS. The builder sets
1294
+ animation: none inline when animate: false; users who prefer reduced
1295
+ motion get static rings automatically. */
1296
+ svg circle.anomaly-pulse {
1297
+ animation: d3-anomaly-pulse var(--chart-animation-duration, 2s) ease-in-out infinite;
1298
+ }
1299
+ @keyframes d3-anomaly-pulse {
1300
+ 0% { opacity: 0.6; r: var(--anomaly-pulse-radius, 4px); }
1301
+ 50% { opacity: 0; r: calc(var(--anomaly-pulse-radius, 4px) * 4); }
1302
+ 100% { opacity: 0.6; r: var(--anomaly-pulse-radius, 4px); }
1303
+ }
1304
+ @media (prefers-reduced-motion: reduce) {
1305
+ svg circle.anomaly-pulse { animation: none; }
1306
+ }
1307
+
1308
+
1309
+ /* ==== src/table/tokens.css ==== */
1310
+ /**
1311
+ * D3Table - Table-Specific Design Tokens
1312
+ *
1313
+ * These tokens extend the core tokens for table-specific styling.
1314
+ * Import after core tokens:
1315
+ *
1316
+ * @import 'chart-factory/core/tokens.css';
1317
+ * @import 'chart-factory/table/tokens.css';
1318
+ */
1319
+
1320
+ :root {
1321
+ /* ===========================
1322
+ TABLE TYPOGRAPHY
1323
+ =========================== */
1324
+ --table-font-size: var(--font-size-lg);
1325
+ --table-header-font-size: var(--font-size-sm);
1326
+
1327
+ /* Cell type font sizes (generic names) */
1328
+ --table-primary-font-size: var(--font-size-xl); /* Bold key column */
1329
+ --table-secondary-font-size: var(--font-size-md); /* Muted index column */
1330
+
1331
+ /* Key-column font family — column-width measurement honors it, so themes
1332
+ can retarget the primary cells' face without breaking layout */
1333
+ --table-primary-font: var(--font-family);
1334
+
1335
+ /* Legacy aliases (for backwards compatibility) */
1336
+ --table-team-font-size: var(--table-primary-font-size);
1337
+ --table-rank-font-size: var(--table-secondary-font-size);
1338
+
1339
+ /* ===========================
1340
+ TABLE LAYOUT
1341
+ =========================== */
1342
+ --table-cell-height: var(--space-7);
1343
+ --table-cell-padding-y: var(--space-1);
1344
+ --table-cell-padding-x: var(--space-5);
1345
+ --table-cell-padding-near: var(--space-2); /* Small padding on aligned edge */
1346
+ --table-header-padding-y: var(--space-2);
1347
+ --table-header-padding-bottom: var(--space-1);
1348
+
1349
+ --table-header-border: 2px solid var(--color-border-header);
1350
+ --table-row-border: 1px solid var(--color-border-row);
1351
+
1352
+ /* ===========================
1353
+ TABLE BAR CHARTS
1354
+ =========================== */
1355
+ --bar-margin-y: 3px;
1356
+ --table-bar-border-radius: 2px; /* renamed from --bar-border-radius, which collided with core tokens */
1357
+ --bar-opacity: 0.85;
1358
+ --bar-max-width-percent: 70; /* Max bar width as percentage of cell */
1359
+ --bar-max-width-percent-highlighted: 65; /* Slightly smaller when highlighted */
1360
+ --bar-label-max-position: 75; /* Max label position as percentage */
1361
+ --bar-label-margin: 2%;
1362
+ --bar-text-padding: var(--space-1);
1363
+ --bar-column-min-width: 130px;
1364
+ --bar-column-max-width: 250px;
1365
+
1366
+ /* Wrapped text cells (column config wrap: true) */
1367
+ --table-wrap-line-height: 1.35;
1368
+ --table-wrap-min-width: 110px; /* fill-layout floor for wrap columns */
1369
+
1370
+ /* Sorted-column highlight (neutral wash on the user-sorted column) */
1371
+ --table-sorted-column-bg: rgba(0, 0, 0, 0.03);
1372
+ --table-sorted-column-bg-hover: rgba(0, 0, 0, 0.05);
1373
+
1374
+ /* ===========================
1375
+ TABLE STICKY / CAP / TOTALS
1376
+ =========================== */
1377
+ --table-sticky-bg: #ffffff; /* opaque backing for stuck header/column cells */
1378
+ --table-sticky-edge-shadow: 6px 0 8px -6px rgba(0, 0, 0, 0.18);
1379
+ --table-total-border: 2px solid var(--color-black);
1380
+
1381
+ /* Column group header rule (inset from cell edges to gap adjacent groups) */
1382
+ --table-group-rule-color: var(--color-border-row);
1383
+ --table-group-rule-width: 1px;
1384
+ --table-group-rule-inset: 10px;
1385
+
1386
+ /* ===========================
1387
+ TABLE EXPANDABLE SUB-ROWS
1388
+ =========================== */
1389
+ --table-toggle-width: 14px;
1390
+ --table-subrow-indent: 22px;
1391
+
1392
+ /* ===========================
1393
+ TABLE IMAGES (logos / flags / headshots)
1394
+ =========================== */
1395
+ --table-image-size: 22px;
1396
+ --table-image-gap: var(--space-2);
1397
+ --table-image-radius: 4px;
1398
+
1399
+ /* ===========================
1400
+ TABLE HEATMAP
1401
+ =========================== */
1402
+ --heatmap-box-padding: var(--space-2);
1403
+ --heatmap-box-height: 25px;
1404
+ --heatmap-box-min-width: 40px;
1405
+ --heatmap-box-border-radius: var(--table-bar-border-radius);
1406
+
1407
+ /* ===========================
1408
+ TABLE COLUMN WIDTHS
1409
+ =========================== */
1410
+ --col-primary-max-width: 250px;
1411
+ --col-secondary-max-width: 40px;
1412
+ --col-default-max-width: 150px;
1413
+ --col-padding-total: 28px;
1414
+ --col-padding-secondary: 10px;
1415
+
1416
+ /* Secondary/rank column constraints */
1417
+ --col-secondary-min: 18px;
1418
+
1419
+ /* Legacy aliases */
1420
+ --col-rank-min: var(--col-secondary-min);
1421
+ --col-rank-max: var(--col-secondary-max-width);
1422
+
1423
+ /* ===========================
1424
+ TABLE SORTING
1425
+ =========================== */
1426
+ --sort-indicator-size: 10px;
1427
+ --sort-indicator-color: var(--color-text-muted);
1428
+ --sort-indicator-active-color: var(--color-black);
1429
+ --sort-hover-bg: var(--color-bg-hover);
1430
+ --sort-transition-duration: var(--animation-duration-fast);
1431
+ /* CSS-drawn chevron on the active sort column (idle columns show nothing) */
1432
+ --sort-chevron-size: 5px;
1433
+ --sort-chevron-stroke: 1.5px;
1434
+ --sort-indicator-opacity: 0.5;
1435
+
1436
+ /* ===========================
1437
+ FILL LAYOUT (readable floor)
1438
+ Per-column minimums summed into the table's min-width;
1439
+ below that floor the wrapper scrolls horizontally.
1440
+ =========================== */
1441
+ --table-min-col-width: 48px;
1442
+ --table-min-col-width-primary: 110px;
1443
+ --table-min-col-width-secondary: 28px;
1444
+
1445
+ /* ===========================
1446
+ TABLE RESPONSIVE
1447
+ =========================== */
1448
+ --table-font-size-mobile: var(--font-size-md);
1449
+ --table-header-font-size-mobile: var(--font-size-xs);
1450
+ }
1451
+
1452
+ /* ===========================
1453
+ DARK THEME
1454
+ ===========================
1455
+ Companion to the core tokens' [data-theme="dark"] block — most table
1456
+ tokens reference core tokens and flip automatically; only the literal
1457
+ light-mode values are overridden here. */
1458
+ :root[data-theme="dark"] {
1459
+ /* White-based wash — the light theme's black-based rgba would vanish */
1460
+ --table-sorted-column-bg: rgba(255, 255, 255, 0.045);
1461
+ --table-sorted-column-bg-hover: rgba(255, 255, 255, 0.07);
1462
+
1463
+ /* Opaque backing for stuck header/column cells */
1464
+ --table-sticky-bg: var(--color-bg-container);
1465
+ --table-sticky-edge-shadow: 6px 0 8px -6px rgba(0, 0, 0, 0.7);
1466
+ }
1467
+
1468
+
1469
+ /* ==== src/table/table.css ==== */
1470
+ /**
1471
+ * D3Table Component Styles
1472
+ *
1473
+ * Import with core and table tokens:
1474
+ *
1475
+ * @import 'chart-factory/core/tokens.css';
1476
+ * @import 'chart-factory/core/base.css';
1477
+ * @import 'chart-factory/table/tokens.css';
1478
+ * @import 'chart-factory/table/table.css';
1479
+ */
1480
+
1481
+ /* ===========================
1482
+ TABLE CONTAINER (extends .d3-container)
1483
+ =========================== */
1484
+
1485
+ .d3-table-container {
1486
+ display: inline-block;
1487
+ background: var(--color-bg-container);
1488
+ padding: var(--space-8);
1489
+ box-shadow: var(--shadow-container);
1490
+ text-align: left;
1491
+ }
1492
+
1493
+ /* Wrapper for table + footer ensures footer matches table width */
1494
+ .d3-table-wrapper {
1495
+ display: inline-flex;
1496
+ flex-direction: column;
1497
+ }
1498
+
1499
+ /* Fill layout: the wrapper must be block-level so the scroll container
1500
+ is bounded by the page container, not shrink-wrapped to the table.
1501
+ (The footer still matches the table — it's 100% width too.)
1502
+ Applied by D3Table.prepareDom(). */
1503
+ .d3-table-wrapper--fill {
1504
+ display: block;
1505
+ width: 100%;
1506
+ }
1507
+
1508
+ .d3-table-title {
1509
+ font-family: var(--font-family);
1510
+ font-size: var(--font-size-title);
1511
+ font-weight: var(--font-weight-bold);
1512
+ margin: 0 0 var(--space-2) 0;
1513
+ color: var(--color-black);
1514
+ letter-spacing: var(--letter-spacing-tight);
1515
+ }
1516
+
1517
+ .d3-table-subtitle {
1518
+ font-family: var(--font-family);
1519
+ font-size: var(--font-size-subtitle);
1520
+ color: var(--color-text-subtle);
1521
+ margin: 0 0 var(--space-6) 0;
1522
+ line-height: var(--line-height-normal);
1523
+ font-weight: var(--font-weight-normal);
1524
+ }
1525
+
1526
+ .d3-table-footer {
1527
+ margin-top: 0;
1528
+ padding-top: var(--space-4);
1529
+ border-top: 1px solid var(--color-border-footer);
1530
+ font-family: var(--font-family);
1531
+ font-size: var(--font-size-xs);
1532
+ color: var(--color-text-faint);
1533
+ text-transform: uppercase;
1534
+ letter-spacing: var(--letter-spacing-wider);
1535
+ font-weight: var(--font-weight-semibold);
1536
+ }
1537
+
1538
+ /* ===========================
1539
+ TABLE BASE
1540
+ =========================== */
1541
+
1542
+ table {
1543
+ width: auto;
1544
+ border-collapse: collapse;
1545
+ font-family: var(--font-family);
1546
+ font-size: var(--table-font-size);
1547
+ margin-top: var(--space-3);
1548
+ margin-bottom: 0;
1549
+ }
1550
+
1551
+ /* ===========================
1552
+ FILL LAYOUT (default)
1553
+ The table fills its container; headers and text cells wrap;
1554
+ numeric cells never wrap. Below the JS-computed min-width
1555
+ floor, .d3-table-scroll takes over with horizontal scroll.
1556
+ =========================== */
1557
+
1558
+ .d3-table-scroll {
1559
+ overflow-x: auto;
1560
+ max-width: 100%;
1561
+ -webkit-overflow-scrolling: touch;
1562
+ }
1563
+
1564
+ table.d3-table--fill {
1565
+ /* Fit content, up to the container, never below the computed floor:
1566
+ columns take their natural widths and leftover card width stays
1567
+ OUTSIDE the table as plain whitespace instead of pooling inside a
1568
+ stretched column. Bar columns then grow floor -> cap from measured
1569
+ container slack (adjustBarColumnFlex). */
1570
+ width: fit-content;
1571
+ max-width: 100%;
1572
+ }
1573
+
1574
+ table.d3-table--fill th {
1575
+ white-space: normal;
1576
+ }
1577
+
1578
+ table.d3-table--fill td {
1579
+ white-space: normal;
1580
+ overflow-wrap: break-word;
1581
+ }
1582
+
1583
+ table.d3-table--fill td.align-right,
1584
+ table.d3-table--fill td.align-center,
1585
+ table.d3-table--fill td.numeric-cell,
1586
+ table.d3-table--fill td.secondary-cell,
1587
+ table.d3-table--fill td.rank-cell {
1588
+ white-space: nowrap;
1589
+ overflow-wrap: normal;
1590
+ }
1591
+
1592
+ /* ===========================
1593
+ HEADER
1594
+ =========================== */
1595
+
1596
+ thead {
1597
+ border-bottom: var(--table-header-border);
1598
+ }
1599
+
1600
+ th {
1601
+ text-align: left;
1602
+ padding: var(--table-header-padding-y) var(--table-cell-padding-x) var(--table-header-padding-bottom) var(--table-cell-padding-near);
1603
+ font-weight: var(--font-weight-normal);
1604
+ font-size: var(--table-header-font-size);
1605
+ text-transform: uppercase;
1606
+ letter-spacing: var(--letter-spacing-wide);
1607
+ color: var(--color-text-muted);
1608
+ white-space: nowrap;
1609
+ vertical-align: baseline;
1610
+ box-sizing: border-box;
1611
+ }
1612
+
1613
+ th:first-child {
1614
+ padding-left: 0;
1615
+ }
1616
+
1617
+ th:last-child {
1618
+ padding-right: 0;
1619
+ }
1620
+
1621
+ th.align-right {
1622
+ text-align: right;
1623
+ padding: var(--table-header-padding-y) var(--table-cell-padding-near) var(--table-header-padding-bottom) var(--table-cell-padding-x);
1624
+ }
1625
+
1626
+ th.align-center {
1627
+ text-align: center;
1628
+ }
1629
+
1630
+ /* ===========================
1631
+ BODY
1632
+ =========================== */
1633
+
1634
+ tbody tr {
1635
+ border-bottom: var(--table-row-border);
1636
+ }
1637
+
1638
+ tbody tr:last-child {
1639
+ border-bottom: none;
1640
+ }
1641
+
1642
+ tbody tr:hover {
1643
+ background-color: var(--color-bg-hover);
1644
+ }
1645
+
1646
+ td {
1647
+ padding: var(--table-cell-padding-y) var(--table-cell-padding-x) var(--table-cell-padding-y) var(--table-cell-padding-near);
1648
+ color: var(--color-text-primary);
1649
+ font-weight: var(--font-weight-normal);
1650
+ height: var(--table-cell-height);
1651
+ box-sizing: border-box;
1652
+ vertical-align: middle;
1653
+ white-space: nowrap;
1654
+ overflow: hidden;
1655
+ text-overflow: ellipsis;
1656
+ }
1657
+
1658
+ td:first-child {
1659
+ padding-left: 0;
1660
+ }
1661
+
1662
+ td:last-child {
1663
+ padding-right: 0;
1664
+ }
1665
+
1666
+ td.align-right {
1667
+ text-align: right;
1668
+ padding: var(--table-cell-padding-y) var(--table-cell-padding-near) var(--table-cell-padding-y) var(--table-cell-padding-x);
1669
+ }
1670
+
1671
+ td.align-center {
1672
+ text-align: center;
1673
+ }
1674
+
1675
+ /* ===========================
1676
+ CELL TYPES
1677
+ =========================== */
1678
+
1679
+ /*
1680
+ * Primary cells - bold, larger font
1681
+ * Use for: names, titles, key identifiers
1682
+ * Generic: primary-cell | Legacy: team-cell
1683
+ */
1684
+ td.primary-cell,
1685
+ td.team-cell {
1686
+ font-family: var(--table-primary-font, inherit);
1687
+ font-weight: var(--font-weight-bold);
1688
+ font-size: var(--table-primary-font-size, var(--table-team-font-size));
1689
+ color: var(--color-black);
1690
+ white-space: normal;
1691
+ }
1692
+
1693
+ /*
1694
+ * Secondary cells - subdued, smaller
1695
+ * Use for: ranks, indices, row numbers
1696
+ * Generic: secondary-cell | Legacy: rank-cell
1697
+ */
1698
+ td.secondary-cell,
1699
+ td.rank-cell {
1700
+ color: var(--color-text-disabled);
1701
+ font-weight: var(--font-weight-semibold);
1702
+ font-size: var(--table-secondary-font-size, var(--table-rank-font-size));
1703
+ }
1704
+
1705
+ th.auto-width.secondary-cell,
1706
+ td.auto-width.secondary-cell,
1707
+ th.auto-width.rank-cell,
1708
+ td.auto-width.rank-cell {
1709
+ /* Compact padding for narrow rank/index columns */
1710
+ padding-left: 0;
1711
+ padding-right: var(--space-2);
1712
+ min-width: var(--col-secondary-min, var(--col-rank-min));
1713
+ max-width: var(--col-secondary-max, var(--col-rank-max));
1714
+ text-align: left;
1715
+ }
1716
+
1717
+
1718
+ /*
1719
+ * Muted cells - de-emphasized text
1720
+ * Use for: supplementary info, metadata
1721
+ */
1722
+ td.muted-cell {
1723
+ color: var(--color-text-disabled);
1724
+ font-size: var(--table-font-size);
1725
+ }
1726
+
1727
+ /*
1728
+ * Numeric cells - right-aligned, tabular figures
1729
+ * Use for: numbers, statistics, amounts
1730
+ */
1731
+ td.numeric-cell {
1732
+ text-align: right;
1733
+ font-variant-numeric: tabular-nums;
1734
+ }
1735
+
1736
+ /* ===========================
1737
+ SUBTITLE CELLS
1738
+ =========================== */
1739
+
1740
+ td.has-subtitle {
1741
+ height: auto;
1742
+ padding-top: var(--space-2);
1743
+ padding-bottom: var(--space-2);
1744
+ }
1745
+
1746
+ .cell-with-subtitle {
1747
+ display: flex;
1748
+ flex-direction: column;
1749
+ gap: 1px;
1750
+ }
1751
+
1752
+ .cell-main-text {
1753
+ font-weight: var(--font-weight-bold);
1754
+ font-size: var(--table-primary-font-size);
1755
+ color: var(--color-black);
1756
+ line-height: 1.2;
1757
+ }
1758
+
1759
+ .cell-subtitle-text {
1760
+ font-weight: var(--font-weight-normal);
1761
+ font-size: var(--font-size-md);
1762
+ color: var(--color-text-disabled);
1763
+ line-height: 1.2;
1764
+ }
1765
+
1766
+ /* ===========================
1767
+ STICKY HEADER / STICKY FIRST COLUMN
1768
+ =========================== */
1769
+
1770
+ /* Sticky header. border-collapse drops borders on stuck cells, so the
1771
+ header's bottom rule is redrawn as an inset shadow (on the last
1772
+ header row only — a group row above it carries no rule). */
1773
+ table.d3-table--sticky-header thead th {
1774
+ position: sticky;
1775
+ top: 0;
1776
+ z-index: 2;
1777
+ background: var(--table-sticky-bg, #fff);
1778
+ }
1779
+
1780
+ table.d3-table--sticky-header thead tr:last-child th {
1781
+ box-shadow: inset 0 -2px 0 var(--color-black, #000);
1782
+ }
1783
+
1784
+ table.d3-table--sticky-header thead {
1785
+ border-bottom: none;
1786
+ }
1787
+
1788
+ /* In-wrapper vertical scrolling (config.maxHeight) */
1789
+ .d3-table-scroll--vertical {
1790
+ overflow-y: auto;
1791
+ }
1792
+
1793
+ /* Floating header: sticky emulation on horizontally-scrolling tables.
1794
+ JS translates the thead with the page scroll (position: sticky can't
1795
+ escape the horizontal scroll container). will-change lives ONLY on the
1796
+ floating state: on the base thead it would create a z-index:auto
1797
+ stacking context that traps the th's z-index, letting tbody bar fills
1798
+ and heatmap chips paint over the page-stuck header. */
1799
+ table.d3-table--sticky-header thead.d3-table-thead--floating {
1800
+ position: relative;
1801
+ z-index: 2;
1802
+ will-change: transform;
1803
+ }
1804
+
1805
+ table.d3-table--sticky-header thead.d3-table-thead--floating th {
1806
+ background: var(--table-sticky-bg, #fff);
1807
+ }
1808
+
1809
+ /* Page-level sticky: overflow-x clip does not create a scroll container,
1810
+ applied by JS only while the table fits its container horizontally */
1811
+ .d3-table-scroll--clip {
1812
+ overflow-x: clip;
1813
+ }
1814
+
1815
+ /* Sticky first column */
1816
+ table.d3-table--sticky-first-col th:first-child,
1817
+ table.d3-table--sticky-first-col td:first-child {
1818
+ position: sticky;
1819
+ left: 0;
1820
+ z-index: 1;
1821
+ background: var(--table-sticky-bg, #fff);
1822
+ }
1823
+
1824
+ table.d3-table--sticky-first-col thead th:first-child {
1825
+ z-index: 3;
1826
+ }
1827
+
1828
+ table.d3-table--sticky-first-col tbody tr:hover td:first-child {
1829
+ background-color: var(--color-bg-hover);
1830
+ }
1831
+
1832
+ /* Edge shadow appears only once the container is actually scrolled */
1833
+ .d3-table-scroll.is-h-scrolled table.d3-table--sticky-first-col th:first-child,
1834
+ .d3-table-scroll.is-h-scrolled table.d3-table--sticky-first-col td:first-child {
1835
+ box-shadow: var(--table-sticky-edge-shadow);
1836
+ }
1837
+
1838
+ /* Both stickies on one table: the corner header cell needs both shadows */
1839
+ table.d3-table--sticky-header.d3-table--sticky-first-col thead tr:last-child th:first-child {
1840
+ box-shadow: inset 0 -2px 0 var(--color-black, #000);
1841
+ }
1842
+
1843
+ .d3-table-scroll.is-h-scrolled table.d3-table--sticky-header.d3-table--sticky-first-col thead tr:last-child th:first-child {
1844
+ box-shadow: inset 0 -2px 0 var(--color-black, #000), var(--table-sticky-edge-shadow);
1845
+ }
1846
+
1847
+ /* ===========================
1848
+ COLUMN GROUP HEADERS
1849
+ =========================== */
1850
+
1851
+ .d3-table-group-row th {
1852
+ padding-bottom: var(--space-1);
1853
+ vertical-align: bottom;
1854
+ }
1855
+
1856
+ .d3-table-group-row th.group-header {
1857
+ text-align: center;
1858
+ color: var(--color-text-muted);
1859
+ /* The rule is a bottom-anchored background (not border-bottom) so it
1860
+ can be inset from the cell edges — adjacent groups get a visible
1861
+ gap that marks where one group ends and the next begins. */
1862
+ background-image: linear-gradient(
1863
+ var(--table-group-rule-color, var(--color-border-row, #e5e7eb)),
1864
+ var(--table-group-rule-color, var(--color-border-row, #e5e7eb)));
1865
+ background-repeat: no-repeat;
1866
+ background-position: bottom center;
1867
+ background-size: calc(100% - 2 * var(--table-group-rule-inset, 10px)) var(--table-group-rule-width, 1px);
1868
+ }
1869
+
1870
+ /* ===========================
1871
+ ROW CAP (maxRows + Show all)
1872
+ =========================== */
1873
+
1874
+ tr.row-capped {
1875
+ display: none !important;
1876
+ }
1877
+
1878
+ .d3-table-showmore {
1879
+ display: block;
1880
+ background: none;
1881
+ border: 0;
1882
+ padding: var(--space-2) 0;
1883
+ margin-top: var(--space-1);
1884
+ font-family: var(--font-family);
1885
+ font-size: var(--table-header-font-size);
1886
+ text-transform: uppercase;
1887
+ letter-spacing: var(--letter-spacing-wide);
1888
+ color: var(--color-text-muted);
1889
+ cursor: pointer;
1890
+ text-align: left;
1891
+ }
1892
+
1893
+ .d3-table-showmore:hover {
1894
+ color: var(--color-text-primary, var(--color-black));
1895
+ }
1896
+
1897
+ .d3-table-showmore:focus-visible {
1898
+ outline: 2px solid var(--chart-primary, #3b82f6);
1899
+ outline-offset: 2px;
1900
+ }
1901
+
1902
+ /* ===========================
1903
+ FOOTER / TOTAL ROW
1904
+ =========================== */
1905
+
1906
+ tfoot td {
1907
+ font-weight: var(--font-weight-bold);
1908
+ }
1909
+
1910
+ /* First summary row carries the heavy total rule; later rows (e.g. a
1911
+ per-game average under the totals) separate with the normal border */
1912
+ tfoot tr:first-child td {
1913
+ border-top: var(--table-total-border);
1914
+ }
1915
+
1916
+ tfoot tr + tr td {
1917
+ border-top: var(--table-row-border);
1918
+ }
1919
+
1920
+ /* ===========================
1921
+ EXPANDABLE SUB-ROWS
1922
+ =========================== */
1923
+
1924
+ /* Chevron toggle at the start of an expandable parent row's first cell.
1925
+ Same drawn-border chevron as sort indicators: points right collapsed,
1926
+ rotates to point down when expanded. */
1927
+ .row-toggle {
1928
+ background: none;
1929
+ border: 0;
1930
+ padding: 0;
1931
+ margin-right: var(--space-2);
1932
+ width: var(--table-toggle-width, 14px);
1933
+ cursor: pointer;
1934
+ color: var(--color-text-muted);
1935
+ display: inline-block;
1936
+ vertical-align: middle;
1937
+ line-height: 0;
1938
+ }
1939
+
1940
+ .row-toggle::after {
1941
+ content: '';
1942
+ display: inline-block;
1943
+ width: var(--sort-chevron-size, 5px);
1944
+ height: var(--sort-chevron-size, 5px);
1945
+ border-right: var(--sort-chevron-stroke, 1.5px) solid currentColor;
1946
+ border-bottom: var(--sort-chevron-stroke, 1.5px) solid currentColor;
1947
+ transform: rotate(-45deg);
1948
+ transition: transform var(--sort-transition-duration) ease;
1949
+ }
1950
+
1951
+ .row-toggle:hover {
1952
+ color: var(--color-text-primary, var(--color-black));
1953
+ }
1954
+
1955
+ .row-toggle:focus-visible {
1956
+ outline: 2px solid var(--chart-primary, #3b82f6);
1957
+ outline-offset: 2px;
1958
+ }
1959
+
1960
+ tr.expandable-row.expanded .row-toggle::after {
1961
+ transform: rotate(45deg);
1962
+ }
1963
+
1964
+ tr.expandable-row td {
1965
+ cursor: default;
1966
+ }
1967
+
1968
+ /* Same-width spacer keeps childless parent rows aligned with toggled ones */
1969
+ .row-toggle-spacer {
1970
+ display: inline-block;
1971
+ width: var(--table-toggle-width, 14px);
1972
+ margin-right: var(--space-2);
1973
+ }
1974
+
1975
+ /* Sub-rows: indented under the parent, quieter type */
1976
+ tr.sub-row td:first-child {
1977
+ padding-left: var(--table-subrow-indent, 22px);
1978
+ }
1979
+
1980
+ tr.sub-row td.primary-cell,
1981
+ tr.sub-row td.team-cell {
1982
+ font-size: var(--table-font-size);
1983
+ font-weight: var(--font-weight-semibold);
1984
+ }
1985
+
1986
+ /* ===========================
1987
+ IMAGE CELLS (logos / flags / headshots)
1988
+ =========================== */
1989
+
1990
+ .cell-with-image {
1991
+ display: flex;
1992
+ align-items: center;
1993
+ gap: var(--table-image-gap);
1994
+ }
1995
+
1996
+ td.align-right .cell-with-image {
1997
+ justify-content: flex-end;
1998
+ }
1999
+
2000
+ td.align-center .cell-with-image {
2001
+ justify-content: center;
2002
+ }
2003
+
2004
+ .d3-table-img {
2005
+ width: var(--table-image-size);
2006
+ height: var(--table-image-size);
2007
+ flex-shrink: 0;
2008
+ display: block;
2009
+ }
2010
+
2011
+ img.d3-table-img.img-fit-cover { object-fit: cover; }
2012
+ img.d3-table-img.img-fit-contain { object-fit: contain; }
2013
+
2014
+ .d3-table-img.img-circle { border-radius: 50%; }
2015
+ .d3-table-img.img-rounded { border-radius: var(--table-image-radius); }
2016
+ .d3-table-img.img-square { border-radius: 0; }
2017
+
2018
+ /* Rows without an image keep an invisible slot so text stays aligned */
2019
+ .d3-table-img-placeholder {
2020
+ visibility: hidden;
2021
+ }
2022
+
2023
+ /* ===========================
2024
+ AUTO WIDTH UTILITY
2025
+ =========================== */
2026
+
2027
+ .auto-width {
2028
+ white-space: nowrap;
2029
+ /* width handled by JavaScript */
2030
+ }
2031
+
2032
+ /* Opt-in wrapping for long text columns (column config `wrap: true`).
2033
+ Beats the auto-width nowrap; pair with a column maxWidth (applied as an
2034
+ inline style) to control where the wrap engages. Data cells only —
2035
+ headers keep their single-line layout. */
2036
+ td.wrap-cell,
2037
+ td.auto-width.wrap-cell {
2038
+ white-space: normal;
2039
+ overflow-wrap: break-word;
2040
+ line-height: var(--table-wrap-line-height, 1.35);
2041
+ }
2042
+
2043
+ /* ===========================
2044
+ BAR CHART CELLS
2045
+ =========================== */
2046
+
2047
+ td.bar-cell {
2048
+ position: relative;
2049
+ min-width: var(--bar-column-min-width);
2050
+ overflow: visible;
2051
+ padding: var(--table-cell-padding-y) var(--table-cell-padding-x);
2052
+ }
2053
+
2054
+ th.bar-cell {
2055
+ text-align: center;
2056
+ padding: var(--table-header-padding-y) var(--table-cell-padding-x) var(--table-header-padding-bottom) var(--table-cell-padding-x);
2057
+ }
2058
+
2059
+ .bar-background {
2060
+ position: absolute;
2061
+ top: var(--bar-margin-y);
2062
+ left: var(--table-cell-padding-x);
2063
+ bottom: var(--bar-margin-y);
2064
+ height: auto;
2065
+ opacity: var(--bar-opacity);
2066
+ z-index: 0;
2067
+ border-radius: var(--table-bar-border-radius, 2px);
2068
+ }
2069
+
2070
+ /* Positioning context spanning the cell's padded interior: bar widths and
2071
+ label positions are percentages OF THE TRACK, so they can never bleed
2072
+ past the cell padding (the old cell-relative percentages overflowed
2073
+ tight columns by the padding width). */
2074
+ .bar-track {
2075
+ position: absolute;
2076
+ top: var(--bar-margin-y);
2077
+ bottom: var(--bar-margin-y);
2078
+ left: var(--table-cell-padding-x);
2079
+ right: var(--table-cell-padding-x);
2080
+ }
2081
+
2082
+ .bar-track .bar-background {
2083
+ top: 0;
2084
+ bottom: 0;
2085
+ left: 0;
2086
+ }
2087
+
2088
+ .bar-track .bar-value {
2089
+ margin-left: 0;
2090
+ }
2091
+
2092
+ /* Scoped to table cells: the ChartFactory bar builders also emit an SVG
2093
+ <text class="bar-value"> for their end-of-bar labels, and an unscoped rule
2094
+ here (position/top/transform) leaks onto those and floats them off their
2095
+ bars. Table bar-values are always a <div> inside a <td>, so `td` scoping
2096
+ fixes the collision without changing table appearance. */
2097
+ td .bar-value {
2098
+ position: absolute;
2099
+ z-index: 1;
2100
+ font-weight: var(--font-weight-semibold);
2101
+ text-align: right;
2102
+ color: var(--color-black);
2103
+ padding: 0 var(--bar-text-padding);
2104
+ white-space: nowrap;
2105
+ top: 50%;
2106
+ transform: translateY(-50%);
2107
+ margin-left: var(--table-cell-padding-x);
2108
+ }
2109
+
2110
+ /* ===========================
2111
+ HEATMAP CELLS
2112
+ =========================== */
2113
+
2114
+ .heatmap-cell-padded {
2115
+ position: relative;
2116
+ }
2117
+
2118
+ .heatmap-cell-padded.align-right {
2119
+ text-align: right;
2120
+ }
2121
+
2122
+ .heatmap-box {
2123
+ display: inline-flex;
2124
+ align-items: center;
2125
+ justify-content: center;
2126
+ padding: 0 var(--heatmap-box-padding);
2127
+ border-radius: var(--heatmap-box-border-radius);
2128
+ font-weight: var(--font-weight-semibold);
2129
+ text-align: center;
2130
+ min-width: var(--heatmap-box-min-width);
2131
+ height: var(--heatmap-box-height);
2132
+ box-sizing: border-box;
2133
+ }
2134
+
2135
+ .heatmap-value {
2136
+ position: relative;
2137
+ z-index: 1;
2138
+ }
2139
+
2140
+ /* Missing value in a heatmap column: quiet em dash, no fill */
2141
+ .heatmap-box-empty {
2142
+ color: var(--color-text-faint);
2143
+ font-weight: var(--font-weight-normal);
2144
+ }
2145
+
2146
+ /* ===========================
2147
+ HIGHLIGHTING
2148
+ =========================== */
2149
+
2150
+ .highlight-column {
2151
+ background-color: var(--color-bg-highlight);
2152
+ }
2153
+
2154
+ .highlight-column .bar-background {
2155
+ left: var(--space-3);
2156
+ }
2157
+
2158
+ .highlight-column .bar-value {
2159
+ margin-left: var(--space-3);
2160
+ }
2161
+
2162
+ .highlight-header {
2163
+ background-color: var(--color-bg-highlight);
2164
+ }
2165
+
2166
+ .highlight-row {
2167
+ background-color: var(--color-bg-highlight);
2168
+ }
2169
+
2170
+ .highlight-row:hover {
2171
+ background-color: var(--color-bg-highlight);
2172
+ }
2173
+
2174
+ /* ===========================
2175
+ SORTING
2176
+ =========================== */
2177
+
2178
+ .sortable-header {
2179
+ cursor: pointer;
2180
+ position: relative;
2181
+ user-select: none;
2182
+ transition: background-color var(--sort-transition-duration) ease;
2183
+ }
2184
+
2185
+ /* Sortability is signaled ambiently: pointer cursor + hover/focus fill.
2186
+ Idle sortable columns show no glyph. */
2187
+ .sortable-header:hover,
2188
+ .sortable-header:focus-within {
2189
+ background-color: var(--sort-hover-bg);
2190
+ }
2191
+
2192
+ /* The header's click/keyboard surface — a real button for aria-sort,
2193
+ Tab focus, and Enter/Space activation. Styled to be invisible. */
2194
+ .sort-button {
2195
+ background: none;
2196
+ border: 0;
2197
+ padding: 0;
2198
+ margin: 0;
2199
+ font: inherit;
2200
+ color: inherit;
2201
+ letter-spacing: inherit;
2202
+ text-transform: inherit;
2203
+ text-align: inherit;
2204
+ cursor: pointer;
2205
+ display: block;
2206
+ width: 100%;
2207
+ }
2208
+
2209
+ .sort-button:focus-visible {
2210
+ outline: 2px solid var(--chart-primary, #3b82f6);
2211
+ outline-offset: -2px;
2212
+ }
2213
+
2214
+ /* Glue wrappers keep the indicator on the same line as the adjacent
2215
+ word — the chevron can never wrap onto a line by itself. */
2216
+ .sort-glue,
2217
+ .sort-glue-lead {
2218
+ white-space: nowrap;
2219
+ }
2220
+
2221
+ .sort-indicator {
2222
+ display: inline-block;
2223
+ color: var(--sort-indicator-color);
2224
+ opacity: var(--sort-indicator-opacity, 0.5);
2225
+ }
2226
+
2227
+ .sort-indicator.inactive {
2228
+ display: none;
2229
+ }
2230
+
2231
+ .sort-glue .sort-indicator.active {
2232
+ margin-left: var(--space-1);
2233
+ }
2234
+
2235
+ .sort-glue-lead .sort-indicator.active {
2236
+ margin-right: var(--space-1);
2237
+ }
2238
+
2239
+ /* CSS-drawn chevron: the same bordered box for both directions, rotated
2240
+ about its center — size, stroke, and baseline are exactly consistent.
2241
+ The rotation leaves the visible strokes entirely on one side of the
2242
+ box center (below it for ▾, above it for ▴), so each direction gets a
2243
+ compensating translate of size × √2/4 that puts the glyph's visual
2244
+ center exactly on the box center — up and down share one center point. */
2245
+ .sort-indicator.asc::after,
2246
+ .sort-indicator.desc::after {
2247
+ content: '';
2248
+ display: inline-block;
2249
+ width: var(--sort-chevron-size, 5px);
2250
+ height: var(--sort-chevron-size, 5px);
2251
+ border-right: var(--sort-chevron-stroke, 1.5px) solid currentColor;
2252
+ border-bottom: var(--sort-chevron-stroke, 1.5px) solid currentColor;
2253
+ vertical-align: 1px;
2254
+ }
2255
+
2256
+ .sort-indicator.desc::after {
2257
+ transform: translateY(calc(var(--sort-chevron-size, 5px) * -0.354)) rotate(45deg);
2258
+ }
2259
+
2260
+ .sort-indicator.asc::after {
2261
+ transform: translateY(calc(var(--sort-chevron-size, 5px) * 0.354)) rotate(225deg);
2262
+ }
2263
+
2264
+ /* Sorted column highlighting — quiet neutral wash over the full column,
2265
+ header cell included, applied when the user sorts manually. */
2266
+ .sorted-column {
2267
+ background-color: var(--table-sorted-column-bg, rgba(0, 0, 0, 0.03));
2268
+ }
2269
+
2270
+ /* A first column is flush-left by default; when it carries a column
2271
+ wash (sorted tint, permanent highlight) the text would hug the
2272
+ shaded edge, so those cells get the small near-edge padding. Row-
2273
+ level shading (hover, highlight-row) is deliberately excluded —
2274
+ per-row padding would misalign first-column text across rows. */
2275
+ th:first-child.sorted-column,
2276
+ td:first-child.sorted-column,
2277
+ th:first-child.highlight-header,
2278
+ td:first-child.highlight-column {
2279
+ padding-left: var(--table-cell-padding-near);
2280
+ }
2281
+
2282
+ .sorted-column:hover {
2283
+ background-color: var(--table-sorted-column-bg-hover, rgba(0, 0, 0, 0.05));
2284
+ }
2285
+
2286
+ th.sorted-column {
2287
+ color: var(--color-text-primary, var(--color-black));
2288
+ }
2289
+
2290
+ /* ===========================
2291
+ TABLE TOOLTIPS
2292
+ =========================== */
2293
+
2294
+ .d3-table-tooltip {
2295
+ position: absolute;
2296
+ background: var(--color-bg-container);
2297
+ border: 1px solid var(--color-border-tooltip);
2298
+ border-radius: var(--tooltip-border-radius);
2299
+ padding: var(--tooltip-padding);
2300
+ box-shadow: var(--shadow-tooltip);
2301
+ z-index: 1000;
2302
+ pointer-events: none;
2303
+ font-family: var(--font-family);
2304
+ font-size: var(--font-size-md);
2305
+ min-width: var(--tooltip-min-width);
2306
+ overflow: hidden;
2307
+ }
2308
+
2309
+ .d3-table-tooltip-chart {
2310
+ display: flex;
2311
+ align-items: flex-end;
2312
+ gap: var(--tooltip-bar-gap);
2313
+ height: var(--tooltip-chart-height);
2314
+ position: relative;
2315
+ box-sizing: border-box;
2316
+ }
2317
+
2318
+ .d3-table-tooltip-bar {
2319
+ flex: 1;
2320
+ background-color: var(--color-text-muted);
2321
+ border-radius: var(--tooltip-bar-border-radius);
2322
+ min-width: var(--tooltip-bar-min-width);
2323
+ position: relative;
2324
+ max-height: var(--tooltip-chart-height);
2325
+ }
2326
+
2327
+ .d3-table-tooltip-bar-label {
2328
+ position: absolute;
2329
+ bottom: calc(-1 * var(--tooltip-label-offset-y));
2330
+ left: 50%;
2331
+ transform: translateX(-50%);
2332
+ font-size: var(--font-size-xs);
2333
+ color: var(--color-text-muted);
2334
+ white-space: nowrap;
2335
+ }
2336
+
2337
+ .d3-table-tooltip-bar-value {
2338
+ position: absolute;
2339
+ top: calc(-1 * var(--tooltip-value-offset-y));
2340
+ left: 50%;
2341
+ transform: translateX(-50%);
2342
+ font-size: var(--font-size-sm);
2343
+ font-weight: var(--font-weight-semibold);
2344
+ color: var(--color-black);
2345
+ white-space: nowrap;
2346
+ }
2347
+
2348
+ /* ===========================
2349
+ RESPONSIVE
2350
+ =========================== */
2351
+
2352
+ @media (max-width: 768px) {
2353
+ .d3-table-container {
2354
+ padding: var(--container-padding-mobile);
2355
+ }
2356
+
2357
+ .d3-table-title {
2358
+ font-size: var(--title-font-size-mobile);
2359
+ }
2360
+
2361
+ table {
2362
+ font-size: var(--table-font-size-mobile);
2363
+ }
2364
+
2365
+ th {
2366
+ font-size: var(--table-header-font-size-mobile);
2367
+ }
2368
+ }