syntheogenesis / dee /static /index.html
github-actions[bot]
Deploy 2565f24
7b284c7
Raw
History Blame Contribute Delete
179 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TuringDNA · Engine</title>
<meta name="description" content="TuringDNA — talk to Turing and it runs the lab: directed evolution, CRISPR guide design, primer analysis, and a plasmid editor, all callable from one conversation." />
<meta name="theme-color" content="#1b1a17" />
<meta name="color-scheme" content="dark light" />
<meta name="referrer" content="strict-origin-when-cross-origin" />
<!-- The canonical product lives at turingdna.com; the raw engine URL isn't indexed. -->
<meta name="robots" content="noindex,follow" />
<!-- Theme bootstrap — runs BEFORE the stylesheet so <html data-theme> is
set before first paint (no flash on load). Dark is the default; a
saved choice (from the topbar toggle) overrides it. First-party
inline script; touches only our own localStorage + documentElement. -->
<script>
(function () {
try {
var t = localStorage.getItem('td-theme');
if (t !== 'light' && t !== 'dark') { t = 'dark'; } // dark by default
document.documentElement.setAttribute('data-theme', t);
} catch (e) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
<!-- Viewport-height bootstrap — the "--vh" custom-property technique.
`100dvh`/`100vh` alone have shipped genuinely inconsistent across
engine versions (this repo has hit two separate cases of Safari
computing a taller box than the real visible viewport, cutting off
bottom-pinned UI — the sidebar's account chip and the Turing
composer). window.innerHeight has been reliable across every
browser for over a decade, so measuring it directly and feeding it
into CSS as a custom property sidesteps whatever the unit itself
gets wrong. Runs before first paint; --vh isn't set until this runs,
so every consumer uses `var(--vh, 1vh)` — falls back to the native
unit for the one frame before this executes. -->
<!-- Confirmed directly on real Safari 15.6.1: getBoundingClientRect()
already reports the CORRECT, uncut geometry at rest — the layout
is right, but Safari doesn't always repaint the screen to match a
`--vh` change made via a custom property. The stale frame (sidebar
account chip clipped) stays on screen until something forces a
repaint — confirmed a user scroll gesture IS that trigger (the
clipped content flashes correctly into view mid-scroll, then
reverts once scrolling settles). Nudge a repaint ourselves right
after every --vh change instead of waiting for an accidental one. -->
<script>
(function () {
// Nudges the sidebar specifically, not <html>/<body> — transforming
// an ancestor of a position:fixed element (the sign-in scrim, any
// modal) temporarily makes THAT ancestor the fixed element's
// containing block instead of the viewport, which would visibly
// mis-jump anything fixed open at that instant. The sidebar itself
// has no fixed descendants, so it's a safe, targeted repaint kick.
function nudgeRepaint() {
var el = document.querySelector('.sidebar');
// Also bail if a nudge is already in flight — without this guard,
// an overlapping call captures the OTHER call's in-progress
// "translateZ(0)" as its own "previous" value, so whichever one
// reverts last leaves the transform stuck on permanently.
if (!el || el.dataset.nudging) return;
el.dataset.nudging = '1';
var prev = el.style.transform;
el.style.transform = 'translateZ(0)';
void el.offsetHeight;
requestAnimationFrame(function () {
el.style.transform = prev;
delete el.dataset.nudging;
});
}
function setVH() {
document.documentElement.style.setProperty('--vh', (window.innerHeight * 0.01) + 'px');
nudgeRepaint();
}
setVH();
window.addEventListener('resize', setVH);
// This script runs in <head>, before .sidebar exists, so the very
// first nudgeRepaint() above is a no-op. Fire it again once the
// sidebar is actually in the DOM — the bug reproduces on a plain
// page load, not just after a resize.
document.addEventListener('DOMContentLoaded', nudgeRepaint);
// TEMP diagnostic — every layout measurement taken directly against
// this page (via osascript `do JavaScript` on the user's real Safari)
// has come back correct, every time, even before any of today's
// fixes. The one thing never directly inspected is this exact page
// AS EMBEDDED in turingdna.com's cross-origin iframe wrapper — that
// context can't be scripted into from the parent (same-origin
// policy), so report our own numbers TO the parent instead, where
// they're readable. Safe no-op when not framed. Remove once the
// wrapper-embedding question is settled either way.
document.addEventListener('DOMContentLoaded', function () {
if (window.self === window.top) return;
try {
var sidebar = document.querySelector('.sidebar');
var signin = document.getElementById('sidebarSignin');
var sr = signin ? signin.getBoundingClientRect() : null;
window.parent.postMessage({
type: 'td-diag',
innerHeight: window.innerHeight,
vh: getComputedStyle(document.documentElement).getPropertyValue('--vh'),
sidebarHeight: sidebar ? Math.round(sidebar.getBoundingClientRect().height) : null,
signinBottom: sr ? sr.bottom : null,
signinWithinViewport: sr ? (sr.bottom <= window.innerHeight) : null,
visibilityState: document.visibilityState
}, '*');
} catch (e) {}
});
})();
</script>
<!-- ?v= query bumps invalidate browser + iframe asset caches when app.css /
app.js change. Bump these numbers whenever you ship a frontend update —
without them, users keep getting the stale file for up to a week. -->
<link rel="stylesheet" href="/static/app.css?v=20260801-review" />
<!-- The work catalog + the draggable rail. Kept out of app.css so two new
self-contained surfaces stay reviewable; every colour is an app.css
token, so both themes work with nothing added. -->
<link rel="stylesheet" href="/static/catalog.css?v=20260801-review" />
<!-- Units, the run strip and the decision trace. Same reasoning as
catalog.css: kept out of app.css so a self-contained surface stays
reviewable, and every colour is an app.css token so both themes
work with nothing added. -->
<link rel="stylesheet" href="/static/trace.css?v=20260801-review" />
<!-- Disclosure surfaces added in the 2026-08 scientific review (CRISPR
scope strip, provenance "Assumes" line). Separate file for the same
reason as catalog.css: small, self-contained, and app.css's cascade
is hostile. Uses app.css tokens only, so both themes work. -->
<link rel="stylesheet" href="/static/science.css?v=20260801-review" />
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=2" />
<link rel="apple-touch-icon" href="/static/favicon.svg?v=2" />
<!-- Mol* (PDBe) 3-D viewer is ~4.9 MB. We do NOT eager-load it on every
page view — app.js ensureMolstar() loads it lazily the first time a
Fold / AlphaFold viewer is opened, with a jsDelivr → unpkg fallback and
no fixed timeout (the old eager `defer` + 8 s wait was why both viewers
"failed to load" so often on slower links). Preconnect just warms the
CDNs so that first lazy load is fast. -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://unpkg.com" crossorigin />
</head>
<body>
<!-- Bio-luminous atmospheric backdrop (ambient glow + vignette + grain),
painted under the app; dark-mode only. Fades in on load. -->
<div class="bg-atmos" id="bgAtmos" aria-hidden="true"></div>
<!-- (Cursor-following light removed — gimmick. Only the static ambient
backdrop remains; fade it in once painted.) -->
<script>
(function(){
var a=document.getElementById('bgAtmos'); if(a) requestAnimationFrame(function(){a.classList.add('lit');});
})();
</script>
<!-- One-time material-change notice (Privacy/Terms v2.0). Dismiss persists in localStorage. -->
<div id="policyNotice" class="policy-notice" hidden>
<span class="policy-notice-text">We've updated our <a href="https://turingdna.com/privacy/" target="_blank" rel="noopener">Privacy Policy</a> &amp; <a href="https://turingdna.com/terms/" target="_blank" rel="noopener">Terms</a> (v2.0, effective 8&nbsp;Jul&nbsp;2026): we now use <strong>anonymous, aggregated</strong> signals to improve our models. Your individual sequences stay private — never shared, exposed, or reproduced. Prefer not to continue? You can export or delete your data before then (<a href="https://turingdna.com/contact/" target="_blank" rel="noopener">contact us</a>).</span>
<button type="button" id="policyNoticeClose" class="policy-notice-close">Got&nbsp;it</button>
</div>
<!--
⌘K finder. Navigation only — constructs, past runs, tools. It is
deliberately NOT a chat entry: the Turing rail is the only place you
talk to the agent, so this can never become a second composer again.
-->
<div class="finder" id="finder" hidden>
<div class="finder-scrim" id="finderScrim"></div>
<div class="finder-box" role="dialog" aria-modal="true" aria-label="Find">
<div class="finder-in">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input id="finderQ" type="text" autocomplete="off" spellcheck="false"
placeholder="Find a construct, run, or tool…" aria-label="Find" />
<kbd>esc</kbd>
</div>
<div class="finder-list" id="finderList" role="listbox"></div>
</div>
</div>
<div class="app">
<!-- ============================== SIDEBAR ============================== -->
<aside class="sidebar" id="sidebar">
<!-- The brand sits in a header BAND, not loose at the top of the
rail: the band is exactly --topbar-h tall and carries the
hairline, so the sidebar's rule lands on the same y as the
cockpit's (.cp-head) and the workspace's (.topbar). Without
it the three panels each started their content at a
different height and the lines stepped. -->
<div class="sb-head">
<a class="brand" href="/" title="TuringDNA">
<span class="brand-mark" aria-hidden="true">
<!-- Living logo: perpetually-writhing supercoiled plasmid
(Canvas, animated by initPlasmidLogo() in app.js). -->
<canvas class="brand-mark-canvas" aria-hidden="true"></canvas>
</span>
<span class="brand-word">TuringDNA</span>
</a>
</div>
<!--
Sidebar nav — restructured 2026-07-13 to stop reading as a flat,
scattered pile of eight coequal links (the founder's note: "looks
like a vibecoded AI platform"). The cure is HIERARCHY, not more
chrome:
• Turing is the FRONT DOOR — a lead block set above everything,
the way in (it drives the whole loop in plain language).
• The four tools are ONE connected sequence — the Design→Build→
Edit→Verify loop — tied together by a hairline "spine" so they
read as a workflow, not four islands. Each carries its loop
phase as a quiet caption.
• Utilities (Start here / My designs / Docs) drop to a muted
tier pinned above the account foot — present, not competing.
Still type-driven and icon-free (the earlier debug widget + SVG
icons + "Workspace/Reference" bucket labels were removed for
reading as SaaS scaffolding); the structure now carries the
meaning the flat list couldn't.
-->
<nav class="nav">
<!-- Mission control — bench-mode home (construct portfolio + loop).
Home sits first; hidden in classic UI via CSS (.nav-item--mission). -->
<a class="nav-item nav-item--mission" href="#mission" title="Mission control">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<rect x="3.5" y="3.5" width="7" height="7" rx="1"/>
<rect x="13.5" y="3.5" width="7" height="7" rx="1"/>
<rect x="3.5" y="13.5" width="7" height="7" rx="1"/>
<rect x="13.5" y="13.5" width="7" height="7" rx="1"/>
</svg>
</span>
<span>Mission control</span>
</a>
<!--
Turing — the LLM agent that drives the whole loop in plain
language, and the workbench HUB + DEFAULT view (2026-07-12,
superseding the 2026-06-04 Plasmid-Editor-as-hub decision made
before tool-calling existed). Embeds the chat panel
(assistant.html?embed=1) so the chrome stays. Full tool-calling
backend — fetch_sequence, design_variant_library,
design_crispr_guides, design_primers — drives the four tools
below; they're its verbs, not its peers. Rendered as the rail's
lead block to make that hierarchy visible.
-->
<a class="nav-item nav-lead active" id="navTuring" href="#turing" title="Turing">
<span class="nav-icon nav-lead-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 11.5a8.5 8.5 0 0 1-12.1 7.7L3.5 20.5l1.3-5.4A8.5 8.5 0 1 1 21 11.5z"/>
<path d="M9.2 10.6l1.6 2 2.8-3.2"/>
</svg>
</span>
<span class="nav-lead-text">
<span class="nav-lead-word">Turing</span>
<span class="nav-lead-sub">Engineer in plain language</span>
</span>
</a>
<!--
THE LOOP — the four tools as one ordered workflow, not a
scattered tab-list. A hairline spine (.nav-loop::before) ties
them together; each name carries its loop phase on the right.
· Directed Evolution — ESM-2 zero-shot variant libraries (Design)
· Plasmid Editor — map / annotate / clone a construct (Build)
· CRISPR — knockout & base-edit guide design (Edit)
· Primer Analysis — score & rank candidate primers (Verify)
All sign-in gated like the rest; hrefs unchanged so showRoute's
active-state + the mobile drawer-close still key off .nav-item.
-->
<div class="nav-loop">
<p class="nav-loop-lbl">The loop</p>
<a class="nav-item nav-step" href="#design" data-analytics="nav-design" title="Directed Evolution">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M7 3c0 4.5 10 6 10 9s-10 4.5-10 9"/>
<path d="M17 3c0 4.5-10 6-10 9s10 4.5 10 9"/>
<path d="M8.5 6.5h7M8.5 17.5h7M7.6 9.5h8.8M7.6 14.5h8.8"/>
</svg>
</span>
<span class="nav-step-nm">Directed Evolution</span>
<span class="nav-step-ph">Design</span>
</a>
<a class="nav-item nav-step" href="#plasmid" data-analytics="nav-plasmid" title="Plasmid Editor">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="8"/>
<path d="M12 4v2.6M20 12h-2.6M12 20v-2.6M4 12h2.6"/>
</svg>
</span>
<span class="nav-step-nm">Plasmid Editor</span>
<span class="nav-step-ph">Build</span>
</a>
<a class="nav-item nav-step" href="#crispr" data-analytics="nav-crispr" title="CRISPR">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="6" cy="6" r="2.6"/>
<circle cx="6" cy="18" r="2.6"/>
<path d="M8.4 7.8L20 18M8.4 16.2L20 6M11 12l-2.6 1.6"/>
</svg>
</span>
<span class="nav-step-nm">CRISPR</span>
<span class="nav-step-ph">Edit</span>
</a>
<a class="nav-item nav-step" href="#primers" data-analytics="nav-primers" title="Primer Analysis">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10.5" cy="10.5" r="6.5"/>
<path d="M20 20l-4.7-4.7"/>
</svg>
</span>
<span class="nav-step-nm">Primer Analysis</span>
<span class="nav-step-ph">Verify</span>
</a>
</div>
<!--
Utilities — quiet secondary tier, pushed to the foot of the nav
(margin-top:auto) so it sits just above the account row without
competing with the workflow.
· Start here — goal router modal (role=button, not a route).
· My designs — saved-work hub modal (2026-05-30); anonymous
click routes to sign-in via the shared guard.
· Documentation — how it all works.
(Library/History tabs were removed 2026-05-27 — they read the
Space-shared output dir, a single-user relic; the per-user
dashboard on the landing site replaces them.)
-->
<div class="nav-utils">
<a class="nav-item nav-util" id="navStartHere" role="button" tabindex="0" title="Start here">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<path d="M15.5 8.5l-2.1 4.9-4.9 2.1 2.1-4.9 4.9-2.1z"/>
</svg>
</span>
<span>Start here</span>
</a>
<a class="nav-item nav-util" id="navMyDesigns" role="button" tabindex="0" title="My designs">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 7a2 2 0 0 1 2-2h3.5l2 2H19a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/>
</svg>
</span>
<span>My designs</span>
</a>
<a class="nav-item nav-util" id="navAtlas" role="button" tabindex="0" title="Field atlas">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<rect x="3.5" y="3.5" width="17" height="17" rx="1"/>
<path d="M9 3.5v17M15 3.5v17M3.5 9h17M3.5 15h17"/>
</svg>
</span>
<span>Field atlas</span>
</a>
<a class="nav-item nav-util" href="#docs" title="Documentation">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 3.5h8L18.5 8v12.5H6z"/>
<path d="M13.5 3.5V8h4.5M9 12.5h6M9 16h4"/>
</svg>
</span>
<span>Documentation</span>
</a>
</div>
</nav>
<!--
Account footer — docked at the FOOT of the sidebar (the .nav above
is flex:1, so this pins to the bottom) the way Claude's account
area sits at the bottom of its rail. Two mutually-exclusive states,
toggled by auth.js (_renderSidebarAccount):
• Anonymous → the "Sign in" row (#sidebarSignin), shown by default
so it renders even before auth.js runs (and with JS disabled).
• Signed in → the account row (#sidebarAccount) with the user's email.
The sign-in link uses target="_top" so it leaves the iframe and loads
/signin on turingdna.com rather than framing it inside the engine.
The account menu opens upward; "Sign out" asks the wrapper to drop the
Supabase session, then lands on /signin.
-->
<button class="theme-toggle sidebar-theme" id="themeToggle" type="button" aria-label="Toggle light and dark theme" aria-pressed="false" title="Toggle light / dark">
<svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="4.5"/>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
<a class="sidebar-signin" id="sidebarSignin" href="https://turingdna.com/signin/?from=app" target="_top">
<span class="acct-avatar acct-avatar--ghost" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
<polyline points="10 17 15 12 10 7"/>
<line x1="15" y1="12" x2="3" y2="12"/>
</svg>
</span>
<span class="acct-meta">
<span class="acct-name">Sign in</span>
<span class="acct-plan">Save your designs</span>
</span>
</a>
<div class="sidebar-account" id="sidebarAccount" hidden>
<button type="button" class="acct-trigger" id="acctTrigger" aria-haspopup="menu" aria-expanded="false">
<span class="acct-avatar" id="acctAvatar" aria-hidden="true"></span>
<span class="acct-meta">
<span class="acct-name" id="acctName">&nbsp;</span>
<span class="acct-plan">Signed in</span>
</span>
<svg class="acct-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="18 15 12 9 6 15"/>
</svg>
</button>
<div class="acct-menu" id="acctMenu" hidden role="menu" aria-label="Account">
<p class="acct-menu-email" id="acctMenuEmail"></p>
<!--
Plan + usage. Nothing in the app said what plan you were
on, what it included, or what you'd used — someone who
had decided to pay could not find out how. Filled by
auth.js from /api/usage; stays hidden if the numbers
aren't available rather than showing a confident zero.
-->
<div class="acct-usage" id="acctUsage" hidden></div>
<button type="button" class="acct-menu-item" id="acctSignOut" role="menuitem">Sign out</button>
</div>
</div>
</aside>
<!-- ============================== MAIN ============================== -->
<main class="content">
<!--
Topbar trimmed to just the mobile hamburger. Was carrying
a breadcrumb to nowhere (Design › New library, when there's
nothing above "Design" to navigate back to) and a Linear-
clone ⌘K search trigger that wasn't wired to anything
useful. Both read as SaaS scaffolding and have been removed.
The hamburger stays for the mobile drawer toggle.
-->
<header class="topbar">
<button class="nav-toggle" id="navToggle" type="button" aria-label="Open navigation" aria-controls="sidebar" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
<div class="topbar-titles">
<h1 class="topbar-title" id="topbarTitle">Plasmid Editor</h1>
<span class="topbar-sub" id="topbarSub">Map, annotate &amp; clone your construct</span>
</div>
<!-- Theme toggle relocated to the sidebar footer, by the account chip
(see #themeToggle in the aside) — it now stays reachable in the bench
view, where this topbar is hidden. -->
</header>
<!-- =================================================================
Research-use-only strip — always visible immediately under the
topbar so the framing is in front of the user during every
action (paste, edit, synthesize, download). The "Why?" button
re-opens the first-use disclaimer modal so the user can re-read
the full text at any time. This strip pairs with the
click-through modal in #firstUseModal below; together they're
the in-UI half of the "research use only" legal narrative.
================================================================= -->
<div class="research-strip" role="status" aria-label="Research use disclaimer">
<span class="research-strip-dot" aria-hidden="true"></span>
<!-- .ruo-long is hidden once the user has ACCEPTED the same
disclaimer in the first-use modal (body[data-ruo=accepted]),
so the strip shrinks to a quiet mark instead of repeating
the full sentence on every page forever. -->
<span class="research-strip-text">
<strong>Research use only.</strong>
<span class="ruo-long">Computational predictions — visually verify every sequence before ordering DNA.</span>
</span>
<button class="research-strip-link" id="researchStripWhy" type="button">Why?</button>
</div>
<!-- Scrim covering the workspace while the sidebar drawer is open
on mobile. Tap to dismiss. Hidden on tablet+ via CSS. -->
<div class="sidebar-scrim" id="sidebarScrim" hidden></div>
<!-- ===================================================================
BENCH chrome (Phase 2 of the construct re-architecture, 2026-07-13).
A construct-centric two-pane workspace. All elements below are
inert until data-ui="bench" AND data-bench="open" (set by
openConstruct in app.js); classic UI never shows them. The tool
views themselves are NOT moved — CSS repositions Turing into a
fixed left rail and the workspace into the right canvas, so every
existing controller keeps working untouched.
=================================================================== -->
<div class="bench-strip" id="benchStrip" hidden>
<button class="bench-back" id="benchBack" type="button" aria-label="Back to mission control">&larr; Mission</button>
<div class="bench-specimen">
<div class="bench-spec-thumb" aria-hidden="true"><svg viewBox="0 0 26 26" fill="none" stroke="currentColor" stroke-width="1.2"><path d="M6 4c8 4 6 14 14 18M6 22c8-4 6-14 14-18"/></svg></div>
<div><div class="bench-spec-nm" id="benchSpecName">Construct</div><div class="bench-spec-sub" id="benchSpecSub"></div></div>
</div>
<div class="bench-spine" id="benchSpine" role="list" aria-label="Loop phase">
<span class="bench-ph" data-ph="0" role="listitem"><span class="bench-ph-n">01</span><span class="bench-ph-t">Design</span></span>
<span class="bench-ph" data-ph="1" role="listitem"><span class="bench-ph-n">02</span><span class="bench-ph-t">Build</span></span>
<span class="bench-ph" data-ph="2" role="listitem"><span class="bench-ph-n">03</span><span class="bench-ph-t">Edit</span></span>
<span class="bench-ph" data-ph="3" role="listitem"><span class="bench-ph-n">04</span><span class="bench-ph-t">Learn</span></span>
</div>
</div>
<div class="bench-canvashead" id="benchCanvasHead" hidden>
<nav class="bench-tabs" id="benchTabs" aria-label="Construct artifacts">
<button class="bench-tab" data-route="structure" data-analytics="bench-tab-structure" type="button">Structure</button>
<button class="bench-tab" data-route="design" data-analytics="bench-tab-design" type="button">Library</button>
<button class="bench-tab" data-route="plasmid" data-analytics="bench-tab-plasmid" type="button">Map</button>
<button class="bench-tab" data-route="crispr" data-analytics="bench-tab-crispr" type="button">Guides</button>
<button class="bench-tab" data-route="primers" data-analytics="bench-tab-primers" type="button">Primers</button>
</nav>
</div>
<div class="workspace">
<!-- =============================== MISSION CONTROL (bench-mode home)
The construct portfolio + loop flywheel + a Turing entry.
Only reachable in the opt-in `bench` UI (data-ui="bench");
populated lazily by initMission() in app.js from the same
saved-work list endpoints "My designs" uses. -->
<section class="view view--mission" data-view="mission" hidden aria-label="Mission control">
<div class="mc">
<p class="mc-hello micro" id="mcHello">Mission Control</p>
<h1 class="mc-h">What are we engineering today?</h1>
<!--
ONE CHAT. There used to be a second composer here, under
the headline, four hundred pixels from the Turing rail's
own — two text fields that did the same thing, with
nothing on screen saying which one you were in. Typing
here made words appear over there.
The rail is the conversation now, everywhere, always. All
this needs to be is the door to it: the headline, and one
unmistakable way in. Search moved to ⌘K, which is a real
overlay reachable from every route rather than an input
that only existed on this page.
-->
<div class="mc-enter">
<button type="button" class="mc-enter-cta" id="mcStartChat">
Tell Turing what you're engineering
<span class="mc-enter-ar" aria-hidden="true">&rarr;</span>
</button>
<button type="button" class="mc-enter-find" id="mcOpenFinder">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
Find a construct, run, or tool
<kbd>&#8984;K</kbd>
</button>
</div>
<!-- constructs as intelligent status cards — the operator's
own work LEADS the home (moved above telemetry+loop
2026-07-21 audit fix: a returning user comes back for
THEIR constructs, not the engine's receipts, which now
sit below). -->
<section class="mc-sec">
<div class="mc-sec-h"><p class="micro" id="mcCxCount">Your constructs</p><a class="mc-sec-r" id="mcAllDesigns" role="button" tabindex="0">Open “My designs” &rarr;</a></div>
<div class="mc-grid" id="mcPortfolio"><p class="muted">Loading…</p></div>
</section>
<!--
"Try another target" — the worked examples, kept reachable.
They used to render only in the cold-start empty state, so
the three most persuasive things on the page vanished the
moment a user had any work of their own. Populated (and
rotated) by TDMission.load(); left empty on a cold start,
where the portfolio slot is already leading with them.
-->
<section class="mc-sec" id="mcTryOne"></section>
<!-- the loop, as real per-user telemetry (Learn is the hero) -->
<section class="mc-sec">
<div class="mc-sec-h"><p class="micro">Your loop — the flywheel turning</p><a class="mc-sec-r" href="#docs" role="button">How it works &rarr;</a></div>
<div class="mc-loop" id="mcFlywheel"></div>
</section>
<!-- engine telemetry: the PUBLIC receipts + commons. The
trust block sits BELOW the user's own work (was first;
moved down 2026-07-21). Rendered by JS from /api/mission. -->
<div class="mc-tele" id="mcTele" aria-label="Engine status"></div>
</div>
</section>
<!-- =============================== STRUCTURE view
The Bench's Structure tab. Populated automatically when
Turing resolves a named protein (fold_structure → a PUBLIC
AlphaFold-DB model; only the gene symbol ever leaves the
Space, never a sequence). Mounts the same resilient Mol*
viewer the identify/AlphaFold embeds use. De-novo folding of
a user's own VARIANT stays an explicit, separately-consented
action (it would POST their sequence to a third party). -->
<section class="view view--structure2" data-view="structure" hidden aria-label="Predicted structure">
<div class="struct">
<header class="struct-head">
<div>
<p class="card-kicker" id="structKicker">&sect; Structure</p>
<h2 id="structTitle">Predicted structure</h2>
<p class="card-sub" id="structSub">Turing loads the wild-type model as soon as you name a protein.</p>
</div>
<div class="struct-head-actions">
<a class="ghost" id="structEntryLink" target="_blank" rel="noopener" hidden>Open AlphaFold entry</a>
<button class="ghost" id="structFullBtn" type="button" aria-pressed="false">Fullscreen</button>
</div>
</header>
<div class="struct-viewer" id="structViewer">
<div class="struct-empty" id="structEmpty">
Name a protein in the conversation — “I want to engineer MC1R” — and its
predicted structure appears here.
</div>
</div>
<p class="struct-note" id="structNote" hidden></p>
</div>
</section>
<!-- =============================== DESIGN view (default) -->
<section class="view view--design" data-view="design">
<!--
Hero. Was carrying a generic eyebrow kicker
("PLM-GUIDED DIRECTED EVOLUTION") AND a 4-step
pipeline-cards row (01 Parse & translate → 02 Zero-shot
score → 03 Combinatorial search → 04 Codon optimize)
on the right. Both were AI-generated SaaS bloat. The
eyebrow said nothing the headline doesn't; the pipeline
chips were decorative "look how complete I am" boxes.
Replaced with a § Begin kicker matching the landing's
editorial register and a single-column layout.
-->
<section class="hero" id="hero">
<p class="hero-kicker">&sect; Begin</p>
<h1>Design a smart mutation library</h1>
<p class="hero-lede">
Provide a wild-type sequence — get a library of multi-mutant variants
ranked by predicted fitness, codon-optimized for your expression host,
and ready to order from your synthesis vendor. From there: build the
construct in the Plasmid Editor, edit it with CRISPR if the project
calls for it, and log what you measure to design a sharper round two.
</p>
</section>
<!--
Input section. Card headers throughout used to carry a
boxy step-num badge (1, 2, 3 + done-checkmark) treating
a research tool like Stripe Checkout. Replaced with the
same § N kicker pattern as the landing chapters so the
app reads as the next chapter of the same publication.
-->
<section class="card" id="inputCard">
<header class="card-header">
<div class="card-header-left">
<p class="card-kicker">&sect; 1 &middot; Sequence</p>
<h2>Provide a wild-type</h2>
<p class="card-sub">FASTA, SnapGene, GenBank, raw DNA, or raw protein. Auto-detected.</p>
</div>
<button class="ghost" type="button" id="deExampleBtn" data-analytics="de-example"
title="Loads GFP — a classic directed-evolution target">Try an example</button>
</header>
<div class="input-tabs" role="tablist">
<button class="tab active" data-tab="upload" role="tab">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/>
</svg>
Upload file
</button>
<button class="tab" data-tab="paste" role="tab">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
</svg>
Paste
</button>
</div>
<div class="tab-panel active" data-panel="upload">
<label class="dropzone" id="dropzone">
<input type="file" id="fileInput" hidden
accept=".fa,.fasta,.fna,.faa,.pep,.dna,.gb,.gbk,.genbank,.embl,.txt" />
<div class="dz-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M12 12v9"/><path d="m16 16-4-4-4 4"/>
</svg>
</div>
<div class="dz-copy">
<strong>Drop a sequence file</strong>
<span>or click to browse</span>
</div>
<!-- The format chip row that used to live here
("FASTA · SnapGene · GenBank · EMBL · plain
DNA / protein") was redundant — the same
list is already in the card-sub directly
above this dropzone. Removed. -->
</label>
</div>
<div class="tab-panel" data-panel="paste">
<div class="paste-shell">
<div class="paste-gutter" id="pasteGutter"><span>1</span></div>
<textarea id="pasteArea" rows="9" spellcheck="false" wrap="off"
autocorrect="off" autocapitalize="off" autocomplete="off" inputmode="text"
placeholder="Paste FASTA, raw DNA (ATGC…), or raw protein (MVSKG…). FASTA headers, whitespace, and stop asterisks all OK."></textarea>
</div>
<div class="paste-actions">
<span class="paste-stats" id="pasteStats">0 chars</span>
<button id="pasteSubmit" class="primary-ghost">Use this sequence</button>
</div>
</div>
<div class="preview" id="preview" hidden>
<div class="preview-meta">
<div>
<span class="meta-label">Identifier</span>
<span class="meta-val" id="metaIdent"></span>
</div>
<div>
<span class="meta-label">Detected</span>
<span class="meta-val pill" id="metaKind"></span>
</div>
<div>
<span class="meta-label">Length</span>
<span class="meta-val" id="metaLen"></span>
</div>
</div>
<div class="preview-seq" id="metaPreview"></div>
<!-- NCBI BLAST identification (opt-in). Sequence leaves
this machine and goes to NCBI's public BLAST
servers. Cached by SHA256(protein) — same input
returns instantly. -->
<div class="identify-row" id="identifyRow">
<button class="ghost identify-btn" id="identifyBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
</svg>
Identify via NCBI BLAST
</button>
<span class="identify-hint">Sends your sequence to NCBI · 30–90 s · skip if your sequence is proprietary</span>
</div>
<!-- BLAST result panel (lives inline below the
Identify button). The first-use CONSENT panel
is no longer rendered here — it pops as a
centered modal (#blastConsentModal at the
bottom of the page) so it stays in view
regardless of scroll position. -->
<div class="identify-panel" id="identifyPanel" hidden></div>
</div>
<div class="cds-picker" id="cdsPicker" hidden>
<h3>Multiple CDS features detected — pick the gene to evolve</h3>
<p class="muted">The longest CDS in a plasmid is usually the antibiotic resistance gene. Pick your gene of interest.</p>
<div class="cds-list" id="cdsList"></div>
</div>
</section>
<section class="card" id="settingsCard">
<header class="card-header">
<div class="card-header-left">
<p class="card-kicker">&sect; 2 &middot; Search</p>
<h2>Tune the parameters</h2>
<p class="card-sub">Sensible defaults — change if you know what you're after.</p>
</div>
</header>
<div class="setting-grid">
<label class="setting">
<span class="setting-name">Variants to generate</span>
<span class="setting-hint">Top-K multi-mutants returned in the library.</span>
<input type="number" id="settingK" value="30" min="5" max="500" step="5" />
<!-- The cap disclosed WHERE THE NUMBER IS TYPED. A
reviewer set 30 here, asked Turing for a library
and got 10 rows; the only notice was one clause
in a banner in a different section of the page.
The numbers are the real ones from
dee/core/agent_tools.py (k clamped to 1–20,
default 10) and tests/test_chat_library_cap.py
fails if this text and that clamp drift apart. -->
<p class="setting-note">This box drives the <strong>Directed Evolution</strong> run
below, which honours it up to 500. A library designed <strong>in conversation
with Turing</strong> does not read this box: that path runs inside a single
reply rather than as a background job, so it returns <strong>10 variants by
default and at most 20</strong>.</p>
</label>
<label class="setting">
<span class="setting-name">Max mutations / variant</span>
<span class="setting-hint">Cap on simultaneous substitutions per variant.</span>
<input type="number" id="settingMax" value="5" min="1" max="20" />
</label>
<label class="setting">
<span class="setting-name">Top-percentile pool</span>
<span class="setting-hint">Keep mutations above this ΔLL percentile.</span>
<input type="number" id="settingPct" value="85" min="50" max="99" step="1" />
</label>
<label class="setting">
<span class="setting-name">Expression host</span>
<span class="setting-hint">Codon-usage table for reverse translation.</span>
<select id="settingHost">
<option value="e_coli">Escherichia coli</option>
<option value="yeast">Saccharomyces cerevisiae</option>
<option value="human">Homo sapiens</option>
</select>
</label>
<label class="setting">
<span class="setting-name">PLM model</span>
<span class="setting-hint">Larger = sharper scoring, slower on CPU.</span>
<select id="settingModel">
<option value="small">ESM-2 · 35M (default, fast)</option>
<option value="medium" disabled>ESM-2 · 650M — Pro · GPU (coming soon)</option>
<option value="large" disabled>ESM-2 · 3B — Pro · GPU (coming soon)</option>
</select>
</label>
</div>
</section>
<section class="card run-card" id="runCard">
<header class="card-header">
<div class="card-header-left">
<p class="card-kicker">&sect; 3 &middot; Generate</p>
<h2>Run the engine</h2>
<p class="card-sub">Scoring runs on our GPU. Your individual sequences are never shared, exposed, or reproduced; only anonymous, aggregated signals improve our models. <a href="https://turingdna.com/privacy/" target="_blank" rel="noopener">Privacy</a>.</p>
</div>
</header>
<button id="runBtn" type="button" class="primary primary-lg" data-analytics="de-run" disabled>
<span class="primary-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="6 3 20 12 6 21 6 3" fill="currentColor"/>
</svg>
</span>
<span class="primary-content">
<span class="primary-label">Generate smart library</span>
<span class="primary-sub">Load a sequence to enable</span>
</span>
</button>
<div class="progress-shell" id="progressShell" hidden>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-meta">
<span id="progressStatus">Starting…</span>
<span id="progressElapsed">0.0s</span>
</div>
<div class="progress-message" id="progressMessage"></div>
</div>
</section>
<!--
Empty state. Was carrying an unDraw-style placeholder
SVG (rectangles + check-circle) that read as the AI-y
"empty state illustration" cliché found on every SaaS
landing screen. Replaced with a quiet editorial block:
§ kicker + italic display title + lede. No illustration.
-->
<section class="empty-state" id="emptyState">
<p class="empty-kicker">&sect; Awaiting input</p>
<h3 class="empty-title">The library appears here.</h3>
<p class="empty-sub">Provide a sequence above and click Generate. Results render as a sortable variant table with mutation map, PCR primers, and one-click synthesis ordering.</p>
</section>
<!--
Results section. Was carrying a step-num done-checkmark
badge (treating results as "step 4 completed") and
download-menu items each had a tautological subtitle
(CSV — "Spreadsheet · universal"). Both removed. Format
names alone carry the meaning; users who don't know
what CSV is aren't the audience.
-->
<section class="card results-card" id="resultsCard" hidden>
<header class="card-header results-header">
<div class="card-header-left">
<p class="card-kicker">&sect; 4 &middot; Library</p>
<h2>Variants</h2>
<p class="card-sub" id="resultSummary"></p>
</div>
<div class="results-actions">
<div class="download-menu" id="downloadMenu">
<button class="ghost download-trigger" type="button" id="downloadTrigger">
Export
<span class="caret"></span>
</button>
<div class="download-menu-items" id="downloadMenuItems" hidden>
<a class="download-item" data-format="csv" data-analytics="export-csv">CSV</a>
<a class="download-item" data-format="xlsx" data-analytics="export-xlsx">Excel</a>
<a class="download-item" data-format="fasta" data-analytics="export-fasta">FASTA &middot; protein</a>
<a class="download-item" data-format="fasta-dna" data-analytics="export-fasta-dna">FASTA &middot; DNA</a>
<a class="download-item" data-format="gb" data-analytics="export-gb">GenBank</a>
<a class="download-item" data-format="json" data-analytics="export-json">JSON</a>
<a class="download-item" data-format="tsv" data-analytics="export-tsv">TSV</a>
</div>
</div>
</div>
</header>
<details class="how-to-read">
<summary>How to read this</summary>
<div class="how-to-read-body">
<p><strong>Fitness</strong> &mdash; the summed ESM-2 log-likelihood gain over wild-type (&Sigma;&Delta;LL) across the variant&rsquo;s mutations. Higher means the model finds that combination more evolutionarily plausible. It&rsquo;s a prior, not a verdict &mdash; screen experimentally.</p>
<p><strong>Mutations</strong> &mdash; substitutions vs. wild-type (WT&middot;position&middot;new, e.g. <code>W58L</code>). &nbsp;<strong>GC %</strong> &mdash; GC of the codon-optimized DNA (40&ndash;60% is PCR-friendly). &nbsp;<strong>Tm</strong> &mdash; primer melting temp. &nbsp;<strong>bp</strong> &mdash; amplicon length.</p>
<p><strong>Filter box:</strong> <code>C49</code> = variants mutating residue 49 &middot; <code>W58L</code> = that exact substitution &middot; <code>gc&gt;50</code>, <code>tm&gt;58</code>, <code>fitness&gt;2</code>, <code>bp&lt;800</code> = numeric ranges.</p>
<p class="how-to-read-note">Stay shallow &mdash; cap at 3&ndash;4 mutations per variant unless you have a structural reason.</p>
</div>
</details>
<!--
Stats-strip (5 KPI tiles: Top fitness / Position
coverage / Mean GC / Cooler primer Tm / Amplicon)
removed. It was THE single most "SaaS dashboard"
element in the entire app — variant-level stats
belong in the table; aggregate run quality belongs
in the methods paragraph below.
-->
<!-- Run metadata + auto-generated methods text. Filled by JS. -->
<div class="run-meta" id="runMeta" hidden></div>
<!-- Round-2 banner: set when a library was proposed from logged results. -->
<div class="round2-banner" id="round2Banner" hidden></div>
<!-- "What Turing learned" — the round-2 re-rank + plain-language
reasons (dee.core.active_learning components/narrate, surfaced
via surrogate.pool_deltas). Only populated + shown when the
surrogate actually learned something (enough measurements +
real spread) — see _renderLearnedPanel in app.js. -->
<section class="learned-panel" id="learnedPanel" hidden>
<p class="card-kicker">&sect; What Turing learned</p>
<!-- These are SINGLE-substitution scores (dee/server.py
_de_round2_library reads m.delta_ll per mutation),
so the unit is ΔLL — not the table's summed ΣΔLL.
Labelling them the same would imply the two columns
are on one scale, and they are not. -->
<p class="unit-legend unit-legend--tight">Per-substitution score, <strong>&Delta;LL</strong> &mdash; ESM-2&rsquo;s prior for that one mutation, before &rarr; after your measured results were folded in. Not the same scale as the table&rsquo;s summed &Sigma;&Delta;LL.</p>
<div class="learned-list" id="learnedList"></div>
</section>
<!-- Predicted-vs-measured calibration — client-side only, built
from the round-1 Predicted_Fitness_Score already in the table
matched against what the user just typed into the round-2
panel. See _renderCalibration in app.js. -->
<section class="calib-panel" id="calibPanel" hidden>
<p class="card-kicker">&sect; How well round 1 predicted your results</p>
<div class="calib-body" id="calibBody"></div>
</section>
<!-- Mutation map (lollipop chart).
The chip legend that used to sit here (Low / Med /
High dots) was deleted as dashboard scaffolding and
its explanation moved into the description below —
which then described a three-step colour ramp that
encoded a signed, continuous quantity. #mutmapKey is
the replacement, and it is built by renderMutationKey()
from the same colour function the dots use, so the
key and the chart cannot disagree. -->
<div class="mutmap-card" id="mutmapCard">
<div class="mutmap-head">
<h3>Mutation landscape</h3>
<!-- Colour is the MEAN attributed ΔLL, signed —
not the sum. The sum grew with the variant
count that bar height already encodes, so the
two channels said the same thing twice and
neither said the direction. The full key,
with units and domain, is rendered into
#mutmapKey below. -->
<p class="muted">Where mutations land across the protein. <strong>x</strong> = residue position (1&ndash;<span id="mutmapLen">N</span>) &middot; <strong>bar height</strong> = variants sharing that position &middot; <strong>colour</strong> = the mean &Delta;LL there, signed, in ESM-2 log-likelihood units. Full scale and domain in the key below.</p>
</div>
<div class="mutmap-canvas" id="mutmapCanvas"></div>
<div class="mutmap-key" id="mutmapKey"></div>
</div>
<!-- ================= INTERACTION RADAR (Pillar 1) =================
Zero-shot epistasis: the ranked library scores each mutation
ALONE (additive). This re-scores each multi-mutant in context
with ESM-2 and flags where substitutions antagonize — catching
a dud combination before it's synthesized. Opt-in (one forward
pass per variant), signed-in only. Wired by initRadar()/
_renderRadar() in app.js against /api/de/epistasis. -->
<section class="radar-card" id="radarCard" hidden>
<div class="radar-head">
<div class="radar-head-txt">
<p class="card-kicker">&sect; Interaction Radar</p>
<h3>Will these mutations play well together?</h3>
<p class="muted">Your library ranks each substitution on its own. This checks the <em>combinations</em> — ESM-2 re-scores every multi-mutant in context and flags where mutations antagonize, before you spend a cent synthesizing a dud.</p>
</div>
<button class="ghost" type="button" id="radarBtn">Analyze interactions &rarr;</button>
</div>
<div class="radar-status" id="radarStatus" hidden></div>
<div class="radar-list" id="radarList"></div>
<p class="radar-foot muted" id="radarFoot" hidden></p>
</section>
<!-- The Fitness column's unit, stated where the column is,
not only in a hover title and not only inside the
collapsed "How to read this" block above. A reviewing
scientist asked "what does a fitness score of 1.0
represent? 2/3 is over and 1/3 is under, but none are
negative" — all three parts of that are answered here:
the unit, the absence of a fixed zero, and why a
top-ranked slice is not expected to contain negatives.
Styling lives in trace.css (app.css is off-limits to
this change set). -->
<p class="unit-legend" id="fitnessLegend">
<strong>Fitness = &Sigma;&Delta;LL</strong> &mdash; the ESM-2 log-likelihood
change vs.&nbsp;wild type, <em>summed</em> over this variant&rsquo;s substitutions.
Unnormalised: no fixed zero and no maximum, and because the table shows the
top-ranked slice of the search, negatives rarely survive into it.
Ranks variants <em>within this library</em> &mdash; not across proteins or runs.
</p>
<!-- Filter bar — instant client-side filter over the rendered table. -->
<div class="table-toolbar">
<div class="filter-input-wrap">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
</svg>
<input type="text" id="variantFilter" placeholder="Filter by mutation (e.g. C49), variant ID, or GC range" />
<button type="button" class="filter-clear" id="filterClear" hidden aria-label="Clear filter">×</button>
</div>
<span class="table-count" id="tableCount"></span>
</div>
<div class="result-table-wrap">
<table class="result-table" id="resultTable">
<thead>
<tr>
<th class="sortable" data-sort="rank" title="Rank by predicted fitness (1 = best).">Rank <span class="sort-ind"></span></th>
<th class="sortable" data-sort="mutations" title="Substitutions vs. the wild-type protein, in the format WT_aa-position-new_aa.">Mutations <span class="sort-ind"></span></th>
<!-- The unit rides in the header itself. It was title-only
since May 2026, which means it did not exist for anyone
who didn't hover the right 60px of the page. -->
<th class="num sortable" data-sort="fitness" title="Cumulative ΣΔLL from ESM-2: sum of log-likelihood improvements over WT across all substitutions in this variant.">Fitness <span class="th-unit">ΣΔLL</span> <span class="sort-ind"></span></th>
<th class="num sortable" data-sort="gc" title="GC content of the codon-optimized DNA. PCR-friendly range: 40–60%.">GC % <span class="sort-ind"></span></th>
<th class="num sortable" data-sort="tm" title="Cooler of the two PCR primer Tm values (nearest-neighbor, standard salt). Annealing = Tm − 5°C for Q5/Phusion.">Tm (°C) <span class="sort-ind"></span></th>
<th class="num sortable" data-sort="bp" title="Amplicon length in base pairs. Same for every variant in this library (length-preserving substitutions only).">bp <span class="sort-ind"></span></th>
<th class="expand-col"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<!-- Learning flywheel: log wet-lab results → propose a
data-conditioned round 2 (Design→Build→Test→Learn). -->
<section class="round2-panel" id="round2Panel" hidden>
<header class="card-header">
<div class="card-header-left">
<p class="card-kicker">&sect; Learn from results</p>
<h3>Tested these on the bench? Teach the engine.</h3>
<p class="card-sub">Enter a measured value for the variants you assayed (any
number &mdash; activity, expression, stability; higher = better). Leave the
rest blank. TuringDNA fits a model on your results and proposes a smarter
round&nbsp;2 &mdash; conditioned on your data, not just the prior.</p>
</div>
</header>
<div class="round2-list" id="round2List"></div>
<div class="round2-status" id="round2Status" hidden></div>
<div class="round2-actions">
<button class="ghost" type="button" id="round2SaveBtn">Save results</button>
<button class="primary" type="button" id="round2Btn">Propose round&nbsp;2 &rarr;</button>
</div>
</section>
</section>
</section>
<!--
Library view. Was carrying the same step-num-done +
decorative table SVG icon pattern as the Design cards,
plus a redundant SVG icon on the Refresh button, plus
another placeholder illustration in the empty state.
All removed; pattern matches the Design view's § kicker
treatment. Refresh is a text-only ghost button now.
-->
<!-- Library + History views removed 2026-05-27. Replaced
by the per-user /dashboard/ page on the landing site
(task #98). See sidebar comment above for context. -->
<!-- ═════════════════════════════ CRISPR view (Cas9 knockout) ═══
Paste a gene → ranked SpCas9 sgRNAs with on-target
scoring. Sign-in gated server-side; anonymous users
see the same layout with the submit button disabled
and a "Sign in to design guides" overlay.
════════════════════════════════════════════════════════ -->
<section class="view view--crispr" data-view="crispr" hidden>
<section class="hero" id="crisprHero">
<p class="hero-kicker">&sect; Begin &middot; CRISPR</p>
<h1>Design a CRISPR edit.</h1>
<p class="hero-lede">
Paste a gene or region. Get ranked sgRNAs for a
SpCas9 / Cas12a knockout &mdash; or switch to
<strong>base&nbsp;editing</strong> to install a precise
C&rarr;T / A&rarr;G change (including premature-stop
knockouts) with predicted amino-acid outcomes. Free for
any account. If a project calls for an edit beyond what a
variant library covers &mdash; a knockout, a precise base
change &mdash; design it here, on the same construct.
</p>
<p class="hero-meta crispr-disclaimer">
On-target scoring is heuristic (Doench-style sequence
features). <strong>Off-target is screened here</strong> &mdash;
choose an organism and your top guides are searched
against the genome and CFD-scored. Coverage is the
complete genome for E.&nbsp;coli, and coding sequence
for human and mouse; intronic and intergenic sites
fall outside that index.
</p>
</section>
<section class="card" id="crisprInputCard">
<header class="card-header">
<div class="card-header-left">
<p class="card-kicker">&sect; 1 &middot; Target</p>
<h2>Paste your DNA</h2>
<p class="card-sub">
The gene or region you want to edit. ACGT only;
FASTA headers OK; up to 1 Mbp.
</p>
</div>
<button class="ghost" type="button" id="crisprExampleBtn"
title="Loads the GFP coding sequence as a knockout target">Try an example</button>
</header>
<!-- Phase 3 (M2): paste-anything resolver. Type a gene
symbol (TP53), an accession (ENST… / NM_…), or just
paste DNA below. Resolving a symbol/accession fills
the box and sets the gene context that base-edit
amino-acid consequences and the structure view use.
Only the (organism, identifier) is sent for lookups —
a pasted sequence never leaves the Space. -->
<div class="crispr-resolve-row">
<input
id="crisprResolveInput"
class="crispr-context-input crispr-resolve-input"
type="text"
maxlength="64"
autocomplete="off"
autocapitalize="characters"
spellcheck="false"
placeholder="Gene symbol (TP53), accession (ENST… / NM_…), or paste DNA below" />
<button id="crisprResolveBtn" type="button" class="ghost">Fetch</button>
</div>
<div class="crispr-resolve-chip" id="crisprResolveChip" hidden></div>
<!-- Same line-numbered gutter pattern as the
directed-evolution paste textarea (see #pasteGutter).
Gutter spans render via app.js renderCrisprGutter()
on every input event; scroll syncs both ways. -->
<div class="paste-shell">
<div class="paste-gutter" id="crisprPasteGutter"><span>1</span></div>
<textarea
id="crisprInput"
class="crispr-input"
rows="9"
spellcheck="false"
wrap="off"
autocorrect="off"
autocapitalize="off"
autocomplete="off"
placeholder="Paste DNA (ATGC…). FASTA headers and whitespace are OK."></textarea>
</div>
<!-- Enzyme picker — Phase 1. SpCas9 (NGG PAM, 20 nt
spacer) is the default + most common. Cas12a
(TTTV PAM, 23 nt spacer) added for AT-rich
regions where Cas9 PAMs are sparse. Hidden
controls — single line, mono labels, the same
visual register as the cloning-designer
enzyme picker on the directed-evolution side. -->
<div class="crispr-enzyme-row">
<span class="crispr-enzyme-label">Nuclease</span>
<label class="crispr-enzyme-opt">
<input type="radio" name="crisprEnzyme" value="cas9" checked />
<span>SpCas9 <em>· NGG PAM</em></span>
</label>
<label class="crispr-enzyme-opt">
<input type="radio" name="crisprEnzyme" value="cas12a" />
<span>Cas12a <em>· TTTV PAM</em></span>
</label>
</div>
<!-- Phase 3 (M1): editing mode. Knockout = a Cas9
double-strand break repaired into frameshift
indels (the default). Base edit = a precise
single-base change (C→T via a cytosine base
editor, A→G via an adenine base editor) with NO
double-strand break — including premature-stop
"CRISPR-STOP" knockouts. Base editing is
SpCas9-only, so choosing it locks the nuclease. -->
<div class="crispr-enzyme-row crispr-mode-row">
<span class="crispr-enzyme-label">Mode</span>
<label class="crispr-enzyme-opt">
<input type="radio" name="crisprMode" value="knockout" checked />
<span>Knockout <em>· DSB + indels</em></span>
</label>
<label class="crispr-enzyme-opt">
<input type="radio" name="crisprMode" value="base_edit" />
<span>Base edit <em>· precise C&rarr;T / A&rarr;G</em></span>
</label>
</div>
<!-- Base-editor picker — shown only in base-edit
mode. Options come from /api/crispr/base-editors
on first switch. The amino-acid consequence
column fills in when a gene symbol + organism
is given or the pasted sequence is a coding
sequence (starts with ATG, ends in a stop). -->
<div class="crispr-context-row crispr-be-row" id="crisprBaseEditorRow" hidden>
<div class="crispr-context-field">
<label class="crispr-context-label" for="crisprBaseEditor">
Base editor <em>· cytosine or adenine</em>
</label>
<select id="crisprBaseEditor" class="crispr-context-input"></select>
</div>
<p class="crispr-be-note" id="crisprBaseEditorMeta"></p>
</div>
<!-- Organism + gene context. Both OPTIONAL — the tool
works on a pasted sequence with neither set. The
organism does double duty: it's the genome the
off-target search runs against AND the species used
to resolve a gene symbol above, so the label names
both jobs (a reviewer picked a symbol, saw only
"off-target genome", and thought they needed an
accession). Six genomes are live; see the dropdown. -->
<div class="crispr-context-row">
<div class="crispr-context-field">
<label class="crispr-context-label" for="crisprOrganism">
Organism <em>· for gene lookup &amp; off-target · optional</em>
</label>
<select id="crisprOrganism" class="crispr-context-input">
<option value="">None — skip genome search</option>
<optgroup label="Complete genome — finds off-targets anywhere">
<option value="ecoli">E. coli K-12 MG1655 · builds in seconds</option>
<option value="yeast">S. cerevisiae (R64-1-1) · builds in ~30 s</option>
<option value="worm">C. elegans (WBcel235) · builds once, ~3 min</option>
<option value="fly">D. melanogaster (BDGP6.46) · builds once, ~5 min</option>
</optgroup>
<optgroup label="Coding regions only — introns/intergenic not covered">
<option value="human">Homo sapiens (GRCh38) · builds once, ~3 min</option>
<option value="mouse">Mus musculus (GRCm39) · builds once, ~2 min</option>
</optgroup>
</select>
</div>
<div class="crispr-context-field">
<label class="crispr-context-label" for="crisprGeneSymbol">
Gene symbol <em>· optional, for exon context</em>
</label>
<input
id="crisprGeneSymbol"
class="crispr-context-input"
type="text"
maxlength="32"
placeholder="e.g. TP53, GAL1, cas9 — any organism"
autocapitalize="characters"
autocomplete="off"
spellcheck="false" />
</div>
</div>
<div class="paste-actions">
<span class="paste-stats" id="crisprInputStats">0 nt</span>
<button
id="crisprDesignBtn"
type="button"
class="primary primary-lg"
disabled>
<span class="primary-content">
<span class="primary-label">Design guides</span>
<span class="primary-sub">Paste a sequence to enable</span>
</span>
</button>
</div>
<!-- Phase 2C-2: progress shell matching the directed-
evolution pattern. Indeterminate (always-shimmering)
because the CRISPR API is synchronous — we can't
emit real percentage, but the moving stripe + status
text reassures the user that the request is alive,
especially during the ~60-90 s first build of the
human off-target index. -->
<div class="progress-shell crispr-progress" id="crisprProgressShell" hidden>
<div class="progress-bar">
<div class="progress-fill progress-fill-indeterminate"></div>
</div>
<div class="progress-meta">
<span id="crisprProgressStatus">Designing…</span>
<span id="crisprProgressElapsed">0.0s</span>
</div>
<div class="progress-message" id="crisprProgressMessage"></div>
</div>
<!-- The old inline #crisprSigninNotice was removed
2026-05-28 — replaced by the #crisprSigninModal
which pops centered (better than a card that
lives below the fold) and fires from BOTH
the sidebar tab click AND the backend 403. -->
</section>
<!-- Results card — hidden until /api/crispr/design returns. -->
<section class="card" id="crisprResultsCard" hidden>
<header class="card-header results-header">
<div class="card-header-left">
<p class="card-kicker">&sect; 2 &middot; Guides</p>
<h2>Ranked sgRNAs</h2>
<p class="card-sub" id="crisprResultsSub"></p>
</div>
<div class="results-actions">
<!-- Phase 3 (M4): save the current guide set to the
signed-in user's library (45-day retention) and
reopen past designs. -->
<button id="crisprSaveBtn" type="button" class="ghost">Save design</button>
<button id="crisprDesignsBtn" type="button" class="ghost">My designs</button>
<!-- Live Collision IP Radar (opt-in, per run). Scans recent
literature for the top guide's exact sequence. Only the
gene + 20-mer leave the Space — never the full design. -->
<button id="crisprRadarBtn" type="button" class="ghost"
title="Scan recent literature & preprints for overlap with your top guide (sends the gene + 20-mer only)">
<span aria-hidden="true"></span> IP radar
</button>
<div class="download-menu" id="crisprDownloadMenu">
<button id="crisprDownloadBtn" type="button" class="ghost download-trigger"
aria-haspopup="true" aria-expanded="false">
Export
<span class="caret"></span>
</button>
<div class="download-menu-items" id="crisprDownloadItems" hidden>
<a class="download-item" data-format="csv">CSV</a>
<a class="download-item" data-format="xlsx">Excel</a>
</div>
</div>
</div>
</header>
<!-- Provenance, on demand. Collapsed by default so it adds
no visual clutter, but every scored column can be
audited: exact formula, published basis, and the
point where the number stops being trustworthy.
Filled by TDMethods from GET /api/crispr/methods,
which mirrors dee/core/crispr.py. -->
<details class="how-to-read" id="crisprMethods">
<summary>How these numbers were computed</summary>
<div class="how-to-read-body" id="crisprMethodsBody">
<p class="muted">Loading methods…</p>
</div>
</details>
<!-- Auto-generated run parameters + copy-paste methods paragraph. -->
<div class="run-meta" id="crisprRunMeta" hidden></div>
<!-- Live Collision IP Radar banner mounts here (above the
results), populated by the radar button handler. -->
<div id="crisprRadarBanner" class="ip-radar-wrap" hidden></div>
<!-- Phase 2C-1: bulk-order strip. Click a vendor → copy all
oligo pairs as TSV to clipboard + open vendor site so the
user can paste into the order form. Vendor list comes from
/api/crispr/vectors. Moved OUT of the header (2026-05-30)
so it no longer squeezed the title into a one-word-per-line
column — now a full-width strip above the cloning picker. -->
<div class="crispr-vendor-row crispr-vendor-strip" id="crisprVendorRow" hidden></div>
<!-- Phase 2C-1: vector picker. Above the table but inside
the results card so the picker contextualizes the
oligo columns. Picking a vector re-renders the
Sense / Antisense columns with that vector's
overhangs. -->
<div class="crispr-vector-row" id="crisprVectorRow" hidden>
<label class="crispr-vector-label" for="crisprVector">
Cloning vector
</label>
<select id="crisprVector" class="crispr-vector-select"></select>
<span class="crispr-vector-meta" id="crisprVectorMeta"></span>
</div>
<div class="result-table-wrap">
<table class="result-table crispr-table">
<thead>
<tr>
<th class="rank-h">Rank</th>
<th>Strand</th>
<th class="num crispr-sortable" data-sort-key="position">Position</th>
<th>Spacer</th>
<th>PAM</th>
<!-- Composite is the ranking column —
on-target × (1 − 0.6 × self-off CFD).
The 0.6 is the engine's only tuning
constant and a judgement call, so it
is spelled out here, in the scope
strip, per guide in the why-panel, and
in the methods paragraph — a weight
that decides the sort order has to be
readable without asking us.
Always the primary sort. -->
<th class="num crispr-sortable" data-sort-key="composite_score" title="on-target × (1 − 0.6 × self-off CFD). The 0.6 off-target weight is our uncalibrated judgement call, not a published constant — expand a row to see the arithmetic for that guide. Click to sort.">Composite</th>
<th class="num crispr-sortable" data-sort-key="on_target_score" title="TuringDNA on-target activity score (Doench-inspired sequence-only features). 1.0 = expected strong cut. Click to sort.">On-target</th>
<!-- CFD-against-input is the Phase 1
differentiator. 0.00 = no other
plausible cut site in the input
(clean unique target). 1.00 = an
identical 20-mer + NGG appears
elsewhere — don't use this guide
unless you want both cut. -->
<th class="num crispr-sortable" data-sort-key="cfd_max_offtarget" title="Worst CFD score against any other NGG site in the pasted input. 0.00 = unique; 1.00 = exact repeat. Click to sort.">Self-off</th>
<th class="num col-ko crispr-sortable" data-sort-key="ko_efficacy" title="Predicted probability the indel produces a true loss-of-function knockout. Click to sort.">KO score</th>
<!-- Phase 2A: inDelphi-inspired prediction of the
single most likely repair outcome at this cut,
e.g. "-3 ATG (28%)" or "+1 T (32%)". The
dominant outcome usually accounts for 15-40%
of all repair events. -->
<th class="col-ko" title="Single most-likely repair outcome (inDelphi-inspired). The number is the predicted frequency of THIS outcome among all repair events. Assumes template-free end-joining repair and is NOT conditioned on your cell type.">Top indel</th>
<!-- Phase 2A: % of predicted repair outcomes that
introduce a frameshift. Higher = more likely
to produce a true loss-of-function allele. -->
<th class="num col-ko crispr-sortable" data-sort-key="frameshift_pct" title="% of predicted repair outcomes that introduce a frameshift (non-multiple-of-3 indel). Higher = stronger KO likelihood. Assumes template-free end-joining (MMEJ + NHEJ) with fixed constants and NO cell-type conditioning — rank guides with it, don't quote it as a rate for your line. Click to sort.">FS %</th>
<!-- Phase 2A: dominance of the SINGLE most-likely
outcome, as a percent. Direct biological
readout — "will I get one main edit product
or many?" 40%+ = clean, 25-40% = typical,
<25% = messy heterogeneous repair. -->
<th class="num col-ko crispr-sortable" data-sort-key="top_dominance_pct" title="Frequency of the single most-likely repair outcome. 40%+ = clean; <25% = messy. Same end-joining assumptions as FS % — no cell-type conditioning. Click to sort.">Dominance</th>
<!-- Phase 2A: does the spacer's editing window
(positions 4-8) hold a C or A? If yes, this
guide could ALSO be used for a point edit
via CBE or ABE base editors. -->
<th class="col-ko" title="If the spacer's positions 4-8 hold a C or A, the same guide could be used for a point edit via cytosine (CBE) or adenine (ABE) base editor — not just a knockout.">Base editor</th>
<!-- Phase 3 (M1) base-edit columns — shown only in
Base edit mode. -->
<th class="col-be" title="The single-base change(s) this editor installs inside its activity window, e.g. C5→T. Multiple targets = bystander edits.">Edit</th>
<th class="num col-be crispr-sortable" data-sort-key="be_editability" title="Strongest in-window editing activity for a target base (0–1). Higher = more likely. Click to sort.">Editability</th>
<th class="col-be" title="Amino-acid consequence of the edit when a reading frame is known (gene symbol given, or pasted sequence is a coding sequence). '* (stop gained)' = a DSB-free knockout via CRISPR-STOP.">AA change</th>
<th class="col-be" title="A clean edit changes a single base in the window; a bystander edit hits more than one. 'STOP' marks guides that install a premature stop codon (knockout by base editing).">Outcome</th>
<!-- Phase 2B-1: scored against the chosen organism's
indexed genome. Empty when no organism is
picked; "unique" when the organism was
searched and no real off-targets found.
COVERAGE IS NOT UNIFORM: E. coli, yeast, worm
and fly are indexed complete; human and mouse
are CODING SEQUENCE ONLY. The scope strip above
the table states which applied to this run,
sourced from offtarget.GENOME_SOURCES. -->
<th class="num" title="Worst CFD score against any site in the chosen organism's INDEXED genome. 'unique' = no off-target found above threshold; 0.5+ = strong off-target that may cleave (tooltip gives the location). Coverage varies by organism — human and mouse are coding sequence only, and only the top-ranked guides are screened. See the scope note above the table.">Genome off</th>
<!-- Phase 2B-1: only populated when the user
gave a gene symbol AND the input could be
aligned to the gene's CDS. Shows exon
number + splice/NMD warnings. -->
<th title="Exon-number, splice-site proximity, and NMD-zone prediction (only when a gene symbol is provided). 'NMD likely' = the predicted PTC is >50 nt before the last exon-exon junction, triggering nonsense-mediated decay and guaranteeing a null protein.">Exon context</th>
<!-- Phase 2C-1: annealable oligo pair for
cloning into the chosen vector. Two
columns so Sense and Antisense are
side-by-side; each cell has a Copy
button (single cell click → copy that
strand only). -->
<th title="Sense strand of the annealable oligo pair, ready to order from Twist/IDT and clone into the chosen vector after BsmBI/BbsI digestion.">Sense oligo</th>
<th title="Antisense strand of the annealable oligo pair. Order alongside the sense strand, anneal, ligate.">Antisense oligo</th>
<!-- Per-guide vendor buttons. Click → copies
THIS guide's sense+antisense oligo pair
to clipboard + opens vendor in a new tab.
Mirrors the DE page's per-variant
"Synthesize via X" row. -->
<th title="Order this guide's oligo pair from a synthesis vendor. Click → copies the sense + antisense oligos to your clipboard, opens the vendor in a new tab. Paste into their bulk-oligo order form.">Order</th>
<th class="num crispr-sortable" data-sort-key="gc_pct">GC%</th>
<th>Flags</th>
</tr>
</thead>
<tbody id="crisprTableBody"></tbody>
</table>
</div>
</section>
<!-- ===== LOG EDITING EFFICIENCY (feeds the cross-user on-target model) ===== -->
<section class="card primer-design-card" id="crisprOutcomeCard" hidden>
<div class="primer-results-head">
<span class="primer-results-title">Log editing efficiency</span>
</div>
<p class="field-hint">Tested these guides at the bench? Enter the measured editing efficiency
(% edits / indels). Your guides stay private — only <strong>anonymous, aggregated</strong>
patterns improve the on-target model for everyone.</p>
<div class="ce-list" id="crisprOutcomeList"></div>
<div class="primer-actions" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<button class="primary primary-lg" type="button" id="crisprOutcomeSave">Save results</button>
<span class="round2-status" id="crisprOutcomeStatus" hidden></span>
</div>
</section>
<!-- Lightweight in-card error banner — shown on
validation errors (sequence too short, no PAM
sites, etc.) without scrolling away. -->
<div class="error-banner" id="crisprError" hidden></div>
</section>
<!-- ===================== PRIMER PICKER ===================== -->
<section class="view view--primers" data-view="primers" hidden>
<header class="view-head">
<h1 class="view-title"><em>Primer Analysis</em></h1>
<p class="view-sub">Paste your candidate primers, BLAST them all in one go, and let the
fitness model rank them — best amplification of your template, fewest off-target matches.
No more BLASTing one primer at a time. Pairs well with Directed Evolution's output —
check or refine the PCR primers for the variants you're ordering.</p>
</header>
<section class="card primer-input-card">
<label class="field-label" for="primerInput">Candidate primers</label>
<p class="field-hint">One per line or FASTA. Optional names and forward/reverse tags
(e.g. <code>GeneA_F</code> / <code>GeneA_R</code>) — pairs are auto-detected and scored
as amplicons.</p>
<textarea id="primerInput" class="primer-textarea mono" rows="6" spellcheck="false"
placeholder=">GeneA_F&#10;ACAGTGACCTGAAGTCCATGC&#10;>GeneA_R&#10;TTGGCAGTCAACGTTACGGAC"></textarea>
<label class="field-label" for="primerTemplate">Template to amplify
<span class="field-opt">· optional, recommended</span></label>
<p class="field-hint">The DNA region you want to amplify. Stays on our server — used to
check each primer's binding site, uniqueness and the predicted amplicon size.</p>
<textarea id="primerTemplate" class="primer-textarea mono" rows="4" spellcheck="false"
placeholder="Paste the target DNA sequence (A/C/G/T)…"></textarea>
<label class="primer-toggle">
<input type="checkbox" id="primerScanNcbi" />
<span class="primer-toggle-text">
<strong>Run specificity scan (in-silico PCR)</strong>
<span class="field-hint">Searches the chosen genome and runs in-silico PCR to
predict every product your primers would make — intended <em>and</em> off-target —
right here, so you don't need a separate tool. Sequences go to NCBI BLAST
(~1–3 min); leave off if your primers are confidential.</span>
</span>
</label>
<div class="primer-organism-row" id="primerOrganismRow" hidden>
<label class="field-label" for="primerOrganism">Check specificity against</label>
<select id="primerOrganism" class="primer-select">
<option value="human" selected>Human — Homo sapiens</option>
<option value="mouse">Mouse — Mus musculus</option>
<option value="rat">Rat — Rattus norvegicus</option>
<option value="zebrafish">Zebrafish — Danio rerio</option>
<option value="yeast">Yeast — S. cerevisiae</option>
<option value="ecoli">E. coli</option>
<option value="all">All sequences (nt) — broad, noisier</option>
</select>
<label class="primer-subtoggle">
<input type="checkbox" id="primerStrict3p" />
<span>Strict 3′-end matching
<span class="field-hint">Only count off-target sites whose <strong>last 5 bases
match perfectly</strong> — the 3′ end that actually licenses extension. Fewer,
higher-confidence predictions; leave off to also catch borderline sites.</span></span>
</label>
</div>
<div class="primer-actions">
<button class="ghost" type="button" id="primerExample" title="Loads candidate GFP primers + a template to score">Try an example</button>
<button class="primary primary-lg" type="button" id="primerAnalyze">Analyse primers</button>
</div>
<div class="error-banner" id="primerError" hidden></div>
<div class="primer-warnings" id="primerWarnings" hidden></div>
</section>
<section class="card primer-results-card" id="primerResultsCard" hidden>
<div class="primer-results-head">
<span class="primer-results-title">Ranked primers</span>
<div class="primer-results-actions">
<div class="download-menu" id="primerDownloadMenu">
<button class="ghost download-trigger" type="button" id="primerDownloadBtn"
aria-haspopup="true" aria-expanded="false">Export <span class="caret"></span></button>
<div class="download-menu-items" id="primerDownloadItems" hidden>
<a class="download-item" data-format="csv">CSV</a>
<a class="download-item" data-format="xlsx">Excel</a>
</div>
</div>
<button class="ghost" type="button" id="primerSaveBtn">Save</button>
</div>
</div>
<details class="how-to-read">
<summary>How to read this</summary>
<div class="how-to-read-body">
<p><strong>Fitness</strong> &mdash; the combined score used to rank your candidates (higher = better pick): Tm, GC, 3&prime;-end stability and specificity.</p>
<p><strong>On template</strong> &mdash; whether the primer binds your template uniquely. <strong>Off-target products</strong> &mdash; extra amplicons in-silico PCR predicts beyond the intended one (0 = specific). <strong>Tm</strong> &mdash; nearest-neighbor melting temp; aim for a matched pair.</p>
<p class="how-to-read-note">These primers are yours &mdash; TuringDNA scores and ranks them; it doesn&rsquo;t invent them.</p>
</div>
</details>
<!-- Auto-generated run parameters + copy-paste methods paragraph. -->
<div class="run-meta" id="primerRunMeta" hidden></div>
<div class="primer-scan-status" id="primerScanStatus" hidden></div>
<div class="primer-best" id="primerBest"></div>
<div class="primer-amplicons" id="primerAmplicons"></div>
<div class="primer-products" id="primerProducts"></div>
<div class="primer-list" id="primerList"></div>
<p class="primer-foot">The specificity scan runs full in-silico PCR against your chosen
genome — the same check NCBI Primer-BLAST performs — so the result is complete here.
Predictions are a strong guide; as always, validate critical results at the bench.</p>
</section>
<!-- ============== DE NOVO PRIMER DESIGN ============== -->
<section class="card primer-design-card" id="primerDesignCard">
<div class="primer-results-head">
<span class="primer-results-title">Design primers from a template</span>
<button class="ghost" type="button" id="designExample" title="Load an example template">Try an example</button>
</div>
<p class="field-hint">Paste the DNA you want to amplify (and, optionally, the region the
product must span). TuringDNA designs Tm-matched forward/reverse pairs entirely
on-server — no NCBI, nothing leaves.</p>
<label class="field-label" for="designTemplate">Template &middot; DNA or FASTA</label>
<textarea id="designTemplate" class="primer-textarea mono" rows="4" spellcheck="false"
placeholder="Paste DNA (A/C/G/T) or a FASTA record…"></textarea>
<div class="design-opts">
<label class="design-opt">Target start<input type="number" id="designTargetStart" min="1" placeholder="optional" /></label>
<label class="design-opt">Target end<input type="number" id="designTargetEnd" min="1" placeholder="optional" /></label>
<label class="design-opt">Product min (bp)<input type="number" id="designProductMin" min="40" value="100" /></label>
<label class="design-opt">Product max (bp)<input type="number" id="designProductMax" min="50" value="1000" /></label>
</div>
<div class="primer-actions">
<button class="primary primary-lg" type="button" id="designBtn">Design primers</button>
</div>
<div class="error-banner" id="designError" hidden></div>
<div class="primer-warnings" id="designWarnings" hidden></div>
<div class="design-results" id="designResults" hidden></div>
<div class="design-gel gel" id="designGel" hidden></div>
</section>
<!-- ============== MULTIPLEX COMPATIBILITY ============== -->
<section class="card primer-design-card" id="primerMultiplexCard">
<div class="primer-results-head">
<span class="primer-results-title">Multiplex compatibility check</span>
</div>
<p class="field-hint">Paste all the primers you want to run in one tube. We flag 3&prime;
cross-dimers between any two of them, and amplicon sizes too close to resolve on a gel.</p>
<label class="field-label" for="mpxInput">Primers &middot; FASTA or one per line</label>
<textarea id="mpxInput" class="primer-textarea mono" rows="5" spellcheck="false"
placeholder=">F1&#10;ACGT…&#10;>R1&#10;ACGT…"></textarea>
<label class="field-label" for="mpxSizes">Expected product sizes (bp, optional)</label>
<input id="mpxSizes" class="primer-textarea mono" style="min-height:40px;font-size:16px"
placeholder="e.g. 320, 540, 900" />
<div class="primer-actions">
<button class="primary primary-lg" type="button" id="mpxBtn">Check multiplex</button>
</div>
<div class="error-banner" id="mpxError" hidden></div>
<div class="design-results" id="mpxResults" hidden></div>
</section>
<!-- ====== LOCAL SPECIFICITY (in-silico PCR vs a genome) ====== -->
<section class="card primer-design-card" id="primerLocalSpecCard">
<div class="primer-results-head">
<span class="primer-results-title">Specificity vs. your own genome</span>
</div>
<p class="field-hint">In-silico PCR against a genome/FASTA you upload &mdash; the same check as
the NCBI scan, but it runs <strong>on our server</strong>, so your primers never leave, and it
works for any organism (incl. non-model). Up to ~12&nbsp;Mb.</p>
<label class="field-label" for="lspecInput">Primers &middot; FASTA or one per line</label>
<textarea id="lspecInput" class="primer-textarea mono" rows="4" spellcheck="false"
placeholder=">F1&#10;ACGT…&#10;>R1&#10;ACGT…"></textarea>
<label class="field-label" for="lspecFasta">Genome / FASTA to scan</label>
<textarea id="lspecFasta" class="primer-textarea mono" rows="4" spellcheck="false"
placeholder="Paste a FASTA genome, or upload a file below…"></textarea>
<div class="primer-actions" style="gap:10px;align-items:center;display:flex;flex-wrap:wrap">
<input type="file" id="lspecFile" accept=".fa,.fasta,.fna,.txt" class="lspec-file" />
<button class="primary primary-lg" type="button" id="lspecBtn">Run specificity</button>
</div>
<div class="error-banner" id="lspecError" hidden></div>
<div class="design-results" id="lspecResults" hidden></div>
<div class="design-gel gel" id="lspecGel" hidden></div>
</section>
<!-- ===== LOG AMPLIFICATION RESULT (feeds the cross-user primer model) ===== -->
<section class="card primer-design-card" id="primerOutcomeCard">
<div class="primer-results-head">
<span class="primer-results-title">Log amplification result</span>
</div>
<p class="field-hint">Ran a PCR with these primers? Tell us if it amplified. Only
<strong>anonymous, aggregated</strong> patterns (Tm/GC/3&prime;-end buckets) improve the
model — your primers stay private.</p>
<div class="design-opts">
<label class="design-opt" style="flex:1;min-width:220px">Forward primer<input type="text" id="poFwd" placeholder="5'→3'" /></label>
<label class="design-opt" style="flex:1;min-width:220px">Reverse primer<input type="text" id="poRev" placeholder="5'→3'" /></label>
<label class="design-opt">Result
<select id="poResult" class="design-opt-select">
<option value="1">Amplified cleanly</option>
<option value="0.5">Partial / weak</option>
<option value="0">Failed</option>
</select>
</label>
</div>
<div class="primer-actions" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<button class="primary primary-lg" type="button" id="poSave">Save result</button>
<span class="round2-status" id="poStatus" hidden></span>
</div>
</section>
</section>
<!-- ===================== PLASMID STUDIO ===================== -->
<section class="view view--plasmid" data-view="plasmid" hidden>
<header class="view-head">
<h1 class="view-title"><em>Plasmid Editor</em></h1>
<p class="view-sub">Import a GenBank or FASTA record (or paste raw DNA), see the annotated
circular &amp; linear map, run restriction analysis across 1,000+ enzymes, simulate a
digest with a virtual gel, and save constructs to your library. Often the next stop
after Directed Evolution &mdash; bring a designed insert here to map it, check the
digest, and get it synthesis-ready.</p>
</header>
<section class="card plasmid-input-card" id="plasmidInputCard">
<div class="plasmid-input-head">
<label class="field-label" for="plasmidInput">Sequence · GenBank / FASTA / SnapGene .dna / raw DNA</label>
<div class="plasmid-input-actions">
<button class="ghost" type="button" id="plasmidLibraryBtn">My plasmids</button>
<button class="ghost" type="button" id="plasmidExample" title="Loads a demo construct with real promoters, an AmpR gene and cut sites">Try an example</button>
</div>
</div>
<textarea id="plasmidInput" class="primer-textarea mono" rows="6" spellcheck="false"
placeholder="Paste a GenBank record (LOCUS…), a FASTA (&gt;name), or raw DNA…"></textarea>
<div class="plasmid-input-row">
<label class="plasmid-file-btn">
Upload file<input type="file" id="plasmidFile" accept=".dna,.gb,.gbk,.genbank,.fasta,.fa,.fna,.txt,.seq" hidden />
</label>
<div class="plasmid-topo">
<span class="field-hint">Topology (raw/FASTA):</span>
<label><input type="radio" name="plasmidTopo" value="circular" checked /> Circular</label>
<label><input type="radio" name="plasmidTopo" value="linear" /> Linear</label>
</div>
<button class="primary primary-lg" type="button" id="plasmidImport">Import &amp; map</button>
</div>
<div class="error-banner" id="plasmidError" hidden></div>
</section>
<!--
Workbench "home" strip (2026-06-04). The Plasmid Editor is
the hub: from a loaded construct you can stay here to map &
cut, or hand a region to the other three tools. Type-led,
editorial — NOT a SaaS card grid (see the nav comment).
Hidden by showResults() once a construct is loaded.
-->
<section class="workbench-home" id="workbenchHome">
<p class="card-kicker">&sect; One workbench</p>
<p class="workbench-lede">Your construct is the centre of gravity. Import it above to map and
cut it here &mdash; then, without leaving your sequence, hand any region to the other tools.</p>
<nav class="workbench-links" aria-label="What you can do from your construct">
<a class="workbench-link" id="wbToImport" href="#plasmid">
<strong>Map &amp; cut</strong>
<span>annotate &middot; restriction across 1,000+ enzymes &middot; virtual gel &middot; Gibson / Golden&nbsp;Gate / restriction cloning</span>
</a>
<a class="workbench-link" href="#crispr">
<strong>Design CRISPR guides</strong>
<span>Cas9 knockout &amp; base editing, with off-target and knockout scoring</span>
</a>
<a class="workbench-link" href="#primers">
<strong>Analyse primers</strong>
<span>in-silico PCR, specificity and Tm &mdash; ranks your candidate pairs</span>
</a>
<a class="workbench-link" href="#design">
<strong>Evolve a CDS</strong>
<span>ESM-2 zero-shot variant library, codon-optimized and synthesis-ready</span>
</a>
</nav>
</section>
<section class="card plasmid-results-card" id="plasmidResultsCard" hidden>
<div class="plasmid-toolbar">
<div class="plasmid-title-wrap">
<input id="plasmidName" class="plasmid-name-input" type="text" maxlength="120" />
<span class="plasmid-meta" id="plasmidMeta"></span>
</div>
<div class="plasmid-toolbar-actions">
<!-- Circular/Linear are two drawings of the SAME
map pane. "Sequence" is no longer a third
option here because the sequence is always
on screen beside the map — it was only ever
a tab because all three rendered into one
container. See docs/PLASMID-EDITOR.md §2. -->
<div class="plasmid-viewtoggle" role="tablist" aria-label="Map style">
<button class="pv-tab pv-tab-active" id="pvCircular" type="button">Circular</button>
<button class="pv-tab" id="pvLinear" type="button">Linear</button>
</div>
<div class="pe-shows" role="group" aria-label="Show on the sequence">
<label><input type="checkbox" id="peShowComp" checked /> Complement</label>
<label><input type="checkbox" id="peShowFeat" checked /> Features</label>
<label><input type="checkbox" id="peShowCuts" checked /> Cut sites</label>
</div>
<button class="ghost" type="button" id="plasmidSave">Save</button>
<div class="plasmid-use">
<button class="ghost" type="button" id="plasmidUseBtn">Use in ▾</button>
</div>
<div class="plasmid-export">
<button class="ghost" type="button" id="plasmidExportGb">GenBank ▾</button>
</div>
</div>
</div>
<!-- Sequence editor toolbar (shown only in Sequence view) -->
<div class="plasmid-seq-tools" id="plasmidSeqTools" hidden>
<div class="seqtool-row">
<div class="seqtool-find">
<input type="text" id="plasmidFind" class="seqtool-input mono" placeholder="Find sequence (ACGT)…" spellcheck="false" />
<button class="ghost" type="button" id="plasmidFindPrev" title="Previous match"></button>
<button class="ghost" type="button" id="plasmidFindNext" title="Next match"></button>
<span class="seqtool-count" id="plasmidFindCount"></span>
</div>
<div class="seqtool-sel">
<label>From <input type="number" id="plasmidSelFrom" class="seqtool-num" min="1" /></label>
<label>To <input type="number" id="plasmidSelTo" class="seqtool-num" min="1" /></label>
<span class="seqtool-sellen" id="plasmidSelLen"></span>
</div>
</div>
<div class="seqtool-row seqtool-ops">
<button class="ghost" type="button" id="seqCopy">Copy</button>
<button class="ghost" type="button" id="seqCopyRC">Copy rev-comp</button>
<button class="ghost" type="button" id="seqTranslate">Translate</button>
<button class="ghost" type="button" id="seqAnnotate">Annotate…</button>
<button class="ghost" type="button" id="seqDelete">Delete</button>
<button class="ghost" type="button" id="seqInsert">Insert…</button>
<button class="ghost" type="button" id="seqRevComp" title="Reverse-complement the whole plasmid">Rev-comp all</button>
<span class="seqtool-divider"></span>
<button class="ghost" type="button" id="seqToCrispr" title="Design CRISPR guides against the selected region">→ CRISPR</button>
<button class="ghost" type="button" id="seqToPrimers" title="Use the selected region as the Primer Analysis template">→ Primers</button>
<button class="ghost" type="button" id="seqToEvolve" title="Evolve the selected CDS in Directed Evolution">→ Evolve</button>
</div>
<div class="seqtool-output" id="plasmidSeqOutput" hidden></div>
</div>
<!-- Auto-generated construct summary + copy-paste methods paragraph. -->
<div class="run-meta" id="plasmidRunMeta" hidden></div>
<!--
THE SPLIT. Sequence left, map right, one selection
model between them. Previously all three views wrote
into #plasmidMapWrap, so they could not coexist and
the tab strip was the honest surface of that; the
map's own selection arc was already implemented and
could never be seen, because the map wasn't in the
DOM whenever you were reading sequence.
-->
<div class="pe-split" id="peSplit">
<section class="pe-pane pe-pane--seq" id="peSeqPane" aria-label="Sequence">
<div class="pe-pane-h">
<span class="pe-pane-t">Sequence</span>
<span class="pe-pane-hint" id="peSeqHint"></span>
</div>
<div class="pe-seq-scroll" id="plasmidSeqWrap"><!-- sequence injected --></div>
</section>
<div class="pe-divider" id="peDivider" role="separator"
aria-orientation="vertical" tabindex="0"
aria-label="Resize the sequence and map panes"></div>
<section class="pe-pane pe-pane--map" aria-label="Map">
<div class="pe-pane-h">
<span class="pe-pane-t">Map</span>
<span class="pe-pane-hint" id="peMapHint"></span>
</div>
<div class="pe-map-scroll">
<div class="plasmid-map-wrap" id="plasmidMapWrap"><!-- SVG map injected --></div>
<h3 class="primer-sec-title pe-feat-t">Features</h3>
<div class="plasmid-feature-list" id="plasmidFeatures"></div>
</div>
</section>
</div>
<!-- One line of truth about what is selected, read by
both panes and by the agent. -->
<div class="pe-status" id="peStatus" role="status" aria-live="polite"></div>
<!-- Restriction analysis + digest -->
<div class="plasmid-enzymes">
<h3 class="primer-sec-title">Restriction analysis</h3>
<p class="field-hint" id="plasmidEnzymeHint">Tap enzymes to show their cut sites on the map; pick a few and digest to size the fragments.</p>
<div class="plasmid-enzyme-grid" id="plasmidEnzymeGrid"></div>
<div class="plasmid-digest-actions">
<button class="ghost" type="button" id="plasmidDigestBtn">Digest selected →</button>
<button class="plasmid-clear-enz" type="button" id="plasmidClearEnz">Clear</button>
</div>
<div class="plasmid-gel-wrap" id="plasmidGel" hidden></div>
</div>
<p class="primer-foot" id="plasmidFoot">Restriction sites from Biopython/REBASE (1,000+ enzymes),
circular-aware. Auto-annotation flags ORFs + common motifs; GenBank features are kept as-is.</p>
</section>
<!-- Pairwise sequence alignment -->
<section class="card plasmid-align-card">
<div class="clone-head">
<h2 class="clone-title">Align two sequences</h2>
<div class="clone-methods" role="tablist">
<button class="clone-method clone-method-active" data-align-mode="global" type="button">Global</button>
<button class="clone-method" data-align-mode="local" type="button">Local</button>
</div>
</div>
<p class="field-hint">Compare two DNA or protein sequences — percent identity, mismatches and gaps. <strong>Global</strong> aligns end-to-end (Needleman–Wunsch); <strong>Local</strong> finds the best shared region (Smith–Waterman).</p>
<div class="align-inputs">
<textarea id="alignA" class="primer-textarea mono" rows="3" spellcheck="false" placeholder="Sequence A — ACGT… or protein"></textarea>
<textarea id="alignB" class="primer-textarea mono" rows="3" spellcheck="false" placeholder="Sequence B"></textarea>
</div>
<div class="align-actions">
<button class="primary" type="button" id="alignRun">Align</button>
<span class="align-stats" id="alignStats"></span>
</div>
<div class="align-output" id="alignOutput" hidden></div>
</section>
<!-- Cloning / assembly -->
<section class="card plasmid-clone-card">
<div class="clone-head">
<h2 class="clone-title">Clone &amp; assemble</h2>
<div class="clone-methods" role="tablist">
<button class="clone-method clone-method-active" data-method="gibson" type="button">Gibson / overlap</button>
<button class="clone-method" data-method="golden_gate" type="button">Golden Gate</button>
<button class="clone-method" data-method="restriction" type="button">Restriction</button>
</div>
</div>
<!-- Gibson / Golden Gate: fragment list -->
<div class="clone-pane" id="clonePaneFragments">
<p class="field-hint" id="cloneFragHint">Paste your fragments — FASTA, or one per line as <code>name: SEQUENCE</code>. Overlapping ends assemble scarlessly; bare junctions get homology arms designed into the primers.</p>
<textarea id="cloneFragments" class="primer-textarea mono" rows="6" spellcheck="false"
placeholder=">backbone&#10;ACGT…&#10;>insert&#10;TTGG…"></textarea>
<div class="clone-opts">
<button class="ghost" type="button" id="cloneUseCurrent">+ Use current plasmid</button>
<label class="clone-circ" id="cloneCircWrap"><input type="checkbox" id="cloneCircular" checked /> Circular product</label>
<label class="clone-gg-enz" id="cloneGgEnzWrap" hidden>Enzyme
<select id="cloneGgEnzyme" class="seqtool-num" style="width:auto">
<option value="BsaI">BsaI</option>
<option value="BbsI">BbsI</option>
<option value="BsmBI">BsmBI</option>
</select>
</label>
</div>
</div>
<!-- Restriction: vector + insert -->
<div class="clone-pane" id="clonePaneRestriction" hidden>
<label class="field-label" for="cloneVector">Vector (cut once by the enzyme)</label>
<textarea id="cloneVector" class="primer-textarea mono" rows="3" spellcheck="false" placeholder="Vector DNA…"></textarea>
<label class="field-label" for="cloneInsert">Insert (flanked by the same enzyme)</label>
<textarea id="cloneInsert" class="primer-textarea mono" rows="3" spellcheck="false" placeholder="Insert DNA…"></textarea>
<div class="clone-opts">
<label>Enzyme <input type="text" id="cloneReEnzyme" class="seqtool-num" style="width:90px" value="EcoRI" /></label>
<label class="clone-circ"><input type="checkbox" id="cloneVecCirc" checked /> Vector is circular</label>
</div>
</div>
<div class="clone-actions">
<button class="primary primary-lg" type="button" id="cloneRun">Assemble →</button>
</div>
<div class="error-banner" id="cloneError" hidden></div>
<div class="clone-result" id="cloneResult" hidden></div>
</section>
<!-- ===== LOG CLONING RESULT (feeds the cross-user assembly model) ===== -->
<section class="card primer-design-card" id="cloneOutcomeCard">
<div class="primer-results-head">
<span class="primer-results-title">Log cloning result</span>
</div>
<p class="field-hint">Did your assembly work at the bench? Logging it teaches the model which
methods/junctions succeed. Only <strong>anonymous, aggregated</strong> patterns are kept.</p>
<div class="design-opts">
<label class="design-opt">Method
<select id="coMethod" class="design-opt-select">
<option value="gibson">Gibson</option>
<option value="goldengate">Golden Gate</option>
<option value="restriction">Restriction</option>
<option value="gateway">Gateway</option>
</select>
</label>
<label class="design-opt">Fragments<input type="number" id="coFrags" min="1" max="20" value="2" /></label>
<label class="design-opt">Overlap (bp)<input type="number" id="coOverlap" min="0" max="120" value="25" /></label>
<label class="design-opt">Result
<select id="coResult" class="design-opt-select">
<option value="" selected>Select outcome…</option>
<option value="1">Assembled</option>
<option value="0.5">Partial</option>
<option value="0">Failed</option>
</select>
</label>
</div>
<div class="primer-actions" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<button class="primary primary-lg" type="button" id="coSave">Save result</button>
<span class="round2-status" id="coStatus" hidden></span>
</div>
</section>
<!-- Library modal -->
<div class="modal" id="plasmidLibraryModal" hidden role="dialog" aria-modal="true">
<div class="modal-backdrop" data-plasmid-lib-close></div>
<div class="modal-panel" role="document">
<header class="modal-header">
<div><p class="modal-sub">&sect; Library</p><h2>My plasmids</h2></div>
<button class="modal-close" aria-label="Close" data-plasmid-lib-close>&times;</button>
</header>
<div class="modal-body"><div id="plasmidLibraryList" class="plasmid-lib-list"></div></div>
</div>
</div>
</section>
<section class="view view--docs" data-view="docs" hidden>
<section class="card docs-card">
<header class="card-header">
<div class="card-header-left">
<p class="card-kicker">&sect; Documentation</p>
<h2>How TuringDNA works</h2>
<p class="card-sub">Turing, and the four tools it runs — what each does, what to trust, how to cite it.</p>
</div>
</header>
<div class="docs-body">
<section>
<h3>Overview</h3>
<p>TuringDNA is driven by <strong>Turing</strong> — tell it what you want (fetch a gene, evolve it, design guides, check primers) and it calls the right tool, in the right order, in one conversation. Every tool is also available directly, for hands-on work without the chat.</p>
<p>Your individual sequences stay private — never shared, exposed, or reproduced, and they only leave this machine on the explicit opt-in network steps listed below. Only anonymous, aggregated signals improve our models (<a href="https://turingdna.com/privacy/" target="_blank" rel="noopener">Privacy Policy</a>).</p>
</section>
<section>
<h3>Turing</h3>
<p>A conversational agent that runs the other four tools for you — describe what you want and it calls the right one, chaining them in a single reply when a task needs more than one (fetch a gene, then evolve it, for example).</p>
<ul class="docs-list">
<li><strong>Tools it can call.</strong> fetch_sequence (gene symbol/accession → real DNA, via Ensembl/NCBI), design_variant_library (directed evolution), design_crispr_guides, design_primers — the same engines the four direct tools use below, not a separate model.</li>
<li><strong>Sign-in.</strong> Chatting is free; running a tool (fetching a real sequence, generating a design) requires a free account, same as the direct tools.</li>
<li><strong>Memory.</strong> Remembers what it's fetched or designed earlier in the same conversation — refer back to &ldquo;it&rdquo; or &ldquo;that gene&rdquo; instead of re-pasting.</li>
<li><strong>Honesty.</strong> If something's out of scope — a capability that isn't wired up, or a request needing bench validation it can't do — it says so instead of fabricating a result.</li>
</ul>
<p class="docs-callout">Turing calls the same scoring/design code the direct tools call — the same caveats apply. See each tool's section below for what to trust and how to cite it.</p>
</section>
<section>
<h3>Plasmid Editor</h3>
<p>Import and work with circular or linear constructs end to end.</p>
<ul class="docs-list">
<li><strong>Import.</strong> GenBank · FASTA · EMBL · SnapGene-style features · raw DNA. Annotations are read from the record where present.</li>
<li><strong>Maps &amp; editing.</strong> Annotated circular and linear maps, plus a sequence view with feature colouring, find, translate, reverse-complement and in-place editing.</li>
<li><strong>Restriction.</strong> Scans the REBASE enzyme set (1,000+ enzymes), separates cutters from non-cutters, and simulates a digest on a virtual agarose gel.</li>
<li><strong>Cloning.</strong> Simulates Gibson assembly, Golden Gate, and restriction-ligation — junctions, fragment order, and the assembled product.</li>
<li><strong>Export.</strong> GenBank or FASTA; save constructs to your per-user library for 45 days.</li>
</ul>
<p class="docs-callout">Cloning and digests are <strong>in-silico predictions</strong>. Confirm overhangs, junctions and orientation before you commit reagents.</p>
</section>
<section>
<h3>CRISPR</h3>
<p>Design SpCas9 guide RNAs against a pasted region (or a gene resolved by symbol/accession) — for knockout or base editing.</p>
<ul class="docs-list">
<li><strong>Guides.</strong> Every NGG-PAM 20-mer with an on-target activity score (Doench-style sequence features), strand and position.</li>
<li><strong>Off-target.</strong> CFD scoring (Doench 2016) against other sites in the input. Base-edit mode adds CBE/ABE windows, the predicted edit, the amino-acid consequence, bystander flags and CRISPR-STOP knockouts.</li>
<li><strong>Knockout.</strong> Predicted indel spectrum, frameshift %, out-of-frame dominance, and a loss-of-function likelihood.</li>
<li><strong>Cloning oligos.</strong> Ready-to-order sense/antisense pairs for the standard vectors (BbsI/BsmBI; Cas12a geometry), copied straight to a vendor order.</li>
</ul>
<p class="docs-callout">Off-target is screened against the genome when you choose an organism: your top-ranked guides are searched with a seed index and CFD-scored. Coverage is the <strong>complete genome</strong> for E.&nbsp;coli and <strong>coding sequence</strong> for human (GRCh38) and mouse (GRCm39) — intronic and intergenic off-targets sit outside that index, so a whole-genome tool is still the right call if your application depends on them. Leave the organism unset and only the sequence you pasted is checked.</p>
</section>
<section>
<h3>Primer Analysis</h3>
<p>Bring your <em>candidate</em> primers; TuringDNA evaluates and ranks them — it does not invent primers from scratch.</p>
<ul class="docs-list">
<li><strong>Scoring.</strong> Nearest-neighbor Tm (SantaLucia), GC content, 3′-end stability, and hairpin / self-dimer heuristics for every primer.</li>
<li><strong>In-silico PCR.</strong> Binds each pair against your template and reports the intended amplicon plus any off-target products and their sizes.</li>
<li><strong>Specificity (opt-in).</strong> Checks candidates against NCBI nt and separates intended from off-target hits.</li>
<li><strong>Ranking.</strong> Combines the above into a recommended pair — no BLASTing one primer at a time.</li>
</ul>
</section>
<section>
<h3>Directed Evolution</h3>
<p>Design a smart mutation library for an existing protein using ESM-2 zero-shot scoring. Provide a wild-type sequence (or send a CDS from the Plasmid Editor); receive a library of multi-mutant variants ranked by predicted evolutionary fitness, codon-optimized for your host, ready to order.</p>
<p>This is <strong>not</strong> de novo protein design. It's directed-evolution library generation: improving an existing protein along an existing axis (stability, expression, mild activity tuning).</p>
<ol class="docs-list">
<li><strong>Parse &amp; translate.</strong> FASTA · SnapGene · GenBank · EMBL · raw DNA · raw protein. Plasmid files surface a CDS picker; raw DNA with multiple stops triggers 6-frame ORF discovery.</li>
<li><strong>Zero-shot scoring.</strong> ESM-2 computes ΔLL = log P(mutant | x_WT) − log P(WT | x_WT) for every position × 19 substitutions (Meier et al. 2021 wild-type marginal scheme).</li>
<li><strong>Combinatorial search.</strong> Simulated annealing over the top-percentile pool of single-site mutations. Multiple restarts; cumulative ΣΔLL as fitness; stop-codon and duplicate-position penalties.</li>
<li><strong>Codon optimization.</strong> Reverse-translate with a host codon-usage table. Synonymously scrub BsaI / BsmBI / NotI sites for Golden Gate compatibility.</li>
</ol>
<table class="docs-table">
<tr><th>Mutations / variant</th><th>Approx. functional retention</th></tr>
<tr><td>1–2</td><td>70–85%</td></tr>
<tr><td>3–4</td><td>50–70%</td></tr>
<tr><td>5–6</td><td>30–55%</td></tr>
<tr><td>7–8</td><td>15–40%</td></tr>
<tr><td>9+</td><td>&lt; 25%</td></tr>
</table>
<p class="docs-callout">Stay shallow. Cap <code>Max mutations / variant</code> at 3–4 unless you have a structural reason. Screen, don't trust.</p>
<p><strong>What ESM-2 sees:</strong> evolutionary plausibility from ~65 M UniRef50 sequences — conservation, coevolution, sequence context. <strong>What it can't see:</strong> structure explicitly, rare-but-essential active-site roles, epistasis between selected mutations; membrane proteins, IDPs and multi-domain assemblies are weakest.</p>
<p><strong>Filter syntax</strong> (variant table): <code>C49</code> = variants mutating residue 49 · <code>W58L</code> = exactly that substitution · <code>gc&gt;50</code>, <code>tm&gt;58</code>, <code>fitness&gt;2</code>, <code>bp&lt;800</code> = numeric ranges.</p>
</section>
<section>
<h3>What runs locally vs over the network</h3>
<ul class="docs-list">
<li><strong>Local:</strong> plasmid parsing, annotation, restriction &amp; cloning simulation; CRISPR design, CFD off-target and oligo generation; primer scoring and in-silico PCR; ESM-2 inference, simulated annealing and codon optimization; all CSV / Excel / GenBank / FASTA export. Sequences never leave this machine for these steps.</li>
<li><strong>Opt-in network:</strong> NCBI BLAST (sequence identification and primer specificity — sends that sequence to NCBI); AlphaFold-DB / ESMFold structure embeds; Europe PMC literature lookup (IP Radar); synthesis-vendor redirects. Each is explicit and off by default.</li>
</ul>
</section>
<section>
<h3>Citations</h3>
<p class="docs-citation">Meier et al. (2021). Language models enable zero-shot prediction of the effects of mutations on protein function. <cite>NeurIPS</cite> 34.</p>
<p class="docs-citation">Lin et al. (2023). Evolutionary-scale prediction of atomic-level protein structure. <cite>Science</cite> 379:1123–1130.</p>
<p class="docs-citation">Doench et al. (2016). Optimized sgRNA design to maximize activity and minimize off-target effects of CRISPR-Cas9. <cite>Nat. Biotechnol.</cite> 34:184–191.</p>
<p class="docs-citation">Hsu et al. (2013). DNA targeting specificity of RNA-guided Cas9 nucleases. <cite>Nat. Biotechnol.</cite> 31:827–832.</p>
<p class="docs-citation">Komor et al. (2016) &amp; Gaudelli et al. (2017). Programmable base editing of C·G and A·T pairs. <cite>Nature</cite> 533:420 / 551:464.</p>
<p class="docs-citation">Allawi &amp; SantaLucia (1997). Thermodynamics and NMR of internal G·T mismatches in DNA. <cite>Biochemistry</cite> 36:10581–10594.</p>
</section>
<section>
<h3>Licenses of bundled components</h3>
<ul class="docs-list">
<li>ESM-2 weights — MIT (Meta/FAIR)</li>
<li>Transformers, Accelerate — Apache 2.0 (Hugging Face)</li>
<li>PyTorch — BSD-style (Meta)</li>
<li>Biopython (parsing, restriction, Tm) — BSD-derived</li>
<li>Mol* viewer — MIT (PDBe / RCSB)</li>
<li>Inter, JetBrains Mono — SIL Open Font License</li>
<li>Lucide icons — ISC</li>
</ul>
</section>
</div>
</section>
</section>
</div>
<footer>
<span class="footer-note">TuringDNA v0.1 &middot; individual sequences kept private &middot; <a href="https://turingdna.com/privacy/" target="_blank" rel="noopener">Privacy</a> &middot; <a href="https://turingdna.com/terms/" target="_blank" rel="noopener">Terms</a></span>
</footer>
</main>
</div>
<!-- CRISPR sign-in modal — fires when an anonymous user clicks
the CRISPR sidebar tab OR when the backend returns 403
signin_required (e.g., expired JWT). Same .modal /
.modal-panel-narrow infrastructure as the BLAST consent
modal — centered overlay, dismissable, doesn't depend on
scroll position. -->
<div class="modal modal--sheet" id="crisprSigninModal" hidden role="dialog" aria-labelledby="crisprSigninTitle" aria-modal="true">
<div class="modal-backdrop" data-crispr-signin-close></div>
<div class="modal-panel modal-panel-narrow" role="document">
<header class="modal-header">
<div>
<p class="modal-sub">&sect; Sign in</p>
<h2 id="crisprSigninTitle">These tools are members-only.</h2>
</div>
<button class="modal-close" aria-label="Close" data-crispr-signin-close>&times;</button>
</header>
<div class="modal-body">
<p>
Don't miss out &mdash; the <strong>CRISPR designer</strong>,
<strong>Primer Analysis</strong>, and the <strong>Plasmid Editor</strong> are free
for anyone with an account. Design, analyse, export any time, and save your
designs and plasmids for <strong>45 days</strong>.
</p>
<p>
No card, no spam, takes ten seconds. <span style="white-space:nowrap;">:))</span>
</p>
<div class="blast-consent-actions">
<button class="ghost" type="button" data-crispr-signin-close>Maybe later</button>
<a class="primary" href="https://turingdna.com/signup/?from=crispr" target="_top" id="crisprSigninCreate">Create free account</a>
<a class="ghost" href="https://turingdna.com/signin/?from=crispr" target="_top" id="crisprSigninExisting">I already have one</a>
</div>
</div>
</div>
</div>
<!-- Phase 3 (M5): CRISPR structure viewer. Opens from the "why this
guide" panel when a gene context exists. Shows the gene's
AlphaFold-DB model (via the Mol* viewer already loaded for the
directed-evolution page) with the Cas9 cut-site residue focused
+ highlighted. -->
<div class="modal" id="crisprStructureModal" hidden role="dialog" aria-labelledby="crisprStructureTitle" aria-modal="true">
<div class="modal-backdrop" data-crispr-struct-close></div>
<div class="modal-panel" role="document">
<header class="modal-header">
<div>
<p class="modal-sub">&sect; Structure</p>
<h2 id="crisprStructureTitle">Cut site on the protein</h2>
<p class="modal-sub" id="crisprStructureMeta">&mdash;</p>
</div>
<button class="modal-close" aria-label="Close" data-crispr-struct-close>&times;</button>
</header>
<div class="modal-body">
<div class="alphafold-viewer crispr-structure-viewer" id="crisprStructureViewer">
<div class="alphafold-loading">Loading AlphaFold-DB structure&hellip;</div>
</div>
<p class="alphafold-disclaimer" id="crisprStructureDisclaimer">
Predicted structure from AlphaFold-DB. The highlighted residue is the
Cas9 cut site mapped through the coding sequence &mdash; an approximate
location, not experimentally determined.
</p>
</div>
</div>
</div>
<!-- Phase 3 (M4): saved-designs list. Opens from "My designs"; click a
row to reopen that guide set in the table. -->
<div class="modal" id="crisprDesignsModal" hidden role="dialog" aria-labelledby="crisprDesignsTitle" aria-modal="true">
<div class="modal-backdrop" data-crispr-designs-close></div>
<div class="modal-panel modal-panel-narrow" role="document">
<header class="modal-header">
<div>
<p class="modal-sub">&sect; Library</p>
<h2 id="crisprDesignsTitle">My saved designs</h2>
<p class="modal-sub">Kept for 45 days. Click one to reopen it.</p>
</div>
<button class="modal-close" aria-label="Close" data-crispr-designs-close>&times;</button>
</header>
<div class="modal-body">
<div id="crisprDesignsList" class="crispr-designs-list">
<p class="muted">Loading…</p>
</div>
</div>
</div>
</div>
<!-- Unified saved-work hub (2026-06-05). One panel listing everything the
signed-in user has saved across all four tools, each reopenable —
replaces the per-tool "My plasmids" list and the CRISPR-only "My
designs". Sections lazy-load in parallel; each degrades independently. -->
<div class="modal" id="savedModal" hidden role="dialog" aria-labelledby="savedTitle" aria-modal="true">
<div class="modal-backdrop" data-saved-close></div>
<div class="modal-panel" role="document">
<header class="modal-header">
<div>
<p class="modal-sub">&sect; Library</p>
<h2 id="savedTitle">My designs</h2>
<p class="modal-sub">Everything you&rsquo;ve saved &mdash; kept for 45 days. Click one to reopen it.</p>
</div>
<button class="modal-close" aria-label="Close" data-saved-close>&times;</button>
</header>
<div class="modal-body saved-body">
<section class="saved-section">
<h3 class="saved-sec-title">Plasmids</h3>
<div id="savedPlasmids" class="saved-list"><p class="muted">Loading…</p></div>
</section>
<section class="saved-section">
<h3 class="saved-sec-title">CRISPR designs</h3>
<div id="savedCrispr" class="saved-list"><p class="muted">Loading…</p></div>
</section>
<section class="saved-section">
<h3 class="saved-sec-title">Primer sets</h3>
<div id="savedPrimers" class="saved-list"><p class="muted">Loading…</p></div>
</section>
<section class="saved-section">
<h3 class="saved-sec-title">Evolution libraries</h3>
<div id="savedLibraries" class="saved-list"><p class="muted">Loading…</p></div>
</section>
</div>
</div>
</div>
<!-- "Start here" goal router (2026-06-05). Shown once after the first-use
disclaimer and re-openable from the sidebar — orients a newcomer to
the right tool by goal. Editorial + dismissible, never a blocker. -->
<!-- ===================== FIELD ATLAS (Pillar 2: the living commons) =====
A public, browsable view of the de-identified, k-anonymized cross-user
substitution-effect aggregate (public.mutation_priors, served by
/api/atlas). A 20×20 from→to amino-acid grid: which substitution TYPES
the field has found favorable (or not), pooled across labs — the
network effect made visible. Honest cold-start: empty until enough
labs have logged outcomes. Controller: TDAtlas in app.js. -->
<div class="modal" id="atlasModal" hidden role="dialog" aria-labelledby="atlasTitle" aria-modal="true">
<div class="modal-backdrop" data-atlas-close></div>
<div class="modal-panel atlas-panel" role="document">
<header class="modal-header">
<div>
<p class="modal-sub">&sect; The living commons</p>
<h2 id="atlasTitle">Field atlas</h2>
<p class="modal-sub">What every lab&rsquo;s bench results, pooled and de-identified, have taught the engine &mdash; by amino-acid substitution. Your data makes this sharper for everyone; nothing here is attributable to a person.</p>
</div>
<button class="modal-close" aria-label="Close" data-atlas-close>&times;</button>
</header>
<div class="modal-body">
<div class="atlas-body" id="atlasBody"><p class="muted">Loading the commons&hellip;</p></div>
</div>
</div>
</div>
<div class="modal" id="startHereModal" hidden role="dialog" aria-labelledby="startHereTitle" aria-modal="true">
<div class="modal-backdrop" data-starthere-close></div>
<div class="modal-panel" role="document">
<header class="modal-header">
<div>
<p class="modal-sub">&sect; Start here</p>
<h2 id="startHereTitle">What are you working on?</h2>
<p class="modal-sub">Tell Turing what you&rsquo;re trying to do &mdash; it fetches genes, evolves them, designs guides, and checks primers for you, in one conversation. Prefer hands-on? Jump straight to a tool below.</p>
</div>
<button class="modal-close" aria-label="Close" data-starthere-close>&times;</button>
</header>
<div class="modal-body">
<div class="starthere-goals">
<button type="button" class="starthere-goal" data-goal="turing">
<strong>Just tell Turing what you need <span class="pill">Start here</span></strong>
<span>Fetch a gene, evolve it, design guides, check primers &mdash; it runs the right tool for you</span>
</button>
</div>
<p class="modal-sub starthere-divider">&hellip;or jump straight to a tool</p>
<div class="starthere-goals">
<button type="button" class="starthere-goal" data-goal="design">
<strong>Evolve or improve a protein</strong>
<span>Design a ranked variant library with ESM-2 &mdash; then build, edit, and log results for round 2</span>
</button>
<button type="button" class="starthere-goal" data-goal="plasmid">
<strong>Build a construct from your variants</strong>
<span>Clone, map, and annotate &mdash; Gibson, Golden Gate, or restriction digest</span>
</button>
<button type="button" class="starthere-goal" data-goal="crispr">
<strong>Edit or knock out a gene</strong>
<span>Design CRISPR guide RNAs &mdash; knockout or base editing</span>
</button>
<button type="button" class="starthere-goal" data-goal="primers">
<strong>Check or pick PCR primers</strong>
<span>Score your candidates with in-silico PCR + specificity</span>
</button>
</div>
<button type="button" class="starthere-skip" data-starthere-close>I&rsquo;ll explore on my own &rarr;</button>
</div>
</div>
</div>
<!-- BLAST consent modal — fires the first time per-session that the
user clicks "Identify via NCBI BLAST". Per-session sessionStorage
flag means the user only sees this once per browser session.
Centered .modal-panel-narrow keeps it visually anchored to the
viewport center regardless of where the user has scrolled to. -->
<div class="modal" id="blastConsentModal" hidden role="dialog" aria-labelledby="blastConsentTitle" aria-modal="true">
<div class="modal-backdrop" data-blast-cancel></div>
<div class="modal-panel modal-panel-narrow" role="document">
<header class="modal-header">
<div>
<p class="modal-sub">&sect; Consent</p>
<h2 id="blastConsentTitle">Send sequence to NCBI?</h2>
</div>
<button class="modal-close" aria-label="Close" data-blast-cancel>&times;</button>
</header>
<div class="modal-body">
<p>
Identification runs BLASTp against NCBI's <code>nr</code>
database. To do that, the wild-type protein sequence is sent
to <code>blast.ncbi.nlm.nih.gov</code> over HTTPS &mdash;
encrypted in transit, visible to the U.S. National Library
of Medicine, and logged by NCBI for service operation and
security.
</p>
<p>
<em>Don't BLAST sequences that are confidential or
pre-publication IP.</em> The on-server pipeline (scoring,
search, codon optimization) keeps sequences local; only
BLAST and the opt-in ESMFold structure prediction leave
the machine.
</p>
<div class="blast-consent-actions">
<button class="ghost" type="button" data-blast-cancel>Cancel</button>
<button class="primary" type="button" id="blastConsentContinue">
Continue &middot; remember for this session
</button>
</div>
</div>
</div>
</div>
<!-- ESMFold modal — fires from the per-row "Fold" button. POSTs the
variant's amino-acid sequence to Meta's public ESM Atlas API and
renders the returned PDB in a Mol* viewer. Standard .modal /
.modal-panel structure with the shared close-wiring. -->
<div class="modal" id="foldModal" hidden role="dialog" aria-labelledby="foldModalTitle">
<div class="modal-backdrop" data-close-modal></div>
<div class="modal-panel" role="document">
<header class="modal-header">
<div>
<h2 id="foldModalTitle">Predicted structure · ESMFold</h2>
<p class="modal-sub" id="foldModalSub">Folding with Meta ESMFold…</p>
</div>
<button class="modal-close" aria-label="Close" data-close-modal>×</button>
</header>
<div class="modal-body" id="foldModalBody">
<!-- Re-uses the .alphafold-viewer / .alphafold-loading
classes so the embed reads identically to the BLAST →
AlphaFold viewer further down the page. -->
<div class="alphafold-viewer" id="foldViewer">
<div class="alphafold-loading">
<span class="identify-spinner"></span>
Submitting sequence to ESMFold…
</div>
</div>
<div class="alphafold-disclaimer" id="foldModalDisclaimer">
Predicted by <a href="https://esmatlas.com/about" target="_blank" rel="noopener">ESMFold</a> (Meta AI).
<strong>The variant's amino-acid sequence is sent to <code>api.esmatlas.com</code></strong>
over HTTPS — encrypted in transit, visible to Meta's servers. Do not fold
confidential or pre-publication sequences.
Drag to rotate · scroll to zoom · double-click to fit.
Backbone confidence colored by pLDDT (blue&nbsp;=&nbsp;high, orange&nbsp;=&nbsp;low).
</div>
</div>
</div>
</div>
<!-- =====================================================================
First-use disclaimer modal — shown automatically on first visit
(localStorage flag) and re-openable via the "Why?" link in the
research-strip under the topbar. Single-click acceptance; no fields
to fill. The text is the in-UI half of the legal narrative — paired
with the ToS the user's lawyer will draft and the per-session
synthesis-confirmation modal below.
The modal blocks app interaction until the user clicks Accept (the
underlying modal-backdrop catches clicks and doesn't dismiss).
===================================================================== -->
<div class="modal modal-disclaimer" id="firstUseModal" hidden role="dialog" aria-labelledby="firstUseTitle" aria-modal="true">
<div class="modal-backdrop modal-backdrop-fixed"></div>
<div class="modal-panel modal-panel-narrow" role="document">
<div class="disclaimer-body">
<p class="boot-kicker">&sect; Before you begin</p>
<h2 id="firstUseTitle" class="disclaimer-title"><em>Research use only.</em></h2>
<hr class="boot-rule" />
<p>
TuringDNA designs candidate variant libraries with a
protein-language-model scoring pipeline. The outputs are
<strong>computational predictions</strong>, not laboratory
measurements.
</p>
<p>
You are responsible for visually verifying every sequence
before sending it to a synthesis vendor. The tool is
intended for research and discovery — <em>not</em> for
diagnostic, therapeutic, or clinical decision-making.
</p>
<p>
Sequences sent through the optional NCBI BLAST and ESMFold
features leave this machine; both are gated behind
explicit per-session consent. Don't run them on
confidential or pre-publication sequences.
</p>
<div class="disclaimer-actions">
<button class="primary" type="button" id="firstUseAccept">
I understand — continue
</button>
</div>
<p class="disclaimer-meta">
Acceptance is remembered on this browser. Clear site data
to be re-prompted.
</p>
</div>
</div>
</div>
<!-- =====================================================================
Per-session synthesis verification modal — interposes between the
user clicking a Synthesize / Copy designed insert button and the
actual network call or clipboard write. Asks the user to actively
tick a "I have visually verified this sequence" checkbox. Once
ticked once per session (sessionStorage), subsequent synth actions
in the same tab skip this modal and use the existing per-action
confirms (RE-warn etc.) only when applicable.
The acknowledgment is intentionally per-session, not per-action:
per-action would create research-tool-hostile friction; per-session
still produces a clear "user actively opted in to shipping DNA"
record for the legal narrative.
===================================================================== -->
<div class="modal modal-disclaimer" id="synthConfirmModal" hidden role="dialog" aria-labelledby="synthConfirmTitle" aria-modal="true">
<div class="modal-backdrop modal-backdrop-fixed"></div>
<div class="modal-panel modal-panel-narrow" role="document">
<div class="disclaimer-body">
<p class="boot-kicker">&sect; Verify before synthesis</p>
<h2 id="synthConfirmTitle" class="disclaimer-title"><em>Confirm this order.</em></h2>
<hr class="boot-rule" />
<div id="synthConfirmDetails" class="disclaimer-detail-block"></div>
<p>
Before you send a sequence for synthesis, take one last
look. Open the variant's detail panel, inspect the DNA
block, and check that the mutations, length, and reading
frame are what you intend to receive in your tube.
</p>
<label class="disclaimer-check">
<input type="checkbox" id="synthConfirmCheck" />
<span>I have visually verified this sequence and accept responsibility for the order.</span>
</label>
<div class="disclaimer-actions">
<button class="ghost-btn" type="button" id="synthConfirmCancel">Cancel</button>
<button class="primary" type="button" id="synthConfirmProceed" disabled>
Proceed &middot; remember for this session
</button>
</div>
</div>
</div>
</div>
<!-- auth.js MUST load before app.js (and ideally before any /api/*
call) — it reads the JWT from the URL fragment, stashes it in
sessionStorage, and wraps window.fetch to attach the
Authorization header on every same-origin /api/* call. Without
this, signed-in users would still hit the anonymous-quota
gate. -->
<script src="/static/auth.js?v=20260706-viewdeep"></script>
<!-- Cloning reference data must load before app.js so the Designer
can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
<script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
<script src="/static/app.js?v=20260801-review" defer></script>
<!-- The decision trace, BEFORE cockpit.js: applyEvent calls TDTrace.push
on the very first event, and both are `defer`, so document order is
load order. Loading it after would drop the opening events of a
restored run on the floor — silently, since the push is guarded. -->
<script src="/static/trace.js?v=20260801-review" defer></script>
<!-- THE COCKPIT — the persistent orchestrator rail. Loads after app.js so
TDBench/TDStructure exist when a tool result asks the workspace to
render something. This is the only conversation surface in the app. -->
<script src="/static/cockpit.js?v=20260801-review" defer></script>
<!-- structcard before catalog: the catalog calls TDStructCard.observe as
soon as it paints. Both are defer, so document order is load order. -->
<script src="/static/structcard.js?v=20260801-review" defer></script>
<script src="/static/catalog.js?v=20260801-review" defer></script>
<script src="/static/railsplit.js?v=20260801-review" defer></script>
<!-- Dwell-time heartbeat. Loads after auth.js so its /api/ping calls go
through the JWT-attaching fetch wrapper (signed-in attribution). -->
<script src="/static/telemetry.js?v=20260622-analytics" defer></script>
<!-- First-party UX-behaviour collector (dead/rage clicks, scroll depth,
funnel steps, feature clicks, client errors). Structure-only, never
reads sequence content; shares telemetry.js's persisted td_session_id
and POSTs batches to /api/collect via sendBeacon. First-party /static
file so it satisfies the strict script-src CSP. -->
<script src="/static/collector.js?v=20260727-replayfix3" defer></script>
</body>
</html>