HUD 디자인, 화면이 나를 겨누는 랜딩 부품 9개
HUD 디자인은 게임이나 SF 영화 속 조준경·레이더처럼, 화면 위에 좌표·상태 수치를 얹어 마치 나를 지켜보고 있는 듯한 인상을 주는 인터페이스 스타일입니다.
아래 원페이지를 먼저 스크롤해 보십시오. 상단 내비게이션부터 맨 아래 터미널 푸터까지, 네 귀퉁이 브래킷과 스캔라인이 화면 전체에서 같은 리듬으로 움직이는지가 이 스타일이 성립하는 기준입니다.

이 화면을 만든 터미널 UI 부품 아홉 개는 아래 격자에서 각각 따로 열어 볼 수 있습니다. 배열 순서는 방문자가 실제로 페이지를 훑는 흐름(첫눈에 들어오기, 메뉴 살피기, 기능 훑기, 진행 로그 보기, 요금 고르기, 후기로 믿음 얻기, 행동 촉구, 가입, 하단 정리) 그대로입니다. 같은 design 기둥에서 이 호박색 브래킷 대신 크롬 광택을 앞세운 쪽이 Y2K 크롬·홀로그램 부품이고, 대비를 끝까지 낮춰 그림자 두 겹만 남긴 쪽이 뉴모피즘 랜딩 UI 키트입니다.
01 hero-hud
02 nav-hud
03 card-grid-hud
04 feature-list-hud
05 pricing-hud
06 testimonial-hud
07 cta-band-hud
08 footer-hud
09 buttons-inputs-hud
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
01락온 히어로
무엇이 움직이나
헤드라인 카드를 감싼 네 귀퉁이 브래킷이 0.15초씩 어긋난 시차로 조여들듯 맥동하고, 뒤로는 스캔라인이 카드 안쪽을 위에서 아래로 한 번씩 훑고 지나갑니다.

