tfrere HF Staff Cursor commited on
Commit
5da533e
·
1 Parent(s): 4f8aad9

Demo polish: Intro visuals, §3 + §6 charts readability, code-snippet UI

Browse files

Intro tab (§1–§5)
- §1 Atoms: bold green base letters, two-line label (mono A/C/G/T over
full name).
- §2 Helix: redesign à la 3D banner — letters sit *between* the two
strands (helix–base–base–helix), rungs interrupted around them, faded
at crossings instead of culled so there are no bald gaps; thinner
strands, 5'/3' tags symmetric on both ends, tighter top spacing,
more breathing room at the bottom of the card. New base-pair legend:
A=T / G≡C in big green type with H-bond meta + caption.
- §3 + §4 Gene strip: horizontally centred; track-labels now have a
"LEGEND" prefix and §4 gets a duplicated legend.
- §5 Translate / fold / 3D viewer: codon-AA grid + caption centred,
fold-arrow margins trimmed, 3Dmol viewer 340 → 280 px, three-tier
protein caption (sans-serif title 19px / italic desc / mono meta with
clickable PDB link). Slow editorial spin (0.4 → 0.15), wheel events
on the viewer container forwarded to window scroll so the page no
longer traps when the cursor crosses the molecule. Loading retry on
setTimeout in case 3Dmol's deferred CDN script hasn't landed yet.

§3 Variant-effect chart (vep.js)
- viewBox grown ~1.7× (rowH 32 → 56, fonts +50%), variant names + Δ
values readable without zoom.
- Header on two rows: "LOG-LIKELIHOOD DIFFERENCE" centred above
VARIANT / ← LESS LIKELY / MORE LIKELY → so the captions stop
overlapping at column width.
- Bottom caption split into two lines (pathogenic / benign) so it no
longer truncates on the right edge.

§6 Results chart (results.js)
- Replaced the cramped 32-bar grouped-vertical plot with a horizontal
Cleveland layout: 8 task rows × 4 horizontal bars, full-width task
labels, italic-serif category gutter (Generative / Variant effect /
Perturbation / Long-context) with hairline separators on multi-row
groups.
- Two-family palette: Carbon-8B/3B in green, Evo2-7B / GENERator in
warm-neutral grey — "Carbon vs the rest" reads at a glance.
- Numerical labels only on the leader of each row (chevron tinted
Carbon-green when a Carbon model wins, neutral grey otherwise);
faint guideline behind every bar so n/a rows still read as continuous.
- Chart height doubled (viewBox H 638 → 1250) so each bar gets ~24 px
on screen instead of ~12.
- Legend variant `.chart-legend`: sentence-case Inter 12 px with bigger
rounded swatches (was mono uppercase 9 px). Removed the
"higher is better · all scores in %" trailer (the % tick + bar
direction speak for themselves).
- Generic `.legend-swatch` rule (was scoped to `.seq-label`, which
silently broke every other inline legend in the demo).

§2 / §3 Track + completion handles
- track.js: exon/intron rect 6/16 → 12/16, total H 28 → 40, vertically
centred so the track no longer reads as squashed.
- completion.js: triangular drag handles widened (TRI_HW 4 → 9) and
arrows lengthened (TRACK_TOP 8 → 12, ARROW = 12) so the prompt /
generation handles are easy to grab on touch and read clearly.

§6 UMAP default framing (umap.js)
- DEFAULT_VIEW pinned to the bottom-right pan extreme at MIN_SCALE
(with a small Y_EXTRA slack), so visitors land on the densest
framing and only pan up/left into the rest of the manifold rather
than out into empty space.

Code-snippet shared component (code-snippet.js + .css)
- Disclosure header restyled as a pill button: warm-off-white fill,
green `</>` chip on the left, chevron on the right that rotates 90°
when the panel opens. Border + bg shift to green tints on hover
/ open.
- Copy button lifted into the tabs strip (flex-aligned with the
language pills) instead of absolute-positioned.
- Highlight.js Python syntax highlighting wired in (idempotent, no-ops
if the CDN script is blocked).

Tab-lede copy
- Removed parentheses around §X chip anchors in the DNA Lab and
Carbon Recipe tab-ledes for cleaner inline reading.

Co-authored-by: Cursor <cursoragent@cursor.com>

assets/js/sections/completion.js CHANGED
@@ -46,11 +46,14 @@
46
  }
47
 
48
  function renderTrack() {
49
- const W = 1000, H = 40;
50
  if (!gene) { els.track.innerHTML = ""; return; }
51
  const scaleX = (bp) => (bp / gene.length) * W;
52
- // Track body sits y=8..32; arrows live at y=0..8 (start, top) and y=32..40 (end, bottom).
53
- const TRACK_TOP = 8, TRACK_BOT = 32, INTRON_Y = 20, EXON_Y = 14, EXON_H = 12;
 
 
 
54
  let svg = "";
55
  // Background line through introns
56
  svg += `<line class="intron" x1="0" y1="${INTRON_Y}" x2="${W}" y2="${INTRON_Y}"/>`;
@@ -70,20 +73,20 @@
70
  // START handle: vertical line through the track body + downward triangle on top.
71
  svg += `<g class="handle${dragging === "start" ? " dragging" : ""}" data-role="start" transform="translate(${xStart.toFixed(1)},0)">`
72
  + `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
73
- + `<polygon points="-4,0 4,0 0,${TRACK_TOP}"/>`
74
- + `<rect x="-8" y="0" width="16" height="${H}" fill="transparent"/>`
75
  + `</g>`;
76
  // END handle (prompt end / gen start): vertical line + upward triangle on bottom.
77
  svg += `<g class="handle${dragging === "end" ? " dragging" : ""}" data-role="end" transform="translate(${xEnd.toFixed(1)},0)">`
78
  + `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
79
- + `<polygon points="0,${TRACK_BOT} -4,${H} 4,${H}"/>`
80
- + `<rect x="-8" y="0" width="16" height="${H}" fill="transparent"/>`
81
  + `</g>`;
82
  // GEN-END handle: vertical line + downward triangle on top, green.
83
  svg += `<g class="handle gen${dragging === "genend" ? " dragging" : ""}" data-role="genend" transform="translate(${xGenEnd.toFixed(1)},0)">`
84
  + `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
85
- + `<polygon points="-4,0 4,0 0,${TRACK_TOP}"/>`
86
- + `<rect x="-8" y="0" width="16" height="${H}" fill="transparent"/>`
87
  + `</g>`;
88
  els.track.innerHTML = svg;
89
  }
 
46
  }
47
 
48
  function renderTrack() {
49
+ const W = 1000, H = 52;
50
  if (!gene) { els.track.innerHTML = ""; return; }
51
  const scaleX = (bp) => (bp / gene.length) * W;
52
+ // Track body sits y=12..40; arrow tips reach y=0 (start/genend, top) and y=52 (end, bottom).
53
+ const TRACK_TOP = 12, TRACK_BOT = 40, INTRON_Y = 26, EXON_Y = 20, EXON_H = 12;
54
+ // Triangle half-width and arrow vertical run: bumped so the draggable
55
+ // handles read clearly without dominating the timeline body.
56
+ const TRI_HW = 9, ARROW = 12;
57
  let svg = "";
58
  // Background line through introns
59
  svg += `<line class="intron" x1="0" y1="${INTRON_Y}" x2="${W}" y2="${INTRON_Y}"/>`;
 
73
  // START handle: vertical line through the track body + downward triangle on top.
74
  svg += `<g class="handle${dragging === "start" ? " dragging" : ""}" data-role="start" transform="translate(${xStart.toFixed(1)},0)">`
75
  + `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
76
+ + `<polygon points="-${TRI_HW},0 ${TRI_HW},0 0,${ARROW}"/>`
77
+ + `<rect x="-${TRI_HW + 4}" y="0" width="${(TRI_HW + 4) * 2}" height="${H}" fill="transparent"/>`
78
  + `</g>`;
79
  // END handle (prompt end / gen start): vertical line + upward triangle on bottom.
80
  svg += `<g class="handle${dragging === "end" ? " dragging" : ""}" data-role="end" transform="translate(${xEnd.toFixed(1)},0)">`
81
  + `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
82
+ + `<polygon points="0,${TRACK_BOT} -${TRI_HW},${H} ${TRI_HW},${H}"/>`
83
+ + `<rect x="-${TRI_HW + 4}" y="0" width="${(TRI_HW + 4) * 2}" height="${H}" fill="transparent"/>`
84
  + `</g>`;
85
  // GEN-END handle: vertical line + downward triangle on top, green.
86
  svg += `<g class="handle gen${dragging === "genend" ? " dragging" : ""}" data-role="genend" transform="translate(${xGenEnd.toFixed(1)},0)">`
87
  + `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
88
+ + `<polygon points="-${TRI_HW},0 ${TRI_HW},0 0,${ARROW}"/>`
89
+ + `<rect x="-${TRI_HW + 4}" y="0" width="${(TRI_HW + 4) * 2}" height="${H}" fill="transparent"/>`
90
  + `</g>`;
91
  els.track.innerHTML = svg;
92
  }
assets/js/sections/intro.js CHANGED
@@ -31,7 +31,7 @@
31
  return e;
32
  }
33
  function text(parent, x, y, str, opts = {}) {
34
- const t = el("text", {
35
  x, y,
36
  "text-anchor": opts.anchor || "middle",
37
  "dominant-baseline": opts.baseline || "central",
@@ -39,7 +39,9 @@
39
  "font-size": opts.size || 11,
40
  "font-weight": opts.weight || 500,
41
  fill: opts.fill || STROKE,
42
- }, parent);
 
 
43
  t.textContent = str;
44
  return t;
45
  }
@@ -78,7 +80,7 @@
78
  }
79
  function drawAtomLabel(parent, a) {
80
  el("circle", { cx: a.x, cy: a.y, r: 7, fill: "#fff", stroke: "none" }, parent);
81
- text(parent, a.x, a.y, a.label, { size: 9.5, weight: 500 });
82
  }
83
  function molecule(spec) {
84
  const svg = el("svg", { viewBox: spec.viewBox, xmlns: NS });
@@ -163,15 +165,19 @@
163
  // ======================================================================
164
  function buildHelix(slot) {
165
  // 20 bp over 2 turns at ~10 bp / turn (matches real B-DNA scale).
166
- const W = 480, H = 142;
167
- const yc = 72, amp = 30, period = 240;
 
 
 
 
 
168
  const phaseA = 0, phaseB = Math.PI;
169
  const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, xmlns: NS }, slot);
170
 
171
  function yAt(x, phase) { return yc + amp * Math.sin((2 * Math.PI * x) / period + phase); }
172
 
173
  const rungSpacing = 24;
174
- const minSep = 16;
175
  const topSeq = "ATCGGCATCGTAGCCAGTCA";
176
  const comp = { A: "T", T: "A", C: "G", G: "C" };
177
  const rungs = [];
@@ -179,24 +185,63 @@
179
  for (let x = rungSpacing / 2; x < W && seqIdx < topSeq.length; x += rungSpacing) {
180
  const yA = yAt(x, phaseA);
181
  const yB = yAt(x, phaseB);
182
- const sep = Math.abs(yA - yB);
183
- if (sep < minSep) continue;
184
  rungs.push({
185
  x,
186
  yTop: Math.min(yA, yB),
187
  yBot: Math.max(yA, yB),
 
188
  top: topSeq[seqIdx],
189
  bot: comp[topSeq[seqIdx]],
190
  });
191
  seqIdx++;
192
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
- // 1. Rungs behind strands
 
 
 
 
195
  for (const r of rungs) {
196
- el("line", {
197
- x1: r.x, y1: r.yTop + 7, x2: r.x, y2: r.yBot - 7,
198
- stroke: GREEN, "stroke-width": 1.4, "stroke-opacity": 0.5,
199
- }, svg);
 
 
 
 
 
 
 
 
 
 
200
  }
201
 
202
  // 2. Strand paths
@@ -208,22 +253,38 @@
208
  }
209
  return d;
210
  }
211
- el("path", { d: pathD(phaseA), fill: "none", stroke: STROKE, "stroke-width": 2, "stroke-linecap": "round" }, svg);
212
- el("path", { d: pathD(phaseB), fill: "none", stroke: STROKE, "stroke-width": 2, "stroke-linecap": "round" }, svg);
213
 
214
- // 3. Letter haloes + glyphs
 
 
 
 
215
  for (const r of rungs) {
216
- el("circle", { cx: r.x, cy: r.yTop, r: 7.5, fill: "#fff", stroke: STROKE, "stroke-width": 0.8 }, svg);
217
- text(svg, r.x, r.yTop, r.top, { mono: true, size: 10, weight: 600 });
218
- el("circle", { cx: r.x, cy: r.yBot, r: 7.5, fill: "#fff", stroke: STROKE, "stroke-width": 0.8 }, svg);
219
- text(svg, r.x, r.yBot, r.bot, { mono: true, size: 10, weight: 600 });
220
  }
221
 
222
- // 5′ / 3′ end tags
223
- text(svg, 6, yAt(6, phaseA) - 12, "5′", { mono: true, size: 9, weight: 500, fill: FAINT });
224
- text(svg, W - 6, yAt(W - 6, phaseA) - 12, "3′", { mono: true, size: 9, weight: 500, fill: FAINT });
225
- text(svg, 6, yAt(6, phaseB) + 14, "3′", { mono: true, size: 9, weight: 500, fill: FAINT });
226
- text(svg, W - 6, yAt(W - 6, phaseB) + 14, "5′", { mono: true, size: 9, weight: 500, fill: FAINT });
 
 
 
 
 
 
 
 
 
 
 
 
227
  }
228
 
229
  root.querySelectorAll("[data-helix]").forEach(buildHelix);
@@ -247,7 +308,15 @@
247
  function init3DViewer() {
248
  if (viewerInitialised) return;
249
  const container = root.querySelector("#cd-protein-3d");
250
- if (!container || !window.$3Dmol) return;
 
 
 
 
 
 
 
 
251
  viewerInitialised = true;
252
 
253
  const loading = container.querySelector(".cd-protein-3d-loading");
@@ -270,7 +339,11 @@
270
  viewer.setStyle({ resn: "HEM" }, { stick: { color: "#b8862c", radius: 0.22 } });
271
  viewer.addStyle({ resn: "HEM", elem: "FE" }, { sphere: { color: "#cd5c2a", radius: 1.0 } });
272
  viewer.zoomTo();
273
- viewer.spin("y", 0.4);
 
 
 
 
274
  viewer.render();
275
  if (loading) loading.remove();
276
  })
