$sloprun.dev

DNA → Protein Translator

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 8 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

A real human insulin precursor is already loaded, transcribed to mRNA and read three bases at a time against an embedded 64-entry genetic code — 110 amino acids from the ATG to the amber stop, drawn as colored beads you can walk with the arrow keys. The point is the frame buttons: nothing in a sequence says where to start counting, so shift the cut by one base and a clean protein collapses into two stop codons and no start at all. Try messy paste next — a notebook dump with line numbers, a header line, two ambiguity codes and one stray letter: one N still resolves to serine because all four possibilities are serine, the other cannot, and the stray letter is flagged in place instead of silently shifting everything after it. Teaching tool only — standard code, no splicing, nothing that happens to a protein after the ribosome lets go.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/codon-translator" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="DNA → Protein Translator — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/codon-translator

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 1062 lines 47.0 KB index.html
demos/codon-translator/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DNA → Protein Translator</title>
<link rel="icon" href="data:,">
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
   Identity: instrument-panel. Machine facts in mono; human words in sans.
   Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }

/* ---------- amino-acid class palette (light) ---------- */
:root {
  --hydro: #96590B; --hydro-s: #FAF0E1;
  --arom:  #6C28D5; --arom-s:  #F2ECFD;
  --polar: #0C6B82; --polar-s: #E5F4F8;
  --basic: #3A38C0; --basic-s: #ECEBFD;
  --acid:  #A61D62; --acid-s:  #FCE9F2;
  --spec:  #59554E; --spec-s:  #EFEDE8;
  --unk:   #6B7178; --unk-s:   #EFEFEC;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --hydro: #E4A45C; --hydro-s: #2F2519;
    --arom:  #B99EFA; --arom-s:  #241E39;
    --polar: #4FC9E9; --polar-s: #10262E;
    --basic: #909CFF; --basic-s: #1B1E3B;
    --acid:  #F17FB7; --acid-s:  #341525;
    --spec:  #B7AEA2; --spec-s:  #272520;
    --unk:   #9AA0A8; --unk-s:   #23262C;
  }
}
:root[data-theme="dark"] {
  --hydro: #E4A45C; --hydro-s: #2F2519;
  --arom:  #B99EFA; --arom-s:  #241E39;
  --polar: #4FC9E9; --polar-s: #10262E;
  --basic: #909CFF; --basic-s: #1B1E3B;
  --acid:  #F17FB7; --acid-s:  #341525;
  --spec:  #B7AEA2; --spec-s:  #272520;
  --unk:   #9AA0A8; --unk-s:   #23262C;
}
.c-hydro { --c: var(--hydro); --cs: var(--hydro-s); }
.c-arom  { --c: var(--arom);  --cs: var(--arom-s);  }
.c-polar { --c: var(--polar); --cs: var(--polar-s); }
.c-basic { --c: var(--basic); --cs: var(--basic-s); }
.c-acid  { --c: var(--acid);  --cs: var(--acid-s);  }
.c-spec  { --c: var(--spec);  --cs: var(--spec-s);  }
.c-unk   { --c: var(--unk);   --cs: var(--unk-s);   }
.c-stop  { --c: var(--ink);   --cs: var(--ink);     }

/* ---------- base ---------- */
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body { -webkit-text-size-adjust: 100%; line-height: 1.5; overflow-x: hidden; }
.wrap { max-width: 1180px; margin: 0 auto; padding: 22px 16px 28px; }
h1, h2, h3 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
p { margin: 0; }
:where(button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px;
}
.sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
      overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ---------- header ---------- */
header { margin-bottom: 16px; }
.eyebrow { font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); display: flex; align-items: center;
  gap: 8px; flex-wrap: wrap; }
.eyebrow .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); flex: none; }
h1 { font-size: clamp(22px, 4.6vw, 30px); margin: 6px 0 6px; }
.lede { color: var(--muted); font-size: 15px; max-width: 70ch; text-wrap: pretty; }
.warnbar { margin-top: 12px; display: flex; gap: 9px; align-items: baseline; flex-wrap: wrap;
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  background: var(--accent-soft); border-radius: 4px; padding: 8px 11px; }
.warnbar b { font-family: var(--font-mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent); flex: none; }
.warnbar span { font-size: 13px; color: var(--ink); max-width: 78ch; }

/* ---------- layout ---------- */
.grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  gap: 14px; align-items: start; }
@media (max-width: 940px) {
  .grid { grid-template-columns: minmax(0, 1fr); }
  .grid > .col { display: contents; }
  #cInput { order: 1; } #cStrip { order: 2; } #cProtein { order: 3; }
  #cRead { order: 4; } #cDetail { order: 5; }
  #cTable { order: 6; } #cMethod { order: 7; }
}
.col { display: grid; gap: 14px; min-width: 0; align-content: start; }
.card { background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 13px; min-width: 0; }
.card > h2, details.card > summary {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); font-weight: 600;
  display: flex; justify-content: space-between; align-items: center; gap: 8px 10px;
  flex-wrap: wrap; }
.card > h2 { margin-bottom: 10px; }
.card > h2 .meta { font-size: 11px; letter-spacing: .04em; text-transform: none;
  color: var(--muted); font-weight: 500; white-space: nowrap; }

/* ---------- controls ---------- */
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 7px;
  overflow: hidden; background: var(--bg); flex: none; }
.seg button { appearance: none; border: 0; background: none; cursor: pointer;
  font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .06em;
  color: var(--muted); padding: 4px 10px; border-right: 1px solid var(--line);
  transition: background .12s, color .12s; }
.seg button:last-child { border-right: 0; }
.seg button:hover { color: var(--ink); }
.seg button[aria-checked="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.seg button:focus-visible { outline-offset: -2px; border-radius: 0; }

.btn { appearance: none; cursor: pointer; font-family: var(--font-mono); font-size: 11.5px;
  padding: 5px 10px; border-radius: 7px; border: 1px solid var(--line);
  background: var(--surface); color: var(--ink); transition: border-color .12s, background .12s; }
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn.ok { border-color: var(--run); color: var(--run); background: var(--run-soft); }
.btn.fail { border-color: var(--danger); color: var(--danger); }
.btnrow { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px; }
.btnrow .lab { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); align-self: center; margin-right: 2px; }

