rastack 0.0.55 → 0.0.57

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,292 @@
1
+ /**
2
+ * The admin's motion toolkit — "the secret is in the motion." A tiny,
3
+ * dependency-free set of animation primitives that make the console feel alive:
4
+ * numbers that count up, bars that grow, and cards/rows that stagger into view.
5
+ *
6
+ * Every primitive honours `prefers-reduced-motion` (it snaps straight to the
7
+ * final value), so the motion is a delight for those who want it and invisible
8
+ * for those who don't. Nothing here fabricates data — the values are the real
9
+ * record counts the dashboard derives from the manifest; motion only animates
10
+ * how they *arrive*.
11
+ */
12
+ import React, { useEffect, useRef, useState } from "react";
13
+
14
+ /** True when the user has asked the OS to minimise animation. */
15
+ function prefersReducedMotion(): boolean {
16
+ return (
17
+ typeof window !== "undefined" &&
18
+ typeof window.matchMedia === "function" &&
19
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
20
+ );
21
+ }
22
+
23
+ /**
24
+ * Ease-out exponential — the number spins rapidly at first and decelerates
25
+ * smoothly onto its final value. The house curve for count-up typography.
26
+ */
27
+ const easeOutExpo = (t: number): number => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
28
+
29
+ /**
30
+ * Animate a number from 0 up to `target` over `duration` ms with an ease-out
31
+ * exponential curve, driven by `requestAnimationFrame`. Re-runs whenever
32
+ * `target` changes, always animating from the previously-shown value so live
33
+ * updates glide rather than jump. Snaps instantly under reduced-motion.
34
+ */
35
+ export function useCountUp(
36
+ target: number,
37
+ opts: { duration?: number; delay?: number } = {},
38
+ ): number {
39
+ const { duration = 1200, delay = 0 } = opts;
40
+ const [value, setValue] = useState(() => (prefersReducedMotion() ? target : 0));
41
+ const fromRef = useRef(0);
42
+ const rafRef = useRef<number | null>(null);
43
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
44
+
45
+ useEffect(() => {
46
+ if (prefersReducedMotion()) {
47
+ setValue(target);
48
+ return;
49
+ }
50
+ const from = fromRef.current;
51
+ let start = 0;
52
+ const step = (ts: number) => {
53
+ if (!start) start = ts;
54
+ const t = Math.min(1, (ts - start) / duration);
55
+ const current = from + (target - from) * easeOutExpo(t);
56
+ setValue(current);
57
+ fromRef.current = current;
58
+ if (t < 1) rafRef.current = requestAnimationFrame(step);
59
+ else fromRef.current = target;
60
+ };
61
+ timerRef.current = setTimeout(() => {
62
+ rafRef.current = requestAnimationFrame(step);
63
+ }, delay);
64
+ return () => {
65
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
66
+ if (timerRef.current) clearTimeout(timerRef.current);
67
+ };
68
+ // eslint-disable-next-line react-hooks/exhaustive-deps
69
+ }, [target, duration, delay]);
70
+
71
+ return value;
72
+ }
73
+
74
+ /** Format a number for a stat readout: thousands-separated, optional decimals. */
75
+ export function formatStat(value: number, decimals = 0): string {
76
+ return value.toLocaleString(undefined, {
77
+ minimumFractionDigits: decimals,
78
+ maximumFractionDigits: decimals,
79
+ });
80
+ }
81
+
82
+ /** A number that counts up to `value` on mount. */
83
+ export function CountUp(props: {
84
+ value: number;
85
+ decimals?: number;
86
+ duration?: number;
87
+ delay?: number;
88
+ suffix?: string;
89
+ prefix?: string;
90
+ }): React.JSX.Element {
91
+ const shown = useCountUp(props.value, {
92
+ duration: props.duration,
93
+ delay: props.delay,
94
+ });
95
+ return (
96
+ <>
97
+ {props.prefix}
98
+ {formatStat(shown, props.decimals ?? 0)}
99
+ {props.suffix}
100
+ </>
101
+ );
102
+ }
103
+
104
+ /**
105
+ * A staggered fade-up wrapper. Children rise a few pixels into place; `index`
106
+ * (or an explicit `delay`) offsets the start so a grid/list cascades in. Pure
107
+ * CSS animation (see `.ras-reveal` in the stylesheet) — no JS on the hot path.
108
+ */
109
+ export function Reveal(props: {
110
+ children: React.ReactNode;
111
+ index?: number;
112
+ delay?: number;
113
+ step?: number;
114
+ className?: string;
115
+ style?: React.CSSProperties;
116
+ as?: keyof React.JSX.IntrinsicElements;
117
+ }): React.JSX.Element {
118
+ const { as = "div", index = 0, step = 55, delay } = props;
119
+ const ms = delay ?? index * step;
120
+ return React.createElement(
121
+ as,
122
+ {
123
+ className: "ras-reveal" + (props.className ? " " + props.className : ""),
124
+ style: { ["--ras-delay" as string]: `${ms}ms`, ...props.style },
125
+ },
126
+ props.children,
127
+ );
128
+ }
129
+
130
+ /** A signed change indicator — up (accent/green) or down (muted/red) arrow + text. */
131
+ export function Delta(props: {
132
+ children: React.ReactNode;
133
+ direction?: "up" | "down" | "flat";
134
+ }): React.JSX.Element {
135
+ const dir = props.direction ?? "flat";
136
+ return (
137
+ <span className={"ras-delta is-" + dir}>
138
+ {dir !== "flat" ? (
139
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" aria-hidden="true">
140
+ <path
141
+ d={dir === "up" ? "M12 19V5M6 11l6-6 6 6" : "M12 5v14M6 13l6 6 6-6"}
142
+ stroke="currentColor"
143
+ strokeWidth="2.4"
144
+ strokeLinecap="round"
145
+ strokeLinejoin="round"
146
+ />
147
+ </svg>
148
+ ) : null}
149
+ {props.children}
150
+ </span>
151
+ );
152
+ }
153
+
154
+ /** One column in the {@link BarChart}. */
155
+ export interface BarDatum {
156
+ label: string;
157
+ value: number;
158
+ }
159
+
160
+ /**
161
+ * The "Performance trend" analogue: a row of bars that grow from the baseline on
162
+ * mount, each offset a touch so the chart sweeps in left-to-right. Hovering a
163
+ * bar reveals its exact value. Heights are real (record counts); only the
164
+ * *reveal* is animated.
165
+ */
166
+ export function BarChart(props: {
167
+ data: BarDatum[];
168
+ height?: number;
169
+ gridLines?: number;
170
+ formatValue?: (v: number) => string;
171
+ }): React.JSX.Element {
172
+ const { data, height = 200, gridLines = 4 } = props;
173
+ const max = Math.max(1, ...data.map((d) => d.value));
174
+ const fmt = props.formatValue ?? ((v: number) => formatStat(v));
175
+ const lines = Array.from({ length: gridLines + 1 }, (_, i) => i);
176
+
177
+ return (
178
+ <div className="ras-chart" style={{ height }}>
179
+ <div className="ras-chart-grid" aria-hidden="true">
180
+ {lines.map((i) => (
181
+ <div className="ras-chart-line" key={i}>
182
+ <span className="ras-chart-tick">
183
+ {fmt(Math.round((max * (gridLines - i)) / gridLines))}
184
+ </span>
185
+ </div>
186
+ ))}
187
+ </div>
188
+ <div className="ras-chart-bars" style={{ ["--ras-bar-n" as string]: data.length }}>
189
+ {data.map((d, i) => {
190
+ const pct = Math.max(2, (d.value / max) * 100);
191
+ return (
192
+ <div className="ras-chart-col" key={d.label + i} title={`${d.label}: ${fmt(d.value)}`}>
193
+ <div className="ras-chart-track">
194
+ <div
195
+ className="ras-chart-bar"
196
+ style={{
197
+ height: `${pct}%`,
198
+ ["--ras-delay" as string]: `${i * 45}ms`,
199
+ }}
200
+ >
201
+ <span className="ras-chart-value">{fmt(d.value)}</span>
202
+ </div>
203
+ </div>
204
+ <div className="ras-chart-xlabel">{d.label}</div>
205
+ </div>
206
+ );
207
+ })}
208
+ </div>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ /** One coloured slice of a {@link SegmentBar}. */
214
+ export interface Segment {
215
+ label: string;
216
+ value: number;
217
+ color: string;
218
+ }
219
+
220
+ /**
221
+ * A single horizontal bar split into coloured proportional segments (the
222
+ * "Workflow breakdown" analogue). Each segment eases open from zero width on
223
+ * mount; the whole bar reads as one composed share.
224
+ */
225
+ export function SegmentBar(props: { segments: Segment[]; height?: number }): React.JSX.Element {
226
+ const total = Math.max(1, props.segments.reduce((s, x) => s + x.value, 0));
227
+ return (
228
+ <div className="ras-segbar" style={{ height: props.height ?? 12 }}>
229
+ {props.segments.map((seg, i) => (
230
+ <div
231
+ key={seg.label + i}
232
+ className="ras-segbar-seg"
233
+ title={`${seg.label}: ${formatStat(seg.value)}`}
234
+ style={{
235
+ width: `${(seg.value / total) * 100}%`,
236
+ background: seg.color,
237
+ ["--ras-delay" as string]: `${i * 90}ms`,
238
+ }}
239
+ />
240
+ ))}
241
+ </div>
242
+ );
243
+ }
244
+
245
+ /**
246
+ * A labelled horizontal meter that fills to `value / max` on mount — used for
247
+ * the "largest tables" ranking. Filling from zero is the whole effect.
248
+ */
249
+ export function MeterRow(props: {
250
+ label: string;
251
+ value: number;
252
+ max: number;
253
+ color?: string;
254
+ index?: number;
255
+ }): React.JSX.Element {
256
+ const pct = Math.max(2, (props.value / Math.max(1, props.max)) * 100);
257
+ return (
258
+ <div className="ras-meter">
259
+ <div className="ras-meter-head">
260
+ <span className="ras-meter-label">{props.label}</span>
261
+ <span className="ras-meter-value">
262
+ <CountUp value={props.value} delay={(props.index ?? 0) * 60} />
263
+ </span>
264
+ </div>
265
+ <div className="ras-meter-track">
266
+ <div
267
+ className="ras-meter-fill"
268
+ style={{
269
+ width: `${pct}%`,
270
+ background: props.color ?? "var(--color-accent)",
271
+ ["--ras-delay" as string]: `${(props.index ?? 0) * 70}ms`,
272
+ }}
273
+ />
274
+ </div>
275
+ </div>
276
+ );
277
+ }
278
+
279
+ /** A stable, pleasant colour for the Nth categorical series (accent-derived). */
280
+ export function seriesColor(i: number): string {
281
+ const hues = [
282
+ "var(--color-accent)",
283
+ "var(--color-success)",
284
+ "var(--color-warning)",
285
+ "var(--color-danger)",
286
+ "color-mix(in srgb, var(--color-accent) 55%, var(--color-success))",
287
+ "color-mix(in srgb, var(--color-accent) 45%, white)",
288
+ "color-mix(in srgb, var(--color-warning) 60%, var(--color-danger))",
289
+ "color-mix(in srgb, var(--color-accent) 65%, var(--color-warning))",
290
+ ];
291
+ return hues[i % hues.length];
292
+ }
@@ -58,9 +58,30 @@ a { color: inherit; text-decoration: none; }
58
58
  display: grid;
59
59
  grid-template-columns: var(--ras-sidebar-w) minmax(0, 1fr);
60
60
  min-height: 100vh;
61
+ transition: grid-template-columns 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
61
62
  }
63
+ .ras-root.is-collapsed { --ras-sidebar-w: 68px; }
62
64
  .ras-main { min-width: 0; display: flex; flex-direction: column; }
63
65
 
66
+ /* ---- sidebar collapse ---------------------------------------------------- */
67
+ .ras-brand-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
68
+ .ras-collapse-btn { width: 30px; height: 30px; flex-shrink: 0; }
69
+ .ras-collapse-btn svg { transition: transform 300ms var(--ras-ease); }
70
+ .ras-brand-text, .ras-nav-text { transition: opacity 0.16s var(--ras-ease); white-space: nowrap; }
71
+ .is-collapsed .ras-sidebar { padding-left: 12px; padding-right: 12px; align-items: stretch; }
72
+ .is-collapsed .ras-brand { padding-right: 0; gap: 0; }
73
+ .is-collapsed .ras-brand-row { justify-content: center; }
74
+ .is-collapsed .ras-brand-text,
75
+ .is-collapsed .ras-nav-text,
76
+ .is-collapsed .ras-nav-label,
77
+ .is-collapsed .ras-foot-meta { opacity: 0; width: 0; overflow: hidden; pointer-events: none; }
78
+ .is-collapsed .ras-nav-label { height: 0; padding: 0; margin: 0; }
79
+ .is-collapsed .ras-brand-row { flex-direction: column; gap: 12px; padding-bottom: 12px; }
80
+ .is-collapsed .ras-collapse-btn svg { transform: scaleX(-1); }
81
+ .is-collapsed .ras-nav-link { justify-content: center; padding-left: 0; padding-right: 0; }
82
+ .is-collapsed .ras-nav-link.is-active::before { left: -12px; }
83
+ .is-collapsed .ras-sidebar-foot { justify-content: center; }
84
+
64
85
  /* ---- sidebar ------------------------------------------------------------- */
65
86
  .ras-sidebar {
66
87
  position: sticky;
@@ -366,6 +387,258 @@ a { color: inherit; text-decoration: none; }
366
387
  .ras-modelcard .ras-mc-name { font-weight: 600; font-size: 15px; }
367
388
  .ras-modelcard .ras-mc-go { color: var(--color-accent); display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; font-weight: 500; }
368
389
  .ras-mc-icon { display:inline-flex; align-items:center; justify-content:center; width:36px; height:36px; border-radius: var(--radius-md); background: var(--color-surface-alt); color: var(--color-accent); }
390
+ .ras-modelcard .ras-mc-count {
391
+ font-family: var(--font-family-mono, ui-monospace, monospace);
392
+ font-size: 13px; font-weight: 600; color: var(--color-text-muted);
393
+ padding: 2px 9px; border-radius: var(--radius-pill); background: var(--color-surface-alt);
394
+ }
395
+ .ras-modelcard .ras-mc-meta { font-size: 11.5px; color: var(--color-text-muted); letter-spacing: 0.03em; text-transform: uppercase; margin-top: -4px; }
396
+ .ras-modelcard .ras-mc-go { margin-top: auto; }
397
+
398
+ /* ==== MOTION ================================================================
399
+ "The secret is in the motion." Entrance + fill animations, all opt-out under
400
+ prefers-reduced-motion. --ras-delay staggers each element's start. */
401
+ @keyframes ras-fade-up {
402
+ from { opacity: 0; transform: translateY(10px); }
403
+ to { opacity: 1; transform: none; }
404
+ }
405
+ @keyframes ras-grow-up { from { transform: scaleY(0); } to { transform: scaleY(1); } }
406
+ @keyframes ras-grow-right { from { transform: scaleX(0); } to { transform: scaleX(1); } }
407
+ @keyframes ras-shimmer { from { background-position: -320px 0; } to { background-position: 320px 0; } }
408
+ @keyframes ras-fade-in { from { opacity: 0; } to { opacity: 1; } }
409
+
410
+ .ras-reveal {
411
+ opacity: 0;
412
+ animation: ras-fade-up 0.62s var(--ras-ease) forwards;
413
+ animation-delay: var(--ras-delay, 0ms);
414
+ will-change: transform, opacity;
415
+ }
416
+
417
+ /* the whole content pane drifts in on route change */
418
+ .ras-content { animation: ras-fade-in 0.3s var(--ras-ease) both; }
419
+
420
+ /* changelist rows cascade in */
421
+ .ras-tablecard tbody tr {
422
+ animation: ras-fade-up 0.44s var(--ras-ease) both;
423
+ }
424
+ .ras-tablecard tbody tr:nth-child(1) { animation-delay: 20ms; }
425
+ .ras-tablecard tbody tr:nth-child(2) { animation-delay: 46ms; }
426
+ .ras-tablecard tbody tr:nth-child(3) { animation-delay: 72ms; }
427
+ .ras-tablecard tbody tr:nth-child(4) { animation-delay: 98ms; }
428
+ .ras-tablecard tbody tr:nth-child(5) { animation-delay: 124ms; }
429
+ .ras-tablecard tbody tr:nth-child(6) { animation-delay: 150ms; }
430
+ .ras-tablecard tbody tr:nth-child(7) { animation-delay: 176ms; }
431
+ .ras-tablecard tbody tr:nth-child(8) { animation-delay: 202ms; }
432
+ .ras-tablecard tbody tr:nth-child(n+9) { animation-delay: 224ms; }
433
+
434
+ /* sidebar nav links glide in on first paint */
435
+ .ras-nav-link { animation: ras-fade-up 0.5s var(--ras-ease) both; }
436
+ .ras-nav-section:nth-child(1) .ras-nav-link { animation-delay: 40ms; }
437
+ .ras-nav-section:nth-child(2) .ras-nav-link { animation-delay: 90ms; }
438
+ .ras-nav-section:nth-child(3) .ras-nav-link { animation-delay: 140ms; }
439
+
440
+ .ras-delta { display: inline-flex; align-items: center; gap: 3px; font-weight: 600; }
441
+ .ras-delta.is-up { color: var(--color-success); }
442
+ .ras-delta.is-down { color: var(--color-danger); }
443
+ .ras-delta.is-flat { color: var(--color-text-muted); }
444
+
445
+ /* ---- KPI strip ----------------------------------------------------------- */
446
+ .ras-statgrid {
447
+ display: grid;
448
+ grid-template-columns: repeat(4, minmax(0, 1fr));
449
+ gap: 14px;
450
+ margin-bottom: 18px;
451
+ }
452
+ .ras-stat {
453
+ padding: 16px 18px;
454
+ border: 1px solid var(--color-border);
455
+ border-radius: var(--radius-lg);
456
+ background: var(--color-surface);
457
+ box-shadow: var(--ras-shadow-sm);
458
+ transition: box-shadow 0.16s var(--ras-ease), transform 0.16s var(--ras-ease), border-color 0.16s;
459
+ }
460
+ .ras-stat:hover { box-shadow: var(--ras-shadow-md); transform: translateY(-2px); border-color: color-mix(in srgb, var(--color-accent) 30%, var(--color-border)); }
461
+ .ras-stat-top { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
462
+ .ras-stat-ico {
463
+ display: inline-flex; align-items: center; justify-content: center;
464
+ width: 30px; height: 30px; border-radius: var(--radius-md);
465
+ background: color-mix(in srgb, currentColor 12%, transparent);
466
+ color: var(--color-accent);
467
+ }
468
+ .ras-stat-label { font-size: 12.5px; font-weight: 500; color: var(--color-text-muted); }
469
+ .ras-stat-value {
470
+ font-size: 30px; font-weight: 700; letter-spacing: -0.02em; line-height: 1;
471
+ font-variant-numeric: tabular-nums; color: var(--color-text);
472
+ }
473
+ .ras-stat-sub { font-size: 12px; color: var(--color-text-muted); margin-top: 7px; }
474
+
475
+ /* ---- panels (chart + breakdown cards) ------------------------------------ */
476
+ .ras-panel { padding: 18px 20px 20px; margin-bottom: 16px; }
477
+ .ras-panel-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }
478
+ .ras-panel-row .ras-reveal { min-width: 0; }
479
+ .ras-panel-row .ras-panel { margin-bottom: 0; height: 100%; }
480
+ .ras-panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
481
+ .ras-panel-title { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; font-size: 14.5px; color: var(--color-text); }
482
+ .ras-panel-title svg { color: var(--color-accent); }
483
+ .ras-panel-sub { font-size: 12px; color: var(--color-text-muted); margin-top: 3px; }
484
+ .ras-pill {
485
+ font-size: 11.5px; font-weight: 600; color: var(--color-text-muted);
486
+ padding: 3px 10px; border-radius: var(--radius-pill);
487
+ background: var(--color-surface-alt); border: 1px solid var(--color-border);
488
+ }
489
+ .ras-panel-empty { padding: 34px 10px; text-align: center; color: var(--color-text-muted); font-size: 13px; }
490
+ .ras-details-btn {
491
+ flex-shrink: 0; font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer;
492
+ padding: 5px 12px; border-radius: var(--radius-md);
493
+ border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-text-muted);
494
+ transition: background 0.14s, color 0.14s, border-color 0.14s;
495
+ }
496
+ .ras-details-btn:hover { color: var(--color-text); border-color: color-mix(in srgb, var(--color-accent) 40%, var(--color-border)); background: var(--ras-hover); }
497
+
498
+ /* ---- backdrop-blur Details modal (§4) ------------------------------------ */
499
+ .ras-modal-overlay {
500
+ position: fixed; inset: 0; z-index: 100;
501
+ display: flex; align-items: center; justify-content: center; padding: 24px;
502
+ background: color-mix(in srgb, var(--color-bg) 40%, transparent);
503
+ backdrop-filter: blur(16px) saturate(140%);
504
+ -webkit-backdrop-filter: blur(16px) saturate(140%);
505
+ will-change: backdrop-filter;
506
+ animation: ras-fade-in 0.4s ease-out both;
507
+ }
508
+ .ras-modal {
509
+ width: 100%; max-width: 520px; max-height: 82vh; overflow-y: auto;
510
+ padding: 22px 24px 20px;
511
+ background: var(--color-surface);
512
+ border: 1px solid var(--color-border);
513
+ border-radius: var(--radius-lg);
514
+ box-shadow: var(--ras-shadow-pop);
515
+ animation: ras-modal-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
516
+ }
517
+ @keyframes ras-modal-in {
518
+ from { opacity: 0; transform: scale(0.95) translate3d(0, 10px, 0); }
519
+ to { opacity: 1; transform: none; }
520
+ }
521
+ .ras-modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 20px; }
522
+ .ras-modal-title { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; color: var(--color-text); }
523
+ .ras-modal-sub { font-size: 12.5px; color: var(--color-text-muted); margin-top: 3px; }
524
+ .ras-modal-x {
525
+ flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
526
+ width: 30px; height: 30px; border-radius: var(--radius-md);
527
+ border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-text-muted); cursor: pointer;
528
+ transition: background 0.14s, color 0.14s;
529
+ }
530
+ .ras-modal-x:hover { color: var(--color-text); background: var(--ras-hover); }
531
+ .ras-modal-rows { display: flex; flex-direction: column; gap: 15px; }
532
+ .ras-modal-row-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 7px; }
533
+ .ras-modal-row-label { display: inline-flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 500; color: var(--color-text); min-width: 0; }
534
+ .ras-modal-row-meta { font-size: 11.5px; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.03em; }
535
+ .ras-modal-row-val { display: inline-flex; align-items: baseline; gap: 8px; font-weight: 600; font-variant-numeric: tabular-nums; }
536
+ .ras-modal-row-pct { font-size: 11.5px; font-weight: 600; color: var(--color-text-muted); min-width: 34px; text-align: right; }
537
+ .ras-modal-track { height: 8px; border-radius: var(--radius-pill); background: var(--color-surface-alt); overflow: hidden; }
538
+ .ras-modal-fill {
539
+ height: 100%; border-radius: var(--radius-pill); width: 0%;
540
+ transition: width 800ms cubic-bezier(0.4, 0, 0.2, 1);
541
+ }
542
+ .ras-modal-foot {
543
+ display: flex; align-items: center; justify-content: space-between;
544
+ margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--color-border);
545
+ font-size: 12.5px; color: var(--color-text-muted);
546
+ }
547
+ .ras-modal-total { font-weight: 600; color: var(--color-text); font-variant-numeric: tabular-nums; }
548
+ .ras-chart-skel {
549
+ height: 200px; border-radius: var(--radius-md);
550
+ background: linear-gradient(100deg, var(--color-surface-alt) 30%, color-mix(in srgb, var(--color-text) 6%, var(--color-surface-alt)) 50%, var(--color-surface-alt) 70%);
551
+ background-size: 640px 100%;
552
+ animation: ras-shimmer 1.3s linear infinite;
553
+ }
554
+
555
+ /* ---- bar chart (data distribution) --------------------------------------- */
556
+ .ras-chart { position: relative; width: 100%; --ras-chart-gutter: 46px; }
557
+ .ras-chart-grid { position: absolute; inset: 0 0 26px var(--ras-chart-gutter); display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; }
558
+ .ras-chart-line { position: relative; border-top: 1px dashed color-mix(in srgb, var(--color-text) 9%, transparent); }
559
+ .ras-chart-tick { position: absolute; top: -7px; right: calc(100% + 10px); font-size: 10.5px; color: var(--color-text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
560
+ .ras-chart-bars { position: absolute; inset: 0 0 0 var(--ras-chart-gutter); display: flex; align-items: flex-end; gap: clamp(3px, 2%, 14px); padding-bottom: 26px; }
561
+ .ras-chart-col { position: relative; flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; height: 100%; }
562
+ /* full-height hover guide band that snaps behind the hovered column */
563
+ .ras-chart-col::before {
564
+ content: ""; position: absolute; inset: 0 -4px 26px -4px; z-index: 0;
565
+ border-radius: var(--radius-sm);
566
+ background: color-mix(in srgb, var(--color-accent) 8%, transparent);
567
+ opacity: 0; transition: opacity 0.16s var(--ras-ease); pointer-events: none;
568
+ }
569
+ .ras-chart-col:hover::before { opacity: 1; }
570
+ .ras-chart-track { position: relative; z-index: 1; flex: 1; display: flex; align-items: flex-end; }
571
+ .ras-chart-bar {
572
+ position: relative; width: 100%; min-height: 3px;
573
+ border-radius: var(--radius-sm) var(--radius-sm) 2px 2px;
574
+ background: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 78%, var(--color-bg)));
575
+ transform-origin: bottom;
576
+ /* overshoot entrance — the bars pop up past target and settle */
577
+ animation: ras-grow-up 0.72s cubic-bezier(0.34, 1.56, 0.64, 1) both;
578
+ animation-delay: var(--ras-delay, 0ms);
579
+ transition: filter 0.14s, background 0.14s;
580
+ cursor: default;
581
+ }
582
+ .ras-chart-col:hover .ras-chart-bar { filter: brightness(1.08) saturate(1.1); }
583
+ .ras-chart-value {
584
+ position: absolute; bottom: calc(100% + 5px); left: 50%; transform: translateX(-50%);
585
+ font-size: 11px; font-weight: 600; color: var(--color-text); white-space: nowrap;
586
+ background: var(--color-surface); border: 1px solid var(--color-border);
587
+ border-radius: var(--radius-sm); padding: 2px 7px; box-shadow: var(--ras-shadow-sm);
588
+ opacity: 0; transition: opacity 0.14s; pointer-events: none; font-variant-numeric: tabular-nums;
589
+ }
590
+ .ras-chart-col:hover .ras-chart-value { opacity: 1; }
591
+ .ras-chart-xlabel {
592
+ height: 26px; display: flex; align-items: center; justify-content: center;
593
+ font-size: 11px; color: var(--color-text-muted); padding-top: 6px;
594
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
595
+ }
596
+
597
+ /* ---- segmented bar + legend ---------------------------------------------- */
598
+ .ras-segbar { display: flex; width: 100%; border-radius: var(--radius-pill); overflow: hidden; background: var(--color-surface-alt); }
599
+ .ras-segbar-seg {
600
+ height: 100%; transform-origin: left; animation: ras-grow-right 0.75s var(--ras-ease) both;
601
+ animation-delay: var(--ras-delay, 0ms);
602
+ }
603
+ .ras-legend { margin-top: 16px; display: flex; flex-direction: column; gap: 2px; }
604
+ .ras-legend-row { display: flex; align-items: center; gap: 10px; padding: 7px 4px; border-radius: var(--radius-sm); font-size: 13px; }
605
+ .ras-legend-row:hover { background: var(--ras-hover); }
606
+ .ras-legend-key { display: inline-flex; align-items: center; gap: 8px; font-weight: 500; color: var(--color-text); flex: 1; min-width: 0; }
607
+ .ras-legend-key { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
608
+ .ras-legend-dot { width: 9px; height: 9px; border-radius: 3px; flex-shrink: 0; }
609
+ .ras-legend-meta { font-size: 12px; color: var(--color-text-muted); }
610
+ .ras-legend-val { display: inline-flex; align-items: baseline; gap: 8px; font-weight: 600; font-variant-numeric: tabular-nums; min-width: 74px; justify-content: flex-end; }
611
+ .ras-legend-pct { font-size: 11.5px; font-weight: 600; color: var(--color-text-muted); min-width: 34px; text-align: right; }
612
+
613
+ /* ---- meter rows (largest tables) ----------------------------------------- */
614
+ .ras-meters { display: flex; flex-direction: column; gap: 15px; }
615
+ .ras-meter-link { display: block; }
616
+ .ras-meter-link:hover .ras-meter-label { color: var(--color-accent-strong); }
617
+ .ras-meter-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 7px; }
618
+ .ras-meter-label { font-size: 13px; font-weight: 500; color: var(--color-text); transition: color 0.14s; }
619
+ .ras-meter-value { font-size: 13px; font-weight: 600; color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
620
+ .ras-meter-track { height: 8px; border-radius: var(--radius-pill); background: var(--color-surface-alt); overflow: hidden; }
621
+ .ras-meter-fill {
622
+ height: 100%; border-radius: var(--radius-pill); transform-origin: left;
623
+ animation: ras-grow-right 0.8s var(--ras-ease) both; animation-delay: var(--ras-delay, 0ms);
624
+ }
625
+
626
+ .ras-section-label {
627
+ font-size: 11px; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase;
628
+ color: var(--color-text-muted); margin: 8px 2px 14px;
629
+ }
630
+
631
+ @media (prefers-reduced-motion: reduce) {
632
+ .ras-reveal, .ras-content, .ras-tablecard tbody tr, .ras-nav-link,
633
+ .ras-chart-bar, .ras-segbar-seg, .ras-meter-fill, .ras-chart-skel,
634
+ .ras-modal-overlay, .ras-modal {
635
+ animation: none !important;
636
+ opacity: 1 !important;
637
+ transform: none !important;
638
+ }
639
+ .ras-root { transition: none !important; }
640
+ .ras-modal-fill { transition: none !important; }
641
+ }
369
642
 
370
643
  /* ---- scrollbars ---------------------------------------------------------- */
371
644
  * { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--color-text) 20%, transparent) transparent; }
@@ -374,6 +647,10 @@ a { color: inherit; text-decoration: none; }
374
647
  *::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--color-text) 30%, transparent); background-clip: content-box; }
375
648
 
376
649
  /* ---- responsive ---------------------------------------------------------- */
650
+ @media (max-width: 1080px) {
651
+ .ras-statgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
652
+ .ras-panel-row { grid-template-columns: 1fr; }
653
+ }
377
654
  @media (max-width: 860px) {
378
655
  .ras-root { grid-template-columns: 1fr; }
379
656
  .ras-sidebar {