feat(skill): 자율 검증 폐쇄 루프 내재화 — 0.6.0
Some checks failed
ci / build (push) Failing after 5s

- 핵심 규칙 6: 완료의 정의는 게이트 통과. 사용자를 QA로 쓰지 않는다
- 5단계 기계 검사 4번째: design-gate 실행 의무화
- references/audit-gate.md: 사고-검사 매핑, SEO/meta 체크리스트, OS/브라우저 특성, 하니스 규칙
- tools/design-gate.mjs: 범용 게이트(메타/SEO·대비·수축·리듬·트랙·스케일×폭 + 옵션 L0/L3/L4, checks 깊은 병합)
- 리듬·트랙 불변식: 숫자 라벨 등폭·빈 셀 트랙 균일·등간격 — 시간표 자동배치 결함 재현 픽스처 검출, 앱 15뷰 통과(오탐 0)

feat(site): 관리 앱 2종 프로덕션 콘솔(v6→v18)

- 가온 학적부 9뷰·두레 수강신청 6뷰: shadcn 문법, Pretendard/Noto Serif/IBM Plex 폰트 전략, 볼드 금지(400/500/600), WCAG AA 대비 전면 교정, 한글 keep-all 조판
- LMS 필수 요소(알림 센터·공지·진도·평가 유형·출결 사유·학점 경고), 12명 기준 데이터 정합, SEO 구조(h1 유일·OG/twitter·og.png)
- 시간표 자동배치 결함 수리(명시적 격자 좌표)
- QA 게이트: L0 stylelint/html-validate · L1 단위 30 · L2 감사 61+불변식 · L3 시각회귀 30화면 · L4 WebKit · L5 키보드 탐색 — npm run verify 실패 시 배포 금지 체인
This commit is contained in:
Yun Chan 2026-08-22 21:22:05 +09:00
parent 78ddc8b61a
commit 72337b7ee0
226 changed files with 18212 additions and 380 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -0,0 +1,331 @@
<!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=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>

View file

