zli12321 commited on
Commit
f1b1a0b
·
verified ·
1 Parent(s): f589d6a

Deploy IntelligenceLab website

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/examples/astro-detail.webp filter=lfs diff=lfs merge=lfs -text
37
+ assets/examples/color-detail.webp filter=lfs diff=lfs merge=lfs -text
38
+ assets/examples/micro-detail.webp filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,10 +1,10 @@
1
  ---
2
- title: AI For Science
3
- emoji: 🔥
4
- colorFrom: blue
5
- colorTo: red
6
  sdk: static
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: AI for Science Collaboration
3
+ colorFrom: green
4
+ colorTo: indigo
 
5
  sdk: static
6
+ app_file: index.html
7
  pinned: false
8
  ---
9
 
10
+ IntelligenceLab AI for Science collaboration site: an open call for biology and materials science graduate students. Static site (`index.html`, `styles.css`, `app.js`, `live.js`, `terminal.js`, `assets/`).
app.js ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (() => {
2
+ 'use strict';
3
+
4
+ // Every message from the interest form and email links goes to all of these addresses.
5
+ const CONTACT_EMAILS = ['zli12321@terpmail.umd.edu', 'zl22754@uga.edu'];
6
+ const CONTACT_TEXT = CONTACT_EMAILS.join(' and ');
7
+
8
+ const $ = (sel, root = document) => root.querySelector(sel);
9
+ const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
10
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
11
+
12
+ const mailto = (subject, body) => {
13
+ const params = [`subject=${encodeURIComponent(subject)}`];
14
+ if (body) params.push(`body=${encodeURIComponent(body)}`);
15
+ return `mailto:${CONTACT_EMAILS.join(',')}?${params.join('&')}`;
16
+ };
17
+
18
+ function initContact() {
19
+ $$('[data-contact]').forEach((link) => {
20
+ link.href = mailto(link.dataset.subject || 'AI for Science collaboration');
21
+ if (link.hasAttribute('data-show-address')) link.textContent = CONTACT_TEXT;
22
+ });
23
+ $$('[data-year]').forEach((el) => {
24
+ el.textContent = String(new Date().getFullYear());
25
+ });
26
+ }
27
+
28
+ function initNav() {
29
+ const nav = $('.nav');
30
+ const onScroll = () => nav.classList.toggle('is-scrolled', window.scrollY > 8);
31
+ onScroll();
32
+ window.addEventListener('scroll', onScroll, { passive: true });
33
+
34
+ const toggle = $('.nav-toggle');
35
+ const menu = $('#mobile-menu');
36
+ const setOpen = (open) => {
37
+ toggle.setAttribute('aria-expanded', String(open));
38
+ toggle.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
39
+ menu.hidden = !open;
40
+ document.body.classList.toggle('menu-open', open);
41
+ };
42
+ toggle.addEventListener('click', () => setOpen(toggle.getAttribute('aria-expanded') !== 'true'));
43
+ $$('a', menu).forEach((a) => a.addEventListener('click', () => setOpen(false)));
44
+ window.addEventListener('keydown', (e) => {
45
+ if (e.key === 'Escape') setOpen(false);
46
+ });
47
+ window.matchMedia('(min-width: 1101px)').addEventListener('change', (e) => {
48
+ if (e.matches) setOpen(false);
49
+ });
50
+
51
+ if (!('IntersectionObserver' in window)) return;
52
+ const links = new Map($$('.nav-links a').map((a) => [a.getAttribute('href').slice(1), a]));
53
+ const observer = new IntersectionObserver((entries) => {
54
+ entries.forEach((entry) => {
55
+ if (!entry.isIntersecting) return;
56
+ links.forEach((a) => a.classList.remove('is-active'));
57
+ const active = links.get(entry.target.dataset.nav || entry.target.id);
58
+ if (active) active.classList.add('is-active');
59
+ });
60
+ }, { rootMargin: '-45% 0px -50% 0px' });
61
+ $$('main > section[id]').forEach((section) => observer.observe(section));
62
+ }
63
+
64
+ function initReveal() {
65
+ const items = $$('.reveal');
66
+ if (reduceMotion || !('IntersectionObserver' in window)) {
67
+ items.forEach((el) => el.classList.add('is-visible'));
68
+ return;
69
+ }
70
+ const observer = new IntersectionObserver((entries) => {
71
+ entries.forEach((entry) => {
72
+ if (!entry.isIntersecting) return;
73
+ entry.target.classList.add('is-visible');
74
+ observer.unobserve(entry.target);
75
+ });
76
+ }, { rootMargin: '0px 0px -8% 0px', threshold: 0.12 });
77
+ items.forEach((el) => observer.observe(el));
78
+ }
79
+
80
+ function initExplorer() {
81
+ const root = $('#explorer');
82
+ if (!root) return;
83
+ root.classList.add('is-enhanced');
84
+ const panels = $$('.explorer-panel', root);
85
+
86
+ panels.forEach((panel) => {
87
+ const stages = $$('.stage', panel);
88
+ if (!stages.length) return;
89
+ const tabs = document.createElement('div');
90
+ tabs.className = 'stage-tabs';
91
+ tabs.setAttribute('role', 'group');
92
+ tabs.setAttribute('aria-label', 'Project stages');
93
+ const buttons = stages.map((stage, i) => {
94
+ const button = document.createElement('button');
95
+ button.type = 'button';
96
+ button.className = 'stage-btn';
97
+ const num = document.createElement('span');
98
+ num.className = 'sb-num';
99
+ num.textContent = String(i + 1);
100
+ const name = document.createElement('span');
101
+ name.textContent = $('.stage-title', stage).textContent;
102
+ button.append(num, name);
103
+ return button;
104
+ });
105
+ const select = (index) => {
106
+ buttons.forEach((b, i) => b.setAttribute('aria-pressed', String(i === index)));
107
+ stages.forEach((s, i) => {
108
+ s.hidden = i !== index;
109
+ });
110
+ };
111
+ buttons.forEach((b, i) => b.addEventListener('click', () => select(i)));
112
+ tabs.append(...buttons);
113
+ panel.insertBefore(tabs, stages[0]);
114
+ select(0);
115
+ });
116
+
117
+ const toggles = $$('.ft-btn', root);
118
+ const setField = (field) => {
119
+ root.dataset.field = field;
120
+ toggles.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.field === field)));
121
+ panels.forEach((p) => {
122
+ p.hidden = p.dataset.field !== field;
123
+ });
124
+ };
125
+ toggles.forEach((b) => b.addEventListener('click', () => setField(b.dataset.field)));
126
+ setField('bio');
127
+ }
128
+
129
+ function initExamples() {
130
+ const grid = $('#ex-grid');
131
+ const modal = $('#ex-modal');
132
+ const source = $('#examples-data');
133
+ if (!grid || !modal || !source) return;
134
+ let examples;
135
+ try {
136
+ examples = JSON.parse(source.textContent);
137
+ } catch (err) {
138
+ return;
139
+ }
140
+ const cards = $$('.ex-card', grid);
141
+
142
+ const more = $('#ex-more');
143
+ const extras = cards.filter((card) => card.classList.contains('is-extra'));
144
+ if (more) {
145
+ if (!extras.length) more.hidden = true;
146
+ more.textContent = `Show ${extras.length} more demos`;
147
+ more.addEventListener('click', () => {
148
+ grid.classList.add('show-all');
149
+ more.setAttribute('aria-expanded', 'true');
150
+ if (extras[0]) extras[0].focus({ preventScroll: true });
151
+ });
152
+ }
153
+
154
+ const sheet = $('.ex-sheet', modal);
155
+ const closeButton = $('.icon-btn[data-ex-close]', modal);
156
+ const field = (id) => document.getElementById(id);
157
+ const fields = {
158
+ domain: field('exm-domain'),
159
+ title: field('exm-title'),
160
+ tracks: field('exm-tracks'),
161
+ img: field('exm-img'),
162
+ desc: field('exm-desc'),
163
+ why: field('exm-why'),
164
+ software: field('exm-software'),
165
+ artifacts: field('exm-artifacts'),
166
+ axes: field('exm-axes'),
167
+ outputs: field('exm-outputs'),
168
+ cta: field('exm-cta'),
169
+ live: field('exm-live'),
170
+ };
171
+ let current = 0;
172
+ let lastFocus = null;
173
+ let liveView = null;
174
+
175
+ const mountLive = (ex) => {
176
+ if (liveView) {
177
+ liveView.dispose();
178
+ liveView = null;
179
+ }
180
+ const live = window.ILLive;
181
+ if (!fields.live || !live || !live.ready) {
182
+ if (fields.live) fields.live.hidden = true;
183
+ return;
184
+ }
185
+ fields.live.hidden = false;
186
+ fields.live.style.setProperty('--dc', ex.color);
187
+ liveView = live.mount(fields.live, ex.kind, { color: ex.color, hero: true });
188
+ if (!liveView) fields.live.hidden = true;
189
+ };
190
+
191
+ const listItems = (ul, items, cls) => {
192
+ ul.replaceChildren(...items.map((text) => {
193
+ const li = document.createElement('li');
194
+ li.textContent = text;
195
+ if (cls) li.className = cls;
196
+ return li;
197
+ }));
198
+ };
199
+
200
+ const render = (index) => {
201
+ current = (index + examples.length) % examples.length;
202
+ const ex = examples[current];
203
+ sheet.style.setProperty('--dc', ex.color);
204
+ fields.domain.textContent = ex.domain;
205
+ fields.title.textContent = ex.title;
206
+ listItems(fields.tracks, ex.tags, 'trk trk-rel');
207
+ fields.img.src = ex.img;
208
+ fields.img.alt = ex.alt;
209
+ fields.desc.textContent = ex.description;
210
+ const lead = document.createElement('strong');
211
+ lead.textContent = 'In your research: ';
212
+ fields.why.replaceChildren(lead, document.createTextNode(ex.why));
213
+ listItems(fields.software, ex.software);
214
+ listItems(fields.artifacts, ex.artifacts);
215
+ listItems(fields.axes, ex.axes);
216
+ listItems(fields.outputs, ex.outputs);
217
+ sheet.scrollTop = 0;
218
+ if (!modal.hidden) mountLive(ex);
219
+ };
220
+
221
+ const open = (index) => {
222
+ lastFocus = document.activeElement;
223
+ render(index);
224
+ modal.hidden = false;
225
+ mountLive(examples[current]);
226
+ document.body.classList.add('modal-open');
227
+ closeButton.focus();
228
+ };
229
+ const close = () => {
230
+ modal.hidden = true;
231
+ if (liveView) {
232
+ liveView.dispose();
233
+ liveView = null;
234
+ }
235
+ document.body.classList.remove('modal-open');
236
+ if (lastFocus) lastFocus.focus({ preventScroll: true });
237
+ };
238
+ document.addEventListener('il-live-ready', () => {
239
+ if (!modal.hidden) mountLive(examples[current]);
240
+ });
241
+
242
+ fields.cta.addEventListener('click', (e) => {
243
+ e.preventDefault();
244
+ const form = $('#join-form');
245
+ if (form) form.elements.namedItem('interest').value = examples[current].title;
246
+ close();
247
+ $('#join').scrollIntoView({ behavior: reduceMotion ? 'auto' : 'smooth' });
248
+ const name = form && form.elements.namedItem('name');
249
+ if (name) setTimeout(() => name.focus({ preventScroll: true }), reduceMotion ? 0 : 600);
250
+ });
251
+
252
+ cards.forEach((card) => {
253
+ const index = Number(card.dataset.index);
254
+ card.addEventListener('click', () => open(index));
255
+ card.addEventListener('keydown', (e) => {
256
+ if (e.key === 'Enter' || e.key === ' ') {
257
+ e.preventDefault();
258
+ open(index);
259
+ }
260
+ });
261
+ });
262
+ $$('[data-ex-close]', modal).forEach((el) => el.addEventListener('click', close));
263
+ $('[data-ex-prev]', modal).addEventListener('click', () => render(current - 1));
264
+ $('[data-ex-next]', modal).addEventListener('click', () => render(current + 1));
265
+
266
+ document.addEventListener('keydown', (e) => {
267
+ if (modal.hidden) return;
268
+ if (e.key === 'Escape') {
269
+ e.preventDefault();
270
+ close();
271
+ } else if (e.key === 'ArrowRight') {
272
+ render(current + 1);
273
+ } else if (e.key === 'ArrowLeft') {
274
+ render(current - 1);
275
+ } else if (e.key === 'Tab') {
276
+ const focusables = $$('button, a[href]', sheet);
277
+ const first = focusables[0];
278
+ const last = focusables[focusables.length - 1];
279
+ if (e.shiftKey && document.activeElement === first) {
280
+ e.preventDefault();
281
+ last.focus();
282
+ } else if (!e.shiftKey && document.activeElement === last) {
283
+ e.preventDefault();
284
+ first.focus();
285
+ }
286
+ }
287
+ });
288
+ }
289
+
290
+ function initForm() {
291
+ const form = $('#join-form');
292
+ if (!form) return;
293
+ const note = $('#form-note');
294
+ const nameInput = form.elements.namedItem('name');
295
+ nameInput.addEventListener('input', () => nameInput.removeAttribute('aria-invalid'));
296
+
297
+ form.addEventListener('submit', (e) => {
298
+ e.preventDefault();
299
+ const data = new FormData(form);
300
+ const value = (key) => String(data.get(key) || '').trim();
301
+ const name = value('name');
302
+ if (!name) {
303
+ nameInput.setAttribute('aria-invalid', 'true');
304
+ note.textContent = 'Please add your name so we know who to reply to.';
305
+ nameInput.focus();
306
+ return;
307
+ }
308
+ const field = value('field') || 'Not specified';
309
+ const lines = [
310
+ `Name: ${name}`,
311
+ `University & program: ${value('program') || '-'}`,
312
+ `Field: ${field}`,
313
+ `Research: ${value('topic') || '-'}`,
314
+ '',
315
+ 'How I use AI in my research today:',
316
+ value('ai_use') || '-',
317
+ '',
318
+ 'Where I would most like help:',
319
+ value('help') || '-',
320
+ '',
321
+ `How I'd like to take part: ${data.getAll('involve').join(', ') || 'Not sure yet'}`,
322
+ ];
323
+ if (value('interest')) lines.push(`Demo that caught my eye: ${value('interest')}`);
324
+
325
+ const link = document.createElement('a');
326
+ link.href = mailto(`AI for Science collaboration: ${name} (${field})`, lines.join('\r\n'));
327
+ link.hidden = true;
328
+ document.body.append(link);
329
+ link.click();
330
+ link.remove();
331
+ note.textContent = `Your email app should open with your message ready to send. If it doesn't, write to ${CONTACT_TEXT}.`;
332
+ });
333
+ }
334
+
335
+ function initMorph() {
336
+ const canvas = $('#morph');
337
+ if (!canvas || !canvas.getContext) return;
338
+ const ctx = canvas.getContext('2d');
339
+ const labels = $$('.morph-labels .ml');
340
+
341
+ // Rock-salt lattice: 5 x 5 x 5 sites, two alternating species.
342
+ const SPACING = 0.42;
343
+ const lattice = [];
344
+ for (let i = 0; i < 5; i += 1) {
345
+ for (let j = 0; j < 5; j += 1) {
346
+ for (let k = 0; k < 5; k += 1) {
347
+ lattice.push({ x: (i - 2) * SPACING, y: (j - 2) * SPACING, z: (k - 2) * SPACING, c: (i + j + k) % 2, r: 3.3 });
348
+ }
349
+ }
350
+ }
351
+ const latticeEdges = [];
352
+ for (let a = 0; a < lattice.length; a += 1) {
353
+ for (let b = a + 1; b < lattice.length; b += 1) {
354
+ const p = lattice[a];
355
+ const q = lattice[b];
356
+ const d = Math.abs(p.x - q.x) + Math.abs(p.y - q.y) + Math.abs(p.z - q.z);
357
+ if (Math.abs(d - SPACING) < 1e-6) latticeEdges.push([a, b]);
358
+ }
359
+ }
360
+
361
+ // Double helix: two backbones of 50 points plus 25 base pairs on the axis.
362
+ const PER_STRAND = 50;
363
+ const helix = [];
364
+ const helixEdges = [];
365
+ for (let s = 0; s < 2; s += 1) {
366
+ for (let i = 0; i < PER_STRAND; i += 1) {
367
+ const t = i / (PER_STRAND - 1);
368
+ const angle = t * 2.1 * Math.PI * 2 + s * Math.PI;
369
+ helix.push({ x: 0.6 * Math.cos(angle), y: (t - 0.5) * 2.1, z: 0.6 * Math.sin(angle), c: 2 + s, r: 3.4 });
370
+ if (i > 0) helixEdges.push([s * PER_STRAND + i - 1, s * PER_STRAND + i]);
371
+ }
372
+ }
373
+ for (let i = 0; i < PER_STRAND; i += 2) {
374
+ const a = helix[i];
375
+ const b = helix[PER_STRAND + i];
376
+ const mid = helix.length;
377
+ helix.push({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2, c: 4, r: 2.3 });
378
+ helixEdges.push([i, mid], [mid, PER_STRAND + i]);
379
+ }
380
+
381
+ // Pair points by height so the morph flows within horizontal bands.
382
+ const byHeight = (pts) => pts.map((_, i) => i).sort((a, b) =>
383
+ (pts[a].y - pts[b].y) || (Math.atan2(pts[a].z, pts[a].x) - Math.atan2(pts[b].z, pts[b].x)));
384
+ const orderA = byHeight(lattice);
385
+ const orderB = byHeight(helix);
386
+ const slotA = [];
387
+ const slotB = [];
388
+ orderA.forEach((orig, k) => { slotA[orig] = k; });
389
+ orderB.forEach((orig, k) => { slotB[orig] = k; });
390
+ const points = orderA.map((orig, k) => ({ a: lattice[orig], b: helix[orderB[k]] }));
391
+ const edgesA = latticeEdges.map(([p, q]) => [slotA[p], slotA[q]]);
392
+ const edgesB = helixEdges.map(([p, q]) => [slotB[p], slotB[q]]);
393
+
394
+ const RGB = [[15, 159, 117], [79, 70, 229], [15, 159, 117], [20, 184, 166], [245, 158, 11]];
395
+ const projected = points.map(() => ({ sx: 0, sy: 0, z: 0, p: 1 }));
396
+ const depthOrder = points.map((_, i) => i);
397
+ const smooth = (x) => x * x * (3 - 2 * x);
398
+ const morphAt = (ms) => {
399
+ const s = (ms / 1000) % 12;
400
+ if (s < 3.6) return 0;
401
+ if (s < 6) return smooth((s - 3.6) / 2.4);
402
+ if (s < 9.6) return 1;
403
+ return 1 - smooth((s - 9.6) / 2.4);
404
+ };
405
+
406
+ let w = 0;
407
+ let h = 0;
408
+ let dpr = 1;
409
+ let shown = -1;
410
+ const resize = () => {
411
+ const rect = canvas.getBoundingClientRect();
412
+ dpr = Math.min(window.devicePixelRatio || 1, 2);
413
+ w = rect.width;
414
+ h = rect.height;
415
+ canvas.width = Math.max(1, Math.round(w * dpr));
416
+ canvas.height = Math.max(1, Math.round(h * dpr));
417
+ };
418
+
419
+ const draw = (now) => {
420
+ if (w < 8 || h < 8) return;
421
+ const m = reduceMotion ? 0 : morphAt(now);
422
+ const rot = reduceMotion ? 0.6 : now * 0.00022;
423
+ const tilt = 0.32;
424
+ const cosR = Math.cos(rot);
425
+ const sinR = Math.sin(rot);
426
+ const cosT = Math.cos(tilt);
427
+ const sinT = Math.sin(tilt);
428
+ const size = Math.min(w, h);
429
+ const scale = size * 0.34;
430
+ const cx = w / 2;
431
+ const cy = h * 0.47;
432
+ const camera = 3.6;
433
+
434
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
435
+ ctx.clearRect(0, 0, w, h);
436
+ for (let k = 0; k < points.length; k += 1) {
437
+ const { a, b } = points[k];
438
+ const x = a.x + (b.x - a.x) * m;
439
+ const y = a.y + (b.y - a.y) * m;
440
+ const z = a.z + (b.z - a.z) * m;
441
+ const x1 = x * cosR + z * sinR;
442
+ const z1 = -x * sinR + z * cosR;
443
+ const y2 = y * cosT - z1 * sinT;
444
+ const z2 = y * sinT + z1 * cosT;
445
+ const persp = camera / (camera - z2);
446
+ const pr = projected[k];
447
+ pr.sx = cx + x1 * scale * persp;
448
+ pr.sy = cy - y2 * scale * persp;
449
+ pr.z = z2;
450
+ pr.p = persp;
451
+ }
452
+
453
+ const drawEdges = (edges, alpha, rgb) => {
454
+ if (alpha < 0.02) return;
455
+ ctx.lineWidth = 1.2;
456
+ for (const [i, j] of edges) {
457
+ const p = projected[i];
458
+ const q = projected[j];
459
+ const depth = Math.min(1, Math.max(0, ((p.z + q.z) / 2 + 1.2) / 2.4));
460
+ ctx.strokeStyle = `rgba(${rgb}, ${(alpha * (0.1 + 0.4 * depth)).toFixed(3)})`;
461
+ ctx.beginPath();
462
+ ctx.moveTo(p.sx, p.sy);
463
+ ctx.lineTo(q.sx, q.sy);
464
+ ctx.stroke();
465
+ }
466
+ };
467
+ drawEdges(edgesA, 1 - m, '79, 70, 229');
468
+ drawEdges(edgesB, m, '15, 159, 117');
469
+
470
+ depthOrder.sort((i, j) => projected[i].z - projected[j].z);
471
+ const unit = size / 440;
472
+ for (const k of depthOrder) {
473
+ const pr = projected[k];
474
+ const { a, b } = points[k];
475
+ const ca = RGB[a.c];
476
+ const cb = RGB[b.c];
477
+ const depth = Math.min(1, Math.max(0, (pr.z + 1.2) / 2.4));
478
+ const radius = (a.r + (b.r - a.r) * m) * pr.p * unit;
479
+ ctx.fillStyle = `rgba(${Math.round(ca[0] + (cb[0] - ca[0]) * m)}, ${Math.round(ca[1] + (cb[1] - ca[1]) * m)}, ${Math.round(ca[2] + (cb[2] - ca[2]) * m)}, ${(0.42 + 0.58 * depth).toFixed(3)})`;
480
+ ctx.beginPath();
481
+ ctx.arc(pr.sx, pr.sy, radius, 0, Math.PI * 2);
482
+ ctx.fill();
483
+ }
484
+
485
+ const shape = m >= 0.5 ? 1 : 0;
486
+ if (shape !== shown) {
487
+ shown = shape;
488
+ labels.forEach((label) => label.classList.toggle('is-on', Number(label.dataset.shape) === shape));
489
+ }
490
+ };
491
+
492
+ let raf = 0;
493
+ let inView = true;
494
+ const loop = (now) => {
495
+ draw(now);
496
+ raf = requestAnimationFrame(loop);
497
+ };
498
+ const start = () => {
499
+ if (!raf && inView && !document.hidden && !reduceMotion) raf = requestAnimationFrame(loop);
500
+ };
501
+ const stop = () => {
502
+ cancelAnimationFrame(raf);
503
+ raf = 0;
504
+ };
505
+
506
+ resize();
507
+ draw(performance.now());
508
+ if ('ResizeObserver' in window) {
509
+ new ResizeObserver(() => {
510
+ resize();
511
+ if (!raf) draw(performance.now());
512
+ }).observe(canvas);
513
+ }
514
+ if (reduceMotion) return;
515
+ if ('IntersectionObserver' in window) {
516
+ new IntersectionObserver((entries) => {
517
+ inView = entries[0].isIntersecting;
518
+ if (inView) start();
519
+ else stop();
520
+ }).observe(canvas);
521
+ }
522
+ document.addEventListener('visibilitychange', () => {
523
+ if (document.hidden) stop();
524
+ else start();
525
+ });
526
+ start();
527
+ }
528
+
529
+ initContact();
530
+ initNav();
531
+ initReveal();
532
+ initExplorer();
533
+ initExamples();
534
+ initForm();
535
+ initMorph();
536
+ })();
assets/examples/astro-detail.webp ADDED

Git LFS Details

  • SHA256: f19af76b3a86a7467418060ac9fb4818f6b3b4cb76d08ddee17cb85d76257088
  • Pointer size: 131 Bytes
  • Size of remote file: 136 kB
assets/examples/astro-thumb.webp ADDED
assets/examples/blender-detail.webp ADDED
assets/examples/blender-thumb.webp ADDED
assets/examples/color-detail.webp ADDED