textarea {
  width: 100%; min-height: 108px; resize: vertical; display: block;
  font-family: var(--font-mono); font-size: 12.5px; line-height: 1.7; letter-spacing: .04em;
  color: var(--ink); background: var(--bg); border: 1px solid var(--line);
  border-radius: 8px; padding: 9px 10px; }
textarea::placeholder { color: var(--muted); }
.hint { font-size: 12px; color: var(--muted); margin-top: 8px; text-wrap: pretty; }
.hint code { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink); }
.notice { margin-top: 9px; font-family: var(--font-mono); font-size: 11.5px;
  color: var(--muted); border: 1px solid var(--line); border-left: 3px solid var(--danger);
  border-radius: 4px; padding: 7px 9px; display: flex; flex-wrap: wrap; gap: 4px 12px; }
.notice b { color: var(--danger); font-weight: 600; }
.notice.calm { border-left-color: var(--line); }

/* ---------- codon strip ---------- */
.strip { display: flex; flex-wrap: wrap; gap: 4px 3px; font-family: var(--font-mono);
  font-size: 12.5px; line-height: 1.25; }
.cod { display: flex; flex-direction: column; align-items: center; width: 3ch;
  padding: 1px 0 3px; border-radius: 4px; border: 1px solid transparent;
  cursor: pointer; background: none; transition: background .1s, border-color .1s; }
.cod .ix { font-size: 8.5px; letter-spacing: 0; color: var(--muted); height: 11px;
  line-height: 11px; opacity: .8; }
.cod .d { color: var(--muted); }
.cod .r { color: var(--ink); }
.cod .u { color: var(--accent); font-weight: 600; }
.cod .bad { color: var(--danger); font-weight: 700; }
.cod .amb { color: var(--muted); text-decoration: underline dotted; }
.cod:hover { background: var(--bg); }
.cod.sel { border-color: var(--accent); background: var(--accent-soft); }
.cod.orf { border-bottom-color: var(--c); }
.cod.start .d, .cod.start .r { font-weight: 700; }
.cod.start { background: var(--accent-soft); }
.cod.stopc .r { color: var(--danger); font-weight: 700; }
.cod.off { width: auto; padding: 1px 3px 3px; opacity: .8; cursor: default; }
.cod.off:hover { background: none; }
.stripnote { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted);
  display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 9px; }
.stripnote i { font-style: normal; color: var(--ink); }

/* ---------- beads ---------- */
.beads { display: flex; flex-wrap: wrap; gap: 4px; }
.bead { appearance: none; cursor: pointer; font-family: var(--font-mono);
  font-size: 12.5px; font-weight: 600; line-height: 1;
  width: 28px; height: 28px; padding: 0; border-radius: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--c); background: var(--cs); color: var(--c);
  transition: transform .1s, box-shadow .1s; }
.beads.three .bead { width: auto; min-width: 40px; padding: 0 7px; font-size: 11.5px; }
.bead:hover { transform: translateY(-1px); }
/* Outside the reading frame: drop the fill, keep the letter at full strength.
   (Dimming with opacity here made the residue unreadable — ~1.7:1.) */
.bead.out { background: none; border-color: var(--line); border-style: dashed; }
.bead.c-stop { background: var(--ink); color: var(--surface); }
.bead.c-unk { border-style: dashed; }
.bead.start { box-shadow: inset 0 0 0 2px var(--c); }
.bead[aria-current="true"] { outline: 2px solid var(--accent); outline-offset: 2px; opacity: 1; }
.chainnote { font-size: 12px; color: var(--muted); margin-top: 10px; }

/* ---------- readout ---------- */
.tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px;
  background: var(--line); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.tile { background: var(--surface); padding: 8px 9px; min-width: 0; }
.tile .k { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); }
.tile .v { font-family: var(--font-mono); font-size: 15px; font-weight: 600; color: var(--ink);
  margin-top: 2px; overflow: hidden; text-overflow: ellipsis; }
.status { margin-top: 10px; border-radius: 7px; padding: 8px 10px; font-size: 13px;
  border: 1px solid var(--line); background: var(--bg); color: var(--muted); text-wrap: pretty; }
.status b { font-family: var(--font-mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; display: block; margin-bottom: 3px; }
.status.good { background: var(--run-soft); border-color: var(--run); color: var(--ink); }
.status.good b { color: var(--run); }
.status.warn { background: var(--accent-soft); border-color: var(--accent); color: var(--ink); }
.status.warn b { color: var(--accent); }
.status.none b { color: var(--muted); }

/* ---------- detail ---------- */
.kv { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 4px 10px;
  font-family: var(--font-mono); font-size: 12.5px; }
.kv .k { color: var(--muted); font-size: 10.5px; letter-spacing: .08em;
  text-transform: uppercase; padding-top: 2px; }
.kv .v { color: var(--ink); overflow-wrap: anywhere; }
.kv .v .u { color: var(--accent); }
.kv .v .sw { display: inline-block; width: 9px; height: 9px; border-radius: 3px;
  background: var(--c); margin-right: 6px; vertical-align: baseline; }
.detnote { margin-top: 10px; font-size: 12.5px; color: var(--muted); text-wrap: pretty; }
.detnote.hot { color: var(--accent); }

/* ---------- key ---------- */
.key { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 11px;
  padding-top: 10px; border-top: 1px solid var(--line); }
.key .row { display: flex; align-items: center; gap: 6px; font-size: 12px;
  color: var(--muted); white-space: nowrap; }
.key .chip { width: 13px; height: 13px; border-radius: 4px; flex: none;
  border: 1px solid var(--c); background: var(--cs); }
.key .chip.c-stop { background: var(--ink); }
.key .chip.c-unk { border-style: dashed; }
.key .row span.n { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); }

/* ---------- codon table ---------- */
details.card { padding: 13px; }
details.card > summary { cursor: pointer; list-style: none; }
details.card > summary::-webkit-details-marker { display: none; }
details.card > summary::after { content: "+"; font-size: 14px; color: var(--accent); }
details.card[open] > summary::after { content: "−"; }
details.card[open] > summary { margin-bottom: 11px; }
.ctwrap { display: grid; gap: 11px 12px; grid-template-columns: repeat(auto-fit, minmax(186px, 1fr)); }
.ctlab { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 4px; }
.ctgrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 3px; }
.ct { border: 1px solid var(--line); border-radius: 5px; padding: 4px 2px; text-align: center;
  background: var(--cs); }
