evalstate HF Staff commited on
Commit
19a8efc
·
verified ·
1 Parent(s): ac26316

One 'output not in trace' measure on the leaderboard; gaps table grouped; row page two-column layout

Browse files
Files changed (4) hide show
  1. home.js +52 -45
  2. index.html +9 -2
  3. row.js +6 -2
  4. style.css +17 -1
home.js CHANGED
@@ -44,7 +44,6 @@
44
  const h = r.scan.holes, m = h.models, out = [];
45
  if (m.mismatch) out.push(["bad", "served " + shortModel(m.served[0].model)]);
46
  else if (m.foreignTrials) out.push(["warn", "fallback " + m.served.filter(s => !s.configured).map(s => shortModel(s.model).replace(/^claude-/, "")).join(", ") + " " + pct0(m.foreignStepShare)]);
47
- if (h.text.uncounted) out.push(["bad", "output ≥" + pct0(h.text.uncounted) + " unbilled"]);
48
  const off = h.lbCost && Math.abs(h.costSum - h.lbCost) / h.lbCost > 0.02;
49
  if (off) out.push(["warn", "trial costs " + money(h.costSum) + "/" + money(h.lbCost)]);
50
  else if (h.uncosted / r.scan.trials > 0.05) out.push(["warn", h.uncosted + " uncosted"]);
@@ -57,17 +56,15 @@
57
  const leadsOnly = $("leads-only");
58
  const SORTS = {
59
  rank: r => r.rank, score: r => -r.lb.accuracy, leads: r => -r.scan.leads, hacks: r => -(r.lb.rewardHacks || 0),
60
- trace: r => visibleShare(r.scan.holes) ?? 0, cost: r => -(r.lb.cost || 0),
61
  };
62
  function board() {
63
  const cols = [
64
  { name: "89 tasks × 5 attempts, easiest first", cls: "l" },
65
  { name: "Score", sort: "score", help: "leaderboard accuracy ± 95% CI half-width" },
66
- { name: "Rewarded", help: "rewarded trials of the row's listed trials" },
67
  { name: "Leads", sort: "leads", help: "rewarded trials with an unexcused high or critical finding" },
68
  { name: "LB hacks", sort: "hacks", help: "leaderboard's own reward-hack rate" },
69
- { name: "In trace", sort: "trace", help: "share of output tokens with text in the trace" },
70
- { name: "Reasoning", help: "how much reasoning the trajectories hold" },
71
  { name: "Cost", sort: "cost", help: "leaderboard total" },
72
  { name: "Notes", cls: "l" },
73
  ];
@@ -78,18 +75,19 @@
78
  const h = r.scan.holes, tr = el("tr");
79
  tr.append(rowHead(r));
80
  const s = el("td", "l"); s.append(strip(r, { leadsOnly: leadsOnly.checked })); tr.append(s);
81
- const sc = el("td"); sc.append(num(fmt1(r.lb.accuracy), "n big"), el("span", "pm", "±" + fmt1(r.lb.ci95))); tr.append(sc);
82
- tr.append(el("td", "n", r.scan.rewarded + "/" + r.scan.trials));
83
  const ld = el("td");
84
  if (r.scan.leads) ld.append(link(rowPath(r) + "highlights/index.html", String(r.scan.leads), "n big hot"));
85
  else ld.append(el("span", "z", "0"));
86
  tr.append(ld);
87
  tr.append(el("td", "n" + (r.lb.rewardHacks >= 1 ? " hot" : ""), fmt1(r.lb.rewardHacks) + "%"));
88
- const vis = visibleShare(h), tc = el("td", "vis");
89
- const bar = el("span", "bar"); const fill = el("i"); fill.style.width = Math.round(100 * (vis ?? 0)) + "%"; bar.append(fill);
90
- tc.append(bar, el("span", "n", pct0(vis)));
 
 
91
  tr.append(tc);
92
- const rz = el("td", "rz rz--" + h.text.form, RZ[h.text.form]); rz.title = RZ_HELP[h.text.form]; tr.append(rz);
93
  tr.append(el("td", "n", money(r.lb.cost)));
94
  const nt = el("td", "l notes");
95
  flags(r).forEach(([lvl, text]) => nt.append(el("span", "fl fl--" + lvl, text)));
@@ -106,31 +104,38 @@
106
  }
107
  leadsOnly.addEventListener("change", board);
108
 
109
- /* 2. Gaps & accounting. */
110
  function gaps() {
111
- const cols = [
112
- { name: "Models served", cls: "l", help: "by share of steps; red: not the configured model" },
113
- { name: "Reasoning", help: "form of the reasoning in the trajectories" },
114
- { name: "Text / tok", help: "recorded characters per output token (≈3.2 = all output in the trace)" },
115
- { name: "In trace", help: "share of output tokens with text in the trace" },
116
- { name: "Reas. tok", help: "reasoning tokens as a share of output, where reported" },
117
- { name: "Low-text trials", help: "trials whose text covers under 80% of their output" },
118
- { name: "Web results missing", help: "trials with web activity whose results aren't in the trace / trials with web activity" },
119
- { name: "Web prov.", help: "web input or reference provenance unresolved" },
120
- { name: "Unpaired results", help: "tool results not paired to their calls" },
121
- { name: "Calls missing", help: "token totals exceed the recorded model calls" },
122
- { name: "Ends on call", help: "trace ends on a tool call with no result" },
123
- { name: "Compacted", help: "history compacted into a summary" },
124
- { name: "No traj.", help: "listed trial without a trajectory" },
125
- { name: "Time­stamps", help: "missing or out of order" },
126
- { name: "Trial cost / LB", help: "sum of per-trial recorded cost against the leaderboard total" },
127
- { name: "Uncosted", help: "trials without a recorded cost" },
128
- { name: "Output unbilled", help: "recorded reasoning not counted in output tokens, as a share of reported output" },
129
- { name: "Undecided", help: "trials with at least one behavioural check atif-scan couldn't decide" },
130
  ];
 
 
 
 
 
 
 
 
 
 
131
  const t = $("gaps-table");
132
- t.replaceChildren(headRow(cols, "Row"));
133
  const tb = el("tbody");
 
134
  D.rows.forEach(r => {
135
  const h = r.scan.holes, n = r.scan.trials, m = h.models, tr = el("tr");
136
  tr.append(rowHead(r));
@@ -138,24 +143,26 @@
138
  m.served.filter(x => x.share >= 0.005).slice(0, 3).forEach(x =>
139
  md.append(el("span", x.configured ? "mdl" : "mdl off", shortModel(x.model) + (m.served.length > 1 ? " " + pct0(x.share) : ""))));
140
  if (m.foreignTrials) md.append(link(holeHref(r, "foreign"), m.foreignTrials + " trials", "mlink"));
141
- tr.append(md);
142
- const rz = el("td", "rz rz--" + h.text.form, RZ[h.text.form]); rz.title = RZ_HELP[h.text.form]; tr.append(rz);
143
- tr.append(el("td", "n", String(h.text.allCpt)));
144
- const vis = visibleShare(h), vc = el("td", "n", pct0(vis)); heat(vc, 1 - (vis ?? 1), 1, "cool"); tr.append(vc);
145
  tr.append(el("td", "n", h.text.reasoningShare !== null ? pct0(h.text.reasoningShare) : "–"));
146
- tr.append(countCell(r, "unseen", h.unseen, n, { scale: 1, hue: "cool" }));
147
- tr.append(h.webTrials ? countCell(r, "webUnrecorded", h.webUnrecorded, n, { of: h.webTrials, scale: 1 }) : el("td", "c z", "no web"));
 
 
 
 
 
148
  ["webUnresolved", "unpaired", "callsMissing", "endsOnCall", "compacted", "noTrajectory", "timestamps"]
149
  .forEach(k => tr.append(countCell(r, k, h[k], n)));
 
150
  const off = h.lbCost ? h.costSum - h.lbCost : 0;
151
- const cost = el("td", "n" + (Math.abs(off) / (h.lbCost || 1) > 0.02 ? " bad" : ""),
152
- Math.abs(off) >= 0.5 ? money(h.costSum) + " / " + money(h.lbCost) : money(h.costSum) + " ="); tr.append(cost);
153
  tr.append(countCell(r, "uncosted", h.uncosted, n, { scale: 0.3 }));
154
- const ub = el("td", "c");
155
- if (h.text.uncounted) { const a = link(holeHref(r, "uncounted"), "≥" + pct0(h.text.uncounted), "bad"); ub.append(a); }
156
- else ub.append(el("span", "z", "·"));
157
- tr.append(ub);
158
- const und = el("td", "n", String(h.evidenceIncomplete)); heat(und, h.evidenceIncomplete / n, 1.5, "cool"); tr.append(und);
159
  tb.append(tr);
160
  });
161
  t.append(tb);
 
44
  const h = r.scan.holes, m = h.models, out = [];
45
  if (m.mismatch) out.push(["bad", "served " + shortModel(m.served[0].model)]);
46
  else if (m.foreignTrials) out.push(["warn", "fallback " + m.served.filter(s => !s.configured).map(s => shortModel(s.model).replace(/^claude-/, "")).join(", ") + " " + pct0(m.foreignStepShare)]);
 
47
  const off = h.lbCost && Math.abs(h.costSum - h.lbCost) / h.lbCost > 0.02;
48
  if (off) out.push(["warn", "trial costs " + money(h.costSum) + "/" + money(h.lbCost)]);
49
  else if (h.uncosted / r.scan.trials > 0.05) out.push(["warn", h.uncosted + " uncosted"]);
 
56
  const leadsOnly = $("leads-only");
57
  const SORTS = {
58
  rank: r => r.rank, score: r => -r.lb.accuracy, leads: r => -r.scan.leads, hacks: r => -(r.lb.rewardHacks || 0),
59
+ trace: r => -(r.scan.holes.text.unseen ?? 0), cost: r => -(r.lb.cost || 0),
60
  };
61
  function board() {
62
  const cols = [
63
  { name: "89 tasks × 5 attempts, easiest first", cls: "l" },
64
  { name: "Score", sort: "score", help: "leaderboard accuracy ± 95% CI half-width" },
 
65
  { name: "Leads", sort: "leads", help: "rewarded trials with an unexcused high or critical finding" },
66
  { name: "LB hacks", sort: "hacks", help: "leaderboard's own reward-hack rate" },
67
+ { name: "Output not in trace", sort: "trace", cls: "l", help: "share of reported output tokens with no text in the trace, and why: the reasoning's form" },
 
68
  { name: "Cost", sort: "cost", help: "leaderboard total" },
69
  { name: "Notes", cls: "l" },
70
  ];
 
75
  const h = r.scan.holes, tr = el("tr");
76
  tr.append(rowHead(r));
77
  const s = el("td", "l"); s.append(strip(r, { leadsOnly: leadsOnly.checked })); tr.append(s);
78
+ const sc = el("td"); sc.append(num(fmt1(r.lb.accuracy), "n big"), el("span", "pm", "±" + fmt1(r.lb.ci95)));
79
+ sc.title = r.scan.rewarded + " of " + r.scan.trials + " listed trials rewarded"; tr.append(sc);
80
  const ld = el("td");
81
  if (r.scan.leads) ld.append(link(rowPath(r) + "highlights/index.html", String(r.scan.leads), "n big hot"));
82
  else ld.append(el("span", "z", "0"));
83
  tr.append(ld);
84
  tr.append(el("td", "n" + (r.lb.rewardHacks >= 1 ? " hot" : ""), fmt1(r.lb.rewardHacks) + "%"));
85
+ const miss = h.text.unseen ?? 0, tc = el("td", "vis l");
86
+ const bar = el("span", "bar bar--miss"); const fill = el("i"); fill.style.width = Math.round(100 * miss) + "%"; bar.append(fill);
87
+ const rz = el("span", "rz rz--" + h.text.form, RZ[h.text.form]); rz.title = RZ_HELP[h.text.form];
88
+ tc.append(bar, el("span", "n pc", pct0(miss)), rz);
89
+ if (h.text.uncounted) tc.append(el("span", "bad unb", " · ≥" + pct0(h.text.uncounted) + " unbilled"));
90
  tr.append(tc);
 
91
  tr.append(el("td", "n", money(r.lb.cost)));
92
  const nt = el("td", "l notes");
93
  flags(r).forEach(([lvl, text]) => nt.append(el("span", "fl fl--" + lvl, text)));
 
104
  }
105
  leadsOnly.addEventListener("change", board);
106
 
107
+ /* 2. Gaps & accounting: grouped columns, each analysis once. */
108
  function gaps() {
109
+ const groups = [
110
+ ["Models", [["Served, by share of steps", "l", "red: not the configured model; the count opens the trials"]]],
111
+ ["Output vs recorded text", [
112
+ ["Chars / output token", "", "recorded agent text (reasoning, messages, tool arguments) per reported output token; ≈3.2 when the trace holds all output"],
113
+ ["Reasoning tokens", "", "reasoning tokens as a share of output, where the harness reports them separately"],
114
+ ["Trials < 80% text", "", "trials whose recorded text covers under 80% of their output tokens"],
115
+ ["Unbilled", "", "recorded reasoning on top of output tokens the visible text already fills: missing from the reported output and cost"]]],
116
+ ["Trace gaps · trials", [
117
+ ["Web results", "", "trials with web activity whose results aren't in the trace / trials with web activity"],
118
+ ["Web prov.", "", "web input or reference provenance unresolved"], ["Unpaired results", "", "tool results not paired to their calls"],
119
+ ["Calls missing", "", "token totals exceed the recorded model calls"], ["Ends on call", "", "trace ends on a tool call with no result"],
120
+ ["Compacted", "", "history compacted into a summary"], ["No traj.", "", "listed trial without a trajectory"],
121
+ ["Time­stamps", "", "missing or out of order"]]],
122
+ ["Cost", [["Trials / LB", "", "sum of per-trial recorded cost against the leaderboard total"], ["Uncosted", "", "trials without a recorded cost"]]],
123
+ ["Evidence", [["Undecided", "", "trials with a behavioural check atif-scan couldn't decide"]]],
 
 
 
 
124
  ];
125
+ const g = el("tr", "groups"), hr = el("tr");
126
+ const g0 = el("th", "rowh", ""); g0.rowSpan = 1; g.append(g0);
127
+ hr.append(el("th", "rowh", "Row"));
128
+ groups.forEach(([name, cols], i) => {
129
+ const th = el("th", "grp" + (i ? " gstart" : ""), name); th.colSpan = cols.length; g.append(th);
130
+ cols.forEach(([cname, cls, help], j) => {
131
+ const c = el("th", (cls || "") + (j === 0 && i ? " gstart" : ""), cname); c.title = help; hr.append(c);
132
+ });
133
+ });
134
+ const thead = el("thead"); thead.append(g, hr);
135
  const t = $("gaps-table");
136
+ t.replaceChildren(thead);
137
  const tb = el("tbody");
138
+ const start = td => { td.classList.add("gstart"); return td; };
139
  D.rows.forEach(r => {
140
  const h = r.scan.holes, n = r.scan.trials, m = h.models, tr = el("tr");
141
  tr.append(rowHead(r));
 
143
  m.served.filter(x => x.share >= 0.005).slice(0, 3).forEach(x =>
144
  md.append(el("span", x.configured ? "mdl" : "mdl off", shortModel(x.model) + (m.served.length > 1 ? " " + pct0(x.share) : ""))));
145
  if (m.foreignTrials) md.append(link(holeHref(r, "foreign"), m.foreignTrials + " trials", "mlink"));
146
+ tr.append(start(md));
147
+
148
+ tr.append(start(el("td", "n", String(h.text.allCpt))));
 
149
  tr.append(el("td", "n", h.text.reasoningShare !== null ? pct0(h.text.reasoningShare) : "–"));
150
+ tr.append(countCell(r, "unseen", h.unseen, n, { scale: 1 }));
151
+ const ub = el("td", "c");
152
+ if (h.text.uncounted) ub.append(link(holeHref(r, "uncounted"), "≥" + pct0(h.text.uncounted), "bad"));
153
+ else ub.append(el("span", "z", "·"));
154
+ tr.append(ub);
155
+
156
+ tr.append(start(h.webTrials ? countCell(r, "webUnrecorded", h.webUnrecorded, n, { of: h.webTrials, scale: 1 }) : el("td", "c z", "no web")));
157
  ["webUnresolved", "unpaired", "callsMissing", "endsOnCall", "compacted", "noTrajectory", "timestamps"]
158
  .forEach(k => tr.append(countCell(r, k, h[k], n)));
159
+
160
  const off = h.lbCost ? h.costSum - h.lbCost : 0;
161
+ tr.append(start(el("td", "n" + (Math.abs(off) / (h.lbCost || 1) > 0.02 ? " bad" : ""),
162
+ Math.abs(off) >= 0.5 ? money(h.costSum) + " / " + money(h.lbCost) : money(h.costSum) + " =")));
163
  tr.append(countCell(r, "uncosted", h.uncosted, n, { scale: 0.3 }));
164
+ const und = el("td", "n", String(h.evidenceIncomplete)); heat(und, h.evidenceIncomplete / n, 1.5, "cool");
165
+ tr.append(start(und));
 
 
 
166
  tb.append(tr);
167
  });
168
  t.append(tb);
index.html CHANGED
@@ -48,12 +48,19 @@
48
  <p class="key"><i class="sq sq--1"></i>rewarded <i class="sq sq--0"></i>failed <i class="sq sq--t"></i>timed out
49
  <i class="sq sq--e"></i>errored · corner: highest finding <i class="sq sq--1 sq--c"></i>critical
50
  <i class="sq sq--1 sq--h"></i>high <i class="sq sq--1 sq--m"></i>medium · <b>Leads</b>: rewarded trials whose
51
- highest unexcused finding is high or critical · <b>In trace</b>: share of output tokens with text in the trace.</p>
 
 
 
 
52
  </section>
53
 
54
  <section id="gaps" class="block">
55
  <header class="block-head"><h2>Gaps &amp; accounting</h2>
56
- <span class="muted small">trials per row unless marked · shading is the share · click a count for its trials</span></header>
 
 
 
57
  <div class="scroll"><table id="gaps-table" class="grid grid--gaps"></table></div>
58
  </section>
59
 
 
48
  <p class="key"><i class="sq sq--1"></i>rewarded <i class="sq sq--0"></i>failed <i class="sq sq--t"></i>timed out
49
  <i class="sq sq--e"></i>errored · corner: highest finding <i class="sq sq--1 sq--c"></i>critical
50
  <i class="sq sq--1 sq--h"></i>high <i class="sq sq--1 sq--m"></i>medium · <b>Leads</b>: rewarded trials whose
51
+ highest unexcused finding is high or critical.
52
+ <b>Output not in trace</b>: share of reported output tokens with no text in the trace, and why —
53
+ <span class="rz rz--hidden">hidden</span> encrypted, tokens reported · <span class="rz rz--unrecorded">unrecorded</span> no text
54
+ or count · <span class="rz rz--dropped">dropped</span> lost converting the session · <span class="rz rz--summary">summary</span> ·
55
+ <span class="rz rz--full">full text</span>.</p>
56
  </section>
57
 
58
  <section id="gaps" class="block">
59
  <header class="block-head"><h2>Gaps &amp; accounting</h2>
60
+ <span class="muted small">trials per row unless marked · shading is the share · click a figure for its trials</span></header>
61
+ <p class="explain-line">The leaderboard's <b>output not in trace</b> comes from this group: recorded text per reported
62
+ output token, ≈3.2 when the trace holds everything. Where reasoning tokens are reported, their share is shown; trials under 80%
63
+ open from their count. <b>Unbilled</b> is the reverse gap: recorded reasoning missing from the output tokens, and so the cost.</p>
64
  <div class="scroll"><table id="gaps-table" class="grid grid--gaps"></table></div>
65
  </section>
66
 
row.js CHANGED
@@ -69,7 +69,7 @@
69
  stats.append(
70
  stat("Score", fmt1(r.lb.accuracy) + "%", "± " + fmt1(r.lb.ci95) + " · " + r.scan.rewarded + " of " + n + " trials rewarded"),
71
  stat("Review leads", String(r.scan.leads), "leaderboard reward hacks " + fmt1(r.lb.rewardHacks) + "%", r.scan.leads ? "stat--hot" : ""),
72
- stat("Trace visibility", pct0(visibleShare(h)), "of output tokens have text in the trace"),
73
  stat("Cost", money(r.lb.cost), "leaderboard · trials sum to " + money(h.costSum)));
74
  const actions = el("div", "actions");
75
  actions.append(link(rowPath(r) + "highlights/index.html", "Highlights", "btn btn--primary"),
@@ -150,8 +150,12 @@
150
  if (h.uncosted) cost.append(trials("uncosted", "Trials without a recorded cost"));
151
  if (!h.text.uncounted && !h.uncosted && Math.abs(gap) < 0.5) cost.append(el("p", "muted small", "Every trial costed; the trials add up to the leaderboard's total."));
152
 
 
153
  const grid = el("div", "panels");
154
- grid.append(found, ran, trace, cost);
 
 
 
155
  main.replaceChildren(head, stats, actions, stripWrap, grid);
156
 
157
  // #k=<gap> (from the home page's tables) opens that list of trials; #found etc. scroll to a panel.
 
69
  stats.append(
70
  stat("Score", fmt1(r.lb.accuracy) + "%", "± " + fmt1(r.lb.ci95) + " · " + r.scan.rewarded + " of " + n + " trials rewarded"),
71
  stat("Review leads", String(r.scan.leads), "leaderboard reward hacks " + fmt1(r.lb.rewardHacks) + "%", r.scan.leads ? "stat--hot" : ""),
72
+ stat("Output not in trace", pct0(h.text.unseen), "reasoning " + T.RZ[h.text.form] + (h.text.uncounted ? " · ≥" + pct0(h.text.uncounted) + " unbilled" : "")),
73
  stat("Cost", money(r.lb.cost), "leaderboard · trials sum to " + money(h.costSum)));
74
  const actions = el("div", "actions");
75
  actions.append(link(rowPath(r) + "highlights/index.html", "Highlights", "btn btn--primary"),
 
150
  if (h.uncosted) cost.append(trials("uncosted", "Trials without a recorded cost"));
151
  if (!h.text.uncounted && !h.uncosted && Math.abs(gap) < 0.5) cost.append(el("p", "muted small", "Every trial costed; the trials add up to the leaderboard's total."));
152
 
153
+ // Two columns that fill independently: findings on the left; what ran, the trace and the cost on the right.
154
  const grid = el("div", "panels");
155
+ const left = el("div", "pcol"), right = el("div", "pcol");
156
+ left.append(found);
157
+ right.append(ran, trace, cost);
158
+ grid.append(left, right);
159
  main.replaceChildren(head, stats, actions, stripWrap, grid);
160
 
161
  // #k=<gap> (from the home page's tables) opens that list of trials; #found etc. scroll to a panel.
style.css CHANGED
@@ -72,7 +72,8 @@ a.sq:hover { outline: 2px solid var(--ink); }
72
  .btn:hover { background: var(--inset); color: var(--ink); }
73
  .strip-wrap { padding: 14px 18px; border: 1px solid var(--line-soft); border-radius: var(--radius); background: var(--card); overflow-x: auto; }
74
  .strip-wrap p { margin: 0 0 8px; }
75
- .panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(440px, 1fr)); gap: 22px; margin-top: 22px; align-items: start; }
 
76
  .panel { padding: 18px 20px; border: 1px solid var(--line-soft); border-radius: var(--radius); background: var(--card); scroll-margin-top: 70px; }
77
  .panel h2 { font-size: 24px; margin: 0 0 4px; }
78
  .checks, .models, .trial-list { list-style: none; padding: 0; margin: 0; }
@@ -192,3 +193,18 @@ details.trials summary .label { font-weight: 650; }
192
  .grid--fam td { min-width: 96px; }
193
  .grid th, .grid td { padding: 5px 6px; }
194
  .grid .notes { max-width: 220px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  .btn:hover { background: var(--inset); color: var(--ink); }
73
  .strip-wrap { padding: 14px 18px; border: 1px solid var(--line-soft); border-radius: var(--radius); background: var(--card); overflow-x: auto; }
74
  .strip-wrap p { margin: 0 0 8px; }
75
+ .panels { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 22px; margin-top: 22px; align-items: start; }
76
+ .pcol { display: flex; flex-direction: column; gap: 22px; min-width: 0; }
77
  .panel { padding: 18px 20px; border: 1px solid var(--line-soft); border-radius: var(--radius); background: var(--card); scroll-margin-top: 70px; }
78
  .panel h2 { font-size: 24px; margin: 0 0 4px; }
79
  .checks, .models, .trial-list { list-style: none; padding: 0; margin: 0; }
 
193
  .grid--fam td { min-width: 96px; }
194
  .grid th, .grid td { padding: 5px 6px; }
195
  .grid .notes { max-width: 220px; }
196
+
197
+ /* Grouped headers. */
198
+ .grid thead tr.groups th { position: static; border-bottom: 0; padding-bottom: 0; font-size: 10px; color: var(--ink); text-align: left; }
199
+ .grid .gstart { border-left: 1.5px solid var(--line-soft); }
200
+ .grid thead tr.groups th.gstart { border-left: 1.5px solid var(--line); }
201
+ .explain-line { margin: 0 0 10px; font-size: 13.5px; color: var(--muted); max-width: 1100px; }
202
+ .explain-line b { color: var(--ink); }
203
+ .vis .pc { display: inline-block; min-width: 36px; margin-right: 6px; }
204
+ .grid td .rz { display: block; font-size: 10.5px; }
205
+ .vis .rz { display: inline; font-size: 12px; }
206
+ .grid td.vis .rz { display: inline; font-size: 12px; }
207
+ .bar--miss { background: color-mix(in srgb, var(--ink) 10%, transparent); }
208
+ .bar--miss i { background: var(--hl-h); }
209
+ .unb { font-size: 12px; }
210
+ .vis .bar { width: 40px; }