Git LFS Details

  • SHA256: af30ee4db8e8621ac823c8ea74d1e386066d7fec0a1910fcf99bca63dda5e842
  • Pointer size: 131 Bytes
  • Size of remote file: 168 kB
assets/examples/color-thumb.webp ADDED
assets/examples/container-detail.webp ADDED
assets/examples/container-thumb.webp ADDED
assets/examples/drc-detail.webp ADDED
assets/examples/drc-thumb.webp ADDED
assets/examples/foam-detail.webp ADDED
assets/examples/foam-thumb.webp ADDED
assets/examples/micro-detail.webp ADDED

Git LFS Details

  • SHA256: 543522ec5acdd5ad13d59f88e917d1b37be8a20e8dd6d12257f30584d61653c7
  • Pointer size: 131 Bytes
  • Size of remote file: 104 kB
assets/examples/micro-thumb.webp ADDED
assets/examples/ocr-detail.webp ADDED
assets/examples/ocr-thumb.webp ADDED
assets/examples/robot-detail.webp ADDED
assets/examples/robot-thumb.webp ADDED
assets/examples/stl-detail.webp ADDED
assets/examples/stl-thumb.webp ADDED
assets/examples/visuals.js ADDED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -1,19 +1,782 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>AI for Science Collaboration · IntelligenceLab</title>
7
+ <meta name="description" content="We're looking for biology and materials science graduate students to collaborate on AI for Science: tell us how you do research, help us test AI agents on real scientific workflows, and co-author the results.">
8
+ <meta property="og:type" content="website">
9
+ <meta property="og:title" content="AI for Science Collaboration · IntelligenceLab">
10
+ <meta property="og:description" content="Help us build AI agents that actually help biology and materials science research, and write the paper with us.">
11
+ <meta name="theme-color" content="#faf8f3">
12
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%2310b981'/%3E%3Cstop offset='1' stop-color='%236366f1'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='8' fill='url(%23g)'/%3E%3Cpath d='M7 23h5v-5h5v-5h5' fill='none' stroke='white' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M19.6 9.4l2.2 2.2 4.2-4.4' fill='none' stroke='white' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
13
+ <link rel="preconnect" href="https://fonts.googleapis.com">
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,500;0,9..144,600;1,9..144,500&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
16
+ <link rel="stylesheet" href="styles.css">
17
+ <script>document.documentElement.classList.add('js');</script>
18
+ </head>
19
+ <body>
20
+ <a class="skip-link" href="#main">Skip to content</a>
21
+
22
+ <header class="nav">
23
+ <div class="container nav-inner">
24
+ <a class="brand" href="#top" aria-label="IntelligenceLab AI for Science home">
25
+ <svg class="brand-mark" viewBox="0 0 32 32" aria-hidden="true">
26
+ <defs>
27
+ <linearGradient id="brand-grad" x1="0" y1="0" x2="1" y2="1">
28
+ <stop offset="0" stop-color="#10b981"/>
29
+ <stop offset="1" stop-color="#6366f1"/>
30
+ </linearGradient>
31
+ </defs>
32
+ <rect width="32" height="32" rx="8" fill="url(#brand-grad)"/>
33
+ <path d="M7 23h5v-5h5v-5h5" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
34
+ <path d="M19.6 9.4l2.2 2.2 4.2-4.4" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
35
+ </svg>
36
+ <span>IntelligenceLab <em>AI for Science</em></span>
37
+ </a>
38
+ <nav class="nav-links" aria-label="Primary">
39
+ <a href="#why">Why</a>
40
+ <a href="#your-research">Your research</a>
41
+ <a href="#examples">Demos</a>
42
+ <a href="#collaborate">How we work</a>
43
+ <a href="#papers">Papers</a>
44
+ </nav>
45
+ <div class="nav-cta">
46
+ <a class="btn btn-dark btn-sm" href="#join">Get involved</a>
47
+ <button class="nav-toggle" type="button" aria-label="Open menu" aria-expanded="false" aria-controls="mobile-menu">
48
+ <span></span><span></span>
49
+ </button>
50
+ </div>
51
+ </div>
52
+ <div class="mobile-menu" id="mobile-menu" hidden>
53
+ <a href="#why">Why</a>
54
+ <a href="#your-research">Your research</a>
55
+ <a href="#examples">Demos</a>
56
+ <a href="#collaborate">How we work</a>
57
+ <a href="#papers">Papers</a>
58
+ <a class="btn btn-dark" href="#join">Get involved</a>
59
+ </div>
60
+ </header>
61
+
62
+ <main id="main">
63
+ <section class="hero" id="top" aria-labelledby="hero-title">
64
+ <div class="container hero-grid">
65
+ <div class="hero-copy">
66
+ <p class="eyebrow"><span class="pulse-dot" aria-hidden="true"></span>Open call · Biology &amp; materials science</p>
67
+ <h1 id="hero-title">Let's build AI that actually <em>helps your research</em>.</h1>
68
+ <p class="lede">We're AI researchers who teach language-model agents to use real scientific software. Now we want to learn from the people doing the science: how you work, where AI already helps, and where it falls short. If you're a graduate student in biology or materials science, come shape what we build and write the paper with us.</p>
69
+ <div class="hero-actions">
70
+ <a class="btn btn-dark" href="#join">Join the collaboration
71
+ <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 10h11m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
72
+ </a>
73
+ <a class="btn btn-ghost" href="#examples">See our demos</a>
74
+ </div>
75
+ <ul class="hero-points">
76
+ <li>No ML background needed</li>
77
+ <li>Your workflows, your expertise</li>
78
+ <li>Contributors can join the paper</li>
79
+ </ul>
80
+ </div>
81
+ <figure class="hero-visual" aria-hidden="true">
82
+ <canvas class="morph-canvas" id="morph"></canvas>
83
+ <figcaption class="morph-labels"><span class="ml is-on" data-shape="0">Crystal lattice</span><span class="ml" data-shape="1">DNA double helix</span></figcaption>
84
+ </figure>
85
+ </div>
86
+ </section>
87
+
88
+ <section class="team-strip" aria-label="Who we are">
89
+ <div class="container">
90
+ <p class="team-title">Our team includes researchers from</p>
91
+ <ul class="team-list">
92
+ <li>University of Maryland</li>
93
+ <li>University of Georgia</li>
94
+ <li>National University of Singapore</li>
95
+ <li>Indiana University</li>
96
+ <li>The Hong Kong Polytechnic University</li>
97
+ </ul>
98
+ <p class="team-more">and collaborators at other universities</p>
99
+ </div>
100
+ </section>
101
+
102
+ <section class="section" id="why" aria-labelledby="why-title">
103
+ <div class="container why-grid">
104
+ <div class="why-copy reveal">
105
+ <p class="kicker">Why we need you</p>
106
+ <h2 id="why-title">Frontier AI agents still struggle with real science.</h2>
107
+ <p class="sub">We built a test from 100 tasks based on real scientific software and ran eight frontier models on it. The best one solved one task in five. Life-science tasks were solved once in 120 attempts.</p>
108
+ <p class="sub">Most training data for AI agents comes from software engineering, so the workflows scientists actually run are barely covered. Closing that gap takes people who know what a correct result looks like in their field.</p>
109
+ </div>
110
+ <div class="stat-stack reveal">
111
+ <div class="stat"><strong>20%</strong><span>Success rate of the best of eight frontier models on our held-out scientific-software test</span></div>
112
+ <div class="stat stat-warm"><strong>1 in 120</strong><span>Attempts that succeeded on life &amp; molecular science tasks, across all eight models</span></div>
113
+ <div class="stat stat-cool"><strong>98%</strong><span>Agreement between our automated checks and blinded human reviewers</span></div>
114
+ </div>
115
+ </div>
116
+ </section>
117
+
118
+ <section class="section section-tint" id="who" data-nav="why" aria-labelledby="who-title">
119
+ <div class="container">
120
+ <div class="section-head reveal">
121
+ <p class="kicker">Who we're looking for</p>
122
+ <h2 id="who-title">Graduate students in biology and materials science.</h2>
123
+ <p class="sub">Master's or PhD, wet lab or computational, any level of AI experience. If you script, simulate, image, or analyze data as part of your research, you have exactly the knowledge we're missing.</p>
124
+ </div>
125
+ <div class="field-grid">
126
+ <article class="field-card field-bio reveal">
127
+ <div class="field-icon" aria-hidden="true">
128
+ <svg viewBox="0 0 48 48"><path d="M16 6c0 12 16 12 16 24s-16 12-16 12M32 6c0 12-16 12-16 24s16 12 16 12" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/><path d="M18 12h12M19.5 19h9M19.5 29h9M18 36h12" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" opacity=".55"/></svg>
129
+ </div>
130
+ <h3>Biology</h3>
131
+ <p>Cell and molecular biology, genomics and bioinformatics, structural biology, neuroscience, microbiology, and ecology.</p>
132
+ <p class="tools-label">Tools you might use</p>
133
+ <ul class="tool-chips">
134
+ <li>Fiji / ImageJ</li><li>CellProfiler</li><li>Biopython</li><li>BLAST</li><li>Snakemake</li><li>R / Bioconductor</li><li>AlphaFold</li><li>PyMOL</li><li>GROMACS</li>
135
+ </ul>
136
+ </article>
137
+ <article class="field-card field-mat reveal">
138
+ <div class="field-icon" aria-hidden="true">
139
+ <svg viewBox="0 0 48 48"><path d="M10 16l14-8 14 8v16l-14 8-14-8z" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linejoin="round"/><path d="M10 16l14 8 14-8M24 24v16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round" opacity=".55"/><g fill="currentColor"><circle cx="24" cy="8" r="3"/><circle cx="10" cy="16" r="3"/><circle cx="38" cy="16" r="3"/><circle cx="24" cy="24" r="3"/><circle cx="10" cy="32" r="3"/><circle cx="38" cy="32" r="3"/><circle cx="24" cy="40" r="3"/></g></svg>
140
+ </div>
141
+ <h3>Materials science</h3>
142
+ <p>Computational materials, characterization, polymers and soft matter, energy materials, and metals and ceramics.</p>
143
+ <p class="tools-label">Tools you might use</p>
144
+ <ul class="tool-chips">
145
+ <li>VASP</li><li>Quantum ESPRESSO</li><li>LAMMPS</li><li>ASE</li><li>pymatgen</li><li>Materials Project</li><li>GSAS-II</li><li>OVITO</li><li>COMSOL</li>
146
+ </ul>
147
+ </article>
148
+ </div>
149
+ <p class="who-note reveal">Working in a neighboring field, like chemistry, biophysics, or chemical engineering? We'd still love to hear from you.</p>
150
+ </div>
151
+ </section>
152
+
153
+ <section class="section" id="your-research" aria-labelledby="yr-title">
154
+ <div class="container">
155
+ <div class="section-head reveal">
156
+ <p class="kicker">Let's talk about your research</p>
157
+ <h2 id="yr-title">Where does AI help you today, and where does it fall short?</h2>
158
+ <p class="sub">We want to understand research from the inside: what fills your week, which steps AI already speeds up, and which ones it gets wrong. Here is our starting picture of a typical project. Help us correct it.</p>
159
+ </div>
160
+ <div class="explorer reveal" id="explorer">
161
+ <div class="explorer-top">
162
+ <div class="field-toggle" role="group" aria-label="Choose a field">
163
+ <button type="button" class="ft-btn" data-field="bio" aria-pressed="true">Biology</button>
164
+ <button type="button" class="ft-btn" data-field="mat" aria-pressed="false">Materials science</button>
165
+ </div>
166
+ <p class="explorer-hint">Pick a stage of a typical project.</p>
167
+ </div>
168
+
169
+ <div class="explorer-panel" data-field="bio">
170
+ <h3 class="panel-title">Biology</h3>
171
+ <article class="stage">
172
+ <h4 class="stage-title">Reading &amp; planning</h4>
173
+ <dl class="stage-grid">
174
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Keeping up with papers and preprints, comparing protocols, and planning the next experiment.</dd></div>
175
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Confident summaries with missing or invented citations, and little sense of which protocol details actually matter.</dd></div>
176
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Tasks that check an agent's answers against the papers and protocols you rely on.</dd></div>
177
+ </dl>
178
+ </article>
179
+ <article class="stage">
180
+ <h4 class="stage-title">Designing experiments</h4>
181
+ <dl class="stage-grid">
182
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Designing primers, guide RNAs, constructs, and controls, often across several separate tools.</dd></div>
183
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Chaining specialized tools together and respecting organism- and lab-specific constraints.</dd></div>
184
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Design tasks scored by the same tools and rules you use to sanity-check a design.</dd></div>
185
+ </dl>
186
+ </article>
187
+ <article class="stage">
188
+ <h4 class="stage-title">Processing data</h4>
189
+ <dl class="stage-grid">
190
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Sequencing pipelines, microscopy segmentation, and flow-cytometry or plate-reader data.</dd></div>
191
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Installing tools, wiring up multi-step pipelines, and catching silent errors in the output.</dd></div>
192
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Pipelines rebuilt from your real runs, with checks on the numbers, not just on whether files exist.</dd></div>
193
+ </dl>
194
+ </article>
195
+ <article class="stage">
196
+ <h4 class="stage-title">Analysis &amp; modeling</h4>
197
+ <dl class="stage-grid">
198
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Statistics, structure prediction, docking, and molecular dynamics.</dd></div>
199
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Choosing sensible methods and parameters, and noticing when a result is biologically implausible.</dd></div>
200
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Analyses checked on hidden test cases, so an agent has to get the method right, not just the format.</dd></div>
201
+ </dl>
202
+ </article>
203
+ <article class="stage">
204
+ <h4 class="stage-title">Figures &amp; writing</h4>
205
+ <dl class="stage-grid">
206
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Figures, methods sections, and notebooks that others can rerun.</dd></div>
207
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Methods text that drifts from what the code actually did.</dd></div>
208
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Tasks where every figure and methods detail has to match the analysis that produced it.</dd></div>
209
+ </dl>
210
+ </article>
211
+ </div>
212
+
213
+ <div class="explorer-panel" data-field="mat">
214
+ <h3 class="panel-title">Materials science</h3>
215
+ <article class="stage">
216
+ <h4 class="stage-title">Reading &amp; planning</h4>
217
+ <dl class="stage-grid">
218
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Finding synthesis routes, processing conditions, and property data scattered across many papers.</dd></div>
219
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Pulling values out of tables and figures without mixing up units, samples, or conditions.</dd></div>
220
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Extraction tasks checked against values curated by people who know the field.</dd></div>
221
+ </dl>
222
+ </article>
223
+ <article class="stage">
224
+ <h4 class="stage-title">Setting up simulations</h4>
225
+ <dl class="stage-grid">
226
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Building DFT or molecular-dynamics inputs, running convergence tests, and writing cluster job scripts.</dd></div>
227
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Physically sensible parameters, pseudopotential and force-field choices, and the quirks of each cluster.</dd></div>
228
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Simulation tasks checked by rerunning the real codes on inputs the agent never saw.</dd></div>
229
+ </dl>
230
+ </article>
231
+ <article class="stage">
232
+ <h4 class="stage-title">Characterization</h4>
233
+ <dl class="stage-grid">
234
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Processing XRD patterns, electron-microscopy images, and spectra.</dd></div>
235
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Peak fits and refinements that look reasonable but aren't physically right.</dd></div>
236
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Characterization pipelines rebuilt from real instrument data, with checks that respect measurement tolerances.</dd></div>
237
+ </dl>
238
+ </article>
239
+ <article class="stage">
240
+ <h4 class="stage-title">Screening &amp; data</h4>
241
+ <dl class="stage-grid">
242
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Querying materials databases, featurizing structures, and screening candidates.</dd></div>
243
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Keeping structures, compositions, and units consistent from one tool to the next.</dd></div>
244
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Screening workflows checked against reference results from the codes you trust.</dd></div>
245
+ </dl>
246
+ </article>
247
+ <article class="stage">
248
+ <h4 class="stage-title">Figures &amp; writing</h4>
249
+ <dl class="stage-grid">
250
+ <div class="sg sg-work"><dt>What it looks like</dt><dd>Phase diagrams, band structures, and methods that others can reproduce.</dd></div>
251
+ <div class="sg sg-gap"><dt>Where AI falls short</dt><dd>Plots and text that drift from the underlying calculations.</dd></div>
252
+ <div class="sg sg-build"><dt>What we'd build together</dt><dd>Tasks where every figure traces back to a verified calculation.</dd></div>
253
+ </dl>
254
+ </article>
255
+ </div>
256
+ </div>
257
+
258
+ <div class="questions reveal">
259
+ <div class="questions-head">
260
+ <h3>Questions we'd love to ask you</h3>
261
+ <p>No preparation needed. These are the kinds of things we'd talk about in a first conversation.</p>
262
+ </div>
263
+ <ol class="question-list">
264
+ <li>Which part of your week feels most like busywork?</li>
265
+ <li>Where do you already use AI, and where have you stopped trusting it?</li>
266
+ <li>What software, instruments, or pipelines does your work depend on?</li>
267
+ <li>How do you check that a result is right before you believe it?</li>
268
+ <li>Which task would you hand to a reliable assistant tomorrow?</li>
269
+ <li>What would an AI agent need to show before you'd publish its result?</li>
270
+ </ol>
271
+ </div>
272
+ </div>
273
+ </section>
274
+
275
+ <section class="section section-tint" id="examples" aria-labelledby="ex-title">
276
+ <div class="container">
277
+ <div class="section-head reveal">
278
+ <p class="kicker">Demos from our previous work</p>
279
+ <h2 id="ex-title">Agents working with real scientific software.</h2>
280
+ <p class="sub">Each demo is a task from our research. An agent sees a few real runs of a scientific tool and writes a program that reproduces it. We then run that program on settings it has never seen and check every output against the real software.</p>
281
+ </div>
282
+ <div class="ex-grid" id="ex-grid">
283
+ <!--DEMO_CARDS:START-->
284
+ <article class="ex-card reveal" data-index="0" data-kind="micro" tabindex="0" role="button" aria-label="Open demo: Microscopy cell segmentation and counting" style="--dc:#16a34a">
285
+ <div class="ex-thumb"><img src="assets/examples/micro-thumb.webp" alt="Microscopy label masks from public runs" width="740" height="394" loading="lazy"></div>
286
+ <div class="ex-card-body">
287
+ <p class="ex-card-domain">Life &amp; Molecular Sciences</p>
288
+ <h3>Microscopy cell segmentation and counting</h3>
289
+ <p>The same steps as counting nuclei or measuring cell shape in your own fluorescence images.</p>
290
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Biology</li><li class="trk trk-rel">Microscopy</li></ul>
291
+ <ul class="ex-sw"><li>CellProfiler</li><li>scikit-image</li><li>Bio-Formats</li></ul>
292
+ </div>
293
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
294
+ </article>
295
+ <article class="ex-card reveal" data-index="1" data-kind="foam" tabindex="0" role="button" aria-label="Open demo: OpenFOAM 2-D dam-break CFD" style="--dc:#0891b2">
296
+ <div class="ex-thumb"><img src="assets/examples/foam-thumb.webp" alt="OpenFOAM dam-break mesh drawn from the public polyMesh" width="740" height="394" loading="lazy"></div>
297
+ <div class="ex-card-body">
298
+ <p class="ex-card-domain">Physical &amp; Engineering Simulation</p>
299
+ <h3>OpenFOAM 2-D dam-break CFD</h3>
300
+ <p>Meshing, boundary conditions, and solver settings: the setup behind any flow or transport simulation.</p>
301
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Simulation</li><li class="trk trk-rel">Fluid dynamics</li></ul>
302
+ <ul class="ex-sw"><li>OpenFOAM</li></ul>
303
+ </div>
304
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
305
+ </article>
306
+ <article class="ex-card reveal" data-index="2" data-kind="stl" tabindex="0" role="button" aria-label="Open demo: OpenSCAD / BOSL2 parametric fan duct" style="--dc:#7c3aed">
307
+ <div class="ex-thumb"><img src="assets/examples/stl-thumb.webp" alt="Rendered fan-duct mesh from a public OpenSCAD run" width="740" height="394" loading="lazy"></div>
308
+ <div class="ex-card-body">
309
+ <p class="ex-card-domain">Spatial, Visual &amp; 3D</p>
310
+ <h3>OpenSCAD / BOSL2 parametric fan duct</h3>
311
+ <p>Parametric CAD for 3D-printed sample holders, fixtures, and flow cells.</p>
312
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Lab hardware</li><li class="trk trk-rel">CAD</li></ul>
313
+ <ul class="ex-sw"><li>OpenSCAD</li><li>BOSL2</li></ul>
314
+ </div>
315
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
316
+ </article>
317
+ <article class="ex-card reveal" data-index="3" data-kind="astro" tabindex="0" role="button" aria-label="Open demo: Astronomy FITS/WCS cutout and photometry" style="--dc:#db2777">
318
+ <div class="ex-thumb"><img src="assets/examples/astro-thumb.webp" alt="Astronomy FITS cutouts from public runs" width="740" height="394" loading="lazy"></div>
319
+ <div class="ex-card-body">
320
+ <p class="ex-card-domain">Earth &amp; Space Sciences</p>
321
+ <h3>Astronomy FITS/WCS cutout and photometry</h3>
322
+ <p>Aperture photometry and background subtraction, the same math as quantifying spots or bands in noisy images.</p>
323
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Imaging</li><li class="trk trk-rel">Signal extraction</li></ul>
324
+ <ul class="ex-sw"><li>Astropy</li><li>Photutils</li></ul>
325
+ </div>
326
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
327
+ </article>
328
+ <article class="ex-card reveal" data-index="4" data-kind="color" tabindex="0" role="button" aria-label="Open demo: Color-space and tone-curve pipeline" style="--dc:#7c3aed">
329
+ <div class="ex-thumb"><img src="assets/examples/color-thumb.webp" alt="Color-pipeline previews from public runs" width="740" height="394" loading="lazy"></div>
330
+ <div class="ex-card-body">
331
+ <p class="ex-card-domain">Spatial, Visual &amp; 3D</p>
332
+ <h3>Color-space and tone-curve pipeline</h3>
333
+ <p>The color-space math behind calibrated imaging, colorimetric assays, and optical characterization.</p>
334
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Imaging</li><li class="trk trk-rel">Optics</li></ul>
335
+ <ul class="ex-sw"><li>colour-science</li><li>ImageMagick</li><li>scikit-image</li></ul>
336
+ </div>
337
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
338
+ </article>
339
+ <article class="ex-card reveal" data-index="5" data-kind="ocr" tabindex="0" role="button" aria-label="Open demo: Tesseract table-image OCR" style="--dc:#d97706">
340
+ <div class="ex-thumb"><img src="assets/examples/ocr-thumb.webp" alt="OCR page preview from a public run" width="740" height="394" loading="lazy"></div>
341
+ <div class="ex-card-body">
342
+ <p class="ex-card-domain">Data &amp; Document Workflows</p>
343
+ <h3>Tesseract table-image OCR</h3>
344
+ <p>Reading numbers out of scanned tables, the first step in mining property data from papers.</p>
345
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Literature</li><li class="trk trk-rel">Data extraction</li></ul>
346
+ <ul class="ex-sw"><li>Pandoc</li><li>Poppler</li><li>Tesseract</li><li>QPDF</li></ul>
347
+ </div>
348
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
349
+ </article>
350
+ <article class="ex-card reveal is-extra" data-index="6" data-kind="robot" tabindex="0" role="button" aria-label="Open demo: MuJoCo / URDF mass-inertia repair" style="--dc:#0891b2">
351
+ <div class="ex-thumb"><img src="assets/examples/robot-thumb.webp" alt="MuJoCo joint-position trajectories across public scenarios" width="740" height="394" loading="lazy"></div>
352
+ <div class="ex-card-body">
353
+ <p class="ex-card-domain">Physical &amp; Engineering Simulation</p>
354
+ <h3>MuJoCo / URDF mass-inertia repair</h3>
355
+ <p>Physical models with masses, inertias, and joint limits, as used to simulate lab robots before running them.</p>
356
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Lab automation</li><li class="trk trk-rel">Physics</li></ul>
357
+ <ul class="ex-sw"><li>MuJoCo</li><li>URDF/MJCF tooling</li></ul>
358
+ </div>
359
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
360
+ </article>
361
+ <article class="ex-card reveal is-extra" data-index="7" data-kind="blender" tabindex="0" role="button" aria-label="Open demo: Blender cloth flag scene" style="--dc:#7c3aed">
362
+ <div class="ex-thumb"><img src="assets/examples/blender-thumb.webp" alt="Blender cloth scene rotation dial across public scenarios" width="740" height="394" loading="lazy"></div>
363
+ <div class="ex-card-body">
364
+ <p class="ex-card-domain">Spatial, Visual &amp; 3D</p>
365
+ <h3>Blender cloth flag scene</h3>
366
+ <p>A physics simulation whose outcome shifts with wind, constraints, and resolution.</p>
367
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Simulation</li><li class="trk trk-rel">Physics</li></ul>
368
+ <ul class="ex-sw"><li>Blender</li></ul>
369
+ </div>
370
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
371
+ </article>
372
+ <article class="ex-card reveal is-extra" data-index="8" data-kind="drc" tabindex="0" role="button" aria-label="Open demo: KiCad LED-matrix driver PCB" style="--dc:#7c3aed">
373
+ <div class="ex-thumb"><img src="assets/examples/drc-thumb.webp" alt="KiCad board with design-rule findings plotted at their positions" width="740" height="394" loading="lazy"></div>
374
+ <div class="ex-card-body">
375
+ <p class="ex-card-domain">Spatial, Visual &amp; 3D</p>
376
+ <h3>KiCad LED-matrix driver PCB</h3>
377
+ <p>Design-rule checks on circuit boards, like the custom electronics inside lab instruments.</p>
378
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Instrumentation</li><li class="trk trk-rel">Electronics</li></ul>
379
+ <ul class="ex-sw"><li>KiCad</li></ul>
380
+ </div>
381
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
382
+ </article>
383
+ <article class="ex-card reveal is-extra" data-index="9" data-kind="container" tabindex="0" role="button" aria-label="Open demo: Container SBOM and supply-chain attestation" style="--dc:#2563eb">
384
+ <div class="ex-thumb"><img src="assets/examples/container-thumb.webp" alt="Container image layers and attestation from a public run" width="740" height="394" loading="lazy"></div>
385
+ <div class="ex-card-body">
386
+ <p class="ex-card-domain">Software &amp; Formal Systems</p>
387
+ <h3>Container SBOM and supply-chain attestation</h3>
388
+ <p>Recording exactly which software went into an environment, the backbone of reproducible analysis.</p>
389
+ <ul class="ex-card-tracks"><li class="trk trk-rel">Reproducibility</li><li class="trk trk-rel">Software</li></ul>
390
+ <ul class="ex-sw"><li>Hadolint</li><li>Syft</li><li>Skopeo</li></ul>
391
+ </div>
392
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
393
+ </article>
394
+ <!--DEMO_CARDS:END-->
395
+ </div>
396
+ <div class="ex-more">
397
+ <button class="btn btn-ghost" id="ex-more" type="button" aria-expanded="false">Show more demos</button>
398
+ </div>
399
+
400
+ <div class="chores">
401
+ <div class="chores-head reveal">
402
+ <h3>Everyday research computing, too</h3>
403
+ <p>Plenty of research time goes to installing tools, managing data, and fixing broken environments. Our agents practice that as well. Each replay shows commands from the task's own reference solution.</p>
404
+ </div>
405
+ <div class="tx-grid" id="tx-grid">
406
+ <!--TERMINAL_CARDS:START-->
407
+ <article class="tx-card reveal" data-index="0" tabindex="0" role="button" aria-label="Open task: Install an HPC scheduler&#x27;s Python bindings, verified">
408
+ <div class="tx-term" aria-hidden="true">
409
+ <div class="tx-bar"><i></i><i></i><i></i><em>fluxrm/flux-core:focal-v0.48.0</em></div>
410
+ <pre class="tx-screen"><code><span class="tx-cmd"><b>$</b> ACTUAL_HASH=$(sha256sum &quot;$TARBALL&quot; | awk &#x27;{print $1}&#x27;)</span><span class="tx-cmd"><b>$</b> pip install --no-compile &quot;$TARBALL&quot;</span><span class="tx-cmd"><b>$</b> find &quot;$FLUX_ROOT&quot; -type f ! -name &#x27;*.pyc&#x27; -exec sha256sum {} \; | sort -k2 &gt; &quot;$INSTALLED_MANIFEST&quot;</span><span class="tx-cmd"><b>$</b> cp &quot;$INSTALLED_MANIFEST&quot; &quot;$EXPECTED_MANIFEST&quot;</span><span class="tx-cmd"><b>$</b> flux start --test-size=4 bash -c &#x27;…&#x27;</span></code></pre>
411
+ </div>
412
+ <div class="tx-body">
413
+ <p class="tx-area">HPC &amp; clusters</p>
414
+ <h3>Install an HPC scheduler&#x27;s Python bindings, verified</h3>
415
+ <p>Install the Flux Python package from the tarball at /app/flux-python-0.48.0rc6.tar.gz after verifying its integrity against the provided checksum file.</p>
416
+ <ul class="tx-meta"><li>171-line reference solution</li><li>Checked in a fresh sandbox</li></ul>
417
+ </div>
418
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
419
+ </article>
420
+ <article class="tx-card reveal" data-index="1" tabindex="0" role="button" aria-label="Open task: Version a dataset with DVC and reconcile its history">
421
+ <div class="tx-term" aria-hidden="true">
422
+ <div class="tx-bar"><i></i><i></i><i></i><em>ubuntu:22.04</em></div>
423
+ <pre class="tx-screen"><code><span class="tx-cmd"><b>$</b> dvc add dataset/foo</span><span class="tx-cmd"><b>$</b> dvc add dataset/test/0</span><span class="tx-cmd"><b>$</b> git add dataset/foo.dvc dataset/test/0.dvc</span><span class="tx-cmd"><b>$</b> git commit -m &quot;DVC track dataset items&quot;</span><span class="tx-cmd"><b>$</b> git diff &quot;${PARENT}..${ADD_COMMIT}&quot; -- &quot;$dvc_file&quot; &gt; &quot;$patch_path&quot;</span></code></pre>
424
+ </div>
425
+ <div class="tx-body">
426
+ <p class="tx-area">Research data management</p>
427
+ <h3>Version a dataset with DVC and reconcile its history</h3>
428
+ <p>Set up a DVC-tracked dataset under /app/dataset/ with two items: dataset/foo and dataset/test/0.</p>
429
+ <ul class="tx-meta"><li>313-line reference solution</li><li>Checked in a fresh sandbox</li></ul>
430
+ </div>
431
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
432
+ </article>
433
+ <article class="tx-card reveal" data-index="2" tabindex="0" role="button" aria-label="Open task: Diagnose and fix a Python dependency bug">
434
+ <div class="tx-term" aria-hidden="true">
435
+ <div class="tx-bar"><i></i><i></i><i></i><em>python:3.11-slim</em></div>
436
+ <pre class="tx-screen"><code><span class="tx-cmd"><b>$</b> python /app/bug.py &gt; /app/result.txt</span><span class="tx-cmd"><b>$</b> /app/validate_output.py /app/result.txt &gt; /app/validation_stdout.txt 2&gt; /app/validation_stderr.txt</span><span class="tx-cmd"><b>$</b> pip install &quot;environs==${REQUIRED_VERSION}&quot; &#x27;marshmallow&lt;4.0.0&#x27;</span><span class="tx-cmd"><b>$</b> python /app/bug.py &gt; /app/fixed.txt</span></code></pre>
437
+ </div>
438
+ <div class="tx-body">
439
+ <p class="tx-area">Python environments</p>
440
+ <h3>Diagnose and fix a Python dependency bug</h3>
441
+ <p>In /app, diagnose and fix the environs URL parsing bug.</p>
442
+ <ul class="tx-meta"><li>202-line reference solution</li><li>Checked in a fresh sandbox</li></ul>
443
+ </div>
444
+ <span class="ex-open" aria-hidden="true">Explore ↗</span>
445
+ </article>
446
+ <!--TERMINAL_CARDS:END-->
447
+ </div>
448
+ </div>
449
+ </div>
450
+ </section>
451
+
452
+ <section class="section" id="collaborate" aria-labelledby="collab-title">
453
+ <div class="container">
454
+ <div class="section-head reveal">
455
+ <p class="kicker">How we'd work together</p>
456
+ <h2 id="collab-title">From one conversation to a shared paper.</h2>
457
+ <p class="sub">Start small and go as far as you like. Every step helps, and you decide how involved you want to be.</p>
458
+ </div>
459
+ <ol class="steps">
460
+ <li class="step reveal"><span class="step-num">1</span><h3>Talk</h3><p>An informal conversation about your research, your tools, and where AI helps or gets in the way.</p></li>
461
+ <li class="step reveal"><span class="step-num">2</span><h3>Pick a workflow</h3><p>Together we choose a real piece of your work: an analysis script, a simulation setup, or a data pipeline.</p></li>
462
+ <li class="step reveal"><span class="step-num">3</span><h3>Turn it into a test</h3><p>We package it as a task an agent can attempt, with checks you agree are scientifically right.</p></li>
463
+ <li class="step reveal"><span class="step-num">4</span><h3>Improve and publish</h3><p>We measure today's agents, train better ones, and write up what we learn together.</p></li>
464
+ </ol>
465
+ <div class="collab-grid">
466
+ <div class="give-card reveal">
467
+ <h3>What you'd bring</h3>
468
+ <ul class="check-list">
469
+ <li>How research in your field actually gets done</li>
470
+ <li>Real workflows: scripts, simulations, and pipelines</li>
471
+ <li>Judgment about what counts as a correct result</li>
472
+ <li>Feedback on what agents get right and wrong</li>
473
+ </ul>
474
+ </div>
475
+ <div class="give-card give-card-accent reveal">
476
+ <h3>What you'd get</h3>
477
+ <ul class="check-list">
478
+ <li>Co-authorship on the resulting paper for substantial contributions</li>
479
+ <li>A first look at how today's AI agents handle your own workflows</li>
480
+ <li>A packaged, tested version of the workflow you contributed</li>
481
+ <li>Hands-on experience with how AI agents are trained and evaluated</li>
482
+ </ul>
483
+ </div>
484
+ </div>
485
+ <div class="ways reveal">
486
+ <h3>Ways to take part</h3>
487
+ <div class="ways-grid">
488
+ <div class="way"><span class="way-tag">One conversation</span><p>Tell us how you work and where AI lets you down.</p></div>
489
+ <div class="way"><span class="way-tag">Contribute a workflow</span><p>Help turn a piece of your research into a task with real checks.</p></div>
490
+ <div class="way"><span class="way-tag">Co-author</span><p>Join the analysis and the writing of the paper.</p></div>
491
+ </div>
492
+ </div>
493
+ </div>
494
+ </section>
495
+
496
+ <section class="section section-tint" id="papers" aria-labelledby="papers-title">
497
+ <div class="container">
498
+ <div class="section-head reveal">
499
+ <p class="kicker">Our research so far</p>
500
+ <h2 id="papers-title">Results from our recent papers.</h2>
501
+ <p class="sub">The demos above come from this work. Each paper tackles one part of the problem: building tasks that check results reliably, making tasks harder over time, and turning an agent's successes into better training data.</p>
502
+ </div>
503
+ <div class="paper-list">
504
+ <article class="paper reveal">
505
+ <div class="paper-main">
506
+ <p class="paper-meta"><span class="paper-tag">Scientific software</span>arXiv · October 2026</p>
507
+ <h3><a href="https://arxiv.org/abs/2610.02710" target="_blank" rel="noopener">Self-Supervised Scaling of Terminal Environments for Scientific Domains</a></h3>
508
+ <p class="paper-authors">Zhongzhi Li, Yucheng Shi, Zongxia Li, et al.</p>
509
+ <p class="paper-summary">Builds agent tasks directly from existing scientific software. The agent sees a few real runs and must reproduce the workflow, and hidden runs check the result. The tasks span six domains, including life and molecular sciences, physical and engineering simulation, and earth and space sciences.</p>
510
+ <p class="paper-you"><strong>Why it matters to you:</strong> this is how we'd turn your workflows into tasks.</p>
511
+ <div class="paper-links"><a class="btn btn-ghost btn-sm" href="https://arxiv.org/abs/2610.02710" target="_blank" rel="noopener">Read the paper</a></div>
512
+ </div>
513
+ <div class="paper-results">
514
+ <h4>Key results</h4>
515
+ <div class="res">
516
+ <p class="res-label">Fine-tuning on these tasks, Terminal-Bench 2</p>
517
+ <div class="rb rb-base" style="--v:47.9"><i></i><span>Before 47.9%</span></div>
518
+ <div class="rb rb-ours" style="--v:53.6"><i></i><span>After 53.6%</span></div>
519
+ </div>
520
+ <div class="res">
521
+ <p class="res-label">Long-horizon terminal tasks</p>
522
+ <div class="rb rb-base" style="--v:20.7"><i></i><span>Before 20.7%</span></div>
523
+ <div class="rb rb-ours" style="--v:27.7"><i></i><span>After 27.7%</span></div>
524
+ </div>
525
+ <div class="res">
526
+ <p class="res-label">Automated checks agree with blinded human reviewers</p>
527
+ <div class="rb rb-ours" style="--v:98"><i></i><span>98%</span></div>
528
+ </div>
529
+ </div>
530
+ </article>
531
+
532
+ <article class="paper reveal">
533
+ <div class="paper-main">
534
+ <p class="paper-meta"><span class="paper-tag">Harder tasks</span>arXiv · August 2026</p>
535
+ <h3><a href="https://arxiv.org/abs/2608.05466" target="_blank" rel="noopener">Recursive Synthesis for Long-Horizon Terminal Tasks</a></h3>
536
+ <p class="paper-authors">Zhongzhi Li, Yucheng Shi, Zongxia Li, et al.</p>
537
+ <p class="paper-summary">Grows simple, verified tasks into long, multi-step ones, round after round. Every new task is checked in a fresh sandbox before it is kept, at roughly $0.05 per task.</p>
538
+ <p class="paper-you"><strong>Why it matters to you:</strong> agents learn to finish whole projects, not just single commands.</p>
539
+ <div class="paper-links"><a class="btn btn-ghost btn-sm" href="https://arxiv.org/abs/2608.05466" target="_blank" rel="noopener">Read the paper</a></div>
540
+ </div>
541
+ <div class="paper-results">
542
+ <h4>Key results</h4>
543
+ <div class="res">
544
+ <p class="res-label">Same strong model, tasks from round 1 vs. round 15</p>
545
+ <div class="rb rb-base" style="--v:90"><i></i><span>Round 1 solved 90%</span></div>
546
+ <div class="rb rb-warm" style="--v:2.5"><i></i><span>Round 15 solved 2.5%</span></div>
547
+ </div>
548
+ <div class="res">
549
+ <p class="res-label">Median reference solution length</p>
550
+ <div class="rb rb-base" style="--v:17.9"><i></i><span>67 lines</span></div>
551
+ <div class="rb rb-ours" style="--v:100"><i></i><span>374 lines</span></div>
552
+ </div>
553
+ <div class="res">
554
+ <p class="res-label">Reinforcement learning on these tasks, Terminal-Bench Hard</p>
555
+ <div class="rb rb-base" style="--v:22.7"><i></i><span>Before 22.7%</span></div>
556
+ <div class="rb rb-ours" style="--v:32"><i></i><span>After 32.0% (+41% relative)</span></div>
557
+ </div>
558
+ </div>
559
+ </article>
560
+
561
+ <article class="paper reveal">
562
+ <div class="paper-main">
563
+ <p class="paper-meta"><span class="paper-tag">Better training data</span>arXiv · October 2026</p>
564
+ <h3><a href="https://arxiv.org/abs/2610.02826" target="_blank" rel="noopener">Scaling Trajectories for Complex Tasks through Recursive Self-Rewrite</a></h3>
565
+ <p class="paper-authors">Zongxia Li, Yucheng Shi, Zhongzhi Li, et al.</p>
566
+ <p class="paper-summary">Collects an agent's successful solutions from several different setups, then rewrites them into one consistent style the model can learn from. The rewritten data teaches the model far more than the raw successes do.</p>
567
+ <p class="paper-you"><strong>Why it matters to you:</strong> one good solution to your task can become many training examples.</p>
568
+ <div class="paper-links">
569
+ <a class="btn btn-ghost btn-sm" href="https://arxiv.org/abs/2610.02826" target="_blank" rel="noopener">Read the paper</a>
570
+ <a class="btn btn-ghost btn-sm" href="https://huggingface.co/IntelligenceLab/RSR-27B" target="_blank" rel="noopener">Model on Hugging Face</a>
571
+ </div>
572
+ </div>
573
+ <div class="paper-results">
574
+ <h4>Key results</h4>
575
+ <div class="res">
576
+ <p class="res-label">Terminal-Bench 2, solved within three tries</p>
577
+ <div class="rb rb-base" style="--v:57"><i></i><span>Before 57.0%</span></div>
578
+ <div class="rb rb-ours" style="--v:74.2"><i></i><span>After 74.2%</span></div>
579
+ </div>
580
+ <div class="res">
581
+ <p class="res-label">Terminal-Bench Hard, solved within three tries</p>
582
+ <div class="rb rb-base" style="--v:39"><i></i><span>Before 39.0%</span></div>
583
+ <div class="rb rb-ours" style="--v:63"><i></i><span>After 63.0%</span></div>
584
+ </div>
585
+ <div class="res">
586
+ <p class="res-label">Terminal-Bench 4, solved within three tries</p>
587
+ <div class="rb rb-base" style="--v:1.5"><i></i><span>Before 1.5%</span></div>
588
+ <div class="rb rb-ours" style="--v:9.1"><i></i><span>After 9.1%</span></div>
589
+ </div>
590
+ </div>
591
+ </article>
592
+ </div>
593
+ </div>
594
+ </section>
595
+
596
+ <section class="section" id="faq" aria-labelledby="faq-title">
597
+ <div class="container faq-grid">
598
+ <div class="faq-head reveal">
599
+ <p class="kicker">Questions</p>
600
+ <h2 id="faq-title">Before you write to us.</h2>
601
+ <p class="sub">Anything else? Ask us directly. We're happy to talk it through.</p>
602
+ </div>
603
+ <div class="faq-list reveal">
604
+ <details open>
605
+ <summary>Who are you?</summary>
606
+ <p>AI researchers from the University of Maryland, the University of Georgia, the National University of Singapore, Indiana University, The Hong Kong Polytechnic University, and other universities. We build and train AI agents that work with real software, and we publish our results on arXiv.</p>
607
+ </details>
608
+ <details>
609
+ <summary>Do I need machine-learning experience?</summary>
610
+ <p>No. We bring the AI side. What we need is your knowledge of how the science is done and how you know a result is right.</p>
611
+ </details>
612
+ <details>
613
+ <summary>How much time does it take?</summary>
614
+ <p>It's up to you. A single conversation already helps us. Contributing a workflow takes more time, and we'll plan it around your schedule.</p>
615
+ </details>
616
+ <details>
617
+ <summary>Do I have to share unpublished data?</summary>
618
+ <p>No. Tasks can be built from open-source software with public or synthetic inputs. We'll agree together on anything that goes into a paper.</p>
619
+ </details>
620
+ <details>
621
+ <summary>Can I be an author on the paper?</summary>
622
+ <p>Yes. Collaborators who contribute workflows, tasks, or analysis can be co-authors on the resulting paper, following standard authorship norms.</p>
623
+ </details>
624
+ <details>
625
+ <summary>Who can join?</summary>
626
+ <p>Graduate students, Master's or PhD, in biology, materials science, and related fields. Not sure you fit? Write to us anyway.</p>
627
+ </details>
628
+ </div>
629
+ </div>
630
+ </section>
631
+
632
+ <section class="section join" id="join" aria-labelledby="join-title">
633
+ <div class="container join-grid">
634
+ <div class="join-copy reveal">
635
+ <p class="kicker">Get involved</p>
636
+ <h2 id="join-title">Tell us about your research.</h2>
637
+ <p class="sub">A few lines is plenty, and we'll reply to set up a conversation. Sending opens your email app with your answers filled in, so nothing is stored on this site.</p>
638
+ <p class="join-alt">Prefer plain email? Write to <a class="mail-link" data-contact data-show-address data-subject="AI for Science collaboration" href="mailto:zli12321@terpmail.umd.edu,zl22754@uga.edu">zli12321@terpmail.umd.edu and zl22754@uga.edu</a>.</p>
639
+ </div>
640
+ <form class="join-form reveal" id="join-form" novalidate>
641
+ <div class="row2">
642
+ <label>Name<input name="name" autocomplete="name" required></label>
643
+ <label>University &amp; program<input name="program" placeholder="e.g. PhD, Materials Science"></label>
644
+ </div>
645
+ <fieldset class="pill-set">
646
+ <legend>Field</legend>
647
+ <div class="pills">
648
+ <label><input type="radio" name="field" value="Biology" checked><span>Biology</span></label>
649
+ <label><input type="radio" name="field" value="Materials science"><span>Materials science</span></label>
650
+ <label><input type="radio" name="field" value="Other"><span>Something else</span></label>
651
+ </div>
652
+ </fieldset>
653
+ <label>Your research in a sentence<input name="topic" placeholder="e.g. segmenting neurons in light-sheet images"></label>
654
+ <label>How do you use AI in your research today?<textarea name="ai_use" rows="3" placeholder="Writing code, reading papers, structure prediction, nothing yet…"></textarea></label>
655
+ <label>Which part of your work would you most like help with?<textarea name="help" rows="3" placeholder="The step that takes the longest or breaks most often"></textarea></label>
656
+ <fieldset class="pill-set">
657
+ <legend>How would you like to take part?</legend>
658
+ <div class="pills">
659
+ <label><input type="checkbox" name="involve" value="A conversation" checked><span>A conversation</span></label>
660
+ <label><input type="checkbox" name="involve" value="Contribute a workflow"><span>Contribute a workflow</span></label>
661
+ <label><input type="checkbox" name="involve" value="Co-author the paper"><span>Co-author the paper</span></label>
662
+ </div>
663
+ </fieldset>
664
+ <input type="hidden" name="interest" value="">
665
+ <div class="form-actions">
666
+ <button class="btn btn-dark" type="submit">Send to the team
667
+ <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 10h11m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
668
+ </button>
669
+ <p class="form-note" id="form-note" aria-live="polite"></p>
670
+ </div>
671
+ </form>
672
+ </div>
673
+ </section>
674
+ </main>
675
+
676
+ <footer class="footer">
677
+ <div class="container footer-inner">
678
+ <a class="brand" href="#top" aria-label="Back to top">
679
+ <svg class="brand-mark" viewBox="0 0 32 32" aria-hidden="true">
680
+ <rect width="32" height="32" rx="8" fill="url(#brand-grad)"/>
681
+ <path d="M7 23h5v-5h5v-5h5" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
682
+ <path d="M19.6 9.4l2.2 2.2 4.2-4.4" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
683
+ </svg>
684
+ <span>IntelligenceLab <em>AI for Science</em></span>
685
+ </a>
686
+ <nav class="footer-links" aria-label="Footer">
687
+ <a href="#your-research">Your research</a>
688
+ <a href="#examples">Demos</a>
689
+ <a href="#papers">Papers</a>
690
+ <a href="#join">Get involved</a>
691
+ </nav>
692
+ <p class="footer-note">© <span data-year>2026</span> IntelligenceLab. Software names belong to their respective projects.</p>
693
+ </div>
694
+ </footer>
695
+
696
+ <div class="ex-modal" id="ex-modal" hidden>
697
+ <div class="ex-backdrop" data-ex-close></div>
698
+ <div class="ex-sheet" role="dialog" aria-modal="true" aria-labelledby="exm-title">
699
+ <header class="ex-head">
700
+ <div>
701
+ <p class="ex-domain" id="exm-domain"></p>
702
+ <h3 id="exm-title"></h3>
703
+ <ul class="ex-tracks" id="exm-tracks"></ul>
704
+ </div>
705
+ <div class="ex-nav">
706
+ <button class="icon-btn" type="button" data-ex-prev aria-label="Previous demo">←</button>
707
+ <button class="icon-btn" type="button" data-ex-next aria-label="Next demo">→</button>
708
+ <button class="icon-btn" type="button" data-ex-close aria-label="Close">✕</button>
709
+ </div>
710
+ </header>
711
+ <div class="ex-body">
712
+ <figure class="ex-figure">
713
+ <div class="ex-live-hero" id="exm-live" aria-hidden="true" hidden></div>
714
+ <figcaption class="ex-evidence-label">Evidence from the task's public runs</figcaption>
715
+ <img id="exm-img" alt="" width="1600" height="939">
716
+ </figure>
717
+ <p class="ex-desc" id="exm-desc"></p>
718
+ <p class="ex-why" id="exm-why"></p>
719
+ <div class="ex-facts">
720
+ <div><h4>Software</h4><ul id="exm-software"></ul></div>
721
+ <div><h4>What the agent sees</h4><ul id="exm-artifacts"></ul></div>
722
+ <div><h4>What we vary</h4><ul id="exm-axes"></ul></div>
723
+ <div><h4>What gets checked</h4><ul id="exm-outputs"></ul></div>
724
+ </div>
725
+ <p class="ex-how">The agent sees public runs of the real software and writes a program that reproduces it. That program is then run on hidden settings of these parameters and must match the software's outputs, field by field.</p>
726
+ <a class="btn btn-dark" id="exm-cta" href="#join">Work with tools like these? Let's talk
727
+ <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 10h11m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
728
+ </a>
729
+ </div>
730
+ </div>
731
+ </div>
732
+
733
+ <div class="ex-modal" id="tx-modal" data-subject="AI for Science, research computing" hidden>
734
+ <div class="ex-backdrop" data-tx-close></div>
735
+ <div class="ex-sheet" role="dialog" aria-modal="true" aria-labelledby="txm-title">
736
+ <header class="ex-head">
737
+ <div>
738
+ <p class="ex-domain" id="txm-area"></p>
739
+ <h3 id="txm-title"></h3>
740
+ <ul class="ex-tracks" id="txm-tags"></ul>
741
+ </div>
742
+ <div class="ex-nav">
743
+ <button class="icon-btn" type="button" data-tx-prev aria-label="Previous task">←</button>
744
+ <button class="icon-btn" type="button" data-tx-next aria-label="Next task">→</button>
745
+ <button class="icon-btn" type="button" data-tx-close aria-label="Close">✕</button>
746
+ </div>
747
+ </header>
748
+ <div class="ex-body">
749
+ <div class="txm-grid">
750
+ <div class="txm-col">
751
+ <h4 class="txm-h">Instruction the agent receives</h4>
752
+ <div class="txm-instruction" id="txm-instruction"></div>
753
+ <dl class="txm-facts">
754
+ <div><dt>Environment</dt><dd id="txm-base"></dd></div>
755
+ <div><dt>Reference solution</dt><dd id="txm-lines"></dd></div>
756
+ <div><dt>Checking</dt><dd>The reference solution passes every check in a fresh sandbox, and each check is stated in the instruction.</dd></div>
757
+ </dl>
758
+ </div>
759
+ <div class="txm-col">
760
+ <h4 class="txm-h">Replay of the reference solution</h4>
761
+ <div class="tx-term tx-term-lg" aria-hidden="true">
762
+ <div class="tx-bar"><i></i><i></i><i></i><em id="txm-term-base"></em></div>
763
+ <pre class="tx-screen" id="txm-replay"><code></code></pre>
764
+ </div>
765
+ <h4 class="txm-h">Excerpt of <code>solve.sh</code></h4>
766
+ <pre class="txm-code" id="txm-code"></pre>
767
+ </div>
768
+ </div>
769
+ <a class="btn btn-dark" id="txm-cta" data-contact data-subject="AI for Science, research computing" href="#join">Email us about a task like this
770
+ <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 10h11m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
771
+ </a>
772
+ </div>
773
+ </div>
774
+ </div>
775
+
776
+ <script type="application/json" id="examples-data">[{"kind": "micro", "title": "Microscopy cell segmentation and counting", "domain": "Life & Molecular Sciences", "color": "#16a34a", "description": "Reconstruct cell segmentation and measurement behavior across channel, illumination, threshold, object size, and measurement feature.", "why": "The same steps as counting nuclei or measuring cell shape in your own fluorescence images.", "tags": ["Biology", "Microscopy"], "software": ["CellProfiler", "scikit-image", "Bio-Formats"], "artifacts": ["label-mask image", "object-measurements CSV"], "axes": ["channel selection", "illumination correction", "measurement feature", "object size range", "threshold method"], "outputs": ["object count", "area sum", "eccentricity sum", "threshold", "minimum size"], "img": "assets/examples/micro-detail.webp", "alt": "Microscopy label masks from public runs"}, {"kind": "foam", "title": "OpenFOAM 2-D dam-break CFD", "domain": "Physical & Engineering Simulation", "color": "#0891b2", "description": "Reconstruct a 2-D dam-break CFD workflow across water height, mesh, gravity, and simulation time.", "why": "Meshing, boundary conditions, and solver settings: the setup behind any flow or transport simulation.", "tags": ["Simulation", "Fluid dynamics"], "software": ["OpenFOAM"], "artifacts": ["blockMesh log", "polyMesh", "initial field files", "case dictionaries"], "axes": ["water height", "mesh", "gravity", "time"], "outputs": ["mesh statistics", "field summaries", "residual/probe behavior"], "img": "assets/examples/foam-detail.webp", "alt": "OpenFOAM dam-break mesh drawn from the public polyMesh"}, {"kind": "stl", "title": "OpenSCAD / BOSL2 parametric fan duct", "domain": "Spatial, Visual & 3D", "color": "#7c3aed", "description": "Regenerate a swept fan-duct mount STL whose inlet size, outlet size, bend angle, and rib count change the geometry.", "why": "Parametric CAD for 3D-printed sample holders, fixtures, and flow cells.", "tags": ["Lab hardware", "CAD"], "software": ["OpenSCAD", "BOSL2"], "artifacts": ["STL mesh", "geometry metrics JSON"], "axes": ["inlet size", "outlet size", "bend angle", "rib count"], "outputs": ["bounding box", "volume", "surface area", "connected components", "sampled surface distance"], "img": "assets/examples/stl-detail.webp", "alt": "Rendered fan-duct mesh from a public OpenSCAD run"}, {"kind": "astro", "title": "Astronomy FITS/WCS cutout and photometry", "domain": "Earth & Space Sciences", "color": "#db2777", "description": "Reconstruct FITS/WCS cutout and photometry behavior under aperture, background, clipping, threshold, and coordinate-frame interventions.", "why": "Aperture photometry and background subtraction, the same math as quantifying spots or bands in noisy images.", "tags": ["Imaging", "Signal extraction"], "software": ["Astropy", "Photutils"], "artifacts": ["FITS image", "photometry catalog CSV"], "axes": ["aperture radius", "background model", "sigma clip", "source threshold", "WCS frame"], "outputs": ["flux", "coordinate frame", "median", "shape", "standard deviation"], "img": "assets/examples/astro-detail.webp", "alt": "Astronomy FITS cutouts from public runs"}, {"kind": "color", "title": "Color-space and tone-curve pipeline", "domain": "Spatial, Visual & 3D", "color": "#7c3aed", "description": "Infer a color pipeline across color space, white point, tone curve, demosaicing, and compression quality.", "why": "The color-space math behind calibrated imaging, colorimetric assays, and optical characterization.", "tags": ["Imaging", "Optics"], "software": ["colour-science", "ImageMagick", "scikit-image"], "artifacts": ["color measurements JSON", "PPM preview"], "axes": ["color space", "white point", "tone curve", "demosaic method", "compression quality"], "outputs": ["mean color", "image shape", "XYZ tristimulus values"], "img": "assets/examples/color-detail.webp", "alt": "Color-pipeline previews from public runs"}, {"kind": "ocr", "title": "Tesseract table-image OCR", "domain": "Data & Document Workflows", "color": "#d97706", "description": "Reconstruct how DPI, font, language, metadata, and page range affect OCR behavior.", "why": "Reading numbers out of scanned tables, the first step in mining property data from papers.", "tags": ["Literature", "Data extraction"], "software": ["Pandoc", "Poppler", "Tesseract", "QPDF"], "artifacts": ["OCR summary JSON", "page-preview image"], "axes": ["DPI", "font", "language", "metadata", "page range"], "outputs": ["ink", "line count", "page size", "text digest", "text length"], "img": "assets/examples/ocr-detail.webp", "alt": "OCR page preview from a public run"}, {"kind": "robot", "title": "MuJoCo / URDF mass-inertia repair", "domain": "Physical & Engineering Simulation", "color": "#0891b2", "description": "Infer robot state and trajectory behavior under actuator, collision, pose, joint-limit, and target interventions.", "why": "Physical models with masses, inertias, and joint limits, as used to simulate lab robots before running them.", "tags": ["Lab automation", "Physics"], "software": ["MuJoCo", "URDF/MJCF tooling"], "artifacts": ["URDF model", "trajectory CSV", "control summary"], "axes": ["actuator gain", "collision margin", "initial pose", "joint limit", "trajectory target"], "outputs": ["gain", "nq", "nv", "qpos", "qvel"], "img": "assets/examples/robot-detail.webp", "alt": "MuJoCo joint-position trajectories across public scenarios"}, {"kind": "blender", "title": "Blender cloth flag scene", "domain": "Spatial, Visual & 3D", "color": "#7c3aed", "description": "Reconstruct cloth-scene behavior under wind, pin-group, frame, and subdivision interventions.", "why": "A physics simulation whose outcome shifts with wind, constraints, and resolution.", "tags": ["Simulation", "Physics"], "software": ["Blender"], "artifacts": ["scene graph JSON"], "axes": ["wind strength", "pin group", "frame", "cloth subdiv"], "outputs": ["evaluated objects", "frame", "materials", "node groups", "object counts", "rotation"], "img": "assets/examples/blender-detail.webp", "alt": "Blender cloth scene rotation dial across public scenarios"}, {"kind": "drc", "title": "KiCad LED-matrix driver PCB", "domain": "Spatial, Visual & 3D", "color": "#7c3aed", "description": "Reconstruct LED-matrix driver board outputs (DRC report and project files) under row, column, driver, and routing interventions.", "why": "Design-rule checks on circuit boards, like the custom electronics inside lab instruments.", "tags": ["Instrumentation", "Electronics"], "software": ["KiCad"], "artifacts": ["DRC report", "project file list"], "axes": ["rows", "cols", "driver", "routing"], "outputs": ["DRC report summary", "project file inventory"], "img": "assets/examples/drc-detail.webp", "alt": "KiCad board with design-rule findings plotted at their positions"}, {"kind": "container", "title": "Container SBOM and supply-chain attestation", "domain": "Software & Formal Systems", "color": "#2563eb", "description": "Infer coupled container base-image, package-set, label, entrypoint, and SBOM-scope behavior.", "why": "Recording exactly which software went into an environment, the backbone of reproducible analysis.", "tags": ["Reproducibility", "Software"], "software": ["Hadolint", "Syft", "Skopeo"], "artifacts": ["Dockerfile", "in-toto attestation", "SPDX SBOM"], "axes": ["base image", "entrypoint", "labels", "package set", "SBOM scope"], "outputs": ["attestation", "base", "layer count", "merkle", "vuln budget"], "img": "assets/examples/container-detail.webp", "alt": "Container image layers and attestation from a public run"}]</script>
777
+ <script type="application/json" id="terminal-data">{"tasks": [{"title": "Install an HPC scheduler's Python bindings, verified", "area": "HPC & clusters", "summary": "Install the Flux Python package from the tarball at /app/flux-python-0.48.0rc6.tar.gz after verifying its integrity against the provided checksum file.", "instruction": "<p>Install the Flux Python package from the tarball at /app/flux-python-0.48.0rc6.tar.gz after verifying its integrity against the provided checksum file.<\/p><p>The expected manifest file may be outdated; if the installed layout differs, update the manifest and note the discrepancy in the installation report.<\/p><p>The installation report at /app/installation_report.txt must include the verified tarball hash, describe any manifest changes, and also contain additional package metadata specified by the configuration at /app/config/metadata.json — follow that config&#x27;s instructions for intermediate reporting.<\/p><p>Finally, use the Flux Python bindings to produce /app/result.txt containing exactly the string found in /app/config/expected.txt.<\/p>", "base": "fluxrm/flux-core:focal-v0.48.0", "lines": 171, "tags": ["flux", "hpc", "job-scheduling", "python-bindings"], "replay": [["$", "ACTUAL_HASH=$(sha256sum \"$TARBALL\" | awk '{print $1}')"], ["$", "pip install --no-compile \"$TARBALL\""], ["$", "find \"$FLUX_ROOT\" -type f ! -name '*.pyc' -exec sha256sum {} \\; | sort -k2 > \"$INSTALLED_MANIFEST\""], ["$", "cp \"$INSTALLED_MANIFEST\" \"$EXPECTED_MANIFEST\""], ["$", "flux start --test-size=4 bash -c '…'"]], "code": "set -euo pipefail\nTARBALL=\"/app/flux-python-0.48.0rc6.tar.gz\"\nCHECKSUM_FILE=\"/app/checksum.sha256\"\nVALIDATION_FILE=\"/app/validation.txt\"\nCONFIG_FILE=\"/app/config/expected.txt\"\nRESULT_FILE=\"/app/result.txt\"\nEXPECTED_MANIFEST=\"/app/expected_manifest.txt\"\nINSTALLED_MANIFEST=\"/app/installed_manifest.txt\"\nINSTALLATION_REPORT=\"/app/installation_report.txt\"\nMETADATA_CONFIG=\"/app/config/metadata.json\"\necho \"=== INSPECT: verify required inputs exist ===\"\nfor f in \"$TARBALL\" \"$CHECKSUM_FILE\" \"$EXPECTED_MANIFEST\" \"$CONFIG_FILE\" \"$METADATA_CONFIG\"; do\n if [ ! -f \"$f\" ]; then\n echo \"ERROR: Required file $f not found\" >&2\n exit 1\n fi\ndone\necho \"=== INSPECT: read metadata configuration ===\"\nMETADATA_FILE=$(python3 -c \"import json; cfg=json.load(open('$METADATA_CONFIG')); print(cfg['metadata_file'])\")\nFIELDS=$(python3 -c \"import json; cfg=json.load(open('$METADATA_CONFIG')); print(' '.join(cfg['required_metadata']))\")\necho \"Metadata will be gathered for fields: $FIELDS\"\necho \"Intermediate metadata file: /app/$METADATA_FILE\"\necho \"=== DERIVE: verify tarball hash ===\"\nACTUAL_HASH=$(sha256sum \"$TARBALL\" | awk '{print $1}')\nEXPECTED_HASH=$(awk '{print $1}' \"$CHECKSUM_FILE\")\nif [ \"$ACTUAL_HASH\" != \"$EXPECTED_HASH\" ]; then\n echo \"ERROR: Tarball checksum mismatch\" >&2\n exit 1\nfi\necho \"=== VALIDATE checksum OK, writing validation report ===\""}, {"title": "Version a dataset with DVC and reconcile its history", "area": "Research data management", "summary": "Set up a DVC-tracked dataset under /app/dataset/ with two items: dataset/foo and dataset/test/0.", "instruction": "<p>Set up a DVC-tracked dataset under /app/dataset/ with two items: dataset/foo and dataset/test/0.<\/p><p>Track both with DVC, remove the original data, and commit the resulting .dvc files.<\/p><p>Then produce the following artifacts under /app: dvc_inventory.json, dvc_reconciliation.md, dvc_changes.diff, dvc_status.txt, and a directory /app/patches/ containing a patch file for each of dataset/foo.dvc and dataset/test/0.dvc that captures the changes introduced when that file was first committed.<\/p><p>The reconciliation report must document the absence of the data, the git commits that introduced each .dvc file (SHA and message), and the baseline tag (name and commit SHA).<\/p><p>The diff should be taken from that tag to HEAD.<\/p><p>The repository already has git and DVC initialized, and a git tag marks the initial DVC initialization; discover and use that tag as the baseline.<\/p><p>Refer to /app/task_files/ for the required formats of the inventory and the reconciliation report.<\/p>", "base": "ubuntu:22.04", "lines": 313, "tags": ["dvc", "data-versioning", "version-control"], "replay": [["$", "dvc add dataset/foo"], ["$", "dvc add dataset/test/0"], ["$", "git add dataset/foo.dvc dataset/test/0.dvc"], ["$", "git commit -m \"DVC track dataset items\""], ["$", "git diff \"${PARENT}..${ADD_COMMIT}\" -- \"$dvc_file\" > \"$patch_path\""]], "code": "set -euo pipefail\nif [ -s /app/dvc_changes.diff ] && \\\n [ -s /app/dvc_reconciliation.md ] && \\\n [ -s /app/dvc_inventory.json ] && \\\n [ -s /app/dvc_status.txt ] && \\\n grep -q 'Commit message:' /app/dvc_reconciliation.md && \\\n grep -q 'Baseline tag:' /app/dvc_reconciliation.md && \\\n [ -d /app/patches ] && \\\n [ \"$(ls -1 /app/patches/*.patch 2>/dev/null | wc -l)\" -ge 2 ]; then\n echo \"Required artifacts with commit messages, baseline tag, and patches already present. Nothing to do.\"\n exit 0\nfi\ncd /app\nrm -f dataset/foo.dvc dataset/test/0.dvc\nrm -rf dataset/foo dataset/test/0\nrm -f /app/dvc_reconciliation.md \\\n /app/dvc_inventory.json \\\n /app/dvc_changes.diff \\\n /app/dvc_status.txt\nrm -rf /app/patches\nINIT_COMMIT=$(git rev-list --max-parents=0 HEAD)\nif [ -z \"$INIT_COMMIT\" ]; then\n echo \"ERROR: could not determine initial commit\"\n exit 1\nfi\necho \"Initial commit: $INIT_COMMIT\"\nreadarray -t TAGS < <(git tag --points-at \"$INIT_COMMIT\")\nif [ \"${#TAGS[@]}\" -eq 0 ]; then\n echo \"ERROR: no tag found pointing to the initial commit\"\n exit 1"}, {"title": "Diagnose and fix a Python dependency bug", "area": "Python environments", "summary": "In /app, diagnose and fix the environs URL parsing bug.", "instruction": "<p>In /app, diagnose and fix the environs URL parsing bug.<\/p><p>Run bug.py to capture the original output and use validate_output.py to diagnose the failure.<\/p><p>Consult changelog.md to determine the required environs version, upgrade the library, and produce a corrected output in fixed.txt.<\/p><p>Write a test suite test_bug.py covering both mongodb and mongodb+srv URL schemes.<\/p><p>The suite must include a test that runs validate_output.py against bug.py’s output after the fix and asserts the validator exits successfully.<\/p><p>Run the tests and save the report as test_report.txt.<\/p><p>Finally, create validation_passed.txt and a debug summary debug_summary.txt describing the error and the fix.<\/p>", "base": "python:3.11-slim", "lines": 202, "tags": ["python", "library-bug", "url-parsing", "environs"], "replay": [["$", "python /app/bug.py > /app/result.txt"], ["$", "/app/validate_output.py /app/result.txt > /app/validation_stdout.txt 2> /app/validation_stderr.txt"], ["$", "pip install \"environs==${REQUIRED_VERSION}\" 'marshmallow<4.0.0'"], ["$", "python /app/bug.py > /app/fixed.txt"]], "code": "set -euo pipefail\necho \"Ensuring pytest is available\"\npython -m pytest --version >/dev/null 2>&1 || pip install pytest\necho \"Stage 1: Create original bug.py (known buggy)\"\ncat > /app/bug.py << 'EOF'\nfrom environs import Env\nenv = Env()\nMONGODB_URL = env.url(\n \"MONGODB_URL\",\n default=\"mongodb://user:pass@mongo.example.local/db?authSource=admin\",\n schemes={\"mongodb\", \"mongodb+srv\"},\n require_tld=False,\n)\nprint(MONGODB_URL)\nEOF\necho \"Stage 2: Run bug.py and capture original output\"\npython /app/bug.py > /app/result.txt\necho \"Stage 3: Validate original output and interpret failure\"\nset +e\n/app/validate_output.py /app/result.txt > /app/validation_stdout.txt 2> /app/validation_stderr.txt\nVALIDATION_EXIT=$?\nset -e\nif [ $VALIDATION_EXIT -eq 0 ]; then\n echo \"ERROR: validate_output.py unexpectedly succeeded on buggy output\" >&2\n exit 1\nfi\nif ! grep -q \"ParseResult\" /app/validation_stderr.txt; then\n echo \"ERROR: Expected 'ParseResult' indication in validator failure output\" >&2\n exit 1\nfi"}]}</script>
778
+ <script src="app.js" defer></script>
779
+ <script src="live.js" defer></script>
780
+ <script src="terminal.js" defer></script>
781
+ </body>
782
  </html>
live.js ADDED
@@ -0,0 +1,810 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (() => {
2
+ 'use strict';
3
+
4
+ const REDUCE = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
5
+ const SC = ['#0d9488', '#2563eb', '#7c3aed', '#db2777', '#d97706', '#059669'];
6
+ const DRC = ['#fbbf24', '#2dd4bf', '#f472b6', '#60a5fa', '#a78bfa', '#34d399', '#fb7185', '#22d3ee'];
7
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
8
+ const easeOut = (t) => 1 - Math.pow(1 - clamp(t, 0, 1), 3);
9
+ const easeInOut = (t) => {
10
+ const x = clamp(t, 0, 1);
11
+ return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
12
+ };
13
+ const fmt = (n) => n.toLocaleString('en-US');
14
+ const hexRgb = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));
15
+
16
+ function decode(b64, Type) {
17
+ const bin = atob(b64);
18
+ const u8 = new Uint8Array(bin.length);
19
+ for (let i = 0; i < bin.length; i += 1) u8[i] = bin.charCodeAt(i);
20
+ return new Type(u8.buffer);
21
+ }
22
+
23
+ function make(tag, cls, parent, text) {
24
+ const node = document.createElement(tag);
25
+ if (cls) node.className = cls;
26
+ if (text != null) node.textContent = text;
27
+ if (parent) parent.appendChild(node);
28
+ return node;
29
+ }
30
+
31
+ // One animation loop drives every visible view.
32
+ const active = new Set();
33
+ let raf = 0;
34
+ function loop(now) {
35
+ active.forEach((view) => { if (view.ready) view.render(now); });
36
+ raf = active.size && !document.hidden ? requestAnimationFrame(loop) : 0;
37
+ }
38
+ function wake() {
39
+ if (!raf && active.size && !document.hidden) raf = requestAnimationFrame(loop);
40
+ }
41
+ document.addEventListener('visibilitychange', () => {
42
+ if (document.hidden) {
43
+ cancelAnimationFrame(raf);
44
+ raf = 0;
45
+ } else {
46
+ wake();
47
+ }
48
+ });
49
+
50
+ class View {
51
+ constructor(box, { canvas = true, hero = false } = {}) {
52
+ this.box = box;
53
+ this.hero = hero;
54
+ this.t0 = performance.now() - (REDUCE ? 600000 : 0);
55
+ this.w = 1;
56
+ this.h = 1;
57
+ if (canvas) {
58
+ this.canvas = make('canvas', 'lv-canvas', box);
59
+ this.ctx = this.canvas.getContext('2d');
60
+ }
61
+ }
62
+
63
+ start() {
64
+ this.ro = new ResizeObserver(() => this.resize());
65
+ this.ro.observe(this.box);
66
+ this.io = new IntersectionObserver((entries) => {
67
+ if (!REDUCE && entries.some((e) => e.isIntersecting)) {
68
+ active.add(this);
69
+ wake();
70
+ } else {
71
+ active.delete(this);
72
+ }
73
+ }, { rootMargin: '60px' });
74
+ this.io.observe(this.box);
75
+ this.resize();
76
+ return this;
77
+ }
78
+
79
+ resize() {
80
+ const r = this.box.getBoundingClientRect();
81
+ this.w = Math.max(1, r.width);
82
+ this.h = Math.max(1, r.height);
83
+ if (this.canvas) {
84
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
85
+ const W = Math.round(this.w * dpr);
86
+ const H = Math.round(this.h * dpr);
87
+ if (this.canvas.width !== W || this.canvas.height !== H) {
88
+ this.canvas.width = W;
89
+ this.canvas.height = H;
90
+ }
91
+ this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
92
+ }
93
+ if (this.w < 8 || this.h < 8) return;
94
+ this.layout();
95
+ this.ready = true;
96
+ this.render(performance.now());
97
+ }
98
+
99
+ layout() {}
100
+
101
+ render() {}
102
+
103
+ elapsed(now) {
104
+ return now - this.t0;
105
+ }
106
+
107
+ caption(text) {
108
+ if (!this.cap) this.cap = make('span', 'lv-cap', this.box);
109
+ if (this.cap.textContent !== text) this.cap.textContent = text;
110
+ }
111
+
112
+ dispose() {
113
+ active.delete(this);
114
+ if (this.io) this.io.disconnect();
115
+ if (this.ro) this.ro.disconnect();
116
+ this.box.replaceChildren();
117
+ }
118
+ }
119
+
120
+ // OpenSCAD meshes: rotate, alternating public runs at one shared scale.
121
+ class MeshLive extends View {
122
+ constructor(box, v, opts) {
123
+ super(box, opts);
124
+ this.base = hexRgb(opts.color || '#7c3aed');
125
+ this.meshes = v.meshes.map((m) => {
126
+ const T = decode(m.tris, Float32Array);
127
+ const mn = [Infinity, Infinity, Infinity];
128
+ const mx = [-Infinity, -Infinity, -Infinity];
129
+ for (let i = 0; i < T.length; i += 3) {
130
+ for (let k = 0; k < 3; k += 1) {
131
+ const x = T[i + k];
132
+ if (x < mn[k]) mn[k] = x;
133
+ if (x > mx[k]) mx[k] = x;
134
+ }
135
+ }
136
+ return {
137
+ id: m.id,
138
+ T,
139
+ n: T.length / 9,
140
+ ctr: [0, 1, 2].map((k) => (mn[k] + mx[k]) / 2),
141
+ rad: Math.hypot(mx[0] - mn[0], mx[1] - mn[1], mx[2] - mn[2]) / 2 || 1,
142
+ };
143
+ });
144
+ this.rad = Math.max(...this.meshes.map((m) => m.rad));
145
+ const maxN = Math.max(...this.meshes.map((m) => m.n));
146
+ this.tmp = new Float32Array(9);
147
+ this.depth = new Float32Array(maxN);
148
+ this.order = new Uint32Array(maxN);
149
+ this.pts = new Float32Array(maxN * 6);
150
+ this.col = new Array(maxN);
151
+ this.cycle = 6500;
152
+ this.start();
153
+ }
154
+
155
+ render(now) {
156
+ const { ctx, w, h, tmp, pts, depth, order } = this;
157
+ const t = this.elapsed(now);
158
+ const mesh = this.meshes[Math.floor(t / this.cycle) % this.meshes.length];
159
+ const ph = (t % this.cycle) / this.cycle;
160
+ const alpha = REDUCE ? 1 : clamp(Math.min(ph, 1 - ph) / 0.07, 0, 1);
161
+ ctx.clearRect(0, 0, w, h);
162
+
163
+ const m = Math.min(w, h);
164
+ const ox = w / 2;
165
+ const oy = h / 2;
166
+ const shadow = ctx.createRadialGradient(ox, oy + h * 0.33, 0, ox, oy + h * 0.33, m * 0.45);
167
+ shadow.addColorStop(0, 'rgba(15,23,42,.24)');
168
+ shadow.addColorStop(1, 'rgba(15,23,42,0)');
169
+ ctx.fillStyle = shadow;
170
+ ctx.beginPath();
171
+ ctx.ellipse(ox, oy + h * 0.33, m * 0.45, m * 0.07, 0, 0, Math.PI * 2);
172
+ ctx.fill();
173
+
174
+ const s = (m * 0.5) / this.rad;
175
+ const yaw = -0.75 + t * 0.00045;
176
+ const cy = Math.cos(yaw);
177
+ const sy = Math.sin(yaw);
178
+ const cp = Math.cos(0.42);
179
+ const sp = Math.sin(0.42);
180
+ const [mx, my, mz] = mesh.ctr;
181
+ let lx = -0.45;
182
+ let ly = -0.72;
183
+ let lz = 0.55;
184
+ const ll = Math.hypot(lx, ly, lz);
185
+ lx /= ll; ly /= ll; lz /= ll;
186
+ let hx = lx;
187
+ let hy = ly - 1;
188
+ let hz = lz;
189
+ const hl = Math.hypot(hx, hy, hz);
190
+ hx /= hl; hy /= hl; hz /= hl;
191
+ const [br, bg, bb] = this.base;
192
+ const { T, n } = mesh;
193
+
194
+ for (let f = 0; f < n; f += 1) {
195
+ const b = f * 9;
196
+ for (let k = 0; k < 3; k += 1) {
197
+ const x = T[b + 3 * k] - mx;
198
+ const y = T[b + 3 * k + 1] - my;
199
+ const z = T[b + 3 * k + 2] - mz;
200
+ const x1 = x * cy - y * sy;
201
+ const y1 = x * sy + y * cy;
202
+ tmp[3 * k] = x1;
203
+ tmp[3 * k + 1] = y1 * cp - z * sp;
204
+ tmp[3 * k + 2] = y1 * sp + z * cp;
205
+ }
206
+ const ux = tmp[3] - tmp[0];
207
+ const uy = tmp[4] - tmp[1];
208
+ const uz = tmp[5] - tmp[2];
209
+ const vx = tmp[6] - tmp[0];
210
+ const vy = tmp[7] - tmp[1];
211
+ const vz = tmp[8] - tmp[2];
212
+ let nx = uy * vz - uz * vy;
213
+ let ny = uz * vx - ux * vz;
214
+ let nz = ux * vy - uy * vx;
215
+ const nl = Math.hypot(nx, ny, nz) || 1;
216
+ nx /= nl; ny /= nl; nz /= nl;
217
+ if (ny > 0) { nx = -nx; ny = -ny; nz = -nz; }
218
+ const dif = Math.max(0, nx * lx + ny * ly + nz * lz);
219
+ const spec = Math.pow(Math.max(0, nx * hx + ny * hy + nz * hz), 30) * 140;
220
+ const k = 0.2 + 0.8 * dif;
221
+ this.col[f] = `rgb(${Math.min(255, br * k + spec) | 0},${Math.min(255, bg * k + spec) | 0},${Math.min(255, bb * k + spec) | 0})`;
222
+ depth[f] = tmp[1] + tmp[4] + tmp[7];
223
+ const o = f * 6;
224
+ pts[o] = ox + tmp[0] * s;
225
+ pts[o + 1] = oy - tmp[2] * s;
226
+ pts[o + 2] = ox + tmp[3] * s;
227
+ pts[o + 3] = oy - tmp[5] * s;
228
+ pts[o + 4] = ox + tmp[6] * s;
229
+ pts[o + 5] = oy - tmp[8] * s;
230
+ order[f] = f;
231
+ }
232
+ const sorted = order.subarray(0, n).sort((p, q) => depth[q] - depth[p]);
233
+ ctx.globalAlpha = alpha;
234
+ ctx.lineJoin = 'round';
235
+ ctx.lineWidth = 0.6;
236
+ for (let j = 0; j < n; j += 1) {
237
+ const f = sorted[j];
238
+ const o = f * 6;
239
+ ctx.beginPath();
240
+ ctx.moveTo(pts[o], pts[o + 1]);
241
+ ctx.lineTo(pts[o + 2], pts[o + 3]);
242
+ ctx.lineTo(pts[o + 4], pts[o + 5]);
243
+ ctx.closePath();
244
+ ctx.fillStyle = this.col[f];
245
+ ctx.strokeStyle = this.col[f];
246
+ ctx.fill();
247
+ ctx.stroke();
248
+ }
249
+ ctx.globalAlpha = 1;
250
+ this.caption(`${mesh.id} · ${fmt(mesh.n)} facets`);
251
+ }
252
+ }
253
+
254
+ // OpenFOAM: the mesh draws in, then tracer particles flow inlet to outlet.
255
+ class FoamLive extends View {
256
+ constructor(box, v, opts) {
257
+ super(box, opts);
258
+ this.P = decode(v.points, Float32Array);
259
+ this.Q = decode(v.quads, Uint16Array);
260
+ const pal = { inlet: '#2563eb', outlet: '#d97706' };
261
+ this.patches = v.patches.map((p) => ({
262
+ E: decode(p.edges, Uint16Array),
263
+ color: pal[p.name] || (p.type === 'wall' ? '#1e293b' : '#db2777'),
264
+ }));
265
+ let x0 = Infinity;
266
+ let x1 = -Infinity;
267
+ let y0 = Infinity;
268
+ let y1 = -Infinity;
269
+ for (let i = 0; i < this.P.length; i += 2) {
270
+ const x = this.P[i];
271
+ const y = this.P[i + 1];
272
+ if (x < x0) x0 = x;
273
+ if (x > x1) x1 = x;
274
+ if (y < y0) y0 = y;
275
+ if (y > y1) y1 = y;
276
+ }
277
+ this.bb = [x0, y0, x1, y1];
278
+
279
+ const bins = 160;
280
+ const lo = new Float32Array(bins).fill(Infinity);
281
+ const hi = new Float32Array(bins).fill(-Infinity);
282
+ for (let i = 0; i < this.P.length; i += 2) {
283
+ const b = clamp(Math.round(((this.P[i] - x0) / (x1 - x0)) * (bins - 1)), 0, bins - 1);
284
+ lo[b] = Math.min(lo[b], this.P[i + 1]);
285
+ hi[b] = Math.max(hi[b], this.P[i + 1]);
286
+ }
287
+ for (let b = 1; b < bins; b += 1) {
288
+ if (!Number.isFinite(lo[b])) { lo[b] = lo[b - 1]; hi[b] = hi[b - 1]; }
289
+ }
290
+ this.bins = bins;
291
+ this.lo = lo;
292
+ this.hi = hi;
293
+ this.maxSpan = Math.max(...Array.from(hi, (v2, b) => v2 - lo[b]));
294
+ this.cells = v.n_cells;
295
+ this.off = document.createElement('canvas');
296
+ this.parts = Array.from({ length: opts.hero ? 280 : 120 }, () => this.spawn(true));
297
+ this.last = 0;
298
+ this.start();
299
+ }
300
+
301
+ spawn(scatter) {
302
+ return { f: scatter ? Math.random() : -Math.random() * 0.04, u: 0.05 + Math.random() * 0.9, v: 0.7 + Math.random() * 0.6, ph: Math.random() * 6.3 };
303
+ }
304
+
305
+ layout() {
306
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
307
+ const pad = this.hero ? 30 : 14;
308
+ const [x0, y0, x1, y1] = this.bb;
309
+ this.s = Math.min((this.w - 2 * pad) / (x1 - x0), (this.h - 2 * pad) / (y1 - y0));
310
+ this.ox = this.w / 2 - ((x0 + x1) / 2) * this.s;
311
+ this.oy = this.h / 2 + ((y0 + y1) / 2) * this.s;
312
+ const off = this.off;
313
+ off.width = Math.round(this.w * dpr);
314
+ off.height = Math.round(this.h * dpr);
315
+ const c = off.getContext('2d');
316
+ c.setTransform(dpr, 0, 0, dpr, 0, 0);
317
+ c.clearRect(0, 0, this.w, this.h);
318
+ const { P, Q } = this;
319
+ const X = (x) => this.ox + x * this.s;
320
+ const Y = (y) => this.oy - y * this.s;
321
+ c.lineWidth = 0.45;
322
+ c.strokeStyle = 'rgba(5,150,105,.5)';
323
+ c.beginPath();
324
+ for (let q = 0; q < Q.length; q += 4) {
325
+ c.moveTo(X(P[2 * Q[q]]), Y(P[2 * Q[q] + 1]));
326
+ for (let k = 1; k < 4; k += 1) c.lineTo(X(P[2 * Q[q + k]]), Y(P[2 * Q[q + k] + 1]));
327
+ c.closePath();
328
+ }
329
+ c.stroke();
330
+ c.lineWidth = this.hero ? 2.4 : 1.6;
331
+ for (const p of this.patches) {
332
+ c.strokeStyle = p.color;
333
+ c.beginPath();
334
+ for (let e = 0; e < p.E.length; e += 2) {
335
+ c.moveTo(X(P[2 * p.E[e]]), Y(P[2 * p.E[e] + 1]));
336
+ c.lineTo(X(P[2 * p.E[e + 1]]), Y(P[2 * p.E[e + 1] + 1]));
337
+ }
338
+ c.stroke();
339
+ }
340
+ }
341
+
342
+ at(p) {
343
+ const [x0, , x1] = this.bb;
344
+ const b = clamp(Math.round(clamp(p.f, 0, 1) * (this.bins - 1)), 0, this.bins - 1);
345
+ const span = this.hi[b] - this.lo[b];
346
+ const y = this.lo[b] + span * (0.04 + 0.92 * p.u);
347
+ return { x: this.ox + (x0 + clamp(p.f, 0, 1) * (x1 - x0)) * this.s, y: this.oy - y * this.s, span };
348
+ }
349
+
350
+ render(now) {
351
+ const { ctx, w, h } = this;
352
+ const t = this.elapsed(now);
353
+ const dt = clamp(now - (this.last || now), 0, 48);
354
+ this.last = now;
355
+ ctx.clearRect(0, 0, w, h);
356
+ const reveal = REDUCE ? 1 : easeOut(t / 1500);
357
+ ctx.save();
358
+ ctx.beginPath();
359
+ ctx.rect(0, 0, w * reveal, h);
360
+ ctx.clip();
361
+ ctx.drawImage(this.off, 0, 0, w, h);
362
+ ctx.restore();
363
+ this.caption(`${fmt(this.cells)} cells`);
364
+ if (reveal < 1) return;
365
+
366
+ ctx.lineCap = 'round';
367
+ ctx.lineWidth = this.hero ? 1.8 : 1.4;
368
+ for (const p of this.parts) {
369
+ const a = this.at(p);
370
+ const speed = 0.00007 * p.v * (this.maxSpan / Math.max(a.span, 1e-6)) ** 0.7;
371
+ p.f += speed * dt;
372
+ if (p.f > 1) Object.assign(p, this.spawn(false));
373
+ const b = this.at(p);
374
+ const wobble = Math.sin(t * 0.002 + p.ph) * 0.6;
375
+ const len = clamp((b.x - a.x) * 5, 2, 14);
376
+ ctx.strokeStyle = 'rgba(14,165,233,.75)';
377
+ ctx.beginPath();
378
+ ctx.moveTo(b.x - len, b.y + wobble);
379
+ ctx.lineTo(b.x, b.y + wobble);
380
+ ctx.stroke();
381
+ }
382
+ }
383
+ }
384
+
385
+ // KiCad: a design-rule scan sweeps the board and lights up each finding.
386
+ class DrcLive extends View {
387
+ constructor(box, v, opts) {
388
+ super(box, opts);
389
+ this.dots = v.dots;
390
+ let x0 = Infinity;
391
+ let x1 = -Infinity;
392
+ let y0 = Infinity;
393
+ let y1 = -Infinity;
394
+ for (const [x, y] of this.dots) {
395
+ x0 = Math.min(x0, x); x1 = Math.max(x1, x);
396
+ y0 = Math.min(y0, y); y1 = Math.max(y1, y);
397
+ }
398
+ const pad = 14;
399
+ this.bb = [x0 - pad, y0 - pad, x1 + pad, y1 + pad];
400
+ this.sweepMs = 3200;
401
+ this.cycle = 4600;
402
+ this.start();
403
+ }
404
+
405
+ layout() {
406
+ const margin = this.hero ? 26 : 12;
407
+ const [x0, y0, x1, y1] = this.bb;
408
+ this.s = Math.min((this.w - 2 * margin) / (x1 - x0), (this.h - 2 * margin) / (y1 - y0));
409
+ this.bw = (x1 - x0) * this.s;
410
+ this.bh = (y1 - y0) * this.s;
411
+ this.bx = (this.w - this.bw) / 2;
412
+ this.by = (this.h - this.bh) / 2;
413
+ }
414
+
415
+ render(now) {
416
+ const { ctx, w, h, s, bx, by, bw, bh } = this;
417
+ const [x0, y0] = this.bb;
418
+ const t = this.elapsed(now) % this.cycle;
419
+ const sweep = REDUCE ? 1.2 : t / this.sweepMs;
420
+ const beam = bx + sweep * bw;
421
+ ctx.clearRect(0, 0, w, h);
422
+
423
+ ctx.save();
424
+ ctx.beginPath();
425
+ if (ctx.roundRect) ctx.roundRect(bx, by, bw, bh, 8);
426
+ else ctx.rect(bx, by, bw, bh);
427
+ ctx.fillStyle = '#05271f';
428
+ ctx.fill();
429
+ ctx.strokeStyle = 'rgba(52,211,153,.5)';
430
+ ctx.lineWidth = 1;
431
+ ctx.stroke();
432
+ ctx.clip();
433
+ ctx.strokeStyle = 'rgba(94,234,212,.1)';
434
+ ctx.lineWidth = 0.6;
435
+ ctx.beginPath();
436
+ const step = 10 * s;
437
+ for (let gx = bx; gx < bx + bw; gx += step) { ctx.moveTo(gx, by); ctx.lineTo(gx, by + bh); }
438
+ for (let gy = by; gy < by + bh; gy += step) { ctx.moveTo(bx, gy); ctx.lineTo(bx + bw, gy); }
439
+ ctx.stroke();
440
+ if (sweep <= 1) {
441
+ const band = ctx.createLinearGradient(beam - 46, 0, beam + 6, 0);
442
+ band.addColorStop(0, 'rgba(45,212,191,0)');
443
+ band.addColorStop(1, 'rgba(45,212,191,.28)');
444
+ ctx.fillStyle = band;
445
+ ctx.fillRect(beam - 46, by, 52, bh);
446
+ ctx.fillStyle = 'rgba(153,246,228,.9)';
447
+ ctx.fillRect(beam, by, 1.5, bh);
448
+ }
449
+ ctx.restore();
450
+
451
+ let found = 0;
452
+ const r = this.hero ? 3.4 : 2.6;
453
+ for (const [x, y, g] of this.dots) {
454
+ const px = bx + (x - x0) * s;
455
+ const py = by + (y - y0) * s;
456
+ const color = DRC[g % DRC.length];
457
+ const passed = px <= beam;
458
+ if (passed) found += 1;
459
+ ctx.globalAlpha = passed ? 1 : 0.22;
460
+ ctx.fillStyle = color;
461
+ ctx.beginPath();
462
+ ctx.arc(px, py, r, 0, Math.PI * 2);
463
+ ctx.fill();
464
+ const since = ((beam - px) / bw) * this.sweepMs;
465
+ if (passed && since < 700 && sweep <= 1) {
466
+ const k = since / 700;
467
+ ctx.globalAlpha = 1 - k;
468
+ ctx.strokeStyle = color;
469
+ ctx.lineWidth = 1.4;
470
+ ctx.beginPath();
471
+ ctx.arc(px, py, r + 2 + k * 9, 0, Math.PI * 2);
472
+ ctx.stroke();
473
+ }
474
+ }
475
+ ctx.globalAlpha = 1;
476
+ this.caption(`DRC scan · ${found} findings`);
477
+ }
478
+ }
479
+
480
+ // Container: layers build bottom-up, then the attestation is written.
481
+ class ContainerLive extends View {
482
+ constructor(box, v, opts) {
483
+ super(box, { ...opts, canvas: false });
484
+ this.sc = v.scenarios;
485
+ const wrap = make('div', 'lv-container', box);
486
+ this.code = make('div', 'lv-code', wrap);
487
+ this.stack = make('div', 'lv-stack', wrap);
488
+ this.hash = make('div', 'lv-hash', wrap);
489
+ this.layers = [];
490
+ this.idx = -1;
491
+ this.cycle = 3800;
492
+ this.start();
493
+ }
494
+
495
+ render(now) {
496
+ const t = this.elapsed(now);
497
+ const idx = REDUCE ? 0 : Math.floor(t / this.cycle) % this.sc.length;
498
+ const lt = REDUCE ? this.cycle : t % this.cycle;
499
+ const sc = this.sc[idx];
500
+ if (idx !== this.idx) {
501
+ this.idx = idx;
502
+ this.code.textContent = `FROM ${sc.base} · ${sc.id}`;
503
+ this.layers = Array.from({ length: sc.layers }, (_, k) => {
504
+ const el = make('div', 'lv-layer', null, k === sc.layers - 1 ? `L1 · base ${sc.base}` : `L${sc.layers - k}`);
505
+ el.style.setProperty('--k', String(k));
506
+ return el;
507
+ });
508
+ this.stack.replaceChildren(...this.layers);
509
+ }
510
+ const n = this.layers.length;
511
+ this.layers.forEach((el, k) => el.classList.toggle('on', lt > 180 + (n - 1 - k) * 260));
512
+ const typeStart = 180 + n * 260 + 120;
513
+ const chars = clamp(Math.floor((lt - typeStart) / 42), 0, sc.attestation.length);
514
+ const cursor = chars > 0 && chars < sc.attestation.length ? '▍' : '';
515
+ const text = `attestation ${sc.attestation.slice(0, chars)}${cursor}`;
516
+ if (this.hash.textContent !== text) this.hash.textContent = text;
517
+ this.box.classList.toggle('lv-fade', !REDUCE && lt > this.cycle - 320);
518
+ }
519
+ }
520
+
521
+ // Blender: rotation dial sweeps out to each scenario's evaluated rotation.
522
+ class DialLive extends View {
523
+ constructor(box, v, opts) {
524
+ super(box, opts);
525
+ this.scenes = v.scenes;
526
+ this.cycle = 4800;
527
+ this.start();
528
+ }
529
+
530
+ render(now) {
531
+ const { ctx, w, h } = this;
532
+ const t = this.elapsed(now) % this.cycle;
533
+ ctx.clearRect(0, 0, w, h);
534
+ const cx = w / 2;
535
+ const cy = h / 2 - (this.hero ? 0 : 8);
536
+ const R = Math.min(w, h) * (this.hero ? 0.3 : 0.32);
537
+ ctx.fillStyle = 'rgba(255,255,255,.92)';
538
+ ctx.strokeStyle = 'rgba(100,116,139,.35)';
539
+ ctx.lineWidth = 1.2;
540
+ ctx.beginPath();
541
+ ctx.arc(cx, cy, R, 0, Math.PI * 2);
542
+ ctx.fill();
543
+ ctx.stroke();
544
+ ctx.strokeStyle = 'rgba(100,116,139,.45)';
545
+ for (let k = 0; k < 36; k += 1) {
546
+ const a = (k / 36) * Math.PI * 2;
547
+ const r1 = R - (k % 9 === 0 ? 10 : 5);
548
+ ctx.lineWidth = k % 9 === 0 ? 1.4 : 0.9;
549
+ ctx.beginPath();
550
+ ctx.moveTo(cx + R * Math.cos(a), cy - R * Math.sin(a));
551
+ ctx.lineTo(cx + r1 * Math.cos(a), cy - r1 * Math.sin(a));
552
+ ctx.stroke();
553
+ }
554
+ this.scenes.forEach((sc, i) => {
555
+ const local = t - i * 220;
556
+ let k;
557
+ if (REDUCE) k = 1;
558
+ else if (local < 1400) k = easeInOut(local / 1400);
559
+ else if (local < 3700) k = 1;
560
+ else k = 1 - easeInOut((local - 3700) / 800);
561
+ const a = sc.rotation_z * clamp(k, 0, 1);
562
+ const color = SC[i % SC.length];
563
+ ctx.strokeStyle = color;
564
+ ctx.lineCap = 'round';
565
+ ctx.lineWidth = 3;
566
+ ctx.globalAlpha = 0.85;
567
+ ctx.beginPath();
568
+ ctx.arc(cx, cy, R + 8 + i * 7, 0, -a, true);
569
+ ctx.stroke();
570
+ ctx.globalAlpha = 1;
571
+ const tip = R - 16;
572
+ ctx.lineWidth = 3.2;
573
+ ctx.beginPath();
574
+ ctx.moveTo(cx, cy);
575
+ ctx.lineTo(cx + tip * Math.cos(a), cy - tip * Math.sin(a));
576
+ ctx.stroke();
577
+ ctx.fillStyle = color;
578
+ ctx.beginPath();
579
+ ctx.arc(cx + tip * Math.cos(a), cy - tip * Math.sin(a), 5, 0, Math.PI * 2);
580
+ ctx.fill();
581
+ });
582
+ ctx.fillStyle = '#0f172a';
583
+ ctx.beginPath();
584
+ ctx.arc(cx, cy, 6, 0, Math.PI * 2);
585
+ ctx.fill();
586
+ this.caption(`rotation_z ${this.scenes.map((sc) => sc.rotation_z).join(' · ')} rad`);
587
+ }
588
+ }
589
+
590
+ // MuJoCo: joint-position trajectories draw in across every public scenario.
591
+ class LinesLive extends View {
592
+ constructor(box, v, opts) {
593
+ super(box, opts);
594
+ this.series = v.series;
595
+ const xs = this.series.flatMap((sr) => sr.step);
596
+ const ys = this.series.flatMap((sr) => sr.qpos);
597
+ this.x0 = Math.min(...xs);
598
+ this.x1 = Math.max(...xs);
599
+ let y0 = Math.min(...ys);
600
+ let y1 = Math.max(...ys);
601
+ const pad = (y1 - y0) * 0.1 || 1;
602
+ this.y0 = y0 - pad;
603
+ this.y1 = y1 + pad;
604
+ this.cycle = 5200;
605
+ this.start();
606
+ }
607
+
608
+ render(now) {
609
+ const { ctx, w, h } = this;
610
+ const t = this.elapsed(now) % this.cycle;
611
+ const p = REDUCE ? 1 : easeInOut(t / 2600);
612
+ const fade = REDUCE || t < this.cycle - 500 ? 1 : (this.cycle - t) / 500;
613
+ ctx.clearRect(0, 0, w, h);
614
+ const L = this.hero ? 40 : 22;
615
+ const Rm = this.hero ? 30 : 22;
616
+ const T = this.hero ? 30 : 22;
617
+ const B = this.hero ? 34 : 24;
618
+ const X = (v) => L + ((v - this.x0) / ((this.x1 - this.x0) || 1)) * (w - L - Rm);
619
+ const Y = (v) => T + (1 - (v - this.y0) / (this.y1 - this.y0)) * (h - T - B);
620
+ ctx.strokeStyle = 'rgba(148,163,184,.25)';
621
+ ctx.lineWidth = 1;
622
+ for (let k = 0; k <= 3; k += 1) {
623
+ const y = T + (k / 3) * (h - T - B);
624
+ ctx.beginPath();
625
+ ctx.moveTo(L, y);
626
+ ctx.lineTo(w - Rm, y);
627
+ ctx.stroke();
628
+ }
629
+ const cursorX = L + p * (w - L - Rm);
630
+ ctx.strokeStyle = 'rgba(15,23,42,.12)';
631
+ ctx.setLineDash([3, 4]);
632
+ ctx.beginPath();
633
+ ctx.moveTo(cursorX, T - 6);
634
+ ctx.lineTo(cursorX, h - B + 6);
635
+ ctx.stroke();
636
+ ctx.setLineDash([]);
637
+ ctx.globalAlpha = fade;
638
+ ctx.lineCap = 'round';
639
+ ctx.lineJoin = 'round';
640
+ this.series.forEach((sr, i) => {
641
+ const n = sr.step.length - 1;
642
+ const reach = p * n;
643
+ const color = SC[i % SC.length];
644
+ ctx.strokeStyle = color;
645
+ ctx.lineWidth = this.hero ? 2.8 : 2.2;
646
+ ctx.beginPath();
647
+ let hx = X(sr.step[0]);
648
+ let hy = Y(sr.qpos[0]);
649
+ ctx.moveTo(hx, hy);
650
+ for (let j = 1; j <= n; j += 1) {
651
+ if (j <= reach) {
652
+ hx = X(sr.step[j]);
653
+ hy = Y(sr.qpos[j]);
654
+ ctx.lineTo(hx, hy);
655
+ } else {
656
+ const f = reach - (j - 1);
657
+ if (f > 0) {
658
+ hx = X(sr.step[j - 1]) + (X(sr.step[j]) - X(sr.step[j - 1])) * f;
659
+ hy = Y(sr.qpos[j - 1]) + (Y(sr.qpos[j]) - Y(sr.qpos[j - 1])) * f;
660
+ ctx.lineTo(hx, hy);
661
+ }
662
+ break;
663
+ }
664
+ }
665
+ ctx.stroke();
666
+ ctx.fillStyle = color;
667
+ ctx.beginPath();
668
+ ctx.arc(hx, hy, this.hero ? 4.2 : 3.2, 0, Math.PI * 2);
669
+ ctx.fill();
670
+ });
671
+ ctx.globalAlpha = 1;
672
+ this.caption(`qpos over the trajectory · ${this.series.length} public scenarios`);
673
+ }
674
+ }
675
+
676
+ // Image tasks: public outputs cycle with a scan line or photometry aperture.
677
+ class ImagesLive extends View {
678
+ constructor(box, v, opts, mode) {
679
+ super(box, { ...opts, canvas: false });
680
+ this.mode = mode;
681
+ this.images = v.images;
682
+ this.wrap = make('div', `lv-images lv-${mode}`, box);
683
+ const slots = { micro: 3, astro: 4, ocr: 1, color: 6 }[mode];
684
+ this.slots = Array.from({ length: slots }, (_, j) => {
685
+ const cell = make('div', 'lv-cell', this.wrap);
686
+ const a = make('img', 'on', cell);
687
+ const b = make('img', '', cell);
688
+ [a, b].forEach((img) => { img.alt = ''; img.decoding = 'async'; });
689
+ const first = this.images[j % this.images.length];
690
+ a.src = first.src;
691
+ if (mode === 'astro') make('span', 'lv-aperture', cell);
692
+ if (mode === 'ocr' || mode === 'micro') make('span', 'lv-scan', cell);
693
+ if (mode === 'color') {
694
+ const sw = make('span', 'lv-swatch', cell);
695
+ sw.style.background = first.m;
696
+ }
697
+ return { cell, layers: [a, b], front: 0, idx: j % this.images.length };
698
+ });
699
+ this.period = mode === 'ocr' ? 3200 : 2600;
700
+ this.start();
701
+ }
702
+
703
+ show(slot, idx) {
704
+ if (slot.idx === idx) return;
705
+ slot.idx = idx;
706
+ const back = slot.layers[1 - slot.front];
707
+ back.src = this.images[idx].src;
708
+ back.classList.add('on');
709
+ slot.layers[slot.front].classList.remove('on');
710
+ slot.front = 1 - slot.front;
711
+ }
712
+
713
+ render(now) {
714
+ const t = this.elapsed(now);
715
+ const n = this.images.length;
716
+ if (this.mode === 'color') {
717
+ const hot = REDUCE ? 0 : Math.floor(t / 900) % n;
718
+ this.slots.forEach((slot, j) => slot.cell.classList.toggle('hot', j === hot));
719
+ const im = this.images[hot];
720
+ this.caption(`${im.id} · mean swatch ${im.m}`);
721
+ return;
722
+ }
723
+ this.slots.forEach((slot, j) => {
724
+ const step = REDUCE ? 0 : Math.floor((t + j * 450) / this.period);
725
+ this.show(slot, (j + step) % n);
726
+ });
727
+ const im = this.images[this.slots[0].idx];
728
+ if (this.mode === 'ocr') this.caption(`${im.id} · ${im.m} text lines`);
729
+ else if (this.mode === 'micro') this.caption(`${im.id} · ${im.m} objects segmented`);
730
+ else this.caption(`${im.id} · flux ${im.m.toFixed(1)}`);
731
+ }
732
+ }
733
+
734
+ const RENDERERS = {
735
+ stl: (box, v, o) => new MeshLive(box, v, o),
736
+ foam: (box, v, o) => new FoamLive(box, v, o),
737
+ drc: (box, v, o) => new DrcLive(box, v, o),
738
+ container: (box, v, o) => new ContainerLive(box, v, o),
739
+ blender: (box, v, o) => new DialLive(box, v, o),
740
+ robot: (box, v, o) => new LinesLive(box, v, o),
741
+ micro: (box, v, o) => new ImagesLive(box, v, o, 'micro'),
742
+ astro: (box, v, o) => new ImagesLive(box, v, o, 'astro'),
743
+ ocr: (box, v, o) => new ImagesLive(box, v, o, 'ocr'),
744
+ color: (box, v, o) => new ImagesLive(box, v, o, 'color'),
745
+ };
746
+
747
+ function mount(box, kind, opts = {}) {
748
+ const data = window.IL_VISUALS && window.IL_VISUALS[kind];
749
+ const build = RENDERERS[kind];
750
+ if (!data || !build) return null;
751
+ box.dataset.kind = kind;
752
+ try {
753
+ return build(box, data, opts);
754
+ } catch (err) {
755
+ box.replaceChildren();
756
+ console.error(`live preview failed for ${kind}`, err);
757
+ return null;
758
+ }
759
+ }
760
+
761
+ function mountCards() {
762
+ document.querySelectorAll('.ex-card[data-kind]').forEach((card) => {
763
+ const thumb = card.querySelector('.ex-thumb');
764
+ if (!thumb || thumb.querySelector('.ex-live')) return;
765
+ const box = make('div', 'ex-live', thumb);
766
+ box.setAttribute('aria-hidden', 'true');
767
+ const color = getComputedStyle(card).getPropertyValue('--dc').trim();
768
+ if (mount(box, card.dataset.kind, { color })) {
769
+ requestAnimationFrame(() => thumb.classList.add('is-live'));
770
+ } else {
771
+ box.remove();
772
+ }
773
+ });
774
+ }
775
+
776
+ function loadData() {
777
+ if (window.IL_VISUALS) return Promise.resolve();
778
+ return new Promise((resolve, reject) => {
779
+ const script = document.createElement('script');
780
+ script.src = 'assets/examples/visuals.js';
781
+ script.async = true;
782
+ script.onload = resolve;
783
+ script.onerror = reject;
784
+ document.head.appendChild(script);
785
+ });
786
+ }
787
+
788
+ const api = { ready: false, mount };
789
+ window.ILLive = api;
790
+
791
+ const section = document.getElementById('examples');
792
+ if (!section) return;
793
+ const go = () => loadData()
794
+ .then(() => {
795
+ api.ready = true;
796
+ mountCards();
797
+ document.dispatchEvent(new CustomEvent('il-live-ready'));
798
+ })
799
+ .catch(() => {});
800
+ if (!('IntersectionObserver' in window)) {
801
+ go();
802
+ } else {
803
+ const io = new IntersectionObserver((entries) => {
804
+ if (!entries.some((e) => e.isIntersecting)) return;
805
+ io.disconnect();
806
+ go();
807
+ }, { rootMargin: '900px 0px' });
808
+ io.observe(section);
809
+ }
810
+ })();
styles.css ADDED
@@ -0,0 +1,598 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #faf8f3;
3
+ --surface: #ffffff;
4
+ --tint: #f3efe6;
5
+ --tint-2: #f8f6f0;
6
+ --ink: #18202e;
7
+ --ink-2: #374151;
8
+ --muted: #6b7280;
9
+ --line: #e7e1d5;
10
+ --accent: #0f9f75;
11
+ --accent-deep: #047857;
12
+ --accent-soft: rgba(15, 159, 117, 0.12);
13
+ --mat: #4f46e5;
14
+ --mat-soft: rgba(79, 70, 229, 0.1);
15
+ --warm: #b45309;
16
+ --indigo: #6366f1;
17
+ --dark: #151b27;
18
+ --radius: 18px;
19
+ --shadow-sm: 0 1px 2px rgba(41, 37, 28, 0.05);
20
+ --shadow: 0 1px 2px rgba(41, 37, 28, 0.05), 0 14px 34px -14px rgba(41, 37, 28, 0.16);
21
+ --shadow-lg: 0 26px 60px -26px rgba(41, 37, 28, 0.35);
22
+ --font: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
23
+ --serif: "Fraunces", Georgia, "Times New Roman", serif;
24
+ --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
25
+ --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
26
+ }
27
+
28
+ *, *::before, *::after { box-sizing: border-box; }
29
+ html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
30
+ body {
31
+ margin: 0;
32
+ font-family: var(--font);
33
+ font-size: 16px;
34
+ line-height: 1.6;
35
+ color: var(--ink);
36
+ background: var(--bg);
37
+ -webkit-font-smoothing: antialiased;
38
+ text-rendering: optimizeLegibility;
39
+ }
40
+ body.menu-open { overflow: hidden; }
41
+ img, svg { display: block; max-width: 100%; }
42
+ a { color: inherit; text-decoration: none; }
43
+ h1, h2, h3, h4 { margin: 0; line-height: 1.22; }
44
+ p { margin: 0; }
45
+ .container { width: min(1200px, 100% - 48px); margin-inline: auto; }
46
+ section[id] { scroll-margin-top: 72px; }
47
+
48
+ .skip-link { position: absolute; left: 16px; top: -48px; z-index: 100; padding: 10px 14px; border-radius: 10px; background: var(--ink); color: #fff; font-weight: 600; }
49
+ .skip-link:focus { top: 12px; }
50
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }
51
+
52
+ /* Buttons */
53
+ .btn {
54
+ display: inline-flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ gap: 8px;
58
+ height: 46px;
59
+ padding: 0 22px;
60
+ border-radius: 999px;
61
+ border: 1px solid transparent;
62
+ font: 600 15px/1 var(--font);
63
+ letter-spacing: -0.005em;
64
+ cursor: pointer;
65
+ transition: transform 0.2s var(--ease), background-color 0.2s, box-shadow 0.2s, border-color 0.2s;
66
+ }
67
+ .btn svg { width: 18px; height: 18px; transition: transform 0.2s var(--ease); }
68
+ .btn:hover svg { transform: translateX(2px); }
69
+ .btn:hover { transform: translateY(-1px); }
70
+ .btn-sm { height: 38px; padding: 0 16px; font-size: 14px; }
71
+ .btn-dark { background: var(--ink); color: #fff; box-shadow: 0 8px 20px -10px rgba(24, 32, 46, 0.6); }
72
+ .btn-dark:hover { background: #26303f; }
73
+ .btn-ghost { background: rgba(255, 255, 255, 0.8); border-color: var(--line); color: var(--ink); }
74
+ .btn-ghost:hover { border-color: #d4cbb9; background: #fff; }
75
+
76
+ /* Navigation */
77
+ .nav {
78
+ position: sticky;
79
+ top: 0;
80
+ z-index: 50;
81
+ background: rgba(250, 248, 243, 0.82);
82
+ -webkit-backdrop-filter: saturate(1.4) blur(14px);
83
+ backdrop-filter: saturate(1.4) blur(14px);
84
+ border-bottom: 1px solid transparent;
85
+ transition: border-color 0.25s, box-shadow 0.25s;
86
+ }
87
+ .nav.is-scrolled { border-bottom-color: var(--line); box-shadow: 0 8px 24px -18px rgba(41, 37, 28, 0.35); }
88
+ .nav-inner { display: flex; align-items: center; justify-content: space-between; height: 66px; gap: 24px; }
89
+ .brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 700; font-size: 17px; letter-spacing: -0.02em; white-space: nowrap; }
90
+ .brand em { margin-left: 4px; padding-left: 10px; border-left: 1px solid var(--line); font-style: normal; font-weight: 500; color: var(--muted); }
91
+ .brand-mark { width: 30px; height: 30px; flex: none; }
92
+ .nav-links { display: flex; gap: 28px; font-size: 14.5px; font-weight: 500; color: var(--ink-2); }
93
+ .nav-links a { position: relative; padding: 6px 0; transition: color 0.2s; }
94
+ .nav-links a::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; border-radius: 2px; background: var(--accent); transform: scaleX(0); transition: transform 0.25s var(--ease); }
95
+ .nav-links a:hover, .nav-links a.is-active { color: var(--ink); }
96
+ .nav-links a.is-active::after { transform: scaleX(1); }
97
+ .nav-cta { display: flex; align-items: center; gap: 10px; }
98
+ .nav-toggle { display: none; width: 40px; height: 40px; border-radius: 12px; border: 1px solid var(--line); background: #fff; cursor: pointer; flex-direction: column; align-items: center; justify-content: center; gap: 5px; }
99
+ .nav-toggle span { width: 16px; height: 2px; border-radius: 2px; background: var(--ink); transition: transform 0.25s var(--ease); }
100
+ .nav-toggle[aria-expanded="true"] span:first-child { transform: translateY(3.5px) rotate(45deg); }
101
+ .nav-toggle[aria-expanded="true"] span:last-child { transform: translateY(-3.5px) rotate(-45deg); }
102
+ .mobile-menu { display: grid; gap: 4px; padding: 12px 24px 24px; border-top: 1px solid var(--line); background: var(--bg); }
103
+ .mobile-menu[hidden] { display: none; }
104
+ .mobile-menu a:not(.btn) { padding: 12px 4px; font-size: 17px; font-weight: 600; border-bottom: 1px solid var(--line); }
105
+ .mobile-menu .btn { margin-top: 14px; }
106
+
107
+ /* Hero */
108
+ .hero { position: relative; isolation: isolate; overflow: hidden; padding: clamp(48px, 7vw, 88px) 0 clamp(64px, 8vw, 104px); }
109
+ .hero::before {
110
+ content: "";
111
+ position: absolute;
112
+ inset: 0;
113
+ z-index: -2;
114
+ background-image: radial-gradient(rgba(24, 32, 46, 0.09) 1px, transparent 1.4px);
115
+ background-size: 22px 22px;
116
+ -webkit-mask-image: radial-gradient(70% 70% at 70% 45%, #000 20%, transparent 100%);
117
+ mask-image: radial-gradient(70% 70% at 70% 45%, #000 20%, transparent 100%);
118
+ }
119
+ .hero::after {
120
+ content: "";
121
+ position: absolute;
122
+ inset: 0;
123
+ z-index: -1;
124
+ background:
125
+ radial-gradient(36% 46% at 12% 26%, rgba(15, 159, 117, 0.13), transparent 70%),
126
+ radial-gradient(32% 42% at 86% 40%, rgba(79, 70, 229, 0.11), transparent 70%),
127
+ radial-gradient(30% 30% at 50% 100%, rgba(217, 119, 6, 0.08), transparent 70%);
128
+ }
129
+ .hero-grid { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); gap: 40px; align-items: center; }
130
+ .eyebrow {
131
+ display: inline-flex;
132
+ align-items: center;
133
+ gap: 10px;
134
+ padding: 7px 14px 7px 10px;
135
+ border-radius: 999px;
136
+ background: rgba(255, 255, 255, 0.8);
137
+ border: 1px solid var(--line);
138
+ font-size: 13px;
139
+ font-weight: 600;
140
+ color: var(--accent-deep);
141
+ box-shadow: var(--shadow-sm);
142
+ }
143
+ .pulse-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: ping 2.2s infinite; }
144
+ @keyframes ping {
145
+ 0% { box-shadow: 0 0 0 0 rgba(15, 159, 117, 0.45); }
146
+ 70% { box-shadow: 0 0 0 9px rgba(15, 159, 117, 0); }
147
+ 100% { box-shadow: 0 0 0 0 rgba(15, 159, 117, 0); }
148
+ }
149
+ .hero h1 { margin: 22px 0 22px; font-family: var(--serif); font-size: clamp(40px, 5.3vw, 68px); line-height: 1.04; font-weight: 600; letter-spacing: -0.025em; }
150
+ .hero h1 em { font-style: italic; font-weight: 500; color: var(--accent-deep); }
151
+ .lede { max-width: 590px; font-size: clamp(17px, 1.4vw, 19px); line-height: 1.65; color: var(--ink-2); }
152
+ .hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 30px; }
153
+ .hero-points { display: flex; flex-wrap: wrap; gap: 10px; margin: 28px 0 0; padding: 0; list-style: none; }
154
+ .hero-points li { padding: 7px 14px; border-radius: 999px; border: 1px solid var(--line); background: rgba(255, 255, 255, 0.85); font-size: 14px; font-weight: 500; color: var(--ink-2); }
155
+ .hero-points li::before { content: "✓"; margin-right: 7px; color: var(--accent); font-weight: 800; }
156
+ .hero-visual { position: relative; width: 100%; max-width: 560px; margin: 0; justify-self: center; aspect-ratio: 1; }
157
+ .hero-visual::before { content: ""; position: absolute; inset: 7%; border-radius: 50%; background: radial-gradient(circle at 50% 44%, rgba(255, 255, 255, 0.95), rgba(15, 159, 117, 0.08) 58%, transparent 72%); }
158
+ .morph-canvas { position: relative; display: block; width: 100%; height: 100%; }
159
+ .morph-labels { position: absolute; left: 0; right: 0; bottom: 3%; height: 34px; }
160
+ .ml { position: absolute; left: 50%; padding: 6px 14px; border-radius: 999px; border: 1px solid var(--line); background: rgba(255, 255, 255, 0.92); font-size: 13px; font-weight: 600; color: var(--ink-2); white-space: nowrap; box-shadow: var(--shadow-sm); opacity: 0; transform: translate(-50%, 6px); transition: opacity 0.5s, transform 0.5s var(--ease); }
161
+ .ml.is-on { opacity: 1; transform: translate(-50%, 0); }
162
+
163
+ /* Team strip */
164
+ .team-strip { padding: 26px 0 28px; border-block: 1px solid var(--line); background: rgba(255, 255, 255, 0.6); text-align: center; }
165
+ .team-title { font-size: 13px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
166
+ .team-list { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 34px; margin: 12px 0 0; padding: 0; list-style: none; }
167
+ .team-list li { font-family: var(--serif); font-size: 19px; font-weight: 500; color: var(--ink-2); }
168
+ .team-more { margin-top: 8px; font-size: 13.5px; color: var(--muted); }
169
+
170
+ /* Sections */
171
+ .section { padding: clamp(80px, 10vw, 124px) 0; }
172
+ .section-tint { background: var(--tint); }
173
+ .section-head { max-width: 780px; margin-bottom: 48px; }
174
+ .kicker { margin-bottom: 14px; font-size: 13px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent-deep); }
175
+ .section h2 { font-family: var(--serif); font-size: clamp(32px, 3.7vw, 48px); line-height: 1.1; font-weight: 600; letter-spacing: -0.02em; }
176
+ .sub { max-width: 700px; margin-top: 18px; font-size: 17.5px; color: var(--ink-2); }
177
+
178
+ /* Why */
179
+ .why-grid { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); gap: 56px; align-items: center; }
180
+ .stat-stack { display: grid; gap: 14px; }
181
+ .stat { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 18px; align-items: center; padding: 20px 22px; border-radius: 18px; border: 1px solid var(--line); background: var(--surface); box-shadow: var(--shadow-sm); }
182
+ .stat strong { font-family: var(--serif); font-size: 40px; font-weight: 600; line-height: 1; letter-spacing: -0.02em; color: var(--accent-deep); white-space: nowrap; }
183
+ .stat-warm strong { color: var(--warm); }
184
+ .stat-cool strong { color: var(--mat); }
185
+ .stat span { font-size: 14.5px; color: var(--ink-2); }
186
+
187
+ /* Who */
188
+ .field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; }
189
+ .field-card { position: relative; overflow: hidden; padding: 30px; border-radius: 22px; border: 1px solid var(--line); background: var(--surface); box-shadow: var(--shadow-sm); transition: transform 0.3s var(--ease), box-shadow 0.3s; }
190
+ .field-card:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
191
+ .field-card::after { content: ""; position: absolute; right: -80px; top: -80px; width: 220px; height: 220px; border-radius: 50%; background: radial-gradient(circle, rgba(15, 159, 117, 0.14), transparent 70%); }
192
+ .field-mat::after { background: radial-gradient(circle, rgba(79, 70, 229, 0.13), transparent 70%); }
193
+ .field-icon { display: grid; place-items: center; width: 58px; height: 58px; border-radius: 16px; background: var(--accent-soft); color: var(--accent-deep); }
194
+ .field-mat .field-icon { background: var(--mat-soft); color: var(--mat); }
195
+ .field-icon svg { width: 38px; height: 38px; }
196
+ .field-card h3 { margin: 18px 0 8px; font-family: var(--serif); font-size: 28px; font-weight: 600; letter-spacing: -0.015em; }
197
+ .field-card > p { font-size: 15.5px; color: var(--ink-2); }
198
+ .field-card .tools-label { margin-top: 20px; font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
199
+ .tool-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; padding: 0; list-style: none; }
200
+ .tool-chips li { padding: 4px 10px; border-radius: 8px; border: 1px solid var(--line); background: var(--tint-2); font: 500 12px var(--mono); color: var(--ink-2); }
201
+ .who-note { margin-top: 24px; text-align: center; font-size: 15px; color: var(--muted); }
202
+
203
+ /* Research explorer */
204
+ .explorer { padding: 26px; border-radius: 24px; border: 1px solid var(--line); background: var(--surface); box-shadow: var(--shadow); }
205
+ .explorer-top { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px; }
206
+ .field-toggle { display: inline-flex; padding: 4px; border-radius: 999px; border: 1px solid var(--line); background: var(--tint); }
207
+ .ft-btn { height: 38px; padding: 0 18px; border: 0; border-radius: 999px; background: transparent; font: 600 14px var(--font); color: var(--ink-2); cursor: pointer; transition: background-color 0.2s, color 0.2s, box-shadow 0.2s; }
208
+ .ft-btn[aria-pressed="true"] { background: #fff; color: var(--accent-deep); box-shadow: var(--shadow-sm); }
209
+ .explorer[data-field="mat"] .ft-btn[aria-pressed="true"] { color: var(--mat); }
210
+ .explorer-hint { font-size: 14px; color: var(--muted); }
211
+ html:not(.js) .explorer-top { display: none; }
212
+ .explorer-panel { margin-top: 22px; }
213
+ .explorer-panel[hidden] { display: none; }
214
+ .panel-title { margin-bottom: 14px; font-family: var(--serif); font-size: 22px; font-weight: 600; }
215
+ .is-enhanced .panel-title { display: none; }
216
+ .stage + .stage { margin-top: 18px; }
217
+ .is-enhanced .stage + .stage { margin-top: 0; }
218
+ .stage[hidden] { display: none; }
219
+ .stage-title { margin-bottom: 10px; font-size: 16px; }
220
+ .is-enhanced .stage-title { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
221
+ .stage-tabs { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; margin-bottom: 16px; }
222
+ .stage-btn { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 56px; padding: 8px 12px; border-radius: 14px; border: 1px solid var(--line); background: var(--tint-2); font: 600 13.5px/1.25 var(--font); color: var(--ink-2); text-align: left; cursor: pointer; transition: background-color 0.2s, border-color 0.2s, color 0.2s; }
223
+ .stage-btn:hover { border-color: #d4cbb9; }
224
+ .sb-num { display: grid; flex: none; place-items: center; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--line); background: #fff; font: 700 12px var(--mono); color: var(--muted); }
225
+ .stage-btn[aria-pressed="true"] { border-color: var(--ink); background: var(--ink); color: #fff; }
226
+ .stage-btn[aria-pressed="true"] .sb-num { border-color: var(--accent); background: var(--accent); color: #fff; }
227
+ .explorer[data-field="mat"] .stage-btn[aria-pressed="true"] .sb-num { border-color: var(--mat); background: var(--mat); }
228
+ .stage-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: 0; }
229
+ .sg { padding: 18px 18px 20px; border-radius: 16px; border: 1px solid var(--line); background: var(--tint-2); }
230
+ .sg dt { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-2); }
231
+ .sg dt::before { content: ""; width: 9px; height: 9px; border-radius: 50%; background: #94a3b8; }
232
+ .sg-gap dt { color: var(--warm); }
233
+ .sg-gap dt::before { background: #f59e0b; }
234
+ .sg-build { border-color: color-mix(in srgb, var(--accent) 26%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, #fff); }
235
+ .sg-build dt { color: var(--accent-deep); }
236
+ .sg-build dt::before { background: var(--accent); }
237
+ .explorer[data-field="mat"] .sg-build { border-color: color-mix(in srgb, var(--mat) 22%, var(--line)); background: color-mix(in srgb, var(--mat) 6%, #fff); }
238
+ .explorer[data-field="mat"] .sg-build dt { color: var(--mat); }
239
+ .explorer[data-field="mat"] .sg-build dt::before { background: var(--mat); }
240
+ .sg dd { margin: 10px 0 0; font-size: 15px; color: var(--ink-2); }
241
+ .is-enhanced .stage:not([hidden]) .sg { animation: sg-in 0.4s var(--ease) both; }
242
+ .is-enhanced .stage:not([hidden]) .sg:nth-child(2) { animation-delay: 0.05s; }
243
+ .is-enhanced .stage:not([hidden]) .sg:nth-child(3) { animation-delay: 0.1s; }
244
+ @keyframes sg-in { from { opacity: 0; transform: translateY(8px); } }
245
+
246
+ .questions { display: grid; grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.25fr); gap: 36px; margin-top: 40px; padding: 30px; border-radius: 24px; background: var(--tint); }
247
+ .questions-head h3 { font-family: var(--serif); font-size: 26px; font-weight: 600; }
248
+ .questions-head p { margin-top: 10px; color: var(--muted); }
249
+ .question-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 0; padding: 0; list-style: none; counter-reset: q; }
250
+ .question-list li { position: relative; padding: 14px 16px 14px 48px; border-radius: 14px; border: 1px solid var(--line); background: #fff; font-size: 15px; counter-increment: q; }
251
+ .question-list li::before { content: counter(q); position: absolute; left: 14px; top: 13px; display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: var(--accent-soft); color: var(--accent-deep); font: 700 11.5px/1 var(--mono); }
252
+
253
+ /* Demos */
254
+ .ex-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; }
255
+ .ex-card {
256
+ position: relative;
257
+ display: flex;
258
+ flex-direction: column;
259
+ overflow: hidden;
260
+ border-radius: 20px;
261
+ border: 1px solid var(--line);
262
+ background: var(--surface);
263
+ box-shadow: var(--shadow-sm);
264
+ cursor: pointer;
265
+ transition: transform 0.3s var(--ease), box-shadow 0.3s, border-color 0.3s;
266
+ }
267
+ .ex-card:hover, .ex-card:focus-visible { transform: translateY(-4px); box-shadow: var(--shadow); border-color: color-mix(in srgb, var(--dc) 35%, var(--line)); }
268
+ .ex-card:focus-visible { outline: 2px solid var(--dc); outline-offset: 3px; }
269
+ .ex-card.is-extra { display: none; }
270
+ .ex-grid.show-all .ex-card.is-extra { display: flex; }
271
+ .ex-thumb { position: relative; aspect-ratio: 740 / 394; overflow: hidden; border-bottom: 1px solid var(--line); background: #f7f8fb; }
272
+ .ex-thumb > img { width: 100%; height: 100%; object-fit: cover; transition: opacity 0.5s ease, transform 0.6s var(--ease); }
273
+ .ex-card:hover .ex-thumb > img { transform: scale(1.04); }
274
+ .ex-card-body { display: flex; flex: 1; flex-direction: column; padding: 18px 20px 20px; }
275
+ .ex-card-domain { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; color: color-mix(in srgb, var(--dc) 80%, #0b1220); }
276
+ .ex-card-domain::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--dc); }
277
+ .ex-card h3 { margin: 8px 0 6px; font-size: 18px; letter-spacing: -0.02em; }
278
+ .ex-card-body > p:not(.ex-card-domain) { font-size: 14px; color: var(--muted); }
279
+ .ex-card-tracks { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0 0; padding: 0; list-style: none; }
280
+ .trk { padding: 4px 10px; border-radius: 999px; font-size: 11.5px; font-weight: 700; }
281
+ .trk-rel { background: var(--accent-soft); color: var(--accent-deep); }
282
+ .trk-rel + .trk-rel { border: 1px solid var(--line); background: var(--tint-2); color: var(--ink-2); font-weight: 600; }
283
+ .trk-mod { border: 1px solid var(--line); color: var(--ink-2); font-weight: 600; }
284
+ .ex-sw { display: flex; flex-wrap: wrap; gap: 6px; margin: auto 0 0; padding: 14px 0 0; list-style: none; }
285
+ .ex-sw li { padding: 3px 8px; border-radius: 6px; border: 1px solid var(--line); background: var(--tint-2); font: 500 11px var(--mono); color: var(--ink-2); }
286
+ .ex-open { position: absolute; top: 12px; right: 12px; padding: 6px 11px; border-radius: 999px; background: rgba(24, 32, 46, 0.86); color: #fff; font-size: 12px; font-weight: 700; opacity: 0; transform: translateY(-4px); transition: opacity 0.25s, transform 0.25s var(--ease); }
287
+ .ex-card:hover .ex-open, .ex-card:focus-visible .ex-open { opacity: 1; transform: none; }
288
+ .ex-more { display: flex; justify-content: center; margin-top: 30px; }
289
+ .ex-grid.show-all + .ex-more { display: none; }
290
+
291
+ .ex-modal { position: fixed; inset: 0; z-index: 80; display: flex; align-items: center; justify-content: center; padding: 24px; }
292
+ .ex-modal[hidden] { display: none; }
293
+ .ex-backdrop { position: absolute; inset: 0; background: rgba(20, 18, 14, 0.5); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); animation: fade-in 0.25s ease; }
294
+ .ex-sheet {
295
+ position: relative;
296
+ width: min(1080px, 100%);
297
+ max-height: min(92vh, 980px);
298
+ overflow: auto;
299
+ border-radius: 24px;
300
+ background: var(--surface);
301
+ box-shadow: 0 40px 120px -30px rgba(0, 0, 0, 0.55);
302
+ animation: sheet-in 0.35s var(--ease);
303
+ }
304
+ @keyframes fade-in { from { opacity: 0; } }
305
+ @keyframes sheet-in { from { opacity: 0; transform: translateY(14px) scale(0.985); } }
306
+ .ex-head { position: sticky; top: 0; z-index: 1; display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; padding: 22px 26px 18px; border-bottom: 1px solid var(--line); background: rgba(255, 255, 255, 0.94); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
307
+ .ex-domain { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 700; color: color-mix(in srgb, var(--dc, #0f9f75) 80%, #0b1220); }
308
+ .ex-domain::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--dc, #0f9f75); }
309
+ .ex-head h3 { margin: 6px 0 10px; font-size: 24px; letter-spacing: -0.025em; }
310
+ .ex-tracks { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
311
+ .ex-nav { display: flex; flex: none; gap: 8px; }
312
+ .icon-btn { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 12px; border: 1px solid var(--line); background: #fff; font: 600 15px/1 var(--font); color: var(--ink); cursor: pointer; transition: background-color 0.2s, border-color 0.2s; }
313
+ .icon-btn:hover { border-color: #d4cbb9; background: var(--tint-2); }
314
+ .ex-body { padding: 22px 26px 28px; }
315
+ .ex-figure { margin: 0; overflow: hidden; border-radius: 16px; border: 1px solid var(--line); background: #f7f8fb; }
316
+ .ex-figure img { width: 100%; height: auto; }
317
+ .ex-desc { margin-top: 18px; font-size: 16.5px; color: var(--ink-2); }
318
+ .ex-why { margin-top: 12px; padding: 12px 16px; border-left: 3px solid var(--accent); border-radius: 0 12px 12px 0; background: var(--accent-soft); font-size: 15px; color: var(--ink-2); }
319
+ .ex-why strong { color: var(--accent-deep); }
320
+ .ex-facts { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 18px; }
321
+ .ex-facts > div { padding: 14px; border-radius: 14px; border: 1px solid var(--line); background: var(--tint-2); }
322
+ .ex-facts h4 { margin: 0 0 10px; font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent-deep); }
323
+ .ex-facts ul { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
324
+ .ex-facts li { padding: 4px 9px; border-radius: 7px; border: 1px solid var(--line); background: #fff; font: 500 11.5px var(--mono); color: var(--ink-2); }
325
+ .ex-how { margin: 18px 0 20px; padding-left: 14px; border-left: 3px solid var(--line); font-size: 15px; color: var(--muted); }
326
+ body.modal-open { overflow: hidden; }
327
+
328
+ /* Live demo previews */
329
+ .ex-thumb.is-live > img { opacity: 0; }
330
+ .ex-live, .ex-live-hero { overflow: hidden; background: radial-gradient(120% 100% at 50% 0%, color-mix(in srgb, var(--dc, #0f9f75) 13%, #fff) 0%, #fbfcfd 70%); }
331
+ .ex-live { position: absolute; inset: 0; opacity: 0; transition: opacity 0.5s ease; }
332
+ .ex-thumb.is-live .ex-live { opacity: 1; }
333
+ .ex-live-hero { position: relative; aspect-ratio: 16 / 7; border-bottom: 1px solid var(--line); }
334
+ .ex-live-hero[hidden] { display: none; }
335
+ .ex-evidence-label { padding: 12px 16px 0; font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent-deep); }
336
+ .ex-live-hero[hidden] + .ex-evidence-label { display: none; }
337
+ .lv-canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
338
+ .lv-cap { position: absolute; left: 14px; bottom: 10px; z-index: 2; font: 500 11px var(--mono); color: var(--muted); }
339
+ .ex-live-hero .lv-cap { left: 22px; bottom: 16px; font-size: 12px; }
340
+ [data-kind="drc"] .lv-cap { left: 24px; bottom: 20px; color: #a7f3d0; }
341
+ .ex-live-hero[data-kind="drc"] .lv-cap { left: 40px; bottom: 36px; }
342
+ .lv-container { position: absolute; inset: 0; display: grid; align-content: center; gap: 6px; width: 74%; margin: auto; transition: opacity 0.3s; }
343
+ .lv-fade .lv-container { opacity: 0; }
344
+ .lv-code { margin-bottom: 4px; font: 600 12px var(--mono); color: #0d9488; }
345
+ .lv-stack { display: grid; gap: 6px; }
346
+ .lv-layer {
347
+ display: flex;
348
+ align-items: center;
349
+ height: 24px;
350
+ margin-left: calc(var(--k) * 16px);
351
+ padding: 0 10px;
352
+ border-radius: 7px;
353
+ border: 1px solid rgba(37, 99, 235, 0.25);
354
+ background: linear-gradient(90deg, rgba(37, 99, 235, 0.16), rgba(37, 99, 235, 0.05));
355
+ font: 500 11px var(--mono);
356
+ color: var(--ink-2);
357
+ opacity: 0;
358
+ transform: translateY(-10px);
359
+ transition: opacity 0.35s, transform 0.35s var(--ease);
360
+ }
361
+ .lv-layer.on { opacity: 1; transform: none; }
362
+ .lv-hash { min-height: 1.6em; margin-top: 6px; font: 500 11px var(--mono); color: var(--muted); }
363
+ .ex-live-hero .lv-container { width: 54%; gap: 8px; }
364
+ .ex-live-hero .lv-layer { height: 34px; margin-left: calc(var(--k) * 26px); font-size: 13px; }
365
+ .ex-live-hero .lv-code, .ex-live-hero .lv-hash { font-size: 13.5px; }
366
+ .lv-images { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 12px; padding: 18px; }
367
+ .lv-cell { position: relative; overflow: hidden; border-radius: 12px; background: #e9eef2; box-shadow: 0 12px 24px -12px rgba(15, 23, 42, 0.4); }
368
+ .lv-cell img { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; opacity: 0; transition: opacity 0.6s ease; }
369
+ .lv-cell img.on { opacity: 1; }
370
+ .lv-micro .lv-cell { width: 25%; aspect-ratio: 1; }
371
+ .lv-astro .lv-cell { width: 18.5%; aspect-ratio: 1; border-radius: 8px; box-shadow: 0 0 30px -6px rgba(249, 140, 10, 0.5); }
372
+ .lv-ocr .lv-cell { width: 84%; aspect-ratio: 237 / 82; border-radius: 8px; }
373
+ .lv-color { display: grid; grid-template-columns: repeat(3, auto); gap: 8px; align-content: center; justify-content: center; }
374
+ .lv-color .lv-cell { width: 62px; height: 62px; border-radius: 10px; transition: transform 0.35s var(--ease), box-shadow 0.35s; }
375
+ .lv-color .lv-cell.hot { transform: translateY(-4px) scale(1.08); box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--dc, #7c3aed), 0 14px 26px -12px rgba(15, 23, 42, 0.5); }
376
+ .ex-live-hero .lv-color .lv-cell { width: 92px; height: 92px; }
377
+ .lv-swatch { position: absolute; right: 5px; bottom: 5px; z-index: 1; width: 12px; height: 12px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); }
378
+ .lv-scan { position: absolute; left: 0; right: 0; top: 0; z-index: 1; height: 2px; background: var(--dc, #0f9f75); box-shadow: 0 0 14px 2px var(--dc, #0f9f75); animation: lv-scan 2.6s ease-in-out infinite; }
379
+ @keyframes lv-scan { 0%, 100% { top: 0; } 50% { top: calc(100% - 2px); } }
380
+ .lv-aperture { position: absolute; left: 50%; top: 50%; z-index: 1; width: 40%; aspect-ratio: 1; border: 1.5px solid rgba(255, 255, 255, 0.92); border-radius: 50%; transform: translate(-50%, -50%); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25); animation: lv-aperture 2.6s ease-in-out infinite; }
381
+ .lv-aperture::before, .lv-aperture::after { content: ""; position: absolute; left: 50%; top: 50%; background: rgba(255, 255, 255, 0.85); transform: translate(-50%, -50%); }
382
+ .lv-aperture::before { width: 140%; height: 1px; }
383
+ .lv-aperture::after { width: 1px; height: 140%; }
384
+ @keyframes lv-aperture { 0%, 100% { width: 30%; } 50% { width: 58%; } }
385
+
386
+ /* Research computing replays */
387
+ .chores { margin-top: 72px; }
388
+ .chores-head { max-width: 720px; margin-bottom: 22px; }
389
+ .chores-head h3 { font-family: var(--serif); font-size: 28px; font-weight: 600; letter-spacing: -0.015em; }
390
+ .chores-head p { margin-top: 8px; font-size: 16px; color: var(--ink-2); }
391
+ .tx-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px; }
392
+ .tx-card {
393
+ position: relative;
394
+ display: flex;
395
+ flex-direction: column;
396
+ overflow: hidden;
397
+ border-radius: 20px;
398
+ border: 1px solid var(--line);
399
+ background: var(--surface);
400
+ box-shadow: var(--shadow-sm);
401
+ cursor: pointer;
402
+ transition: transform 0.3s var(--ease), box-shadow 0.3s, border-color 0.3s;
403
+ }
404
+ .tx-card:hover, .tx-card:focus-visible { transform: translateY(-4px); box-shadow: var(--shadow); border-color: rgba(15, 159, 117, 0.4); }
405
+ .tx-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
406
+ .tx-card .ex-open { background: #fff; color: var(--ink); box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.5); }
407
+ .tx-card:hover .ex-open, .tx-card:focus-visible .ex-open { opacity: 1; transform: none; }
408
+ .tx-term { overflow: hidden; background: #0b1220; }
409
+ .tx-card .tx-term { border-bottom: 1px solid #1e293b; }
410
+ .tx-bar { display: flex; align-items: center; gap: 6px; padding: 10px 14px; border-bottom: 1px solid #1e293b; background: #0f172a; }
411
+ .tx-bar i { flex: none; width: 9px; height: 9px; border-radius: 50%; background: #334155; }
412
+ .tx-bar i:nth-child(1) { background: #f26b5b; }
413
+ .tx-bar i:nth-child(2) { background: #f5b84b; }
414
+ .tx-bar i:nth-child(3) { background: #34d399; }
415
+ .tx-bar em { min-width: 0; margin-left: 8px; overflow: hidden; font: 500 11px var(--mono); font-style: normal; color: #8b9ab0; white-space: nowrap; text-overflow: ellipsis; }
416
+ .tx-screen { height: 200px; margin: 0; padding: 12px 16px; overflow: hidden; font: 11.5px/1.65 var(--mono); color: #e2e8f0; }
417
+ .tx-screen code { white-space: normal; }
418
+ .tx-screen span { display: block; white-space: pre-wrap; overflow-wrap: anywhere; }
419
+ .tx-screen b { color: #34d399; font-weight: 600; }
420
+ .tx-screen .tx-sys { margin-top: 6px; color: #a5b4fc; }
421
+ .tx-screen .tx-ok { color: #34d399; font-weight: 600; }
422
+ .tx-caret { display: inline-block; width: 7px; height: 1.15em; margin-left: 2px; vertical-align: -3px; background: #34d399; animation: tx-caret 1s steps(1) infinite; }
423
+ @keyframes tx-caret { 50% { opacity: 0; } }
424
+ .tx-body { display: flex; flex: 1; flex-direction: column; padding: 18px 20px 20px; }
425
+ .tx-area { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; color: var(--accent-deep); }
426
+ .tx-area::before { content: ""; width: 8px; height: 8px; border-radius: 2px; background: var(--accent); }
427
+ .tx-card h3 { margin: 8px 0 6px; font-size: 18px; letter-spacing: -0.02em; }
428
+ .tx-body > p:not(.tx-area) { font-size: 14px; color: var(--muted); overflow-wrap: anywhere; }
429
+ .tx-body code, .txm-instruction code { padding: 1px 5px; border-radius: 5px; border: 1px solid var(--line); background: #fff; font: 500 0.86em var(--mono); color: var(--ink-2); overflow-wrap: anywhere; }
430
+ .tx-meta { display: flex; flex-wrap: wrap; gap: 6px; margin: auto 0 0; padding: 14px 0 0; list-style: none; }
431
+ .tx-meta li { padding: 3px 8px; border-radius: 6px; border: 1px solid var(--line); background: var(--tint-2); font: 500 11px var(--mono); color: var(--ink-2); }
432
+ .txm-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
433
+ .txm-h { margin: 0 0 10px; font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent-deep); }
434
+ .txm-h code { font: inherit; text-transform: none; letter-spacing: 0; }
435
+ .txm-col .tx-term + .txm-h { margin-top: 20px; }
436
+ .txm-instruction { padding: 16px 18px; border-radius: 14px; border: 1px solid var(--line); background: var(--tint-2); font-size: 15px; line-height: 1.65; color: var(--ink-2); overflow-wrap: anywhere; }
437
+ .txm-instruction p + p { margin-top: 6px; }
438
+ .txm-facts { margin: 16px 0 0; }
439
+ .txm-facts div { display: grid; grid-template-columns: 136px minmax(0, 1fr); gap: 12px; padding: 11px 0; border-top: 1px solid var(--line); }
440
+ .txm-facts dt { padding-top: 2px; font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent-deep); }
441
+ .txm-facts dd { margin: 0; font-size: 14px; color: var(--ink-2); overflow-wrap: anywhere; }
442
+ .tx-term-lg { border-radius: 14px; border: 1px solid #1e293b; }
443
+ .tx-term-lg .tx-screen { height: 236px; font-size: 12px; }
444
+ .txm-code { max-height: 300px; margin: 0; padding: 14px 16px; overflow: auto; border-radius: 14px; background: #0b1220; color: #cbd5e1; font: 11.5px/1.65 var(--mono); white-space: pre; }
445
+ #txm-cta { margin-top: 22px; }
446
+
447
+ /* Collaboration */
448
+ .steps { position: relative; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; margin: 0; padding: 0; list-style: none; }
449
+ .steps::before { content: ""; position: absolute; left: 26px; right: calc(25% - 40px); top: 25px; height: 2px; background: repeating-linear-gradient(90deg, #d6cdbb 0 8px, transparent 8px 14px); }
450
+ .step { position: relative; padding-right: 8px; }
451
+ .step-num { position: relative; display: grid; place-items: center; width: 52px; height: 52px; border-radius: 50%; border: 1px solid var(--line); background: #fff; box-shadow: var(--shadow-sm); font-family: var(--serif); font-size: 22px; font-weight: 600; color: var(--accent-deep); }
452
+ .step:last-child .step-num { border-color: var(--accent); background: var(--accent); color: #fff; }
453
+ .step h3 { margin: 16px 0 6px; font-size: 19px; letter-spacing: -0.015em; }
454
+ .step p { font-size: 15px; color: var(--ink-2); }
455
+ .collab-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; margin-top: 52px; }
456
+ .give-card { padding: 28px; border-radius: 22px; border: 1px solid var(--line); background: var(--surface); box-shadow: var(--shadow-sm); }
457
+ .give-card h3 { margin-bottom: 14px; font-family: var(--serif); font-size: 24px; font-weight: 600; }
458
+ .give-card-accent { border-color: color-mix(in srgb, var(--accent) 28%, var(--line)); background: linear-gradient(160deg, color-mix(in srgb, var(--accent) 9%, #fff), #fff 72%); }
459
+ .check-list { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
460
+ .check-list li { display: flex; gap: 10px; font-size: 15.5px; color: var(--ink-2); }
461
+ .check-list li::before { content: "✓"; flex: none; color: var(--accent); font-weight: 800; }
462
+ .ways { margin-top: 20px; padding: 24px 28px; border-radius: 22px; border: 1px dashed #d6cdbb; background: rgba(255, 255, 255, 0.6); }
463
+ .ways h3 { font-size: 13px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
464
+ .ways-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 14px; }
465
+ .way-tag { display: inline-block; padding: 5px 12px; border-radius: 999px; background: var(--ink); color: #fff; font-size: 13px; font-weight: 600; }
466
+ .way p { margin-top: 8px; font-size: 14.5px; color: var(--ink-2); }
467
+
468
+ /* Papers */
469
+ .paper-list { display: grid; gap: 22px; }
470
+ .paper { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: 32px; padding: 30px; border-radius: 24px; border: 1px solid var(--line); background: var(--surface); box-shadow: var(--shadow); }
471
+ .paper-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; font-size: 13px; color: var(--muted); }
472
+ .paper-tag { padding: 4px 10px; border-radius: 999px; background: var(--accent-soft); color: var(--accent-deep); font-size: 12px; font-weight: 700; }
473
+ .paper h3 { margin: 12px 0 8px; font-family: var(--serif); font-size: clamp(22px, 2.1vw, 27px); font-weight: 600; line-height: 1.22; letter-spacing: -0.01em; }
474
+ .paper h3 a { text-decoration: underline; text-decoration-color: transparent; text-decoration-thickness: 1px; text-underline-offset: 5px; transition: color 0.2s, text-decoration-color 0.2s; }
475
+ .paper h3 a:hover { color: var(--accent-deep); text-decoration-color: currentColor; }
476
+ .paper-authors { font-size: 14px; color: var(--muted); }
477
+ .paper-summary { margin-top: 14px; font-size: 15.5px; color: var(--ink-2); }
478
+ .paper-you { margin-top: 12px; padding: 10px 14px; border-radius: 12px; background: var(--tint-2); font-size: 14.5px; color: var(--ink-2); }
479
+ .paper-you strong { color: var(--ink); }
480
+ .paper-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
481
+ .paper-results { align-self: start; padding: 20px 22px 22px; border-radius: 18px; border: 1px solid var(--line); background: var(--tint-2); }
482
+ .paper-results h4 { font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent-deep); }
483
+ .res { margin-top: 16px; }
484
+ .res-label { margin-bottom: 2px; font-size: 13.5px; font-weight: 600; color: var(--ink); }
485
+ .rb { display: grid; gap: 3px; margin-top: 7px; }
486
+ .rb span { order: -1; font: 500 12px var(--mono); color: var(--ink-2); }
487
+ .rb i { position: relative; display: block; height: 10px; overflow: hidden; border-radius: 999px; background: rgba(24, 32, 46, 0.07); }
488
+ .rb i::after { content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: calc(var(--v) * 1%); min-width: 4px; border-radius: inherit; background: #b8c0cc; transform-origin: left; }
489
+ .rb-ours i::after { background: linear-gradient(90deg, var(--accent), #14b8a6); }
490
+ .rb-warm i::after { background: linear-gradient(90deg, #f59e0b, #ea580c); }
491
+ .js .paper .rb i::after { transform: scaleX(0); transition: transform 1s var(--ease) 0.25s; }
492
+ .js .paper.is-visible .rb i::after { transform: none; }
493
+
494
+ /* FAQ */
495
+ .faq-grid { display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: 48px; align-items: start; }
496
+ .faq-list { display: grid; gap: 10px; }
497
+ .faq-list details { border-radius: 16px; border: 1px solid var(--line); background: var(--surface); box-shadow: var(--shadow-sm); }
498
+ .faq-list summary { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 18px 20px; font-size: 16.5px; font-weight: 600; list-style: none; cursor: pointer; }
499
+ .faq-list summary::-webkit-details-marker { display: none; }
500
+ .faq-list summary::after { content: "+"; display: grid; flex: none; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--tint); font-size: 18px; font-weight: 500; color: var(--ink-2); transition: transform 0.25s var(--ease); }
501
+ .faq-list details[open] summary::after { transform: rotate(45deg); }
502
+ .faq-list details p { padding: 0 20px 18px; font-size: 15.5px; color: var(--ink-2); }
503
+
504
+ /* Join */
505
+ .join {
506
+ color: #e8ecf2;
507
+ background:
508
+ radial-gradient(60% 80% at 0% 0%, rgba(15, 159, 117, 0.18), transparent 60%),
509
+ radial-gradient(50% 70% at 100% 100%, rgba(79, 70, 229, 0.16), transparent 60%),
510
+ var(--dark);
511
+ }
512
+ .join .kicker { color: #6ee7b7; }
513
+ .join h2 { color: #fff; }
514
+ .join .sub { color: #b9c2cf; }
515
+ .join-grid { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); gap: 48px; align-items: start; }
516
+ .join-alt { margin-top: 22px; font-size: 15px; color: #b9c2cf; }
517
+ .mail-link { color: #fff; font-weight: 600; text-decoration: underline; text-decoration-color: rgba(255, 255, 255, 0.4); text-underline-offset: 3px; overflow-wrap: anywhere; }
518
+ .join-form { display: grid; gap: 16px; padding: 28px; border-radius: 24px; background: #fff; color: var(--ink); box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.6); }
519
+ .join-form > label, .row2 > label { display: grid; gap: 6px; font-size: 14px; font-weight: 600; }
520
+ .join-form input:not([type="radio"]):not([type="checkbox"]), .join-form textarea { width: 100%; padding: 11px 13px; border-radius: 12px; border: 1px solid var(--line); background: var(--tint-2); font: 400 15px/1.45 var(--font); color: var(--ink); transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s; }
521
+ .join-form textarea { min-height: 84px; resize: vertical; }
522
+ .join-form input:focus, .join-form textarea:focus { outline: none; border-color: var(--accent); background: #fff; box-shadow: 0 0 0 4px var(--accent-soft); }
523
+ .join-form input[aria-invalid="true"] { border-color: #dc2626; box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.12); }
524
+ .row2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
525
+ .pill-set { margin: 0; padding: 0; border: 0; }
526
+ .pill-set legend { margin-bottom: 8px; padding: 0; font-size: 14px; font-weight: 600; }
527
+ .pills { display: flex; flex-wrap: wrap; gap: 8px; }
528
+ .pills label { position: relative; display: inline-flex; cursor: pointer; }
529
+ .pills input { position: absolute; width: 1px; height: 1px; opacity: 0; }
530
+ .pills span { padding: 8px 14px; border-radius: 999px; border: 1px solid var(--line); background: var(--tint-2); font-size: 14px; font-weight: 500; color: var(--ink-2); transition: background-color 0.2s, border-color 0.2s, color 0.2s; }
531
+ .pills input:checked + span { border-color: var(--ink); background: var(--ink); color: #fff; }
532
+ .pills input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
533
+ .form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; margin-top: 4px; }
534
+ .form-note { flex: 1 1 220px; font-size: 13.5px; color: var(--muted); }
535
+
536
+ /* Footer */
537
+ .footer { padding: 30px 0 34px; border-top: 1px solid rgba(255, 255, 255, 0.08); background: #10151f; color: #9aa4b2; }
538
+ .footer-inner { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 18px; }
539
+ .footer .brand { color: #fff; }
540
+ .footer .brand em { border-color: rgba(255, 255, 255, 0.2); color: #9aa4b2; }
541
+ .footer-links { display: flex; flex-wrap: wrap; gap: 24px; font-size: 14px; }
542
+ .footer-links a:hover { color: #fff; }
543
+ .footer-note { width: 100%; font-size: 12.5px; color: #6b7686; }
544
+
545
+ /* Scroll reveal */
546
+ .js .reveal { opacity: 0; transform: translateY(18px); transition: opacity 0.8s var(--ease), transform 0.8s var(--ease); }
547
+ .js .reveal.is-visible { opacity: 1; transform: none; }
548
+ .field-grid .reveal:nth-child(2), .collab-grid .reveal:nth-child(2), .steps .reveal:nth-child(2) { transition-delay: 0.08s; }
549
+ .steps .reveal:nth-child(3) { transition-delay: 0.16s; }
550
+ .steps .reveal:nth-child(4) { transition-delay: 0.24s; }
551
+
552
+ /* Responsive */
553
+ @media (max-width: 1100px) {
554
+ .nav-links { display: none; }
555
+ .nav-toggle { display: inline-flex; }
556
+ .nav-cta .btn { display: none; }
557
+ .hero-grid { grid-template-columns: minmax(0, 1fr); gap: 8px; }
558
+ .hero-visual { max-width: 440px; }
559
+ .why-grid { grid-template-columns: minmax(0, 1fr); gap: 36px; }
560
+ .stage-tabs { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(170px, 1fr); overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }
561
+ .stage-tabs::-webkit-scrollbar { display: none; }
562
+ .questions { grid-template-columns: minmax(0, 1fr); gap: 20px; }
563
+ .ex-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
564
+ .ex-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
565
+ .tx-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
566
+ .txm-grid { grid-template-columns: minmax(0, 1fr); }
567
+ .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 34px; }
568
+ .steps::before { display: none; }
569
+ .paper { grid-template-columns: minmax(0, 1fr); gap: 22px; }
570
+ .faq-grid, .join-grid { grid-template-columns: minmax(0, 1fr); gap: 32px; }
571
+ }
572
+
573
+ @media (max-width: 760px) {
574
+ .field-grid, .collab-grid, .ways-grid, .question-list, .stage-grid { grid-template-columns: minmax(0, 1fr); }
575
+ }
576
+
577
+ @media (max-width: 640px) {
578
+ .container { width: min(1200px, 100% - 36px); }
579
+ .brand em { display: none; }
580
+ .hero-visual { max-width: 340px; }
581
+ .stat { grid-template-columns: minmax(0, 1fr); gap: 6px; }
582
+ .explorer { padding: 18px; border-radius: 20px; }
583
+ .questions { padding: 22px 18px; }
584
+ .field-card, .give-card, .paper { padding: 22px; }
585
+ .ex-grid, .ex-facts, .tx-grid, .steps, .row2 { grid-template-columns: minmax(0, 1fr); }
586
+ .ex-modal { padding: 10px; }
587
+ .ex-sheet { max-height: 100%; border-radius: 18px; }
588
+ .ex-head { padding: 16px 16px 14px; }
589
+ .ex-head h3 { font-size: 20px; }
590
+ .ex-body { padding: 16px; }
591
+ .txm-facts div { grid-template-columns: minmax(0, 1fr); gap: 4px; }
592
+ .join-form { padding: 20px; }
593
+ }
594
+
595
+ @media (prefers-reduced-motion: reduce) {
596
+ html { scroll-behavior: auto; }
597
+ *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; transition-delay: 0s !important; }
598
+ }
terminal.js ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (() => {
2
+ 'use strict';
3
+
4
+ const $ = (sel, root = document) => root.querySelector(sel);
5
+ const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
6
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
7
+
8
+ const source = $('#terminal-data');
9
+ if (!source) return;
10
+ let DATA;
11
+ try {
12
+ DATA = JSON.parse(source.textContent);
13
+ } catch (err) {
14
+ return;
15
+ }
16
+
17
+ const contactHref = (subject) => {
18
+ const link = $('[data-contact]');
19
+ const href = link ? link.getAttribute('href') || '' : '';
20
+ const address = href.startsWith('mailto:') ? href.slice(7).split('?')[0] : '';
21
+ return address ? `mailto:${address}?subject=${encodeURIComponent(subject)}` : '#contact';
22
+ };
23
+
24
+ const VERIFY = [
25
+ ['tx-sys', 'Running verifier in a fresh sandbox…'],
26
+ ['tx-ok', '✓ All checks passed'],
27
+ ];
28
+
29
+ class Replayer {
30
+ constructor(screen, lines) {
31
+ this.screen = screen;
32
+ this.code = $('code', screen) || screen;
33
+ this.lines = lines;
34
+ this.timer = 0;
35
+ this.running = false;
36
+ this.caret = document.createElement('i');
37
+ this.caret.className = 'tx-caret';
38
+ this.reset();
39
+ }
40
+
41
+ reset() {
42
+ this.code.replaceChildren();
43
+ this.line = 0;
44
+ this.char = 0;
45
+ this.phase = 'type';
46
+ this.verified = 0;
47
+ this.text = null;
48
+ }
49
+
50
+ prompt(symbol) {
51
+ const span = document.createElement('span');
52
+ span.className = 'tx-cmd';
53
+ const b = document.createElement('b');
54
+ b.textContent = symbol;
55
+ const text = document.createTextNode('');
56
+ span.append(b, ' ', text);
57
+ this.code.append(span);
58
+ return { span, text };
59
+ }
60
+
61
+ status(cls, message) {
62
+ const span = document.createElement('span');
63
+ span.className = cls;
64
+ span.textContent = message;
65
+ this.code.append(span);
66
+ }
67
+
68
+ renderStatic() {
69
+ this.stop();
70
+ this.reset();
71
+ this.lines.forEach(([symbol, line]) => {
72
+ this.prompt(symbol).text.data = line;
73
+ });
74
+ VERIFY.forEach(([cls, message]) => this.status(cls, message));
75
+ }
76
+
77
+ start(delay = 0) {
78
+ if (this.running) return;
79
+ this.running = true;
80
+ this.schedule(delay);
81
+ }
82
+
83
+ stop() {
84
+ this.running = false;
85
+ clearTimeout(this.timer);
86
+ }
87
+
88
+ schedule(ms) {
89
+ clearTimeout(this.timer);
90
+ if (this.running) this.timer = setTimeout(() => this.tick(), ms);
91
+ }
92
+
93
+ scroll() {
94
+ this.screen.scrollTop = this.screen.scrollHeight;
95
+ }
96
+
97
+ tick() {
98
+ if (!this.running) return;
99
+ if (this.phase === 'type') {
100
+ if (this.line >= this.lines.length) {
101
+ this.caret.remove();
102
+ this.phase = 'verify';
103
+ this.schedule(420);
104
+ return;
105
+ }
106
+ const [symbol, line] = this.lines[this.line];
107
+ if (!this.text) {
108
+ const { span, text } = this.prompt(symbol);
109
+ span.append(this.caret);
110
+ this.text = text;
111
+ this.scroll();
112
+ this.schedule(160);
113
+ return;
114
+ }
115
+ this.char = Math.min(line.length, this.char + Math.max(1, Math.ceil(line.length / 40)));
116
+ this.text.data = line.slice(0, this.char);
117
+ if (this.char >= line.length) {
118
+ this.line += 1;
119
+ this.char = 0;
120
+ this.text = null;
121
+ this.scroll();
122
+ this.schedule(300);
123
+ return;
124
+ }
125
+ this.schedule(22);
126
+ return;
127
+ }
128
+ if (this.phase === 'verify') {
129
+ if (this.verified >= VERIFY.length) {
130
+ this.prompt('$').span.append(this.caret);
131
+ this.scroll();
132
+ this.phase = 'hold';
133
+ this.schedule(3200);
134
+ return;
135
+ }
136
+ const [cls, message] = VERIFY[this.verified];
137
+ this.verified += 1;
138
+ this.status(cls, message);
139
+ this.scroll();
140
+ this.schedule(this.verified === 1 ? 1000 : 200);
141
+ return;
142
+ }
143
+ this.reset();
144
+ this.schedule(260);
145
+ }
146
+ }
147
+
148
+ function initCards(tasks) {
149
+ const grid = $('#tx-grid');
150
+ if (!grid) return;
151
+ const cards = $$('.tx-card', grid);
152
+ const players = cards.map((card) => {
153
+ const task = tasks[Number(card.dataset.index)];
154
+ return task ? new Replayer($('.tx-screen', card), task.replay) : null;
155
+ });
156
+
157
+ if (reduceMotion || !('IntersectionObserver' in window)) {
158
+ players.forEach((p) => p && p.renderStatic());
159
+ } else {
160
+ const observer = new IntersectionObserver((entries) => {
161
+ entries.forEach((entry) => {
162
+ const i = cards.indexOf(entry.target);
163
+ const player = players[i];
164
+ if (!player) return;
165
+ if (entry.isIntersecting) {
166
+ player.start(player.started ? 0 : (i % 3) * 450);
167
+ player.started = true;
168
+ } else {
169
+ player.stop();
170
+ }
171
+ });
172
+ }, { threshold: 0.25 });
173
+ cards.forEach((card) => observer.observe(card));
174
+ }
175
+
176
+ const more = $('#tx-more');
177
+ const extras = cards.filter((card) => card.classList.contains('is-extra'));
178
+ if (more) {
179
+ if (!extras.length) more.hidden = true;
180
+ more.textContent = `Show ${extras.length} more terminal tasks`;
181
+ more.addEventListener('click', () => {
182
+ grid.classList.add('show-all');
183
+ more.setAttribute('aria-expanded', 'true');
184
+ if (extras[0]) extras[0].focus({ preventScroll: true });
185
+ });
186
+ }
187
+ return cards;
188
+ }
189
+
190
+ function initModal(tasks, cards) {
191
+ const modal = $('#tx-modal');
192
+ if (!modal || !cards) return;
193
+ const sheet = $('.ex-sheet', modal);
194
+ const closeButton = $('.icon-btn[data-tx-close]', modal);
195
+ const field = (id) => document.getElementById(id);
196
+ const f = {
197
+ area: field('txm-area'),
198
+ title: field('txm-title'),
199
+ tags: field('txm-tags'),
200
+ instruction: field('txm-instruction'),
201
+ base: field('txm-base'),
202
+ termBase: field('txm-term-base'),
203
+ lines: field('txm-lines'),
204
+ replay: field('txm-replay'),
205
+ code: field('txm-code'),
206
+ cta: field('txm-cta'),
207
+ };
208
+ let current = 0;
209
+ let lastFocus = null;
210
+ let player = null;
211
+
212
+ const play = () => {
213
+ if (player) player.stop();
214
+ player = new Replayer(f.replay, tasks[current].replay);
215
+ if (reduceMotion) player.renderStatic();
216
+ else player.start(250);
217
+ };
218
+
219
+ const render = (index) => {
220
+ current = (index + tasks.length) % tasks.length;
221
+ const t = tasks[current];
222
+ f.area.textContent = t.area;
223
+ f.title.textContent = t.title;
224
+ f.tags.replaceChildren(...t.tags.map((tag) => {
225
+ const li = document.createElement('li');
226
+ li.className = 'trk trk-mod';
227
+ li.textContent = tag;
228
+ return li;
229
+ }));
230
+ f.instruction.innerHTML = t.instruction;
231
+ f.base.textContent = t.base;
232
+ f.termBase.textContent = t.base;
233
+ f.lines.textContent = `${t.lines}-line solve.sh`;
234
+ f.code.textContent = t.code;
235
+ f.code.scrollTop = 0;
236
+ f.cta.href = contactHref(`${modal.dataset.subject || 'Terminal task sample'}: ${t.title}`);
237
+ sheet.scrollTop = 0;
238
+ if (!modal.hidden) play();
239
+ };
240
+
241
+ const open = (index) => {
242
+ lastFocus = document.activeElement;
243
+ modal.hidden = false;
244
+ render(index);
245
+ document.body.classList.add('modal-open');
246
+ closeButton.focus();
247
+ };
248
+ const close = () => {
249
+ modal.hidden = true;
250
+ if (player) player.stop();
251
+ document.body.classList.remove('modal-open');
252
+ if (lastFocus) lastFocus.focus({ preventScroll: true });
253
+ };
254
+
255
+ cards.forEach((card) => {
256
+ const index = Number(card.dataset.index);
257
+ card.addEventListener('click', () => open(index));
258
+ card.addEventListener('keydown', (e) => {
259
+ if (e.key === 'Enter' || e.key === ' ') {
260
+ e.preventDefault();
261
+ open(index);
262
+ }
263
+ });
264
+ });
265
+ $$('[data-tx-close]', modal).forEach((el) => el.addEventListener('click', close));
266
+ $('[data-tx-prev]', modal).addEventListener('click', () => render(current - 1));
267
+ $('[data-tx-next]', modal).addEventListener('click', () => render(current + 1));
268
+
269
+ document.addEventListener('keydown', (e) => {
270
+ if (modal.hidden) return;
271
+ if (e.key === 'Escape') {
272
+ e.preventDefault();
273
+ close();
274
+ } else if (e.key === 'ArrowRight') {
275
+ render(current + 1);
276
+ } else if (e.key === 'ArrowLeft') {
277
+ render(current - 1);
278
+ } else if (e.key === 'Tab') {
279
+ const focusables = $$('button, a[href]', sheet);
280
+ const first = focusables[0];
281
+ const last = focusables[focusables.length - 1];
282
+ if (e.shiftKey && document.activeElement === first) {
283
+ e.preventDefault();
284
+ last.focus();
285
+ } else if (!e.shiftKey && document.activeElement === last) {
286
+ e.preventDefault();
287
+ first.focus();
288
+ }
289
+ }
290
+ });
291
+ }
292
+
293
+ function initGrowth(growth) {
294
+ const root = $('#growth');
295
+ if (!root || !growth || !growth.stages || !growth.stages.length) return;
296
+ const stages = growth.stages;
297
+ const steps = $('#growth-steps');
298
+ const instruction = $('#growth-instruction');
299
+ const linesEl = $('#growth-lines');
300
+ const wordsEl = $('#growth-words');
301
+ const linesBar = $('#growth-lines-bar');
302
+ const wordsBar = $('#growth-words-bar');
303
+ const spark = $('#growth-spark');
304
+ const maxLines = Math.max(...stages.map((s) => s.lines));
305
+ const maxWords = Math.max(...stages.map((s) => s.words));
306
+ const svgNS = 'http://www.w3.org/2000/svg';
307
+
308
+ const W = 240;
309
+ const H = 72;
310
+ const P = 6;
311
+ const xs = stages.map((_, i) => P + (i * (W - 2 * P)) / Math.max(1, stages.length - 1));
312
+ const ys = stages.map((s) => H - P - ((H - 2 * P) * s.lines) / maxLines);
313
+ const pts = xs.map((x, i) => `${x.toFixed(1)},${ys[i].toFixed(1)}`).join(' ');
314
+ const make = (tag, attrs) => {
315
+ const node = document.createElementNS(svgNS, tag);
316
+ Object.entries(attrs).forEach(([k, v]) => node.setAttribute(k, v));
317
+ spark.append(node);
318
+ return node;
319
+ };
320
+ spark.replaceChildren();
321
+ make('polygon', { class: 'gs-area', points: `${P},${H - P} ${pts} ${W - P},${H - P}` });
322
+ make('polyline', { class: 'gs-line', points: pts });
323
+ const guide = make('line', { class: 'gs-guide', x1: 0, x2: 0, y1: 2, y2: H - 2 });
324
+ const dots = xs.map((x, i) => make('circle', { class: 'gs-dot', cx: x.toFixed(1), cy: ys[i].toFixed(1), r: 3 }));
325
+
326
+ let index = 0;
327
+ let shown = { lines: 0, words: 0 };
328
+ let tween = 0;
329
+ const countTo = (target) => {
330
+ cancelAnimationFrame(tween);
331
+ const from = { ...shown };
332
+ const t0 = performance.now();
333
+ const step = (now) => {
334
+ const k = reduceMotion ? 1 : Math.min(1, (now - t0) / 600);
335
+ const e = 1 - (1 - k) ** 3;
336
+ shown = {
337
+ lines: Math.round(from.lines + (target.lines - from.lines) * e),
338
+ words: Math.round(from.words + (target.words - from.words) * e),
339
+ };
340
+ linesEl.textContent = `${shown.lines} lines`;
341
+ wordsEl.textContent = `${shown.words} words`;
342
+ if (k < 1) tween = requestAnimationFrame(step);
343
+ };
344
+ tween = requestAnimationFrame(step);
345
+ };
346
+
347
+ const buttons = stages.map((_, i) => {
348
+ const b = document.createElement('button');
349
+ b.type = 'button';
350
+ b.className = 'gstep';
351
+ b.setAttribute('aria-label', `Stage ${i + 1} of ${stages.length}`);
352
+ b.innerHTML = `<span>Stage</span><strong>${i + 1}</strong>`;
353
+ b.addEventListener('click', () => {
354
+ setAuto(false);
355
+ show(i);
356
+ });
357
+ return b;
358
+ });
359
+ const toggle = document.createElement('button');
360
+ toggle.type = 'button';
361
+ toggle.className = 'gplay';
362
+ steps.replaceChildren(...buttons, toggle);
363
+
364
+ const show = (i) => {
365
+ index = i;
366
+ const stage = stages[i];
367
+ buttons.forEach((b, k) => {
368
+ b.setAttribute('aria-pressed', String(k === i));
369
+ b.classList.toggle('is-done', k < i);
370
+ });
371
+ const text = document.createElement('p');
372
+ text.className = 'g-text';
373
+ text.innerHTML = stage.segments.map((s) => (s.new ? `<mark>${s.html}</mark>` : s.html)).join(' ');
374
+ instruction.replaceChildren(text);
375
+ linesBar.style.width = `${((100 * stage.lines) / maxLines).toFixed(1)}%`;
376
+ wordsBar.style.width = `${((100 * stage.words) / maxWords).toFixed(1)}%`;
377
+ countTo(stage);
378
+ dots.forEach((d, k) => {
379
+ d.classList.toggle('is-on', k <= i);
380
+ d.setAttribute('r', k === i ? 5 : 3);
381
+ });
382
+ guide.style.transform = `translateX(${xs[i].toFixed(1)}px)`;
383
+ };
384
+
385
+ let auto = !reduceMotion;
386
+ let visible = false;
387
+ let timer = 0;
388
+ const schedule = () => {
389
+ clearTimeout(timer);
390
+ if (!auto || !visible) return;
391
+ timer = setTimeout(() => {
392
+ show(index === stages.length - 1 ? 0 : index + 1);
393
+ schedule();
394
+ }, index === stages.length - 1 ? 6000 : 4200);
395
+ };
396
+ const setAuto = (on) => {
397
+ auto = on;
398
+ toggle.textContent = on ? 'Pause' : 'Play';
399
+ toggle.setAttribute('aria-label', on ? 'Pause stage autoplay' : 'Play stages automatically');
400
+ schedule();
401
+ };
402
+ toggle.addEventListener('click', () => setAuto(!auto));
403
+
404
+ show(0);
405
+ setAuto(auto);
406
+ if ('IntersectionObserver' in window) {
407
+ new IntersectionObserver((entries) => {
408
+ visible = entries[0].isIntersecting;
409
+ schedule();
410
+ }, { threshold: 0.35 }).observe(root);
411
+ }
412
+ }
413
+
414
+ const tasks = DATA.tasks || [];
415
+ const cards = initCards(tasks);
416
+ initModal(tasks, cards);
417
+ initGrowth(DATA.growth);
418
+ })();