feat: strengthen design skill and refresh all showcases
Some checks failed
ci / build (push) Has been cancelled

This commit is contained in:
Yun Chan 2026-09-12 18:16:33 +09:00
parent 69e5cc1efd
commit e09d4efb1c
257 changed files with 8196 additions and 2374 deletions

View file

@ -0,0 +1,27 @@
# Lumina Optics design.md
## 브리프
- 무엇을: IOR와 분산을 조작하는 가상 광학 시험대.
- 누구에게: 광선 모델의 조건과 결과물을 함께 읽으려는 방문자.
- 제약: WebGL/정적 폴백, 기존 슬라이더와 가상·측정 아님 표기를 유지한다.
## 결정표
| 축 | 관찰과 적용 범위 | 결정·역할 토큰 | 기각 조건 |
|---|---|---|---|
| 구도 | 기준 1440 화면에서 큰 프리즘이 제목과 조작을 폴드 아래로 밀었다. | 제목·한계 설명을 상단에 두고 `9:4` 캔버스/제어 프레임을 배치한다. `--container`, `--space-4`, `--space-6`. | 캔버스가 제어보다 큰 면적을 차지해 둘 중 하나가 첫 화면에서 사라지면 재조정한다. |
| 타입 | 제목은 모델의 의미, 모노는 조건과 조작값을 읽게 해야 한다. | `--step-3` 제목, `--step--1` 시험대 원장과 제어 보조문을 유지한다. | 실제 긴 라벨이 320px 또는 200% 확대에서 제어를 밀면 폭·간격을 수정한다. |
| 색 | 스펙트럼만 광학 결과를 나타내며 UI 색은 보조여야 한다. | `--surface`, `--surface-raised`, `--accent-stroke`을 프레임·포커스·입력에만 쓴다. | UI 색이 캔버스의 분산 광선보다 먼저 읽히면 채도를 줄인다. |
## 근거와 경계
[art-direction.md](../../../../../packages/skill/references/art-direction.md)의 Hofmann 관련 구도·군집 관찰을 제목/작업면 분리에만 적용했다. [typography.md](../../../../../packages/skill/references/typography.md)의 Ruder 관련 실제 카피·수치 proof 범위를 타입 계층에만 적용했다. [visual-design-lineage.md](../../../../../packages/skill/references/visual-design-lineage.md)의 Albers 관련 색 맥락은 캔버스와 UI 표면의 대비 확인에만 적용했다. 이 세 자료는 성능·이해도 법칙으로 사용하지 않는다.
[trustworthy-showcases.md](../../../../../packages/skill/references/trustworthy-showcases.md)의 진실 범위에 따라 모델·사양·곡선의 illustrative 표기를 가까운 공통 표면에 유지한다. 캔버스 안에는 텍스트를 굽지 않는다.
## 2026-09-12 현재 검증 기록
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.

View file