@@ -278,6 +351,18 @@
278
  console.error("intro: failed to load hemoglobin PDB:", err);
279
  if (loading) loading.textContent = "failed to load model";
280
  });
 
 
 
 
 
 
 
 
 
 
 
 
281
  }
282
  function maybeInit() {
283
  if (root.classList.contains("active")) init3DViewer();
 
31
  return e;
32
  }
33
  function text(parent, x, y, str, opts = {}) {
34
+ const attrs = {
35
  x, y,
36
  "text-anchor": opts.anchor || "middle",
37
  "dominant-baseline": opts.baseline || "central",
 
39
  "font-size": opts.size || 11,
40
  "font-weight": opts.weight || 500,
41
  fill: opts.fill || STROKE,
42
+ };
43
+ if (opts.opacity != null) attrs["fill-opacity"] = opts.opacity;
44
+ const t = el("text", attrs, parent);
45
  t.textContent = str;
46
  return t;
47
  }
 
80
  }
81
  function drawAtomLabel(parent, a) {
82
  el("circle", { cx: a.x, cy: a.y, r: 7, fill: "#fff", stroke: "none" }, parent);
83
+ text(parent, a.x, a.y, a.label, { size: 9.5, weight: 600, fill: GREEN });
84
  }
85
  function molecule(spec) {
86
  const svg = el("svg", { viewBox: spec.viewBox, xmlns: NS });
 
165
  // ======================================================================
166
  function buildHelix(slot) {
167
  // 20 bp over 2 turns at ~10 bp / turn (matches real B-DNA scale).
168
+ // Vertical layout: brin paths oscillate between y = yc-amp and y = yc+amp
169
+ // (= 22..82). 5'/3' end tags add ySpread+2 = 14px of slack above and
170
+ // below those bounds. H = 96 sits 4px below the lower-most label so the
171
+ // SVG ends almost flush with the legend underneath instead of leaving
172
+ // a fat band of whitespace.
173
+ const W = 480, H = 96;
174
+ const yc = 52, amp = 30, period = 240;
175
  const phaseA = 0, phaseB = Math.PI;
176
  const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, xmlns: NS }, slot);
177
 
178
  function yAt(x, phase) { return yc + amp * Math.sin((2 * Math.PI * x) / period + phase); }
179
 
180
  const rungSpacing = 24;
 
181
  const topSeq = "ATCGGCATCGTAGCCAGTCA";
182
  const comp = { A: "T", T: "A", C: "G", G: "C" };
183
  const rungs = [];
 
185
  for (let x = rungSpacing / 2; x < W && seqIdx < topSeq.length; x += rungSpacing) {
186
  const yA = yAt(x, phaseA);
187
  const yB = yAt(x, phaseB);
 
 
188
  rungs.push({
189
  x,
190
  yTop: Math.min(yA, yB),
191
  yBot: Math.max(yA, yB),
192
+ sep: Math.abs(yA - yB),
193
  top: topSeq[seqIdx],
194
  bot: comp[topSeq[seqIdx]],
195
  });
196
  seqIdx++;
197
  }
198
+ // Depth fade: rungs at the helix waist (small sep) tint towards
199
+ // near-transparent so they read as "behind / out of focus" instead
200
+ // of as front-and-centre rungs. This fills the otherwise-empty gaps
201
+ // at the strand crossings (was previously skipping those rungs
202
+ // outright with a hard minSep filter) while keeping the visible
203
+ // letters concentrated where the helix is wide. Smoothstep echoes
204
+ // the depth-tint logic in the banner helix.
205
+ function fadeForSep(sep) {
206
+ const lo = 12, hi = 28;
207
+ const t = Math.max(0, Math.min(1, (sep - lo) / (hi - lo)));
208
+ // smoothstep, then map to [0.18, 1.0]: the floor stays just visible
209
+ // even at full crossing so the rung never reads as a hard gap.
210
+ const ease = t * t * (3 - 2 * t);
211
+ return 0.18 + 0.82 * ease;
212
+ }
213
+
214
+ // Letter glyphs sit *between* the strands (not on them), echoing the
215
+ // banner helix's [strand]─letter─letter─[strand] layout. The two letters
216
+ // along each rung are pinned at 25% / 75% of the rung's vertical span so
217
+ // they sit clearly inside the rung rather than on either strand.
218
+ const LETTER_GAP = 6; // half-height of the slot punched out around each glyph
219
+ const STRAND_HALO = 3; // pull rungs back from the strand path so they don't kiss it
220
+ function rungLetterYs(r) {
221
+ const sep = r.yBot - r.yTop;
222
+ return [r.yTop + sep * 0.25, r.yTop + sep * 0.75];
223
+ }
224
 
225
+ // 1. Rungs behind strands. Each rung becomes up to three short segments,
226
+ // interrupted around each letter so the letter reads as sitting *in*
227
+ // the rung. Rungs at the helix waist still get drawn but with their
228
+ // opacity tapered via fadeForSep so the crossings don't read as
229
+ // bald gaps.
230
  for (const r of rungs) {
231
+ const fade = fadeForSep(r.sep);
232
+ const [yA, yB] = rungLetterYs(r);
233
+ const segs = [
234
+ [r.yTop + STRAND_HALO, yA - LETTER_GAP],
235
+ [yA + LETTER_GAP, yB - LETTER_GAP],
236
+ [yB + LETTER_GAP, r.yBot - STRAND_HALO],
237
+ ];
238
+ for (const [y1, y2] of segs) {
239
+ if (y2 - y1 < 1) continue;
240
+ el("line", {
241
+ x1: r.x, y1, x2: r.x, y2,
242
+ stroke: GREEN, "stroke-width": 1.4, "stroke-opacity": 0.55 * fade,
243
+ }, svg);
244
+ }
245
  }
246
 
247
  // 2. Strand paths
 
253
  }
254
  return d;
255
  }
256
+ el("path", { d: pathD(phaseA), fill: "none", stroke: STROKE, "stroke-width": 1.5, "stroke-linecap": "round" }, svg);
257
+ el("path", { d: pathD(phaseB), fill: "none", stroke: STROKE, "stroke-width": 1.5, "stroke-linecap": "round" }, svg);
258
 
259
+ // 3. Letter glyphs themselves (no haloes, since the rung is already
260
+ // interrupted around each letter). fade tracks fadeForSep so the
261
+ // glyphs at the crossings sit as ghosted "out of focus" letters
262
+ // rather than disappearing entirely — fills the visual gap at the
263
+ // helix waist without competing with the rungs at full opacity.
264
  for (const r of rungs) {
265
+ const fade = fadeForSep(r.sep);
266
+ const [yA, yB] = rungLetterYs(r);
267
+ text(svg, r.x, yA, r.top, { mono: true, size: 10, weight: 600, fill: GREEN, opacity: fade });
268
+ text(svg, r.x, yB, r.bot, { mono: true, size: 10, weight: 600, fill: GREEN, opacity: fade });
269
  }
270
 
271
+ // 5′ / 3′ end tags. Each label is placed *outside* the higher/lower
272
+ // strand at that x i.e. min/max over the two strands at the edge
273
+ // rather than tied to one specific strand. The previous logic pinned
274
+ // 5′ to phaseA (above) and 3′ to phaseB (below), which made the two
275
+ // labels collapse vertically wherever the strands happened to cross
276
+ // close to the SVG edge (left side: ~17px apart) while staying nicely
277
+ // spread on the side where they didn't (right side: ~35px). Using
278
+ // min/max instead gives a symmetric ~35px gap on both ends regardless
279
+ // of where in the helix cycle the edge falls.
280
+ const ySpread = 12;
281
+ const yA0 = yAt(6, phaseA), yB0 = yAt(6, phaseB);
282
+ const yA1 = yAt(W - 6, phaseA), yB1 = yAt(W - 6, phaseB);
283
+ const endTagOpts = { mono: true, size: 9, weight: 400, fill: FAINT, opacity: 0.55 };
284
+ text(svg, 6, Math.min(yA0, yB0) - ySpread, "5′", endTagOpts);
285
+ text(svg, W - 6, Math.min(yA1, yB1) - ySpread, "3′", endTagOpts);
286
+ text(svg, 6, Math.max(yA0, yB0) + ySpread + 2, "3′", endTagOpts);
287
+ text(svg, W - 6, Math.max(yA1, yB1) + ySpread + 2, "5′", endTagOpts);
288
  }
289
 
290
  root.querySelectorAll("[data-helix]").forEach(buildHelix);
 
308
  function init3DViewer() {
309
  if (viewerInitialised) return;
310
  const container = root.querySelector("#cd-protein-3d");
311
+ if (!container) return;
312
+ // 3Dmol is loaded as <script defer> from a CDN, so when intro.js runs
313
+ // synchronously during the body parse the global may not be ready yet.
314
+ // Retry on a short timer rather than giving up, otherwise the viewer
315
+ // never initialises and the panel is stuck on "loading hemoglobin…".
316
+ if (!window.$3Dmol) {
317
+ setTimeout(init3DViewer, 50);
318
+ return;
319
+ }
320
  viewerInitialised = true;
321
 
322
  const loading = container.querySelector(".cd-protein-3d-loading");
 
339
  viewer.setStyle({ resn: "HEM" }, { stick: { color: "#b8862c", radius: 0.22 } });
340
  viewer.addStyle({ resn: "HEM", elem: "FE" }, { sphere: { color: "#cd5c2a", radius: 1.0 } });
341
  viewer.zoomTo();
342
+ // Slow editorial spin: 0.15 deg / tick at 3Dmol's ~30 fps render
343
+ // = ~4.5 deg/sec, a full rotation every ~80 seconds. Slow enough
344
+ // that the molecule reads as "alive" in peripheral vision but
345
+ // never demands attention while the visitor reads the prose.
346
+ viewer.spin("y", 0.15);
347
  viewer.render();
348
  if (loading) loading.remove();
349
  })
 
351
  console.error("intro: failed to load hemoglobin PDB:", err);
352
  if (loading) loading.textContent = "failed to load model";
353
  });
354
+
355
+ // 3Dmol installs a wheel listener on its internal canvas that zooms
356
+ // the camera AND preventDefaults the page scroll, which traps the
357
+ // page scroll whenever the cursor is over the viewer (the molecule
358
+ // sits mid-page so this hits constantly). Intercept wheel on the
359
+ // container in capture phase and stopImmediatePropagation so 3Dmol
360
+ // never sees the event — no preventDefault → the browser's native
361
+ // scroll (with trackpad momentum etc.) runs untouched. Pan / rotate
362
+ // (mouse-drag) are unaffected. Same pattern as §5 folding.
363
+ container.addEventListener("wheel", (e) => {
364
+ e.stopImmediatePropagation();
365
+ }, { capture: true, passive: true });
366
  }
367
  function maybeInit() {
368
  if (root.classList.contains("active")) init3DViewer();
assets/js/sections/results.js CHANGED
@@ -1,114 +1,196 @@
1
  // =========================================================================
2
- // §12, Results: training-free benchmark barplot
3
  // =========================================================================
4
  // Numbers come straight from HuggingFaceBio/plots-release-code/barplot/barplot.py
5
  // (the canonical paper figure source). NIAH @ 393 kbp matches the
6
  // niah_heatmap CSV (yarn4x · plain · 65,536 tokens).
 
 
 
 
 
 
 
 
7
  (function initDemo12() {
8
  const host = document.getElementById("d12-bars");
9
  if (!host) return;
10
 
 
 
 
 
 
11
  const MODELS = [
12
- { name: "Carbon 8B", color: "#1A7A40" },
13
- { name: "Carbon 3B", color: "#6DBF7E" },
14
- { name: "Evo2-7B", color: "#8C7355" },
15
- { name: "GENERator-v2 3B", color: "#C8BC99" },
16
  ];
17
 
18
- // [task short-label, category, scores by model in same order as MODELS]
19
- // null = not evaluated / not applicable
 
20
  const ROWS = [
21
- { task: "Sequence\nRecovery", cat: "Generative", vals: [64.03, 61.50, 59.83, 55.72] },
22
- { task: "BRCA2", cat: "Variant effect", vals: [85.60, 84.64, 83.52, 80.57] },
23
- { task: "TraitGym\nMendelian", cat: "Variant effect", vals: [36.81, 34.24, 37.36, 20.68] },
24
- { task: "ClinVar\nCoding", cat: "Variant effect", vals: [93.43, 93.30, 93.70, 91.98] },
25
- { task: "ClinVar\nNon-coding", cat: "Variant effect", vals: [91.98, 91.56, 90.03, 90.61] },
26
- { task: "Triplet\nExpansion", cat: "Perturbation", vals: [65.62, 65.94, 63.78, 49.82] },
27
- { task: "Synonymous\nCodons", cat: "Perturbation", vals: [92.18, 82.78, 84.90, 74.08] },
28
- { task: "Genome-NIAH\n393 kbp", cat: "Long-context", vals: [86.00, 79.00, 80.00, null] },
29
  ];
30
 
 
 
 
 
 
 
 
 
 
 
31
  function renderBars() {
32
- const W = 1280, H = 540;
33
- const padL = 56, padR = 24, padT = 24, padB = 90;
34
- const innerW = W - padL - padR;
35
- const innerH = H - padT - padB;
36
- const groupW = innerW / ROWS.length;
37
- const groupBarW = groupW * 0.76;
38
- const barGap = 2;
39
- const barW = (groupBarW - (MODELS.length - 1) * barGap) / MODELS.length;
40
-
41
- let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMinYMin meet" style="display:block;width:100%;height:auto;background:#fff;border:1px solid #eee">`;
42
-
43
- // y-axis gridlines + ticks (0-100, every 20)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  for (let tick = 0; tick <= 100; tick += 20) {
45
- const y = padT + innerH * (1 - tick / 100);
46
- if (tick !== 0) {
47
- svg += `<line x1="${padL}" y1="${y}" x2="${W - padR}" y2="${y}" stroke="#eee" stroke-width="1"/>`;
 
48
  }
49
- svg += `<text x="${padL - 10}" y="${y + 4}" font-family="JetBrains Mono" font-size="10" fill="#888" text-anchor="end">${tick}</text>`;
50
  }
51
- // axes
52
- svg += `<line x1="${padL}" y1="${padT}" x2="${padL}" y2="${padT + innerH}" stroke="#1f1f1d" stroke-width="1"/>`;
53
- svg += `<line x1="${padL}" y1="${padT + innerH}" x2="${W - padR}" y2="${padT + innerH}" stroke="#1f1f1d" stroke-width="1"/>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- // Group separators + bars + per-task best-star
56
  ROWS.forEach((row, ri) => {
57
- const groupLeft = padL + ri * groupW;
58
- if (ri > 0) {
59
- svg += `<line x1="${groupLeft}" y1="${padT}" x2="${groupLeft}" y2="${padT + innerH}" stroke="#f0f0f0" stroke-width="1"/>`;
60
- }
61
- const barsLeft = groupLeft + (groupW - groupBarW) / 2;
62
  const present = row.vals.filter(v => v !== null);
63
  const best = Math.max(...present);
64
 
 
 
 
 
 
65
  MODELS.forEach((m, mi) => {
 
66
  const v = row.vals[mi];
67
- const x = barsLeft + mi * (barW + barGap);
 
 
 
 
68
  if (v === null) {
69
- svg += `<text x="${x + barW / 2}" y="${padT + innerH + 14}" font-family="JetBrains Mono" font-size="9" fill="#aaa" text-anchor="middle">n/a</text>`;
 
 
 
70
  return;
71
  }
72
- const h = innerH * (v / 100);
73
- const y = padT + innerH - h;
74
- svg += `<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${barW.toFixed(1)}" height="${h.toFixed(1)}" fill="${m.color}"/>`;
75
- // value label on top of bar
76
- svg += `<text x="${(x + barW / 2).toFixed(1)}" y="${(y - 4).toFixed(1)}" font-family="JetBrains Mono" font-size="9" fill="${m.color}" text-anchor="middle" font-weight="500">${v.toFixed(1)}</text>`;
77
- // tiny ▲ on the best bar in this group
78
- if (v === best) {
79
- svg += `<text x="${(x + barW / 2).toFixed(1)}" y="${(y - 16).toFixed(1)}" font-family="JetBrains Mono" font-size="9" fill="#1f1f1d" text-anchor="middle">▲</text>`;
80
- }
81
- });
82
 
83
- // task short label below x-axis (two lines)
84
- const labelX = groupLeft + groupW / 2;
85
- const lines = row.task.split("\n");
86
- lines.forEach((line, li) => {
87
- svg += `<text x="${labelX.toFixed(1)}" y="${(padT + innerH + 18 + li * 13).toFixed(1)}" font-family="JetBrains Mono" font-size="10" fill="#1f1f1d" text-anchor="middle">${line}</text>`;
 
 
 
 
 
 
 
 
88
  });
89
- });
90
 