<main class="stage">
<section class="hero is-demo">
<span class="hero__corner hero__corner--tl" aria-hidden="true"></span>
<span class="hero__corner hero__corner--tr" aria-hidden="true"></span>
<span class="hero__corner hero__corner--bl" aria-hidden="true"></span>
<span class="hero__corner hero__corner--br" aria-hidden="true"></span>
<div class="hero__scan" aria-hidden="true"></div>
<p class="hero__eyebrow"><span class="hero__dot" aria-hidden="true"></span>SENTRY / ONLINE</p>
<h1 class="hero__title">놓치는 순간 없이<br>지켜보는 대시보드</h1>
<p class="hero__desc">실시간으로 감지하고 기록합니다</p>
<button class="hero__cta" type="button">무료로 시작</button>
<p class="hero__readout">SIG <span class="hero__cursor" aria-hidden="true"></span></p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 브래킷 락온 + 스캔 한 주기
$easing: $ease-spring; // 브래킷이 조여드는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-ink);
.hero {
position: relative;
box-sizing: border-box;
width: clamp(240px, 82vw, 380px);
padding: $sp-5 $sp-4;
border-radius: $r-sm;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
overflow: hidden;
// 320px 폰 체크: eyebrow·2줄 제목·설명·CTA·터미널 readout 5단이 129·130 형제보다 한 단 더 많아
// 200px 높이를 넘는다 (131-01 실측: scrollHeight 238px vs innerHeight 200px). 단마다 여백을
// 한 단계씩 좁혀 맞춘다.
}
.hero__scan {
position: absolute;
inset: 0;
pointer-events: none;
background:
repeating-linear-gradient(to bottom, rgba($color, 0) 0, rgba($color, 0) 5px, rgba($color, .1) 6px, rgba($color, 0) 7px),
linear-gradient(to bottom, rgba($color, 0) 0%, rgba($color, .32) 48%, rgba($color, 0) 100%);
background-size: 100% 100%, 100% 240%;
background-position: 0 0, 0 -140%;
animation: scanSweep $duration linear infinite;
}
@keyframes scanSweep {
0% { background-position: 0 0, 0 -140%; }
100% { background-position: 0 0, 0 140%; }
}
.hero__corner {
position: absolute;
width: 18px;
height: 18px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.hero__corner--tl { top: 0; left: 0; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.hero__corner--tr { top: 0; right: 0; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.hero__corner--bl { bottom: 0; left: 0; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.hero__corner--br { bottom: 0; right: 0; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.hero__eyebrow {
position: relative;
margin: 0 0 $sp-2;
display: flex;
align-items: center;
gap: $sp-2;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: .12em;
text-transform: uppercase;
}
.hero__dot {
width: 6px;
height: 6px;
flex: none;
border-radius: 50%;
background: $color;
animation: blink $dur-loop steps(1) infinite;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: .2; }
}
.hero__title {
position: relative;
margin: 0 0 $sp-1;
color: $subject-cream;
font-weight: 700;
font-size: clamp(18px, 5vw, 25px);
line-height: 1.15;
font-family: inherit;
}
.hero__desc {
position: relative;
margin: 0 0 $sp-3;
color: rgba($subject-cream, .85); // 스캔라인이 지나는 밝은 프레임(#544623)에서 .6 은 4.29:1 — .85 로 6.77:1
font-weight: 500;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
font-family: inherit;
}
.hero__cta {
position: relative;
border: 1px solid $color;
border-radius: $r-control;
background: rgba($color, .12);
color: $color;
padding: $sp-1 $sp-5;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: .02em;
}
.hero__readout {
position: relative;
margin: $sp-2 0 0;
color: rgba($color, .7);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .04em;
}
.hero__cursor {
display: inline-block;
width: 6px;
height: 10px;
background: $color;
vertical-align: -1px;
animation: blink $dur-loop steps(1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.hero__scan, .hero__corner, .hero__dot, .hero__cursor {
animation: none !important;
}
.hero__corner { transform: scale(1); opacity: 1; }
.hero__dot, .hero__cursor { opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.hero {
position: relative;
box-sizing: border-box;
width: clamp(240px, 82vw, 380px);
padding: 20px 16px;
border-radius: 8px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
overflow: hidden;
}
.hero__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(to bottom, rgba(255, 210, 63, 0) 0, rgba(255, 210, 63, 0) 5px, rgba(255, 210, 63, 0.1) 6px, rgba(255, 210, 63, 0) 7px), linear-gradient(to bottom, rgba(255, 210, 63, 0) 0%, rgba(255, 210, 63, 0.32) 48%, rgba(255, 210, 63, 0) 100%);
background-size: 100% 100%, 100% 240%;
background-position: 0 0, 0 -140%;
animation: scanSweep 2s linear infinite;
}
@keyframes scanSweep {
0% {
background-position: 0 0, 0 -140%;
}
100% {
background-position: 0 0, 0 140%;
}
}
.hero__corner {
position: absolute;
width: 18px;
height: 18px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.hero__corner--tl {
top: 0;
left: 0;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.hero__corner--tr {
top: 0;
right: 0;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.hero__corner--bl {
bottom: 0;
left: 0;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.hero__corner--br {
bottom: 0;
right: 0;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.hero__eyebrow {
position: relative;
margin: 0 0 8px;
display: flex;
align-items: center;
gap: 8px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.hero__dot {
width: 6px;
height: 6px;
flex: none;
border-radius: 50%;
background: #ffd23f;
animation: blink 2s steps(1) infinite;
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0.2;
}
}
.hero__title {
position: relative;
margin: 0 0 4px;
color: #fff7e6;
font-weight: 700;
font-size: clamp(18px, 5vw, 25px);
line-height: 1.15;
font-family: inherit;
}
.hero__desc {
position: relative;
margin: 0 0 12px;
color: rgba(255, 247, 230, 0.85);
font-weight: 500;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
font-family: inherit;
}
.hero__cta {
position: relative;
border: 1px solid #ffd23f;
border-radius: 12px;
background: rgba(255, 210, 63, 0.12);
color: #ffd23f;
padding: 4px 20px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.02em;
}
.hero__readout {
position: relative;
margin: 8px 0 0;
color: rgba(255, 210, 63, 0.7);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.04em;
}
.hero__cursor {
display: inline-block;
width: 6px;
height: 10px;
background: #ffd23f;
vertical-align: -1px;
animation: blink 2s steps(1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.hero__scan, .hero__corner, .hero__dot, .hero__cursor {
animation: none !important;
}
.hero__corner {
transform: scale(1);
opacity: 1;
}
.hero__dot, .hero__cursor {
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./HeroHud.module.scss";
export type HeroHudProps = {
brand?: string;
title?: string;
desc?: string;
cta?: string;
readout?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function HeroHud({
brand = "SENTRY / ONLINE",
title = "놓치는 순간 없이 지켜보는 대시보드",
desc = "실시간으로 감지하고 기록합니다",
cta = "무료로 시작",
readout = "SIG",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: HeroHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.hero} style={vars}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<div className={styles.scan} aria-hidden="true" />
<p className={styles.eyebrow}>
<span className={styles.dot} aria-hidden="true" />
{brand}
</p>
<h1 className={styles.title}>{title}</h1>
<p className={styles.desc}>{desc}</p>
<button type="button" className={styles.cta}>{cta}</button>
<p className={styles.readoutLine}>
{readout} <span className={styles.cursor} aria-hidden="true" />
</p>
</section>
);
}01 · 락온 히어로 — 측정 누적면적 48.094% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
SaaS 랜딩 첫 화면이나 제품 출시 커버처럼, 처음 보는 순간 "이 화면이 나를 인식하고 있다"는 인상을 줘야 하는 자리에 씁니다.
<div class="relative w-full max-w-md overflow-hidden rounded-control bg-ink px-6 py-8 shadow-glow">
<span class="corner corner-tl" aria-hidden="true"></span>
<span class="corner corner-tr" aria-hidden="true"></span>
<span class="corner corner-bl" aria-hidden="true"></span>
<span class="corner corner-br" aria-hidden="true"></span>
<div class="scan-sweep absolute inset-0 pointer-events-none" aria-hidden="true"></div>
<p class="mb-3 flex items-center gap-2 text-[11px] font-bold uppercase tracking-widest text-accent">
<span class="blink-dot inline-block h-1.5 w-1.5 rounded-full bg-accent"></span>SENTRY / ONLINE
</p>
<h1 class="mb-2 text-2xl font-bold leading-tight">놓치는 순간 없이 지켜보는 대시보드</h1>
<p class="mb-4 text-sm text-cream/60">실시간으로 감지하고 기록합니다</p>
<button class="rounded-control border border-accent bg-accent/10 px-5 py-2 text-sm font-bold text-accent">무료로 시작</button>
</div>
02상태 표시줄 내비
무엇이 움직이나
내비게이션 전체가 HUD 상태 표시줄처럼 브래킷에 감싸이고, 활성 탭 아래 밑줄이 스캔하듯 탭 사이를 미끄러져 이동합니다.

<main class="stage">
<nav class="navhud is-demo" aria-label="SENTRY">
<span class="navhud__brand">SENTRY</span>
<ul class="navhud__track">
<li class="navhud__item">홈</li>
<li class="navhud__item">기능</li>
<li class="navhud__item">요금</li>
<li class="navhud__item">문의</li>
<span class="navhud__indicator" aria-hidden="true"></span>
</ul>
<span class="navhud__status"><span class="navhud__dot" aria-hidden="true"></span>LIVE</span>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 밑줄 인디케이터가 탭 4곳을 도는 한 주기
$easing: $ease-spring; // 인디케이터가 미끄러지는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-yellow);
.navhud {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
gap: $sp-3;
width: clamp(260px, 84vw, 420px);
padding: $sp-2 $sp-3;
border-radius: $r-control;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
}
.navhud__brand {
flex: none;
color: $subject-cream;
font-weight: 800;
font-size: clamp(12px, 3vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: .04em;
white-space: nowrap;
}
.navhud__track {
position: relative;
flex: 1 1 auto;
min-width: 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
margin: 0;
padding: 0;
list-style: none;
}
.navhud__item {
padding: $sp-1 0;
color: rgba($subject-cream, .55);
font-weight: 600;
font-size: clamp(10px, 2.6vw, 11px);
line-height: 1;
font-family: inherit;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.navhud__indicator {
position: absolute;
left: 0;
bottom: 0;
width: 25%;
height: 2px;
background: $color;
animation: navScan $duration $easing infinite;
}
@keyframes navScan {
0%, 20% { transform: translateX(0); }
25%, 45% { transform: translateX(100%); }
50%, 70% { transform: translateX(200%); }
75%, 95% { transform: translateX(300%); }
100% { transform: translateX(0); }
}
.navhud__status {
flex: none;
display: flex;
align-items: center;
gap: $sp-1;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .08em;
white-space: nowrap;
}
.navhud__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: $color;
animation: blink $dur-loop steps(1) infinite;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: .2; }
}
@media (prefers-reduced-motion: reduce) {
.navhud__indicator, .navhud__dot {
animation: none !important;
}
.navhud__indicator { transform: translateX(0); }
.navhud__dot { opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.navhud {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 12px;
width: clamp(260px, 84vw, 420px);
padding: 8px 12px;
border-radius: 12px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
}
.navhud__brand {
flex: none;
color: #fff7e6;
font-weight: 800;
font-size: clamp(12px, 3vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.04em;
white-space: nowrap;
}
.navhud__track {
position: relative;
flex: 1 1 auto;
min-width: 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
margin: 0;
padding: 0;
list-style: none;
}
.navhud__item {
padding: 4px 0;
color: rgba(255, 247, 230, 0.55);
font-weight: 600;
font-size: clamp(10px, 2.6vw, 11px);
line-height: 1;
font-family: inherit;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.navhud__indicator {
position: absolute;
left: 0;
bottom: 0;
width: 25%;
height: 2px;
background: #ffd23f;
animation: navScan 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes navScan {
0%, 20% {
transform: translateX(0);
}
25%, 45% {
transform: translateX(100%);
}
50%, 70% {
transform: translateX(200%);
}
75%, 95% {
transform: translateX(300%);
}
100% {
transform: translateX(0);
}
}
.navhud__status {
flex: none;
display: flex;
align-items: center;
gap: 4px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.08em;
white-space: nowrap;
}
.navhud__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #ffd23f;
animation: blink 2s steps(1) infinite;
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion: reduce) {
.navhud__indicator, .navhud__dot {
animation: none !important;
}
.navhud__indicator {
transform: translateX(0);
}
.navhud__dot {
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./NavHud.module.scss";
export type NavHudProps = {
brand?: string;
links?: string[];
status?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function NavHud({
brand = "SENTRY",
links = ["홈", "기능", "요금", "문의"],
status = "LIVE",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: NavHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<nav className={styles.nav} style={vars} aria-label={brand}>
<span className={styles.brand}>{brand}</span>
<ul className={styles.track}>
{links.map((label) => (
<li key={label} className={styles.item}>{label}</li>
))}
<span className={styles.indicator} aria-hidden="true" />
</ul>
<span className={styles.status}>
<span className={styles.dot} aria-hidden="true" />
{status}
</span>
</nav>
);
}02 · 상태 표시줄 내비 — 측정 누적면적 0.389% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
랜딩 페이지 상단바나 대시보드 내비처럼 브랜드·메뉴·상태가 한 줄에 같이 들어가는 자리에 씁니다.
<nav class="relative flex items-center gap-4 rounded-control bg-ink/95 px-3 py-2 shadow-glow" aria-label="SENTRY">
<span class="shrink-0 whitespace-nowrap pl-2 font-extrabold text-sm">SENTRY</span>
<div class="relative flex min-w-0 flex-1 items-stretch">
<span class="relative z-10 flex-1 whitespace-nowrap py-2 text-center text-xs font-semibold text-cream/70">홈</span>
<span class="relative z-10 flex-1 whitespace-nowrap py-2 text-center text-xs font-semibold text-cream/70">기능</span>
<span class="relative z-10 flex-1 whitespace-nowrap py-2 text-center text-xs font-semibold text-cream/70">요금</span>
<span class="relative z-10 flex-1 whitespace-nowrap py-2 text-center text-xs font-semibold text-cream/70">문의</span>
<span class="nav-indicator absolute bottom-0 left-0 h-[2px] w-1/4 bg-accent"></span>
</div>
<span class="shrink-0 whitespace-nowrap flex items-center gap-1 pr-2 text-[10px] font-bold tracking-widest text-accent">
<span class="blink-dot inline-block h-1.5 w-1.5 rounded-full bg-accent"></span>LIVE
</span>
</nav>
03타겟 인식 카드
무엇이 움직이나
격자의 카드마다 카메라 뷰파인더 같은 브래킷 프레임이 둘리고, 스캔라인이 카드 순서대로 하나씩 훑으며 우상단 좌표 리드아웃이 함께 표시됩니다.

<main class="stage">
<div class="cardgrid is-demo">
<article class="cardgrid__card">
<span class="cardgrid__corner cardgrid__corner--tl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--tr" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--bl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--br" aria-hidden="true"></span>
<div class="cardgrid__scan" aria-hidden="true"></div>
<span class="cardgrid__coord">04.12</span>
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">감지</p>
</article>
<article class="cardgrid__card">
<span class="cardgrid__corner cardgrid__corner--tl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--tr" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--bl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--br" aria-hidden="true"></span>
<div class="cardgrid__scan" aria-hidden="true"></div>
<span class="cardgrid__coord cardgrid__coord--alt">#A1</span>
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">추적</p>
</article>
<article class="cardgrid__card">
<span class="cardgrid__corner cardgrid__corner--tl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--tr" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--bl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--br" aria-hidden="true"></span>
<div class="cardgrid__scan" aria-hidden="true"></div>
<span class="cardgrid__coord">07.03</span>
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">기록</p>
</article>
<article class="cardgrid__card">
<span class="cardgrid__corner cardgrid__corner--tl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--tr" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--bl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--br" aria-hidden="true"></span>
<div class="cardgrid__scan" aria-hidden="true"></div>
<span class="cardgrid__coord cardgrid__coord--alt">#C4</span>
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">분석</p>
</article>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 스캔이 카드 4장을 순서대로 도는 한 주기
$easing: $ease-spring; // 브래킷이 조여드는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-orange);
.cardgrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $sp-2;
width: clamp(240px, 84vw, 340px);
}
// 320px 폰 체크: 2x2 고정 그리드라 폭이 줄어도 항상 2행 — coord 줄(HUD 전용 읽음값)까지 4줄이라
// 카드 padding·행간이 129·130 형제와 같은 값이면 2행 합계가 200px 높이를 넘는다
// (131-03 실측: scrollHeight 239px vs innerHeight 200px). padding·margin 을 한 단계씩 좁힌다.
.cardgrid__card {
position: relative;
box-sizing: border-box;
padding: $sp-2;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
overflow: hidden;
}
.cardgrid__card:nth-child(1) .cardgrid__scan { animation-delay: 0s; }
.cardgrid__card:nth-child(2) .cardgrid__scan { animation-delay: -.5s; }
.cardgrid__card:nth-child(3) .cardgrid__scan { animation-delay: -1s; }
.cardgrid__card:nth-child(4) .cardgrid__scan { animation-delay: -1.5s; }
.cardgrid__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(to bottom, rgba($color, 0) 0%, rgba($color, .3) 48%, rgba($color, 0) 100%);
background-size: 100% 240%;
background-position: 0 -140%;
animation: cardScan $duration linear infinite;
}
@keyframes cardScan {
0% { background-position: 0 -140%; }
100% { background-position: 0 140%; }
}
.cardgrid__corner {
position: absolute;
width: 12px;
height: 12px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.cardgrid__corner--tl { top: $sp-2; left: $sp-2; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.cardgrid__corner--tr { top: $sp-2; right: $sp-2; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.cardgrid__corner--bl { bottom: $sp-2; left: $sp-2; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.cardgrid__corner--br { bottom: $sp-2; right: $sp-2; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.cardgrid__coord {
position: relative;
display: block;
margin: 0 0 $sp-1;
color: rgba($color, .85); // 스캔라인이 지나는 밝은 프레임(#4e4123)에서 .7 은 4.30:1 — .85 로 5.49:1
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .04em;
text-align: right;
}
.cardgrid__coord--alt {
color: $subject-sky; // 어두운 카드 면 위 쿨 강조 글자. 이 카드는 호박색 스캔라인이 늘 지나 면이 #352c1f 까지 밝아진다 — $subject-blue 2.59:1, $subject-blue-light 도 그 프레임에서 4.28:1 로 미달이라 한 단계 더 밝은 $subject-sky(7.42:1)
}
.cardgrid__icon {
position: relative;
display: block;
width: 20px;
height: 20px;
margin: 0 0 $sp-2;
border: 1px solid $color;
border-radius: $r-xs;
background: rgba($color, .18);
}
.cardgrid__label {
position: relative;
margin: 0;
color: $subject-cream;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1.3;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.cardgrid__scan, .cardgrid__corner {
animation: none !important;
}
.cardgrid__corner { transform: scale(1); opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cardgrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
width: clamp(240px, 84vw, 340px);
}
.cardgrid__card {
position: relative;
box-sizing: border-box;
padding: 8px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
overflow: hidden;
}
.cardgrid__card:nth-child(1) .cardgrid__scan {
animation-delay: 0s;
}
.cardgrid__card:nth-child(2) .cardgrid__scan {
animation-delay: -0.5s;
}
.cardgrid__card:nth-child(3) .cardgrid__scan {
animation-delay: -1s;
}
.cardgrid__card:nth-child(4) .cardgrid__scan {
animation-delay: -1.5s;
}
.cardgrid__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(to bottom, rgba(255, 210, 63, 0) 0%, rgba(255, 210, 63, 0.3) 48%, rgba(255, 210, 63, 0) 100%);
background-size: 100% 240%;
background-position: 0 -140%;
animation: cardScan 2s linear infinite;
}
@keyframes cardScan {
0% {
background-position: 0 -140%;
}
100% {
background-position: 0 140%;
}
}
.cardgrid__corner {
position: absolute;
width: 12px;
height: 12px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.cardgrid__corner--tl {
top: 8px;
left: 8px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.cardgrid__corner--tr {
top: 8px;
right: 8px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.cardgrid__corner--bl {
bottom: 8px;
left: 8px;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.cardgrid__corner--br {
bottom: 8px;
right: 8px;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.cardgrid__coord {
position: relative;
display: block;
margin: 0 0 4px;
color: rgba(255, 210, 63, 0.85);
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.04em;
text-align: right;
}
.cardgrid__coord--alt {
color: #6ec8ff;
}
.cardgrid__icon {
position: relative;
display: block;
width: 20px;
height: 20px;
margin: 0 0 8px;
border: 1px solid #ffd23f;
border-radius: 4px;
background: rgba(255, 210, 63, 0.18);
}
.cardgrid__label {
position: relative;
margin: 0;
color: #fff7e6;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1.3;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.cardgrid__scan, .cardgrid__corner {
animation: none !important;
}
.cardgrid__corner {
transform: scale(1);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./CardGridHud.module.scss";
export type CardGridHudCard = { coord: string; label: string; alt?: boolean };
export type CardGridHudProps = {
cards?: CardGridHudCard[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const defaultCards: CardGridHudCard[] = [
{ coord: "04.12", label: "감지" },
{ coord: "#A1", label: "추적", alt: true },
{ coord: "07.03", label: "기록" },
{ coord: "#C4", label: "분석", alt: true },
];
export default function CardGridHud({
cards = defaultCards,
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: CardGridHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.grid} style={vars}>
{cards.map((card) => (
<article key={card.coord} className={styles.card}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<div className={styles.scan} aria-hidden="true" />
<span className={`${styles.coord} ${card.alt ? styles.coordAlt : ""}`}>{card.coord}</span>
<span className={styles.icon} aria-hidden="true" />
<p className={styles.label}>{card.label}</p>
</article>
))}
</div>
);
}03 · 타겟 인식 카드 — 측정 누적면적 34.317% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
기능 소개 격자나 포트폴리오 카드처럼 같은 형태가 여러 칸 반복되는 자리에 씁니다.
<div class="relative overflow-hidden rounded-card bg-ink p-4 shadow-glow">
<span class="corner corner-sm corner-tl" aria-hidden="true"></span>
<span class="corner corner-sm corner-tr" aria-hidden="true"></span>
<span class="corner corner-sm corner-bl" aria-hidden="true"></span>
<span class="corner corner-sm corner-br" aria-hidden="true"></span>
<div class="scan-sweep absolute inset-0 pointer-events-none" style="animation-delay:-.5s"></div>
<span class="relative mb-2 block text-right text-[10px] font-bold text-data/85">#A1</span>
<span class="relative mb-3 block h-5 w-5 rounded border border-accent bg-accent/20"></span>
<p class="relative text-sm font-bold">추적</p>
</div>
04시스템 로그 리스트
무엇이 움직이나
목록 각 줄이 타임스탬프 접두어를 달고 있고, 밝은 스캔 바가 위에서 아래로 줄을 하나씩 훑으며 OK 표시를 남깁니다.

<main class="stage">
<div class="loglist is-demo">
<ul class="loglist__rows">
<li class="loglist__row"><span class="loglist__ts">00:01</span><span class="loglist__text">사용자 인증 확인</span><span class="loglist__ok">OK</span></li>
<li class="loglist__row"><span class="loglist__ts">00:02</span><span class="loglist__text">위치 데이터 동기화</span><span class="loglist__ok">OK</span></li>
<li class="loglist__row"><span class="loglist__ts">00:03</span><span class="loglist__text">이상 패턴 스캔</span><span class="loglist__ok">OK</span></li>
<li class="loglist__row"><span class="loglist__ts">00:04</span><span class="loglist__text">백업 서버 연결</span><span class="loglist__ok">OK</span></li>
</ul>
<p class="loglist__cursorline">READY <span class="loglist__cursor" aria-hidden="true"></span></p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 스캔 바가 줄 4개를 순서대로 내려가는 한 주기
$easing: $ease-spring; // 스캔 바가 밝아지는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-paper);
.loglist {
box-sizing: border-box;
width: clamp(240px, 84vw, 360px);
padding: $sp-4;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
// 320px 폰 체크: 로그 4줄 + 커서줄이 129·130 형제 값 그대로면 200px 높이를 넘는다
// (131-04 실측: scrollHeight 223px vs innerHeight 200px). padding·행간을 한 단계씩 좁힌다.
}
.loglist__rows {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: $sp-1;
}
.loglist__row {
position: relative;
display: flex;
align-items: center;
gap: $sp-3;
padding: $sp-1 $sp-3;
border-radius: $r-xs;
animation: rowScan $duration $easing infinite;
}
.loglist__row:nth-child(1) { animation-delay: 0s; }
.loglist__row:nth-child(2) { animation-delay: -.5s; }
.loglist__row:nth-child(3) { animation-delay: -1s; }
.loglist__row:nth-child(4) { animation-delay: -1.5s; }
@keyframes rowScan {
0%, 100% { box-shadow: inset 0 0 0 1px rgba($color, 0), inset 0 0 0 0 rgba($color, 0); }
5% { box-shadow: inset 0 0 0 1px rgba($color, .5), inset 0 0 16px 0 rgba($color, .18); }
15% { box-shadow: inset 0 0 0 1px rgba($color, 0), inset 0 0 0 0 rgba($color, 0); }
}
.loglist__ts {
flex: none;
color: rgba($color, .7);
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
}
.loglist__text {
flex: 1 1 auto;
min-width: 0;
color: $subject-cream;
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1.3;
font-family: inherit;
}
.loglist__ok {
flex: none;
opacity: 0;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .06em;
animation: rowOk $duration $easing infinite;
}
.loglist__row:nth-child(1) .loglist__ok { animation-delay: 0s; }
.loglist__row:nth-child(2) .loglist__ok { animation-delay: -.5s; }
.loglist__row:nth-child(3) .loglist__ok { animation-delay: -1s; }
.loglist__row:nth-child(4) .loglist__ok { animation-delay: -1.5s; }
@keyframes rowOk {
0%, 8% { opacity: 0; }
14%, 100% { opacity: 1; }
}
.loglist__cursorline {
margin: $sp-2 0 0;
color: rgba($color, .7);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .04em;
}
.loglist__cursor {
display: inline-block;
width: 6px;
height: 10px;
background: $color;
vertical-align: -1px;
animation: blink $dur-loop steps(1) infinite;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: .2; }
}
@media (prefers-reduced-motion: reduce) {
.loglist__row, .loglist__ok, .loglist__cursor {
animation: none !important;
}
.loglist__row { box-shadow: inset 0 0 0 1px rgba($color, 0); }
.loglist__ok { opacity: 1; }
.loglist__cursor { opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.loglist {
box-sizing: border-box;
width: clamp(240px, 84vw, 360px);
padding: 16px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
}
.loglist__rows {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.loglist__row {
position: relative;
display: flex;
align-items: center;
gap: 12px;
padding: 4px 12px;
border-radius: 4px;
animation: rowScan 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.loglist__row:nth-child(1) {
animation-delay: 0s;
}
.loglist__row:nth-child(2) {
animation-delay: -0.5s;
}
.loglist__row:nth-child(3) {
animation-delay: -1s;
}
.loglist__row:nth-child(4) {
animation-delay: -1.5s;
}
@keyframes rowScan {
0%, 100% {
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0), inset 0 0 0 0 rgba(255, 210, 63, 0);
}
5% {
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0.5), inset 0 0 16px 0 rgba(255, 210, 63, 0.18);
}
15% {
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0), inset 0 0 0 0 rgba(255, 210, 63, 0);
}
}
.loglist__ts {
flex: none;
color: rgba(255, 210, 63, 0.7);
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
}
.loglist__text {
flex: 1 1 auto;
min-width: 0;
color: #fff7e6;
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1.3;
font-family: inherit;
}
.loglist__ok {
flex: none;
opacity: 0;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.06em;
animation: rowOk 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.loglist__row:nth-child(1) .loglist__ok {
animation-delay: 0s;
}
.loglist__row:nth-child(2) .loglist__ok {
animation-delay: -0.5s;
}
.loglist__row:nth-child(3) .loglist__ok {
animation-delay: -1s;
}
.loglist__row:nth-child(4) .loglist__ok {
animation-delay: -1.5s;
}
@keyframes rowOk {
0%, 8% {
opacity: 0;
}
14%, 100% {
opacity: 1;
}
}
.loglist__cursorline {
margin: 8px 0 0;
color: rgba(255, 210, 63, 0.7);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.04em;
}
.loglist__cursor {
display: inline-block;
width: 6px;
height: 10px;
background: #ffd23f;
vertical-align: -1px;
animation: blink 2s steps(1) infinite;
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion: reduce) {
.loglist__row, .loglist__ok, .loglist__cursor {
animation: none !important;
}
.loglist__row {
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0);
}
.loglist__ok {
opacity: 1;
}
.loglist__cursor {
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./FeatureListHud.module.scss";
export type FeatureListHudRow = { ts: string; text: string };
export type FeatureListHudProps = {
rows?: FeatureListHudRow[];
ok?: string;
ready?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const defaultRows: FeatureListHudRow[] = [
{ ts: "00:01", text: "사용자 인증 확인" },
{ ts: "00:02", text: "위치 데이터 동기화" },
{ ts: "00:03", text: "이상 패턴 스캔" },
{ ts: "00:04", text: "백업 서버 연결" },
];
export default function FeatureListHud({
rows = defaultRows,
ok = "OK",
ready = "READY",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: FeatureListHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.list} style={vars}>
<ul className={styles.rows}>
{rows.map((row) => (
<li key={row.ts} className={styles.row}>
<span className={styles.ts}>{row.ts}</span>
<span className={styles.text}>{row.text}</span>
<span className={styles.ok}>{ok}</span>
</li>
))}
</ul>
<p className={styles.cursorline}>
{ready} <span className={styles.cursor} aria-hidden="true" />
</p>
</div>
);
}04 · 시스템 로그 리스트 — 측정 누적면적 9.292% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
기능 목록이나 온보딩 체크리스트처럼 "확인이 순서대로 끝나고 있다"는 진행감을 줘야 하는 자리에 씁니다.
<ul class="flex flex-col gap-1">
<li class="row-scan flex items-center gap-3 rounded px-3 py-2">
<span class="flex-none text-[10px] font-bold text-accent/70">00:01</span>
<span class="min-w-0 flex-1 text-sm font-semibold">사용자 인증 확인</span>
<span class="flex-none text-[10px] font-bold text-accent">OK</span>
</li>
<li class="row-scan flex items-center gap-3 rounded px-3 py-2" style="animation-delay:-.5s">
<span class="flex-none text-[10px] font-bold text-accent/70">00:02</span>
<span class="min-w-0 flex-1 text-sm font-semibold">위치 데이터 동기화</span>
<span class="flex-none text-[10px] font-bold text-accent">OK</span>
</li>
</ul>
<p class="mt-3 text-[10px] text-accent/70">READY <span class="blink-dot inline-block h-2.5 w-1.5 bg-accent"></span></p>
05락 타겟 플랜
무엇이 움직이나
추천 플랜 카드만 브래킷이 숨 쉬듯 맥동하고 LOCKED 배지가 붙어, 다른 카드는 불투명도를 낮춰 초점이 나간 듯 물러납니다.

<main class="stage">
<div class="pricing is-demo">
<article class="pricing__card pricing__card--dim">
<p class="pricing__name">라이트</p>
<p class="pricing__price">무료</p>
<ul class="pricing__feats">
<li>기본 스캔</li>
<li>채널 1개</li>
</ul>
</article>
<article class="pricing__card pricing__card--locked">
<span class="pricing__badge">LOCKED</span>
<span class="pricing__corner pricing__corner--tl" aria-hidden="true"></span>
<span class="pricing__corner pricing__corner--tr" aria-hidden="true"></span>
<span class="pricing__corner pricing__corner--bl" aria-hidden="true"></span>
<span class="pricing__corner pricing__corner--br" aria-hidden="true"></span>
<p class="pricing__name">프로</p>
<p class="pricing__price">월 9,900원</p>
<ul class="pricing__feats">
<li>실시간 추적</li>
<li>채널 무제한</li>
</ul>
</article>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 브래킷 맥동 + 글로우 호흡 한 주기
$easing: $ease-spring; // 잠금이 숨 쉬는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-ink);
.pricing {
display: flex;
align-items: flex-start;
gap: $sp-4;
width: clamp(260px, 84vw, 420px);
}
.pricing__card {
position: relative;
box-sizing: border-box;
min-width: 0;
// flex-basis 고정 160px + flex-wrap:wrap 조합은 320px 폰 체크에서 84vw(268.8px) 폭에
// 카드 2장(160+160+gap)이 안 들어가 줄바꿈되어 세로로 쌓인다 — 높이가 200px 을 훌쩍 넘겼다
// (131-05 실측: docEl.scrollHeight 311px vs innerHeight 200px). 129·130 형제 항목처럼
// wrap 없이 flex:1 로 균등 분배해 항상 한 줄에 나란히 놓는다.
flex: 1 1 0;
padding: $sp-5 $sp-4;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
}
.pricing__card--dim {
opacity: .55;
}
.pricing__card--locked::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
pointer-events: none;
box-shadow: 0 0 0 1px rgba($color, .6), 0 0 32px rgba($color, .4);
opacity: .4;
animation: glowBreathe $duration $easing infinite;
}
@keyframes glowBreathe {
0%, 100% { opacity: .4; }
50% { opacity: 1; }
}
.pricing__badge {
position: relative;
display: inline-block;
margin: 0 0 $sp-2;
padding: $sp-1 $sp-2;
border-radius: $r-xs;
background: rgba($color, .16);
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .08em;
}
.pricing__corner {
position: absolute;
width: 16px;
height: 16px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.pricing__corner--tl { top: $sp-2; left: $sp-2; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.pricing__corner--tr { top: $sp-2; right: $sp-2; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.pricing__corner--bl { bottom: $sp-2; left: $sp-2; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.pricing__corner--br { bottom: $sp-2; right: $sp-2; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.pricing__name {
position: relative;
margin: 0 0 $sp-1;
color: $subject-cream;
font-weight: 700;
font-size: clamp(13px, 3vw, 15px);
line-height: 1.2;
font-family: inherit;
}
.pricing__price {
position: relative;
margin: 0 0 $sp-3;
color: rgba($subject-cream, .6);
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1.2;
font-family: inherit;
}
// dim 카드는 .pricing__card--dim 의 opacity .55 가 글자 알파에 곱해진다 — 기본 .6/.75 는 실효 .33/.41 이라 2.89~3.82:1.
// dim 카드에서만 알파를 빼 실효 .55 (5.85:1) 로 올린다. 밝은 카드의 위계(.6/.75)는 그대로 둔다
.pricing__card--dim .pricing__price,
.pricing__card--dim .pricing__feats { color: $subject-cream; }
.pricing__feats {
position: relative;
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: $sp-1;
color: rgba($subject-cream, .75);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.5;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.pricing__card--locked::before, .pricing__corner {
animation: none !important;
}
.pricing__card--locked::before { opacity: 1; }
.pricing__corner { transform: scale(1); opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pricing {
display: flex;
align-items: flex-start;
gap: 16px;
width: clamp(260px, 84vw, 420px);
}
.pricing__card {
position: relative;
box-sizing: border-box;
min-width: 0;
flex: 1 1 0;
padding: 20px 16px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
}
.pricing__card--dim {
opacity: 0.55;
}
.pricing__card--locked::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
pointer-events: none;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.6), 0 0 32px rgba(255, 210, 63, 0.4);
opacity: 0.4;
animation: glowBreathe 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes glowBreathe {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}
.pricing__badge {
position: relative;
display: inline-block;
margin: 0 0 8px;
padding: 4px 8px;
border-radius: 4px;
background: rgba(255, 210, 63, 0.16);
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.08em;
}
.pricing__corner {
position: absolute;
width: 16px;
height: 16px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.pricing__corner--tl {
top: 8px;
left: 8px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.pricing__corner--tr {
top: 8px;
right: 8px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.pricing__corner--bl {
bottom: 8px;
left: 8px;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.pricing__corner--br {
bottom: 8px;
right: 8px;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.pricing__name {
position: relative;
margin: 0 0 4px;
color: #fff7e6;
font-weight: 700;
font-size: clamp(13px, 3vw, 15px);
line-height: 1.2;
font-family: inherit;
}
.pricing__price {
position: relative;
margin: 0 0 12px;
color: rgba(255, 247, 230, 0.6);
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1.2;
font-family: inherit;
}
.pricing__card--dim .pricing__price,
.pricing__card--dim .pricing__feats {
color: #fff7e6;
}
.pricing__feats {
position: relative;
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
color: rgba(255, 247, 230, 0.75);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.5;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.pricing__card--locked::before, .pricing__corner {
animation: none !important;
}
.pricing__card--locked::before {
opacity: 1;
}
.pricing__corner {
transform: scale(1);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./PricingHud.module.scss";
export type PricingHudProps = {
dimName?: string;
dimPrice?: string;
dimFeats?: string[];
lockedName?: string;
lockedPrice?: string;
lockedFeats?: string[];
badge?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PricingHud({
dimName = "라이트",
dimPrice = "무료",
dimFeats = ["기본 스캔", "채널 1개"],
lockedName = "프로",
lockedPrice = "월 9,900원",
lockedFeats = ["실시간 추적", "채널 무제한"],
badge = "LOCKED",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: PricingHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.pricing} style={vars}>
<article className={`${styles.card} ${styles.dim}`}>
<p className={styles.name}>{dimName}</p>
<p className={styles.price}>{dimPrice}</p>
<ul className={styles.feats}>
{dimFeats.map((f) => <li key={f}>{f}</li>)}
</ul>
</article>
<article className={`${styles.card} ${styles.locked}`}>
<span className={styles.badge}>{badge}</span>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<p className={styles.name}>{lockedName}</p>
<p className={styles.price}>{lockedPrice}</p>
<ul className={styles.feats}>
{lockedFeats.map((f) => <li key={f}>{f}</li>)}
</ul>
</article>
</div>
);
}05 · 락 타겟 플랜 — 측정 누적면적 5.45% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
2단 요금제나 무료·유료 비교처럼 어느 쪽을 밀고 있는지 한눈에 보여줘야 할 때 씁니다.
<div class="glow-breathe relative flex-1 basis-40 rounded-card bg-ink p-5 shadow-glow">
<span class="corner corner-sm corner-tl" aria-hidden="true"></span>
<span class="corner corner-sm corner-tr" aria-hidden="true"></span>
<span class="corner corner-sm corner-bl" aria-hidden="true"></span>
<span class="corner corner-sm corner-br" aria-hidden="true"></span>
<span class="mb-2 inline-block rounded bg-accent/15 px-2 py-1 text-[10px] font-bold tracking-widest text-accent">LOCKED</span>
<p class="mb-1 text-base font-bold">프로</p>
<p class="mb-3 text-xs text-cream/60">월 9,900원</p>
<ul class="flex flex-col gap-1 text-xs text-cream/75">
<li>실시간 추적</li>
<li>채널 무제한</li>
</ul>
</div>
06얼굴 인식 후기
무엇이 움직이나
아바타 위로 얼굴 인식 상자가 한 번 훑고 지나가고, 인용 카드는 VERIFIED 태그와 함께 브래킷 프레임을 두릅니다.

<main class="stage">
<div class="testimonial is-demo">
<div class="testimonial__avatar" aria-hidden="true">
<span class="testimonial__scanbox"></span>
</div>
<figure class="testimonial__card">
<span class="testimonial__corner testimonial__corner--tl" aria-hidden="true"></span>
<span class="testimonial__corner testimonial__corner--tr" aria-hidden="true"></span>
<span class="testimonial__corner testimonial__corner--bl" aria-hidden="true"></span>
<span class="testimonial__corner testimonial__corner--br" aria-hidden="true"></span>
<span class="testimonial__verified">VERIFIED</span>
<blockquote class="testimonial__quote">놓치는 게 없어서 마음이 편해요</blockquote>
<figcaption class="testimonial__author">이도윤 · 보안팀 매니저</figcaption>
</figure>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 얼굴 인식 상자가 한 번 훑고 멈추는 한 주기
$easing: $ease-spring; // 스캔 상자가 내려가는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-yellow);
.testimonial {
display: flex;
align-items: center;
gap: $sp-4;
width: clamp(260px, 84vw, 420px);
}
.testimonial__avatar {
position: relative;
flex: none;
width: 64px;
height: 64px;
border-radius: $r-control;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4);
overflow: hidden;
}
.testimonial__scanbox {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 3px;
background: rgba($color, .85);
box-shadow: 0 0 8px 1px rgba($color, .6);
animation: faceScan $duration $easing infinite;
}
@keyframes faceScan {
0% { transform: translateY(0); opacity: 1; }
20% { transform: translateY(61px); opacity: 1; }
28% { transform: translateY(61px); opacity: 0; }
100% { transform: translateY(61px); opacity: 0; }
}
.testimonial__card {
position: relative;
box-sizing: border-box;
min-width: 0;
// flex-wrap:wrap + flex-basis 220px 는 320px 폰 체크(84vw=268.8px)에서 아바타(64px)+gap+220px 가
// 안 들어가 줄바꿈 위험이 있다 (131-06 실측: scrollHeight 203px vs innerHeight 200px). 129 형제처럼
// wrap 없이 flex:1 로 균등 분배해 항상 한 줄에 나란히 놓는다.
flex: 1 1 0;
margin: 0;
padding: $sp-4;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
}
.testimonial__corner {
position: absolute;
width: 14px;
height: 14px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.testimonial__corner--tl { top: $sp-2; left: $sp-2; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.testimonial__corner--tr { top: $sp-2; right: $sp-2; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.testimonial__corner--bl { bottom: $sp-2; left: $sp-2; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.testimonial__corner--br { bottom: $sp-2; right: $sp-2; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.7); opacity: .5; }
50% { transform: scale(1); opacity: 1; }
}
.testimonial__verified {
position: relative;
display: inline-block;
margin: 0 0 $sp-2;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .08em;
}
.testimonial__quote {
position: relative;
margin: 0 0 $sp-2;
color: $subject-cream;
font-weight: 600;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1.4;
font-family: inherit;
}
.testimonial__author {
position: relative;
margin: 0;
color: rgba($subject-cream, .6);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.3;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.testimonial__scanbox, .testimonial__corner {
animation: none !important;
}
.testimonial__scanbox { opacity: 0; }
.testimonial__corner { transform: scale(1); opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.testimonial {
display: flex;
align-items: center;
gap: 16px;
width: clamp(260px, 84vw, 420px);
}
.testimonial__avatar {
position: relative;
flex: none;
width: 64px;
height: 64px;
border-radius: 12px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4);
overflow: hidden;
}
.testimonial__scanbox {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 3px;
background: rgba(255, 210, 63, 0.85);
box-shadow: 0 0 8px 1px rgba(255, 210, 63, 0.6);
animation: faceScan 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes faceScan {
0% {
transform: translateY(0);
opacity: 1;
}
20% {
transform: translateY(61px);
opacity: 1;
}
28% {
transform: translateY(61px);
opacity: 0;
}
100% {
transform: translateY(61px);
opacity: 0;
}
}
.testimonial__card {
position: relative;
box-sizing: border-box;
min-width: 0;
flex: 1 1 0;
margin: 0;
padding: 16px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
}
.testimonial__corner {
position: absolute;
width: 14px;
height: 14px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.testimonial__corner--tl {
top: 8px;
left: 8px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.testimonial__corner--tr {
top: 8px;
right: 8px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.testimonial__corner--bl {
bottom: 8px;
left: 8px;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.testimonial__corner--br {
bottom: 8px;
right: 8px;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.7);
opacity: 0.5;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.testimonial__verified {
position: relative;
display: inline-block;
margin: 0 0 8px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.08em;
}
.testimonial__quote {
position: relative;
margin: 0 0 8px;
color: #fff7e6;
font-weight: 600;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1.4;
font-family: inherit;
}
.testimonial__author {
position: relative;
margin: 0;
color: rgba(255, 247, 230, 0.6);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.3;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.testimonial__scanbox, .testimonial__corner {
animation: none !important;
}
.testimonial__scanbox {
opacity: 0;
}
.testimonial__corner {
transform: scale(1);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./TestimonialHud.module.scss";
export type TestimonialHudProps = {
verified?: string;
quote?: string;
author?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TestimonialHud({
verified = "VERIFIED",
quote = "놓치는 게 없어서 마음이 편해요",
author = "이도윤 · 보안팀 매니저",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: TestimonialHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.testimonial} style={vars}>
<div className={styles.avatar} aria-hidden="true">
<span className={styles.scanbox} />
</div>
<figure className={styles.card}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<span className={styles.verified}>{verified}</span>
<blockquote className={styles.quote}>{quote}</blockquote>
<figcaption className={styles.author}>{author}</figcaption>
</figure>
</div>
);
}06 · 얼굴 인식 후기 — 측정 누적면적 1.751% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
고객 후기나 파트너 인용처럼 "이 말은 확인된 사람이 했다"는 신뢰를 시각적으로 얹고 싶을 때 씁니다.
<div class="flex flex-wrap items-center gap-4">
<div class="relative h-16 w-16 flex-none overflow-hidden rounded-control bg-ink">
<span class="face-scan absolute left-0 top-0 h-[3px] w-full bg-accent/85"></span>
</div>
<div class="relative min-w-[220px] flex-1 rounded-card bg-ink p-4 shadow-glow">
<span class="corner corner-sm corner-tl" aria-hidden="true"></span>
<span class="corner corner-sm corner-tr" aria-hidden="true"></span>
<span class="mb-2 inline-block text-[10px] font-bold tracking-widest text-accent">VERIFIED</span>
<blockquote class="mb-2 text-sm font-semibold leading-relaxed">놓치는 게 없어서 마음이 편해요</blockquote>
<p class="text-xs text-cream/60">이도윤 · 보안팀 매니저</p>
</div>
</div>
07경보 밴드
무엇이 움직이나
화면 폭 전체 밴드 위로 대각선 스캔라인이 천천히 흐르고 브래킷 모서리가 맥동하며, 버튼은 2초에 한 번 짧게 흔들리는 글리치를 냅니다.

<main class="stage">
<div class="ctaband is-demo">
<span class="ctaband__corner ctaband__corner--tl" aria-hidden="true"></span>
<span class="ctaband__corner ctaband__corner--tr" aria-hidden="true"></span>
<span class="ctaband__corner ctaband__corner--bl" aria-hidden="true"></span>
<span class="ctaband__corner ctaband__corner--br" aria-hidden="true"></span>
<div class="ctaband__scan" aria-hidden="true"></div>
<p class="ctaband__eyebrow">ALERT</p>
<h2 class="ctaband__title">지금 바로 감시망을 켜보세요</h2>
<button class="ctaband__cta" type="button">무료 체험 시작</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 대각 스캔 드리프트 + 버튼 글리치 한 주기
$easing: $ease-out; // 스캔이 흐르는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-orange);
.ctaband {
position: relative;
box-sizing: border-box;
width: clamp(280px, 90vw, 520px);
padding: $sp-8 $sp-6;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
overflow: hidden;
}
.ctaband__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(45deg, rgba($color, 0) 0 6px, rgba($color, .12) 6px 7px);
background-size: 200% 200%;
background-position: 0% 0%;
animation: diagDrift $duration linear infinite;
}
@keyframes diagDrift {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
.ctaband__corner {
position: absolute;
width: 20px;
height: 20px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.ctaband__corner--tl { top: $sp-3; left: $sp-3; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.ctaband__corner--tr { top: $sp-3; right: $sp-3; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.ctaband__corner--bl { bottom: $sp-3; left: $sp-3; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.ctaband__corner--br { bottom: $sp-3; right: $sp-3; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.ctaband__eyebrow {
position: relative;
margin: 0 0 $sp-2;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: .12em;
}
.ctaband__title {
position: relative;
margin: 0 0 $sp-5;
max-width: 22ch;
color: $subject-cream;
font-weight: 700;
font-size: clamp(18px, 4.4vw, 24px);
line-height: 1.3;
font-family: inherit;
}
.ctaband__cta {
position: relative;
box-sizing: border-box;
border: 1px solid $color;
border-radius: $r-control;
background: rgba($color, .14);
color: $color;
padding: $sp-3 $sp-6;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: .02em;
animation: ctaGlitch $duration linear infinite;
}
@keyframes ctaGlitch {
0%, 59%, 64%, 100% { transform: translateX(0); opacity: 1; }
60% { transform: translateX(-1px); opacity: .85; }
61% { transform: translateX(1px); opacity: 1; }
62% { transform: translateX(-1px); opacity: .9; }
63% { transform: translateX(0); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.ctaband__scan, .ctaband__corner, .ctaband__cta {
animation: none !important;
}
.ctaband__corner { transform: scale(1); opacity: 1; }
.ctaband__cta { transform: translateX(0); opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ctaband {
position: relative;
box-sizing: border-box;
width: clamp(280px, 90vw, 520px);
padding: 32px 24px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
overflow: hidden;
}
.ctaband__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(45deg, rgba(255, 210, 63, 0) 0 6px, rgba(255, 210, 63, 0.12) 6px 7px);
background-size: 200% 200%;
background-position: 0% 0%;
animation: diagDrift 2s linear infinite;
}
@keyframes diagDrift {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
.ctaband__corner {
position: absolute;
width: 20px;
height: 20px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s ease-out infinite;
}
.ctaband__corner--tl {
top: 12px;
left: 12px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.ctaband__corner--tr {
top: 12px;
right: 12px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.ctaband__corner--bl {
bottom: 12px;
left: 12px;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.ctaband__corner--br {
bottom: 12px;
right: 12px;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.ctaband__eyebrow {
position: relative;
margin: 0 0 8px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: 0.12em;
}
.ctaband__title {
position: relative;
margin: 0 0 20px;
max-width: 22ch;
color: #fff7e6;
font-weight: 700;
font-size: clamp(18px, 4.4vw, 24px);
line-height: 1.3;
font-family: inherit;
}
.ctaband__cta {
position: relative;
box-sizing: border-box;
border: 1px solid #ffd23f;
border-radius: 12px;
background: rgba(255, 210, 63, 0.14);
color: #ffd23f;
padding: 12px 24px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.02em;
animation: ctaGlitch 2s linear infinite;
}
@keyframes ctaGlitch {
0%, 59%, 64%, 100% {
transform: translateX(0);
opacity: 1;
}
60% {
transform: translateX(-1px);
opacity: 0.85;
}
61% {
transform: translateX(1px);
opacity: 1;
}
62% {
transform: translateX(-1px);
opacity: 0.9;
}
63% {
transform: translateX(0);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.ctaband__scan, .ctaband__corner, .ctaband__cta {
animation: none !important;
}
.ctaband__corner {
transform: scale(1);
opacity: 1;
}
.ctaband__cta {
transform: translateX(0);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./CtaBandHud.module.scss";
export type CtaBandHudProps = {
eyebrow?: string;
title?: string;
cta?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function CtaBandHud({
eyebrow = "ALERT",
title = "지금 바로 감시망을 켜보세요",
cta = "무료 체험 시작",
duration = 2000,
easing = "ease-out",
color = "#ffd23f",
}: CtaBandHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.band} style={vars}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<div className={styles.scan} aria-hidden="true" />
<p className={styles.eyebrow}>{eyebrow}</p>
<h2 className={styles.title}>{title}</h2>
<button type="button" className={styles.cta}>{cta}</button>
</div>
);
}07 · 경보 밴드 — 측정 누적면적 48.985% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
가입 유도 띠나 무료 체험 시작처럼 페이지 중간에서 한 번 더 시선을 붙잡아야 하는 자리에 씁니다.
<div class="relative overflow-hidden rounded-card bg-ink px-8 py-12 shadow-glow">
<span class="corner corner-tl" aria-hidden="true"></span>
<span class="corner corner-tr" aria-hidden="true"></span>
<span class="corner corner-bl" aria-hidden="true"></span>
<span class="corner corner-br" aria-hidden="true"></span>
<div class="scan-diag absolute inset-0 pointer-events-none"></div>
<p class="mb-2 text-[11px] font-bold uppercase tracking-widest text-accent">ALERT</p>
<h2 class="mb-5 max-w-2xl text-2xl font-bold leading-snug sm:text-3xl">지금 바로 감시망을 켜보세요</h2>
<button class="glitch-btn rounded-control border border-accent bg-accent/10 px-6 py-3 text-sm font-bold text-accent">무료 체험 시작</button>
</div>
08터미널 푸터
무엇이 움직이나
먹색 배경 위 옅은 노이즈가 프레임마다 자리를 바꿔 깜빡이고, 링크 칸은 데이터 리드아웃처럼 정리되며 상단 모서리엔 브래킷이 남습니다.

<main class="stage">
<footer class="footerhud is-demo">
<span class="footerhud__corner footerhud__corner--tl" aria-hidden="true"></span>
<span class="footerhud__corner footerhud__corner--tr" aria-hidden="true"></span>
<div class="footerhud__noise" aria-hidden="true"></div>
<div class="footerhud__cols">
<div class="footerhud__col">
<p class="footerhud__head">SYSTEM</p>
<ul>
<li>대시보드</li>
<li>알림 설정</li>
</ul>
</div>
<div class="footerhud__col">
<p class="footerhud__head">SUPPORT</p>
<ul>
<li>문의하기</li>
<li>이용 가이드</li>
</ul>
</div>
<div class="footerhud__col">
<p class="footerhud__head">LEGAL</p>
<ul>
<li>이용약관</li>
<li>개인정보처리방침</li>
</ul>
</div>
</div>
<p class="footerhud__copy">SENTRY © 2026</p>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 노이즈 플리커 프레임 스텝의 한 주기
$easing: $ease-out; // 브래킷이 맥동하는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-paper);
.footerhud {
position: relative;
box-sizing: border-box;
width: clamp(260px, 88vw, 460px);
padding: $sp-6 $sp-5 $sp-4;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
overflow: hidden;
}
.footerhud__noise {
position: absolute;
inset: 0;
pointer-events: none;
opacity: .06;
background: repeating-linear-gradient(45deg, rgba($color, .9) 0 1px, rgba($color, 0) 1px 4px);
background-size: 12px 12px;
animation: noiseFlicker $duration steps(8) infinite;
}
@keyframes noiseFlicker {
0% { background-position: 0 0; }
100% { background-position: 120px 84px; }
}
.footerhud__corner {
position: absolute;
width: 16px;
height: 16px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.footerhud__corner--tl { top: $sp-2; left: $sp-2; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.footerhud__corner--tr { top: $sp-2; right: $sp-2; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.footerhud__cols {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-3;
margin: 0 0 $sp-4;
}
.footerhud__col {
min-width: 0;
}
.footerhud__head {
margin: 0 0 $sp-2;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .1em;
}
.footerhud__col ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: $sp-1;
}
.footerhud__col li {
color: rgba($subject-cream, .7);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.4;
font-family: inherit;
}
.footerhud__copy {
position: relative;
margin: 0;
padding-top: $sp-3;
border-top: 1px solid rgba($color, .18);
color: rgba($subject-cream, .72); // 잉크 면 위 9.18:1 — .4 는 3.66 으로 미달
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .04em;
}
@media (prefers-reduced-motion: reduce) {
.footerhud__noise, .footerhud__corner {
animation: none !important;
}
.footerhud__noise { opacity: .03; }
.footerhud__corner { transform: scale(1); opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.footerhud {
position: relative;
box-sizing: border-box;
width: clamp(260px, 88vw, 460px);
padding: 24px 20px 16px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
overflow: hidden;
}
.footerhud__noise {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.06;
background: repeating-linear-gradient(45deg, rgba(255, 210, 63, 0.9) 0 1px, rgba(255, 210, 63, 0) 1px 4px);
background-size: 12px 12px;
animation: noiseFlicker 2s steps(8) infinite;
}
@keyframes noiseFlicker {
0% {
background-position: 0 0;
}
100% {
background-position: 120px 84px;
}
}
.footerhud__corner {
position: absolute;
width: 16px;
height: 16px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s ease-out infinite;
}
.footerhud__corner--tl {
top: 8px;
left: 8px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.footerhud__corner--tr {
top: 8px;
right: 8px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.footerhud__cols {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin: 0 0 16px;
}
.footerhud__col {
min-width: 0;
}
.footerhud__head {
margin: 0 0 8px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.1em;
}
.footerhud__col ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.footerhud__col li {
color: rgba(255, 247, 230, 0.7);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.4;
font-family: inherit;
}
.footerhud__copy {
position: relative;
margin: 0;
padding-top: 12px;
border-top: 1px solid rgba(255, 210, 63, 0.18);
color: rgba(255, 247, 230, 0.72);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.04em;
}
@media (prefers-reduced-motion: reduce) {
.footerhud__noise, .footerhud__corner {
animation: none !important;
}
.footerhud__noise {
opacity: 0.03;
}
.footerhud__corner {
transform: scale(1);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./FooterHud.module.scss";
export type FooterHudColumn = { head: string; links: string[] };
export type FooterHudProps = {
columns?: FooterHudColumn[];
copy?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const defaultColumns: FooterHudColumn[] = [
{ head: "SYSTEM", links: ["대시보드", "알림 설정"] },
{ head: "SUPPORT", links: ["문의하기", "이용 가이드"] },
{ head: "LEGAL", links: ["이용약관", "개인정보처리방침"] },
];
export default function FooterHud({
columns = defaultColumns,
copy = "SENTRY © 2026",
duration = 2000,
easing = "ease-out",
color = "#ffd23f",
}: FooterHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<footer className={styles.footer} style={vars}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<div className={styles.noise} aria-hidden="true" />
<div className={styles.cols}>
{columns.map((col) => (
<div key={col.head} className={styles.col}>
<p className={styles.head}>{col.head}</p>
<ul>
{col.links.map((link) => <li key={link}>{link}</li>)}
</ul>
</div>
))}
</div>
<p className={styles.copy}>{copy}</p>
</footer>
);
}08 · 터미널 푸터 — 측정 누적면적 29.669% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
멀티 컬럼 푸터나 사이트맵 정리처럼 링크가 많은 하단 영역에 톤을 맞출 때 씁니다.
<footer class="relative overflow-hidden rounded-t-card bg-ink px-6 pt-8 pb-6 shadow-glow">
<span class="corner corner-tl" aria-hidden="true"></span>
<span class="corner corner-tr" aria-hidden="true"></span>
<div class="noise-flicker absolute inset-0 pointer-events-none"></div>
<div class="relative grid grid-cols-1 gap-6 sm:grid-cols-3">
<div class="min-w-0">
<p class="mb-2 text-[10px] font-bold uppercase tracking-widest text-accent">SYSTEM</p>
<ul class="flex flex-col gap-1 text-xs text-cream/70">
<li>대시보드</li>
<li>알림 설정</li>
</ul>
</div>
</div>
<p class="relative mt-6 border-t border-accent/20 pt-3 text-[10px] text-cream/40">SENTRY © 2026</p>
</footer>
09락온 버튼·스캔 인풋
무엇이 움직이나
버튼을 누르면 네 귀퉁이 브래킷이 안쪽으로 스냅되며 잠기고, 입력창은 포커스를 받으면 밑줄이 스캔하듯 번지는 호박색 글로우로 바뀝니다.

<main class="stage">
<form class="formhud is-demo">
<button class="formhud__btn" type="button">
<span class="formhud__corner formhud__corner--tl" aria-hidden="true"></span>
<span class="formhud__corner formhud__corner--tr" aria-hidden="true"></span>
<span class="formhud__corner formhud__corner--bl" aria-hidden="true"></span>
<span class="formhud__corner formhud__corner--br" aria-hidden="true"></span>
락온 시작
</button>
<label class="formhud__field">
<input class="formhud__input" type="email" placeholder="이메일 주소" readonly>
<span class="formhud__caret" aria-hidden="true"></span>
</label>
</form>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 버튼 스냅 + 입력창 포커스 글로우 한 주기
$easing: $ease-pop; // 스냅이 튕기는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-ink);
.formhud {
display: flex;
flex-direction: column;
gap: $sp-4;
width: clamp(220px, 78vw, 300px);
}
.formhud__btn {
position: relative;
box-sizing: border-box;
border: 1px solid $color;
border-radius: $r-control;
background: rgba($color, .14);
color: $color;
padding: $sp-3 $sp-5;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: .02em;
animation: btnSnap $duration $easing infinite;
}
@keyframes btnSnap {
0%, 55%, 100% { transform: scale(1); }
75% { transform: scale(.97); }
}
.formhud__corner {
position: absolute;
width: 10px;
height: 10px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
}
.formhud__corner--tl { top: $sp-1; left: $sp-1; border-top-width: 2px; border-left-width: 2px; animation: snapTl $duration $easing infinite; }
.formhud__corner--tr { top: $sp-1; right: $sp-1; border-top-width: 2px; border-right-width: 2px; animation: snapTr $duration $easing infinite; }
.formhud__corner--bl { bottom: $sp-1; left: $sp-1; border-bottom-width: 2px; border-left-width: 2px; animation: snapBl $duration $easing infinite; }
.formhud__corner--br { bottom: $sp-1; right: $sp-1; border-bottom-width: 2px; border-right-width: 2px; animation: snapBr $duration $easing infinite; }
@keyframes snapTl { 0%, 55%, 100% { transform: translate(0, 0); } 75% { transform: translate(3px, 3px); } }
@keyframes snapTr { 0%, 55%, 100% { transform: translate(0, 0); } 75% { transform: translate(-3px, 3px); } }
@keyframes snapBl { 0%, 55%, 100% { transform: translate(0, 0); } 75% { transform: translate(3px, -3px); } }
@keyframes snapBr { 0%, 55%, 100% { transform: translate(0, 0); } 75% { transform: translate(-3px, -3px); } }
.formhud__field {
position: relative;
display: block;
border-radius: $r-sm;
background: $subject-ink;
box-shadow: 0 1px 0 0 rgba($color, .4);
animation: focusGlow $duration $easing infinite;
}
@keyframes focusGlow {
0%, 55%, 100% { box-shadow: 0 1px 0 0 rgba($color, .4); }
75% { box-shadow: 0 1px 0 0 rgba($color, .9), 0 0 0 3px rgba($color, .25), 0 0 20px rgba($color, .35); }
}
.formhud__input {
box-sizing: border-box;
width: 100%;
border: 0;
background: transparent;
padding: $sp-3 $sp-4;
color: $subject-cream;
font-weight: 500;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
outline: none;
}
.formhud__input::placeholder {
color: rgba($subject-cream, .45);
}
.formhud__caret {
position: absolute;
left: $sp-4;
top: 50%;
width: 2px;
height: 14px;
transform: translateY(-50%);
background: $color;
animation: blink $dur-loop steps(1) infinite;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: .2; }
}
@media (prefers-reduced-motion: reduce) {
.formhud__btn, .formhud__corner, .formhud__field, .formhud__caret {
animation: none !important;
}
.formhud__btn { transform: scale(1); }
.formhud__corner { transform: translate(0, 0); }
.formhud__field { box-shadow: 0 1px 0 0 rgba($color, .4); }
.formhud__caret { opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.formhud {
display: flex;
flex-direction: column;
gap: 16px;
width: clamp(220px, 78vw, 300px);
}
.formhud__btn {
position: relative;
box-sizing: border-box;
border: 1px solid #ffd23f;
border-radius: 12px;
background: rgba(255, 210, 63, 0.14);
color: #ffd23f;
padding: 12px 20px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.02em;
animation: btnSnap 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes btnSnap {
0%, 55%, 100% {
transform: scale(1);
}
75% {
transform: scale(0.97);
}
}
.formhud__corner {
position: absolute;
width: 10px;
height: 10px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
}
.formhud__corner--tl {
top: 4px;
left: 4px;
border-top-width: 2px;
border-left-width: 2px;
animation: snapTl 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.formhud__corner--tr {
top: 4px;
right: 4px;
border-top-width: 2px;
border-right-width: 2px;
animation: snapTr 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.formhud__corner--bl {
bottom: 4px;
left: 4px;
border-bottom-width: 2px;
border-left-width: 2px;
animation: snapBl 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.formhud__corner--br {
bottom: 4px;
right: 4px;
border-bottom-width: 2px;
border-right-width: 2px;
animation: snapBr 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes snapTl {
0%, 55%, 100% {
transform: translate(0, 0);
}
75% {
transform: translate(3px, 3px);
}
}
@keyframes snapTr {
0%, 55%, 100% {
transform: translate(0, 0);
}
75% {
transform: translate(-3px, 3px);
}
}
@keyframes snapBl {
0%, 55%, 100% {
transform: translate(0, 0);
}
75% {
transform: translate(3px, -3px);
}
}
@keyframes snapBr {
0%, 55%, 100% {
transform: translate(0, 0);
}
75% {
transform: translate(-3px, -3px);
}
}
.formhud__field {
position: relative;
display: block;
border-radius: 8px;
background: #17141a;
box-shadow: 0 1px 0 0 rgba(255, 210, 63, 0.4);
animation: focusGlow 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes focusGlow {
0%, 55%, 100% {
box-shadow: 0 1px 0 0 rgba(255, 210, 63, 0.4);
}
75% {
box-shadow: 0 1px 0 0 rgba(255, 210, 63, 0.9), 0 0 0 3px rgba(255, 210, 63, 0.25), 0 0 20px rgba(255, 210, 63, 0.35);
}
}
.formhud__input {
box-sizing: border-box;
width: 100%;
border: 0;
background: transparent;
padding: 12px 16px;
color: #fff7e6;
font-weight: 500;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
outline: none;
}
.formhud__input::placeholder {
color: rgba(255, 247, 230, 0.45);
}
.formhud__caret {
position: absolute;
left: 16px;
top: 50%;
width: 2px;
height: 14px;
transform: translateY(-50%);
background: #ffd23f;
animation: blink 2s steps(1) infinite;
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion: reduce) {
.formhud__btn, .formhud__corner, .formhud__field, .formhud__caret {
animation: none !important;
}
.formhud__btn {
transform: scale(1);
}
.formhud__corner {
transform: translate(0, 0);
}
.formhud__field {
box-shadow: 0 1px 0 0 rgba(255, 210, 63, 0.4);
}
.formhud__caret {
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./ButtonsInputsHud.module.scss";
export type ButtonsInputsHudProps = {
buttonLabel?: string;
placeholder?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ButtonsInputsHud({
buttonLabel = "락온 시작",
placeholder = "이메일 주소",
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#ffd23f",
}: ButtonsInputsHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<form className={styles.form} style={vars}>
<button type="button" className={styles.btn}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
{buttonLabel}
</button>
<label className={styles.field}>
<input className={styles.input} type="email" placeholder={placeholder} readOnly />
<span className={styles.caret} aria-hidden="true" />
</label>
</form>
);
}09 · 락온 버튼·스캔 인풋 — 측정 누적면적 7.12% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
폼 제출 버튼이나 이메일 입력창처럼 방문자의 마지막 클릭 하나에 확실한 반응을 줘야 하는 자리에 씁니다.
<form class="mx-auto flex max-w-md flex-col gap-3 rounded-card bg-ink p-6 shadow-glow sm:flex-row">
<label class="field-glow relative flex-1 rounded-control bg-ink">
<input class="w-full rounded-control bg-transparent px-4 py-3 text-sm text-cream outline-none" type="email" placeholder="이메일 주소">
</label>
<button class="btn-snap relative rounded-control border border-accent bg-accent/10 px-5 py-3 text-sm font-bold text-accent">
<span class="snap-corner snap-tl" aria-hidden="true"></span>
<span class="snap-corner snap-tr" aria-hidden="true"></span>
<span class="snap-corner snap-bl" aria-hidden="true"></span>
<span class="snap-corner snap-br" aria-hidden="true"></span>
가입하기
</button>
</form>
tailwind.config 커스텀
이 편의 부품 아홉 개와 원페이지는 색·글꼴·반경·그림자 값을 오직 이 설정 하나에서만 가져다 씁니다. 여기 숫자를 고치면 브래킷 색부터 카드 모서리까지 화면 전체가 한 번에 같이 바뀝니다.
theme: {
extend: {
colors: {
ink: "#17141a",
cream: "#fff7e6",
accent: "#ffd23f",
data: "#2f6df6",
line: "rgba(255,210,63,.12)",
},
fontFamily: {
display: ["'Space Grotesk'", "system-ui", "sans-serif"],
body: ["'Inter'", "system-ui", "sans-serif"],
},
borderRadius: { control: "12px", card: "16px", pill: "999px" },
boxShadow: {
glow: "0 0 0 1px rgba(255,210,63,.4), 0 0 24px rgba(255,210,63,.16)",
"glow-strong": "0 0 0 1px rgba(255,210,63,.6), 0 0 32px rgba(255,210,63,.4)",
},
spacing: { gutter: "16px" },
},
},
색은 두 개뿐입니다. accent(#ffd23f) 호박색이 브래킷·상태 점·버튼을 전부 맡고, data(#2f6df6) 파란색은 좌표·아이디 번호처럼 "이건 수치다"라고 구분해야 할 딱 한 자리에만 예외로 씁니다.
바이브코딩 프롬프트
이 값을 프로젝트에 먼저 심고 나면, Claude Code나 Cursor에 남길 지시는 아래 한 단락으로 충분합니다. 더 자세한 규칙까지 담은 전체 원문은 zip의 prompt.md에 있습니다.
배경은 ink(#17141a) 하나, 강조는 accent(#ffd23f) 호박색 하나. 카드·헤더·CTA 모서리마다
ㄱ자 브래킷을 두고 accent 색으로 옅게 맥동시킨다. 좌표·타임스탬프·상태 텍스트만
모노스페이스로 쓰고 본문 폰트는 그대로 둔다. 반경은 rounded-card·rounded-pill·
rounded-control 세 값만 쓴다.
page/index.html에 이 지시를 실제로 적용한 뒤 1280px 너비로 전체 페이지를 캡처해 아래에 남겼습니다.

어디서 깨지는가 — 함정
스캔라인 효과, 어디서 깨지나요?
가장 흔한 함정은 큰 제목에 max-width를 걸어두고 여러 화면 폭을 확인하지 않는 것입니다. 이 편 07번 경보 밴드도 처음엔 제목에 max-w-sm(384px)을 걸어 뒀는데, 1280px 데스크톱처럼 화면이 넓어도 "지금 바로 감시망을 켜보세" 다음 줄에 "요" 한 글자만 남는 어색한 줄바꿈이 났습니다 — 뷰포트 폭이 아니라 max-width 값 자체가 원인이라, 좁은 화면만 살펴서는 절대 못 잡습니다. max-w-2xl로 바꾸고서야 1280·768·375px 세 폭 모두 자연스럽게 정리됐습니다. 두 번째 함정은 06번 얼굴 인식 후기입니다. 스캔 상자가 내려가는 거리(61px)가 아바타 높이(64px)에서 상자 두께를 뺀 값으로 고정 px로 박혀 있어, 아바타 크기를 바꾸면 이 값도 손으로 같이 계산해서 고쳐야 합니다.
다음으로 걸린 것
모노스페이스 리드아웃은 메일 본문으로 옮기는 순간 자릿수부터 무너지는데, 그 한계는 뉴스레터 디자인에서 따로 정리했습니다. 이 아홉 개 파일과 react 판·프롬프트 원문을 전부 담은 압축 파일의 비밀번호는 t2nt5x9v 이고, 아래 버튼을 누르면 내려받을 수 있습니다.
세 번째 함정은 03번 카드 격자처럼 스캔 순서를 nth-child 지연값으로 매기는 항목입니다. 카드 수를 4장에서 늘리거나 줄이면 animation-delay 배열도 손으로 다시 맞춰야 하고, 그렇지 않으면 스캔이 카드 중간에서 겹치거나 빈 순번이 생깁니다.
접근성
챙긴 것
부품 아홉 개는 MDN이 정의한 prefers-reduced-motion 조건을 하나도 빠짐없이 넣어, 동작 줄이기를 켠 방문자 화면에는 스캔·글리치·맥동 대신 "이미 잠긴" 정지 상태(브래킷 100% 크기, 점·캐럿은 항상 켜짐)만 남습니다. 브래킷·스캔라인·노이즈 레이어는 모두 aria-hidden="true"로 감춰 스크린리더가 읽지 않게 했고, LOCKED·VERIFIED·LIVE 같은 상태는 색이 아니라 글자 자체로 전달되므로 색맹 방문자도 그대로 알아볼 수 있습니다.
더 볼 곳
감시 카메라 톤 말고 다른 디자인 유행도 궁금하면 디자인 트렌드 모음을 열어 보시고, 부품마다 어떤 기준으로 통과시켰는지는 소개 페이지에 적어 뒀습니다.
FAQ
실제 게임이나 SF 영화의 사이버펑크 UI를 그대로 베낀 건가요?
아니요. 이 편의 부품은 특정 게임·영화·서비스의 화면을 복제한 것이 아니라 브래킷 락온·스캔라인·모노스페이스 리드아웃 같은 일반적인 감시 카메라 시각 언어만 참고했습니다. 서비스명 SENTRY도 예시로 지어낸 가상의 이름입니다.
이 다크 톤을 밝은(라이트) 사이트에도 쓸 수 있나요?
부분적으로는 가능하지만 권하지 않습니다. HUD·터미널 감성은 어두운 배경 위에서 브래킷과 스캔라인이 도드라져야 성립하는데, 배경을 밝게 바꾸면 같은 대비를 내기 위해 accent 색을 더 진하게 써야 해서 다른 트렌드(경고·에러 톤)처럼 보이기 쉽습니다. 반대로 밝은 바탕을 처음부터 전제로 깔고 짠 구성은 파스텔 톤 랜딩 UI 키트에 모여 있습니다.
Tailwind 없이 순수 CSS로도 쓸 수 있나요?
가능합니다. zip에 같이 담긴 tokens.css가 tailwind.config.js의 색·반경 값을 --color-ink·--color-accent 같은 CSS 변수로 그대로 옮겨 둔 파일이라, 이 한 장만 불러오면 Tailwind가 없는 프로젝트에서도 var(--color-accent)로 같은 호박색을 낼 수 있습니다.









