bobframes 0.1.0__py3-none-any.whl

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.
Files changed (130) hide show
  1. bobframes/__init__.py +3 -0
  2. bobframes/_version.py +1 -0
  3. bobframes/catalog.py +154 -0
  4. bobframes/cli.py +266 -0
  5. bobframes/derive_post_merge.py +365 -0
  6. bobframes/derives/__init__.py +0 -0
  7. bobframes/derives/pass_class_breakdown.py +102 -0
  8. bobframes/derives/texture_usage.py +121 -0
  9. bobframes/discovery.py +132 -0
  10. bobframes/global_entities.py +99 -0
  11. bobframes/html/__init__.py +0 -0
  12. bobframes/html/template.py +1056 -0
  13. bobframes/lint.py +114 -0
  14. bobframes/manifest.py +127 -0
  15. bobframes/parquetize.py +282 -0
  16. bobframes/parsers/__init__.py +0 -0
  17. bobframes/parsers/derive_program_transitions.py +73 -0
  18. bobframes/parsers/parse_init_state.py +675 -0
  19. bobframes/paths.py +111 -0
  20. bobframes/probes/__init__.py +0 -0
  21. bobframes/probes/whatif.py +165 -0
  22. bobframes/qrd_harness.py +119 -0
  23. bobframes/query_examples.py +222 -0
  24. bobframes/rdcmd.py +72 -0
  25. bobframes/replay/__init__.py +26 -0
  26. bobframes/replay/replay_main.py +2305 -0
  27. bobframes/reports/__init__.py +0 -0
  28. bobframes/reports/_dashboard.py +425 -0
  29. bobframes/reports/ab.py +88 -0
  30. bobframes/reports/base.py +114 -0
  31. bobframes/reports/cache.py +147 -0
  32. bobframes/reports/chrome.py +1306 -0
  33. bobframes/reports/cli.py +99 -0
  34. bobframes/reports/delta.py +167 -0
  35. bobframes/reports/discovery.py +118 -0
  36. bobframes/reports/draws_by_class.py +165 -0
  37. bobframes/reports/formatters.py +122 -0
  38. bobframes/reports/instancing_opportunities.py +276 -0
  39. bobframes/reports/orchestrator.py +59 -0
  40. bobframes/reports/overdraw.py +293 -0
  41. bobframes/reports/pass_gpu.py +190 -0
  42. bobframes/reports/shader_hotlist.py +240 -0
  43. bobframes/reports/trend_table.py +444 -0
  44. bobframes/resource_labels.py +162 -0
  45. bobframes/run.py +480 -0
  46. bobframes/schemas.py +426 -0
  47. bobframes/stable_keys.py +83 -0
  48. bobframes/tests/__init__.py +0 -0
  49. bobframes/tests/_render_util.py +84 -0
  50. bobframes/tests/data/golden/_reports/draws_by_class.html +323 -0
  51. bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +1560 -0
  52. bobframes/tests/data/golden/_reports/index.html +264 -0
  53. bobframes/tests/data/golden/_reports/instancing_opportunities.html +266 -0
  54. bobframes/tests/data/golden/_reports/overdraw.html +275 -0
  55. bobframes/tests/data/golden/_reports/pass_gpu.html +277 -0
  56. bobframes/tests/data/golden/_reports/shader_hotlist.html +265 -0
  57. bobframes/tests/data/golden/_reports/trend_table.html +390 -0
  58. bobframes/tests/data/golden/index.html +1175 -0
  59. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +51 -0
  60. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
  61. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
  62. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
  63. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
  64. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
  65. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
  66. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
  67. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
  68. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
  69. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
  70. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
  71. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
  72. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
  73. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
  74. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
  75. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
  76. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
  77. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
  78. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
  79. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
  80. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
  81. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
  82. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
  83. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
  84. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
  85. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
  86. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
  87. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +51 -0
  88. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
  89. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
  90. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
  91. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
  92. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
  93. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
  94. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
  95. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
  96. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
  97. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
  98. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
  99. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
  100. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
  101. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
  102. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
  103. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
  104. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
  105. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
  106. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
  107. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
  108. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
  109. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
  110. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
  111. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
  112. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
  113. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
  114. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
  115. bobframes/tests/make_synthetic.py +171 -0
  116. bobframes/tests/smoke.py +199 -0
  117. bobframes/tests/test_determinism.py +19 -0
  118. bobframes/tests/test_discovery.py +97 -0
  119. bobframes/tests/test_hardening.py +142 -0
  120. bobframes/tests/test_parity.py +22 -0
  121. bobframes/tests/test_perf.py +18 -0
  122. bobframes/tests/test_replay_drift.py +115 -0
  123. bobframes/tests/test_schemas.py +26 -0
  124. bobframes/tests/test_schemas_unit.py +55 -0
  125. bobframes/tests/test_stable_keys.py +61 -0
  126. bobframes-0.1.0.dist-info/METADATA +144 -0
  127. bobframes-0.1.0.dist-info/RECORD +130 -0
  128. bobframes-0.1.0.dist-info/WHEEL +4 -0
  129. bobframes-0.1.0.dist-info/entry_points.txt +2 -0
  130. bobframes-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1175 @@
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8">
2
+ <title>capture analysis catalog</title>
3
+ <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect x='2' y='3' width='12' height='2' fill='%23888'/><rect x='2' y='7' width='12' height='2' fill='%234a8'/><rect x='2' y='11' width='12' height='2' fill='%23d54'/></svg>">
4
+ <style>
5
+ :root {
6
+ color-scheme: light dark;
7
+
8
+ --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
9
+ --sp-6: 24px; --sp-8: 32px; --sp-12: 48px;
10
+
11
+ --fs-display: 2.25rem;
12
+ --fs-h1: 1.5rem; --fs-h2: 1.1rem; --fs-h3: 0.9rem;
13
+ --fs-body: 14px; --fs-mono: 13px; --fs-small: 11px;
14
+
15
+ --motion-hover: 150ms ease-out;
16
+ --motion-focus: 100ms ease-out;
17
+ --motion-vt: 200ms ease-in-out;
18
+ --motion-disclosure: 180ms ease-out;
19
+
20
+ --bg: light-dark(oklch(97.2% 0.012 80), oklch(16.4% 0.012 260));
21
+ --surface-0: var(--bg);
22
+ --surface-1: light-dark(oklch(94.6% 0.018 80), oklch(19.5% 0.013 260));
23
+ --surface-2: light-dark(oklch(94.6% 0.020 80), oklch(20.7% 0.014 260));
24
+ --code-bg: var(--surface-2);
25
+
26
+ --fg: light-dark(oklch(22.0% 0.005 80), oklch(93.6% 0.005 260));
27
+ --text-1: var(--fg);
28
+ --muted: light-dark(oklch(49.4% 0.005 80), oklch(67.5% 0.005 260));
29
+ --text-2: var(--muted);
30
+ --text-3: light-dark(oklch(60.0% 0.012 80), oklch(56.4% 0.020 280));
31
+
32
+ --border: light-dark(oklch(84.0% 0.024 82), oklch(28.0% 0.014 260));
33
+ --border-1: var(--border);
34
+ --border-strong: light-dark(oklch(75.0% 0.030 82), oklch(35.0% 0.018 260));
35
+ --border-2: var(--border-strong);
36
+
37
+ --row-alt: light-dark(oklch(93.2% 0.022 82), oklch(20.5% 0.015 260));
38
+ --row-hover: light-dark(oklch(90.0% 0.038 82), oklch(25.0% 0.022 260));
39
+
40
+ --accent-primary: light-dark(oklch(38.0% 0.020 260), oklch(78.0% 0.015 260));
41
+ --accent-data: light-dark(oklch(55.0% 0.155 230), oklch(75.0% 0.115 230));
42
+ --accent: var(--accent-primary);
43
+
44
+ --status-alarm: light-dark(oklch(52.0% 0.180 28), oklch(70.0% 0.155 28));
45
+ --status-warn: light-dark(oklch(70.0% 0.155 85), oklch(80.0% 0.130 85));
46
+ --status-ok: light-dark(oklch(55.0% 0.135 145), oklch(72.0% 0.115 145));
47
+ --status-info: light-dark(oklch(55.0% 0.115 260), oklch(75.0% 0.090 260));
48
+
49
+ --c-opaque: light-dark(oklch(60.5% 0.110 135), oklch(73.5% 0.115 135));
50
+ --c-prepass: light-dark(oklch(64.0% 0.135 50), oklch(73.0% 0.130 50));
51
+ --c-translucent: light-dark(oklch(56.0% 0.085 240), oklch(71.0% 0.080 240));
52
+ --c-additive: light-dark(oklch(56.0% 0.115 305), oklch(72.0% 0.105 305));
53
+ --c-decal: light-dark(oklch(60.0% 0.115 65), oklch(71.0% 0.115 65));
54
+ --c-shadow: light-dark(oklch(42.0% 0.025 285), oklch(55.0% 0.020 285));
55
+ --c-ui: light-dark(oklch(71.0% 0.115 90), oklch(80.0% 0.115 90));
56
+ --c-postprocess: light-dark(oklch(66.0% 0.055 240), oklch(78.0% 0.055 240));
57
+ --c-other: light-dark(oklch(64.0% 0.000 0), oklch(75.0% 0.000 0));
58
+
59
+ --pos: var(--status-ok);
60
+ --neg: var(--status-alarm);
61
+ --neutral: var(--text-2);
62
+ }
63
+ @media (prefers-reduced-motion: reduce) {
64
+ :root {
65
+ --motion-hover: 0s;
66
+ --motion-focus: 0s;
67
+ --motion-vt: 0s;
68
+ --motion-disclosure: 0s;
69
+ }
70
+ }
71
+
72
+ * { box-sizing: border-box; }
73
+ html, body { margin: 0; padding: 0; background: var(--surface-0); color: var(--text-1); }
74
+ body {
75
+ padding: var(--sp-6) var(--sp-6) var(--sp-12);
76
+ font: var(--fs-body)/1.5 'Inter', 'Segoe UI', system-ui, sans-serif;
77
+ max-width: 1600px;
78
+ margin: 0 auto;
79
+ }
80
+
81
+ h1 {
82
+ font-size: var(--fs-h1); font-weight: 600; color: var(--accent);
83
+ margin: 0 0 var(--sp-2); padding-bottom: var(--sp-2);
84
+ border-bottom: 1px solid var(--border-2);
85
+ letter-spacing: -0.01em;
86
+ }
87
+ h2 {
88
+ font-size: var(--fs-h2); font-weight: 600; color: var(--text-1);
89
+ margin: var(--sp-6) 0 var(--sp-3);
90
+ padding: 0 0 0 var(--sp-3);
91
+ border-left: 3px solid var(--accent);
92
+ line-height: 1.4;
93
+ scroll-margin-top: 64px;
94
+ }
95
+ h2:first-of-type { margin-top: 0; }
96
+ section[id], h2[id] { scroll-margin-top: 64px; }
97
+ h2[id]:target { color: var(--accent); }
98
+ h3 {
99
+ font-size: var(--fs-h3); font-weight: 500; color: var(--text-2);
100
+ margin: var(--sp-3) 0 var(--sp-2);
101
+ }
102
+ a { color: var(--accent); text-decoration: none; }
103
+ a:hover { text-decoration: underline; text-decoration-thickness: 2px; }
104
+ a:visited { color: var(--text-2); }
105
+ a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
106
+ table.report a { text-decoration: underline; text-decoration-thickness: 1px;
107
+ text-underline-offset: 2px; }
108
+ table.report a:visited { color: var(--text-3); }
109
+
110
+ header.strip {
111
+ display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-6);
112
+ align-items: baseline;
113
+ padding-bottom: var(--sp-3);
114
+ border-bottom: 1px solid var(--border-2);
115
+ margin: 0 0 var(--sp-4);
116
+ }
117
+ header.strip span { color: var(--text-2); font-size: var(--fs-small); }
118
+ header.strip span strong { color: var(--text-1); font-weight: 600; }
119
+
120
+ nav.crumb {
121
+ font-size: var(--fs-small);
122
+ color: var(--text-2);
123
+ margin: 0 0 var(--sp-3);
124
+ display: flex; flex-wrap: wrap; gap: var(--sp-2);
125
+ align-items: center;
126
+ }
127
+ nav.crumb a, nav.crumb a[data-link-kind] {
128
+ display: inline-flex; align-items: center; gap: 4px;
129
+ color: var(--accent-primary);
130
+ text-decoration: none;
131
+ padding: 2px 8px;
132
+ border: 1px solid var(--border-1);
133
+ border-radius: 2px;
134
+ background: var(--surface-1);
135
+ transition: border-color var(--motion-hover), background var(--motion-hover);
136
+ }
137
+ nav.crumb a:hover, nav.crumb a[data-link-kind]:hover {
138
+ border-color: var(--accent-primary);
139
+ background: var(--row-hover);
140
+ text-decoration: none;
141
+ }
142
+ nav.crumb a + a::before { content: ''; }
143
+
144
+ .kpi-strip {
145
+ display: grid;
146
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
147
+ gap: var(--sp-3);
148
+ margin: 0 0 var(--sp-8);
149
+ }
150
+ .kpi-chip {
151
+ background: var(--surface-1);
152
+ border: 1px solid var(--border-1);
153
+ padding: var(--sp-3) var(--sp-4);
154
+ display: flex; flex-direction: column; gap: var(--sp-1);
155
+ min-height: 88px;
156
+ }
157
+ .kpi-chip .kpi-label {
158
+ font: var(--fs-small) ui-monospace, monospace;
159
+ color: var(--text-3);
160
+ text-transform: lowercase;
161
+ letter-spacing: 0.04em;
162
+ }
163
+ .kpi-chip .kpi-value {
164
+ font: 600 var(--fs-display)/1.05 ui-monospace, monospace;
165
+ color: var(--text-1);
166
+ font-variant-numeric: tabular-nums;
167
+ letter-spacing: -0.02em;
168
+ }
169
+ .kpi-chip .kpi-delta {
170
+ font: var(--fs-small) ui-monospace, monospace;
171
+ font-variant-numeric: tabular-nums;
172
+ color: var(--text-3);
173
+ }
174
+ .kpi-chip.tone-pos .kpi-value, .kpi-chip.tone-pos .kpi-delta { color: var(--pos); }
175
+ .kpi-chip.tone-neg .kpi-value, .kpi-chip.tone-neg .kpi-delta { color: var(--neg); }
176
+
177
+ .table-wrap {
178
+ overflow-x: auto;
179
+ border: 1px solid var(--border-1);
180
+ border-radius: 4px;
181
+ margin: 0 0 var(--sp-6);
182
+ }
183
+ .table-wrap > table.report { border: 0; margin: 0; }
184
+
185
+ nav.toc {
186
+ display: grid;
187
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
188
+ gap: var(--sp-1) var(--sp-3);
189
+ font: var(--fs-mono) ui-monospace, monospace;
190
+ margin: 0 0 var(--sp-6);
191
+ padding: var(--sp-2) var(--sp-3);
192
+ background: var(--surface-2);
193
+ border: 1px solid var(--border-1);
194
+ }
195
+ nav.toc a { display: inline-block; padding: 2px 0; }
196
+
197
+ table.report {
198
+ width: 100%;
199
+ border-collapse: collapse;
200
+ font: var(--fs-mono) ui-monospace, 'Cascadia Code', Consolas, monospace;
201
+ margin-top: var(--sp-1);
202
+ }
203
+ table.report thead th {
204
+ position: sticky;
205
+ top: var(--hdr-offset);
206
+ background: var(--surface-2); color: var(--accent);
207
+ text-align: left; font-weight: 600;
208
+ padding: var(--sp-2) var(--sp-3);
209
+ border-bottom: 1px solid var(--border-2);
210
+ white-space: nowrap;
211
+ z-index: 1;
212
+ }
213
+ table.report thead th.num { text-align: right; }
214
+ table.report tbody td {
215
+ padding: var(--sp-2) var(--sp-3);
216
+ border-bottom: 1px solid var(--border-1);
217
+ vertical-align: top;
218
+ }
219
+ table.report tbody td:first-child {
220
+ font-weight: 600; color: var(--text-1);
221
+ }
222
+ table.report td.num { text-align: right; font-variant-numeric: tabular-nums; }
223
+ table.report tbody tr:nth-child(even) td { background: var(--surface-2); }
224
+ table.report tbody tr:hover td { background: var(--row-hover); }
225
+ table.report tbody td .lbl {
226
+ color: var(--text-2);
227
+ margin-left: 6px;
228
+ font-style: italic;
229
+ opacity: .85;
230
+ }
231
+ table.report tr.area-break td { border-top: 2px solid var(--border-2); }
232
+ table.report td.area-cell { color: var(--text-2); }
233
+
234
+ .bar-row {
235
+ display: grid;
236
+ grid-template-columns: minmax(240px, 1fr) 2fr 90px;
237
+ gap: var(--sp-3);
238
+ align-items: center;
239
+ padding: 4px 0;
240
+ font: var(--fs-mono) ui-monospace, 'Cascadia Code', Consolas, monospace;
241
+ }
242
+ .bar-row .key { white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
243
+ font-weight: 600; color: var(--text-1); }
244
+ .bar-row .total { text-align: right; font-variant-numeric: tabular-nums;
245
+ color: var(--text-2); }
246
+ .bar { display: flex; height: 18px; background: var(--surface-2);
247
+ border: 1px solid var(--border-1); overflow: hidden; }
248
+ .bar .seg { flex: 0 0 auto; color: #fff; font-size: 10px; line-height: 18px;
249
+ text-align: center; overflow: hidden; white-space: nowrap; }
250
+
251
+ .ibar {
252
+ display: inline-block; width: 80px; height: 6px;
253
+ background: var(--surface-2); border: 1px solid var(--border-1);
254
+ vertical-align: middle; margin-left: 6px;
255
+ }
256
+ .ibar > div { height: 100%; background: var(--accent); }
257
+
258
+ .delta { font-variant-numeric: tabular-nums; padding: 2px var(--sp-2);
259
+ text-align: right; }
260
+ .delta-pill {
261
+ display: inline-block; padding: 1px 6px; border-radius: 2px;
262
+ background: var(--surface-2);
263
+ font: 600 var(--fs-small) ui-monospace, monospace;
264
+ }
265
+ .delta.pos, .delta-pill.pos { color: var(--pos); font-weight: 600; }
266
+ .delta.neg, .delta-pill.neg { color: var(--neg); font-weight: 600; }
267
+ .delta.flat, .delta-pill.flat { color: var(--text-3); }
268
+ .delta.new, .delta-pill.new { color: var(--text-3); font-style: italic; }
269
+ .delta.alarm { border-left: 3px solid var(--status-alarm); padding-left: 6px; }
270
+ .delta-latest { border-left: 2px solid var(--border-2); }
271
+
272
+ .legend { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
273
+ margin: 0 0 var(--sp-3);
274
+ font-size: var(--fs-small); color: var(--text-2); }
275
+ .legend .chip { display: inline-flex; align-items: center; gap: 6px; }
276
+ .legend .swatch { display: inline-block; width: 12px; height: 12px;
277
+ border-radius: 2px; }
278
+
279
+ .device-strip {
280
+ font: var(--fs-small) ui-monospace, monospace;
281
+ color: var(--text-2);
282
+ background: var(--surface-2);
283
+ padding: var(--sp-2) var(--sp-3);
284
+ border: 1px solid var(--border-1);
285
+ margin: 0 0 var(--sp-4);
286
+ }
287
+ .ab-strip {
288
+ font: var(--fs-small) ui-monospace, monospace;
289
+ color: var(--text-2);
290
+ background: var(--surface-2);
291
+ padding: var(--sp-2) var(--sp-3);
292
+ border: 1px solid var(--border-2);
293
+ margin: 0 0 var(--sp-4);
294
+ }
295
+
296
+ .rank {
297
+ display: inline-block; width: 1.4em; text-align: center;
298
+ font: 600 var(--fs-small) ui-monospace, monospace;
299
+ margin-right: var(--sp-1);
300
+ color: var(--text-3);
301
+ }
302
+ .rank.rank-1 { color: var(--accent); }
303
+ .rank.rank-2 { color: var(--text-1); }
304
+ .rank.rank-3 { color: var(--text-2); }
305
+
306
+ details.matrix, details.category {
307
+ margin: 0 0 var(--sp-4);
308
+ background: var(--surface-1);
309
+ border: 1px solid var(--border-1);
310
+ }
311
+ details.matrix > summary, details.category > summary {
312
+ cursor: pointer; list-style: none; user-select: none;
313
+ padding: var(--sp-3) var(--sp-4);
314
+ font: 600 var(--fs-body) 'Inter', system-ui, sans-serif;
315
+ color: var(--accent);
316
+ border-bottom: 1px solid transparent;
317
+ display: flex; justify-content: space-between; align-items: baseline;
318
+ gap: var(--sp-3);
319
+ }
320
+ details.matrix[open] > summary, details.category[open] > summary {
321
+ border-bottom-color: var(--border-1);
322
+ }
323
+ details > summary::-webkit-details-marker { display: none; }
324
+ details.matrix > summary::before, details.category > summary::before {
325
+ content: '+'; margin-right: var(--sp-2);
326
+ font-family: ui-monospace, monospace; color: var(--text-3);
327
+ display: inline-block; width: 0.8em;
328
+ }
329
+ details[open] > summary::before { content: '-'; }
330
+ details.matrix > .matrix-body, details.category > .cat-body {
331
+ padding: var(--sp-4);
332
+ }
333
+ .cat-meta { font: var(--fs-small) ui-monospace, monospace;
334
+ color: var(--text-3); font-variant-numeric: tabular-nums; }
335
+ .spark { display: inline-block; vertical-align: middle; color: var(--text-2); }
336
+
337
+ footer.legend {
338
+ font: var(--fs-small) ui-monospace, monospace;
339
+ color: var(--text-2);
340
+ margin-top: var(--sp-12);
341
+ padding-top: var(--sp-3);
342
+ border-top: 1px solid var(--border-1);
343
+ display: block;
344
+ }
345
+
346
+ .note { font-size: var(--fs-small); color: var(--text-2);
347
+ margin-top: var(--sp-2); }
348
+
349
+ .dash-grid {
350
+ display: grid;
351
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
352
+ gap: var(--sp-6);
353
+ margin: 0 0 var(--sp-6);
354
+ }
355
+ a.dash-card { border: 1px solid var(--border-1);
356
+ padding: var(--sp-4); display: flex; flex-direction: column;
357
+ gap: var(--sp-3); text-decoration: none; color: inherit;
358
+ transition: border-color 0.1s, background 0.1s; }
359
+ a.dash-card:hover { background: var(--surface-1); border-color: var(--border-2);
360
+ text-decoration: none; }
361
+ a.dash-card:visited { color: inherit; }
362
+ a.dash-card h3 { margin: 0; color: var(--accent); font-size: var(--fs-h2);
363
+ border-left: 3px solid var(--accent); padding-left: var(--sp-3); }
364
+ a.dash-card table.report { font-size: var(--fs-small); }
365
+ a.dash-card table.report a { pointer-events: none; }
366
+
367
+ a[data-link-kind="primary"] {
368
+ display: inline-flex;
369
+ align-items: center;
370
+ gap: var(--sp-2);
371
+ padding: 6px 12px;
372
+ background: var(--surface-1);
373
+ border: 1px solid var(--border-2);
374
+ color: var(--accent-primary);
375
+ text-decoration: none;
376
+ transition: background var(--motion-hover), border-color var(--motion-hover);
377
+ }
378
+ a[data-link-kind="primary"]:hover {
379
+ background: var(--row-hover);
380
+ border-color: var(--accent-primary);
381
+ text-decoration: none;
382
+ }
383
+ a[data-link-kind="primary"]:visited { color: var(--accent-primary); }
384
+
385
+ a[data-link-kind="inline"] {
386
+ color: var(--accent-primary);
387
+ text-decoration: underline;
388
+ text-underline-offset: 2px;
389
+ text-decoration-thickness: 1px;
390
+ }
391
+ a[data-link-kind="inline"]:hover { text-decoration-thickness: 2px; }
392
+ a[data-link-kind="inline"] .icon {
393
+ margin-left: 3px;
394
+ color: var(--text-2);
395
+ }
396
+
397
+ a[data-link-kind="drill"] {
398
+ color: var(--text-1);
399
+ text-decoration: underline;
400
+ text-decoration-color: var(--border-2);
401
+ text-decoration-thickness: 1px;
402
+ text-underline-offset: 2px;
403
+ }
404
+ a[data-link-kind="drill"]:hover {
405
+ color: var(--accent-primary);
406
+ text-decoration-color: var(--accent-primary);
407
+ text-decoration-thickness: 2px;
408
+ }
409
+ tr:has(a[data-link-kind="drill"]):hover td { background: var(--row-hover); }
410
+
411
+ a[data-link-kind="copy"] {
412
+ display: inline-flex;
413
+ align-items: center;
414
+ justify-content: center;
415
+ width: 24px; height: 24px;
416
+ color: var(--text-2);
417
+ text-decoration: none;
418
+ border-radius: 2px;
419
+ transition: color var(--motion-hover), background var(--motion-hover);
420
+ }
421
+ a[data-link-kind="copy"]:hover {
422
+ color: var(--accent-primary);
423
+ background: var(--row-hover);
424
+ }
425
+
426
+ a[data-link-kind="crumb"] {
427
+ color: var(--text-2);
428
+ text-decoration: none;
429
+ }
430
+ a[data-link-kind="crumb"]:hover {
431
+ color: var(--accent-primary);
432
+ text-decoration: underline;
433
+ }
434
+
435
+ .icon {
436
+ width: 11px; height: 11px;
437
+ display: inline-block;
438
+ vertical-align: -1px;
439
+ fill: currentColor;
440
+ }
441
+
442
+ /* Sticky stack: crumb (top:0) -> summary-bar (top:crumb-h) -> thead (top:hdr-offset).
443
+ --hdr-offset is the combined height of crumb + summary-bar (pages set per page).
444
+ --crumb-h is the crumb height alone. Defaults work for most pages.
445
+ h2 is NOT sticky: the cascading sticky h2 + thead + summary-bar over-stacks
446
+ on long multi-section pages. Sticky thead is sufficient for table reading. */
447
+ body { --hdr-offset: 120px; --crumb-h: 36px; }
448
+ nav.crumb {
449
+ position: sticky; top: 0; z-index: 3;
450
+ background: var(--bg);
451
+ padding-top: var(--sp-1); padding-bottom: var(--sp-1);
452
+ }
453
+ .summary-bar {
454
+ position: sticky; top: var(--crumb-h); z-index: 3;
455
+ display: grid;
456
+ grid-template-columns: minmax(140px, max-content) 1fr auto;
457
+ gap: var(--sp-2) var(--sp-6);
458
+ align-items: center;
459
+ background: var(--surface-1);
460
+ border: 1px solid var(--border-1);
461
+ border-top: 2px solid var(--accent-data);
462
+ padding: var(--sp-3) var(--sp-4);
463
+ margin: 0 0 var(--sp-4);
464
+ }
465
+ .summary-bar .sb-label {
466
+ font: var(--fs-small) ui-monospace, monospace;
467
+ color: var(--text-2);
468
+ text-transform: lowercase;
469
+ letter-spacing: 0.04em;
470
+ }
471
+ .summary-bar .sb-headline {
472
+ font: 600 var(--fs-h1)/1.15 ui-monospace, monospace;
473
+ color: var(--text-1);
474
+ font-variant-numeric: tabular-nums;
475
+ letter-spacing: -0.01em;
476
+ }
477
+ .summary-bar .sb-sub {
478
+ font: var(--fs-small) ui-monospace, monospace;
479
+ color: var(--text-2);
480
+ grid-column: 2;
481
+ font-variant-numeric: tabular-nums;
482
+ }
483
+ .summary-bar .sb-link { align-self: center; grid-row: 1 / span 2; grid-column: 3; }
484
+ .summary-bar.tone-alarm { border-top-color: var(--status-alarm); }
485
+ .summary-bar.tone-ok { border-top-color: var(--status-ok); }
486
+ .summary-bar.tone-warn { border-top-color: var(--status-warn); }
487
+ .summary-bar.tone-info { border-top-color: var(--status-info); }
488
+
489
+ body { container-type: inline-size; container-name: page; }
490
+
491
+ @container page (max-width: 1100px) {
492
+ nav.toc { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
493
+ }
494
+
495
+ @container page (max-width: 860px) {
496
+ .dash-grid { grid-template-columns: 1fr; }
497
+ .summary-bar { grid-template-columns: 1fr auto; }
498
+ .summary-bar .sb-sub { grid-column: 1; }
499
+ .summary-bar .sb-link { grid-row: auto; grid-column: 2; }
500
+ .summary-bar .sb-headline { font-size: var(--fs-h1); }
501
+ }
502
+
503
+ @container page (max-width: 768px) {
504
+ .bar-row {
505
+ grid-template-columns: 1fr;
506
+ grid-template-rows: auto auto auto;
507
+ gap: var(--sp-1);
508
+ }
509
+ .bar-row .total { text-align: left; }
510
+ .kpi-strip { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
511
+ .kpi-chip .kpi-value { font-size: var(--fs-h1); }
512
+ body { padding: var(--sp-3); }
513
+ .summary-bar .sb-headline { font-size: var(--fs-h2); }
514
+ table.report thead th,
515
+ table.report tbody td { padding: var(--sp-1) var(--sp-2); }
516
+ }
517
+
518
+ @media print {
519
+ @page { size: A4; margin: 12mm; }
520
+ :root { color-scheme: light; }
521
+ html, body { background: #fff; color: #000; }
522
+ body { max-width: none; padding: 0; }
523
+
524
+ nav.crumb, .device-strip, .ab-strip,
525
+ rdc-copy-button, rdc-search-cards, rdc-ab-picker { display: none; }
526
+
527
+ .summary-bar {
528
+ position: static;
529
+ background: #fff;
530
+ border-color: #888;
531
+ border-top-width: 3px;
532
+ break-inside: avoid;
533
+ break-after: avoid;
534
+ print-color-adjust: exact;
535
+ }
536
+ .summary-bar .sb-headline { color: #000; }
537
+ .summary-bar .sb-link { display: none; }
538
+
539
+ h1, h2 { color: #000; }
540
+ h2[id] { position: static; background: transparent; break-after: avoid; }
541
+ table.report thead th {
542
+ position: static;
543
+ background: #f0f0f0;
544
+ color: #000;
545
+ print-color-adjust: exact;
546
+ }
547
+ table.report thead { display: table-header-group; }
548
+ table.report tbody tr { break-inside: avoid; }
549
+
550
+ .kpi-strip { break-inside: avoid; grid-template-columns: repeat(4, 1fr); }
551
+ .kpi-chip { background: #fff; border-color: #888; }
552
+ .kpi-chip .kpi-value { color: #000; }
553
+
554
+ .dash-grid { grid-template-columns: repeat(2, 1fr); }
555
+ a.dash-card { break-inside: avoid; }
556
+
557
+ .bar, .bar .seg, .ibar, .ibar > div,
558
+ .legend .swatch, .delta.pos, .delta.neg, .delta-pill {
559
+ print-color-adjust: exact;
560
+ -webkit-print-color-adjust: exact;
561
+ }
562
+
563
+ details, details > summary { display: block; }
564
+ details > .matrix-body, details > .cat-body { display: block; }
565
+
566
+ a[data-link-kind="primary"] {
567
+ background: transparent;
568
+ border: 1px solid #888;
569
+ color: #000;
570
+ }
571
+ a[data-link-kind="inline"] { color: #000; }
572
+ a[data-link-kind="inline"] .icon { display: none; }
573
+
574
+ /* Multi-section trend_table: page break before each h2 section after the first */
575
+ body[data-multi-section="true"] h2[id] ~ h2[id] { break-before: page; }
576
+
577
+ /* Per-drop browser: too large for print; show fallback message */
578
+ body[data-page-kind="drop-browser"] > * { display: none; }
579
+ body[data-page-kind="drop-browser"]::before {
580
+ content: "Per-drop browser is not designed for print. See the cumulative reports.";
581
+ display: block;
582
+ padding: 20mm;
583
+ font: bold 1.2rem system-ui, sans-serif;
584
+ color: #000;
585
+ }
586
+ }
587
+
588
+ rdc-sortable-table { display: block; }
589
+ rdc-sortable-table table.report thead th { cursor: pointer; user-select: none; }
590
+ rdc-sortable-table table.report thead th[aria-sort="ascending"]::after {
591
+ content: ' \25B4'; color: var(--text-3);
592
+ }
593
+ rdc-sortable-table table.report thead th[aria-sort="descending"]::after {
594
+ content: ' \25BE'; color: var(--text-3);
595
+ }
596
+
597
+ rdc-copy-button {
598
+ display: inline-flex; align-items: center; justify-content: center;
599
+ min-width: 28px; padding: 0 6px; height: 22px;
600
+ color: var(--text-2);
601
+ cursor: pointer;
602
+ border: 1px solid transparent;
603
+ border-radius: 2px;
604
+ font: var(--fs-small) ui-monospace, monospace;
605
+ margin-left: 4px;
606
+ transition: color var(--motion-hover), background var(--motion-hover), border-color var(--motion-hover);
607
+ }
608
+ rdc-copy-button:hover {
609
+ color: var(--accent-primary);
610
+ background: var(--row-hover);
611
+ border-color: var(--border-1);
612
+ }
613
+ rdc-copy-button:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 1px; }
614
+ rdc-copy-button::before { content: 'copy'; }
615
+ rdc-copy-button.copied { color: var(--status-ok); border-color: var(--status-ok); }
616
+ rdc-copy-button.copied::before { content: 'ok'; }
617
+
618
+ rdc-heatmap-cell {
619
+ display: inline-block;
620
+ padding: 1px 4px;
621
+ border-radius: 2px;
622
+ font-variant-numeric: tabular-nums;
623
+ }
624
+
625
+ rdc-sticky-h2 { display: contents; }
626
+ rdc-sticky-h2 h2[aria-current="section"] {
627
+ border-left-color: var(--accent-data);
628
+ }
629
+
630
+ rdc-row-drill { display: contents; }
631
+ rdc-row-drill > tr { cursor: pointer; }
632
+ rdc-row-drill > tr:hover td { background: var(--row-hover); }
633
+
634
+ rdc-search-cards {
635
+ display: flex; align-items: center; gap: var(--sp-3);
636
+ margin: 0 0 var(--sp-4);
637
+ font: var(--fs-small) ui-monospace, monospace;
638
+ }
639
+ rdc-search-cards input[type="search"] {
640
+ font: inherit;
641
+ padding: 6px 10px;
642
+ border: 1px solid var(--border-1);
643
+ background: var(--surface-0);
644
+ color: var(--text-1);
645
+ border-radius: 2px;
646
+ min-width: 280px;
647
+ }
648
+ rdc-search-cards input[type="search"]:focus {
649
+ outline: 2px solid var(--accent-primary);
650
+ outline-offset: 1px;
651
+ }
652
+ rdc-search-cards .rdc-count {
653
+ color: var(--text-2);
654
+ font-variant-numeric: tabular-nums;
655
+ }
656
+
657
+ rdc-ab-picker {
658
+ display: inline-flex; align-items: center; gap: var(--sp-2);
659
+ margin: 0 0 var(--sp-4);
660
+ font: var(--fs-small) ui-monospace, monospace;
661
+ }
662
+ rdc-ab-picker label { color: var(--text-2); }
663
+ rdc-ab-picker select {
664
+ font: inherit;
665
+ padding: 4px 8px;
666
+ border: 1px solid var(--border-1);
667
+ background: var(--surface-0);
668
+ color: var(--text-1);
669
+ border-radius: 2px;
670
+ }
671
+
672
+ rdc-alarm-banner { display: contents; }
673
+
674
+ /* Chip cluster: wrap N primary chips in flex row, no table abuse */
675
+ .chip-cluster {
676
+ display: flex; flex-wrap: wrap; gap: var(--sp-2);
677
+ margin: 0 0 var(--sp-4);
678
+ }
679
+ .chip-cluster a[data-link-kind="primary"] { padding: 4px 10px; font-size: var(--fs-small); }
680
+
681
+ /* Pair list: grouped variant chips per A/B pair */
682
+ .pair-list { display: flex; flex-direction: column; gap: var(--sp-4); margin: 0 0 var(--sp-6); }
683
+ .pair-group { border: 1px solid var(--border-1); padding: var(--sp-3) var(--sp-4); background: var(--surface-1); }
684
+ .pair-group > h3 {
685
+ margin: 0 0 var(--sp-3); padding: 0;
686
+ font: var(--fs-mono) ui-monospace, monospace;
687
+ color: var(--text-2);
688
+ font-weight: 500;
689
+ text-transform: none;
690
+ letter-spacing: 0;
691
+ }
692
+
693
+ /* Catalog grid: flex-wrap chip area for report shortcuts */
694
+ .catalog-grid {
695
+ display: grid;
696
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
697
+ gap: var(--sp-2);
698
+ margin: 0 0 var(--sp-6);
699
+ }
700
+ .catalog-grid a[data-link-kind="primary"] { justify-content: flex-start; }
701
+
702
+ :root { --label: #4a6a3a; --th-bg: var(--surface-2); --th-bg-active: var(--row-hover); }
703
+ @media (prefers-color-scheme: dark) {
704
+ :root { --label: #a3d39c; }
705
+ }
706
+
707
+ body { max-width: 1800px; }
708
+
709
+ nav.toc a { display: flex; justify-content: space-between; padding: 2px 0; gap: 1rem; }
710
+ nav.toc a .ct { color: var(--text-3); font-variant-numeric: tabular-nums; }
711
+
712
+ .controls {
713
+ display: flex; gap: var(--sp-3); align-items: center;
714
+ font-size: var(--fs-small); flex-wrap: wrap;
715
+ margin: var(--sp-2) 0 var(--sp-2);
716
+ }
717
+ .controls input[type=search] {
718
+ font: inherit; padding: 4px 8px;
719
+ border: 1px solid var(--border-2); background: var(--surface-0); color: var(--text-1);
720
+ border-radius: 2px; min-width: 22rem;
721
+ }
722
+ .controls input[type=search]:focus { outline: 1px solid var(--accent); }
723
+ .controls .ct { color: var(--text-2); font-variant-numeric: tabular-nums; }
724
+ .controls .dl { font-size: var(--fs-small); color: var(--accent); }
725
+
726
+ .table-scroll {
727
+ height: 60vh; overflow: auto;
728
+ border: 1px solid var(--border-1);
729
+ background: var(--surface-0);
730
+ position: relative;
731
+ }
732
+ .table-scroll.short { height: auto; max-height: 60vh; }
733
+ table.data {
734
+ border-collapse: separate; border-spacing: 0;
735
+ font: var(--fs-mono) ui-monospace, 'Cascadia Code', Consolas, monospace;
736
+ width: max-content; min-width: 100%; table-layout: auto;
737
+ }
738
+ table.data thead th {
739
+ position: sticky; top: 0; z-index: 2;
740
+ background: var(--th-bg);
741
+ text-align: left; cursor: pointer; user-select: none;
742
+ color: var(--accent); font-weight: 600;
743
+ padding: 4px 8px;
744
+ border-bottom: 1px solid var(--border-2);
745
+ white-space: nowrap;
746
+ }
747
+ table.data thead th:hover { background: var(--th-bg-active); }
748
+ table.data thead th .sort-arrow { display: inline-block; width: 10px; color: var(--text-3); }
749
+ table.data thead th.numeric, table.data tbody td.numeric {
750
+ text-align: right; font-variant-numeric: tabular-nums;
751
+ }
752
+ table.data tbody td {
753
+ padding: 2px 8px;
754
+ border-bottom: 1px solid var(--border-1);
755
+ vertical-align: top; white-space: nowrap;
756
+ max-width: 380px; overflow: hidden; text-overflow: ellipsis;
757
+ background: var(--surface-0);
758
+ }
759
+ table.data tbody tr.alt td { background: var(--surface-1); }
760
+ table.data tbody tr:hover td { background: var(--row-hover); }
761
+ table.data tbody td .lbl {
762
+ color: var(--label); margin-left: 6px;
763
+ font-style: italic; opacity: .85;
764
+ }
765
+ table.data tbody td a {
766
+ color: inherit; text-decoration: none;
767
+ border-bottom: 1px dotted var(--accent);
768
+ }
769
+ table.data tbody td a:hover { color: var(--accent); border-bottom-style: solid; }
770
+ .spacer td { padding: 0; border: 0; background: var(--surface-0); }
771
+
772
+ .sidecar-list a { font-family: ui-monospace, monospace; font-size: var(--fs-small); }
773
+ .sidecar-list span { color: var(--text-2); margin-left: .4rem; font-size: var(--fs-small); }
774
+ ul.sidecar-list { list-style: none; padding: 0; margin: var(--sp-2) 0;
775
+ columns: 5; column-gap: var(--sp-6); column-rule: 1px solid var(--border-1); }
776
+ ul.sidecar-list li { padding: 1px 0; break-inside: avoid; }
777
+
778
+ /* Per-drop table sections: inline (no full card chrome) */
779
+ section.table-section {
780
+ margin: 0 0 var(--sp-4);
781
+ }
782
+ section.table-section > header.table-header {
783
+ display: flex; align-items: baseline; justify-content: space-between;
784
+ gap: var(--sp-3); margin: 0 0 var(--sp-2);
785
+ padding: 0 0 0 var(--sp-3);
786
+ border-left: 3px solid var(--border-2);
787
+ }
788
+ section.table-section > header.table-header h2 {
789
+ margin: 0; padding: 0; border: 0;
790
+ font-size: var(--fs-h3); color: var(--text-1);
791
+ }
792
+ section.table-section .table-meta {
793
+ font: var(--fs-small) ui-monospace, monospace;
794
+ color: var(--text-3);
795
+ }
796
+
797
+ details.category > .cat-body {
798
+ background: var(--surface-0);
799
+ }
800
+ </style></head><body style="--hdr-offset: 120px">
801
+ <header class="strip">
802
+ <span>built <strong><TS></strong></span>
803
+ <span>drops <strong>2</strong></span>
804
+ </header>
805
+ <aside class="summary-bar tone-neutral" aria-label="page summary"><div class="sb-label">latest drop</div><div class="sb-headline">2026-05-28 / r110600</div><div class="sb-sub">1 areas; 10 catalog rows</div><a class="sb-link" href="_reports/index.html" data-link-kind="primary">dashboard</a></aside>
806
+ <section><h2 id="dashboard">dashboard</h2><div class="chip-cluster"><a href="_reports/index.html" data-link-kind="primary">cumulative reports dashboard</a></div></section>
807
+ <section><h2 id="reports">reports</h2><div class="catalog-grid">
808
+ <a href="_reports/draws_by_class.html" data-link-kind="primary">draws_by_class</a>
809
+ <a href="_reports/instancing_opportunities.html" data-link-kind="primary">instancing_opportunities</a>
810
+ <a href="_reports/overdraw.html" data-link-kind="primary">overdraw</a>
811
+ <a href="_reports/pass_gpu.html" data-link-kind="primary">pass_gpu</a>
812
+ <a href="_reports/shader_hotlist.html" data-link-kind="primary">shader_hotlist</a>
813
+ <a href="_reports/trend_table.html" data-link-kind="primary">trend_table</a>
814
+ </div></section>
815
+ <section><h2>catalog</h2>
816
+ <div class="controls">
817
+ <input type="search" placeholder="filter">
818
+ <span class="ct visible-count"></span>
819
+ <a class="dl" href="_data/_catalog.csv" data-link-kind="inline">CSV</a>
820
+ <a class="dl" href="_data/_catalog.parquet" data-link-kind="inline">parquet</a>
821
+ </div>
822
+ <div class="table-scroll" data-table="catalog"></div>
823
+ </section>
824
+ <script>window.__data_catalog={"cols":["area","drop_date","drop_label","capture","schema_version","build_timestamp","replay_status","row_count_draws","row_count_events","row_count_shaders","row_count_textures","row_count_render_targets","row_count_buffers","row_count_programs","row_count_samplers","row_count_fbos","row_count_state_change_events","row_count_counters_per_event","row_count_descriptor_access","row_count_passes","row_count_frame_totals","row_count_clears","row_count_dispatches","row_count_rt_event_timeline","row_count_vertex_inputs","row_count_resource_creation","row_count_draw_bindings","row_count_program_transitions","row_count_pixel_history","row_count_vbo_samples","row_count_ibo_samples","row_count_post_vs_samples","row_count_texture_samples","row_count_indirect_args","row_count_pass_class_breakdown","row_count_texture_usage","analysis_out_path"],"rows":[["District 01","2026-05-27","r110565","1",3,"2026-01-01T00:00:00+00:00","ok",60,60,60,60,9,60,60,11,13,60,60,60,60,1,8,9,60,60,60,60,60,60,60,60,60,60,0,0,0,"_reports/drill/District 01/2026-05-27_r110565/index.html"],["District 01","2026-05-27","r110565","2",3,"2026-01-01T00:00:00+00:00","ok",0,0,0,0,9,0,0,11,13,0,0,0,0,1,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,"_reports/drill/District 01/2026-05-27_r110565/index.html"],["District 01","2026-05-27","r110565","3",3,"2026-01-01T00:00:00+00:00","ok",0,0,0,0,9,0,0,11,13,0,0,0,0,1,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,"_reports/drill/District 01/2026-05-27_r110565/index.html"],["District 01","2026-05-27","r110565","4",3,"2026-01-01T00:00:00+00:00","ok",0,0,0,0,9,0,0,11,13,0,0,0,0,1,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,"_reports/drill/District 01/2026-05-27_r110565/index.html"],["District 01","2026-05-27","r110565","5",3,"2026-01-01T00:00:00+00:00","ok",0,0,0,0,9,0,0,11,8,0,0,0,0,1,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,"_reports/drill/District 01/2026-05-27_r110565/index.html"],["District 01","2026-05-28","r110600","1",3,"2026-01-01T00:00:00+00:00","ok",60,60,60,60,9,60,60,11,13,60,60,60,60,1,8,9,60,60,60,60,60,60,60,60,60,60,0,60,1,"_reports/drill/District 01/2026-05-28_r110600/index.html"],["District 01","2026-05-28","r110600","2",3,"2026-01-01T00:00:00+00:00","ok",0,0,0,0,9,0,0,11,13,0,0,0,0,1,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,"_reports/drill/District 01/2026-05-28_r110600/index.html"],["District 01","2026-05-28","r110600","3",3,"2026-01-01T00:00:00+00:00","ok",0,0,0,0,9,0,0,11,13,0,0,0,0,1,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,"_reports/drill/District 01/2026-05-28_r110600/index.html"],["District 01","2026-05-28","r110600","4",3,"2026-01-01T00:00:00+00:00","ok",0,0,0,0,9,0,0,11,13,0,0,0,0,1,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,"_reports/drill/District 01/2026-05-28_r110600/index.html"],["District 01","2026-05-28","r110600","5",3,"2026-01-01T00:00:00+00:00","ok",0,0,0,0,9,0,0,11,8,0,0,0,0,1,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,"_reports/drill/District 01/2026-05-28_r110600/index.html"]],"labelCols":{}};</script>
825
+ <script>window.__labels={};</script>
826
+ <script>
827
+ (function(){
828
+ const ROW_H = 22;
829
+ const BUFFER = 8;
830
+
831
+ // For ID kinds: where to jump when an ID cell is clicked
832
+ const LINK_TARGET = {
833
+ shader: { table: 'shaders', col: 'shader_id' },
834
+ program: { table: 'programs', col: 'program_id' },
835
+ texture: { table: 'textures', col: 'tex_id' },
836
+ sampler: { table: 'samplers', col: 'sampler_id' },
837
+ buffer: { table: 'buffers', col: 'buffer_id' },
838
+ fbo: { table: 'fbos', col: 'fbo_id' },
839
+ };
840
+
841
+ function isNumeric(v){
842
+ return v != null && (typeof v === 'number' || (typeof v === 'string' && /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(v)));
843
+ }
844
+ function fmt(v){
845
+ if (v == null) return '';
846
+ if (typeof v === 'number'){
847
+ if (v === 0) return '0';
848
+ if (Math.abs(v) < 1e-4 || Math.abs(v) >= 1e7) return v.toExponential(4);
849
+ return (Math.round(v * 1e6) / 1e6).toString();
850
+ }
851
+ return String(v);
852
+ }
853
+
854
+ function lookupLabel(labels, kind, id){
855
+ if (!labels || !kind || id == null || id === '' || id === 0 || id === '0') return '';
856
+ const k = String(id);
857
+ const cap = labels.capture;
858
+ if (!cap || !labels.by_capture || !labels.by_capture[cap]) return '';
859
+ const buckets = labels.by_capture[cap];
860
+ if (kind === 'auto_by_slot_kind' || kind === 'auto_by_kind') return '';
861
+ if (kind === 'texture_list') return '';
862
+ return (buckets[kind] && buckets[kind][k]) || '';
863
+ }
864
+
865
+ function autoKindForSlot(slotKind){
866
+ if (slotKind === 'texture') return 'texture';
867
+ if (slotKind === 'sampler') return 'sampler';
868
+ if (slotKind === 'ubo' || slotKind === 'ssbo') return 'buffer';
869
+ return '';
870
+ }
871
+ function autoKindForDescriptor(descriptorKind){
872
+ if (descriptorKind === 'ReadOnlyResource' || descriptorKind === 'ImageSampler' || descriptorKind === 'TypedBuffer') return 'texture';
873
+ if (descriptorKind === 'Sampler') return 'sampler';
874
+ if (descriptorKind === 'ConstantBuffer' || descriptorKind === 'ReadWriteResource' || descriptorKind === 'ReadWriteBuffer') return 'buffer';
875
+ return '';
876
+ }
877
+
878
+ class VTable {
879
+ constructor(host, payload, labels){
880
+ this.host = host;
881
+ this.cols = payload.cols;
882
+ this.rows = payload.rows;
883
+ this.labelCols = payload.labelCols || {};
884
+ this.labels = labels;
885
+ this.view = this.rows.slice();
886
+ this.sortCol = -1;
887
+ this.sortDir = 1;
888
+
889
+ // detect numeric columns from first 50 non-null cells
890
+ this.numericCols = new Set();
891
+ for (let ci = 0; ci < this.cols.length; ci++){
892
+ let count = 0, num = 0;
893
+ for (let ri = 0; ri < this.rows.length && count < 50; ri++){
894
+ const v = this.rows[ri][ci];
895
+ if (v == null || v === '') continue;
896
+ count++;
897
+ if (typeof v === 'number' || (typeof v === 'string' && /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(v))) num++;
898
+ }
899
+ if (count > 0 && num / count > 0.7) this.numericCols.add(ci);
900
+ }
901
+
902
+ // detect slot_kind column for draw_bindings (resource_id auto-label)
903
+ this.slotKindCol = this.cols.indexOf('slot_kind');
904
+ this.descriptorKindCol = this.cols.indexOf('descriptor_kind');
905
+
906
+ this.build();
907
+ }
908
+
909
+ build(){
910
+ const table = document.createElement('table');
911
+ table.className = 'data';
912
+ const thead = table.createTHead();
913
+ const tr = thead.insertRow();
914
+ for (let i = 0; i < this.cols.length; i++){
915
+ const th = document.createElement('th');
916
+ const txt = document.createTextNode(this.cols[i]);
917
+ th.appendChild(txt);
918
+ if (this.numericCols.has(i)) th.classList.add('numeric');
919
+ const arrow = document.createElement('span');
920
+ arrow.className = 'sort-arrow';
921
+ th.appendChild(arrow);
922
+ th.addEventListener('click', () => this.sort(i));
923
+ tr.appendChild(th);
924
+ }
925
+ const tbody = document.createElement('tbody');
926
+ const sTop = document.createElement('tr');
927
+ const sBot = document.createElement('tr');
928
+ sTop.className = 'spacer'; sBot.className = 'spacer';
929
+ const tdTop = document.createElement('td');
930
+ const tdBot = document.createElement('td');
931
+ tdTop.colSpan = this.cols.length;
932
+ tdBot.colSpan = this.cols.length;
933
+ sTop.appendChild(tdTop); sBot.appendChild(tdBot);
934
+ tbody.appendChild(sTop); tbody.appendChild(sBot);
935
+ table.appendChild(tbody);
936
+ this.host.appendChild(table);
937
+
938
+ this.tbody = tbody;
939
+ this.sTop = sTop;
940
+ this.sBot = sBot;
941
+ this.tdTop = tdTop;
942
+ this.tdBot = tdBot;
943
+
944
+ this.host.addEventListener('scroll', () => this.render());
945
+ window.addEventListener('resize', () => this.render());
946
+ // Re-render when a containing <details class="category"> opens.
947
+ const detailsEl = this.host.closest('details');
948
+ if (detailsEl){
949
+ detailsEl.addEventListener('toggle', () => {
950
+ if (detailsEl.open){
951
+ requestAnimationFrame(() => this.render());
952
+ setTimeout(() => this.render(), 50);
953
+ }
954
+ });
955
+ }
956
+ this.render();
957
+ // Re-render after layout settles (initial clientHeight can be 0)
958
+ requestAnimationFrame(() => this.render());
959
+ // And once more after fonts/sizes stabilize
960
+ setTimeout(() => this.render(), 50);
961
+ }
962
+
963
+ sort(ci){
964
+ if (this.sortCol === ci) this.sortDir = -this.sortDir;
965
+ else { this.sortCol = ci; this.sortDir = 1; }
966
+ const dir = this.sortDir;
967
+ const isNum = this.numericCols.has(ci);
968
+ this.view.sort((a, b) => {
969
+ const aa = a[ci], bb = b[ci];
970
+ if (aa == null && bb == null) return 0;
971
+ if (aa == null) return 1;
972
+ if (bb == null) return -1;
973
+ if (isNum){
974
+ const na = +aa, nb = +bb;
975
+ return (na - nb) * dir;
976
+ }
977
+ return String(aa).localeCompare(String(bb)) * dir;
978
+ });
979
+ const headers = this.host.querySelectorAll('thead th');
980
+ for (let i = 0; i < headers.length; i++){
981
+ const a = headers[i].querySelector('.sort-arrow');
982
+ if (a) a.textContent = (i === ci) ? (dir > 0 ? ' ▲' : ' ▼') : '';
983
+ }
984
+ this.host.scrollTop = 0;
985
+ this.render();
986
+ }
987
+
988
+ filter(query){
989
+ const q = (query || '').trim().toLowerCase();
990
+ if (!q){
991
+ this.view = this.rows.slice();
992
+ } else {
993
+ const labels = this.labels;
994
+ const labelCols = this.labelCols;
995
+ const cols = this.cols;
996
+ const slotKindCol = this.slotKindCol;
997
+ const descriptorKindCol = this.descriptorKindCol;
998
+ this.view = this.rows.filter(r => {
999
+ for (let i = 0; i < r.length; i++){
1000
+ const v = r[i];
1001
+ if (v == null) continue;
1002
+ if (String(v).toLowerCase().indexOf(q) >= 0) return true;
1003
+ // also match against the resolved label, if any
1004
+ const lc = labelCols[cols[i]];
1005
+ if (!lc || v === 0 || v === '0' || v === '') continue;
1006
+ let kind = lc;
1007
+ if (kind === 'auto_by_slot_kind' && slotKindCol >= 0) kind = autoKindForSlot(r[slotKindCol]);
1008
+ else if (kind === 'auto_by_kind' && descriptorKindCol >= 0) kind = autoKindForDescriptor(r[descriptorKindCol]);
1009
+ if (kind === 'texture_list'){
1010
+ const ids = String(v).split(';').filter(x => x);
1011
+ for (const id of ids){
1012
+ const lbl = lookupLabel(labels, 'texture', id);
1013
+ if (lbl && lbl.toLowerCase().indexOf(q) >= 0) return true;
1014
+ }
1015
+ } else if (kind){
1016
+ const lbl = lookupLabel(labels, kind, v);
1017
+ if (lbl && lbl.toLowerCase().indexOf(q) >= 0) return true;
1018
+ }
1019
+ }
1020
+ return false;
1021
+ });
1022
+ }
1023
+ // resort if a sort was active
1024
+ if (this.sortCol >= 0){
1025
+ const ci = this.sortCol, dir = this.sortDir;
1026
+ const isNum = this.numericCols.has(ci);
1027
+ this.view.sort((a, b) => {
1028
+ const aa = a[ci], bb = b[ci];
1029
+ if (aa == null && bb == null) return 0;
1030
+ if (aa == null) return 1;
1031
+ if (bb == null) return -1;
1032
+ if (isNum) return (+aa - +bb) * dir;
1033
+ return String(aa).localeCompare(String(bb)) * dir;
1034
+ });
1035
+ }
1036
+ this.host.scrollTop = 0;
1037
+ this.render();
1038
+ }
1039
+
1040
+ cellNode(value, ri, ci){
1041
+ const td = document.createElement('td');
1042
+ if (this.numericCols.has(ci)) td.classList.add('numeric');
1043
+
1044
+ const colName = this.cols[ci];
1045
+ const lc = this.labelCols[colName];
1046
+ let kind = lc;
1047
+ if (kind === 'auto_by_slot_kind' && this.slotKindCol >= 0){
1048
+ kind = autoKindForSlot(this.view[ri][this.slotKindCol]);
1049
+ } else if (kind === 'auto_by_kind' && this.descriptorKindCol >= 0){
1050
+ kind = autoKindForDescriptor(this.view[ri][this.descriptorKindCol]);
1051
+ }
1052
+
1053
+ // primary cell value: cross-link if we have a target for this kind
1054
+ const formatted = fmt(value);
1055
+ const link = LINK_TARGET[kind];
1056
+ if (link && value != null && value !== '' && value !== 0 && value !== '0' && kind !== 'texture_list'){
1057
+ const a = document.createElement('a');
1058
+ a.href = '#' + link.table;
1059
+ a.textContent = formatted;
1060
+ a.title = 'jump to ' + link.table + ' filtered to ' + link.col + '=' + value;
1061
+ a.addEventListener('click', (ev) => {
1062
+ ev.preventDefault();
1063
+ jumpToTable(link.table, String(value));
1064
+ });
1065
+ td.appendChild(a);
1066
+ } else {
1067
+ td.textContent = formatted;
1068
+ }
1069
+
1070
+ // label enrichment (appears after the value/link)
1071
+ if (lc && value != null && value !== '' && value !== 0 && value !== '0'){
1072
+ if (kind === 'texture_list'){
1073
+ const ids = String(value).split(';').filter(x => x);
1074
+ const labels = ids.map(id => lookupLabel(this.labels, 'texture', id))
1075
+ .filter(x => x);
1076
+ if (labels.length){
1077
+ const span = document.createElement('span');
1078
+ span.className = 'lbl';
1079
+ span.textContent = labels.join(', ');
1080
+ td.appendChild(span);
1081
+ }
1082
+ } else if (kind){
1083
+ const label = lookupLabel(this.labels, kind, value);
1084
+ if (label){
1085
+ const span = document.createElement('span');
1086
+ span.className = 'lbl';
1087
+ span.textContent = label;
1088
+ td.appendChild(span);
1089
+ }
1090
+ }
1091
+ }
1092
+ return td;
1093
+ }
1094
+
1095
+ render(){
1096
+ const scrollTop = this.host.scrollTop;
1097
+ const height = this.host.clientHeight || 600;
1098
+ const len = this.view.length;
1099
+ const start = Math.max(0, Math.floor(scrollTop / ROW_H) - BUFFER);
1100
+ const end = Math.min(len, Math.ceil((scrollTop + height) / ROW_H) + BUFFER);
1101
+
1102
+ // clear data rows between spacers
1103
+ while (this.sTop.nextSibling !== this.sBot){
1104
+ this.tbody.removeChild(this.sTop.nextSibling);
1105
+ }
1106
+ // height on <tr> directly; <td> alone doesn't make a row tall in all browsers
1107
+ this.sTop.style.height = (start * ROW_H) + 'px';
1108
+ this.sBot.style.height = ((len - end) * ROW_H) + 'px';
1109
+ this.tdTop.style.height = (start * ROW_H) + 'px';
1110
+ this.tdBot.style.height = ((len - end) * ROW_H) + 'px';
1111
+
1112
+ const frag = document.createDocumentFragment();
1113
+ for (let i = start; i < end; i++){
1114
+ const tr = document.createElement('tr');
1115
+ tr.style.height = ROW_H + 'px';
1116
+ if (i % 2 === 1) tr.className = 'alt';
1117
+ const row = this.view[i];
1118
+ for (let ci = 0; ci < this.cols.length; ci++){
1119
+ tr.appendChild(this.cellNode(row[ci], i, ci));
1120
+ }
1121
+ frag.appendChild(tr);
1122
+ }
1123
+ this.tbody.insertBefore(frag, this.sBot);
1124
+ }
1125
+ }
1126
+
1127
+ function jumpToTable(tableName, idValue){
1128
+ const host = document.querySelector('div.table-scroll[data-table="' + tableName + '"]');
1129
+ if (!host || !host._vt) return;
1130
+ const section = host.closest('section');
1131
+ const input = section ? section.querySelector('input[type=search]') : null;
1132
+ if (input){
1133
+ input.value = idValue;
1134
+ // trigger filter immediately (no debounce on programmatic set)
1135
+ host._vt.filter(idValue);
1136
+ const counter = section.querySelector('.ct.visible-count');
1137
+ if (counter){
1138
+ const v = host._vt.view.length, t = host._vt.rows.length;
1139
+ counter.textContent = v.toLocaleString() + ' / ' + t.toLocaleString() + ' visible';
1140
+ }
1141
+ }
1142
+ section.scrollIntoView({behavior: 'smooth', block: 'start'});
1143
+ }
1144
+ window.__jumpToTable = jumpToTable;
1145
+
1146
+ window.addEventListener('DOMContentLoaded', () => {
1147
+ const labels = window.__labels || {};
1148
+ document.querySelectorAll('div.table-scroll[data-table]').forEach(host => {
1149
+ const name = host.dataset.table;
1150
+ const payload = window['__data_' + name];
1151
+ if (!payload){ return; }
1152
+ const labelsForTable = Object.assign({}, labels);
1153
+ // Per-section: capture from data is row-level; use the section's data-capture if set
1154
+ labelsForTable.capture = host.dataset.capture || (payload.rows[0] ? payload.rows[0][3] : '');
1155
+ const vt = new VTable(host, payload, labelsForTable);
1156
+ host._vt = vt;
1157
+
1158
+ const section = host.closest('section');
1159
+ const input = section.querySelector('input[type=search]');
1160
+ const counter = section.querySelector('.ct.visible-count');
1161
+ function updateCounter(){
1162
+ const v = vt.view.length, t = vt.rows.length;
1163
+ counter.textContent = v.toLocaleString() + ' / ' + t.toLocaleString() + ' visible';
1164
+ }
1165
+ updateCounter();
1166
+ let timer = null;
1167
+ input.addEventListener('input', () => {
1168
+ clearTimeout(timer);
1169
+ timer = setTimeout(() => { vt.filter(input.value); updateCounter(); }, 80);
1170
+ });
1171
+ });
1172
+ });
1173
+ })();
1174
+ </script>
1175
+ </body></html>