91
- // Category brackets along the bottom
92
- const cats = [];
93
- ROWS.forEach((row, ri) => {
94
- const last = cats[cats.length - 1];
95
- if (last && last.cat === row.cat) { last.end = ri; }
96
- else { cats.push({ cat: row.cat, start: ri, end: ri }); }
97
  });
98
- const bracketY = padT + innerH + 56;
99
- cats.forEach(c => {
100
- const x1 = padL + c.start * groupW + 6;
101
- const x2 = padL + (c.end + 1) * groupW - 6;
102
- svg += `<line x1="${x1}" y1="${bracketY}" x2="${x2}" y2="${bracketY}" stroke="#888" stroke-width="1"/>`;
103
- svg += `<line x1="${x1}" y1="${bracketY - 3}" x2="${x1}" y2="${bracketY + 3}" stroke="#888" stroke-width="1"/>`;
104
- svg += `<line x1="${x2}" y1="${bracketY - 3}" x2="${x2}" y2="${bracketY + 3}" stroke="#888" stroke-width="1"/>`;
105
- const mid = (x1 + x2) / 2;
106
- svg += `<text x="${mid.toFixed(1)}" y="${bracketY + 16}" font-family="Georgia, serif" font-size="11" font-style="italic" fill="#666" text-anchor="middle">${c.cat}</text>`;
 
 
 
 
 
 
 
107
  });
108
 
109
  svg += `</svg>`;
110
  host.innerHTML = svg;
111
  }
112
 
 
 
 
 
 
 
 
 
113
  renderBars();
114
  })();
 
1
  // =========================================================================
2
+ // §12, Results: training-free benchmark — horizontal Cleveland bar chart
3
  // =========================================================================
4
  // Numbers come straight from HuggingFaceBio/plots-release-code/barplot/barplot.py
5
  // (the canonical paper figure source). NIAH @ 393 kbp matches the
6
  // niah_heatmap CSV (yarn4x · plain · 65,536 tokens).
7
+ //
8
+ // Layout rationale: the previous vertical grouped-bar plot crammed 32 bars
9
+ // into ~750 px of column width; differences were visually crushed and labels
10
+ // were two-line mono at 10 px. The horizontal Cleveland format gives each
11
+ // task its own row with a fully readable label, lets the reader compare
12
+ // models at a glance per row, and keeps the "Carbon vs baselines" message
13
+ // front-and-centre via a two-family palette (greens for Carbon, neutrals
14
+ // for the rest).
15
  (function initDemo12() {
16
  const host = document.getElementById("d12-bars");
17
  if (!host) return;
18
 
19
+ // Two-family palette: Carbon variants in editorial green (the demo's brand
20
+ // hue), baselines in warm neutral grey. The point is that the reader can
21
+ // tell "Carbon vs the rest" at a glance — the previous brown/beige pair
22
+ // for Evo2 / GENERator was too close in luminance to the green pair for
23
+ // that to register.
24
  const MODELS = [
25
+ { name: "Carbon-8B", color: "#1A7A40", isCarbon: true },
26
+ { name: "Carbon-3B", color: "#6DBF7E", isCarbon: true },
27
+ { name: "Evo2-7B", color: "#5A5A56", isCarbon: false },
28
+ { name: "GENERator-v2 3B", color: "#B5B0A6", isCarbon: false },
29
  ];
30
 
31
+ // Rows are ordered by capability axis. Category strings group consecutive
32
+ // rows; the renderer collapses runs of identical category into a single
33
+ // italic gutter label on the left.
34
  const ROWS = [
35
+ { task: "Sequence recovery", cat: "Generative", vals: [64.03, 61.50, 59.83, 55.72] },
36
+ { task: "BRCA2", cat: "Variant effect", vals: [85.60, 84.64, 83.52, 80.57] },
37
+ { task: "TraitGym Mendelian", cat: "Variant effect", vals: [36.81, 34.24, 37.36, 20.68] },
38
+ { task: "ClinVar coding", cat: "Variant effect", vals: [93.43, 93.30, 93.70, 91.98] },
39
+ { task: "ClinVar non-coding", cat: "Variant effect", vals: [91.98, 91.56, 90.03, 90.61] },
40
+ { task: "Triplet expansion", cat: "Perturbation", vals: [65.62, 65.94, 63.78, 49.82] },
41
+ { task: "Synonymous codons", cat: "Perturbation", vals: [92.18, 82.78, 84.90, 74.08] },
42
+ { task: "Genome-NIAH · 393 kbp", cat: "Long-context", vals: [86.00, 79.00, 80.00, null] },
43
  ];
44
 
45
+ // ---- Render ----------------------------------------------------------
46
+ // Layout constants. The SVG renders at viewBox W×H but scales to fit the
47
+ // host width via preserveAspectRatio. Heights are computed from row count
48
+ // so adding/removing tasks doesn't require manual H tweaks.
49
+ //
50
+ // Viewbox aspect ratio: at the previous (1280×638) the rendered chart sat
51
+ // at ~778×388 px in the two-column layout — readable but cramped. We
52
+ // double the internal heights (barH, gaps, fonts) so the viewBox grows
53
+ // to ~1280×1250, which scales to a ~778×760 rendered chart: roughly 2×
54
+ // taller, which makes individual bars and labels comfortable to read.
55
  function renderBars() {
56
+ const W = 1280;
57
+ const padL = 24; // outer left padding (svg edge category gutter)
58
+ const catW = 130; // category gutter width (italic serif label)
59
+ const taskW = 240; // task label column width
60
+ const padR = 90; // room for value label at end of bar
61
+ const padT = 60; // top padding (axis lives here)
62
+ const padB = 36; // bottom padding
63
+
64
+ const barH = 24; // bar thickness
65
+ const barGap = 5; // gap between bars within a task
66
+ const rowH = MODELS.length * barH + (MODELS.length - 1) * barGap; // 4*24 + 3*5 = 111
67
+ const taskGap = 26; // gap between consecutive tasks in same category
68
+ const catGap = 42; // extra gap between category groups
69
+
70
+ // Compute total H by walking rows + inserting catGap when category changes.
71
+ let bodyH = 0;
72
+ ROWS.forEach((row, i) => {
73
+ bodyH += rowH;
74
+ if (i < ROWS.length - 1) {
75
+ bodyH += (ROWS[i + 1].cat === row.cat) ? taskGap : catGap;
76
+ }
77
+ });
78
+ const H = padT + bodyH + padB;
79
+
80
+ const barsX = padL + catW + taskW; // x where bars start
81
+ const barsW = W - barsX - padR; // width of bars zone
82
+ const xForVal = (v) => barsX + barsW * (v / 100);
83
+
84
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMinYMin meet" style="display:block;width:100%;height:auto;background:#fff">`;
85
+
86
+ // ---- Axis (top) --------------------------------------------------
87
+ // Light gridlines every 20%, plus a baseline at the left edge. The
88
+ // x-axis ticks sit at the top of the chart so they don't get lost in
89
+ // the row separators below. We skip the "0" label because the strong
90
+ // baseline at x=0 already conveys it (and the label competed visually
91
+ // with the row labels just to its left).
92
+ const axisY = padT - 28;
93
  for (let tick = 0; tick <= 100; tick += 20) {
94
+ const x = xForVal(tick);
95
+ svg += `<line x1="${x}" y1="${padT - 10}" x2="${x}" y2="${padT + bodyH}" stroke="#eee" stroke-width="1"/>`;
96
+ if (tick > 0) {
97
+ svg += `<text x="${x}" y="${axisY}" font-family="JetBrains Mono" font-size="14" fill="#888" text-anchor="middle">${tick}</text>`;
98
  }
 
99
  }
100
+ svg += `<text x="${xForVal(100) + 12}" y="${axisY}" font-family="JetBrains Mono" font-size="14" fill="#888" text-anchor="start">%</text>`;
101
+
102
+ // Strong baseline at x=0 anchors the bars visually.
103
+ svg += `<line x1="${xForVal(0)}" y1="${padT - 6}" x2="${xForVal(0)}" y2="${padT + bodyH}" stroke="#1f1f1d" stroke-width="1.5"/>`;
104
+
105
+ // ---- Group rows by category for the gutter labels ----------------
106
+ const groups = [];
107
+ ROWS.forEach((row, i) => {
108
+ const last = groups[groups.length - 1];
109
+ if (last && last.cat === row.cat) last.rows.push(i);
110
+ else groups.push({ cat: row.cat, rows: [i] });
111
+ });
112
+
113
+ // ---- Render rows --------------------------------------------------
114
+ let y = padT;
115
+ const rowYs = []; // remember each task's top y for category bracket pass
116
 
 
117
  ROWS.forEach((row, ri) => {
118
+ rowYs.push(y);
119
+ const rowMid = y + rowH / 2;
 
 
 
120
  const present = row.vals.filter(v => v !== null);
121
  const best = Math.max(...present);
122
 
123
+ // Task label (left of bars). Sans-serif, slightly bolder so it reads
124
+ // as the row's title rather than as caption.
125
+ svg += `<text x="${barsX - 16}" y="${rowMid + 5}" font-family="Inter, sans-serif" font-size="18" font-weight="500" fill="#1f1f1d" text-anchor="end">${escapeXml(row.task)}</text>`;
126
+
127
+ // Bars + values for each model in the row.
128
  MODELS.forEach((m, mi) => {
129
+ const barY = y + mi * (barH + barGap);
130
  const v = row.vals[mi];
131
+
132
+ // Faint guideline behind the bar so the row reads as continuous
133
+ // even when a model has a short bar or null.
134
+ svg += `<line x1="${xForVal(0)}" y1="${barY + barH / 2}" x2="${xForVal(100)}" y2="${barY + barH / 2}" stroke="#f5f5f1" stroke-width="1"/>`;
135
+
136
  if (v === null) {
137
+ // n/a marker: dashed segment + faint label, in line with baseline
138
+ // grey so it doesn't pop visually.
139
+ svg += `<line x1="${xForVal(0)}" y1="${barY + barH / 2}" x2="${xForVal(100)}" y2="${barY + barH / 2}" stroke="#d8d4c8" stroke-width="1.5" stroke-dasharray="3 4"/>`;
140
+ svg += `<text x="${xForVal(100) + 12}" y="${barY + barH / 2 + 5}" font-family="JetBrains Mono" font-size="14" fill="#aaa" text-anchor="start">n/a</text>`;
141
  return;
142
  }
 
 
 
 
 
 
 
 
 
 
143
 
144
+ const w = xForVal(v) - xForVal(0);
145
+ svg += `<rect x="${xForVal(0).toFixed(1)}" y="${barY}" width="${w.toFixed(1)}" height="${barH}" fill="${m.color}" rx="2"/>`;
146
+
147
+ // Value label at the end of the bar. Best is bold + dark with a
148
+ // chevron; chevron is tinted Carbon-green when a Carbon model leads
149
+ // (subtle editorial flair appuying the "Carbon leads on 6/8" line)
150
+ // and stays neutral grey when Evo2 wins, to read honestly.
151
+ const isBest = v === best;
152
+ const valColor = isBest ? "#1f1f1d" : "#999";
153
+ const valWeight = isBest ? 600 : 400;
154
+ const chevronColor = isBest ? (m.isCarbon ? "#1A7A40" : "#5A5A56") : null;
155
+ const chevron = chevronColor ? `<tspan fill="${chevronColor}" font-weight="700">▸ </tspan>` : "";
156
+ svg += `<text x="${(xForVal(v) + 10).toFixed(1)}" y="${barY + barH / 2 + 5}" font-family="JetBrains Mono" font-size="15" font-weight="${valWeight}" fill="${valColor}" text-anchor="start">${chevron}${v.toFixed(1)}</text>`;
157
  });
 
158
 
159
+ // Advance y. catGap if next row belongs to a new category.
160
+ y += rowH;
161
+ if (ri < ROWS.length - 1) {
162
+ y += (ROWS[ri + 1].cat === row.cat) ? taskGap : catGap;
163
+ }
 
164
  });
165
+
166
+ // ---- Category gutter labels -------------------------------------
167
+ // One italic serif label per category, vertically centred on its run
168
+ // of rows. Pure typographic separator no boxes, no rules — to keep
169
+ // the chart calm.
170
+ groups.forEach(g => {
171
+ const yTop = rowYs[g.rows[0]];
172
+ const yBot = rowYs[g.rows[g.rows.length - 1]] + rowH;
173
+ const yMid = (yTop + yBot) / 2 + 6;
174
+ svg += `<text x="${padL + 4}" y="${yMid}" font-family="Georgia, serif" font-size="18" font-style="italic" fill="#666" text-anchor="start">${escapeXml(g.cat)}</text>`;
175
+ // Subtle vertical hairline to bracket the category run when it has
176
+ // more than one row. Skip for solo-row categories (Generative,
177
+ // Long-context) — a hairline next to a single row reads as noise.
178
+ if (g.rows.length > 1) {
179
+ svg += `<line x1="${padL + catW - 20}" y1="${yTop + 4}" x2="${padL + catW - 20}" y2="${yBot - 4}" stroke="#d8d4c8" stroke-width="1.5"/>`;
180
+ }
181
  });
182
 
183
  svg += `</svg>`;
184
  host.innerHTML = svg;
185
  }
186
 
187
+ // SVG-safe text. The dataset is hard-coded so this is mostly defensive,
188
+ // but cheap insurance if someone later adds a label with "&" or "<".
189
+ function escapeXml(s) {
190
+ return String(s).replace(/[<>&"']/g, c => ({
191
+ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&#39;"
192
+ }[c]));
193
+ }
194
+
195
  renderBars();
196
  })();