@ -0,0 +1,433 @@
@import url("./tokens.css");
/* ==========================================================================
Synapse BCI Lab dark-instrument. 측정 조건이 붙은 계기.
========================================================================== */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html {
-webkit-text-size-adjust: 100%;
scroll-behavior: smooth;
background: var(--surface);
color-scheme: dark;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
body {
background: var(--surface);
color: var(--ink);
font-family: var(--font-body);
font-size: var(--step-0);
line-height: var(--leading-normal);
-webkit-font-smoothing: antialiased;
}
:where(h1, h2, h3) {
font-weight: 500;
line-height: var(--leading-tight);
letter-spacing: -0.015em;
overflow-wrap: break-word;
text-wrap: balance;
}
h1 {
font-size: var(--step-3);
}
h2 {
font-size: var(--step-2);
}
h3 {
font-size: var(--step-0);
font-weight: 600;
}
:where(p, th, td, figcaption, li) {
overflow-wrap: break-word;
line-height: var(--leading-normal);
text-wrap: pretty;
}
.lead {
color: var(--ink-muted);
max-width: 36em;
}
.muted {
color: var(--ink-muted);
}
a {
color: inherit;
}
.link-quiet {
color: var(--ink-muted);
text-decoration: none;
border-bottom: 1px solid var(--line-strong);
padding-bottom: 1px;
transition: color var(--dur-quick) var(--ease-soft),
border-color var(--dur-quick) var(--ease-soft);
}
.link-quiet:hover {
color: var(--accent);
border-color: var(--accent);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.skip {
position: absolute;
left: -9999px;
top: 0;
background: var(--ink);
color: var(--surface);
padding: var(--space-3) var(--space-4);
z-index: 100;
}
.skip:focus {
left: 0;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.wrap {
width: min(100% - 2 * var(--pad-inline), var(--container));
margin-inline: auto;
}
/* ── 헤더 ─────────────────────────────────────────────────────────────── */
.site-head {
position: sticky;
top: 0;
z-index: 50;
background: color-mix(in srgb, var(--surface) 86%, transparent);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--line);
}
.head-inner {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-4);
padding-block: var(--space-3);
}
.wordmark {
font-family: var(--font-mono);
font-size: var(--step-1);
font-weight: 500;
text-decoration: none;
letter-spacing: -0.02em;
}
.wordmark-dot {
color: var(--accent);
}
.nav {
display: flex;
gap: clamp(var(--space-3), 2vw, var(--space-5));
font-size: var(--step--1);
}
.nav a {
text-decoration: none;
color: var(--ink-muted);
padding-block: 2px;
border-bottom: 1px solid transparent;
transition: color var(--dur-quick) var(--ease-soft),
border-color var(--dur-quick) var(--ease-soft);
}
.nav a:hover {
color: var(--ink);
border-color: var(--accent);
}
/* ── 히어로 + 래스터 ──────────────────────────────────────────────────── */
.hero {
padding-block: var(--space-8) var(--space-7);
}
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 6fr) minmax(0, 6fr);
gap: var(--space-6) var(--space-7);
align-items: center;
}
.hero-kicker {
font-family: var(--font-mono);
font-size: var(--step--1);
color: var(--accent);
letter-spacing: 0.08em;
margin-bottom: var(--space-4);
}
.hero .lead {
margin-top: var(--space-4);
}
.hero-actions {
margin-top: var(--space-5);
display: flex;
align-items: center;
gap: var(--space-4);
flex-wrap: wrap;
}
.btn {
display: inline-block;
border: 1px solid var(--accent);
color: var(--accent);
text-decoration: none;
font-weight: 500;
padding: 0.7rem 1.4rem;
border-radius: var(--radius);
transition: background var(--dur-quick) var(--ease-soft),
color var(--dur-quick) var(--ease-soft);
}
.btn:hover {
background: var(--accent);
color: var(--surface);
}
.raster-frame {
border: 1px solid var(--line);
border-radius: var(--radius-frame);
background: var(--surface-raised);
padding: var(--space-3);
}
.raster-head {
display: flex;
justify-content: space-between;
gap: var(--space-3);
font-family: var(--font-mono);
font-size: 0.6875rem;
letter-spacing: 0.08em;
color: var(--ink-faint);
margin-bottom: var(--space-2);
}
#raster {
display: block;
width: 100%;
height: 320px;
}
.raster-legend {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-2);
font-size: 0.75rem;
color: var(--ink-faint);
}
.legend-tick {
display: inline-block;
width: 2px;
height: 12px;
background: var(--accent-dim);
}
/* ── 섹션 공통 ────────────────────────────────────────────────────────── */
.section {
padding-block: var(--space-8);
}
.band {
background: var(--surface-raised);
border-block: 1px solid var(--line);
}
/* ── 디바이스 2단 ─────────────────────────────────────────────────────── */
.two {
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
gap: var(--space-6) var(--space-7);
align-items: center;
}
.two-photo img {
display: block;
width: 100%;
height: auto;
border-radius: var(--radius);
}
.two-photo figcaption {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--ink-faint);
margin-top: var(--space-2);
}
.two-text p + p {
margin-top: var(--space-4);
}
/* ── 수치 표 ──────────────────────────────────────────────────────────── */
.table-wrap {
margin-top: var(--space-4);
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
text-align: left;
font-weight: 500;
font-size: var(--step--1);
color: var(--ink-faint);
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--line-strong);
}
tbody th {
font-weight: 500;
text-align: left;
white-space: nowrap;
}
tbody td,
tbody th {
padding: var(--space-3);
border-bottom: 1px solid var(--line);
vertical-align: baseline;
}
tbody td:not(.num) {
color: var(--ink-muted);
font-size: var(--step--1);
}
.num {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
text-align: right;
white-space: nowrap;
}
.measure-note {
margin-top: var(--space-3);
font-size: var(--step--1);
color: var(--ink-faint);
}
/* ── 어레이 판 ────────────────────────────────────────────────────────── */
.plate-fig img {
display: block;
width: 100%;
height: auto;
}
.plate-fig figcaption {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--ink-faint);
margin-top: var(--space-2);
}
/* ── 윤리 ─────────────────────────────────────────────────────────────── */
.ethics-grid h2 {
margin-bottom: var(--space-5);
}
.ethics-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: var(--space-5) var(--space-6);
border-top: 1px solid var(--line);
padding-top: var(--space-5);
}
.ethics-list h3 {
color: var(--ink);
}
.ethics-list p {
margin-top: var(--space-2);
color: var(--ink-muted);
font-size: var(--step--1);
}
/* ── 연락 ─────────────────────────────────────────────────────────────── */
.contact-grid {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: var(--space-4) var(--space-7);
align-items: center;
}
.contact-grid p {
color: var(--ink-muted);
max-width: 34em;
}
/* ── 푸터 ─────────────────────────────────────────────────────────────── */
.site-foot {
border-top: 1px solid var(--line);
padding-block: var(--space-4);
font-size: var(--step--1);
color: var(--ink-muted);
}
.foot-inner {
display: flex;
justify-content: space-between;
gap: var(--space-3) var(--space-5);
flex-wrap: wrap;
}
.foot-meta {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--ink-faint);
}
/* ── 등장 모션 ────────────────────────────────────────────────────────── */
.reveal {
opacity: 0;
translate: 0 12px;
transition: opacity var(--dur-normal) var(--ease-soft),
translate var(--dur-normal) var(--ease-soft);
transition-delay: var(--delay, 0ms);
}
.reveal.is-in {
opacity: 1;
translate: 0 0;
}
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
translate: 0 0;
transition: none;
}
}
/* ── 반응형 ───────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
.hero-grid,
.two,
.contact-grid {
grid-template-columns: 1fr;
gap: var(--space-5);
}
.hero,
.section {
padding-block: var(--space-6);
}
}
@media (max-width: 560px) {
.head-inner {
flex-direction: column;
align-items: flex-start;
gap: var(--space-1);
}
.nav {
width: 100%;
justify-content: space-between;
}
}

View file

@ -0,0 +1,54 @@
/* ==========================================================================
Synapse BCI Lab 디자인 토큰
프리셋: dark-instrument (연구 계기판)
컨셉 문장: 측정 조건이 붙은 계기 신경 데이터는 장식이 아니라 증거다.
레퍼런스: 방향 앵커 dogfood-synapse(신경인터페이스 업종). 파일의 수치는
audit-references.md 날조 판명 토큰은 브리프 재정의. 골드 1색은
'전극의 금'에서, 배경은 측정 암실(거의 )에서.
하지 않은 : 'superhuman' 카피, 두뇌 일러스트, 싸이버 글로우, 채널
자랑 배너. 이유: 의료 기기의 신뢰는 미측정값의 공백에서도 온다.
========================================================================== */
:root {
/* 타입 — 최대(58px) ÷ 본문(16px) ≈ 3.6배 */
--step--1: 0.8125rem;
--step-0: 1rem;
--step-1: 1.1875rem;
--step-2: clamp(1.375rem, 1.2rem + 0.9vw, 1.75rem);
--step-3: clamp(2.125rem, 1.4rem + 3.4vw, 3.625rem);
--leading-tight: 1.24;
--leading-normal: 1.72;
--font-body: "IBM Plex Sans", -apple-system, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--surface: #08090b;
--surface-raised: #0d0f13;
--ink: #e9ebee;
--ink-muted: #9aa1ab;
--ink-faint: #69707a;
--line: #1e2229;
--line-strong: #343a45;
--accent: #d9a441; /* 전극 금 — 텍스트·선 */
--accent-dim: rgba(217, 164, 65, 0.4);
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 1rem;
--space-4: 1.5rem;
--space-5: 2.5rem;
--space-6: 4rem;
--space-7: 6rem;
--space-8: clamp(4.5rem, 3rem + 6vw, 8.5rem);
--radius: 2px;
--radius-frame: 6px;
--container: 1120px;
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4rem);
--dur-quick: 160ms;
--dur-normal: 420ms;
--ease-soft: cubic-bezier(0.25, 1, 0.35, 1);
}