.ct .c { font-family: var(--font-mono); font-size: 11.5px; font-weight: 600; color: var(--c); }
.ct .a { font-family: var(--font-mono); font-size: 9.5px; color: var(--muted); letter-spacing: .02em; }
.ct.sel { border-color: var(--accent); outline: 1px solid var(--accent); }
.ct.c-stop { background: var(--bg); }
.ct.c-stop .c { color: var(--danger); }
.body { font-size: 13px; color: var(--muted); display: grid; gap: 9px; text-wrap: pretty; }
.body b { color: var(--ink); font-weight: 600; }
.body code { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink); }

.empty { font-family: var(--font-mono); font-size: 12px; color: var(--muted);
  border: 1px dashed var(--line); border-radius: 7px; padding: 14px; text-align: center; }

footer { margin-top: 22px; padding-top: 12px; border-top: 1px solid var(--line);
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; }

@media (max-width: 420px) {
  .kv { grid-template-columns: 74px minmax(0, 1fr); }
  .bead { width: 26px; height: 26px; }
}
@media (prefers-reduced-motion: reduce) {
  * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
  .bead:hover { transform: none; }
}
</style>
</head>
<body>
<div class="wrap">

<header>
  <div class="eyebrow"><span class="dot" aria-hidden="true"></span>pharma · pure js · no libraries</div>
  <h1>DNA → Protein Translator</h1>
  <p class="lede">Paste DNA. It is transcribed to mRNA, cut into triplets, and each triplet is looked up
  in the genetic code — the same 64-entry table every cell reads. Shift the reading frame by one base and
  watch a clean protein turn into nonsense.</p>
  <div class="warnbar">
    <b>teaching tool</b>
    <span>Standard genetic code only — no introns, no alternative codes, nothing that happens to a protein
    after it is made. Good for learning how the lookup works; not for lab or clinical work.</span>
  </div>
</header>

<div class="grid">

  <div class="col">

    <section class="card" id="cInput">
      <h2>DNA input <span class="meta" id="mInput">—</span></h2>
      <label class="sr" for="seq">DNA sequence</label>
      <textarea id="seq" spellcheck="false" autocapitalize="off" autocorrect="off"
        placeholder="paste a DNA sequence — A, C, G, T (U is read as T)"></textarea>
      <div class="btnrow">
        <span class="lab">load</span>
        <button class="btn" type="button" data-ex="ins">insulin precursor</button>
        <button class="btn" type="button" data-ex="hbb">hemoglobin β</button>
        <button class="btn" type="button" data-ex="messy">messy paste</button>
        <button class="btn" type="button" id="revcomp">reverse complement ⇄</button>
      </div>
      <p class="hint">Line numbers, spaces and <code>&gt;</code> header lines are ignored. Lowercase is fine.
      <code>N</code> and the other ambiguity letters are resolved whenever every possibility gives the same
      amino acid. Anything else is flagged and read as unknown.</p>
      <div class="notice" id="notice" hidden></div>
    </section>

    <section class="card" id="cStrip">
      <h2>Transcript
        <span class="seg" role="radiogroup" aria-label="Reading frame" id="frames">
          <button type="button" role="radio" aria-checked="true" data-v="0" tabindex="0">frame 1</button>
          <button type="button" role="radio" aria-checked="false" data-v="1" tabindex="-1">frame 2</button>
          <button type="button" role="radio" aria-checked="false" data-v="2" tabindex="-1">frame 3</button>
        </span>
      </h2>
      <div class="stripnote">
        <span><i>top</i> DNA</span>
        <span><i>bottom</i> mRNA, T→<i style="color:var(--accent)">U</i></span>
        <span><i>dimmed</i> bases outside the frame</span>
      </div>
      <div class="strip" id="strip" role="group" aria-label="DNA cut into codons with its mRNA transcript"></div>
    </section>

    <section class="card" id="cProtein">
      <h2>Protein chain
        <span class="seg" role="radiogroup" aria-label="Amino acid notation" id="notation">
          <button type="button" role="radio" aria-checked="true" data-v="1" tabindex="0">1-letter</button>
          <button type="button" role="radio" aria-checked="false" data-v="3" tabindex="-1">3-letter</button>
        </span>
      </h2>
      <div class="beads" id="beads" role="group" aria-label="Amino acid chain"></div>
      <div class="key">
        <div class="row c-hydro"><span class="chip c-hydro"></span>hydrophobic <span class="n">A V L I M</span></div>
        <div class="row c-arom"><span class="chip c-arom"></span>aromatic <span class="n">F W Y</span></div>
        <div class="row c-polar"><span class="chip c-polar"></span>polar <span class="n">S T N Q</span></div>
        <div class="row c-basic"><span class="chip c-basic"></span>basic + <span class="n">K R H</span></div>
        <div class="row c-acid"><span class="chip c-acid"></span>acidic − <span class="n">D E</span></div>
        <div class="row c-spec"><span class="chip c-spec"></span>special <span class="n">G P C</span></div>
        <div class="row c-stop"><span class="chip c-stop"></span>stop</div>
        <div class="row c-unk"><span class="chip c-unk"></span>unreadable</div>
      </div>
      <p class="chainnote">Click a bead to inspect its codon — or focus the chain and walk it with
      <b>←</b> / <b>→</b>. Residues outside the start-to-stop stretch are drawn hollow.</p>
    </section>

  </div>

  <div class="col">

    <section class="card" id="cRead">
      <h2>Readout</h2>
      <div class="tiles">
        <div class="tile"><div class="k">bases</div><div class="v" id="tBases">—</div></div>
        <div class="tile"><div class="k">gc</div><div class="v" id="tGC">—</div></div>
        <div class="tile"><div class="k">codons</div><div class="v" id="tCodons">—</div></div>
        <div class="tile"><div class="k">residues</div><div class="v" id="tRes">—</div></div>
        <div class="tile"><div class="k">stops</div><div class="v" id="tStops">—</div></div>
        <div class="tile"><div class="k">start→stop</div><div class="v" id="tOrf">—</div></div>
      </div>
      <div class="status none" id="status" role="status"><b>—</b><span id="statusText">waiting for a sequence</span></div>
      <div class="btnrow">
        <button class="btn" type="button" id="copyAA">copy protein</button>
        <button class="btn" type="button" id="copyRNA">copy mRNA</button>
      </div>
    </section>

    <section class="card" id="cDetail">
      <h2>Selected codon</h2>
      <div class="kv" id="detail"></div>
      <p class="detnote" id="detnote"></p>
    </section>

    <details class="card" id="cTable" open>
      <summary>the codon table · 64 entries</summary>
      <div class="ctwrap" id="ctwrap"></div>
      <p class="hint">Shown in mRNA letters, grouped by first base; rows are the second base, columns the
      third. The selected codon is ringed. Notice how often a whole row of four says the same thing —
      that redundancy is why many single-base typos change nothing.</p>
    </details>

    <details class="card" id="cMethod">
      <summary>how this works</summary>
      <div class="body">
        <p>The whole translator is one string: <code>FFLLSSSSYY**CC*W…</code> — 64 amino-acid letters in
        codon order, unpacked into a lookup map at load. Every triplet is one map read.</p>
        <p><b>Transcription</b> here is just T→U. Real cells copy the template strand, so the mRNA matches
        the coding strand you pasted — which is why the letters look unchanged apart from the U.</p>
        <p><b>Reading frame.</b> Nothing in the sequence says where to start counting. Frames 2 and 3 shift
        the cut by one and two bases, and almost always shatter into stop codons — that is the point.</p>
        <p><b>Ambiguity.</b> A codon containing <code>N</code> (or R, Y, S, W, K, M, B, D, H, V) is expanded
        into every base it could be. If all of them land on the same amino acid — <code>TCN</code> is always
        serine — the residue is called. If not, it is <code>X</code>.</p>
        <p><b>Stop codon nicknames</b> are the old lab ones: TAA ochre, TAG amber, TGA opal.</p>
        <p>What this does not do: introns and splicing, alternative codon tables, signal-peptide cleavage,
        folding, or anything else that happens after the ribosome lets go. The insulin precursor loaded
        here becomes actual insulin only after a lot of cutting this page knows nothing about.</p>
      </div>
    </details>

  </div>