assets/js/sections/track.js CHANGED
@@ -26,17 +26,18 @@
26
  const MAX_WINDOW = 24000; // matches scripts/precompute.py TRACK_MAX_BP
27
 
28
  function renderTrack(scoredLen) {
29
- const W = 1000, H = 28;
30
  if (!gene) { els.track.innerHTML = ""; return; }
31
  const total = scoredLen || gene.length;
32
  const scaleX = (bp) => (bp / total) * W;
 
33
  let svg = "";
34
  svg += `<line class="intron" x1="0" y1="${H/2}" x2="${W}" y2="${H/2}"/>`;
35
  for (const e of gene.exons) {
36
  if (e.start > total) continue;
37
  const x = scaleX(e.start);
38
  const w = Math.max(1, scaleX(Math.min(e.end, total) - e.start));
39
- svg += `<rect class="exon" x="${x.toFixed(1)}" y="6" width="${w.toFixed(1)}" height="16"/>`;
40
  }
41
  els.track.innerHTML = svg;
42
  }
 
26
  const MAX_WINDOW = 24000; // matches scripts/precompute.py TRACK_MAX_BP
27
 
28
  function renderTrack(scoredLen) {
29
+ const W = 1000, H = 40;
30
  if (!gene) { els.track.innerHTML = ""; return; }
31
  const total = scoredLen || gene.length;
32
  const scaleX = (bp) => (bp / total) * W;
33
+ const EXON_H = 16, EXON_Y = (H - EXON_H) / 2; // vertically centered
34
  let svg = "";
35
  svg += `<line class="intron" x1="0" y1="${H/2}" x2="${W}" y2="${H/2}"/>`;
36
  for (const e of gene.exons) {
37
  if (e.start > total) continue;
38
  const x = scaleX(e.start);
39
  const w = Math.max(1, scaleX(Math.min(e.end, total) - e.start));
40
+ svg += `<rect class="exon" x="${x.toFixed(1)}" y="${EXON_Y}" width="${w.toFixed(1)}" height="${EXON_H}"/>`;
41
  }
42
  els.track.innerHTML = svg;
43
  }
assets/js/sections/umap.js CHANGED
@@ -161,7 +161,18 @@
161
  // zoom-out (scale ≥ MIN_SCALE in the wheel handler) so visitors can pan
162
  // around but can never zoom *back* into the empty-margins view.
163
  const MIN_SCALE = 1.35;
164
- const DEFAULT_VIEW = { tx: -0.18, ty: 0.15, scale: MIN_SCALE };
 
 
 
 
 
 
 
 
 
 
 
165
  let view = { ...DEFAULT_VIEW };
166
  let dpr = Math.max(1, window.devicePixelRatio || 1);
167
  let needsRedraw = false;
@@ -721,7 +732,11 @@
721
  view.tx = 0; view.ty = 0;
722
  } else {
723
  view.tx = Math.max(-m, Math.min(m, view.tx));
724
- view.ty = Math.max(-m, Math.min(m, view.ty));
 
 
 
 
725
  }
726
  }
727
 
 
161
  // zoom-out (scale ≥ MIN_SCALE in the wheel handler) so visitors can pan
162
  // around but can never zoom *back* into the empty-margins view.
163
  const MIN_SCALE = 1.35;
164
+ // Default framing = the bottom-right *extreme* reachable by pan at MIN_SCALE.
165
+ // clampPan() allows |tx|, |ty| ≤ 0.92·scale − 1 before whitespace would creep
166
+ // in at an edge; pinning the default to that exact corner means visitors land
167
+ // on the densest framing possible, and from the reset state they can only
168
+ // pan up / left into the rest of the layout — never down / right into empty
169
+ // space they shouldn't be looking at.
170
+ // Y_EXTRA: extra downward pan slack (in NDC) so the editorial framing can sit
171
+ // a touch below the natural bottom clamp — trades a thin sliver of whitespace
172
+ // along the bottom for a heavier anchor on the bottom-right clusters.
173
+ const PAN_EDGE = 0.92 * MIN_SCALE - 1;
174
+ const Y_EXTRA = 0.08;
175
+ const DEFAULT_VIEW = { tx: -PAN_EDGE, ty: PAN_EDGE + Y_EXTRA, scale: MIN_SCALE };
176
  let view = { ...DEFAULT_VIEW };
177
  let dpr = Math.max(1, window.devicePixelRatio || 1);
178
  let needsRedraw = false;
 
732
  view.tx = 0; view.ty = 0;
733
  } else {
734
  view.tx = Math.max(-m, Math.min(m, view.tx));
735
+ // Asymmetric Y clamp: +Y_EXTRA of slack on the downward side so the
736
+ // editorial default framing (ty = PAN_EDGE + Y_EXTRA) survives a redraw
737
+ // without snapping back to PAN_EDGE. Visitors can also pan that extra
738
+ // sliver down themselves — accepted whitespace cost.
739
+ view.ty = Math.max(-m, Math.min(m + Y_EXTRA, view.ty));
740
  }
741
  }
742
 
assets/js/sections/vep.js CHANGED
@@ -224,7 +224,19 @@
224
 
225
  function renderForestBars() {
226
  if (!VARIANTS) return;
227
- const W = 1000, rowH = 32, padL = 280, padR = 60, padT = 36, padB = 50;
 
 
 
 
 
 
 
 
 
 
 
 
228
  // Sort variants by Δ ascending (most surprising-to-the-model first), but
229
  // keep unscored ones at the bottom in their original order.
230
  const indexed = VARIANTS.map((v, i) => ({ v, idx: i, d: cache[v.rs] ? cache[v.rs].altSum - cache[v.rs].refSum : null }));
@@ -260,26 +272,34 @@
260
  // gray → charcoal
261
  return `rgb(${lerp(170, 40, t)},${lerp(170, 40, t)},${lerp(170, 40, t)})`;
262
  }
263
- const VALUE_INSIDE_MIN = 44;
 
264
 
265
  let svg = "";
266
 
267
  // --- Top axis: directional caption ---
268
- svg += `<text x="${padL.toFixed(1)}" y="14" font-family="JetBrains Mono" font-size="9" fill="#bc2e25" letter-spacing="1">← MUTATION LESS LIKELY</text>`;
269
- svg += `<text x="${(W - padR).toFixed(1)}" y="14" font-family="JetBrains Mono" font-size="9" fill="#317f3f" letter-spacing="1" text-anchor="end">MUTATION MORE LIKELY →</text>`;
270
- svg += `<text x="${center.toFixed(1)}" y="14" font-family="JetBrains Mono" font-size="9" fill="#888" text-anchor="middle" letter-spacing="1">log-likelihood difference</text>`;
271
- svg += `<text x="${(padL - 12).toFixed(1)}" y="14" font-family="JetBrains Mono" font-size="9" fill="#888" text-anchor="end" letter-spacing="1">VARIANT</text>`;
 
 
 
 
 
 
 
272
 
273
  // Faint shading: pathogenic-expected zone (left of 0)
274
- svg += `<rect x="${padL.toFixed(1)}" y="${(padT - 4).toFixed(1)}" width="${(center - padL).toFixed(1)}" height="${(ordered.length * rowH + 8).toFixed(1)}" fill="#bc2e25" opacity="0.04"/>`;
275
 
276
  // Center line
277
- svg += `<line x1="${center}" y1="${padT - 4}" x2="${center}" y2="${H - padB + 4}" stroke="#bbb" stroke-width="1"/>`;
278
  // Axis ticks
279
  for (const t of [-absMax, -absMax/2, 0, absMax/2, absMax]) {
280
  const x = center + t * scale;
281
- svg += `<line x1="${x.toFixed(1)}" y1="${(H - padB).toFixed(1)}" x2="${x.toFixed(1)}" y2="${(H - padB + 4).toFixed(1)}" stroke="#aaa"/>`;
282
- svg += `<text x="${x.toFixed(1)}" y="${(H - padB + 14).toFixed(1)}" font-family="JetBrains Mono" font-size="9" fill="#888" text-anchor="middle">${t.toFixed(1)}</text>`;
283
  }
284
 
285
  // --- Rows ---
@@ -287,16 +307,16 @@
287
  const y = padT + i * rowH + rowH / 2;
288
 
289
  // Curated category dot next to the variant name
290
- const dotR = 4;
291
- const dotX = padL - 12 - dotR;
292
  svg += `<circle cx="${dotX.toFixed(1)}" cy="${(y - 0.5).toFixed(1)}" r="${dotR}" fill="${sigColor(v.sig)}"><title>${v.sig}</title></circle>`;
293
 
294
  // Variant name + tiny category label
295
- svg += `<text x="${(dotX - dotR - 6).toFixed(1)}" y="${(y - 1).toFixed(1)}" font-family="JetBrains Mono" font-size="11" fill="#222" text-anchor="end">${v.name}</text>`;
296
- svg += `<text x="${(dotX - dotR - 6).toFixed(1)}" y="${(y + 11).toFixed(1)}" font-family="JetBrains Mono" font-size="9" fill="${sigColor(v.sig)}" text-anchor="end">${v.sig.toLowerCase()}</text>`;
297
 
298
  if (d == null) {
299
- svg += `<text x="${(center + 6).toFixed(1)}" y="${(y + 4).toFixed(1)}" font-family="JetBrains Mono" font-size="10" fill="#ccc">not scored</text>`;
300
  return;
301
  }
302
 
@@ -304,24 +324,30 @@
304
  const color = barColor(d);
305
  const barX = Math.min(center, x);
306
  const barW = Math.max(2, Math.abs(x - center));
307
- svg += `<rect x="${barX.toFixed(1)}" y="${(y - 8).toFixed(1)}" width="${barW.toFixed(1)}" height="14" fill="${color}" stroke="${v === selected ? '#1f1f1d' : 'none'}" stroke-width="${v === selected ? 1 : 0}"/>`;
308
 
309
  const label = (d >= 0 ? "+" : "") + d.toFixed(2);
310
  const insideOK = barW >= VALUE_INSIDE_MIN && Math.abs(d) >= 0.5; // color is dark enough only away from neutral
311
  if (insideOK) {
312
- const tx = x + (d >= 0 ? -5 : 5);
313
  const anchor = d >= 0 ? "end" : "start";
314
- svg += `<text x="${tx.toFixed(1)}" y="${(y + 3.5).toFixed(1)}" font-family="JetBrains Mono" font-size="10" fill="#fff" text-anchor="${anchor}" font-weight="500">${label}</text>`;
315
  } else {
316
- const tx = x + (d >= 0 ? 5 : -5);
317
  const anchor = d >= 0 ? "start" : "end";
318
- svg += `<text x="${tx.toFixed(1)}" y="${(y + 3.5).toFixed(1)}" font-family="JetBrains Mono" font-size="10" fill="#333" text-anchor="${anchor}">${label}</text>`;
319
  }
320
  });
321
 
322
  // --- Bottom caption ---
323
- const capY = H - padB + 32;
324
- svg += `<text x="${padL.toFixed(1)}" y="${capY}" font-family="JetBrains Mono" font-size="9" fill="#888" letter-spacing="0.5">pathogenic loss-of-function mutation much less likely · benign / common variants → ≈ even</text>`;
 
 
 
 
 
 
325
 
326
  els.bars.innerHTML = svg;
327
  }
 
224
 
225
  function renderForestBars() {
226
  if (!VARIANTS) return;
227
+ // Layout sized so the SVG renders comfortably tall in the right-hand
228
+ // column of the §3 two-col layout (~828 px of width). The previous
229
+ // (W=1000, rowH=32) viewBox was ~3.6:1 wide which scaled down to ~230 px
230
+ // tall at column width, making the per-row text and bar values squint-
231
+ // small. Bumping rowH and font sizes here, and padding generously,
232
+ // gets us to roughly a 2.2:1 viewBox → ~380 px rendered, where the
233
+ // variant names and ±Δ values are readable without zoom.
234
+ //
235
+ // padT carries two stacked header lines (axis title above, then
236
+ // VARIANT / ← LESS LIKELY / MORE LIKELY → row), so it has to be
237
+ // a bit taller than a single-line caption would need. padB carries
238
+ // the tick row + a two-line bottom caption.
239
+ const W = 1000, rowH = 56, padL = 320, padR = 80, padT = 72, padB = 90;
240
  // Sort variants by Δ ascending (most surprising-to-the-model first), but
241
  // keep unscored ones at the bottom in their original order.
242
  const indexed = VARIANTS.map((v, i) => ({ v, idx: i, d: cache[v.rs] ? cache[v.rs].altSum - cache[v.rs].refSum : null }));
 
272
  // gray → charcoal
273
  return `rgb(${lerp(170, 40, t)},${lerp(170, 40, t)},${lerp(170, 40, t)})`;
274
  }
