Files
ThothII/presentation/prototype/scatter-variants.html
T
Codex f796b14b70 prototype: slide 02 — clickable missing-pieces center (popups on rects + icons)
- the four 'to build' rects now open popups explaining each missing
  piece (Clinical Intelligence, Datawarehouse, ML-ready data,
  Professional Portal) with 'The gap' highlight box
- system icons keep their own problem popups; pop positioned near the
  clicked element with the red connector line
2026-09-07 12:03:47 +02:00

384 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PROTOTYPE · Scatter layouts — emblem icons (slides 02 & 03)</title>
<link rel="stylesheet" href="../deck/fonts.css">
<style>
:root {
--bordeaux: #cb333b; --bordeaux-deep: #8f1204; --bordeaux-tint: #f4dfe1;
--surface: #fdfcfc; --surface-alt: #f6f5f5; --surface-sunken: #ecebeb;
--ink: #2a2426; --ink-muted: #6b6568;
--line: #e3e0e1; --line-strong: #c9c5c6;
--sans: 'Source Sans 3', system-ui, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--surface-sunken); font-family: var(--sans); padding: 36px 0 90px; color: var(--ink); }
.note { width: 1280px; margin: 0 auto 18px; padding: 12px 18px; background: #fff8e6; border: 1px solid #e8d9a8;
border-radius: 8px; font: 400 13px/1.5 var(--sans); color: #6b5d2a; }
.note b { font-weight: 700; }
.switcher { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
display: flex; align-items: center; gap: 6px; padding: 8px 10px; background: #2a2426;
border-radius: 9999px; box-shadow: 0 6px 24px rgba(0,0,0,.25); z-index: 10; white-space: nowrap; }
.switcher .tag { font: 700 10px/1 var(--mono); letter-spacing: .1em; color: #9c9598; padding: 0 10px; }
.switcher a { display: inline-flex; height: 30px; align-items: center; padding: 0 14px; border-radius: 9999px;
font: 700 12px/1 var(--sans); color: #fdfcfc; text-decoration: none; opacity: .75; }
.switcher a.active { background: var(--bordeaux); opacity: 1; }
.switcher .who { font: 400 11px/1 var(--sans); color: #9c9598; padding: 0 12px 0 8px; max-width: 430px; white-space: normal; }
.slide { position: relative; width: 1280px; height: 720px; margin: 0 auto 34px;
overflow: hidden; background: var(--surface); color: var(--ink); border: 1px solid var(--line);
border-radius: 8px; display: flex; flex-direction: column; }
.head { flex: 0 0 104px; display: flex; align-items: center; justify-content: space-between;
padding: 0 40px; background: var(--surface); border-bottom: 1px solid var(--line); }
.head img { height: 80px; display: block; }
.head-org { font: 700 11.5px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-muted); }
.band { flex: 0 0 46px; display: flex; align-items: center; justify-content: space-between;
padding: 0 40px; background: var(--bordeaux); color: var(--surface); }
.band-section { font: 700 13px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; }
.foot { flex: 0 0 34px; display: flex; align-items: center; justify-content: space-between; gap: 24px;
padding: 0 40px; border-top: 1px solid var(--line); font: 600 10px/1.4 var(--sans);
letter-spacing: .03em; color: var(--ink-muted); }
.foot span { white-space: nowrap; }
.foot span:first-child { overflow: hidden; text-overflow: ellipsis; }
.sbody { flex: 1; display: flex; flex-direction: column; padding: 26px 48px 16px; }
.kicker { font: 700 12px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase;
color: var(--bordeaux); margin-bottom: 12px; }
h2 { font: 700 30px/1.15 var(--sans); color: var(--ink); margin: 0 0 6px; }
.lead { font: 400 15px/1.5 var(--sans); color: var(--ink-muted); margin: 4px 0 0; }
/* ===== scattered islands canvas ===== */
.scatter { position: relative; flex: 1; min-height: 0; }
.isle { position: absolute; display: flex; flex-direction: column; align-items: center; gap: 2px;
cursor: pointer; transform: rotate(var(--rot, 0deg)); transition: transform .18s ease; border: none; background: none;
font-family: var(--sans); width: var(--w, 240px); }
.isle:hover { transform: rotate(var(--rot, 0deg)) scale(1.04); }
.isle:focus-visible { outline: none; transform: rotate(var(--rot, 0deg)) scale(1.04); }
.isle .ico { width: var(--size, 140px); height: var(--size, 140px); color: var(--bordeaux); display: block; }
.isle .nm { font: 700 16px/1.3 var(--sans); color: var(--ink); margin-top: 4px; }
.isle .sc { font: 400 11.5px/1.4 var(--sans); color: var(--ink-muted); max-width: 240px; }
.isle.ghost .ico { color: var(--ink-muted); opacity: .5; }
.isle.ghost .nm { color: var(--bordeaux-deep); font-size: 14px; }
.isle.ghost .sc { font-style: italic; }
/* slide 03 existed zone */
.flow { display: flex; align-items: stretch; gap: 14px; flex: 1; min-height: 0; margin-top: 6px; }
.zone { border-radius: 8px; position: relative; }
.zone > .zlabel { position: absolute; top: 8px; left: 14px; font: 700 10px/1 var(--mono);
letter-spacing: .14em; text-transform: uppercase; }
.zone.existed { flex: 0 0 250px; background: var(--surface-alt); border: 1px dashed var(--line-strong); position: relative; }
.zone.existed > .zlabel { color: var(--ink-muted); }
.zone.built { flex: 1; background: #faf4f5; border: 1px solid #e7c3c6; }
.zone.built > .zlabel { color: var(--bordeaux); }
.zone.built .row { display: flex; align-items: center; gap: 8px; height: 100%; padding: 0 10px; }
.fcol { flex: 1; display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.fbox { background: #fff; border: 1px solid var(--line-strong); border-radius: 6px; padding: 9px 8px; text-align: center; }
.fbox b { display: block; font: 700 12px/1.25 var(--sans); }
.fbox span { display: block; margin-top: 2px; font: 400 10px/1.3 var(--sans); color: var(--ink-muted); }
.fbox.end { background: var(--bordeaux); border-color: var(--bordeaux); color: #fff; }
.fbox.end span { color: rgba(255,255,255,.85); }
.farrow { align-self: center; color: var(--bordeaux); font: 700 14px/1 var(--sans); }
.src-scatter { position: relative; flex: 1; min-height: 0; }
.src-scatter .isle { --w: 120px; position: absolute; }
.src-scatter .isle .ico { width: var(--size, 64px); height: var(--size, 64px); }
.src-scatter .isle .nm { font-size: 11.5px; }
.todo { display: flex; align-items: center; gap: 12px; border: 2px dashed var(--bordeaux); border-radius: 8px; background: #fff; padding: 10px 14px;
margin-bottom: 10px; box-shadow: 0 2px 8px rgba(42,36,38,.06); }
.todo .q { font: 800 26px/1 var(--sans); color: var(--bordeaux-deep); }
.todo .tx { display: flex; flex-direction: column; }
.todo b { font: 700 15px/1.3 var(--sans); color: var(--bordeaux-deep); }
.todo .tx span { font: 400 12px/1.4 var(--sans); color: var(--ink-muted); margin-top: 2px; }
.missing-title { position: absolute; font: 700 30px/1.2 var(--sans); color: var(--bordeaux-deep); }
.todo-stack { position: absolute; z-index: 2; }
.todo-stack .todo { cursor: pointer; }
.isle .ico { color: var(--bordeaux); }
.isle.ghost .ico { color: #9a938f; }
.isle .nm { font-size: 15px; }
.isle .sc { font-size: 13.5px; max-width: 240px; }
/* popup */
.ai-pop { position: fixed; inset: 0; z-index: 100; background: rgba(42,36,38,.45);
display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity .2s; }
.ai-pop.open { opacity: 1; pointer-events: auto; }
.ai-pop-card { position: relative; width: min(560px, 92%); background: #fff; border: 1px solid var(--line);
border-radius: 12px; box-shadow: 0 18px 50px rgba(42,36,38,.28); padding: 24px 28px 22px;
transform: translateY(14px) scale(.95); transition: transform .24s cubic-bezier(.2,.9,.3,1.15); }
.ai-pop.open .ai-pop-card { transform: none; }
.ai-pop-kicker { font: 700 10.5px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--bordeaux); margin-bottom: 10px; }
.ai-pop-title { font: 700 18px/1.3 var(--sans); color: var(--ink); margin-bottom: 12px; }
.ai-pop-card p { font: 400 14px/1.65 var(--sans); color: var(--ink); margin: 0 0 12px; }
.pop-list { list-style: none; display: flex; flex-direction: column; gap: 9px; }
.pop-list li { position: relative; padding-left: 20px; font: 400 13.5px/1.5 var(--sans); }
.pop-list li::before { content: ''; position: absolute; left: 0; top: 7px; width: 8px; height: 8px; border-radius: 9999px; background: var(--bordeaux); }
.pop-list b { font-weight: 700; }
.roi { margin-top: 14px; border: 1.5px solid var(--bordeaux); background: #faf4f5; border-radius: 8px; padding: 10px 14px; font: 400 13.5px/1.55 var(--sans); }
.roi .k { display: block; margin-bottom: 4px; font: 800 10.5px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--bordeaux-deep); }
.ai-pop-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; border: none; border-radius: 9999px;
background: var(--surface-alt); font: 700 15px/1 var(--sans); color: var(--ink-muted); cursor: pointer; }
</style>
</head>
<body>
<div class="note"><b>EVALUATION</b> — icone Emblems (le tue preferite), <b>più grandi, senza bordi/card</b>, disposte in modo
<b>sparsamente organico</b> invece che a griglia. Tre composizioni per slide 02 e adattamento alla 03.</div>
<main id="deck"></main>
<nav class="switcher">
<span class="tag">PROTOTYPE</span>
<a href="?s=1" data-v="1">A · Constellation</a>
<a href="?s=2" data-v="2">B · Cascade</a>
<a href="?s=3" data-v="3">C · Orbit</a>
<span class="who" id="who"></span>
</nav>
<script>
/* ---- emblem icons (style 3, winner) ---- */
const EMB = {
cardioref: `<circle cx="24" cy="24" r="20" stroke-width="2.6"/>
<path d="M24 33 c-6-3.5-9-7-9-10.5 0-3 4-5.5 7-3 1 .8 1.6 1.8 2 3 .4-1.2 1-2.2 2-3 3-2.5 7 0 7 3 0 3.5-3 7-9 10.5z" fill="currentColor" stroke="none"/>
<path d="M22.5 21 h3 v3 h3 v3 h-3 v3 h-3 v-3 h-3 v-3 h3 z" fill="#fff" stroke="none"/>`,
genetic: `<circle cx="24" cy="24" r="20" stroke-width="2.6"/>
<path d="M18 8 C28 14 28 20 18 26 C12 30 12 35 18 40"/>
<path d="M30 8 C20 14 20 20 30 26 C36 30 36 35 30 40"/>
<path d="M17 12 H31"/><path d="M15.5 18 H32.5"/><path d="M17 24 H31"/><path d="M14.5 31 H22"/>
<rect x="25" y="29" width="12" height="12" rx="1.5" stroke-width="2"/>
<path d="M25 34 H37"/><path d="M30.5 29 V41"/>`,
omics: `<circle cx="24" cy="24" r="20" stroke-width="2.6"/>
<circle cx="24" cy="24" r="4" fill="currentColor" stroke="none"/>
<circle cx="24" cy="10" r="3.4"/><circle cx="11.5" cy="31" r="3.4"/><circle cx="36.5" cy="31" r="3.4"/>
<path d="M24 20.5 V13.5"/><path d="M21 26.5 L14 29.5"/><path d="M27 26.5 L34 29.5"/>`,
ecg: `<circle cx="24" cy="24" r="20" stroke-width="2.6"/>
<path d="M9 24 h6 l3-9 5 16 3-7 h14" stroke-width="2.4"/>
<circle cx="24" cy="24" r="1.8" fill="currentColor" stroke="none"/>`,
};
const GHOST = `<circle cx="24" cy="24" r="20" stroke-dasharray="5 4" stroke-width="2.4"/>
<path d="M15 33 L33 15" stroke-width="2.4"/><circle cx="17" cy="17" r="2.4" fill="currentColor" stroke="none"/>`;
const ico = (key, size) =>
`<svg class="ico" viewBox="0 0 48 48" style="width:${size}px;height:${size}px" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">${key === 'ghost' ? GHOST : EMB[key]}</svg>`;
const SYS = [
{ key: 'cardioref', name: 'Cardioref', scope: 'visits · operations · reports — twenty years of management on structured forms' },
{ key: 'genetic', name: 'Genetic data', scope: 'DNA instruments — results typed into Excel by hand' },
{ key: 'omics', name: 'Omics Portal', scope: 'meant to host Cardioref + genetics — still immature' },
{ key: 'ecg', name: 'ECG', scope: 'paper strip + a CSV on request' },
];
const GHOSTS = [
{ name: 'No Health Intelligence', scope: 'nothing for historical data' },
{ name: 'No ML-ready datamarts', scope: 'nothing for predictive statistics' },
];
const CONTRIBS = {
cardioref: { kicker: 'The starting point · 20 years ago', title: 'Cardioref — the daily workhorse',
body: `<p class="lead">A traditional relational application: classic forms for the typical hospital procedures.</p>
<ul class="pop-list"><li><b>Used for</b> — visits, operations, reports: the daily management of the Unit</li>
<li><b>Good at it</b> — twenty years of patient management on structured records</li>
<li><b>The catch</b> — the clinical story lives in free-text fields, not in the boxes designed for it</li></ul>
<div class="roi"><b class="k">Problem</b>Almost unusable for research: the knowledge is there — locked in prose.</div>` },
genetic: { kicker: 'The starting point · 20 years ago', title: 'Genetic data — sophisticated in, Excel out',
body: `<p class="lead">The DNA instruments are state-of-the-art. What happens after them is not.</p>
<ul class="pop-list"><li><b>Used for</b> — running the genetic analyses the Unit prescribes</li>
<li><b>The catch</b> — results are transcribed by hand into Excel files</li>
<li><b>Never connected</b> — Cardioref has no idea genetics exists</li></ul>
<div class="roi"><b class="k">Problem</b>Two parallel worlds: genotypes on one side, clinical records on the other.</div>` },
omics: { kicker: 'The starting point · 20 years ago', title: 'Omics Portal — the right idea, too soon',
body: `<p class="lead">A portal meant to host both Cardioref and the genetic data — the seed of AritmoLab.</p>
<ul class="pop-list"><li><b>Used for</b> — the first 360° patient-management features</li>
<li><b>The catch</b> — functionality still immature</li>
<li><b>Never connected</b> — it could not see the data it was meant to host</li></ul>
<div class="roi"><b class="k">Problem</b>A promising idea without a bridge: too young to carry the data.</div>` },
ecg: { kicker: 'The starting point · 20 years ago', title: 'ECG — paper, and a CSV if you ask',
body: `<p class="lead">Classic ECG devices: they print the strip, and they can export a file.</p>
<ul class="pop-list"><li><b>Used for</b> — acquiring and viewing ECG traces</li>
<li><b>The catch</b> — no machine-to-machine communication</li>
<li><b>The only door out</b> — a CSV file, produced on request</li></ul>
<div class="roi"><b class="k">Problem</b>Every ECG meant a manual download — automation was impossible by design.</div>` },
};
const chrome = (band) => `
<header class="head"><img src="logo.png" alt=""><span class="head-org">AritmoLab · Policlinico San Donato</span></header>
<nav class="band"><span class="band-section">${band}</span><span class="jump">0X · …</span></nav>`;
const foot = `<footer class="foot"><span>Role of AI in the Analysis of Unstructured Clinical Databases · Dr. Marco Pancotti - MultiPhysixLab · Dr. Sara Paratico - Gruppo San Donato · San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span>0X / 15</span></footer>`;
function isle(s, size, rot, x, y) {
const pos = x ? `left:${x}; top:${y};` : '';
return `<button class="isle" data-start="${s.key}" style="--rot:${rot}deg; --size:${size}px; ${pos}">
${ico(s.key, size)}<span class="nm">${s.name}</span><span class="sc">${s.scope}</span></button>`;
}
function ghostIsle(g, size, rot, x, y) {
const pos = x ? `left:${x}; top:${y};` : '';
return `<div class="isle ghost" style="--rot:${rot}deg; --size:${size}px; ${pos}">
${ico('ghost', size)}<span class="nm">${g.name}</span><span class="sc">${g.scope}</span></div>`;
}
/* ---- A · Constellation: scattered islands (hand-placed, organic jitter) ---- */
const TODO_STACK = [
{ key: 'ci', name: 'Clinical Intelligence', cap: 'dashboards on the clinical history of the Unit and its patients' },
{ key: 'dwh', name: 'Datawarehouse', cap: 'the single source of truth' },
{ key: 'ml', name: 'ML-ready data', cap: 'cohort tables, ready for model training' },
{ key: 'portal', name: 'Professional Portal', cap: 'AritmoLab — the grown-up Omics Portal' },
];
/* three placements of the central "to build" rectangles */
const ORBIT_CFG = {
1: { isles: [
{ key: 'cardioref', x: '0%', y: '2%', rot: -3, size: 124 },
{ key: 'genetic', x: '72%', y: '2%', rot: 3, size: 118 },
{ key: 'omics', x: '0%', y: '56%', rot: -2, size: 120 },
{ key: 'ecg', x: '72%', y: '56%', rot: 2, size: 118 } ],
stack: { left: '27%', top: '20%', width: '46%' }, offsets: ['0%', '8%', '4%', '10%'], icon: 116 },
2: { isles: [
{ key: 'cardioref', x: '36%', y: '4%', rot: -3, size: 118 },
{ key: 'genetic', x: '64%', y: '18%', rot: 3, size: 112 },
{ key: 'omics', x: '36%', y: '48%', rot: -2, size: 114 },
{ key: 'ecg', x: '63%', y: '60%', rot: 2, size: 112 } ],
stack: { left: '3%', top: '16%', width: '25%' }, offsets: ['0%', '10%', '4%', '12%'], icon: 106 },
3: { isles: [
{ key: 'cardioref', x: '1%', y: '6%', rot: -3, size: 120 },
{ key: 'genetic', x: '75%', y: '6%', rot: 3, size: 114 },
{ key: 'omics', x: '1%', y: '56%', rot: -2, size: 118 },
{ key: 'ecg', x: '75%', y: '56%', rot: 2, size: 114 } ],
stack: { left: '32%', top: '22%', width: '35%' }, offsets: ['0%', '7%', '4%', '9%'], icon: 114, grid: true },
};
function orbitSlide02(v) {
const cfg = ORBIT_CFG[v];
let h = `<div class="todo-stack" style="left:${cfg.stack.left}; top:${cfg.stack.top}; width:${cfg.stack.width}">` +
TODO_STACK.map((td, i) => `<div class="todo" data-missing="${td.key}" style="margin-left:${cfg.offsets[i]}"><span class="q">?</span><span class="tx"><b>${td.name}</b><span>${td.cap}</span></span></div>`).join('') + `</div>`;
for (const s of cfg.isles) {
const sys = SYS.find(vv => vv.key === s.key);
h += `<button class="isle" data-start="${s.key}" style="--rot:${s.rot}deg; --size:${s.size}px; left:${s.x}; top:${s.y}; width:230px">
${ico(s.key, s.size)}<span class="nm">${sys.name}</span><span class="sc">${sys.scope}</span></button>`;
}
h += `<div class="missing-title" style="left:calc(${cfg.stack.left} + 14px); top:calc(${cfg.stack.top} - 46px); width:${cfg.stack.width}">Missing Pieces</div>`;
return h;
}
function layoutZone(v) {
const items = [
['cardioref', 62, -3, '2%', '0%'],
['genetic', 62, 3, '52%', '16%'],
['omics', 62, -2, '4%', '38%'],
['ecg', 62, 3, '54%', '60%'],
];
let h = items.map(([k, size, rot, x, y]) => {
const s = SYS.find(vv => vv.key === k);
return `<div class="isle" style="--rot:${rot}deg; --size:${size}px; left:${x}; top:${y}; width:170px">
${ico(k, size)}<span class="nm">${s.name}</span></div>`;
}).join('');
h += `<div class="isle ghost" style="--rot:-2deg; --size:56px; left:32%; top:82%; width:170px">
${ico('ghost', 56)}<span class="nm" style="color:var(--bordeaux-deep)">missing</span></div>`;
return h;
}
const LAYOUTS = {
1: { desc: 'A · Center stack: i quattro obiettivi impilati al centro, i sistemi in orbita attorno.' },
2: { desc: 'B · Side column: la pila degli obiettivi a sinistra, i sistemi orbitano più larghi a destra.' },
3: { desc: 'C · Compact 2×2: griglia compatta e sfalsata al centro, orbita larga.' },
};
const q = new URLSearchParams(location.search);
const v = ['1','2','3'].includes(q.get('s')) ? q.get('s') : '1';
document.body.className = 's' + v;
document.querySelectorAll('.switcher a').forEach(a => a.classList.toggle('active', a.dataset.v === v));
document.getElementById('who').textContent = LAYOUTS[v].desc;
document.getElementById('deck').innerHTML = `
<section class="slide">
${chrome('Where we started')}
<div class="sbody">
<h2>Four islands and four missing pieces</h2>
<div class="scatter">${orbitSlide02(v)}</div>
</div>
${foot}
</section>
<section class="slide">
${chrome('The plan')}
<div class="sbody">
<div class="kicker">The plan · what we wanted to build</div>
<h2>One platform, two destinations</h2>
<div class="flow">
<div class="zone existed">
<span class="zlabel">What existed — to connect</span>
<div class="src-scatter">${layoutZone(v)}</div>
</div>
<div class="farrow">→</div>
<div class="zone built">
<span class="zlabel">What we wanted to build</span>
<div class="row">
<div class="fcol"><div class="fbox"><b>Staging</b><span>raw replica</span></div></div>
<div class="farrow">→</div>
<div class="fcol"><div class="fbox"><b>Integration</b><span>clean · text mining</span></div></div>
<div class="farrow">→</div>
<div class="fcol"><div class="fbox"><b>Star schema</b><span>warehouse</span></div></div>
<div class="farrow">→</div>
<div class="fcol"><div class="fbox"><b>Datamarts</b><span>for research</span></div></div>
<div class="farrow">→</div>
<div class="fcol"><div class="fbox end"><b>AritmoLab</b><span>portal + warehouse</span></div></div>
</div>
</div>
</div>
</div>
${foot}
</section>`;
const pop = document.createElement('div');
pop.className = 'ai-pop';
pop.innerHTML = `<div class="ai-pop-card" role="dialog"><button class="ai-pop-x">✕</button>
<div class="ai-pop-kicker" id="kick"></div><div class="ai-pop-title" id="ttl"></div><div id="bdy"></div></div>`;
document.body.appendChild(pop);
const MISSING_CONTRIBS = {
ci: { kicker: 'The missing piece · 20 years ago', title: 'Clinical Intelligence — the history, made visible',
body: `<p class="lead">Twenty years of clinical history, with no way to show it.</p>
<ul class="pop-list"><li><b>What it is</b> — dashboards on the Unit's activity and its patients: volumes, incidence, outcomes</li>
<li><b>Who needed it</b> — management, audit, clinical research</li>
<li><b>Why it never existed</b> — the data was in prose; building dashboards on text was impractical</li></ul>
<div class="roi"><b class="k">The gap</b>No fast answer to “how are we doing?” — every question meant a manual query.</div>` },
dwh: { kicker: 'The missing piece · 20 years ago', title: 'Datawarehouse — one analysis-ready home',
body: `<p class="lead">One home for every record the Unit produces: text, structured fields, genetics, ECG.</p>
<ul class="pop-list"><li><b>What it is</b> — a single schema joining all the sources, designed for analysis</li>
<li><b>Why it never existed</b> — each system kept its own silo; joining them meant hand-stitching exports</li></ul>
<div class="roi"><b class="k">The gap</b>Every research question started with manual exports — and ended in fragile spreadsheets.</div>` },
ml: { kicker: 'The missing piece · 20 years ago', title: 'ML-ready data — cohorts for research',
body: `<p class="lead">Tables shaped for statistics and model training: clean, wide, reproducible.</p>
<ul class="pop-list"><li><b>What it is</b> — one row per patient, features beside outcomes, rebuildable at will</li>
<li><b>Why it never existed</b> — building it by hand from free text and separate silos took weeks</li></ul>
<div class="roi"><b class="k">The gap</b>Predictive research on the Unit's data stayed out of reach.</div>` },
portal: { kicker: 'The missing piece · 20 years ago', title: 'Professional Portal — the 360° view',
body: `<p class="lead">The 360° view of the patient: Cardioref and genetics finally in one place.</p>
<ul class="pop-list"><li><b>What it is</b> — the grown-up Omics Portal: management, care and research on the same platform</li>
<li><b>Why it never existed</b> — the idea was there; the integrated data to feed it was not</li></ul>
<div class="roi"><b class="k">The gap</b>A single door for patients and clinicians — promised, never delivered.</div>` },
};
document.addEventListener('click', (e) => {
const s = e.target.closest('[data-start]');
if (s) {
const c = CONTRIBS[s.dataset.start];
document.getElementById('kick').textContent = c.kicker;
document.getElementById('ttl').textContent = c.title;
document.getElementById('bdy').innerHTML = c.body;
pop.classList.add('open');
return;
}
const m = e.target.closest('[data-missing]');
if (m) {
const c = MISSING_CONTRIBS[m.dataset.missing];
document.getElementById('kick').textContent = c.kicker;
document.getElementById('ttl').textContent = c.title;
document.getElementById('bdy').innerHTML = c.body;
pop.classList.add('open');
return;
}
if (e.target.closest('.ai-pop-x') || e.target === pop) pop.classList.remove('open');
});
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') pop.classList.remove('open'); });
</script>
</body>
</html>