stylelint 도 html-validate 도 "var(--x) 를 썼는데 --x 가 어디에도 없다"를 잡지 못한다. 미정의 변수는 선언 전체를 무효로 만드는데 콘솔 에러가 없어 조용히 남는다. 검사기를 심자마자 v0.9.1 의 실제 결함이 나왔다. 게이트 신설 - tools/integrity.mjs — L0 무결성. CSS 커스텀 프로퍼티 참조, 상대경로, 한글 조판 필수 규칙(keep-all·한글 폰트 지정), 슬롭 지문, 스크롤 리스너를 검사. verify.mjs 체인에 편입해 실패 시 배포를 막는다. 폴백이 있는 var(--x, 기본값) 은 선택적 주입이므로 미정의로 세지 않는다. - tools/showcase-audit.mjs — 320/390/768/1440 실렌더 감사. 가로 스크롤·오버플로· 컨트롤 줄바꿈·대비 AA·왼쪽 인셋·h1 개수·히어로 줄 수·깨진 이미지·콘솔 에러. 반투명 배경을 합성해 대비를 재고, WebGL 페이지를 위해 swiftshader 로 렌더한다. - html-validate 대상을 두 페이지에서 public/work 전체로 넓혔다. 수리한 결함 - 미정의 변수 64건: --dur-fast·--radius-xs 가 정의된 적이 없어 cask-archive, cheongon-care, materia-interior 의 transition 선언이 통째로 무효였다. 새 토큰을 만들지 않고 기존 --dur-quick·--radius 로 매핑했다 — 토큰을 늘리면 radius 어휘가 4종이 되어 하드 게이트 3에 걸린다. - 한글 조판: lumina-optics·pulsegate·synapse-bci 가 word-break: keep-all 없이 IBM Plex Sans 만 지정해 한글이 시스템 기본으로 떨어지고 있었다. 같은 계열의 IBM Plex Sans KR 을 스택에 넣고 keep-all 을 추가했다. - 슬롭 지문 19건: text-transform: uppercase 로 만든 영문 대문자 eyebrow 를 한글 문장으로 바꾸고, 대문자용으로 잡아둔 넓은 자간과 모노 서체도 되돌렸다. - 하드 게이트 9: materia-interior/hannam-duplex.html 의 100vh → 100dvh. 이 파일은 어디서도 참조되지 않으면서 인라인 hex 까지 갖고 있어 토큰 기반으로 다시 썼다. - 접근성 11건: label 하나가 output 과 input 둘에 걸리던 것 5건(font-foundry, lumina-optics), dl > div > div > dt 4건(muwol-coffee), div 에 붙은 aria-label 2건(overtone-sound, pulsegate). 측정기 자체의 오류 둘도 고쳤다 — 눈에 보이지 않는 .sr-only 를 대비 실패로 잡던 것과, overflow-x: auto 상자 안의 넓은 표를 오버플로로 잡던 것. 재는 도구가 틀리면 통과도 실패도 의미가 없다.
331 lines
13 KiB
HTML
331 lines
13 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Synapse BCI Lab · 64-Channel Neural Interface Engine</title>
|
||
<meta
|
||
name="description"
|
||
content="A 64-channel μECoG array decoding cortical intent in 1.82 ms. Every number on this page carries its measurement condition."
|
||
/>
|
||
<meta property="og:title" content="Synapse BCI Lab · 64-Channel Neural Interface" />
|
||
<meta
|
||
property="og:description"
|
||
content="Cortial intent, decoded in under two milliseconds."
|
||
/>
|
||
<meta property="og:type" content="website" />
|
||
<meta name="theme-color" content="#08090B" />
|
||
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link
|
||
rel="stylesheet"
|
||
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Sans+KR:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
|
||
/>
|
||
<link rel="stylesheet" href="./styles/main.css?v=2" />
|
||
<link
|
||
rel="icon"
|
||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%2308090B'/%3E%3Cpath d='M6 24l6-6 4 4 9-14' fill='none' stroke='%23D9A441' stroke-width='2'/%3E%3Cpath d='M19 8h6v6' fill='none' stroke='%23D9A441' stroke-width='2'/%3E%3C/svg%3E"
|
||
/>
|
||
</head>
|
||
|
||
<body>
|
||
<a class="skip" href="#main">Skip to main content</a>
|
||
|
||
<header class="site-head">
|
||
<div class="wrap head-inner">
|
||
<a class="wordmark" href="#main">synapse<span class="wordmark-dot">·</span>bci</a>
|
||
<nav class="nav" aria-label="Sections">
|
||
<a href="#raster">Raster</a>
|
||
<a href="#device">Device</a>
|
||
<a href="#numbers">Numbers</a>
|
||
<a href="#ethics">Ethics</a>
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
|
||
<main id="main">
|
||
<!--
|
||
Hero — the product is a signal, so the first screen shows one:
|
||
a 64-channel spike raster, procedurally generated (no recording
|
||
claimed). Static frame under reduced motion; never drawn offscreen.
|
||
-->
|
||
<section class="hero" aria-labelledby="hero-h">
|
||
<div class="wrap hero-grid">
|
||
<div class="hero-text">
|
||
<p class="hero-kicker reveal">64-CHANNEL μECoG · 30 kS/s · 1.82 ms</p>
|
||
<h1 id="hero-h" class="reveal" style="--delay: 60ms">
|
||
Intent, decoded<br />before the hand moves
|
||
</h1>
|
||
<p class="lead reveal" style="--delay: 120ms">
|
||
Synapse reads cortical surface potentials through a 1.8 mm-thin
|
||
array and turns them into device commands in under two
|
||
milliseconds — faster than the signal reaches the muscle. The
|
||
raster on the right is a simulation of that stream, not a
|
||
recording of a person.
|
||
</p>
|
||
<div class="hero-actions reveal" style="--delay: 180ms">
|
||
<a class="btn" href="#numbers">Read the numbers</a>
|
||
<a class="link-quiet" href="#ethics">Our implant ethics</a>
|
||
</div>
|
||
</div>
|
||
|
||
<figure class="raster-frame reveal" style="--delay: 140ms">
|
||
<figcaption class="raster-head">
|
||
<span>SPIKE RASTER · 64 CH · SIMULATED</span>
|
||
<span>2.0 s window</span>
|
||
</figcaption>
|
||
<canvas id="raster" aria-label="Simulated 64-channel spike raster: sparse vertical ticks scrolling across a two-second window, with occasional synchronized bursts."></canvas>
|
||
<div class="raster-legend">
|
||
<span class="legend-tick" aria-hidden="true"></span>
|
||
<span>each tick — one detected spike · bursts are channel-sync events</span>
|
||
</div>
|
||
</figure>
|
||
</div>
|
||
</section>
|
||
|
||
<!--
|
||
Device — what is actually implanted. Macro photography, no renders.
|
||
-->
|
||
<section id="device" class="section" aria-labelledby="device-h">
|
||
<div class="wrap two">
|
||
<figure class="two-photo reveal">
|
||
<img
|
||
src="./assets/chip-macro.webp"
|
||
alt="A black silicon neural interface die with dense gold traces and a micro-electrode grid at one edge"
|
||
width="900"
|
||
height="1200"
|
||
loading="lazy"
|
||
/>
|
||
<figcaption>SYN-64 die · 4.2 × 4.2 mm · gold on black silicon</figcaption>
|
||
</figure>
|
||
<div class="two-text">
|
||
<h2 id="device-h">Thin enough<br />to be forgotten</h2>
|
||
<p>
|
||
The array sits on the cortical surface, under the dura — 1.8 mm
|
||
thin, 64 electrodes at 2.5 mm pitch. No puncturing tissue, no
|
||
threads into the parenchyma. Patients in our trial report the
|
||
implant becomes imperceptible within weeks; that, not channel
|
||
count, is the design goal we report first.
|
||
</p>
|
||
<p class="muted">
|
||
The die photographs are of the actual implant, not renders. What
|
||
you see is what a surgeon holds.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!--
|
||
Numbers — every value with its condition. The trust band.
|
||
-->
|
||
<section id="numbers" class="section band" aria-labelledby="numbers-h">
|
||
<div class="wrap">
|
||
<h2 id="numbers-h" class="reveal">Numbers, with their conditions</h2>
|
||
<div class="table-wrap reveal" style="--delay: 80ms">
|
||
<table>
|
||
<caption class="sr-only">
|
||
Measured performance — metric, value, measurement condition
|
||
</caption>
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Metric</th>
|
||
<th scope="col" class="num">Value</th>
|
||
<th scope="col">Condition</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<th scope="row">Decode latency</th>
|
||
<td class="num">1.82 ms</td>
|
||
<td>p50 · on-device inference · 12 participants</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row">Raw sampling</th>
|
||
<td class="num">30 kS/s</td>
|
||
<td>per channel · 24-bit ΔΣ</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row">SNR</th>
|
||
<td class="num">18.4 dB</td>
|
||
<td>referenced to skull electrode · chronic week 26</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row">Gesture classes</th>
|
||
<td class="num">11</td>
|
||
<td>offline, cross-validated · 94.1% ± 1.8</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row">Wireless link</th>
|
||
<td class="num">—</td>
|
||
<td>transcutaneous power + 2.4 GHz telemetry</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="measure-note reveal" style="--delay: 140ms">
|
||
All values from the first-in-human trial (N=12), 2025-09 – 2026-06.
|
||
Wireless link figure withheld pending regulatory filing — a blank
|
||
beats a guess.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!--
|
||
Array plate — full-bleed.
|
||
-->
|
||
<section class="section plate" aria-label="Microelectrode array photograph">
|
||
<figure class="plate-fig reveal">
|
||
<img
|
||
src="./assets/thread-array.webp"
|
||
alt="Dozens of fine gold-tipped microelectrode threads fanning out across a dark stage under a microscope"
|
||
width="1440"
|
||
height="810"
|
||
loading="lazy"
|
||
/>
|
||
<figcaption class="wrap">Electrode fan-out · microscope stage · no color grading</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<!--
|
||
Ethics — a BCI page without an ethics section is a red flag.
|
||
This is deliberate content, not compliance decoration.
|
||
-->
|
||
<section id="ethics" class="section" aria-labelledby="ethics-h">
|
||
<div class="wrap ethics-grid">
|
||
<h2 id="ethics-h">What we will not do</h2>
|
||
<ul class="ethics-list">
|
||
<li class="reveal">
|
||
<h3>No neural advertising</h3>
|
||
<p>
|
||
Decoded signals are commands, not content. We do not build — or
|
||
license — emotion or attention inference.
|
||
</p>
|
||
</li>
|
||
<li class="reveal" style="--delay: 80ms">
|
||
<h3>Participants can leave</h3>
|
||
<p>
|
||
Explantation on request within 30 days, at our cost, at any
|
||
point in the trial. Three participants have exercised it.
|
||
</p>
|
||
</li>
|
||
<li class="reveal" style="--delay: 160ms">
|
||
<h3>Raw data stays in the vault</h3>
|
||
<p>
|
||
Raw neural data never leaves our encrypted cluster. Published
|
||
figures are aggregate statistics, as on this page.
|
||
</p>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Research contact -->
|
||
<section class="section band contact" aria-labelledby="contact-h">
|
||
<div class="wrap contact-grid">
|
||
<h2 id="contact-h">Research contact</h2>
|
||
<p>
|
||
Trial enrollment is closed. Collaboration and data-access inquiries
|
||
from accredited institutions:
|
||
</p>
|
||
<a class="btn" href="mailto:lab@synapse.bci">lab@synapse.bci</a>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<footer class="site-foot">
|
||
<div class="wrap foot-inner">
|
||
<p>synapse bci lab</p>
|
||
<p class="foot-meta">IRB-2025-0412 · results are preliminary until peer review</p>
|
||
</div>
|
||
</footer>
|
||
|
||
<script>
|
||
/*
|
||
Spike raster — procedural, self-scrolling. Poisson-ish per-channel
|
||
spikes plus occasional cross-channel bursts. No audio, no recording,
|
||
no data claim: it is labeled SIMULATED. Pauses offscreen; a static
|
||
frame under prefers-reduced-motion.
|
||
*/
|
||
const canvas = document.getElementById("raster");
|
||
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||
const CH = 64;
|
||
let ctx, w, h, dpr, running = false;
|
||
|
||
function fit() {
|
||
dpr = Math.min(devicePixelRatio || 1, 1.5);
|
||
w = canvas.clientWidth;
|
||
h = canvas.clientHeight;
|
||
canvas.width = Math.round(w * dpr);
|
||
canvas.height = Math.round(h * dpr);
|
||
ctx = canvas.getContext("2d");
|
||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
ctx.fillStyle = "#0b0d10";
|
||
ctx.fillRect(0, 0, w, h);
|
||
}
|
||
|
||
const rate = () => 0.02 + Math.random() * 0.05; // per channel per frame
|
||
const GOLD = "rgba(217, 164, 65, 0.9)";
|
||
const GOLD_DIM = "rgba(217, 164, 65, 0.45)";
|
||
|
||
function column(x) {
|
||
const burst = Math.random() < 0.006;
|
||
const rowH = h / CH;
|
||
for (let ch = 0; ch < CH; ch++) {
|
||
const fire = burst ? Math.random() < 0.55 : Math.random() < rate();
|
||
if (!fire) continue;
|
||
ctx.strokeStyle = burst ? GOLD : GOLD_DIM;
|
||
ctx.beginPath();
|
||
const y = ch * rowH + rowH / 2;
|
||
ctx.moveTo(x, y - rowH * 0.36);
|
||
ctx.lineTo(x, y + rowH * 0.36);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
function step() {
|
||
if (!running) return;
|
||
const dx = 2;
|
||
// self-copy shift: draw the canvas onto itself, one column left
|
||
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, -dx * dpr, 0, canvas.width, canvas.height);
|
||
ctx.fillStyle = "#0b0d10";
|
||
ctx.fillRect(w - dx - 1, 0, dx + 2, h);
|
||
column(w - 1);
|
||
requestAnimationFrame(step);
|
||
}
|
||
|
||
function staticFrame() {
|
||
for (let x = 0; x < w; x += 3) column(x);
|
||
}
|
||
|
||
fit();
|
||
addEventListener("resize", fit, { passive: true });
|
||
if (reduced) {
|
||
staticFrame();
|
||
} else {
|
||
const io = new IntersectionObserver((entries) => {
|
||
const on = entries[0].isIntersecting;
|
||
if (on && !running) {
|
||
running = true;
|
||
requestAnimationFrame(step);
|
||
} else if (!on) {
|
||
running = false;
|
||
}
|
||
});
|
||
io.observe(canvas);
|
||
}
|
||
|
||
const io2 = new IntersectionObserver(
|
||
(entries) => {
|
||
for (const e of entries) {
|
||
if (!e.isIntersecting) continue;
|
||
e.target.classList.add("is-in");
|
||
io2.unobserve(e.target);
|
||
}
|
||
},
|
||
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
|
||
);
|
||
for (const el of document.querySelectorAll(".reveal")) io2.observe(el);
|
||
</script>
|
||
</body>
|
||
</html>
|