275
+ const VALUE_INSIDE_MIN = 64;
276
+ const BAR_H = 22;
277
 
278
  let svg = "";
279
 
280
  // --- Top axis: directional caption ---
281
+ // Two lines: the axis title sits above on its own row, then VARIANT
282
+ // / LESS LIKELY / MORE LIKELY share the row below. Splitting these
283
+ // avoids the centre title colliding with the two side-arrow captions
284
+ // when the chart renders at column width (~778 px on screen, where
285
+ // viewBox 1000 px squashes everything to ~0.78 of its declared size).
286
+ const capTopY1 = 20; // axis title row
287
+ const capTopY2 = 46; // VARIANT / arrows row
288
+ svg += `<text x="${center.toFixed(1)}" y="${capTopY1}" font-family="JetBrains Mono" font-size="13" fill="#666" text-anchor="middle" letter-spacing="2" font-weight="500">LOG-LIKELIHOOD DIFFERENCE</text>`;
289
+ svg += `<text x="${(padL - 16).toFixed(1)}" y="${capTopY2}" font-family="JetBrains Mono" font-size="12" fill="#888" text-anchor="end" letter-spacing="1">VARIANT</text>`;
290
+ svg += `<text x="${padL.toFixed(1)}" y="${capTopY2}" font-family="JetBrains Mono" font-size="12" fill="#bc2e25" letter-spacing="1">← MUTATION LESS LIKELY</text>`;
291
+ svg += `<text x="${(W - padR).toFixed(1)}" y="${capTopY2}" font-family="JetBrains Mono" font-size="12" fill="#317f3f" letter-spacing="1" text-anchor="end">MUTATION MORE LIKELY →</text>`;
292
 
293
  // Faint shading: pathogenic-expected zone (left of 0)
294
+ svg += `<rect x="${padL.toFixed(1)}" y="${(padT - 6).toFixed(1)}" width="${(center - padL).toFixed(1)}" height="${(ordered.length * rowH + 12).toFixed(1)}" fill="#bc2e25" opacity="0.04"/>`;
295
 
296
  // Center line
297
+ svg += `<line x1="${center}" y1="${padT - 6}" x2="${center}" y2="${H - padB + 6}" stroke="#bbb" stroke-width="1"/>`;
298
  // Axis ticks
299
  for (const t of [-absMax, -absMax/2, 0, absMax/2, absMax]) {
300
  const x = center + t * scale;
301
+ svg += `<line x1="${x.toFixed(1)}" y1="${(H - padB).toFixed(1)}" x2="${x.toFixed(1)}" y2="${(H - padB + 6).toFixed(1)}" stroke="#aaa"/>`;
302
+ svg += `<text x="${x.toFixed(1)}" y="${(H - padB + 20).toFixed(1)}" font-family="JetBrains Mono" font-size="13" fill="#888" text-anchor="middle">${t.toFixed(1)}</text>`;
303
  }
304
 
305
  // --- Rows ---
 
307
  const y = padT + i * rowH + rowH / 2;
308
 
309
  // Curated category dot next to the variant name
310
+ const dotR = 6;
311
+ const dotX = padL - 16 - dotR;
312
  svg += `<circle cx="${dotX.toFixed(1)}" cy="${(y - 0.5).toFixed(1)}" r="${dotR}" fill="${sigColor(v.sig)}"><title>${v.sig}</title></circle>`;
313
 
314
  // Variant name + tiny category label
315
+ svg += `<text x="${(dotX - dotR - 8).toFixed(1)}" y="${(y - 1).toFixed(1)}" font-family="JetBrains Mono" font-size="16" fill="#222" text-anchor="end">${v.name}</text>`;
316
+ svg += `<text x="${(dotX - dotR - 8).toFixed(1)}" y="${(y + 16).toFixed(1)}" font-family="JetBrains Mono" font-size="12" fill="${sigColor(v.sig)}" text-anchor="end">${v.sig.toLowerCase()}</text>`;
317
 
318
  if (d == null) {
319
+ svg += `<text x="${(center + 8).toFixed(1)}" y="${(y + 5).toFixed(1)}" font-family="JetBrains Mono" font-size="14" fill="#ccc">not scored</text>`;
320
  return;
321
  }
322
 
 
324
  const color = barColor(d);
325
  const barX = Math.min(center, x);
326
  const barW = Math.max(2, Math.abs(x - center));
327
+ svg += `<rect x="${barX.toFixed(1)}" y="${(y - BAR_H / 2).toFixed(1)}" width="${barW.toFixed(1)}" height="${BAR_H}" fill="${color}" stroke="${v === selected ? '#1f1f1d' : 'none'}" stroke-width="${v === selected ? 1 : 0}"/>`;
328
 
329
  const label = (d >= 0 ? "+" : "") + d.toFixed(2);
330
  const insideOK = barW >= VALUE_INSIDE_MIN && Math.abs(d) >= 0.5; // color is dark enough only away from neutral
331
  if (insideOK) {
332
+ const tx = x + (d >= 0 ? -8 : 8);
333
  const anchor = d >= 0 ? "end" : "start";
334
+ svg += `<text x="${tx.toFixed(1)}" y="${(y + 5).toFixed(1)}" font-family="JetBrains Mono" font-size="14" fill="#fff" text-anchor="${anchor}" font-weight="500">${label}</text>`;
335
  } else {
336
+ const tx = x + (d >= 0 ? 8 : -8);
337
  const anchor = d >= 0 ? "start" : "end";
338
+ svg += `<text x="${tx.toFixed(1)}" y="${(y + 5).toFixed(1)}" font-family="JetBrains Mono" font-size="14" fill="#333" text-anchor="${anchor}">${label}</text>`;
339
  }
340
  });
341
 
342
  // --- Bottom caption ---
343
+ // Split across two lines so the full sentence fits at column width
344
+ // without truncating on the right edge. Each line is one half of the
345
+ // dichotomy (pathogenic vs benign) so the visual structure mirrors
346
+ // the meaning.
347
+ const capY1 = H - padB + 44;
348
+ const capY2 = H - padB + 60;
349
+ svg += `<text x="${padL.toFixed(1)}" y="${capY1}" font-family="JetBrains Mono" font-size="12" fill="#888" letter-spacing="0.5">pathogenic loss-of-function → mutation much less likely</text>`;
350
+ svg += `<text x="${padL.toFixed(1)}" y="${capY2}" font-family="JetBrains Mono" font-size="12" fill="#888" letter-spacing="0.5">benign / common variants → about as likely as the original</text>`;
351
 
352
  els.bars.innerHTML = svg;
353
  }
assets/js/shared/code-snippet.js CHANGED
@@ -26,11 +26,33 @@
26
  });
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  function wire(snippet) {
30
  snippet.querySelectorAll(".code-snippet__tab").forEach(tab => {
31
  tab.addEventListener("click", () => activate(snippet, tab.dataset.tab));
32
  });
 
 
 
 
33
  const copyBtn = snippet.querySelector(".code-snippet__copy");
 
 
 
34
  if (copyBtn) {
35
  copyBtn.addEventListener("click", async () => {
36
  const panel = snippet.querySelector(".code-snippet__panel.active");
@@ -47,6 +69,7 @@
47
  } catch (e) { /* clipboard blocked; fail quietly */ }
48
  });
49
  }
 
50
  }
51
 
52
  function init() {
 
26
  });
27
  }
28
 
29
+ // Run every <code> block through highlight.js. Every snippet in the demo
30
+ // is Python, so we hard-tag the language rather than rely on autodetect
31
+ // (which can mis-call short snippets). hljs is loaded via CDN with the
32
+ // same `defer` attribute as this script, but in case CSP / network
33
+ // blocks it we no-op gracefully and the snippets stay plain text.
34
+ function highlight(snippet) {
35
+ if (!window.hljs) return;
36
+ snippet.querySelectorAll(".code-snippet__panel code").forEach(code => {
37
+ if (code.dataset.highlighted) return;
38
+ code.classList.add("language-python");
39
+ try { window.hljs.highlightElement(code); } catch (e) { /* ignore */ }
40
+ code.dataset.highlighted = "1";
41
+ });
42
+ }
43
+
44
  function wire(snippet) {
45
  snippet.querySelectorAll(".code-snippet__tab").forEach(tab => {
46
  tab.addEventListener("click", () => activate(snippet, tab.dataset.tab));
47
  });
48
+ // Lift the copy button into the tabs strip if the markup still has it
49
+ // as a sibling. Lets the strip's flex layout vertically centre it
50
+ // against the tab pills instead of fighting absolute `top` pixels.
51
+ const tabsRow = snippet.querySelector(".code-snippet__tabs");
52
  const copyBtn = snippet.querySelector(".code-snippet__copy");
53
+ if (tabsRow && copyBtn && copyBtn.parentNode !== tabsRow) {
54
+ tabsRow.appendChild(copyBtn);
55
+ }
56
  if (copyBtn) {
57
  copyBtn.addEventListener("click", async () => {
58
  const panel = snippet.querySelector(".code-snippet__panel.active");
 
69
  } catch (e) { /* clipboard blocked; fail quietly */ }
70
  });
71
  }
72
+ highlight(snippet);
73
  }
74
 
75
  function init() {
assets/styles/code-snippet.css CHANGED
@@ -8,39 +8,72 @@
8
  font-size: 11px;
9
  }
10
 
11
- /* Disclosure header. We replace the native triangle with a small green +/−
12
- chip so the affordance reads as a card-level action rather than a tree
13
- node, and we don't lean on em dashes per the writing style. */
 
 
 
14
  .code-snippet > summary {
15
  cursor: pointer;
16
- color: #6b7a6e;
17
- font-weight: 500;
18
  letter-spacing: 1.2px;
19
  text-transform: uppercase;
20
- font-size: 10px;
21
- padding: 6px 0;
22
  list-style: none;
23
  display: inline-flex;
24
  align-items: center;
25
  gap: 10px;
26
  user-select: none;
 
 
 
 
27
  }
28
  .code-snippet > summary::-webkit-details-marker { display: none; }
29
- .code-snippet > summary::before {
30
- content: "+";
31
- display: inline-block;
32
- width: 14px; height: 14px;
33
- text-align: center;
34
  background: #f4f8f4;
35
- border: 1px solid #d4dcc9;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  border-radius: 2px;
37
- color: var(--green);
 
 
 
 
 
 
38
  font-weight: 700;
39
- font-size: 11px;
40
- line-height: 12px;
41
  }
42
- .code-snippet[open] > summary::before {
43
- content: "−";
 
 
 
 
44
  }
45
 
46
  .code-snippet__body {
@@ -48,13 +81,14 @@
48
  border: 1px solid #d4dcc9;
49
  border-radius: 3px;
50
  overflow: hidden;
51
- background: #fbfaf3;
52
  position: relative;
53
  }
54
 
55
  .code-snippet__tabs {
56
  display: flex;
57
- background: #f1ece0;
 
58
  border-bottom: 1px solid #d4dcc9;
59
  }
60
  .code-snippet__tab {
@@ -77,7 +111,7 @@
77
  border-bottom-color: var(--green);
78
  }
79
 
80
- .code-snippet__panel { display: none; }
81
  .code-snippet__panel.active { display: block; }