</div>

<footer>
  <span>demo · sloprun.dev</span>
  <span>standard genetic code · 64 codons</span>
</footer>

</div>

<script>
(function () {
  'use strict';
  function $(id) { return document.getElementById(id); }

  /* ---------------- the embedded genetic code ----------------
     64 amino-acid letters, bases ordered T,C,A,G at each of the three positions. */
  var CODE = 'FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG';
  var ORD = 'TCAG';
  var TABLE = {};
  for (var i1 = 0; i1 < 4; i1++)
    for (var i2 = 0; i2 < 4; i2++)
      for (var i3 = 0; i3 < 4; i3++)
        TABLE[ORD.charAt(i1) + ORD.charAt(i2) + ORD.charAt(i3)] = CODE.charAt(i1 * 16 + i2 * 4 + i3);

  var AA = {
    A: ['Ala', 'alanine', 'hydro'],       R: ['Arg', 'arginine', 'basic'],
    N: ['Asn', 'asparagine', 'polar'],    D: ['Asp', 'aspartic acid', 'acid'],
    C: ['Cys', 'cysteine', 'spec'],       E: ['Glu', 'glutamic acid', 'acid'],
    Q: ['Gln', 'glutamine', 'polar'],     G: ['Gly', 'glycine', 'spec'],
    H: ['His', 'histidine', 'basic'],     I: ['Ile', 'isoleucine', 'hydro'],
    L: ['Leu', 'leucine', 'hydro'],       K: ['Lys', 'lysine', 'basic'],
    M: ['Met', 'methionine', 'hydro'],    F: ['Phe', 'phenylalanine', 'arom'],
    P: ['Pro', 'proline', 'spec'],        S: ['Ser', 'serine', 'polar'],
    T: ['Thr', 'threonine', 'polar'],     W: ['Trp', 'tryptophan', 'arom'],
    Y: ['Tyr', 'tyrosine', 'arom'],       V: ['Val', 'valine', 'hydro'],
    '*': ['Stop', 'stop', 'stop'],        X: ['Xaa', 'unknown', 'unk']
  };
  var CLASSNAME = { hydro: 'hydrophobic', arom: 'aromatic', polar: 'polar',
    basic: 'basic (+)', acid: 'acidic (−)', spec: 'special', stop: 'stop', unk: 'unreadable' };
  var STOPNICK = { TAA: 'ochre', TAG: 'amber', TGA: 'opal' };
  var IUPAC = { A: 'A', C: 'C', G: 'G', T: 'T', R: 'AG', Y: 'CT', S: 'CG', W: 'AT',
    K: 'GT', M: 'AC', B: 'CGT', D: 'AGT', H: 'ACT', V: 'ACG', N: 'ACGT' };
  var COMPL = { A: 'T', T: 'A', G: 'C', C: 'G', R: 'Y', Y: 'R', S: 'S', W: 'W',
    K: 'M', M: 'K', B: 'V', V: 'B', D: 'H', H: 'D', N: 'N' };
  var AMBIG = 'RYSWKMBDHVN';
  var MAX_BASES = 60000;
  var MAX_DRAW = 1500;

  function translate(codon) {
    var sets = [];
    for (var i = 0; i < 3; i++) {
      var s = IUPAC[codon.charAt(i)];
      if (!s) return 'X';
      sets.push(s);
    }
    var found = null;
    for (var a = 0; a < sets[0].length; a++)
      for (var b = 0; b < sets[1].length; b++)
        for (var c = 0; c < sets[2].length; c++) {
          var aa = TABLE[sets[0].charAt(a) + sets[1].charAt(b) + sets[2].charAt(c)];
          if (found === null) found = aa;
          else if (found !== aa) return 'X';
        }
    return found === null ? 'X' : found;
  }

  /* ---------------- examples ---------------- */
  var EX = {
    ins:
      'GATCACTGTCCTTCTGCC\n' +
      'ATGGCCCTGTGGATGCGCCTCCTGCCCCTGCTGGCGCTGCTGGCCCTCTGGGGACCTGACCCAGCCGCAGCC\n' +
      'TTTGTGAACCAACACCTGTGCGGCTCACACCTGGTGGAAGCTCTCTACCTAGTGTGCGGGGAACGAGGCTTC\n' +
      'TTCTACACACCCAAGACCCGCCGGGAGGCAGAGGACCTGCAGGTGGGGCAGGTGGAGCTGGGCGGGGGCCCT\n' +
      'GGTGCAGGCAGCCTGCAGCCCTTGGCCCTGGAGGGGTCCCTGCAGAAGCGTGGCATTGTGGAACAATGCTGT\n' +
      'ACCAGCATCTGCTCCCTCTACCAGCTGGAGAACTACTGCAACTAG',
    hbb:
      'ATGGTGCATCTGACTCCTGAGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGGTGAACGTGGATGAAGTT\n' +
      'GGTGGTGAGGCCCTGGGCAGGCTGCTGGTGGTCTACCCTTGGACC',
    messy:
      '>notebook paste — two flagged bases, one stray letter\n' +
      '  1 atggccctgt ggatgcgcct cctgcccctg ctggcgctgc\n' +
      ' 41 tggccctctg gggacctgac ccagccgcag cctttgtgaa\n' +
      ' 81 ccaacacctg tgcggctcnc acctggtgga agctctctnc\n' +
      '121 ctagtgtgcg gggaacgagg cttcttctac acacccaaga\n' +
      '161 cccgccggga ggcagaggac ctgcaggtgz ggcaggtgga\n' +
      '201 gctgggcggg ggccctggtg caggcagcct gcagcccttg\n' +
      '241 gccctggagg ggtccctgca gaagcgtggc attgtggaac\n' +
      '281 aatgctgtac cagcatctgc tccctctacc agctggagaa\n' +
      '321 ctactgcaac tag'
  };

  /* ---------------- parsing ---------------- */
  function parse(raw) {
    var bases = [], bad = {}, badN = 0, ambN = 0, ignored = 0, headers = 0, gc = 0, truncated = false;
    var lines = raw.split(/\r?\n/);
    for (var li = 0; li < lines.length; li++) {
      var line = lines[li];
      if (/^\s*>/.test(line)) { headers++; continue; }
      for (var i = 0; i < line.length; i++) {
        if (bases.length >= MAX_BASES) { truncated = true; break; }
        var ch = line.charAt(i).toUpperCase();
        if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\f' || ch === '\v'
            || (ch >= '0' && ch <= '9') || ch === '-' || ch === '.') {
          ignored++; continue;
        }
        if (ch === 'U') ch = 'T';
        if (ch === 'A' || ch === 'C' || ch === 'G' || ch === 'T') {
          if (ch === 'G' || ch === 'C') gc++;
          bases.push({ c: ch, ok: true, amb: false });
        } else if (AMBIG.indexOf(ch) >= 0) {
          ambN++;
          bases.push({ c: ch, ok: true, amb: true });
        } else {
          badN++; bad[ch] = (bad[ch] || 0) + 1;
          bases.push({ c: ch, ok: false, amb: false });
        }
      }
      if (truncated) break;
    }
    return { bases: bases, bad: bad, badN: badN, ambN: ambN, ignored: ignored,
      headers: headers, gc: gc, truncated: truncated };
  }

  function frameModel(bases, frame) {
    var codons = [], stops = 0, orfStart = -1, orfStop = -1;
    for (var i = frame; i + 3 <= bases.length; i += 3) {
      var t0 = bases[i], t1 = bases[i + 1], t2 = bases[i + 2];
      var dna = t0.c + t1.c + t2.c;
      var aa = translate(dna);
      var c = {
        i: codons.length, at: i, dna: dna, rna: dna.replace(/T/g, 'U'), aa: aa,
        bases: [t0, t1, t2],
        ok: t0.ok && t1.ok && t2.ok,
        amb: !!(t0.amb || t1.amb || t2.amb),
        isStart: dna === 'ATG',
        isStop: aa === '*'
      };
      if (c.isStop) stops++;
      if (orfStart < 0 && c.isStart) orfStart = c.i;
      if (orfStart >= 0 && orfStop < 0 && c.isStop && c.i > orfStart) orfStop = c.i;
      codons.push(c);
    }
    return { codons: codons, stops: stops, orfStart: orfStart, orfStop: orfStop,
      lead: Math.min(frame, bases.length),
      tail: bases.length - frame - codons.length * 3 };
  }

  /* ---------------- state ---------------- */
  var S = { frame: 0, three: false, sel: -1, p: null, m: null, moved: false };

  function el(tag, cls, text) {
    var n = document.createElement(tag);
    if (cls) n.className = cls;
    if (text !== undefined) n.textContent = text;
    return n;
  }
  function cls(aa) { return 'c-' + (AA[aa] ? AA[aa][2] : 'unk'); }
  function inOrf(m, i) {
    return m.orfStart >= 0 && i >= m.orfStart && (m.orfStop < 0 ? true : i <= m.orfStop);
  }

  /* ---------------- render: codon strip ---------------- */
  function baseChars(parent, trio, rna) {
    for (var i = 0; i < trio.length; i++) {
      var b = trio[i];
      var ch = (rna && b.c === 'T') ? 'U' : b.c;
      var k = !b.ok ? 'bad' : (b.amb ? 'amb' : (rna && b.c === 'T' ? 'u' : ''));
      if (k) { var s = el('span', k, ch); parent.appendChild(s); }
      else parent.appendChild(document.createTextNode(ch));
    }
  }
  function offTile(bases, from, count, label) {
    if (count <= 0) return null;
    var t = el('span', 'cod off');
    t.appendChild(el('span', 'ix', label));
    var d = el('span', 'd'), r = el('span', 'r');
    var trio = bases.slice(from, from + count);
    baseChars(d, trio, false);
    baseChars(r, trio, true);
    t.appendChild(d); t.appendChild(r);
    t.title = count + (count === 1 ? ' base' : ' bases') + ' outside this reading frame';
    return t;
  }
  function renderStrip() {
    var host = $('strip');
    host.textContent = '';
    var m = S.m, bases = S.p.bases;
    if (!m.codons.length) {
      host.appendChild(el('div', 'empty', bases.length
        ? 'fewer than three readable bases in this frame'
        : 'no bases yet — paste a sequence, or load an example above'));
      return;
    }
    var frag = document.createDocumentFragment();
    var t = offTile(bases, 0, m.lead, "5′");
    if (t) frag.appendChild(t);
    var n = Math.min(m.codons.length, MAX_DRAW);
    for (var i = 0; i < n; i++) {
      var c = m.codons[i];
      var tile = el('span', 'cod ' + cls(c.aa));
      if (inOrf(m, i)) tile.className += ' orf';
      if (c.isStart) tile.className += ' start';
      if (c.isStop) tile.className += ' stopc';
      if (i === S.sel) tile.className += ' sel';
      tile.setAttribute('data-i', i);
      tile.appendChild(el('span', 'ix', (i + 1) % 10 === 0 ? String(i + 1) : ''));
      var d = el('span', 'd'), r = el('span', 'r');
      baseChars(d, c.bases, false);
      baseChars(r, c.bases, true);
      tile.appendChild(d); tile.appendChild(r);
      tile.title = 'codon ' + (i + 1) + ' · ' + c.dna + ' → ' + c.rna + ' · ' + AA[c.aa][0];
      frag.appendChild(tile);
    }
    if (m.codons.length > MAX_DRAW) {
      frag.appendChild(el('span', 'cod off', '…'));
    }
    var tt = offTile(bases, bases.length - m.tail, m.tail, "3′");
    if (tt) frag.appendChild(tt);
    host.appendChild(frag);
  }

  /* ---------------- render: beads ---------------- */
  function renderBeads() {
    var host = $('beads');
    host.textContent = '';
    host.className = 'beads' + (S.three ? ' three' : '');
    var m = S.m;
    if (!m.codons.length) {
      host.appendChild(el('div', 'empty', 'nothing to translate yet'));
      return;
    }
    var frag = document.createDocumentFragment();
    var n = Math.min(m.codons.length, MAX_DRAW);
    for (var i = 0; i < n; i++) {
      var c = m.codons[i];
      var meta = AA[c.aa];
      var b = el('button', 'bead ' + cls(c.aa), S.three ? meta[0] : c.aa);
      b.type = 'button';
      b.setAttribute('data-i', i);
      b.tabIndex = (i === S.sel) ? 0 : -1;
      b.setAttribute('aria-current', i === S.sel ? 'true' : 'false');
      /* Only mark residues as "outside" when there IS a reading frame to be
         outside of — otherwise every bead in frames 2/3 renders as a ghost. */
      if (m.orfStart >= 0 && !inOrf(m, i)) b.className += ' out';
      if (c.isStart) b.className += ' start';
      b.setAttribute('aria-label', 'codon ' + (i + 1) + ', ' + c.rna + ', ' + meta[1]
        + (c.isStart ? ', start codon' : '') + (c.isStop ? ', stop codon' : ''));
      frag.appendChild(b);
    }
    host.appendChild(frag);
    if (m.codons.length > MAX_DRAW) {
      var note = el('div', 'empty', 'long sequence — showing the first ' + MAX_DRAW + ' codons');
      note.style.width = '100%';
      host.appendChild(note);
    }
  }

  /* ---------------- render: readout ---------------- */
  function renderReadout() {
    var p = S.p, m = S.m;
    var n = p.bases.length;
    $('tBases').textContent = n ? String(n) : '—';
    $('tGC').textContent = n ? (p.gc / n * 100).toFixed(1) + '%' : '—';
    $('tCodons').textContent = m.codons.length ? String(m.codons.length) : '—';
    $('tRes').textContent = m.codons.length ? String(m.codons.length - m.stops) : '—';
    $('tStops').textContent = m.codons.length ? String(m.stops) : '—';

    var st = $('status'), txt = $('statusText'), lab = st.firstElementChild;
    var orfCell = $('tOrf');
    if (!m.codons.length) {
      orfCell.textContent = '—';
      st.className = 'status none'; lab.textContent = '—';
      txt.textContent = n ? 'not enough bases to make a codon in this frame.'
                          : 'waiting for a sequence.';
    } else if (m.orfStart >= 0 && m.orfStop >= 0) {
      var len = m.orfStop - m.orfStart;
      orfCell.textContent = len + ' aa';
      st.className = 'status good'; lab.textContent = 'complete reading frame';
      txt.textContent = 'ATG at codon ' + (m.orfStart + 1) + ' runs to '
        + m.codons[m.orfStop].dna + ' (' + (STOPNICK[m.codons[m.orfStop].dna] || 'stop')
        + ') at codon ' + (m.orfStop + 1) + ' — ' + len + ' amino acids, start to stop.';
    } else if (m.orfStart >= 0) {
      orfCell.textContent = 'open';
      st.className = 'status warn'; lab.textContent = 'runs off the end';
      txt.textContent = 'ATG at codon ' + (m.orfStart + 1)
        + ', but no stop codon after it — the sequence ends mid-protein.';
    } else {
      orfCell.textContent = 'none';
      st.className = 'status warn'; lab.textContent = 'no start codon';
      txt.textContent = 'no ATG in this frame'
        + (m.stops ? ', and ' + m.stops + ' stop codon' + (m.stops === 1 ? '' : 's') + ' chop it up' : '')
        + '. Try another frame.';
    }

    var meta = [];
    if (n) meta.push(n + ' bases');
    if (n) meta.push('GC ' + (p.gc / n * 100).toFixed(1) + '%');
    $('mInput').textContent = meta.length ? meta.join(' · ') : '—';

    var nt = $('notice');
    nt.textContent = '';
    nt.className = 'notice' + (p.badN ? '' : ' calm');
    var first = true;
    if (p.badN) {
      var keys = Object.keys(p.bad).sort();
      var lead = el('span');
      lead.appendChild(el('b', null, p.badN + ' unreadable character'
        + (p.badN === 1 ? '' : 's') + ' (' + keys.join(' ') + ')'));
      lead.appendChild(document.createTextNode(' kept in place, read as unknown'));
      nt.appendChild(lead);
      first = false;
    }
    var parts = [];
    if (p.ambN) parts.push(p.ambN + ' ambiguity code' + (p.ambN === 1 ? '' : 's') + ' resolved where possible');
    if (p.ignored) parts.push(p.ignored + ' spacing/number character' + (p.ignored === 1 ? '' : 's') + ' ignored');
    if (p.headers) parts.push(p.headers + ' header line' + (p.headers === 1 ? '' : 's') + ' skipped');
    if (p.truncated) parts.push('input trimmed to ' + MAX_BASES + ' bases');
    for (var i = 0; i < parts.length; i++) {
      nt.appendChild(el('span', null, (first && i === 0 ? '' : '· ') + parts[i]));
    }
    nt.hidden = !(p.badN || parts.length);
  }

  /* ---------------- render: detail ---------------- */
  function renderDetail() {
    var host = $('detail'), note = $('detnote');
    host.textContent = ''; note.textContent = ''; note.className = 'detnote';
    var m = S.m;
    if (!m.codons.length || S.sel < 0 || S.sel >= m.codons.length) {
      host.appendChild(el('div', 'k', 'codon'));
      host.appendChild(el('div', 'v', '—'));
      note.textContent = 'Nothing selected.';
      highlightTable(null);
      return;
    }
    var c = m.codons[S.sel], meta = AA[c.aa];
    function row(k, node) {
      host.appendChild(el('div', 'k', k));
      var v = el('div', 'v');
      if (typeof node === 'string') v.textContent = node; else v.appendChild(node);
      host.appendChild(v);
    }
    row('codon', (S.sel + 1) + ' of ' + m.codons.length);
    row('bases', (c.at + 1) + '–' + (c.at + 3));
    var conv = el('span');
    conv.appendChild(document.createTextNode(c.dna + '  →  '));
    for (var i = 0; i < 3; i++) {
      var ch = c.rna.charAt(i);
      if (ch === 'U') conv.appendChild(el('span', 'u', 'U'));
      else conv.appendChild(document.createTextNode(ch));
    }
    row('dna → mrna', conv);
    var resid = el('span');
    var sw = el('span', 'sw ' + cls(c.aa));
    resid.appendChild(sw);
    resid.appendChild(document.createTextNode(
      c.aa === '*' ? 'Stop' : meta[0] + ' · ' + c.aa));
    resid.className = cls(c.aa);
    row('residue', resid);
    row('name', meta[1]);
    row('class', CLASSNAME[meta[2]]);

    if (c.isStop) {
      note.className = 'detnote hot';
      note.textContent = 'Stop codon (' + (STOPNICK[c.dna] || 'stop')
        + ') — no amino acid, the chain is released here.';
    } else if (!c.ok) {
      note.className = 'detnote hot';
      note.textContent = 'This triplet contains a character that is not a base, so it cannot be read.';
    } else if (c.amb && c.aa === 'X') {
      note.className = 'detnote hot';
      note.textContent = 'Ambiguous: the possible bases lead to different amino acids, so the residue is unknown.';
    } else if (c.amb) {
      note.textContent = 'Ambiguous, but every possibility gives ' + meta[1] + ' — so the residue is certain anyway.';
    } else if (S.sel === m.orfStart) {
      note.className = 'detnote hot';
      note.textContent = 'Start codon — the first ATG in this frame. Translation begins here.';
    } else if (c.isStart) {
      note.textContent = 'Another ATG, downstream of the first. Methionine doubles as the start signal.';
    } else if (inOrf(m, S.sel)) {
      note.textContent = 'Residue ' + (S.sel - m.orfStart + 1) + ' of the reading frame.';
    } else if (m.orfStart < 0) {
      note.textContent = 'No ATG anywhere in this frame, so nothing here would ever be '
        + 'translated — this triplet is only what the letters happen to spell.';
    } else {
      note.textContent = 'Outside the reading frame — read, but never translated in a cell.';
    }
    highlightTable(c.rna);
  }

  /* ---------------- codon table ---------------- */
  var ctCells = {};
  function buildTable() {
    var host = $('ctwrap'), frag = document.createDocumentFragment();
    for (var a = 0; a < 4; a++) {
      var first = ORD.charAt(a) === 'T' ? 'U' : ORD.charAt(a);
      var block = el('div', 'ctblock');
      block.appendChild(el('div', 'ctlab', 'first base ' + first));
      var grid = el('div', 'ctgrid');
      for (var b = 0; b < 4; b++)
        for (var c = 0; c < 4; c++) {
          var dna = ORD.charAt(a) + ORD.charAt(b) + ORD.charAt(c);
          var aa = TABLE[dna], rna = dna.replace(/T/g, 'U');
          var cell = el('div', 'ct ' + cls(aa));
          cell.appendChild(el('div', 'c', rna));
          cell.appendChild(el('div', 'a', AA[aa][0]));
          ctCells[rna] = cell;
          grid.appendChild(cell);
        }
      block.appendChild(grid);
      frag.appendChild(block);
    }
    host.appendChild(frag);
  }
  var ctSel = null;
  function highlightTable(rna) {
    if (ctSel) ctSel.classList.remove('sel');
    ctSel = ctCells[rna] || null;
    if (ctSel) ctSel.classList.add('sel');
  }

  /* ---------------- selection ---------------- */
  function markSelection(scroll) {
    var beads = $('beads').children, strip = $('strip').children, i;
    for (i = 0; i < beads.length; i++) {
      var b = beads[i];
      if (!b.getAttribute) continue;
      var bi = b.getAttribute('data-i');
      if (bi === null) continue;
      var on = (+bi === S.sel);
      b.setAttribute('aria-current', on ? 'true' : 'false');
      b.tabIndex = on ? 0 : -1;
      if (on && scroll) { try { b.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } catch (e) { } }
    }
    for (i = 0; i < strip.length; i++) {
      var t = strip[i];
      if (!t.getAttribute) continue;
      var ti = t.getAttribute('data-i');
      if (ti === null) continue;
      t.classList.toggle('sel', +ti === S.sel);
    }
    renderDetail();
  }
  function select(i, scroll, focus) {
    var m = S.m;
    if (!m.codons.length) return;
    if (i < 0) i = 0;
    if (i > Math.min(m.codons.length, MAX_DRAW) - 1) i = Math.min(m.codons.length, MAX_DRAW) - 1;
    S.sel = i;
    markSelection(scroll);
    if (focus) {
      var b = $('beads').querySelector('[data-i="' + i + '"]');
      if (b) b.focus();
    }
  }

  /* ---------------- build ---------------- */
  function build(resetSel) {
    S.p = parse($('seq').value);
    S.m = frameModel(S.p.bases, S.frame);
    var n = Math.min(S.m.codons.length, MAX_DRAW);
    if (resetSel || S.sel < 0 || S.sel >= n) {
      S.sel = (S.m.orfStart >= 0 && S.m.orfStart < n) ? S.m.orfStart : (n ? 0 : -1);
    }
    renderStrip();
    renderBeads();
    renderReadout();
    renderDetail();
  }

  /* ---------------- segmented controls ---------------- */
  function seg(id, onpick) {
    var host = $(id);
    var btns = [].slice.call(host.querySelectorAll('button'));
    function pick(idx, focus) {
      btns.forEach(function (b, i) {
        var on = i === idx;
        b.setAttribute('aria-checked', on ? 'true' : 'false');
        b.tabIndex = on ? 0 : -1;
      });
      if (focus) btns[idx].focus();
      onpick(btns[idx].getAttribute('data-v'));
    }
    btns.forEach(function (b, i) {
      b.addEventListener('click', function () { pick(i, false); });
    });
    host.addEventListener('keydown', function (e) {
      var cur = btns.indexOf(document.activeElement);
      if (cur < 0) return;
      var next = null;
      if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = (cur + 1) % btns.length;
      else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = (cur - 1 + btns.length) % btns.length;
      else if (e.key === 'Home') next = 0;
      else if (e.key === 'End') next = btns.length - 1;
      if (next !== null) { e.preventDefault(); pick(next, true); }
    });
  }

  /* ---------------- copy ---------------- */
  function fallbackCopy(text) {
    try {
      var ta = document.createElement('textarea');
      ta.value = text;
      ta.setAttribute('readonly', '');
      ta.style.position = 'fixed';
      ta.style.top = '-2000px';
      document.body.appendChild(ta);
      ta.select();
      var ok = document.execCommand('copy');
      document.body.removeChild(ta);
      return ok;
    } catch (e) { return false; }
  }
  function copy(btn, label, text) {
    if (!text) { flash(btn, label, false, 'nothing to copy'); return; }
    var done = function (ok) { flash(btn, label, ok, ok ? 'copied ✓' : 'copy blocked'); };
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text).then(function () { done(true); },
        function () { done(fallbackCopy(text)); });
    } else {
      done(fallbackCopy(text));
    }
  }
  var flashTimers = {};
  function flash(btn, label, ok, msg) {
    btn.textContent = msg;
    btn.classList.toggle('ok', ok);
    btn.classList.toggle('fail', !ok);
    clearTimeout(flashTimers[label]);
    flashTimers[label] = setTimeout(function () {
      btn.textContent = label;
      btn.classList.remove('ok', 'fail');
    }, 1500);
  }
  function proteinText() {
    var out = [];
    for (var i = 0; i < S.m.codons.length; i++) {
      var c = S.m.codons[i];
      out.push(S.three ? AA[c.aa][0] : c.aa);
    }
    return S.three ? out.join('-') : out.join('');
  }
  function rnaText() {
    var out = [];
    for (var i = 0; i < S.m.codons.length; i++) out.push(S.m.codons[i].rna);
    return out.join(' ');
  }

  /* ---------------- wiring ---------------- */
  var debounce = null;
  $('seq').addEventListener('input', function () {
    clearTimeout(debounce);
    debounce = setTimeout(function () { debounce = null; build(false); }, 110);
  });
  /* Any control that reads S.p / S.m must first settle a pending keystroke,
     or it acts on the sequence as it was up to 110ms ago. */
  function flush() {
    if (debounce !== null) { clearTimeout(debounce); debounce = null; build(false); }
  }

  seg('frames', function (v) {
    var f = +v;
    if (f === S.frame) return;
    S.frame = f;
    build(true);
  });
  seg('notation', function (v) {
    var three = v === '3';
    if (three === S.three) return;
    S.three = three;
    renderBeads();
    markSelection(false);
  });

  [].slice.call(document.querySelectorAll('[data-ex]')).forEach(function (b) {
    b.addEventListener('click', function () {
      $('seq').value = EX[b.getAttribute('data-ex')];
      build(true);
    });
  });

  $('revcomp').addEventListener('click', function () {
    flush();
    var b = S.p.bases, out = [];
    if (!b.length) { flash(this, 'reverse complement ⇄', false, 'no sequence'); return; }
    for (var i = b.length - 1; i >= 0; i--) out.push(COMPL[b[i].c] || b[i].c);
    var s = out.join(''), lines = [];
    for (var j = 0; j < s.length; j += 72) lines.push(s.slice(j, j + 72));
    $('seq').value = lines.join('\n');
    build(true);
  });

  $('copyAA').addEventListener('click', function () { flush(); copy(this, 'copy protein', proteinText()); });
  $('copyRNA').addEventListener('click', function () { flush(); copy(this, 'copy mRNA', rnaText()); });

  $('beads').addEventListener('click', function (e) {
    var t = e.target.closest ? e.target.closest('.bead') : null;
    if (!t) return;
    select(+t.getAttribute('data-i'), false, false);
  });
  $('beads').addEventListener('focusin', function (e) {
    var t = e.target.closest ? e.target.closest('.bead') : null;
    if (!t) return;
    var i = +t.getAttribute('data-i');
    if (i !== S.sel) select(i, false, false);
  });
  $('beads').addEventListener('keydown', function (e) {
    var next = null;
    if (e.key === 'ArrowRight') next = S.sel + 1;
    else if (e.key === 'ArrowLeft') next = S.sel - 1;
    else if (e.key === 'Home') next = 0;
    else if (e.key === 'End') next = Math.min(S.m.codons.length, MAX_DRAW) - 1;
    if (next === null) return;
    e.preventDefault();
    select(next, true, true);
  });
  $('strip').addEventListener('click', function (e) {
    var t = e.target.closest ? e.target.closest('.cod') : null;
    if (!t) return;
    var i = t.getAttribute('data-i');
    if (i === null) return;
    select(+i, false, false);
  });

  /* ---------------- theme ---------------- */
  window.addEventListener('message', function (e) {
    var d = e.data;
    if (d && d.type === 'sloprun:theme' && (d.theme === 'light' || d.theme === 'dark')) {
      document.documentElement.setAttribute('data-theme', d.theme);
    }
  });

  /* ---------------- boot ---------------- */
  buildTable();
  $('seq').value = EX.ins;
  build(true);
})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong