bindsight 0.3.5__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 (106) hide show
  1. bindsight/README.md +44 -0
  2. bindsight/__init__.py +15 -0
  3. bindsight/benchmark/__init__.py +71 -0
  4. bindsight/benchmark/core.py +497 -0
  5. bindsight/benchmark/defaults.py +28 -0
  6. bindsight/benchmark/designer_bench.py +899 -0
  7. bindsight/benchmark/outcomes.py +508 -0
  8. bindsight/benchmark/panel.py +621 -0
  9. bindsight/benchmark/rediscovery.py +1153 -0
  10. bindsight/benchmark/statistics.py +622 -0
  11. bindsight/benchmark/study.py +1153 -0
  12. bindsight/benchmark/study_figures.py +164 -0
  13. bindsight/benchmark/study_report.py +607 -0
  14. bindsight/cli.py +2429 -0
  15. bindsight/config.py +451 -0
  16. bindsight/cost.py +258 -0
  17. bindsight/deg/__init__.py +7 -0
  18. bindsight/deg/pydeseq2_runner.py +285 -0
  19. bindsight/design/__init__.py +23 -0
  20. bindsight/design/_common.py +501 -0
  21. bindsight/design/bindcraft.py +78 -0
  22. bindsight/design/boltzgen.py +79 -0
  23. bindsight/design/developability.py +140 -0
  24. bindsight/design/embeddings.py +165 -0
  25. bindsight/design/prescreen.py +169 -0
  26. bindsight/design/protocol.py +111 -0
  27. bindsight/design/rfdiff_mpnn.py +93 -0
  28. bindsight/epitopes/__init__.py +7 -0
  29. bindsight/epitopes/surface_bind.py +134 -0
  30. bindsight/export/__init__.py +7 -0
  31. bindsight/export/ro_crate.py +595 -0
  32. bindsight/io/__init__.py +19 -0
  33. bindsight/io/cbioportal.py +118 -0
  34. bindsight/io/gdc.py +343 -0
  35. bindsight/io/paths.py +124 -0
  36. bindsight/pipelines/__init__.py +9 -0
  37. bindsight/pipelines/caveats.py +60 -0
  38. bindsight/pipelines/discover.py +1443 -0
  39. bindsight/pipelines/full_run.py +410 -0
  40. bindsight/plugins.py +335 -0
  41. bindsight/provenance/__init__.py +34 -0
  42. bindsight/provenance/append.py +199 -0
  43. bindsight/provenance/fragments.py +216 -0
  44. bindsight/provenance/manifest.py +506 -0
  45. bindsight/rank/__init__.py +7 -0
  46. bindsight/rank/scoring.py +355 -0
  47. bindsight/report/__init__.py +39 -0
  48. bindsight/report/format.py +100 -0
  49. bindsight/report/html.py +645 -0
  50. bindsight/report/showcase.py +653 -0
  51. bindsight/report/templates/report.css +67 -0
  52. bindsight/report/templates/report.html.j2 +420 -0
  53. bindsight/report/theme.py +231 -0
  54. bindsight/report/web/__init__.py +35 -0
  55. bindsight/report/web/app.py +624 -0
  56. bindsight/report/web/static/binder_viewer.js +245 -0
  57. bindsight/report/web/static/bindsight.css +455 -0
  58. bindsight/report/web/static/charts.js +425 -0
  59. bindsight/report/web/static/vendor/3Dmol-min.js +2 -0
  60. bindsight/report/web/static/vendor/3Dmol.LICENSE +90 -0
  61. bindsight/report/web/static/your_data_check.js +370 -0
  62. bindsight/report/web/templates/base.html.j2 +48 -0
  63. bindsight/report/web/templates/evidence.html.j2 +341 -0
  64. bindsight/report/web/templates/overview.html.j2 +140 -0
  65. bindsight/report/web/templates/runs.html.j2 +125 -0
  66. bindsight/report/web/templates/try.html.j2 +155 -0
  67. bindsight/report/web/templates/your_data.html.j2 +46 -0
  68. bindsight/runners/__init__.py +7 -0
  69. bindsight/runners/colab.py +158 -0
  70. bindsight/runners/job_exec.py +1110 -0
  71. bindsight/runners/kaggle.py +425 -0
  72. bindsight/runners/kaggle_kernel.py +489 -0
  73. bindsight/runners/local_docker.py +154 -0
  74. bindsight/runners/mock.py +167 -0
  75. bindsight/runners/modal_runner.py +199 -0
  76. bindsight/runners/notebook.py +116 -0
  77. bindsight/runners/notebook_content.py +217 -0
  78. bindsight/runners/protocol.py +81 -0
  79. bindsight/runners/source_wheel.py +104 -0
  80. bindsight/runners/tools.py +997 -0
  81. bindsight/structures/__init__.py +7 -0
  82. bindsight/structures/alphafolddb.py +93 -0
  83. bindsight/structures/plddt.py +92 -0
  84. bindsight/structures/topology.py +227 -0
  85. bindsight/surfaceome/__init__.py +42 -0
  86. bindsight/surfaceome/data/surfaceome_extended.tsv +4814 -0
  87. bindsight/surfaceome/data/surfy_offline.txt +17 -0
  88. bindsight/surfaceome/data/surfy_v1.ensembl.tsv +3380 -0
  89. bindsight/surfaceome/data/surfy_v1.uniprot.txt +2901 -0
  90. bindsight/surfaceome/surfy.py +441 -0
  91. bindsight/targets/__init__.py +7 -0
  92. bindsight/targets/ensembl_uniprot.py +56 -0
  93. bindsight/targets/gtex.py +250 -0
  94. bindsight/targets/open_targets.py +239 -0
  95. bindsight/validate/__init__.py +7 -0
  96. bindsight/validate/af2_ig.py +49 -0
  97. bindsight/validate/boltz2.py +256 -0
  98. bindsight/validate/chai1r.py +42 -0
  99. bindsight/validate/protocol.py +188 -0
  100. bindsight-0.3.5.data/data/bindsight_demo/config.yaml +54 -0
  101. bindsight-0.3.5.dist-info/METADATA +495 -0
  102. bindsight-0.3.5.dist-info/RECORD +106 -0
  103. bindsight-0.3.5.dist-info/WHEEL +4 -0
  104. bindsight-0.3.5.dist-info/entry_points.txt +20 -0
  105. bindsight-0.3.5.dist-info/licenses/COPYRIGHT +24 -0
  106. bindsight-0.3.5.dist-info/licenses/LICENSE +681 -0
@@ -0,0 +1,245 @@
1
+ // SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ //
4
+ // The predicted binder-target complexes, drawn in 3-D.
5
+ //
6
+ // This file is loaded by three surfaces and authored once, because copies of a
7
+ // viewer diverge and the copy nobody is looking at is the one that drifts:
8
+ //
9
+ // `bindsight ui` templates/evidence.html.j2, structures from a route
10
+ // the documentation site docs/results.md, structures from files beside it
11
+ // the HTML report report/templates/report.html.j2, structures inside it
12
+ //
13
+ // None of them owns it. What differs is only where a structure comes from, and
14
+ // that is read off the host element rather than branched on by surface.
15
+ //
16
+ // The host element carries the contract:
17
+ //
18
+ // data-binder-viewer marks the element to draw into
19
+ // data-structure-url="<template>" {id} is replaced with the design id
20
+ // data-viewer-lib="<url>" optional: load 3Dmol from here if absent
21
+ //
22
+ // and, where the page carries its structures rather than fetching them,
23
+ // `window.BINDSIGHT_STRUCTURES` maps design id to mmCIF text.
24
+ //
25
+ // and the picker carries `data-binder-picker`. Attributes, not ids, so the two
26
+ // surfaces can keep their own class names and their own stylesheets.
27
+ (function () {
28
+ "use strict";
29
+
30
+ // Captured at parse time: `document.currentScript` is null inside any later
31
+ // callback. Every URL below resolves against THIS FILE's location rather than
32
+ // the page's, which is what lets one script serve a docs site published under
33
+ // a subpath (/bindsight/), a local `mkdocs serve` at /, and the app at / --
34
+ // with nothing encoding a site root. mkdocs-material has already put the
35
+ // correct base on this src; borrowing it is more reliable than deriving it.
36
+ var SELF =
37
+ (document.currentScript && document.currentScript.src) ||
38
+ (document.querySelector('script[src*="binder_viewer.js"]') || {}).src ||
39
+ window.location.href;
40
+ var HERE = new URL(".", SELF).href;
41
+
42
+ // bindsight writes the designed chain as B and the target it was designed
43
+ // against as T. tests/test_web_ui.py asserts that of every committed
44
+ // structure, so if it ever stops being true this stops being a silent
45
+ // mis-colouring and becomes a failing test.
46
+ var BINDER_CHAIN = "B";
47
+
48
+ // Composed as nodes rather than markup, for the reason the fetch handler
49
+ // below gives: a message that later starts carrying something a server said
50
+ // must not be interpolated into innerHTML.
51
+ function replaceViewerWith(host, text) {
52
+ host.innerHTML = "";
53
+ var note = document.createElement("p");
54
+ note.className = "small";
55
+ note.style.padding = "1rem";
56
+ // Announced, not just drawn: this replaces a fallback that a screen
57
+ // reader had already been given, so losing the role would make the
58
+ // message silent for exactly the reader who needs it most.
59
+ note.setAttribute("role", "alert");
60
+ note.textContent = text;
61
+ var where = document.createElement("p");
62
+ where.className = "small muted";
63
+ where.style.padding = "0 1rem 1rem";
64
+ where.textContent =
65
+ "Every complex is committed as mmCIF under " +
66
+ "benchmarks/designer_benchmark/, which is the file this viewer reads.";
67
+ host.appendChild(note);
68
+ host.appendChild(where);
69
+ }
70
+
71
+ // WebGL is unavailable in more places than it looks: locked-down machines,
72
+ // remote desktops, some VMs, browsers with hardware acceleration off. There
73
+ // `createViewer` does not throw -- it returns a viewer over a canvas with a
74
+ // dead context, so the page drew a white rectangle under a caption promising
75
+ // a rotatable complex and said nothing. The rule is the one the fetch
76
+ // handler below states: a blank viewer and a design that produced nothing
77
+ // are different facts and must not look the same.
78
+ function webglAvailable() {
79
+ try {
80
+ var probe = document.createElement("canvas");
81
+ return !!(probe.getContext("webgl2") || probe.getContext("webgl"));
82
+ } catch (e) {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ function start(host, pick) {
88
+ if (!webglAvailable()) {
89
+ replaceViewerWith(
90
+ host,
91
+ "This browser cannot draw 3-D structures: WebGL is unavailable or " +
92
+ "switched off. The structures are here, the viewer is not."
93
+ );
94
+ return;
95
+ }
96
+
97
+ host.innerHTML = "";
98
+ var viewer;
99
+ try {
100
+ viewer = $3Dmol.createViewer(host, { backgroundColor: "white" });
101
+ } catch (e) {
102
+ replaceViewerWith(host, "Could not start the structure viewer: " + String(e && e.message));
103
+ return;
104
+ }
105
+ if (!viewer) {
106
+ replaceViewerWith(host, "Could not start the structure viewer.");
107
+ return;
108
+ }
109
+
110
+ // The app serves structures from a route; the docs site serves them as
111
+ // files beside this script. The id is never user input -- it comes from
112
+ // the picker this page generated -- and encodeURIComponent keeps a
113
+ // crafted one from walking out of the directory either way.
114
+ var template = host.getAttribute("data-structure-url") || "/api/structure/{id}";
115
+
116
+ // The self-contained HTML report has no server AND no sibling files: it
117
+ // is one file that must survive being emailed, so its structures travel
118
+ // inside it. Same viewer, third source -- rather than a second viewer that
119
+ // would be the copy nobody noticed had drifted.
120
+ var inline = window.BINDSIGHT_STRUCTURES || null;
121
+
122
+ function load(id) {
123
+ if (inline && Object.prototype.hasOwnProperty.call(inline, id)) {
124
+ return Promise.resolve(inline[id]);
125
+ }
126
+ var url = new URL(template.replace("{id}", encodeURIComponent(id)), HERE).href;
127
+ return fetch(url).then(function (r) {
128
+ if (!r.ok) throw new Error("no structure for " + id);
129
+ return r.text();
130
+ });
131
+ }
132
+
133
+ // Which selection is allowed to draw. A reader clicking through the picker
134
+ // has several loads in flight, and they do not resolve in the order they
135
+ // were asked for -- so the slowest would win and the viewer would show a
136
+ // complex the picker is no longer naming, with nothing on screen to say so.
137
+ var latestPick = 0;
138
+
139
+ function show(id) {
140
+ var thisPick = ++latestPick;
141
+ load(id)
142
+ .then(function (cif) {
143
+ if (thisPick !== latestPick) return;
144
+ viewer.clear();
145
+ var model = viewer.addModel(cif, "cif");
146
+ // An mmCIF the parser did not understand yields a model with no atoms,
147
+ // and 3Dmol renders that as an empty canvas without complaint -- the
148
+ // same white rectangle a reader would get from a design that produced
149
+ // nothing. Counted rather than assumed, for the reason the catch block
150
+ // below gives.
151
+ var atoms = 0;
152
+ try {
153
+ atoms = model && model.selectedAtoms ? model.selectedAtoms({}).length : 0;
154
+ } catch (e) {
155
+ atoms = 0;
156
+ }
157
+ if (!atoms) {
158
+ throw new Error("the structure file for " + id + " parsed to no atoms");
159
+ }
160
+ // Paint everything first, then override the binder. Styling named
161
+ // chains only meant a file whose chains are named something else kept
162
+ // 3Dmol's default line style and rendered as a wireframe haze -- with
163
+ // no error, and under a caption claiming it was grey cartoon.
164
+ viewer.setStyle({}, { cartoon: { color: "#9aa5b1" } });
165
+ viewer.setStyle({ chain: BINDER_CHAIN }, { cartoon: { color: "#0f7d73" } });
166
+ viewer.zoomTo();
167
+ viewer.render();
168
+ })
169
+ .catch(function (err) {
170
+ if (thisPick !== latestPick) return;
171
+ // Said plainly. A viewer that stays blank is indistinguishable from a
172
+ // design that produced nothing, and those are different facts.
173
+ //
174
+ // Built as a node with textContent rather than assembled into
175
+ // innerHTML. The message is composed here today, but an error string
176
+ // is exactly the kind of value that later starts carrying something a
177
+ // server said, and by then the interpolation looks harmless.
178
+ //
179
+ // One case this reports honestly rather than hiding: opening the
180
+ // built site from a file:// path, where fetch is refused by CORS.
181
+ host.innerHTML = "";
182
+ var note = document.createElement("p");
183
+ note.className = "small";
184
+ note.style.padding = "1rem";
185
+ note.setAttribute("role", "alert");
186
+ note.textContent = "Could not load this structure: " + String(err && err.message);
187
+ host.appendChild(note);
188
+ });
189
+ }
190
+
191
+ pick.addEventListener("change", function () {
192
+ show(pick.value);
193
+ });
194
+ show(pick.value);
195
+ }
196
+
197
+ function boot() {
198
+ var host = document.querySelector("[data-binder-viewer]");
199
+ var pick = document.querySelector("[data-binder-picker]");
200
+ if (!host || !pick) return;
201
+ // mkdocs-material's instant navigation swaps page content without a
202
+ // reload, so this runs again on a page that may already be wired up.
203
+ if (host.dataset.viewerReady === "1") return;
204
+ host.dataset.viewerReady = "1";
205
+
206
+ if (typeof $3Dmol !== "undefined") {
207
+ start(host, pick);
208
+ return;
209
+ }
210
+
211
+ // The app loads the library itself, in a tag beside this one. The docs
212
+ // page cannot: `extra_javascript` would put half a megabyte on all eight
213
+ // pages to serve the one that draws structures. So it names the library
214
+ // here and it is fetched only where a viewer exists.
215
+ var lib = host.getAttribute("data-viewer-lib");
216
+ if (!lib) {
217
+ // The app and the report both ship the library in a tag beside this one,
218
+ // so reaching here without one means it failed to parse or was stripped.
219
+ replaceViewerWith(host, "The structure viewer library did not load.");
220
+ return;
221
+ }
222
+ var tag = document.createElement("script");
223
+ tag.src = new URL(lib, HERE).href;
224
+ tag.onload = function () {
225
+ start(host, pick);
226
+ };
227
+ tag.onerror = function () {
228
+ // A 404 on the library would otherwise leave the bare host element on
229
+ // screen: a blank rectangle under a caption promising a complex.
230
+ replaceViewerWith(host, "Could not load the structure viewer.");
231
+ };
232
+ document.head.appendChild(tag);
233
+ }
234
+
235
+ // mkdocs-material publishes `document$` and re-emits it on every instant
236
+ // navigation; without subscribing, a reader arriving at this page from
237
+ // another docs page gets an inert picker and an empty box.
238
+ if (window.document$ && typeof window.document$.subscribe === "function") {
239
+ window.document$.subscribe(boot);
240
+ } else if (document.readyState !== "loading") {
241
+ boot();
242
+ } else {
243
+ document.addEventListener("DOMContentLoaded", boot);
244
+ }
245
+ })();
@@ -0,0 +1,455 @@
1
+ /* SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
2
+ * SPDX-License-Identifier: AGPL-3.0-or-later
3
+ *
4
+ * bindsight design system.
5
+ *
6
+ * One stylesheet, two surfaces: the interactive app and the standalone HTML
7
+ * report a collaborator opens from an email. They looked like different
8
+ * products before; a reader who has seen one should recognise the other.
9
+ *
10
+ * Three rules this file exists to enforce, because they are scientific claims
11
+ * and not decoration:
12
+ *
13
+ * 1. A figure never appears without what makes it interpretable. `.stat`
14
+ * requires a denominator or interval slot; there is no variant without one.
15
+ * 2. Nothing is encoded by colour alone. Every status carries a glyph, so the
16
+ * meaning survives a projector, a greyscale printout, and deuteranopia.
17
+ * 3. Absence renders as an em dash, never as zero. `.stat--absent` exists so
18
+ * "not measured" cannot be mistaken for "measured as none".
19
+ */
20
+
21
+ /* ---------------------------------------------------------------- tokens */
22
+ :root {
23
+ /* Ink and ground. Deliberately not pure black on pure white: 1b1f24 on
24
+ fcfdfe is easier to read for long stretches and still clears WCAG AAA. */
25
+ --ink: #1b1f24;
26
+ --ink-soft: #55606d;
27
+ /* 4.9:1 on the page background. Was #7d8896, which is 3.6:1 -- below WCAG
28
+ AA's 4.5:1 for text under 18.7px, and every `.stat__label` and
29
+ `.stat__note` is 12px. That put the words naming each headline number, and
30
+ the caveats attached to them, below the threshold while the numbers
31
+ themselves stayed legible: exactly backwards for a design whose rule is
32
+ that the limitation sits beside the claim. */
33
+ --ink-faint: #68727f;
34
+ --ground: #fcfdfe;
35
+ --surface: #ffffff;
36
+ --rule: #e3e6ea;
37
+ --rule-strong: #c9d2de;
38
+
39
+ /* Brand. Navy carries structure; teal marks the designed half. */
40
+ --navy: #0b5394;
41
+ --navy-deep: #073a68;
42
+ --navy-tint: #e8f0fb;
43
+ --teal: #0e766d;
44
+ --teal-tint: #e6f4f2;
45
+
46
+ /* Status. Each is paired with a glyph in the markup — never used alone. */
47
+ --ok: #1f6f35;
48
+ --ok-tint: #e8f4ea;
49
+ --warn: #8a6100;
50
+ --warn-tint: #fdf3dd;
51
+ --err: #a32020;
52
+ --err-tint: #fbeaea;
53
+
54
+ /* Type scale, 1.2 ratio from 16px. */
55
+ --t-xs: 0.75rem;
56
+ --t-sm: 0.875rem;
57
+ --t-base: 1rem;
58
+ --t-md: 1.2rem;
59
+ --t-lg: 1.44rem;
60
+ --t-xl: 1.728rem;
61
+ --t-2xl: 2.074rem;
62
+
63
+ /* Spacing, 4px base. */
64
+ --s-1: 0.25rem;
65
+ --s-2: 0.5rem;
66
+ --s-3: 0.75rem;
67
+ --s-4: 1rem;
68
+ --s-6: 1.5rem;
69
+ --s-8: 2rem;
70
+ --s-12: 3rem;
71
+ --s-16: 4rem;
72
+
73
+ --radius: 8px;
74
+ --radius-sm: 4px;
75
+ --shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);
76
+ --measure: 68ch;
77
+
78
+ --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
79
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
80
+ }
81
+
82
+ *, *::before, *::after { box-sizing: border-box; }
83
+
84
+ html { -webkit-text-size-adjust: 100%; }
85
+
86
+ body {
87
+ margin: 0;
88
+ font-family: var(--font);
89
+ font-size: var(--t-base);
90
+ line-height: 1.6;
91
+ color: var(--ink);
92
+ background: var(--ground);
93
+ /* Tabular figures everywhere: a column of numbers that does not align is a
94
+ column a reader cannot scan. */
95
+ font-variant-numeric: tabular-nums;
96
+ }
97
+
98
+ /* ---------------------------------------------------------------- layout */
99
+ .shell { display: flex; min-height: 100vh; }
100
+
101
+ .sidebar {
102
+ flex: 0 0 15rem;
103
+ background: var(--navy-deep);
104
+ color: #dce7f3;
105
+ padding: var(--s-6) var(--s-4);
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: var(--s-6);
109
+ }
110
+ .sidebar a { color: #dce7f3; }
111
+
112
+ .brand { display: block; text-decoration: none; }
113
+ .brand__name {
114
+ font-size: var(--t-lg);
115
+ font-weight: 700;
116
+ letter-spacing: -0.01em;
117
+ color: #fff;
118
+ }
119
+ .brand__tag { font-size: var(--t-xs); color: #9fb8d4; }
120
+
121
+ .nav { display: flex; flex-direction: column; gap: 2px; }
122
+ .nav a {
123
+ display: flex;
124
+ align-items: baseline;
125
+ gap: var(--s-2);
126
+ padding: var(--s-2) var(--s-3);
127
+ border-radius: var(--radius-sm);
128
+ text-decoration: none;
129
+ font-size: var(--t-sm);
130
+ }
131
+ .nav a:hover { background: rgba(255, 255, 255, 0.08); }
132
+ .nav a[aria-current="page"] {
133
+ background: rgba(255, 255, 255, 0.14);
134
+ color: #fff;
135
+ font-weight: 600;
136
+ }
137
+ /* Not colour alone: the current page also carries a marker. */
138
+ .nav a[aria-current="page"]::before { content: "▸"; margin-left: -0.75em; }
139
+
140
+ .sidebar__foot {
141
+ margin-top: auto;
142
+ font-size: var(--t-xs);
143
+ color: #9fb8d4;
144
+ line-height: 1.5;
145
+ }
146
+
147
+ .main { flex: 1 1 auto; min-width: 0; }
148
+ .wrap { max-width: 78rem; margin: 0 auto; padding: var(--s-8) var(--s-6) var(--s-16); }
149
+ .prose { max-width: var(--measure); }
150
+
151
+ /* ------------------------------------------------------------ typography */
152
+ h1, h2, h3, h4 { line-height: 1.25; letter-spacing: -0.015em; margin: 0 0 var(--s-3); }
153
+ h1 { font-size: var(--t-2xl); font-weight: 700; }
154
+ h2 { font-size: var(--t-xl); font-weight: 650; margin-top: var(--s-12); }
155
+ h3 { font-size: var(--t-md); font-weight: 650; margin-top: var(--s-8); }
156
+ h2:first-of-type, h3:first-of-type { margin-top: 0; }
157
+ p { margin: 0 0 var(--s-4); }
158
+ .lede { font-size: var(--t-md); color: var(--ink-soft); max-width: var(--measure); }
159
+ .muted { color: var(--ink-soft); }
160
+ .small { font-size: var(--t-sm); }
161
+ code, kbd, samp { font-family: var(--mono); font-size: 0.9em; }
162
+ :not(pre) > code {
163
+ background: var(--navy-tint);
164
+ padding: 0.1em 0.35em;
165
+ border-radius: var(--radius-sm);
166
+ }
167
+ pre {
168
+ background: #0f1720;
169
+ color: #e6edf3;
170
+ padding: var(--s-4);
171
+ border-radius: var(--radius);
172
+ overflow-x: auto;
173
+ font-size: var(--t-sm);
174
+ line-height: 1.55;
175
+ }
176
+ pre code { background: none; padding: 0; color: inherit; }
177
+ a { color: var(--navy); text-underline-offset: 2px; }
178
+ a:hover { color: var(--navy-deep); }
179
+
180
+ /* ----------------------------------------------------------------- cards */
181
+ .card {
182
+ background: var(--surface);
183
+ border: 1px solid var(--rule);
184
+ border-radius: var(--radius);
185
+ padding: var(--s-6);
186
+ box-shadow: var(--shadow);
187
+ }
188
+ .card + .card { margin-top: var(--s-4); }
189
+ .card__title { font-size: var(--t-md); font-weight: 650; margin: 0 0 var(--s-2); }
190
+
191
+ .grid { display: grid; gap: var(--s-4); }
192
+ .grid--2 { grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); }
193
+ .grid--3 { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
194
+ .grid--4 { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }
195
+
196
+ /* ----------------------------------------------------------------- stats
197
+ *
198
+ * The central component, and the one with a rule attached: a `.stat` must
199
+ * carry a `.stat__of` (its denominator) or a `.stat__ci` (its interval). The
200
+ * old interface put intervals in hover tooltips — including one spanning
201
+ * 15–70% — under a paragraph promising every rate carried its interval. A
202
+ * tooltip is not a disclosure: it does not print, does not exist on touch, and
203
+ * is not there when someone photographs the screen.
204
+ */
205
+ .stat { display: flex; flex-direction: column; gap: 2px; }
206
+ .stat__label {
207
+ font-size: var(--t-xs);
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.06em;
210
+ color: var(--ink-faint);
211
+ font-weight: 600;
212
+ }
213
+ .stat__value {
214
+ font-size: var(--t-xl);
215
+ font-weight: 680;
216
+ line-height: 1.15;
217
+ letter-spacing: -0.02em;
218
+ }
219
+ .stat__of { font-size: var(--t-sm); color: var(--ink-soft); }
220
+ .stat__ci { font-size: var(--t-sm); color: var(--ink-soft); }
221
+ .stat__note { font-size: var(--t-xs); color: var(--ink-faint); line-height: 1.45; }
222
+
223
+ /* Absence. Never a zero: four crisp zeros in metric chrome read as a
224
+ measurement, and a broken run then looks like an empty one. */
225
+ .stat--absent .stat__value { color: var(--ink-faint); font-weight: 500; }
226
+
227
+ /* Descriptive key/value, deliberately *not* `.stat`.
228
+ *
229
+ * `.stat` is a measurement and carries a rule: it must have a denominator or an
230
+ * interval. Descriptions have neither and never will, so they get their own
231
+ * component -- otherwise the rule would have to admit exceptions, and a rule
232
+ * with exceptions cannot be checked. */
233
+ .fact { display: flex; flex-direction: column; gap: 2px; }
234
+ .fact__label {
235
+ font-size: var(--t-xs);
236
+ text-transform: uppercase;
237
+ letter-spacing: 0.06em;
238
+ color: var(--ink-faint);
239
+ font-weight: 600;
240
+ }
241
+ .fact__value { font-size: var(--t-base); font-weight: 600; line-height: 1.4; }
242
+
243
+ /* ---------------------------------------------------------------- badges */
244
+ .badge {
245
+ display: inline-flex;
246
+ align-items: baseline;
247
+ gap: 0.35em;
248
+ padding: 0.15em 0.55em;
249
+ border-radius: 999px;
250
+ font-size: var(--t-xs);
251
+ font-weight: 600;
252
+ border: 1px solid transparent;
253
+ }
254
+ .badge__glyph { font-weight: 700; }
255
+ .badge--ok { background: var(--ok-tint); color: var(--ok); border-color: #bfe0c7; }
256
+ .badge--warn { background: var(--warn-tint); color: var(--warn); border-color: #ecd9a4; }
257
+ .badge--err { background: var(--err-tint); color: var(--err); border-color: #eebcbc; }
258
+ .badge--info { background: var(--navy-tint); color: var(--navy); border-color: #c3d8f0; }
259
+ .badge--teal { background: var(--teal-tint); color: var(--teal); border-color: #b7ded8; }
260
+
261
+ /* ---------------------------------------------------------------- notices */
262
+ .note {
263
+ display: flex;
264
+ gap: var(--s-3);
265
+ padding: var(--s-4);
266
+ border-radius: var(--radius);
267
+ border: 1px solid var(--rule);
268
+ background: var(--surface);
269
+ margin: var(--s-4) 0;
270
+ }
271
+ .note__glyph { flex: 0 0 auto; font-weight: 700; line-height: 1.5; }
272
+ .note__body { min-width: 0; }
273
+ .note__body > :last-child { margin-bottom: 0; }
274
+ .note--info { background: var(--navy-tint); border-color: #c3d8f0; }
275
+ .note--ok { background: var(--ok-tint); border-color: #bfe0c7; }
276
+ .note--warn { background: var(--warn-tint); border-color: #ecd9a4; }
277
+ .note--err { background: var(--err-tint); border-color: #eebcbc; }
278
+
279
+ /* Withdrawal. Its own component, because a retracted figure must not be
280
+ stylistically interchangeable with an ordinary caveat. */
281
+ .withdrawn {
282
+ border-left: 3px solid var(--err);
283
+ background: var(--err-tint);
284
+ padding: var(--s-3) var(--s-4);
285
+ border-radius: 0 var(--radius) var(--radius) 0;
286
+ margin: var(--s-4) 0;
287
+ font-size: var(--t-sm);
288
+ }
289
+ .withdrawn strong { color: var(--err); }
290
+
291
+ /* ---------------------------------------------------------------- tables */
292
+ .table-wrap { overflow-x: auto; border: 1px solid var(--rule); border-radius: var(--radius); }
293
+ table { border-collapse: collapse; width: 100%; font-size: var(--t-sm); background: var(--surface); }
294
+ th, td { padding: var(--s-2) var(--s-3); text-align: left; border-bottom: 1px solid var(--rule); }
295
+ th {
296
+ background: #f5f8fc;
297
+ font-weight: 650;
298
+ font-size: var(--t-xs);
299
+ text-transform: uppercase;
300
+ letter-spacing: 0.05em;
301
+ color: var(--ink-soft);
302
+ position: sticky;
303
+ top: 0;
304
+ white-space: nowrap;
305
+ }
306
+ tbody tr:last-child td { border-bottom: none; }
307
+ tbody tr:hover { background: #f8fbfe; }
308
+ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
309
+
310
+ /* A column header that carries its own definition. */
311
+ th abbr { text-decoration: underline dotted; cursor: help; }
312
+
313
+ /* ---------------------------------------------------------------- charts */
314
+ .chart { width: 100%; overflow: visible; }
315
+ .chart text { font-family: var(--font); font-size: 11px; fill: var(--ink-soft); }
316
+ .chart .axis line, .chart .axis path { stroke: var(--rule-strong); }
317
+ .chart .gridline { stroke: var(--rule); stroke-dasharray: 2 3; }
318
+ .chart .mark { stroke: var(--surface); stroke-width: 1; }
319
+ .chart .mark:hover { stroke: var(--ink); stroke-width: 1.5; }
320
+ .chart .band { fill: var(--navy-tint); }
321
+ .chart__caption { font-size: var(--t-sm); color: var(--ink-soft); margin-top: var(--s-2); }
322
+
323
+ .tooltip {
324
+ position: fixed;
325
+ pointer-events: none;
326
+ background: var(--ink);
327
+ color: #fff;
328
+ padding: var(--s-2) var(--s-3);
329
+ border-radius: var(--radius-sm);
330
+ font-size: var(--t-xs);
331
+ line-height: 1.5;
332
+ z-index: 50;
333
+ max-width: 22rem;
334
+ box-shadow: 0 4px 12px rgba(16, 24, 40, 0.18);
335
+ }
336
+ .tooltip[hidden] { display: none; }
337
+
338
+ /* --------------------------------------------------------------- buttons */
339
+ .btn {
340
+ display: inline-flex;
341
+ align-items: center;
342
+ gap: 0.4em;
343
+ padding: var(--s-2) var(--s-4);
344
+ border-radius: var(--radius-sm);
345
+ border: 1px solid var(--rule-strong);
346
+ background: var(--surface);
347
+ color: var(--ink);
348
+ font: inherit;
349
+ font-size: var(--t-sm);
350
+ font-weight: 600;
351
+ cursor: pointer;
352
+ text-decoration: none;
353
+ }
354
+ .btn:hover { background: #f5f8fc; }
355
+ .btn--primary { background: var(--navy); border-color: var(--navy); color: #fff; }
356
+ .btn--primary:hover { background: var(--navy-deep); color: #fff; }
357
+ .btn:disabled, .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
358
+ .btn-row { display: flex; flex-wrap: wrap; gap: var(--s-3); margin: var(--s-4) 0; }
359
+
360
+ /* ----------------------------------------------------------------- forms */
361
+ label { display: block; font-size: var(--t-sm); font-weight: 600; margin-bottom: var(--s-1); }
362
+ .field { margin-bottom: var(--s-4); }
363
+ .field__help { font-size: var(--t-xs); color: var(--ink-soft); margin-top: var(--s-1); }
364
+ input[type="text"], input[type="number"], input[type="file"], select {
365
+ width: 100%;
366
+ padding: var(--s-2) var(--s-3);
367
+ border: 1px solid var(--rule-strong);
368
+ border-radius: var(--radius-sm);
369
+ font: inherit;
370
+ font-size: var(--t-sm);
371
+ background: var(--surface);
372
+ color: var(--ink);
373
+ }
374
+ input:focus-visible, select:focus-visible, .btn:focus-visible, a:focus-visible {
375
+ outline: 2px solid var(--navy);
376
+ outline-offset: 2px;
377
+ }
378
+
379
+ /* -------------------------------------------------------------- progress */
380
+ .steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
381
+ .steps li {
382
+ display: flex;
383
+ align-items: baseline;
384
+ gap: var(--s-3);
385
+ padding: var(--s-2) var(--s-3);
386
+ border-radius: var(--radius-sm);
387
+ font-size: var(--t-sm);
388
+ }
389
+ .steps li[data-state="running"] { background: var(--navy-tint); font-weight: 600; }
390
+ .steps li[data-state="done"] { color: var(--ink-soft); }
391
+ .steps li[data-state="failed"] { background: var(--err-tint); color: var(--err); font-weight: 600; }
392
+ .steps__glyph { flex: 0 0 1.25em; font-weight: 700; }
393
+
394
+ /* ------------------------------------------------------------- structure */
395
+ .viewer { width: 100%; height: 26rem; position: relative; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--rule); }
396
+ /* Height is set explicitly above, and must stay that way: 3Dmol measures its
397
+ container when the viewer is created, and a container that is zero-high at
398
+ that moment renders a canvas nothing can see, with no error anywhere. */
399
+ .viewer__fallback { padding: var(--s-4); }
400
+ .swatch {
401
+ display: inline-block;
402
+ width: 0.75rem;
403
+ height: 0.75rem;
404
+ border-radius: 2px;
405
+ vertical-align: -1px;
406
+ margin-right: var(--s-1);
407
+ border: 1px solid var(--rule-strong);
408
+ }
409
+ .viewer-legend { display: flex; gap: var(--s-4); flex-wrap: wrap; margin-top: var(--s-2); font-size: var(--t-sm); }
410
+
411
+ /* ---------------------------------------------------------------- misc */
412
+ .stack > * + * { margin-top: var(--s-4); }
413
+ .row { display: flex; gap: var(--s-4); flex-wrap: wrap; align-items: baseline; }
414
+ hr { border: none; border-top: 1px solid var(--rule); margin: var(--s-8) 0; }
415
+ .visually-hidden {
416
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
417
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
418
+ }
419
+
420
+ /* --------------------------------------------------------------- narrow */
421
+ @media (max-width: 60rem) {
422
+ .shell { flex-direction: column; }
423
+ .sidebar { flex: 0 0 auto; flex-direction: row; align-items: center; gap: var(--s-4); flex-wrap: wrap; padding: var(--s-3) var(--s-4); }
424
+ .nav { flex-direction: row; flex-wrap: wrap; }
425
+ .sidebar__foot { margin-top: 0; width: 100%; }
426
+ .wrap { padding: var(--s-6) var(--s-4) var(--s-12); }
427
+ h1 { font-size: var(--t-xl); }
428
+ }
429
+
430
+ /* ---------------------------------------------------------------- print
431
+ * The report is emailed and printed. Hover states do not survive paper, so
432
+ * nothing meaningful may live in one. */
433
+ @media print {
434
+ .sidebar, .btn-row, .tooltip { display: none !important; }
435
+ body { background: #fff; }
436
+ .card { box-shadow: none; break-inside: avoid; }
437
+ .table-wrap { overflow: visible; }
438
+ a { color: var(--ink); text-decoration: underline; }
439
+ a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; color: var(--ink-faint); }
440
+ }
441
+
442
+ .print-only { display: none; }
443
+
444
+ @media print {
445
+ /* A WebGL canvas prints as a blank rectangle, so the box is dropped. Its
446
+ furniture has to go with it: a paper page kept the design picker, the
447
+ colour legend for a picture that was not there, and an instruction to
448
+ "Rotate it". The caption's print-only half says where the files are
449
+ instead. */
450
+ .viewer,
451
+ .viewer + .viewer-legend,
452
+ .viewer-picker { display: none !important; }
453
+ .screen-only { display: none !important; }
454
+ .print-only { display: inline !important; }
455
+ }