82
  .code-snippet__panel pre {
83
  margin: 0;
@@ -86,18 +120,79 @@
86
  font-size: 12px;
87
  line-height: 1.55;
88
  color: #1f1f1d;
 
89
  overflow-x: auto;
90
  white-space: pre;
91
  tab-size: 4;
92
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
- /* Copy button (anchored to top-right of the body, above whichever panel
95
- is active). Uses navigator.clipboard.writeText() from the JS helper. */
 
 
 
 
96
  .code-snippet__copy {
97
- position: absolute;
98
- top: 8px; right: 12px;
99
  padding: 4px 10px;
100
- background: rgba(255, 255, 255, 0.85);
101
  border: 1px solid #d4dcc9;
102
  border-radius: 3px;
103
  font-family: inherit;
@@ -107,8 +202,7 @@
107
  text-transform: uppercase;
108
  color: #6b7a6e;
109
  cursor: pointer;
110
- z-index: 1;
111
- transition: color 0.12s, border-color 0.12s;
112
  }
113
- .code-snippet__copy:hover { color: #1f1f1d; }
114
- .code-snippet__copy.copied { color: var(--green); border-color: var(--green); }
 
8
  font-size: 11px;
9
  }
10
 
11
+ /* Disclosure header. Styled as a pill-button so the affordance reads as
12
+ a card-level action rather than a faint inline label: warm-off-white
13
+ fill + outline, a brand-green </> chip on the left to signal "code",
14
+ and a chevron on the right that rotates 90° when the panel is open.
15
+ Border + background shift to brand-green tints on hover / open to
16
+ reinforce state without needing a separate +/− glyph. */
17
  .code-snippet > summary {
18
  cursor: pointer;
19
+ color: #1f1f1d;
20
+ font-weight: 600;
21
  letter-spacing: 1.2px;
22
  text-transform: uppercase;
23
+ font-size: 10.5px;
24
+ padding: 7px 14px 7px 8px;
25
  list-style: none;
26
  display: inline-flex;
27
  align-items: center;
28
  gap: 10px;
29
  user-select: none;
30
+ background: #f7f5ee;
31
+ border: 1px solid #c8c2b3;
32
+ border-radius: 3px;
33
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
34
  }
35
  .code-snippet > summary::-webkit-details-marker { display: none; }
36
+ .code-snippet > summary:hover {
37
+ border-color: var(--green);
 
 
 
38
  background: #f4f8f4;
39
+ }
40
+ .code-snippet[open] > summary {
41
+ border-color: var(--green);
42
+ background: #f4f8f4;
43
+ }
44
+ /* "</>" code chip on the left: solid green pill, white glyph. Same
45
+ visual weight as the brand chips used elsewhere in the page. */
46
+ .code-snippet > summary::before {
47
+ content: "</>";
48
+ display: inline-flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ height: 18px;
52
+ padding: 0 5px;
53
+ background: var(--green);
54
+ color: #ffffff;
55
+ font-family: "JetBrains Mono", monospace;
56
+ font-weight: 700;
57
+ font-size: 10px;
58
+ letter-spacing: -0.5px;
59
  border-radius: 2px;
60
+ }
61
+ /* Chevron on the right, pivots on open via CSS transform. */
62
+ .code-snippet > summary::after {
63
+ content: "›";
64
+ margin-left: 2px;
65
+ color: #6b7a6e;
66
+ font-size: 14px;
67
  font-weight: 700;
68
+ line-height: 0;
69
+ transition: transform 0.15s, color 0.15s;
70
  }
71
+ .code-snippet > summary:hover::after,
72
+ .code-snippet[open] > summary::after {
73
+ color: var(--green);
74
+ }
75
+ .code-snippet[open] > summary::after {
76
+ transform: rotate(90deg);
77
  }
78
 
79
  .code-snippet__body {
 
81
  border: 1px solid #d4dcc9;
82
  border-radius: 3px;
83
  overflow: hidden;
84
+ background: #ffffff;
85
  position: relative;
86
  }
87
 
88
  .code-snippet__tabs {
89
  display: flex;
90
+ align-items: center;
91
+ background: #f7f5ee;
92
  border-bottom: 1px solid #d4dcc9;
93
  }
94
  .code-snippet__tab {
 
111
  border-bottom-color: var(--green);
112
  }
113
 
114
+ .code-snippet__panel { display: none; background: #ffffff; }
115
  .code-snippet__panel.active { display: block; }
116
  .code-snippet__panel pre {
117
  margin: 0;
 
120
  font-size: 12px;
121
  line-height: 1.55;
122
  color: #1f1f1d;
123
+ background: #ffffff;
124
  overflow-x: auto;
125
  white-space: pre;
126
  tab-size: 4;
127
  }
128
+ /* highlight.js sets `background` on the `.hljs` class once it's done
129
+ processing a block. Force-override to transparent so our pure-white
130
+ panel background shows through regardless of which hljs theme (if any)
131
+ gets loaded. */
132
+ .code-snippet__panel pre code,
133
+ .code-snippet__panel pre code.hljs {
134
+ background: transparent;
135
+ padding: 0;
136
+ color: inherit;
137
+ }
138
+
139
+ /* --- highlight.js token palette --------------------------------------
140
+ We intentionally don't load a hljs theme stylesheet. Instead, define
141
+ token colours here so the Python snippets stay on-brand: brand green
142
+ for keywords / builtins, warm amber for strings, blue for numbers,
143
+ italic muted gray for comments. Everything else falls through to the
144
+ default ink colour set on <pre>. */
145
+ .code-snippet__panel .hljs-comment,
146
+ .code-snippet__panel .hljs-quote {
147
+ color: #9a958a;
148
+ font-style: italic;
149
+ }
150
+ .code-snippet__panel .hljs-keyword,
151
+ .code-snippet__panel .hljs-built_in,
152
+ .code-snippet__panel .hljs-literal,
153
+ .code-snippet__panel .hljs-type {
154
+ color: #317f3f;
155
+ }
156
+ .code-snippet__panel .hljs-string,
157
+ .code-snippet__panel .hljs-regexp,
158
+ .code-snippet__panel .hljs-doctag {
159
+ color: #a9762f;
160
+ }
161
+ .code-snippet__panel .hljs-number {
162
+ color: #2c5aa0;
163
+ }
164
+ .code-snippet__panel .hljs-meta,
165
+ .code-snippet__panel .hljs-meta .hljs-string {
166
+ color: #7a6242;
167
+ }
168
+ .code-snippet__panel .hljs-title,
169
+ .code-snippet__panel .hljs-title.function_,
170
+ .code-snippet__panel .hljs-title.class_,
171
+ .code-snippet__panel .hljs-class .hljs-title,
172
+ .code-snippet__panel .hljs-section {
173
+ color: #1f1f1d;
174
+ font-weight: 500;
175
+ }
176
+ .code-snippet__panel .hljs-attr,
177
+ .code-snippet__panel .hljs-params,
178
+ .code-snippet__panel .hljs-variable,
179
+ .code-snippet__panel .hljs-symbol,
180
+ .code-snippet__panel .hljs-operator,
181
+ .code-snippet__panel .hljs-punctuation {
182
+ color: #1f1f1d;
183
+ }
184
 
185
+ /* Copy button. Lives inside .code-snippet__tabs (the JS lifts it there
186
+ on init) so the flex `align-items: center` on the strip handles
187
+ vertical centring against the tab pills. `margin-left: auto` shoves
188
+ it to the right edge. On a pure-white panel background we lean on a
189
+ faint warm fill + border so the chip stays visible without competing
190
+ with the code. */
191
  .code-snippet__copy {
192
+ margin-left: auto;
193
+ margin-right: 12px;
194
  padding: 4px 10px;
195
+ background: #f7f5ee;
196
  border: 1px solid #d4dcc9;
197
  border-radius: 3px;
198
  font-family: inherit;
 
202
  text-transform: uppercase;
203
  color: #6b7a6e;
204
  cursor: pointer;
205
+ transition: color 0.12s, border-color 0.12s, background 0.12s;
 
206
  }
207
+ .code-snippet__copy:hover { color: #1f1f1d; background: #f1ece0; }
208
+ .code-snippet__copy.copied { color: var(--green); border-color: var(--green); background: #ffffff; }
assets/styles/section-intro.css CHANGED
@@ -134,33 +134,87 @@
134
  }
135
  .cd-mol-svg { width: 100%; max-width: 130px; }
136
  .cd-mol-svg svg { display: block; width: 100%; height: auto; }
 
 
 
 
 
137
  .cd-mol-label {
 
 
138
  font-family: "JetBrains Mono", monospace;
139
  font-size: 11px; letter-spacing: 0.06em;
140
  color: var(--ink-soft);
 
 
 
 
 
 
 
 
141
  }
142
- .cd-mol-label b { color: var(--ink); font-weight: 600; margin-right: 6px; }
143
 
144
  /* --- row 2 : DNA helix ------------------------------------------------- */
145
  /* No background/border on the wrapper; the parent .demo card carries
146
  the white frame. The wrapper is just here to host the SVG. */
147
  .cd-helix-wrap svg { display: block; width: 100%; height: auto; }
 
 
 
 
 
148
  .cd-helix-rules {
149
- margin-top: 16px;
150
- display: flex; gap: 22px;
 
 
 
 
 
 
151
  font-family: "JetBrains Mono", monospace;
152
- font-size: 11px; letter-spacing: 0.06em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  color: var(--ink-soft);
 
154
  }
155
- .cd-helix-rules .pair {
156
- display: inline-flex; align-items: center; gap: 4px;
157
- color: var(--ink);
 
 
 
 
 
 
 
158
  }
159
- .cd-helix-rules .pair b { font-weight: 600; color: var(--green); }
160
 
161
  /* --- rows 3 + 4 : gene strip (per-segment bar + letters) --------------- */
 
 
 
 
 
 
162
  .cd-gene-strip {
163
- display: flex; flex-wrap: wrap; align-items: flex-start;
 
 
164
  font-family: "JetBrains Mono", monospace;
165
  font-size: 14px; font-weight: 400;
166
  letter-spacing: 0.06em; line-height: 1.9;
@@ -189,8 +243,11 @@
189
  .cd-genex:last-child .cd-genex-bar { border-top-right-radius: 2px; border-bottom-right-radius: 2px; }
190
 
191
  .cd-track-labels {
192
- display: flex; align-items: center; gap: 14px;
193
- margin-top: 12px;
 
 
 
194
  font-family: "JetBrains Mono", monospace;
195
  font-size: 10px;
196
  letter-spacing: 0.14em; text-transform: uppercase;
@@ -201,24 +258,45 @@
201
  width: 18px; height: 8px; border-radius: 1px;
202
  vertical-align: 1px; margin-right: 6px;
203
  }
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  /* --- row 4 specific : splicing (top strip → arrows → bottom mRNA) ----- */
 
 
 
 
 
206
  .cd-splice {
207
- display: inline-block;
208
- vertical-align: top;
209
  max-width: 100%;
 
210
  }
211
  .cd-splice .cd-gene-strip { flex-wrap: nowrap; margin-bottom: 0; }
212
  .cd-splice-arrows { display: block; width: 100%; }
213
- .cd-mrna-strip { justify-content: center; }
214
 
215
  /* --- row 5 : protein, codon → AA translation table -------------------- */
 
 
 
 
216
  .cd-translate {
217
  display: grid;
218
  grid-template-columns: max-content repeat(10, max-content);
219
  align-items: center; justify-items: center;
220
  column-gap: 6px; row-gap: 4px;
221
- margin-bottom: 14px;
 
222
  }
223
  .cd-trow-label {
224
  justify-self: end;
@@ -280,7 +358,7 @@
280
  .cd-fold-arrow {
281
  display: flex; flex-direction: column;
282
  align-items: center;
283
- margin: 14px 0 16px;
284
  }
285
  .cd-fold-arrow-icon {
286
  font-family: "JetBrains Mono", monospace;
@@ -297,10 +375,12 @@
297
 
298
  /* Container for the 3Dmol.js viewer (id="cd-protein-3d"). The .demo
299
  parent provides the white card; this just supplies a height for the
300
- WebGL canvas. */
 
 
301
  .cd-protein-3d {
302
  width: 100%;
303
- height: 340px;
304
  position: relative;
305
  overflow: hidden;
306
  }
@@ -313,13 +393,53 @@
313
  color: var(--ink-faint);
314
  pointer-events: none;
315
  }
 
 
 
 
 
 
 
 
 
316
  .cd-protein-caption {
317
- margin-top: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  font-family: "JetBrains Mono", monospace;
319
  font-size: 10px;
320
  letter-spacing: 0.14em; text-transform: uppercase;
321
  color: var(--ink-faint);
322
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
  @media (max-width: 720px) {
325
  .cd-mols { grid-template-columns: repeat(2, 1fr); }
 
134
  }
135
  .cd-mol-svg { width: 100%; max-width: 130px; }
136
  .cd-mol-svg svg { display: block; width: 100%; height: auto; }
137
+ /* Two-line label: big mono letter (A / C / G / T) sits above the full
138
+ name. The <b> is the only element in the source — the trailing text
139
+ node "adenine"/etc. becomes an anonymous flex item, so flex column +
140
+ align-items: center stacks them automatically without touching the
141
+ HTML. */
142
  .cd-mol-label {
143
+ display: flex; flex-direction: column;
144
+ align-items: center; gap: 2px;
145
  font-family: "JetBrains Mono", monospace;
146
  font-size: 11px; letter-spacing: 0.06em;
147
  color: var(--ink-soft);
148
+ text-align: center;
149
+ }
150
+ .cd-mol-label b {
151
+ display: block;
152
+ margin: 0;
153
+ font-size: 22px; font-weight: 700;
154
+ letter-spacing: 0.02em; line-height: 1;
155
+ color: var(--ink);
156
  }
 
157
 
158
  /* --- row 2 : DNA helix ------------------------------------------------- */
159
  /* No background/border on the wrapper; the parent .demo card carries
160
  the white frame. The wrapper is just here to host the SVG. */
161
  .cd-helix-wrap svg { display: block; width: 100%; height: auto; }
162
+ /* Pairing legend under the helix. Two centred A=T / G≡C tiles + an
163
+ H-bond sub-label that turns the typographic difference between
164
+ = and ≡ into actual chemistry. Caption below, mono uppercase, faint,
165
+ matching the .cd-protein-caption / .cd-track-labels treatment so the
166
+ primer reads with one consistent caption voice. */
167
  .cd-helix-rules {
168
+ /* Tighter gap to the helix above so the legend reads as a caption to
169
+ the diagram, plus extra slack below to push the whole pair-block
170
+ further from the bottom edge of the .demo card. The .demo's own
171
+ padding-bottom (24px from controls.css) sits underneath this margin
172
+ for ~40px of total breathing room below the caption. */
173
+ margin: 12px 0 16px;
174
+ display: flex; flex-direction: column;
175
+ align-items: center; gap: 12px;
176
  font-family: "JetBrains Mono", monospace;
177
+ }
178
+ .cd-helix-rules-pairs {
179
+ display: flex; gap: 56px; align-items: flex-start;
180
+ }
181
+ .cd-pair {
182
+ display: flex; flex-direction: column;
183
+ align-items: center; gap: 8px;
184
+ }
185
+ .cd-pair-formula {
186
+ display: inline-flex; align-items: center; gap: 10px;
187
+ font-size: 24px; font-weight: 700;
188
+ letter-spacing: 0.04em; line-height: 1;
189
+ color: var(--green);
190
+ }
191
+ .cd-pair-bond {
192
  color: var(--ink-soft);
193
+ font-weight: 500;
194
  }
195
+ .cd-pair-meta {
196
+ font-size: 9.5px; letter-spacing: 0.18em;
197
+ text-transform: uppercase;
198
+ color: var(--ink-faint);
199
+ }
200
+ .cd-pair-caption {
201
+ margin-top: 2px;
202
+ font-size: 10px; letter-spacing: 0.16em;
203
+ text-transform: uppercase;
204
+ color: var(--ink-faint);
205
  }
 
206
 
207
  /* --- rows 3 + 4 : gene strip (per-segment bar + letters) --------------- */
208
+ /* Strip is centred in its parent .demo card, not left-aligned: the gene
209
+ sketch is a self-contained diagram and reads better with breathing room
210
+ on both sides than tucked against the card's left edge. justify-content
211
+ handles the in-flow case (segments fit in one line); margin: 0 auto with
212
+ width: max-content + max-width: 100% covers the inline-block-style
213
+ centring when the strip is shorter than the parent. */
214
  .cd-gene-strip {
215
+ display: flex; flex-wrap: wrap;
216
+ align-items: flex-start; justify-content: center;
217
+ width: max-content; max-width: 100%; margin: 0 auto;
218
  font-family: "JetBrains Mono", monospace;
219
  font-size: 14px; font-weight: 400;
220
  letter-spacing: 0.06em; line-height: 1.9;
 
243
  .cd-genex:last-child .cd-genex-bar { border-top-right-radius: 2px; border-bottom-right-radius: 2px; }
244
 
245
  .cd-track-labels {
246
+ /* margin-top bumped 12 → 28 so the legend reads as a separate caption
247
+ row under the gene strip rather than sitting flush against the
248
+ letters of the sequence. */
249
+ display: flex; align-items: center; justify-content: center; gap: 14px;
250
+ margin-top: 28px;
251
  font-family: "JetBrains Mono", monospace;
252
  font-size: 10px;
253
  letter-spacing: 0.14em; text-transform: uppercase;
 
258
  width: 18px; height: 8px; border-radius: 1px;
259
  vertical-align: 1px; margin-right: 6px;
260
  }
261
+ /* "LEGEND" prefix that labels the row as a key. Slightly darker + tighter
262
+ tracking than the items themselves so the eye groups it as the title
263
+ of the row rather than another item. The margin-right adds a wider
264
+ gap between "LEGEND" and the first swatch on top of the row's gap, so
265
+ the title visually detaches from the items. */
266
+ .cd-track-labels__title {
267
+ color: var(--ink-soft);
268
+ font-weight: 600;
269
+ letter-spacing: 0.2em;
270
+ margin-right: 6px;
271
+ }
272
 
273
  /* --- row 4 specific : splicing (top strip → arrows → bottom mRNA) ----- */
274
+ /* Splice block hosts pre-mRNA + arrow svg + spliced mRNA. We centre the
275
+ whole stack so the three rows line up on a single vertical axis inside
276
+ the parent card. The inner .cd-gene-strip rules above already do the
277
+ centring for the two strips themselves; this block centres the
278
+ transcribe-arrow svg between them too. */
279
  .cd-splice {
280
+ display: block;
281
+ width: max-content;
282
  max-width: 100%;
283
+ margin: 0 auto;
284
  }
285
  .cd-splice .cd-gene-strip { flex-wrap: nowrap; margin-bottom: 0; }
286
  .cd-splice-arrows { display: block; width: 100%; }
 
287
 
288
  /* --- row 5 : protein, codon → AA translation table -------------------- */
289
+ /* Grid sized to its content (label + 10 codon columns) and centred in the
290
+ parent card. The grid itself stays a fixed width regardless of the card
291
+ width, so margin: 0 auto picks up the leftover space symmetrically on
292
+ both sides. */
293
  .cd-translate {
294
  display: grid;
295
  grid-template-columns: max-content repeat(10, max-content);
296
  align-items: center; justify-items: center;
297
  column-gap: 6px; row-gap: 4px;
298
+ width: max-content; max-width: 100%;
299
+ margin: 0 auto 14px;
300
  }
301
  .cd-trow-label {
302
  justify-self: end;
 
358
  .cd-fold-arrow {
359
  display: flex; flex-direction: column;
360
  align-items: center;
361
+ margin: 8px 0 10px;
362
  }
363
  .cd-fold-arrow-icon {
364
  font-family: "JetBrains Mono", monospace;
 
375
 
376
  /* Container for the 3Dmol.js viewer (id="cd-protein-3d"). The .demo
377
  parent provides the white card; this just supplies a height for the
378
+ WebGL canvas. Height trimmed (340 → 280) so the viewer doesn't dwarf
379
+ the rest of the row; 3Dmol's zoomTo() refits the molecule to whatever
380
+ container size it gets, so the molecule still fills the frame. */
381
  .cd-protein-3d {
382
  width: 100%;
383
+ height: 280px;
384
  position: relative;
385
  overflow: hidden;
386
  }
 
393
  color: var(--ink-faint);
394
  pointer-events: none;
395
  }
396
+ /* Three-tier caption under the 3D viewer:
397
+ · title (sans, medium): the protein's common name, page-voice
398
+ · desc (sans, light, italic): one-line plain-language gloss
399
+ · meta (mono, uppercase, faint): chain count + clickable PDB ID
400
+ Mirrors the editorial pattern used by museum / journal captions:
401
+ name → what it is in plain English → the technical reference.
402
+ The mono meta line ties this row back to the rest of the primer's
403
+ captions (cd-pair-caption, cd-track-labels) so the page still has
404
+ one consistent caption voice. */
405
  .cd-protein-caption {
406
+ margin-top: 10px;
407
+ text-align: center;
408
+ display: flex; flex-direction: column;
409
+ align-items: center; gap: 4px;
410
+ }
411
+ .cd-protein-caption__title {
412
+ font-family: "Inter", sans-serif;
413
+ font-size: 19px; font-weight: 500;
414
+ letter-spacing: -0.01em; line-height: 1.2;
415
+ color: var(--ink);
416
+ }
417
+ .cd-protein-caption__desc {
418
+ font-family: "Inter", sans-serif;
419
+ font-size: 13px; font-weight: 300;
420
+ line-height: 1.45;
421
+ color: var(--ink-soft);
422
+ }
423
+ .cd-protein-caption__meta {
424
+ margin-top: 6px;
425
  font-family: "JetBrains Mono", monospace;
426
  font-size: 10px;
427
  letter-spacing: 0.14em; text-transform: uppercase;
428
  color: var(--ink-faint);
429
  }
430
+ .cd-protein-caption__meta a {
431
+ color: inherit;
432
+ text-decoration: underline;
433
+ text-decoration-color: rgba(138, 138, 131, 0.4);
434
+ text-underline-offset: 2px;
435
+ transition: color 0.15s ease, text-decoration-color 0.15s ease;
436
+ }
437
+ .cd-protein-caption__meta a:hover,
438
+ .cd-protein-caption__meta a:focus-visible {
439
+ color: var(--green);
440
+ text-decoration-color: var(--green);
441
+ outline: none;
442
+ }
443
 
444
  @media (max-width: 720px) {
445
  .cd-mols { grid-template-columns: repeat(2, 1fr); }
assets/styles/sequence.css CHANGED
@@ -13,10 +13,10 @@
13
  }
14
  .gene-info strong { color: #1f1f1d; font-weight: 500; }
15
  .gene-track {
16
- width: 100%; height: 28px; display: block;
17
  margin: 4px 0 8px;
18
  }
19
- .gene-track.draggable { height: 40px; touch-action: none; }
20
  .gene-track .exon { fill: #317f3f; }
21
  .gene-track .intron { stroke: #aaa; stroke-width: 1; }
22
  .gene-track .playhead { stroke: #bc2e25; stroke-width: 2; }
@@ -85,10 +85,44 @@
85
  font-size: 9px; color: #888; text-transform: uppercase; letter-spacing: 1.5px;
86
  margin-top: 14px; margin-bottom: 4px; display: flex; gap: 12px; align-items: center;
87
  }
88
- .seq-label .legend-swatch {
 
 
 
 
 
 
89
  display: inline-block; width: 8px; height: 8px; vertical-align: middle;
90
  margin-right: 4px; border-radius: 1px;
91
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  /* Inline tag chips used in §5 to disambiguate carbon vs reference rows.
93
  Same shape/size, different colour band so the eye instantly maps a
94
  row of AAs to the correct identity without re-reading the full label. */
 
13
  }
14
  .gene-info strong { color: #1f1f1d; font-weight: 500; }
15
  .gene-track {
16
+ width: 100%; height: 40px; display: block;
17
  margin: 4px 0 8px;
18
  }
19
+ .gene-track.draggable { height: 52px; touch-action: none; }
20
  .gene-track .exon { fill: #317f3f; }
21
  .gene-track .intron { stroke: #aaa; stroke-width: 1; }
22
  .gene-track .playhead { stroke: #bc2e25; stroke-width: 2; }
 
85
  font-size: 9px; color: #888; text-transform: uppercase; letter-spacing: 1.5px;
86
  margin-top: 14px; margin-bottom: 4px; display: flex; gap: 12px; align-items: center;
87
  }
88
+ /* Generic colour-swatch chip used in inline legends across the demo
89
+ (.seq-label, .track-axis-label, etc.). The class is the obvious
90
+ semantic hook, so the selector is intentionally global rather than
91
+ nested under a single parent — having the rule scoped to .seq-label
92
+ broke every other legend that reused the pattern (e.g. the §6 results
93
+ chart legend), which all rendered as zero-width invisible spans. */
94
+ .legend-swatch {
95
  display: inline-block; width: 8px; height: 8px; vertical-align: middle;
96
  margin-right: 4px; border-radius: 1px;
97
  }
98
+
99
+ /* Chart legend variant of .track-axis-label (used by the §6 results
100
+ bars chart). The base class is mono uppercase 9px which suits a thin
101
+ axis caption; for a 4-model legend that pattern was hard to read.
102
+ The variant keeps the demo's typographic system but bumps the legend
103
+ to sentence-case 12px Inter and gives the swatches more visual weight
104
+ so they read as a proper colour key rather than caption text. */
105
+ .chart-legend {
106
+ font-family: "Inter", sans-serif;
107
+ font-size: 12px;
108
+ text-transform: none;
109
+ letter-spacing: 0;
110
+ color: var(--ink, #1f1f1d);
111
+ padding-top: 16px;
112
+ gap: 22px;
113
+ flex-wrap: wrap;
114
+ align-items: center;
115
+ justify-content: flex-start;
116
+ }
117
+ .chart-legend__item {
118
+ display: inline-flex; align-items: center;
119
+ font-weight: 500;
120
+ }
121
+ .chart-legend__item .legend-swatch {
122
+ width: 14px; height: 14px;
123
+ margin-right: 7px;
124
+ border-radius: 2px;
125
+ }
126
  /* Inline tag chips used in §5 to disambiguate carbon vs reference rows.
127
  Same shape/size, different colour band so the eye instantly maps a
128
  row of AAs to the correct identity without re-reading the full label. */
demo.html CHANGED
@@ -10,6 +10,15 @@
10
  <!-- 3Dmol.js: lightweight WebGL molecular viewer, used by §5 (folding) to
11
  render ESMFold-predicted protein cartoons. Pinned for reproducibility. -->
12
  <script defer src="https://cdn.jsdelivr.net/npm/3dmol@2.5.1/build/3Dmol-min.js"></script>
 
 
 
 
 
 
 
 
 
13
 
14
  <!-- Modular CSS, served from /assets/styles/. Order matters because
15
  several keyframes (pulse) and shared atoms (.seq-block, .seq-label,
@@ -225,10 +234,22 @@
225
  <div class="section-body">
226
  <div class="demo">
227
  <div class="cd-helix-wrap" data-helix></div>
 
 
 
 
228
  <div class="cd-helix-rules">
229
- <span class="pair"><b>A</b> = <b>T</b></span>
230
- <span class="pair"><b>G</b> ≡ <b>C</b></span>
231
- <span style="color: var(--ink-faint)">complementary base pairing</span>
 
 
 
 
 
 
 
 
232
  </div>
233
  </div>
234
  </div>
@@ -249,6 +270,7 @@
249
  <div class="demo">
250
  <div class="cd-gene-strip"><span class="cd-genex cd-genex--promoter"><span class="cd-genex-bar"></span><span class="cd-genex-text">TATAAA</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">ATGGCCGAACTG</span></span><span class="cd-genex cd-genex--intron"><span class="cd-genex-bar"></span><span class="cd-genex-text">GTAAGCATATAG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">CCCGGGTGGTTC</span></span><span class="cd-genex cd-genex--intron"><span class="cd-genex-bar"></span><span class="cd-genex-text">GTACGCCATTAG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AGCCGT</span></span></div>
251
  <div class="cd-track-labels">
 
252
  <span><span class="sw" style="background: var(--promoter)"></span>promoter</span>
253
  <span><span class="sw" style="background: var(--green)"></span>exon</span>
254
  <span><span class="sw" style="background: transparent; border-top: 1px solid var(--intron); height: 1px; margin-top: 4px;"></span>intron</span>
@@ -288,6 +310,12 @@
288
  </svg>
289
  <div class="cd-gene-strip cd-mrna-strip"><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AUGGCCGAACUG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">CCCGGGUGGUUC</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AGCCGU</span></span></div>
290
  </div>
 
 
 
 
 
 
291
  </div>
292
  </div>
293
  </div>
@@ -326,7 +354,11 @@
326
  <div class="cd-protein-3d" id="cd-protein-3d">
327
  <div class="cd-protein-3d-loading">loading hemoglobin…</div>
328
  </div>
329
- <div class="cd-protein-caption">human hemoglobin · 4 chains · the molecule that carries oxygen in your blood · PDB 1A3N</div>
 
 
 
 
330
  </div>
331
  </div>
332
  </div>
@@ -349,14 +381,14 @@
349
  The interesting question is what else falls out of that. We didn't tell Carbon what an
350
  exon is. We didn't tell it which mutations are pathogenic. We didn't tell it how genes
351
  differ between species. The sections below are ways to read what it picked up
352
- anyway: autocomplete a gene (<a class="lede-chip" href="#completion">§1</a>), see
353
- structure emerge in its confidence (<a class="lede-chip" href="#track">§2</a>), score
354
- a disease variant against a healthy one (<a class="lede-chip" href="#vep">§3</a>),
355
- recognise a gene's species of origin (<a class="lede-chip" href="#species">§4</a>),
356
  and then push further into folded protein structure
357
- (<a class="lede-chip" href="#folding">§5</a>), the embedding manifold
358
- (<a class="lede-chip" href="#umap">§6</a>), and the species tree
359
- (<a class="lede-chip" href="#speciesTree">§7</a>). Each demo runs against the public
360
  <code>HuggingFaceBio/Carbon-3B</code> checkpoint behind a live inference endpoint.
361
  </p>
362
  </div>
@@ -376,11 +408,9 @@
376
  intron just before the 2nd exon plus the first 35 bp of that exon, and ask it to
377
  <em>finish the exon</em>. The model streams the remaining bases one 6-base token at a
378
  time. Exons are the protein-coding parts of a gene and are under strong evolutionary
379
- constraint, so they should be the most predictable stretches of DNA. The
380
- draggable&nbsp;▼ markers on the track let you move the prompt and generation windows
381
- anywhere along the gene to try a different stretch. We overlay the <em>real</em>
382
- exon/intron annotations on top of the output so you can compare what Carbon produces to
383
- what's actually there.
384
  </p>
385
  </div>
386
 
@@ -400,7 +430,7 @@
400
  </div>
401
 
402
  <div class="gene-info" id="d1-info">loading genes…</div>
403
- <svg class="gene-track draggable" id="d1-track" viewBox="0 0 1000 40" preserveAspectRatio="none"></svg>
404
  <div class="track-axis-label" style="justify-content:flex-end;gap:20px;align-items:center">
405
  <span class="legend-tip"
406
  data-tip="Exon: coding segment of the gene. Stays in the mature mRNA and gets translated into protein."
@@ -460,9 +490,10 @@
460
  <div class="takeaway">
461
  <p>
462
  <strong>Try it</strong>
463
- Drag the prompt window so the green generated region lands on an exon (the dark green
464
- blocks) and count the green underlines you get. Then try the same length over an
465
- intron and compare.
 
466
  </p>
467
  <p>
468
  <strong>What to look for</strong>
@@ -560,9 +591,9 @@ print(tok.decode(new_ids))</code></pre></div>
560
  </div>
561
 
562
  <div class="gene-info" id="d3-info">loading genes…</div>
563
- <svg class="gene-track" id="d3-track" viewBox="0 0 1000 28" preserveAspectRatio="none"></svg>
564
  <svg id="d3-chart" style="display:block;width:100%;height:140px;background:#fff;border:1px solid #eee;margin-top:6px" preserveAspectRatio="none" viewBox="0 0 1000 140"></svg>
565
- <div class="track-axis-label">
566
  <span><span class="legend-swatch" style="background:#317f3f"></span>exon (shaded)</span>
567
  <span style="color:#aaa">y-axis: log P per 6-bp token (higher = more confident)</span>
568
  <span id="d3-bp-label" style="color:#888">0 bp</span>
@@ -787,7 +818,6 @@ print(f"delta = {delta:+.2f} (less likely if negative)")</code></pre></div>
787
  <div class="demo-toolbar">
788
  <span>gene</span>
789
  <span id="d4-pills" class="pills"></span>
790
- <span class="spacer"></span>
791
  <span>prefix</span>
792
  <span id="d4-prefix-pills" class="pills">
793
  <button class="pill" data-prefix="200">200</button>
@@ -799,6 +829,7 @@ print(f"delta = {delta:+.2f} (less likely if negative)")</code></pre></div>
799
  <button class="pill active" data-gen="60">60</button>
800
  <button class="pill" data-gen="200">200</button>
801
  </span>
 
802
  <button id="d4-go" class="action primary">▶ run all</button>
803
  <span class="status is-hidden" id="d4-status"><span class="dot"></span><span></span></span>
804
  </div>
@@ -1158,15 +1189,15 @@ for name, ids in zip(species_prefixes, new_ids):
1158
  </p>
1159
  <p class="tab-lede__note">
1160
  The sections below walk through each of those choices: how the tokenizer changes
1161
- what a "token" means in DNA (<a class="lede-chip" href="#tokenizer">§1</a>), how
1162
- FNS rescues training in the BF16 regime (<a class="lede-chip" href="#loss">§2</a>),
1163
- what's in the training corpus (<a class="lede-chip" href="#data">§3</a>),
1164
- what the architecture looks like (<a class="lede-chip" href="#architecture">§4</a>),
1165
  how 8k-token pretraining reaches 786 kbp at inference
1166
- (<a class="lede-chip" href="#longcontext">§5</a>), how Carbon stacks up against
1167
  Evo2-7B and GENERator-v2 on the full training-free suite
1168
- (<a class="lede-chip" href="#results">§6</a>), and why the model runs so fast
1169
- (<a class="lede-chip" href="#efficiency">§7</a>).
1170
  </p>
1171
  </div>
1172
  </div>
@@ -1425,12 +1456,11 @@ for name, ids in zip(species_prefixes, new_ids):
1425
  <div class="section-body">
1426
  <div class="demo" id="demo12">
1427
  <div id="d12-bars"></div>
1428
- <div class="track-axis-label" style="padding-top:14px">
1429
- <span><span class="legend-swatch" style="background:#1A7A40"></span>Carbon 8B</span>
1430
- <span><span class="legend-swatch" style="background:#6DBF7E"></span>Carbon 3B</span>
1431
- <span><span class="legend-swatch" style="background:#8C7355"></span>Evo2-7B</span>
1432
- <span><span class="legend-swatch" style="background:#C8BC99"></span>GENERator-v2 3B</span>
1433
- <span style="color:#888">higher is better · all scores in %</span>
1434
  </div>
1435
  </div>
1436
 
 
10
  <!-- 3Dmol.js: lightweight WebGL molecular viewer, used by §5 (folding) to
11
  render ESMFold-predicted protein cartoons. Pinned for reproducibility. -->
12
  <script defer src="https://cdn.jsdelivr.net/npm/3dmol@2.5.1/build/3Dmol-min.js"></script>
13
+ <!-- highlight.js: syntax-highlights the Python snippets inside every
14
+ <details class="code-snippet"> "Run this from code" block. We load
15
+ the official browser distribution from the `cdn-release` repo (the
16
+ /npm/ path serves CommonJS modules that throw `require is not
17
+ defined` in the browser). Bundle ships Python pre-registered. We
18
+ intentionally do NOT load a hljs theme stylesheet, code-snippet.css
19
+ defines our own token colours so the snippets stay on-brand with
20
+ the editorial palette. -->
21
+ <script defer src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
22
 
23
  <!-- Modular CSS, served from /assets/styles/. Order matters because
24
  several keyframes (pulse) and shared atoms (.seq-block, .seq-label,
 
234
  <div class="section-body">
235
  <div class="demo">
236
  <div class="cd-helix-wrap" data-helix></div>
237
+ <!-- Pairing legend: two big A=T / G≡C tiles with an H-bond
238
+ sub-label that turns the visual difference between =
239
+ and ≡ into the actual chemistry (2 vs 3 hydrogen bonds).
240
+ Caption sits below the pair row, centred. -->
241
  <div class="cd-helix-rules">
242
+ <div class="cd-helix-rules-pairs">
243
+ <div class="cd-pair">
244
+ <div class="cd-pair-formula"><span class="cd-pair-letter">A</span><span class="cd-pair-bond">═</span><span class="cd-pair-letter">T</span></div>
245
+ <div class="cd-pair-meta">2 H bonds</div>
246
+ </div>
247
+ <div class="cd-pair">
248
+ <div class="cd-pair-formula"><span class="cd-pair-letter">G</span><span class="cd-pair-bond">≡</span><span class="cd-pair-letter">C</span></div>
249
+ <div class="cd-pair-meta">3 H bonds</div>
250
+ </div>
251
+ </div>
252
+ <div class="cd-pair-caption">complementary base pairing</div>
253
  </div>
254
  </div>
255
  </div>
 
270
  <div class="demo">
271
  <div class="cd-gene-strip"><span class="cd-genex cd-genex--promoter"><span class="cd-genex-bar"></span><span class="cd-genex-text">TATAAA</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">ATGGCCGAACTG</span></span><span class="cd-genex cd-genex--intron"><span class="cd-genex-bar"></span><span class="cd-genex-text">GTAAGCATATAG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">CCCGGGTGGTTC</span></span><span class="cd-genex cd-genex--intron"><span class="cd-genex-bar"></span><span class="cd-genex-text">GTACGCCATTAG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AGCCGT</span></span></div>
272
  <div class="cd-track-labels">
273
+ <span class="cd-track-labels__title">Legend</span>
274
  <span><span class="sw" style="background: var(--promoter)"></span>promoter</span>
275
  <span><span class="sw" style="background: var(--green)"></span>exon</span>
276
  <span><span class="sw" style="background: transparent; border-top: 1px solid var(--intron); height: 1px; margin-top: 4px;"></span>intron</span>
 
310
  </svg>
311
  <div class="cd-gene-strip cd-mrna-strip"><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AUGGCCGAACUG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">CCCGGGUGGUUC</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AGCCGU</span></span></div>
312
  </div>
313
+ <div class="cd-track-labels">
314
+ <span class="cd-track-labels__title">Legend</span>
315
+ <span><span class="sw" style="background: var(--promoter)"></span>promoter</span>
316
+ <span><span class="sw" style="background: var(--green)"></span>exon</span>
317
+ <span><span class="sw" style="background: transparent; border-top: 1px solid var(--intron); height: 1px; margin-top: 4px;"></span>intron</span>
318
+ </div>
319
  </div>
320
  </div>
321
  </div>
 
354
  <div class="cd-protein-3d" id="cd-protein-3d">
355
  <div class="cd-protein-3d-loading">loading hemoglobin…</div>
356
  </div>
357
+ <div class="cd-protein-caption">
358
+ <div class="cd-protein-caption__title">Human hemoglobin</div>
359
+ <div class="cd-protein-caption__desc">the molecule that carries oxygen in your blood</div>
360
+ <div class="cd-protein-caption__meta">4 chains · PDB <a href="https://www.rcsb.org/structure/1A3N" target="_blank" rel="noopener">1A3N</a></div>
361
+ </div>
362
  </div>
363
  </div>
364
  </div>
 
381
  The interesting question is what else falls out of that. We didn't tell Carbon what an
382
  exon is. We didn't tell it which mutations are pathogenic. We didn't tell it how genes
383
  differ between species. The sections below are ways to read what it picked up
384
+ anyway: autocomplete a gene <a class="lede-chip" href="#completion">§1</a>, see
385
+ structure emerge in its confidence <a class="lede-chip" href="#track">§2</a>, score
386
+ a disease variant against a healthy one <a class="lede-chip" href="#vep">§3</a>,
387
+ recognise a gene's species of origin <a class="lede-chip" href="#species">§4</a>,
388
  and then push further into folded protein structure
389
+ <a class="lede-chip" href="#folding">§5</a>, the embedding manifold
390
+ <a class="lede-chip" href="#umap">§6</a>, and the species tree
391
+ <a class="lede-chip" href="#speciesTree">§7</a>. Each demo runs against the public
392
  <code>HuggingFaceBio/Carbon-3B</code> checkpoint behind a live inference endpoint.
393
  </p>
394
  </div>
 
408
  intron just before the 2nd exon plus the first 35 bp of that exon, and ask it to
409
  <em>finish the exon</em>. The model streams the remaining bases one 6-base token at a
410
  time. Exons are the protein-coding parts of a gene and are under strong evolutionary
411
+ constraint, so they should be the most predictable stretches of DNA. We overlay the
412
+ <em>real</em> exon/intron annotations on top of the output so you can compare what
413
+ Carbon produces to what's actually there.
 
 
414
  </p>
415
  </div>
416
 
 
430
  </div>
431
 
432
  <div class="gene-info" id="d1-info">loading genes…</div>
433
+ <svg class="gene-track draggable" id="d1-track" viewBox="0 0 1000 52" preserveAspectRatio="none"></svg>
434
  <div class="track-axis-label" style="justify-content:flex-end;gap:20px;align-items:center">
435
  <span class="legend-tip"
436
  data-tip="Exon: coding segment of the gene. Stays in the mature mRNA and gets translated into protein."
 
490
  <div class="takeaway">
491
  <p>
492
  <strong>Try it</strong>
493
+ Drag the dark&nbsp;▼ markers to slide the prompt window and the green&nbsp;▼ to set
494
+ where generation stops, then hit&nbsp;▶&nbsp;generate. Land the green-shaded region
495
+ inside an exon (dark green block) and note the count of green-underlined matches;
496
+ repeat with a similar-length window over an intron and compare.
497
  </p>
498
  <p>
499
  <strong>What to look for</strong>
 
591
  </div>
592
 
593
  <div class="gene-info" id="d3-info">loading genes…</div>
594
+ <svg class="gene-track" id="d3-track" viewBox="0 0 1000 40" preserveAspectRatio="none"></svg>
595
  <svg id="d3-chart" style="display:block;width:100%;height:140px;background:#fff;border:1px solid #eee;margin-top:6px" preserveAspectRatio="none" viewBox="0 0 1000 140"></svg>
596
+ <div class="track-axis-label" style="padding-top:8px">
597
  <span><span class="legend-swatch" style="background:#317f3f"></span>exon (shaded)</span>
598
  <span style="color:#aaa">y-axis: log P per 6-bp token (higher = more confident)</span>
599
  <span id="d3-bp-label" style="color:#888">0 bp</span>
 
818
  <div class="demo-toolbar">
819
  <span>gene</span>
820
  <span id="d4-pills" class="pills"></span>
 
821
  <span>prefix</span>
822
  <span id="d4-prefix-pills" class="pills">
823
  <button class="pill" data-prefix="200">200</button>
 
829
  <button class="pill active" data-gen="60">60</button>
830
  <button class="pill" data-gen="200">200</button>
831
  </span>
832
+ <span class="spacer"></span>
833
  <button id="d4-go" class="action primary">▶ run all</button>
834
  <span class="status is-hidden" id="d4-status"><span class="dot"></span><span></span></span>
835
  </div>
 
1189
  </p>
1190
  <p class="tab-lede__note">
1191
  The sections below walk through each of those choices: how the tokenizer changes
1192
+ what a "token" means in DNA <a class="lede-chip" href="#tokenizer">§1</a>, how
1193
+ FNS rescues training in the BF16 regime <a class="lede-chip" href="#loss">§2</a>,
1194
+ what's in the training corpus <a class="lede-chip" href="#data">§3</a>,
1195
+ what the architecture looks like <a class="lede-chip" href="#architecture">§4</a>,
1196
  how 8k-token pretraining reaches 786 kbp at inference
1197
+ <a class="lede-chip" href="#longcontext">§5</a>, how Carbon stacks up against
1198
  Evo2-7B and GENERator-v2 on the full training-free suite
1199
+ <a class="lede-chip" href="#results">§6</a>, and why the model runs so fast
1200
+ <a class="lede-chip" href="#efficiency">§7</a>.
1201
  </p>
1202
  </div>
1203
  </div>
 
1456
  <div class="section-body">
1457
  <div class="demo" id="demo12">
1458
  <div id="d12-bars"></div>
1459
+ <div class="track-axis-label chart-legend">
1460
+ <span class="chart-legend__item"><span class="legend-swatch" style="background:#1A7A40"></span>Carbon 8B</span>
1461
+ <span class="chart-legend__item"><span class="legend-swatch" style="background:#6DBF7E"></span>Carbon 3B</span>
1462
+ <span class="chart-legend__item"><span class="legend-swatch" style="background:#5A5A56"></span>Evo2-7B</span>
1463
+ <span class="chart-legend__item"><span class="legend-swatch" style="background:#B5B0A6"></span>GENERator-v2 3B</span>
 
1464
  </div>
1465
  </div>
1466