@ -3,15 +3,15 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumina Optics · Refraction Bench</title>
<title>Lumina Optics · Interactive Optical Study</title>
<meta
name="description"
content="A WebGL bench where a single beam meets a prism: refraction and dispersion computed per frame. Drag the prism, change the index."
content="An interactive optical study of refraction and dispersion. The lens profile, curves, and specifications are illustrative."
/>
<meta property="og:title" content="Lumina Optics · Refraction Bench" />
<meta property="og:title" content="Lumina Optics · Interactive Optical Study" />
<meta
property="og:description"
content="One beam, one prism, per-frame refraction."
content="An illustrative light path you can inspect and adjust."
/>
<meta property="og:type" content="website" />
<meta name="theme-color" content="#101214" />
@ -22,7 +22,7 @@
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="stylesheet" href="./styles/main.css?v=20260912all" />
<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='%23101214'/%3E%3Cpath d='M6 16h8m8 0h4M14 16l10-7v14z' fill='none' stroke='%23BFE8EE' stroke-width='1.4'/%3E%3C/svg%3E"
@ -45,40 +45,54 @@
</header>
<main id="main">
<!--
The bench. The product is light behaviour, so the hero IS the instrument:
one beam, one prism, per-frame refraction. Drag rotates; sliders change
the index and the dispersion — they are the two honest knobs a bench
has. Everything degrades to a static photograph.
-->
<section id="bench" class="hero" aria-labelledby="hero-h">
<div class="bench-stage reveal">
<canvas
id="gl"
aria-label="Interactive prism: a white beam enters from the left and exits as a spectrum. Drag to rotate the prism."
></canvas>
<img
class="bench-fallback"
src="./assets/prism-bench.webp"
alt="A glass prism on a dark optical breadboard splitting one collimated beam into a spectrum"
width="1440"
height="810"
/>
</div>
<div class="wrap hero-inner">
<div class="wrap hero-summary">
<div class="hero-text">
<p class="hero-kicker reveal">Refraction bench · WebGL</p>
<p class="hero-kicker reveal">Interactive optical study</p>
<h1 id="hero-h" class="reveal" style="--delay: 60ms">
One beam,<br />bent honestly
</h1>
<p class="lead reveal" style="--delay: 120ms">
No photograph on this screen is doing the work — the prism is
being refracted right now, per frame, on your GPU. Drag it. Change
the index. The spectrum you see is the same arithmetic lens
designers use, minus the glasses.
Move the prism to compare how refractive index and dispersion
redirect one beam. This view illustrates an optical model; it is
not a measured photograph, a product specification, or a lab result.
</p>
</div>
<div class="hud reveal" style="--delay: 160ms">
</div>
<div class="wrap bench-layout">
<div class="bench-shell">
<div class="bench-stage reveal">
<canvas
id="gl"
aria-label="Illustrative interactive prism: a white beam enters from the left and exits as a spectrum. Drag to rotate the concept model."
></canvas>
<img
class="bench-fallback"
src="./assets/prism-bench.webp"
alt="Illustrative glass prism on a dark optical surface splitting one collimated beam into a spectrum"
width="1440"
height="810"
/>
</div>
</div>
<aside class="bench-controls">
<div class="bench-ledger" role="group" aria-label="Bench configuration">
<dl>
<div>
<dt>illustrative beam reference</dt>
<dd>632.8 nm · collimated</dd>
</div>
<div>
<dt>variable</dt>
<dd>index + dispersion</dd>
</div>
<div>
<dt>view</dt>
<dd>interactive light study · static reference</dd>
</div>
</dl>
</div>
<div class="hud reveal" style="--delay: 160ms">
<div class="hud-row">
<div class="hud-row-head"><label for="ior">Index of refraction</label><output id="ior-out" for="ior">1.52</output></div>
<input type="range" id="ior" min="1.45" max="2.20" step="0.01" value="1.52" />
@ -87,56 +101,48 @@
<div class="hud-row-head"><label for="disp">Dispersion (Abbe ↓)</label><output id="disp-out" for="disp">0.18</output></div>
<input type="range" id="disp" min="0" max="0.6" step="0.01" value="0.18" />
</div>
<p class="hud-note" id="hud-note">Drag the prism to rotate · beam is collimated at 632.8 nm reference</p>
</div>
<p class="hud-note" id="hud-note">Drag the prism to rotate · illustrative light path at a 632.8 nm reference</p>
</div>
</aside>
</div>
</section>
<!--
Anatomy — cut lens photography. Real glass, not renders.
-->
<section id="anatomy" class="section" aria-labelledby="anatomy-h">
<div class="wrap two">
<figure class="two-photo reveal">
<img
src="./assets/lens-section.webp"
alt="Cutaway of a precision lens: a stack of glass elements in a metal barrel, edge-lit so each surface shows a refraction line"
alt="Illustrative lens cutaway: a stack of glass elements in a metal barrel, edge-lit to show refraction paths"
width="900"
height="1200"
loading="lazy"
/>
<figcaption>L-85 cutaway · 7 elements in 6 groups</figcaption>
<figcaption>L-85 concept cutaway · illustrative element layout</figcaption>
</figure>
<div class="two-text">
<h2 id="anatomy-h">Glass decides<br />before software does</h2>
<h2 id="anatomy-h">Glass sets<br />the light path</h2>
<p>
A lens is a chain of refractions, and every surface answers before
any firmware runs. Our L-85 uses two low-dispersion elements to
keep the spectrum you just dragged from fanning out across the
frame — dispersion corrected in glass, not in a profile.
A lens is a chain of refractions: every surface bends the path
before an image is made. The L-85 concept uses low-dispersion
elements to explain why a design can keep colours from spreading
across the frame.
</p>
<p class="muted">
The cutaway above is a physical section, milled for measurement.
Element count and spacing are measured from it, not from the
drawing.
The cutaway is a constructed reference image. Its element count
and spacing are illustrative, not a measurement of a commercial lens.
</p>
</div>
</div>
</section>
<!--
MTF — the only curve that argues. Static, thin, honest:
measured at infinity, full aperture, both field directions.
-->
<section id="mtf" class="section band" aria-labelledby="mtf-h">
<div class="wrap mtf-grid">
<div class="mtf-text">
<h2 id="mtf-h">MTF, not adjectives</h2>
<h2 id="mtf-h">MTF, read with conditions</h2>
<p>
“Tack sharp” is not a specification. Contrast transfer at given
line pairs per millimetre is. These curves are measured on an
optical bench at full aperture — sagittal and meridional, out to
the corner.
Contrast transfer at stated line pairs per millimetre gives a
clearer comparison than a slogan. These curves illustrate the
labels a measured chart needs; they are not optical-bench results.
</p>
<ul class="mtf-legend">
<li><span class="key key-solid" aria-hidden="true"></span>10 lp/mm · sagittal</li>
@ -146,7 +152,7 @@
</ul>
</div>
<figure class="mtf-plot reveal">
<svg viewBox="0 0 560 320" role="img" aria-label="MTF curves: contrast stays above 0.85 at 10 lp/mm across the field; at 30 lp/mm it falls from 0.75 at center to 0.42 at the corner.">
<svg viewBox="0 0 560 320" role="img" aria-label="Illustrative MTF curves showing how contrast can be compared from the centre to the corner; this is not a measured result.">
<g class="chart-grid">
<line x1="48" y1="16" x2="48" y2="272" />
<line x1="48" y1="272" x2="536" y2="272" />
@ -166,76 +172,62 @@
<text x="292" y="292" text-anchor="middle">image field (mm)</text>
<text x="536" y="292" text-anchor="end">corner 21.6</text>
</g>
<!-- 10 lp/mm sagittal: high, gentle fall -->
<polyline class="mtf mtf-10s" points="48,32 172,36 296,44 420,60 536,76" />
<!-- 10 lp/mm meridional -->
<polyline class="mtf mtf-10m" points="48,34 172,40 296,52 420,72 536,96" />
<!-- 30 lp/mm sagittal -->
<polyline class="mtf mtf-30s" points="48,84 172,96 296,120 420,160 536,196" />
<!-- 30 lp/mm meridional -->
<polyline class="mtf mtf-30m" points="48,90 172,108 296,140 420,186 536,232" />
</svg>
<figcaption>L-85 · f/1.8 · infinity focus · bench-measured 2026-07</figcaption>
<figcaption>L-85 concept · f/1.8 · infinity focus · illustrative curve</figcaption>
</figure>
</div>
</section>
<!--
Spec — with conditions.
-->
<section class="section" aria-labelledby="spec-h">
<div class="wrap">
<h2 id="spec-h" class="reveal">Specification</h2>
<h2 id="spec-h" class="reveal">Reference specification</h2>
<div class="table-wrap reveal" style="--delay: 80ms">
<table>
<caption class="sr-only">L-85 specification — item, value, condition</caption>
<caption class="sr-only">L-85 concept specification — illustrative item, value, and condition</caption>
<thead>
<tr>
<th scope="col">Item</th>
<th scope="col" class="num">Value</th>
<th scope="col" class="num">Illustrative value</th>
<th scope="col">Condition</th>
</tr>
</thead>
<tbody>
<tr><th scope="row">Focal length</th><td class="num">85 mm</td><td>nominal · ±1.5% at infinity</td></tr>
<tr><th scope="row">Aperture</th><td class="num">f/1.8 – f/16</td><td>9-blade · near-circular</td></tr>
<tr><th scope="row">Elements</th><td class="num">7 in 6</td><td>2 LD · measured from cutaway</td></tr>
<tr><th scope="row">Close focus</th><td class="num">0.62 m</td><td>1:7.2 magnification</td></tr>
<tr><th scope="row">Filter</th><td class="num">Ø 62 mm</td><td>brass, front-threaded</td></tr>
<tr><th scope="row">Weight</th><td class="num">438 g</td><td>without caps, with hood</td></tr>
<tr><th scope="row">Focal length</th><td class="num">85 mm</td><td>concept reference</td></tr>
<tr><th scope="row">Aperture</th><td class="num">f/1.8 – f/16</td><td>concept reference</td></tr>
<tr><th scope="row">Elements</th><td class="num">7 in 6</td><td>illustrative layout</td></tr>
<tr><th scope="row">Close focus</th><td class="num">0.62 m</td><td>illustrative configuration</td></tr>
<tr><th scope="row">Filter</th><td class="num">Ø 62 mm</td><td>illustrative configuration</td></tr>
<tr><th scope="row">Weight</th><td class="num">438 g</td><td>illustrative configuration</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Contact -->
<section id="contact" class="section band contact" aria-labelledby="contact-h">
<div class="wrap contact-grid">
<h2 id="contact-h">Bench visits</h2>
<h2 id="contact-h">About this study</h2>
<p>
We measure loaner samples for free and publish the curves —
manufacturers welcome or not. Bench time and MTF requests:
This is a concept page for reading an optical model. It does not
schedule sample loans, measurements, or MTF requests.
</p>
<a class="btn" href="mailto:bench@lumina.optics">bench@lumina.optics</a>
<a class="btn" href="#bench">Return to the light study</a>
</div>
</section>
</main>
<footer class="site-foot">
<div class="wrap foot-inner">
<p>lumina optics</p>
<p class="foot-meta">measurements reproducible on request · bench cal. 2026-06</p>
<p>lumina optics · optical study</p>
<p class="foot-meta">illustrative model · no commercial measurement service</p>
</div>
</footer>
<script type="module">
/*
Refraction bench — three.js, one prism, two honest uniforms.
Budget note: three.module.js ≈ 165KB gzip. 예산 상향 — 이 페이지의
제품이 WebGL 자체다(데모 브리프 예산 상향 허용).
폴백: WebGL 실패 시 사진이 보이고 캔버스는 뜨지 않는다(이미 마크업에 있음).
*/
const canvas = document.getElementById("gl");
const fallback = document.querySelector(".bench-fallback");
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
@ -243,32 +235,30 @@
let gl = null;
try {
gl = canvas.getContext("webgl2", { antialias: true, alpha: true });
} catch (e) { /* fall through */ }
} catch {}
if (!gl) {
if (!gl || reduced) {
canvas.remove();
document.getElementById("hud-note").textContent =
"WebGL unavailable — showing the bench photograph instead.";
reduced
? "Motion reduced — showing a static reference image instead."
: "Interactive view unavailable — showing a static reference image instead.";
} else {
fallback.remove();
const THREE = await import("https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js");
const renderer = new THREE.WebGLRenderer({ canvas, context: gl, antialias: true, alpha: true });
renderer.setClearColor(0x000000, 0);
const DPR = Math.min(devicePixelRatio || 1, 1.5);
renderer.setPixelRatio(DPR);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(32, 1, 0.1, 50);
camera.position.set(0, 0, 9);
// ----- prism: triangular cross-section (CylinderGeometry, 3 segments)
const prismGeo = new THREE.CylinderGeometry(2.1, 2.1, 2.6, 3, 1, false);
prismGeo.rotateX(Math.PI / 2); // axis toward camera
prismGeo.rotateZ(Math.PI / 2); // flat edge up to the beam
prismGeo.rotateX(Math.PI / 2);
prismGeo.rotateZ(Math.PI / 2);
const uniforms = {
uTime: { value: 0 },
uIor: { value: 1.52 },
uDisp: { value: 0.18 },
};
@ -276,7 +266,7 @@
const prismMat = new THREE.ShaderMaterial({
uniforms,
transparent: true,
vertexShader: /* glsl */`
vertexShader: `
varying vec3 vN;
varying vec3 vView;
void main() {
@ -285,44 +275,31 @@
vView = normalize(-mv.xyz);
gl_Position = projectionMatrix * mv;
}`,
fragmentShader: /* glsl */`
fragmentShader: `
precision highp float;
varying vec3 vN;
varying vec3 vView;
uniform float uIor;
uniform float uDisp;
// wavelength ramp: violet → cyan → yellow → red
vec3 spectrum(float t) {
t = clamp(t, 0.0, 1.0);
return clamp(
vec3(
smoothstep(0.30, 0.00, t) + smoothstep(0.70, 1.00, t),
smoothstep(0.00, 0.35, t) - smoothstep(0.60, 1.00, t),
smoothstep(0.00, 0.30, t) - smoothstep(0.45, 0.75, t)
), 0.0, 1.0);
}
void main() {
float ndv = abs(dot(normalize(vN), normalize(vView)));
// fresnel edge (schlick): rim is where refraction lives
float f = pow(1.0 - ndv, 3.0);
// refraction deflection grows with index
float bend = (uIor - 1.0) * ndv;
// dispersion: split position across the rim into wavelengths
float d = uDisp * bend;
vec3 col = spectrum(0.5 + (1.0 - ndv) * (0.6 + d * 4.0));
// glass body: near-black with faint interior glow
vec3 body = vec3(0.02, 0.025, 0.03) * (0.4 + ndv);
vec3 rim = col * f * 1.35;
float alpha = clamp(f * 0.95 + 0.06, 0.0, 1.0);
vec3 body = vec3(0.035, 0.082, 0.094) * (0.7 + ndv * 0.65);
vec3 rim = vec3(0.22, 0.70, 0.75) * (0.24 + f * 0.92);
float alpha = clamp(0.18 + f * 0.62, 0.0, 0.82);
gl_FragColor = vec4(body + rim, alpha);
}`,
side: THREE.DoubleSide,
});
const prism = new THREE.Mesh(prismGeo, prismMat);
scene.add(prism);
const prismEdges = new THREE.LineSegments(
new THREE.EdgesGeometry(prismGeo, 8),
new THREE.LineBasicMaterial({ color: 0xbfe8ee, transparent: true, opacity: 0.78 }),
);
prismEdges.renderOrder = 2;
const prismGroup = new THREE.Group();
prismGroup.add(prism, prismEdges);
scene.add(prismGroup);
// ----- beam: incoming line + dispersed fan (additive planes)
const beamGroup = new THREE.Group();
const mkBeam = (w, h, color1, color2, op) => {
const m = new THREE.Mesh(
@ -345,24 +322,35 @@
);
return m;
};
const beamIn = mkBeam(9, 0.10, 0xdfe8ec, 0xffffff, 0.55);
beamIn.position.set(0, 1.45, -0.2);
const beamIn = mkBeam(4.1, 0.12, 0xdfe8ec, 0xffffff, 0.92);
beamIn.position.set(-3.05, 0, -0.34);
beamGroup.add(beamIn);
const fan = mkBeam(9, 1.7, 0x8fd8e8, 0xf2b060, 0.30);
fan.position.set(-0.4, 0.35, -0.25);
fan.rotation.z = 0.085;
beamGroup.add(fan);
const spectrumRays = [0x7379e8, 0x62c9e8, 0x89dbaa, 0xf0da72, 0xee9866].map((color) => {
const ray = mkBeam(3.9, 0.075, color, color, 0.92);
beamGroup.add(ray);
return ray;
});
scene.add(beamGroup);
// ----- interaction
let dragging = false, lastX = 0, vel = 0.0016, rot = 0;
function updateSpectrum() {
const spread = 0.11 + uniforms.uDisp.value * 0.56;
const bend = (uniforms.uIor.value - 1.45) * 0.26;
spectrumRays.forEach((ray, i) => {
const offset = i / (spectrumRays.length - 1) - 0.5;
ray.position.set(2.75, offset * spread * 2.4 + bend, -0.38);
ray.rotation.z = offset * spread * 0.76;
});
}
updateSpectrum();
let dragging = false, lastX = 0, rot = -0.08;
canvas.addEventListener("pointerdown", (e) => { dragging = true; lastX = e.clientX; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener("pointermove", (e) => {
if (!dragging) return;
const dx = e.clientX - lastX;
lastX = e.clientX;
rot += dx * 0.006;
vel = dx * 0.0006;
renderFrame();
});
const end = () => (dragging = false);
canvas.addEventListener("pointerup", end);
@ -377,38 +365,32 @@
el.addEventListener("input", () => {
uni.value = parseFloat(el.value);
out.textContent = fmt(uni.value);
updateSpectrum();
renderFrame();
});
bind(iorEl, iorOut, uniforms.uIor, (v) => v.toFixed(2));
bind(dispEl, dispOut, uniforms.uDisp, (v) => v.toFixed(2));
// ----- sizing
const stage = canvas.parentElement;
function fit() {
const r = stage.getBoundingClientRect();
const h = Math.max(320, Math.min(r.height, 760));
const aspect = r.width / h;
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, aspect < 1.25 ? 1.25 : 1.5));
renderer.setSize(r.width, h, false);
camera.aspect = r.width / h;
camera.aspect = aspect;
camera.position.z = aspect < 1.25 ? 15.5 : 9;
camera.updateProjectionMatrix();
renderFrame();
}
fit();
new ResizeObserver(fit).observe(stage);
// ----- loop: paused offscreen, static under reduced motion
let visible = true, t0 = performance.now();
const io = new IntersectionObserver((en) => { visible = en[0].isIntersecting; });
io.observe(canvas);
function frame() {
if (!visible) { requestAnimationFrame(frame); return; }
const t = (performance.now() - t0) / 1000;
uniforms.uTime.value = t;
if (!dragging) { vel *= 0.98; rot += Math.max(0.0016, Math.abs(vel)) * Math.sign(vel || 1); }
prism.rotation.z = rot;
beamGroup.rotation.z = Math.sin(t * 0.4) * 0.01;
function renderFrame() {
prismGroup.rotation.z = rot;
renderer.render(scene, camera);
if (!reduced) requestAnimationFrame(frame);
}
frame(); // reduced-motion: renders exactly one static frame
renderFrame();
}
const io2 = new IntersectionObserver(

View file

@ -1,4 +1,4 @@
@import url("./tokens.css");
@import url("./tokens.css?v=20260912all");
/* ==========================================================================
Lumina Optics — 광학 시험대. 색은 캔버스 안에서만.
@ -145,15 +145,63 @@ a {
R2 원칙(design.md): 3D 위에 본문을 얹지 않는다. ─────────────────────── */
.hero {
padding-block: var(--space-4);
border-bottom: 1px solid var(--line);
}
.hero-summary {
margin-bottom: var(--space-4);
}
.bench-layout {
display: grid;
grid-template-columns: minmax(0, 9fr) minmax(17rem, 4fr);
gap: var(--space-4);
align-items: stretch;
}
.bench-shell {
min-width: 0;
background:
radial-gradient(72% 90% at 50% 22%, rgba(127, 185, 196, 0.06), transparent 66%),
var(--surface);
}
.bench-stage {
position: relative;
height: clamp(320px, 56vh, 560px);
height: clamp(20rem, 28vw, 22rem);
background:
radial-gradient(80% 90% at 50% 30%, rgba(127, 185, 196, 0.05), transparent 60%),
radial-gradient(70% 88% at 50% 48%, rgba(127, 185, 196, 0.10), transparent 68%),
var(--surface);
border-bottom: 1px solid var(--line);
border: 1px solid var(--line-strong);
border-radius: var(--radius);
overflow: hidden;
}
.bench-ledger {
background: var(--surface-raised);
border: 1px solid var(--line-strong);
border-radius: var(--radius);
}
.bench-ledger dl {
display: grid;
grid-template-columns: 1fr;
}
.bench-ledger dl > div {
min-width: 0;
padding: 0.85rem var(--space-3) 0.95rem;
border-top: 1px solid var(--line);
}
.bench-ledger dl > div:first-child {
border-top: 0;
}
.bench-ledger dt,
.bench-ledger dd {
font-family: var(--font-mono);
font-size: 0.6875rem;
letter-spacing: 0.045em;
}
.bench-ledger dt {
color: var(--ink-faint);
}
.bench-ledger dd {
margin-top: 0.2rem;
color: var(--ink-muted);
}
#gl,
.bench-fallback {
@ -169,13 +217,6 @@ a {
#gl:active {
cursor: grabbing;
}
.hero-inner {
display: grid;
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
gap: var(--space-5) var(--space-7);
align-items: start;
padding-block: var(--space-6);
}
.hero-kicker {
font-family: var(--font-mono);
font-size: var(--step--1);
@ -187,8 +228,8 @@ a {
margin-top: var(--space-3);
}
.hud {
background: color-mix(in srgb, var(--surface) 78%, transparent);
backdrop-filter: blur(8px);
margin-top: var(--space-3);
background: var(--surface-raised);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: var(--space-4);
@ -392,7 +433,9 @@ tbody td:not(.num) {
max-width: 34em;
}
.btn {
display: inline-block;
display: inline-flex;
align-items: center;
min-height: 44px;
border: 1px solid var(--accent-stroke);
color: var(--accent);
text-decoration: none;
@ -451,13 +494,11 @@ tbody td:not(.num) {
/* ── 반응형 ───────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
.hero-inner {
position: static;
.bench-layout {
grid-template-columns: 1fr;
padding-block: var(--space-5);
}
.bench-stage {
height: clamp(300px, 48vh, 460px);
height: clamp(320px, 82vw, 540px);
}
.two,
.mtf-grid,
@ -469,6 +510,20 @@ tbody td:not(.num) {
padding-block: var(--space-6);
}
}
@media (max-width: 640px) {
.bench-ledger dl {
grid-template-columns: 1fr;
}
.bench-ledger dl > div,
.bench-ledger dl > div:first-child {
padding-inline: var(--space-3);
border-left: 0;
border-top: 1px solid var(--line);
}
.bench-ledger dl > div:first-child {
border-top: 0;
}
}
@media (max-width: 560px) {
.head-inner {
flex-direction: column;
@ -477,6 +532,9 @@ tbody td:not(.num) {
}
.nav {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
justify-content: flex-start;
column-gap: var(--space-4);
row-gap: var(--space-2);
}
}

View file

@ -31,7 +31,7 @@
--surface-raised: #15181b;
--ink: #e8ecee;
--ink-muted: #9ba4a9;
--ink-faint: #6b7479;
--ink-faint: #849096;
--line: #23272b;
--line-strong: #394046;
--accent: #bfe8ee; /* 레이저 라인(밝게 — 텍스트용) */
@ -48,8 +48,8 @@
--radius: 2px;
--container: 1120px;
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4rem);
--container: 1240px;
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 5rem);
--dur-quick: 160ms;
--dur-normal: 420ms;