Last active
February 19, 2026 22:14
-
-
Save jbdamask/3fbc9cc2c81c1c9e0212af8a17b717aa to your computer and use it in GitHub Desktop.
How cells commit to their identity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Decoding Cell Identity — Dynamical Trap Spaces</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0a0a0f; | |
| --bg2: #0e0e18; | |
| --surface: #14142a; | |
| --border: #1e1e3a; | |
| --text: #e0dfe8; | |
| --text-dim: #8887a0; | |
| --accent: #00e5a0; | |
| --accent2: #00b8ff; | |
| --accent3: #ff6b9d; | |
| --accent4: #ffd166; | |
| --glow: rgba(0, 229, 160, 0.15); | |
| --glow2: rgba(0, 184, 255, 0.15); | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| html { | |
| scroll-behavior: smooth; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--accent) var(--bg); | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 17px; | |
| line-height: 1.7; | |
| overflow-x: hidden; | |
| } | |
| /* ======================== HERO ======================== */ | |
| .hero { | |
| position: relative; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| text-align: center; | |
| padding: 2rem; | |
| overflow: hidden; | |
| } | |
| .hero-bg { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 0; | |
| } | |
| .hero-bg canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .hero-content { | |
| position: relative; | |
| z-index: 1; | |
| max-width: 900px; | |
| } | |
| .hero-tag { | |
| display: inline-block; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.75rem; | |
| letter-spacing: 0.2em; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| border: 1px solid var(--accent); | |
| padding: 0.4em 1.2em; | |
| border-radius: 100px; | |
| margin-bottom: 2rem; | |
| opacity: 0; | |
| animation: fadeUp 0.8s 0.3s forwards; | |
| } | |
| .hero h1 { | |
| font-family: 'Instrument Serif', serif; | |
| font-size: clamp(2.8rem, 7vw, 5.5rem); | |
| line-height: 1.08; | |
| font-weight: 400; | |
| color: #fff; | |
| margin-bottom: 1.5rem; | |
| opacity: 0; | |
| animation: fadeUp 0.8s 0.5s forwards; | |
| } | |
| .hero h1 em { | |
| font-style: italic; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .hero-sub { | |
| font-size: 1.15rem; | |
| color: var(--text-dim); | |
| max-width: 600px; | |
| margin: 0 auto 3rem; | |
| opacity: 0; | |
| animation: fadeUp 0.8s 0.7s forwards; | |
| } | |
| .scroll-cue { | |
| opacity: 0; | |
| animation: fadeUp 0.8s 0.9s forwards; | |
| } | |
| .scroll-cue a { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| color: var(--accent); | |
| text-decoration: none; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.8rem; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| transition: gap 0.3s; | |
| } | |
| .scroll-cue a:hover { gap: 0.8rem; } | |
| .scroll-cue .arrow { | |
| display: inline-block; | |
| animation: bounce 2s infinite; | |
| font-size: 1.2rem; | |
| } | |
| @keyframes bounce { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(6px); } | |
| } | |
| @keyframes fadeUp { | |
| from { opacity: 0; transform: translateY(30px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* ======================== SECTIONS ======================== */ | |
| section { | |
| padding: 6rem 2rem; | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| } | |
| .section-label { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.7rem; | |
| letter-spacing: 0.25em; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| margin-bottom: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .section-label::before { | |
| content: ''; | |
| display: block; | |
| width: 40px; | |
| height: 1px; | |
| background: var(--accent); | |
| } | |
| section h2 { | |
| font-family: 'Instrument Serif', serif; | |
| font-size: clamp(2rem, 4vw, 3rem); | |
| font-weight: 400; | |
| color: #fff; | |
| margin-bottom: 1.5rem; | |
| line-height: 1.15; | |
| } | |
| section p { | |
| color: var(--text-dim); | |
| margin-bottom: 1.5rem; | |
| max-width: 720px; | |
| } | |
| section p strong { | |
| color: var(--text); | |
| font-weight: 500; | |
| } | |
| /* ======================== CARDS ======================== */ | |
| .card-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 1.5rem; | |
| margin: 3rem 0; | |
| } | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| transition: transform 0.3s, border-color 0.3s, box-shadow 0.3s; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .card:hover { | |
| transform: translateY(-4px); | |
| border-color: var(--accent); | |
| box-shadow: 0 8px 40px var(--glow); | |
| } | |
| .card-icon { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| margin-bottom: 1.2rem; | |
| } | |
| .card h3 { | |
| font-family: 'Instrument Serif', serif; | |
| font-size: 1.3rem; | |
| color: #fff; | |
| margin-bottom: 0.8rem; | |
| font-weight: 400; | |
| } | |
| .card p { | |
| font-size: 0.92rem; | |
| color: var(--text-dim); | |
| line-height: 1.6; | |
| } | |
| /* ======================== INTERACTIVE NETWORK ======================== */ | |
| .network-demo { | |
| margin: 3rem 0; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| overflow: hidden; | |
| } | |
| .network-header { | |
| padding: 1.5rem 2rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| } | |
| .network-header h3 { | |
| font-family: 'Instrument Serif', serif; | |
| font-size: 1.3rem; | |
| color: #fff; | |
| font-weight: 400; | |
| } | |
| .network-controls { | |
| display: flex; | |
| gap: 0.5rem; | |
| flex-wrap: wrap; | |
| } | |
| .net-btn { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.72rem; | |
| letter-spacing: 0.05em; | |
| padding: 0.5em 1.2em; | |
| border-radius: 100px; | |
| border: 1px solid var(--border); | |
| background: transparent; | |
| color: var(--text-dim); | |
| cursor: pointer; | |
| transition: all 0.25s; | |
| text-transform: uppercase; | |
| } | |
| .net-btn:hover, .net-btn.active { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| background: var(--glow); | |
| } | |
| .network-canvas-wrap { | |
| position: relative; | |
| height: 450px; | |
| } | |
| .network-canvas-wrap canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .network-info { | |
| padding: 1.2rem 2rem; | |
| border-top: 1px solid var(--border); | |
| font-size: 0.85rem; | |
| color: var(--text-dim); | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| /* ======================== TRAP SPACE VISUAL ======================== */ | |
| .trap-visual { | |
| margin: 3rem 0; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 2rem; | |
| align-items: center; | |
| } | |
| @media (max-width: 768px) { | |
| .trap-visual { grid-template-columns: 1fr; } | |
| } | |
| .trap-diagram { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| position: relative; | |
| min-height: 300px; | |
| } | |
| .trap-diagram canvas { | |
| width: 100%; | |
| height: 300px; | |
| } | |
| /* ======================== PHENOTYPE TABLE ======================== */ | |
| .phenotype-explorer { | |
| margin: 3rem 0; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| overflow: hidden; | |
| } | |
| .pheno-header { | |
| padding: 1.5rem 2rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| } | |
| .pheno-header h3 { | |
| font-family: 'Instrument Serif', serif; | |
| font-size: 1.3rem; | |
| color: #fff; | |
| font-weight: 400; | |
| } | |
| .pheno-filter-btns { | |
| display: flex; | |
| gap: 0.4rem; | |
| flex-wrap: wrap; | |
| } | |
| .filter-btn { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.68rem; | |
| padding: 0.4em 0.9em; | |
| border-radius: 100px; | |
| border: 1px solid var(--border); | |
| background: transparent; | |
| color: var(--text-dim); | |
| cursor: pointer; | |
| transition: all 0.25s; | |
| } | |
| .filter-btn:hover, .filter-btn.active { | |
| border-color: var(--accent3); | |
| color: var(--accent3); | |
| background: rgba(255,107,157,0.1); | |
| } | |
| .pheno-table-wrap { | |
| overflow-x: auto; | |
| padding: 1rem; | |
| } | |
| .pheno-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.82rem; | |
| } | |
| .pheno-table th { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.68rem; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: var(--text-dim); | |
| padding: 0.7rem 0.6rem; | |
| text-align: center; | |
| border-bottom: 1px solid var(--border); | |
| white-space: nowrap; | |
| position: sticky; | |
| top: 0; | |
| background: var(--surface); | |
| } | |
| .pheno-table th:first-child { text-align: left; } | |
| .pheno-table td { | |
| padding: 0.6rem; | |
| text-align: center; | |
| border-bottom: 1px solid rgba(30,30,58,0.5); | |
| transition: background 0.2s; | |
| } | |
| .pheno-table td:first-child { | |
| text-align: left; | |
| font-weight: 500; | |
| color: #fff; | |
| white-space: nowrap; | |
| font-size: 0.78rem; | |
| } | |
| .pheno-table tr:hover td { background: rgba(0,229,160,0.04); } | |
| .cell-on { | |
| display: inline-block; | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 6px; | |
| background: var(--accent); | |
| box-shadow: 0 0 12px var(--glow); | |
| } | |
| .cell-off { | |
| display: inline-block; | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 6px; | |
| background: rgba(255,255,255,0.05); | |
| border: 1px solid rgba(255,255,255,0.08); | |
| } | |
| .cell-osc { | |
| display: inline-block; | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 6px; | |
| background: linear-gradient(135deg, var(--accent4), var(--accent3)); | |
| animation: pulse-osc 1.5s ease-in-out infinite; | |
| } | |
| @keyframes pulse-osc { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.4; } | |
| } | |
| .pheno-table .tag { | |
| display: inline-block; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.65rem; | |
| padding: 0.2em 0.6em; | |
| border-radius: 4px; | |
| letter-spacing: 0.03em; | |
| } | |
| .tag-resting { background: rgba(0,184,255,0.15); color: var(--accent2); } | |
| .tag-active { background: rgba(0,229,160,0.15); color: var(--accent); } | |
| .tag-anergic { background: rgba(255,209,102,0.15); color: var(--accent4); } | |
| /* ======================== TIMELINE ======================== */ | |
| .timeline { | |
| position: relative; | |
| margin: 3rem 0; | |
| padding-left: 3rem; | |
| } | |
| .timeline::before { | |
| content: ''; | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| bottom: 0; | |
| width: 2px; | |
| background: linear-gradient(to bottom, var(--accent), var(--accent2), var(--accent3)); | |
| border-radius: 2px; | |
| } | |
| .tl-item { | |
| position: relative; | |
| margin-bottom: 3rem; | |
| padding-left: 2rem; | |
| } | |
| .tl-item::before { | |
| content: ''; | |
| position: absolute; | |
| left: -3.55rem; | |
| top: 0.4rem; | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| box-shadow: 0 0 16px var(--glow); | |
| border: 2px solid var(--bg); | |
| } | |
| .tl-item:nth-child(2)::before { background: var(--accent2); box-shadow: 0 0 16px var(--glow2); } | |
| .tl-item:nth-child(3)::before { background: var(--accent3); box-shadow: 0 0 16px rgba(255,107,157,0.3); } | |
| .tl-item:nth-child(4)::before { background: var(--accent4); box-shadow: 0 0 16px rgba(255,209,102,0.3); } | |
| .tl-step { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.7rem; | |
| letter-spacing: 0.15em; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| margin-bottom: 0.5rem; | |
| } | |
| .tl-item:nth-child(2) .tl-step { color: var(--accent2); } | |
| .tl-item:nth-child(3) .tl-step { color: var(--accent3); } | |
| .tl-item:nth-child(4) .tl-step { color: var(--accent4); } | |
| .tl-item h3 { | |
| font-family: 'Instrument Serif', serif; | |
| font-size: 1.4rem; | |
| color: #fff; | |
| font-weight: 400; | |
| margin-bottom: 0.5rem; | |
| } | |
| .tl-item p { | |
| font-size: 0.92rem; | |
| color: var(--text-dim); | |
| } | |
| /* ======================== STAT BLOCKS ======================== */ | |
| .stats-row { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 1.5rem; | |
| margin: 3rem 0; | |
| } | |
| .stat-block { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| text-align: center; | |
| transition: border-color 0.3s; | |
| } | |
| .stat-block:hover { border-color: var(--accent); } | |
| .stat-num { | |
| font-family: 'Instrument Serif', serif; | |
| font-size: 3rem; | |
| color: #fff; | |
| line-height: 1; | |
| margin-bottom: 0.3rem; | |
| } | |
| .stat-num .accent { color: var(--accent); } | |
| .stat-label { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.68rem; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: var(--text-dim); | |
| } | |
| /* ======================== ANALOGY SECTION ======================== */ | |
| .analogy-box { | |
| background: linear-gradient(135deg, rgba(0,229,160,0.06), rgba(0,184,255,0.06)); | |
| border: 1px solid rgba(0,229,160,0.2); | |
| border-radius: 20px; | |
| padding: 3rem; | |
| margin: 3rem 0; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .analogy-box::before { | |
| content: '"'; | |
| position: absolute; | |
| top: -20px; | |
| left: 20px; | |
| font-family: 'Instrument Serif', serif; | |
| font-size: 10rem; | |
| color: rgba(0,229,160,0.08); | |
| line-height: 1; | |
| } | |
| .analogy-box h3 { | |
| font-family: 'Instrument Serif', serif; | |
| font-size: 1.6rem; | |
| color: #fff; | |
| font-weight: 400; | |
| margin-bottom: 1rem; | |
| position: relative; | |
| } | |
| .analogy-box p { | |
| color: var(--text-dim); | |
| position: relative; | |
| max-width: 700px; | |
| } | |
| /* ======================== DIVIDERS ======================== */ | |
| .divider { | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| padding: 0 2rem; | |
| } | |
| .divider-line { | |
| height: 1px; | |
| background: linear-gradient(to right, transparent, var(--border), transparent); | |
| } | |
| /* ======================== FOOTER ======================== */ | |
| footer { | |
| text-align: center; | |
| padding: 4rem 2rem; | |
| border-top: 1px solid var(--border); | |
| margin-top: 4rem; | |
| } | |
| footer p { | |
| color: var(--text-dim); | |
| font-size: 0.85rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| footer a { | |
| color: var(--accent); | |
| text-decoration: none; | |
| } | |
| footer a:hover { text-decoration: underline; } | |
| /* ======================== SCROLL ANIMATIONS ======================== */ | |
| .reveal { | |
| opacity: 0; | |
| transform: translateY(40px); | |
| transition: opacity 0.8s, transform 0.8s; | |
| } | |
| .reveal.visible { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| /* ======================== RESPONSIVE ======================== */ | |
| @media (max-width: 600px) { | |
| section { padding: 4rem 1.2rem; } | |
| .card-grid { grid-template-columns: 1fr; } | |
| .stats-row { grid-template-columns: 1fr 1fr; } | |
| .network-canvas-wrap { height: 300px; } | |
| .analogy-box { padding: 2rem 1.5rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ==================== HERO ==================== --> | |
| <div class="hero"> | |
| <div class="hero-bg"><canvas id="heroBg"></canvas></div> | |
| <div class="hero-content"> | |
| <div class="hero-tag">Systems Biology • February 2026</div> | |
| <h1>How Cells <em>Commit</em> to Their Identity</h1> | |
| <p class="hero-sub">A new mathematical framework reveals how biological networks lock cells into specific fates — without needing to know every detail of what's happening inside.</p> | |
| <div class="scroll-cue"> | |
| <a href="#problem"><span class="arrow">↓</span> Explore the science</a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ==================== THE PROBLEM ==================== --> | |
| <section id="problem"> | |
| <div class="reveal"> | |
| <div class="section-label">The Challenge</div> | |
| <h2>What makes a cell… a cell?</h2> | |
| <p>Your body contains hundreds of different cell types, all sharing the same DNA. A skin cell, a neuron, and an immune cell all have identical genetic blueprints — yet they look and behave completely differently. <strong>What determines a cell's identity?</strong></p> | |
| <p>Scientists typically answer this by measuring a handful of <strong>biomarkers</strong> — specific proteins or molecules whose presence signals a particular cell type. But here's the problem: choosing the wrong biomarkers can make you miss important differences, while measuring everything is overwhelming and expensive.</p> | |
| </div> | |
| <div class="card-grid reveal"> | |
| <div class="card"> | |
| <div class="card-icon" style="background: rgba(0,229,160,0.12);">🔬</div> | |
| <h3>Too few biomarkers</h3> | |
| <p>Measuring only a couple of proteins might lump genuinely different cell states together, missing crucial distinctions that matter for disease.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="card-icon" style="background: rgba(0,184,255,0.12);">🌊</div> | |
| <h3>Too many measurements</h3> | |
| <p>Modern "omics" technologies can measure thousands of molecules, but it's nearly impossible to figure out which ones actually define the cell type.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="card-icon" style="background: rgba(255,107,157,0.12);">🎯</div> | |
| <h3>The Goldilocks zone</h3> | |
| <p>This paper finds the sweet spot — a principled way to identify the <em>right</em> set of biomarkers that capture just enough information to define cell identity.</p> | |
| </div> | |
| </div> | |
| </section> | |
| <div class="divider"><div class="divider-line"></div></div> | |
| <!-- ==================== BOOLEAN MODELS ==================== --> | |
| <section id="models"> | |
| <div class="reveal"> | |
| <div class="section-label">The Tool</div> | |
| <h2>Modeling cells as networks of switches</h2> | |
| <p>Biologists build <strong>Boolean models</strong> — simplified digital representations of how genes and proteins interact inside a cell. In these models, each molecule is either ON (1) or OFF (0), and simple logical rules describe how they influence each other.</p> | |
| <p>Think of it like a circuit board where each component flips on or off based on its inputs. Despite this simplicity, these models can predict real cellular behavior with surprising accuracy.</p> | |
| </div> | |
| <div class="network-demo reveal"> | |
| <div class="network-header"> | |
| <h3>Interactive Boolean Network</h3> | |
| <div class="network-controls"> | |
| <button class="net-btn active" onclick="setScenario('default')">Default</button> | |
| <button class="net-btn" onclick="setScenario('signal')">Add Signal</button> | |
| <button class="net-btn" onclick="setScenario('trapped')">Trap Space</button> | |
| </div> | |
| </div> | |
| <div class="network-canvas-wrap"><canvas id="networkCanvas"></canvas></div> | |
| <div class="network-info" id="networkInfo">Click a node to toggle it. Watch how changes propagate through the network.</div> | |
| </div> | |
| </section> | |
| <div class="divider"><div class="divider-line"></div></div> | |
| <!-- ==================== TRAP SPACES ==================== --> | |
| <section id="trapspaces"> | |
| <div class="reveal"> | |
| <div class="section-label">The Key Insight</div> | |
| <h2>Trap spaces: the point of no return</h2> | |
| <p>Imagine a ball rolling on a landscape. Once it falls into a valley, it stays there — it's <strong>trapped</strong>. Similarly, in a Boolean network, certain patterns of ON/OFF states form <strong>"trap spaces"</strong> — once the network enters one, it can never leave.</p> | |
| <p>The paper focuses on <strong>complete trap spaces</strong>: patterns where all the consequences of the locked-in states have fully propagated through the network. These represent stable commitments — <strong>the cell has decided what it's going to be</strong>.</p> | |
| </div> | |
| <div class="analogy-box reveal"> | |
| <h3>Think of it like career paths</h3> | |
| <p>Choosing to go to medical school (an input) doesn't determine every detail of your life, but it does lock in certain outcomes: you'll study anatomy, do residency, and likely practice medicine. Many different "life states" are possible within that commitment, but the core identity is set. That's a trap space — <strong>committed but not rigid</strong>.</p> | |
| </div> | |
| <div class="reveal"> | |
| <div class="section-label">Definition</div> | |
| <h2>Dynamical phenotypes</h2> | |
| <p>The authors define a <strong>dynamical phenotype</strong> as a complete trap space where chosen biomarkers are locked to specific values, while the external environment (inputs) remains as free as possible. This captures the idea that a cell type should be defined by its <em>internal commitment</em>, not by which signals are currently present.</p> | |
| </div> | |
| <div class="timeline reveal"> | |
| <div class="tl-item"> | |
| <div class="tl-step">Step 1</div> | |
| <h3>Choose your biomarkers</h3> | |
| <p>Select a set of "phenotype-determining nodes" (PDNs) — molecules whose states you think define the cell type. These could be transcription factors, cytokines, or other key proteins.</p> | |
| </div> | |
| <div class="tl-item"> | |
| <div class="tl-step">Step 2</div> | |
| <h3>Find the trap spaces</h3> | |
| <p>Using clever math (binary decision diagrams), identify all the complete trap spaces where your chosen biomarkers are locked in. No need to enumerate every possible state of the network!</p> | |
| </div> | |
| <div class="tl-item"> | |
| <div class="tl-step">Step 3</div> | |
| <h3>Map inputs to phenotypes</h3> | |
| <p>For each dynamical phenotype, determine exactly which environmental signals (inputs) allow it to exist. This creates a complete map from environment → cell fate.</p> | |
| </div> | |
| <div class="tl-item"> | |
| <div class="tl-step">Step 4</div> | |
| <h3>Evaluate and refine</h3> | |
| <p>Compare different biomarker choices. The paper introduces an automated method (using "logical domain of influence") to find biomarkers that maximally constrain the network, even without prior biological knowledge.</p> | |
| </div> | |
| </div> | |
| </section> | |
| <div class="divider"><div class="divider-line"></div></div> | |
| <!-- ==================== CASE STUDY ==================== --> | |
| <section id="tcell"> | |
| <div class="reveal"> | |
| <div class="section-label">Case Study</div> | |
| <h2>T cell differentiation: 30 flavors of immune cell</h2> | |
| <p>The star case study is a <strong>70-node Boolean model</strong> of how helper T cells — key players in your immune system — differentiate into specialized subtypes (Th1, Th2, Th17, Treg) based on signals in their environment.</p> | |
| <p>The original 2010 study examined only 8 of the 8,192 possible input combinations. This paper analyzed <strong>all of them</strong>, revealing 30 distinct dynamical phenotypes — including 13 cell types never identified before.</p> | |
| </div> | |
| <div class="stats-row reveal"> | |
| <div class="stat-block"> | |
| <div class="stat-num">70</div> | |
| <div class="stat-label">Nodes in model</div> | |
| </div> | |
| <div class="stat-block"> | |
| <div class="stat-num">8<span class="accent">,</span>192</div> | |
| <div class="stat-label">Input combinations</div> | |
| </div> | |
| <div class="stat-block"> | |
| <div class="stat-num">68<span class="accent">K</span></div> | |
| <div class="stat-label">Attractors found</div> | |
| </div> | |
| <div class="stat-block"> | |
| <div class="stat-num"><span class="accent">30</span></div> | |
| <div class="stat-label">Dynamical phenotypes</div> | |
| </div> | |
| </div> | |
| <div class="phenotype-explorer reveal"> | |
| <div class="pheno-header"> | |
| <h3>Explore T Cell Phenotypes</h3> | |
| <div class="pheno-filter-btns"> | |
| <button class="filter-btn active" onclick="filterPheno('all')">All</button> | |
| <button class="filter-btn" onclick="filterPheno('resting')">Resting</button> | |
| <button class="filter-btn" onclick="filterPheno('active')">Active</button> | |
| <button class="filter-btn" onclick="filterPheno('anergic')">Anergic</button> | |
| <button class="filter-btn" onclick="filterPheno('hybrid')">Hybrid</button> | |
| </div> | |
| </div> | |
| <div class="pheno-table-wrap"> | |
| <table class="pheno-table" id="phenoTable"> | |
| <thead> | |
| <tr> | |
| <th>Phenotype</th> | |
| <th>NFAT</th> | |
| <th>TBET</th> | |
| <th>IFNG</th> | |
| <th>GATA3</th> | |
| <th>IL4</th> | |
| <th>FOXP3</th> | |
| <th>TGFB</th> | |
| <th>RORGT</th> | |
| <th>IL17</th> | |
| <th>State</th> | |
| </tr> | |
| </thead> | |
| <tbody id="phenoBody"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <div class="divider"><div class="divider-line"></div></div> | |
| <!-- ==================== SMART BIOMARKERS ==================== --> | |
| <section id="biomarkers"> | |
| <div class="reveal"> | |
| <div class="section-label">Smart Biomarker Selection</div> | |
| <h2>Letting the math choose the markers</h2> | |
| <p>The paper's second major contribution is an automated method to identify the <strong>most informative biomarkers</strong> based on the network's regulatory logic — specifically, which nodes have the strongest "canalizing" (overriding) influence on the rest of the network.</p> | |
| <p>For the T cell model, this algorithm identified just <strong>5 nodes</strong> (IL4R_b1, NFAT, STAT1, TBET, proliferation) that capture cell identity better than the original 9 biologically-chosen markers. These 5 nodes fixed an average of 21 additional nodes each, compared to only 17 for the 9 biological markers.</p> | |
| </div> | |
| <div class="card-grid reveal"> | |
| <div class="card" style="border-left: 3px solid var(--accent3);"> | |
| <h3>Biological PDNs</h3> | |
| <p style="font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--accent3); margin-bottom: 0.8rem;">9 nodes chosen by experts</p> | |
| <p>TBET, GATA3, FOXP3, RORGT, NFAT, IFNG, IL4, IL17, TGFB — transcription factors and cytokines traditionally used to identify T cell subtypes.</p> | |
| <p style="margin-bottom: 0;"><strong>17 nodes</strong> fixed on average per phenotype</p> | |
| </div> | |
| <div class="card" style="border-left: 3px solid var(--accent);"> | |
| <h3>LDOI-based PDNs</h3> | |
| <p style="font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--accent); margin-bottom: 0.8rem;">5 nodes chosen by algorithm</p> | |
| <p>IL4R_b1, NFAT, STAT1, TBET, proliferation — identified by maximizing the "logical domain of influence" across the network's canalizing regulation.</p> | |
| <p style="margin-bottom: 0;"><strong>21 nodes</strong> fixed on average per phenotype</p> | |
| </div> | |
| </div> | |
| <div class="analogy-box reveal"> | |
| <h3>Why does fewer = better?</h3> | |
| <p>Imagine identifying a city by landmarks. You could list 9 minor landmarks, or you could name 5 major intersections that each tell you which neighborhood you're in, what trains are nearby, and what stores surround you. The LDOI method finds those "major intersections" of the gene regulatory network — nodes whose state ripples outward to constrain many other nodes.</p> | |
| </div> | |
| </section> | |
| <div class="divider"><div class="divider-line"></div></div> | |
| <!-- ==================== WHY IT MATTERS ==================== --> | |
| <section id="impact"> | |
| <div class="reveal"> | |
| <div class="section-label">Why It Matters</div> | |
| <h2>From models to medicine</h2> | |
| <p>This framework isn't just theoretical — it has direct implications for how we study and treat disease.</p> | |
| </div> | |
| <div class="card-grid reveal"> | |
| <div class="card"> | |
| <div class="card-icon" style="background: rgba(0,229,160,0.12);">💡</div> | |
| <h3>Better biomarker panels</h3> | |
| <p>Hospitals and labs can use this approach to design diagnostic panels that capture the most information with the fewest measurements — reducing costs and improving accuracy.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="card-icon" style="background: rgba(0,184,255,0.12);">🧬</div> | |
| <h3>Understanding cell plasticity</h3> | |
| <p>The finding that multiple phenotypes coexist under the same environment explains the "continuum" of cell states seen in experiments — cells aren't just one thing or another.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="card-icon" style="background: rgba(255,107,157,0.12);">🎛️</div> | |
| <h3>Phenotype control</h3> | |
| <p>By identifying trap spaces rather than specific attractors, therapeutic interventions can be less precise (and therefore more practical) — you just need to push the cell into the right "valley."</p> | |
| </div> | |
| <div class="card"> | |
| <div class="card-icon" style="background: rgba(255,209,102,0.12);">📈</div> | |
| <h3>Scalable analysis</h3> | |
| <p>The BDD-based algorithm avoids enumerating all possible states, making it feasible to analyze models with billions of possible configurations that were previously intractable.</p> | |
| </div> | |
| </div> | |
| </section> | |
| <div class="divider"><div class="divider-line"></div></div> | |
| <!-- ==================== KEY TAKEAWAY ==================== --> | |
| <section id="takeaway"> | |
| <div class="reveal"> | |
| <div class="section-label">Key Takeaway</div> | |
| <h2>Phenotypes as commitments, not endpoints</h2> | |
| <p>The most powerful idea in this paper: <strong>a cell's identity isn't a single frozen state</strong>. It's a commitment to a region of possibilities. A Th1 immune cell doesn't have one exact molecular configuration — it occupies a "trap space" of many possible configurations, all of which share the core identity markers.</p> | |
| <p>This reframing — from rigid endpoints to flexible commitments — mirrors how we think about identity in everyday life. You can be a "musician" whether you're playing guitar, composing, or teaching. The label captures a commitment, not a frozen moment.</p> | |
| <p>By formalizing this intuition mathematically, this paper gives biologists a principled tool for connecting the messy reality of molecular networks to the clean categories we use to understand cells.</p> | |
| </div> | |
| </section> | |
| <!-- ==================== FOOTER ==================== --> | |
| <footer> | |
| <p>Based on <a href="https://arxiv.org/abs/2602.15691" target="_blank">"Relating biomarkers and phenotypes using dynamical trap spaces"</a></p> | |
| <p style="font-size: 0.78rem;">Pastva, Park, Rozum, Trinh & Albert • arXiv:2602.15691 • February 2026</p> | |
| </footer> | |
| <script> | |
| // ==================== HERO BACKGROUND ==================== | |
| (function() { | |
| const canvas = document.getElementById('heroBg'); | |
| const ctx = canvas.getContext('2d'); | |
| let w, h, nodes = [], edges = []; | |
| function resize() { | |
| w = canvas.width = canvas.offsetWidth * devicePixelRatio; | |
| h = canvas.height = canvas.offsetHeight * devicePixelRatio; | |
| ctx.scale(devicePixelRatio, devicePixelRatio); | |
| } | |
| function init() { | |
| resize(); | |
| const rw = canvas.offsetWidth, rh = canvas.offsetHeight; | |
| nodes = []; | |
| for (let i = 0; i < 60; i++) { | |
| nodes.push({ | |
| x: Math.random() * rw, | |
| y: Math.random() * rh, | |
| vx: (Math.random() - 0.5) * 0.4, | |
| vy: (Math.random() - 0.5) * 0.4, | |
| r: Math.random() * 2.5 + 1, | |
| state: Math.random() > 0.5 ? 1 : 0, | |
| phase: Math.random() * Math.PI * 2 | |
| }); | |
| } | |
| } | |
| function draw() { | |
| const rw = canvas.offsetWidth, rh = canvas.offsetHeight; | |
| ctx.clearRect(0, 0, rw, rh); | |
| // edges | |
| for (let i = 0; i < nodes.length; i++) { | |
| for (let j = i + 1; j < nodes.length; j++) { | |
| const dx = nodes[i].x - nodes[j].x; | |
| const dy = nodes[i].y - nodes[j].y; | |
| const d = Math.sqrt(dx * dx + dy * dy); | |
| if (d < 150) { | |
| const alpha = (1 - d / 150) * 0.12; | |
| ctx.strokeStyle = nodes[i].state ? `rgba(0,229,160,${alpha})` : `rgba(0,184,255,${alpha})`; | |
| ctx.lineWidth = 0.5; | |
| ctx.beginPath(); | |
| ctx.moveTo(nodes[i].x, nodes[i].y); | |
| ctx.lineTo(nodes[j].x, nodes[j].y); | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| // nodes | |
| const t = Date.now() * 0.001; | |
| nodes.forEach(n => { | |
| n.x += n.vx; | |
| n.y += n.vy; | |
| if (n.x < 0 || n.x > rw) n.vx *= -1; | |
| if (n.y < 0 || n.y > rh) n.vy *= -1; | |
| const pulse = 0.5 + 0.5 * Math.sin(t * 1.5 + n.phase); | |
| const col = n.state ? `rgba(0,229,160,${0.3 + pulse * 0.5})` : `rgba(0,184,255,${0.15 + pulse * 0.3})`; | |
| ctx.beginPath(); | |
| ctx.arc(n.x, n.y, n.r + pulse * 1.5, 0, Math.PI * 2); | |
| ctx.fillStyle = col; | |
| ctx.fill(); | |
| if (n.state && pulse > 0.7) { | |
| ctx.beginPath(); | |
| ctx.arc(n.x, n.y, n.r + 6, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(0,229,160,${pulse * 0.08})`; | |
| ctx.fill(); | |
| } | |
| }); | |
| // randomly toggle states | |
| if (Math.random() < 0.01) { | |
| const idx = Math.floor(Math.random() * nodes.length); | |
| nodes[idx].state = 1 - nodes[idx].state; | |
| } | |
| requestAnimationFrame(draw); | |
| } | |
| window.addEventListener('resize', () => { resize(); }); | |
| init(); | |
| draw(); | |
| })(); | |
| // ==================== INTERACTIVE NETWORK ==================== | |
| (function() { | |
| const canvas = document.getElementById('networkCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let W, H; | |
| const nodeData = [ | |
| { id: 'APC', x: 0.08, y: 0.2, type: 'input', state: 0, label: 'APC' }, | |
| { id: 'TCR', x: 0.18, y: 0.15, type: 'internal', state: 0, label: 'TCR' }, | |
| { id: 'NFAT', x: 0.32, y: 0.22, type: 'pdn', state: 0, label: 'NFAT' }, | |
| { id: 'IL12e', x: 0.08, y: 0.55, type: 'input', state: 0, label: 'IL12' }, | |
| { id: 'STAT4', x: 0.22, y: 0.5, type: 'internal', state: 0, label: 'STAT4' }, | |
| { id: 'TBET', x: 0.38, y: 0.45, type: 'pdn', state: 0, label: 'TBET' }, | |
| { id: 'IFNG', x: 0.52, y: 0.35, type: 'pdn', state: 0, label: 'IFNG' }, | |
| { id: 'IL4e', x: 0.08, y: 0.85, type: 'input', state: 0, label: 'IL4' }, | |
| { id: 'STAT6', x: 0.22, y: 0.8, type: 'internal', state: 0, label: 'STAT6' }, | |
| { id: 'GATA3', x: 0.38, y: 0.75, type: 'pdn', state: 0, label: 'GATA3' }, | |
| { id: 'IL4out', x: 0.52, y: 0.7, type: 'pdn', state: 0, label: 'IL4 out' }, | |
| { id: 'TGFBe', x: 0.65, y: 0.15, type: 'input', state: 0, label: 'TGFBe' }, | |
| { id: 'SMAD3', x: 0.75, y: 0.3, type: 'internal', state: 0, label: 'SMAD3' }, | |
| { id: 'FOXP3', x: 0.82, y: 0.45, type: 'pdn', state: 0, label: 'FOXP3' }, | |
| { id: 'TGFB', x: 0.9, y: 0.6, type: 'pdn', state: 0, label: 'TGFB' }, | |
| { id: 'RORGT', x: 0.68, y: 0.8, type: 'pdn', state: 0, label: 'RORGT' }, | |
| { id: 'IL17', x: 0.82, y: 0.85, type: 'pdn', state: 0, label: 'IL17' }, | |
| { id: 'prolif', x: 0.55, y: 0.53, type: 'internal', state: 0, label: 'prolif.' }, | |
| ]; | |
| const edgeData = [ | |
| { from: 'APC', to: 'TCR', sign: '+' }, | |
| { from: 'TCR', to: 'NFAT', sign: '+' }, | |
| { from: 'NFAT', to: 'IFNG', sign: '+' }, | |
| { from: 'NFAT', to: 'IL4out', sign: '+' }, | |
| { from: 'NFAT', to: 'prolif', sign: '+' }, | |
| { from: 'IL12e', to: 'STAT4', sign: '+' }, | |
| { from: 'STAT4', to: 'TBET', sign: '+' }, | |
| { from: 'TBET', to: 'IFNG', sign: '+' }, | |
| { from: 'TBET', to: 'GATA3', sign: '-' }, | |
| { from: 'IL4e', to: 'STAT6', sign: '+' }, | |
| { from: 'STAT6', to: 'GATA3', sign: '+' }, | |
| { from: 'GATA3', to: 'IL4out', sign: '+' }, | |
| { from: 'GATA3', to: 'TBET', sign: '-' }, | |
| { from: 'TGFBe', to: 'SMAD3', sign: '+' }, | |
| { from: 'SMAD3', to: 'FOXP3', sign: '+' }, | |
| { from: 'FOXP3', to: 'TGFB', sign: '+' }, | |
| { from: 'SMAD3', to: 'RORGT', sign: '+' }, | |
| { from: 'RORGT', to: 'IL17', sign: '+' }, | |
| { from: 'prolif', to: 'IL17', sign: '+' }, | |
| { from: 'prolif', to: 'TGFB', sign: '+' }, | |
| { from: 'FOXP3', to: 'RORGT', sign: '-' }, | |
| ]; | |
| function getNode(id) { return nodeData.find(n => n.id === id); } | |
| function resize() { | |
| const rect = canvas.parentElement.getBoundingClientRect(); | |
| W = rect.width; | |
| H = rect.height; | |
| canvas.width = W * devicePixelRatio; | |
| canvas.height = H * devicePixelRatio; | |
| canvas.style.width = W + 'px'; | |
| canvas.style.height = H + 'px'; | |
| ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); | |
| } | |
| function colorForNode(n) { | |
| if (n.state) { | |
| if (n.type === 'input') return '#00e5a0'; | |
| if (n.type === 'pdn') return '#00e5a0'; | |
| return '#00b8ff'; | |
| } | |
| return '#2a2a4a'; | |
| } | |
| function drawArrow(x1, y1, x2, y2, sign, active) { | |
| const dx = x2 - x1, dy = y2 - y1; | |
| const len = Math.sqrt(dx*dx + dy*dy); | |
| const nx = dx/len, ny = dy/len; | |
| const startOff = 22, endOff = 22; | |
| const sx = x1 + nx * startOff, sy = y1 + ny * startOff; | |
| const ex = x2 - nx * endOff, ey = y2 - ny * endOff; | |
| ctx.beginPath(); | |
| ctx.moveTo(sx, sy); | |
| ctx.lineTo(ex, ey); | |
| const alpha = active ? 0.7 : 0.15; | |
| ctx.strokeStyle = sign === '+' ? `rgba(0,229,160,${alpha})` : `rgba(255,107,157,${alpha})`; | |
| ctx.lineWidth = active ? 2 : 1; | |
| ctx.stroke(); | |
| // arrowhead | |
| const aLen = 8; | |
| const angle = Math.atan2(ey - sy, ex - sx); | |
| if (sign === '+') { | |
| ctx.beginPath(); | |
| ctx.moveTo(ex, ey); | |
| ctx.lineTo(ex - aLen * Math.cos(angle - 0.4), ey - aLen * Math.sin(angle - 0.4)); | |
| ctx.lineTo(ex - aLen * Math.cos(angle + 0.4), ey - aLen * Math.sin(angle + 0.4)); | |
| ctx.closePath(); | |
| ctx.fillStyle = ctx.strokeStyle; | |
| ctx.fill(); | |
| } else { | |
| ctx.beginPath(); | |
| ctx.moveTo(ex - 5 * ny, ey + 5 * nx); | |
| ctx.lineTo(ex + 5 * ny, ey - 5 * nx); | |
| ctx.strokeStyle = ctx.strokeStyle; | |
| ctx.lineWidth = active ? 2.5 : 1.5; | |
| ctx.stroke(); | |
| } | |
| } | |
| let trapHighlight = false; | |
| function draw() { | |
| ctx.clearRect(0, 0, W, H); | |
| // edges | |
| edgeData.forEach(e => { | |
| const from = getNode(e.from), to = getNode(e.to); | |
| const active = from.state === 1; | |
| drawArrow(from.x * W, from.y * H, to.x * W, to.y * H, e.sign, active); | |
| }); | |
| // nodes | |
| const t = Date.now() * 0.002; | |
| nodeData.forEach(n => { | |
| const x = n.x * W, y = n.y * H; | |
| const r = 18; | |
| // glow for active | |
| if (n.state) { | |
| const grd = ctx.createRadialGradient(x, y, r, x, y, r * 3); | |
| grd.addColorStop(0, n.type === 'pdn' ? 'rgba(0,229,160,0.2)' : 'rgba(0,184,255,0.15)'); | |
| grd.addColorStop(1, 'transparent'); | |
| ctx.fillStyle = grd; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, r * 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| // trap highlight ring | |
| if (trapHighlight && n.trapped) { | |
| ctx.beginPath(); | |
| ctx.arc(x, y, r + 6, 0, Math.PI * 2); | |
| ctx.strokeStyle = 'rgba(255,209,102,0.6)'; | |
| ctx.lineWidth = 2; | |
| ctx.setLineDash([4, 4]); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| } | |
| // node circle | |
| ctx.beginPath(); | |
| ctx.arc(x, y, r, 0, Math.PI * 2); | |
| ctx.fillStyle = colorForNode(n); | |
| ctx.fill(); | |
| if (n.type === 'input') { | |
| ctx.strokeStyle = 'rgba(0,229,160,0.5)'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| } else if (n.type === 'pdn') { | |
| ctx.strokeStyle = n.state ? 'rgba(0,229,160,0.8)' : 'rgba(255,107,157,0.3)'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| } | |
| // label | |
| ctx.fillStyle = n.state ? '#fff' : '#6a6a8a'; | |
| ctx.font = '500 10px "JetBrains Mono", monospace'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(n.label, x, y + r + 14); | |
| }); | |
| requestAnimationFrame(draw); | |
| } | |
| canvas.addEventListener('click', (e) => { | |
| const rect = canvas.getBoundingClientRect(); | |
| const mx = e.clientX - rect.left, my = e.clientY - rect.top; | |
| nodeData.forEach(n => { | |
| const dx = mx - n.x * W, dy = my - n.y * H; | |
| if (dx*dx + dy*dy < 400) { | |
| n.state = 1 - n.state; | |
| propagate(); | |
| } | |
| }); | |
| }); | |
| function propagate() { | |
| // simple propagation simulation | |
| for (let iter = 0; iter < 3; iter++) { | |
| edgeData.forEach(e => { | |
| const from = getNode(e.from), to = getNode(e.to); | |
| if (from.state && e.sign === '+' && to.type !== 'input') { | |
| to.state = 1; | |
| } | |
| }); | |
| } | |
| } | |
| window.setScenario = function(s) { | |
| document.querySelectorAll('.net-btn').forEach(b => b.classList.remove('active')); | |
| event.target.classList.add('active'); | |
| trapHighlight = false; | |
| nodeData.forEach(n => { n.state = 0; n.trapped = false; }); | |
| if (s === 'signal') { | |
| getNode('APC').state = 1; | |
| getNode('IL12e').state = 1; | |
| propagate(); | |
| document.getElementById('networkInfo').textContent = 'APC + IL12 signals are ON → activating the Th1 pathway via TCR→NFAT and STAT4→TBET→IFNG.'; | |
| } else if (s === 'trapped') { | |
| getNode('APC').state = 1; | |
| getNode('IL12e').state = 1; | |
| propagate(); | |
| trapHighlight = true; | |
| ['NFAT','TBET','IFNG','prolif'].forEach(id => { getNode(id).trapped = true; }); | |
| document.getElementById('networkInfo').textContent = 'Yellow rings show the "trap space" — these nodes are locked ON. The cell is committed to being a Th1 cell.'; | |
| } else { | |
| document.getElementById('networkInfo').textContent = 'Click a node to toggle it. Watch how changes propagate through the network.'; | |
| } | |
| }; | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| draw(); | |
| })(); | |
| // ==================== PHENOTYPE TABLE ==================== | |
| const phenotypes = [ | |
| { name: 'Th0 resting', nfat:0,tbet:0,ifng:0,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:0,il17:0, state:'resting', cat:'resting' }, | |
| { name: 'Th1 resting', nfat:0,tbet:1,ifng:0,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:0,il17:0, state:'resting', cat:'resting' }, | |
| { name: 'Th2 resting', nfat:0,tbet:0,ifng:0,gata3:1,il4:0,foxp3:0,tgfb:0,rorgt:0,il17:0, state:'resting', cat:'resting' }, | |
| { name: 'Th17 resting', nfat:0,tbet:0,ifng:0,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:1,il17:0, state:'resting', cat:'resting' }, | |
| { name: 'Th0 active', nfat:1,tbet:0,ifng:0,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:0,il17:0, state:'active', cat:'active' }, | |
| { name: 'Th1 active', nfat:1,tbet:1,ifng:1,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:0,il17:0, state:'active', cat:'active' }, | |
| { name: 'Th1 anergic', nfat:1,tbet:1,ifng:0,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:0,il17:0, state:'anergic', cat:'anergic' }, | |
| { name: 'Th2 active', nfat:1,tbet:0,ifng:0,gata3:1,il4:1,foxp3:0,tgfb:0,rorgt:0,il17:0, state:'active', cat:'active' }, | |
| { name: 'Th2 anergic', nfat:1,tbet:0,ifng:0,gata3:1,il4:0,foxp3:0,tgfb:0,rorgt:0,il17:0, state:'anergic', cat:'anergic' }, | |
| { name: 'Treg active', nfat:1,tbet:0,ifng:0,gata3:0,il4:0,foxp3:1,tgfb:1,rorgt:0,il17:0, state:'active', cat:'active' }, | |
| { name: 'Treg anergic', nfat:1,tbet:0,ifng:0,gata3:0,il4:0,foxp3:1,tgfb:0,rorgt:0,il17:0, state:'anergic', cat:'anergic' }, | |
| { name: 'Th17 anergic', nfat:1,tbet:0,ifng:0,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:1,il17:0, state:'anergic', cat:'anergic' }, | |
| { name: 'Th1-Th17 resting', nfat:0,tbet:1,ifng:0,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:1,il17:0, state:'resting', cat:'resting hybrid' }, | |
| { name: 'Th2-Th17 resting', nfat:0,tbet:0,ifng:0,gata3:1,il4:0,foxp3:0,tgfb:0,rorgt:1,il17:0, state:'resting', cat:'resting hybrid' }, | |
| { name: 'Th1-Treg active', nfat:1,tbet:1,ifng:0,gata3:0,il4:0,foxp3:1,tgfb:1,rorgt:0,il17:0, state:'active', cat:'active hybrid' }, | |
| { name: 'Th1-Treg anergic', nfat:1,tbet:1,ifng:0,gata3:0,il4:0,foxp3:1,tgfb:0,rorgt:0,il17:0, state:'anergic', cat:'anergic hybrid' }, | |
| { name: 'Th1-Th17 active (IFNG)', nfat:1,tbet:1,ifng:1,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:1,il17:0, state:'active', cat:'active hybrid' }, | |
| { name: 'Th1-Th17 active (IL17)', nfat:1,tbet:1,ifng:0,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:1,il17:1, state:'active', cat:'active hybrid' }, | |
| { name: 'Th1-Th17 anergic', nfat:1,tbet:1,ifng:0,gata3:0,il4:0,foxp3:0,tgfb:0,rorgt:1,il17:0, state:'anergic', cat:'anergic hybrid' }, | |
| { name: 'Th2-Treg anergic', nfat:1,tbet:0,ifng:0,gata3:1,il4:0,foxp3:1,tgfb:0,rorgt:0,il17:0, state:'anergic', cat:'anergic hybrid' }, | |
| { name: 'Th2-Th17 active (IL4)', nfat:1,tbet:0,ifng:0,gata3:1,il4:1,foxp3:0,tgfb:0,rorgt:1,il17:0, state:'active', cat:'active hybrid' }, | |
| { name: 'Th2-Th17 anergic', nfat:1,tbet:0,ifng:0,gata3:1,il4:0,foxp3:0,tgfb:0,rorgt:1,il17:0, state:'anergic', cat:'anergic hybrid' }, | |
| { name: 'Treg-Th17 active', nfat:1,tbet:0,ifng:0,gata3:0,il4:0,foxp3:1,tgfb:1,rorgt:1,il17:0, state:'active', cat:'active hybrid' }, | |
| { name: 'Treg-Th17 anergic', nfat:1,tbet:0,ifng:0,gata3:0,il4:0,foxp3:1,tgfb:0,rorgt:1,il17:0, state:'anergic', cat:'anergic hybrid' }, | |
| { name: 'Treg active (osc)', nfat:1,tbet:0,ifng:0,gata3:0,il4:0,foxp3:'osc',tgfb:'osc',rorgt:0,il17:0, state:'active', cat:'active' }, | |
| { name: 'Treg-Th17 active (osc)', nfat:1,tbet:0,ifng:0,gata3:0,il4:0,foxp3:'osc',tgfb:'osc',rorgt:1,il17:0, state:'active', cat:'active hybrid' }, | |
| { name: 'Th1-Treg-Th17 active', nfat:1,tbet:1,ifng:0,gata3:0,il4:0,foxp3:1,tgfb:1,rorgt:1,il17:0, state:'active', cat:'active hybrid' }, | |
| { name: 'Th1-Treg-Th17 anergic', nfat:1,tbet:1,ifng:0,gata3:0,il4:0,foxp3:1,tgfb:0,rorgt:1,il17:0, state:'anergic', cat:'anergic hybrid' }, | |
| { name: 'Th2-Treg-Th17 active', nfat:1,tbet:0,ifng:0,gata3:1,il4:0,foxp3:1,tgfb:1,rorgt:1,il17:0, state:'active', cat:'active hybrid' }, | |
| { name: 'Th2-Treg-Th17 anergic', nfat:1,tbet:0,ifng:0,gata3:1,il4:0,foxp3:1,tgfb:0,rorgt:1,il17:0, state:'anergic', cat:'anergic hybrid' }, | |
| ]; | |
| function cellHTML(val) { | |
| if (val === 'osc') return '<span class="cell-osc" title="Oscillating"></span>'; | |
| return val === 1 ? '<span class="cell-on" title="ON"></span>' : '<span class="cell-off" title="OFF"></span>'; | |
| } | |
| function stateTag(state) { | |
| const cls = state === 'resting' ? 'tag-resting' : state === 'active' ? 'tag-active' : 'tag-anergic'; | |
| return `<span class="tag ${cls}">${state}</span>`; | |
| } | |
| function renderTable(filter) { | |
| const body = document.getElementById('phenoBody'); | |
| const filtered = filter === 'all' ? phenotypes : | |
| filter === 'hybrid' ? phenotypes.filter(p => p.cat.includes('hybrid')) : | |
| phenotypes.filter(p => p.state === filter); | |
| body.innerHTML = filtered.map(p => ` | |
| <tr> | |
| <td>${p.name}</td> | |
| <td>${cellHTML(p.nfat)}</td> | |
| <td>${cellHTML(p.tbet)}</td> | |
| <td>${cellHTML(p.ifng)}</td> | |
| <td>${cellHTML(p.gata3)}</td> | |
| <td>${cellHTML(p.il4)}</td> | |
| <td>${cellHTML(p.foxp3)}</td> | |
| <td>${cellHTML(p.tgfb)}</td> | |
| <td>${cellHTML(p.rorgt)}</td> | |
| <td>${cellHTML(p.il17)}</td> | |
| <td>${stateTag(p.state)}</td> | |
| </tr> | |
| `).join(''); | |
| } | |
| window.filterPheno = function(f) { | |
| document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); | |
| event.target.classList.add('active'); | |
| renderTable(f); | |
| }; | |
| renderTable('all'); | |
| // ==================== SCROLL REVEAL ==================== | |
| const reveals = document.querySelectorAll('.reveal'); | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| entry.target.classList.add('visible'); | |
| } | |
| }); | |
| }, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }); | |
| reveals.forEach(el => observer.observe(el)); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment