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.
- bindsight/README.md +44 -0
- bindsight/__init__.py +15 -0
- bindsight/benchmark/__init__.py +71 -0
- bindsight/benchmark/core.py +497 -0
- bindsight/benchmark/defaults.py +28 -0
- bindsight/benchmark/designer_bench.py +899 -0
- bindsight/benchmark/outcomes.py +508 -0
- bindsight/benchmark/panel.py +621 -0
- bindsight/benchmark/rediscovery.py +1153 -0
- bindsight/benchmark/statistics.py +622 -0
- bindsight/benchmark/study.py +1153 -0
- bindsight/benchmark/study_figures.py +164 -0
- bindsight/benchmark/study_report.py +607 -0
- bindsight/cli.py +2429 -0
- bindsight/config.py +451 -0
- bindsight/cost.py +258 -0
- bindsight/deg/__init__.py +7 -0
- bindsight/deg/pydeseq2_runner.py +285 -0
- bindsight/design/__init__.py +23 -0
- bindsight/design/_common.py +501 -0
- bindsight/design/bindcraft.py +78 -0
- bindsight/design/boltzgen.py +79 -0
- bindsight/design/developability.py +140 -0
- bindsight/design/embeddings.py +165 -0
- bindsight/design/prescreen.py +169 -0
- bindsight/design/protocol.py +111 -0
- bindsight/design/rfdiff_mpnn.py +93 -0
- bindsight/epitopes/__init__.py +7 -0
- bindsight/epitopes/surface_bind.py +134 -0
- bindsight/export/__init__.py +7 -0
- bindsight/export/ro_crate.py +595 -0
- bindsight/io/__init__.py +19 -0
- bindsight/io/cbioportal.py +118 -0
- bindsight/io/gdc.py +343 -0
- bindsight/io/paths.py +124 -0
- bindsight/pipelines/__init__.py +9 -0
- bindsight/pipelines/caveats.py +60 -0
- bindsight/pipelines/discover.py +1443 -0
- bindsight/pipelines/full_run.py +410 -0
- bindsight/plugins.py +335 -0
- bindsight/provenance/__init__.py +34 -0
- bindsight/provenance/append.py +199 -0
- bindsight/provenance/fragments.py +216 -0
- bindsight/provenance/manifest.py +506 -0
- bindsight/rank/__init__.py +7 -0
- bindsight/rank/scoring.py +355 -0
- bindsight/report/__init__.py +39 -0
- bindsight/report/format.py +100 -0
- bindsight/report/html.py +645 -0
- bindsight/report/showcase.py +653 -0
- bindsight/report/templates/report.css +67 -0
- bindsight/report/templates/report.html.j2 +420 -0
- bindsight/report/theme.py +231 -0
- bindsight/report/web/__init__.py +35 -0
- bindsight/report/web/app.py +624 -0
- bindsight/report/web/static/binder_viewer.js +245 -0
- bindsight/report/web/static/bindsight.css +455 -0
- bindsight/report/web/static/charts.js +425 -0
- bindsight/report/web/static/vendor/3Dmol-min.js +2 -0
- bindsight/report/web/static/vendor/3Dmol.LICENSE +90 -0
- bindsight/report/web/static/your_data_check.js +370 -0
- bindsight/report/web/templates/base.html.j2 +48 -0
- bindsight/report/web/templates/evidence.html.j2 +341 -0
- bindsight/report/web/templates/overview.html.j2 +140 -0
- bindsight/report/web/templates/runs.html.j2 +125 -0
- bindsight/report/web/templates/try.html.j2 +155 -0
- bindsight/report/web/templates/your_data.html.j2 +46 -0
- bindsight/runners/__init__.py +7 -0
- bindsight/runners/colab.py +158 -0
- bindsight/runners/job_exec.py +1110 -0
- bindsight/runners/kaggle.py +425 -0
- bindsight/runners/kaggle_kernel.py +489 -0
- bindsight/runners/local_docker.py +154 -0
- bindsight/runners/mock.py +167 -0
- bindsight/runners/modal_runner.py +199 -0
- bindsight/runners/notebook.py +116 -0
- bindsight/runners/notebook_content.py +217 -0
- bindsight/runners/protocol.py +81 -0
- bindsight/runners/source_wheel.py +104 -0
- bindsight/runners/tools.py +997 -0
- bindsight/structures/__init__.py +7 -0
- bindsight/structures/alphafolddb.py +93 -0
- bindsight/structures/plddt.py +92 -0
- bindsight/structures/topology.py +227 -0
- bindsight/surfaceome/__init__.py +42 -0
- bindsight/surfaceome/data/surfaceome_extended.tsv +4814 -0
- bindsight/surfaceome/data/surfy_offline.txt +17 -0
- bindsight/surfaceome/data/surfy_v1.ensembl.tsv +3380 -0
- bindsight/surfaceome/data/surfy_v1.uniprot.txt +2901 -0
- bindsight/surfaceome/surfy.py +441 -0
- bindsight/targets/__init__.py +7 -0
- bindsight/targets/ensembl_uniprot.py +56 -0
- bindsight/targets/gtex.py +250 -0
- bindsight/targets/open_targets.py +239 -0
- bindsight/validate/__init__.py +7 -0
- bindsight/validate/af2_ig.py +49 -0
- bindsight/validate/boltz2.py +256 -0
- bindsight/validate/chai1r.py +42 -0
- bindsight/validate/protocol.py +188 -0
- bindsight-0.3.5.data/data/bindsight_demo/config.yaml +54 -0
- bindsight-0.3.5.dist-info/METADATA +495 -0
- bindsight-0.3.5.dist-info/RECORD +106 -0
- bindsight-0.3.5.dist-info/WHEEL +4 -0
- bindsight-0.3.5.dist-info/entry_points.txt +20 -0
- bindsight-0.3.5.dist-info/licenses/COPYRIGHT +24 -0
- 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
|
+
}
|