designpaca/apps/site/public/work/synapse-bci/index.html
Yun Chan 123d9fc5a8 fix(showcase): 미정의 CSS 변수·한글 조판·접근성 결함 수리 및 무결성 게이트 두 겹 신설
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 상자 안의 넓은 표를 오버플로로 잡던 것. 재는 도구가 틀리면
통과도 실패도 의미가 없다.
2026-08-24 12:50:57 +09:00

331 lines
13 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>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>