diff --git a/docs/design-system.md b/docs/design-system.md
index 2b3fd549..7b4f4687 100644
--- a/docs/design-system.md
+++ b/docs/design-system.md
@@ -12,7 +12,7 @@
1. **신뢰감 우선** — 면접 도구이므로 가벼운 톤 지양. 진중·차분한 시각 언어.
2. **집중 환경 보호** — 면접 화면은 노이즈 최소화. 핵심 UI만 노출, 부가 정보는 hover/click 으로 점진적 공개.
3. **상태 가시성** — 분석 진행, 세션 상태, SSE 이벤트 등 비동기 상태는 항상 명시적으로 표현.
-4. **모노크로매틱 + 의미 컬러** — 로고 블루(#3E63D8) 기반 **쿨 블루그레이** 단일 베이스(토큰명은 `sage-*` 유지)로 톤 일관성을 유지하고, 브랜드 강조는 로고 블루 `primary`, Status / Domain 만 muted jewel tone 으로 식별성 부여.
+4. **모노크로매틱 + 의미 컬러** — 브랜드 블루(#4662D2) 기반 **쿨 블루그레이** 단일 베이스(토큰명은 `sage-*` 유지)로 톤 일관성을 유지하고, 브랜드 강조는 로고 블루 `primary`, Status / Domain 만 muted jewel tone 으로 식별성 부여.
5. **접근성 (WCAG 2.1 AA)** — 키보드 only 조작, 명도 대비 4.5:1 이상, focus ring 명확.
6. **모바일 웹뷰 대응** — 데스크탑 우선이지만 mobile breakpoint(`< lg`)에서도 깨지지 않게.
@@ -32,7 +32,7 @@
### 2.2 컬러 — Sage Scale (메인 테마, 11 단계)
-> 토큰 이름은 `sage-*` 로 유지하되 **값은 로고(#3E63D8)에 맞춘 쿨 블루그레이 모노크롬** 이다(저채도 — 중립/본문용). 브랜드 강조색은 별도 `primary` (아래 §2.3, 스케일과 분리).
+> 토큰 이름은 `sage-*` 로 유지하되 **값은 브랜드 블루(#4662D2)에 맞춘 쿨 블루그레이 모노크롬** 이다(저채도 — 중립/본문용). 브랜드 강조색은 별도 `primary` (아래 §2.3, 스케일과 분리).
| 토큰 | Hex | 권장 용도 |
|---|---|---|
@@ -67,9 +67,12 @@ Tailwind 사용: `bg-sage-{n}`, `text-sage-{n}`, `border-sage-{n}`.
| `--color-fg-subtle` | `sage-400` | `text-fg-subtle` |
| `--color-fg-disabled` | `sage-200` | `text-fg-disabled` |
| `--color-fg-on-primary` | `white` | `text-fg-on-primary` |
-| `--color-primary` | `#3e63d8` (로고 블루, 스케일과 분리) | `bg-primary`, `text-primary` |
-| `--color-primary-hover` | `#3050be` | `bg-primary-hover` |
-| `--color-primary-pressed` | `#274099` | `bg-primary-pressed` |
+| `--color-primary` | `#4662d2` (브랜드 블루, 스케일과 분리) | `bg-primary`, `text-primary` |
+| `--color-primary-hover` | `#3b54bd` | `bg-primary-hover` |
+| `--color-primary-pressed` | `#31479f` | `bg-primary-pressed` |
+| `--color-primary-50` | `#eff2fd` (연한 파랑 표면) | `bg-primary-50` |
+| `--color-primary-100` | `#dee4fa` | `bg-primary-100` |
+| `--color-primary-200` | `#c3cdf5` (틴트 위 보더) | `border-primary-200` |
### 2.4 컬러 — Status
@@ -412,7 +415,7 @@ Tailwind v4 기본 `--spacing: 0.25rem` (= 4px) 사용. `p-4` = `16px`.
## 12. 변경 이력
-- **2026-06** — **블루 리테마**: 로고(#3E63D8)에 맞춰 `sage-*` 스케일 값을 쿨 블루그레이 모노크롬으로 교체(토큰명 유지), `primary` 를 스케일에서 분리해 로고 블루로 지정. 배경/그림자/포커스링도 쿨톤·네이비로 재조정. 위젯 하드코딩 hex 일괄 치환. Status / Domain 컬러는 유지.
+- **2026-06** — **블루 리테마**: 브랜드 블루(#4662D2)에 맞춰 `sage-*` 스케일 값을 쿨 블루그레이 모노크롬으로 교체(토큰명 유지), `primary` 를 스케일에서 분리해 로고 블루로 지정. 배경/그림자/포커스링도 쿨톤·네이비로 재조정. 위젯 하드코딩 hex 일괄 치환. Status / Domain 컬러는 유지.
- **2026-05** — Tailwind CSS v4 기반 **sage 모노크로매틱** 시스템으로 전면 개편. 이전 `Pretendard + 블루 브랜드` 스펙은 본 문서로 흡수.
- **2026-05** — Status / Domain 컬러를 muted jewel tone 으로 재정의 (Sage 톤과 조화).
- **2026-05** — Z-index, Container max-width, 4-State UI 패턴 등 누락 토큰 / 규약 보강.
diff --git a/frontend/index.html b/frontend/index.html
index 02a6296f..8c75c813 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -15,7 +15,7 @@
/>
StackUp — AI Interview Solution
diff --git a/frontend/public/last-section-get-started.png b/frontend/public/last-section-get-started.png
deleted file mode 100644
index 44728d4f..00000000
Binary files a/frontend/public/last-section-get-started.png and /dev/null differ
diff --git a/frontend/public/second-section-backend-interview.avif b/frontend/public/second-section-backend-interview.avif
deleted file mode 100644
index aa31614f..00000000
Binary files a/frontend/public/second-section-backend-interview.avif and /dev/null differ
diff --git a/frontend/public/second-section-cs-interview.webp b/frontend/public/second-section-cs-interview.webp
deleted file mode 100644
index 234f1afb..00000000
Binary files a/frontend/public/second-section-cs-interview.webp and /dev/null differ
diff --git a/frontend/public/second-section-frontend-interview.png b/frontend/public/second-section-frontend-interview.png
deleted file mode 100644
index 0772b77a..00000000
Binary files a/frontend/public/second-section-frontend-interview.png and /dev/null differ
diff --git a/frontend/src/app/styles/global.css b/frontend/src/app/styles/global.css
index 57063994..48568970 100644
--- a/frontend/src/app/styles/global.css
+++ b/frontend/src/app/styles/global.css
@@ -6,7 +6,6 @@
* 폰트는 `index.html` 의 `` 로 로드.
* - Pretendard Variable (한·영 본문/서브헤딩)
* - Bricolage Grotesque (디스플레이 헤딩)
- * - Caveat (스크립트 강조)
* - Geist Mono (코드/모노)
*/
diff --git a/frontend/src/app/styles/tokens.css b/frontend/src/app/styles/tokens.css
index bb6ddb40..994bb5c5 100644
--- a/frontend/src/app/styles/tokens.css
+++ b/frontend/src/app/styles/tokens.css
@@ -16,7 +16,7 @@
/* =========================================================
* 2. Color — Sage / Neutral Scale (Main Theme)
* bg-sage-500, text-sage-800, border-sage-200 ...
- * NOTE: 토큰 이름은 'sage' 로 유지하되 값은 로고(#3E63D8)에 맞춘
+ * NOTE: 토큰 이름은 'sage' 로 유지하되 값은 브랜드 블루(#4662D2)에 맞춘
* 쿨 블루그레이 모노크롬 램프다(저채도 — 본문/중립용). 브랜드 강조색은 §3 primary.
* ========================================================= */
--color-sage-50: #eef1f6;
@@ -51,11 +51,18 @@
--color-fg-disabled: var(--color-sage-200);
--color-fg-on-primary: var(--color-white);
- /* 브랜드 강조색 — 로고 블루(#3E63D8). 중립 스케일과 분리(고채도라 본문엔 미사용,
- * 버튼·링크·강조에만). bg-primary / text-primary / border-primary 가 이 값을 쓴다. */
- --color-primary: #3e63d8;
- --color-primary-hover: #3050be;
- --color-primary-pressed: #274099;
+ /* 브랜드 강조색 — #4662D2. 중립 스케일과 분리(고채도라 본문엔 미사용,
+ * 버튼·링크·강조에만). bg-primary / text-primary / border-primary 가 이 값을 쓴다.
+ * 흰 글자 대비 5.33:1 — 일반 텍스트 AA 통과. */
+ --color-primary: #4662d2;
+ --color-primary-hover: #3b54bd;
+ --color-primary-pressed: #31479f;
+
+ /* primary 틴트 — 연한 파랑 표면/보더용. 랜딩의 강조 블록, 뱃지 배경에 사용.
+ * 본문 텍스트 배경으로 쓸 때 fg(#101627) 대비 충분. */
+ --color-primary-50: #eff2fd;
+ --color-primary-100: #dee4fa;
+ --color-primary-200: #c3cdf5;
/* =========================================================
* 4. Color — Status (Success / Warning / Danger / Info)
@@ -112,8 +119,6 @@
'Pretendard Variable', system-ui, sans-serif;
--font-body:
'Pretendard Variable', system-ui, sans-serif;
- --font-script:
- 'Caveat', 'Pretendard Variable', system-ui, sans-serif;
--font-mono:
'Geist Mono', ui-monospace, 'JetBrains Mono', Consolas, monospace;
@@ -214,7 +219,7 @@
--shadow-lg:
0 10px 15px -3px rgba(16, 22, 39, 0.10),
0 4px 6px -4px rgba(16, 22, 39, 0.05);
- --shadow-focus-ring: 0 0 0 3px rgba(62, 99, 216, 0.45);
+ --shadow-focus-ring: 0 0 0 3px rgba(70, 98, 210, 0.45);
/* =========================================================
* 10. Motion
diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png
deleted file mode 100644
index cc51a3d2..00000000
Binary files a/frontend/src/assets/hero.png and /dev/null differ
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
deleted file mode 100644
index 6c87de9b..00000000
--- a/frontend/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg
deleted file mode 100644
index 5101b674..00000000
--- a/frontend/src/assets/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/frontend/src/pages/DesignSystem/sections/Colors.tsx b/frontend/src/pages/DesignSystem/sections/Colors.tsx
index 43ac7be4..08e3284f 100644
--- a/frontend/src/pages/DesignSystem/sections/Colors.tsx
+++ b/frontend/src/pages/DesignSystem/sections/Colors.tsx
@@ -26,9 +26,9 @@ const SEMANTIC_ROWS = [
{ token: 'fg-muted', alias: 'sage-500', hex: '#4a5a7e', usage: '보조 텍스트' },
{ token: 'fg-subtle', alias: 'sage-400', hex: '#6e7f9f', usage: '흐린 텍스트' },
{ token: 'fg-disabled', alias: 'sage-200', hex: '#c6cedd', usage: '비활성' },
- { token: 'primary', alias: '브랜드 블루(로고)', hex: '#3e63d8', usage: 'Primary 액션·링크·강조' },
- { token: 'primary-hover', alias: '—', hex: '#3050be', usage: 'hover' },
- { token: 'primary-pressed', alias: '—', hex: '#274099', usage: 'active' },
+ { token: 'primary', alias: '브랜드 블루', hex: '#4662d2', usage: 'Primary 액션·링크·강조' },
+ { token: 'primary-hover', alias: '—', hex: '#3b54bd', usage: 'hover' },
+ { token: 'primary-pressed', alias: '—', hex: '#31479f', usage: 'active' },
]
const STATUS_COLORS = [
diff --git a/frontend/src/pages/Home/ui/HomePage.tsx b/frontend/src/pages/Home/ui/HomePage.tsx
index 9f059179..8cd012eb 100644
--- a/frontend/src/pages/Home/ui/HomePage.tsx
+++ b/frontend/src/pages/Home/ui/HomePage.tsx
@@ -2,8 +2,9 @@ import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { SiteNav } from '@/widgets/site-nav'
import { HomeHero } from '@/widgets/home-hero'
+import { HomeSteps } from '@/widgets/home-steps'
+import { HomeFeatures } from '@/widgets/home-features'
import { HomeServices } from '@/widgets/home-services'
-import { HomeQuote } from '@/widgets/home-quote'
import { HomeFaq } from '@/widgets/home-faq'
import { HomeCta } from '@/widgets/home-cta'
import { SiteFooter } from '@/widgets/site-footer'
@@ -27,12 +28,13 @@ export default function HomePage() {
}, [hash])
return (
-
+
+
+
-
diff --git a/frontend/src/shared/hooks/index.ts b/frontend/src/shared/hooks/index.ts
index 02a5dea3..57706383 100644
--- a/frontend/src/shared/hooks/index.ts
+++ b/frontend/src/shared/hooks/index.ts
@@ -1,5 +1,3 @@
-export { useTypewriter } from './useTypewriter'
-export type { UseTypewriterOptions } from './useTypewriter'
export { useEventStream } from './useEventStream'
export { useAnalysisProgress, analysisProgress } from './useAnalysisProgress'
export type { AnalysisProgress } from './useAnalysisProgress'
diff --git a/frontend/src/shared/hooks/useTypewriter.ts b/frontend/src/shared/hooks/useTypewriter.ts
deleted file mode 100644
index d7f36398..00000000
--- a/frontend/src/shared/hooks/useTypewriter.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { useEffect, useState } from 'react'
-
-export type UseTypewriterOptions = {
- startDelayMs?: number
- stepMs?: number
- respectReducedMotion?: boolean
-}
-
-export function useTypewriter(
- text: string,
- options: UseTypewriterOptions = {},
-) {
- const {
- startDelayMs = 0,
- stepMs = 100,
- respectReducedMotion = true,
- } = options
-
- const [typed, setTyped] = useState('')
-
- useEffect(() => {
- const reduce =
- respectReducedMotion &&
- typeof window !== 'undefined' &&
- window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
-
- if (reduce) {
- setTyped(text)
- return
- }
-
- setTyped('')
- let rafId = 0
- let startTime: number | null = null
-
- const tick = (now: number) => {
- if (startTime === null) startTime = now
- const elapsed = now - startTime - startDelayMs
- if (elapsed < 0) {
- rafId = window.requestAnimationFrame(tick)
- return
- }
- const next = Math.min(
- Math.floor(elapsed / stepMs) + 1,
- text.length,
- )
- setTyped(text.slice(0, next))
- if (next < text.length) {
- rafId = window.requestAnimationFrame(tick)
- }
- }
-
- rafId = window.requestAnimationFrame(tick)
-
- return () => {
- window.cancelAnimationFrame(rafId)
- }
- }, [text, startDelayMs, stepMs, respectReducedMotion])
-
- return { typed, done: typed.length >= text.length }
-}
diff --git a/frontend/src/shared/ui/Reveal/Reveal.tsx b/frontend/src/shared/ui/Reveal/Reveal.tsx
new file mode 100644
index 00000000..22787e62
--- /dev/null
+++ b/frontend/src/shared/ui/Reveal/Reveal.tsx
@@ -0,0 +1,68 @@
+import { useEffect, useRef, useState, type ReactNode } from 'react'
+
+export type RevealProps = {
+ children: ReactNode
+ /** 뷰포트 진입 후 지연(ms). 같은 블록 내 요소를 순차 등장시킬 때만 쓴다(최대 3~4단계). */
+ delayMs?: number
+ className?: string
+ /** 감싸는 태그. 레이아웃(grid item 등)을 깨지 않게 필요 시 교체. */
+ as?: 'div' | 'li' | 'section'
+}
+
+/**
+ * 뷰포트에 들어올 때 한 번만 페이드업.
+ *
+ * 랜딩 섹션의 등장 모션 전용 — 스크롤을 따라 내용이 "도착하는" 정도의 절제된 모션만
+ * 준다(이동 12px, 320ms). 한 번 보이면 다시 숨기지 않는다(스크롤 되돌릴 때 깜빡임 방지).
+ *
+ * `prefers-reduced-motion` 이면 관찰 없이 즉시 표시한다 — global.css 의 duration 무력화만으로는
+ * 초기 opacity:0 이 남아 내용이 안 보일 수 있어서, 여기서 직접 분기한다.
+ */
+/** 관찰 없이 처음부터 보여줄 상황인지 — 모션 축소 선호 또는 IO 미지원. */
+function shouldSkipObserving() {
+ if (typeof IntersectionObserver === 'undefined') return true
+ if (typeof window === 'undefined') return false
+ return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true
+}
+
+export function Reveal({ children, delayMs = 0, className = '', as = 'div' }: RevealProps) {
+ const ref = useRef(null)
+ // 초기값으로 결정한다 — effect 안에서 setState 하면 불필요한 연쇄 렌더가 생긴다.
+ const [shown, setShown] = useState(shouldSkipObserving)
+
+ useEffect(() => {
+ if (shown) return
+
+ const el = ref.current
+ if (!el) return
+
+ const io = new IntersectionObserver(
+ (entries) => {
+ if (entries.some((e) => e.isIntersecting)) {
+ setShown(true)
+ io.disconnect()
+ }
+ },
+ // 살짝 올라온 시점에 시작해 "이미 보이는데 뒤늦게 뜨는" 느낌을 없앤다.
+ { rootMargin: '0px 0px -12% 0px', threshold: 0.01 },
+ )
+ io.observe(el)
+ return () => io.disconnect()
+ }, [shown])
+
+ const Tag = as
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/frontend/src/shared/ui/Reveal/index.ts b/frontend/src/shared/ui/Reveal/index.ts
new file mode 100644
index 00000000..1bd709d8
--- /dev/null
+++ b/frontend/src/shared/ui/Reveal/index.ts
@@ -0,0 +1,2 @@
+export { Reveal } from './Reveal'
+export type { RevealProps } from './Reveal'
diff --git a/frontend/src/shared/ui/index.ts b/frontend/src/shared/ui/index.ts
index 590d29f0..1c118836 100644
--- a/frontend/src/shared/ui/index.ts
+++ b/frontend/src/shared/ui/index.ts
@@ -9,3 +9,5 @@ export type { ToastTone, ToastItem } from './Toast'
export { EmptyState } from './EmptyState'
export type { EmptyStateProps } from './EmptyState'
export { ListSkeleton } from './ListSkeleton'
+export { Reveal } from './Reveal'
+export type { RevealProps } from './Reveal'
diff --git a/frontend/src/widgets/home-cta/ui/HomeCta.tsx b/frontend/src/widgets/home-cta/ui/HomeCta.tsx
index 74f42026..476b21a6 100644
--- a/frontend/src/widgets/home-cta/ui/HomeCta.tsx
+++ b/frontend/src/widgets/home-cta/ui/HomeCta.tsx
@@ -1,54 +1,41 @@
import { Link } from 'react-router-dom'
import { useGetStartedTarget } from '@/features/auth'
+import { Reveal } from '@/shared/ui'
export function HomeCta() {
const getStartedTo = useGetStartedTarget()
- return (
-
-
-
-
-
-
+ return (
+
+
+
+
- Ready to level up?
+ 첫 면접, 지금 시작해보세요
-
- 지금 GitHub 계정만 연결하면, 30초 안에 첫 모의면접이 시작됩니다.
+
+ GitHub 계정을 연결하면 자료 분석부터 바로 시작됩니다.
- Get Started
-
- →
-
+ GitHub으로 시작하기
-
+
)
diff --git a/frontend/src/widgets/home-faq/ui/HomeFaq.tsx b/frontend/src/widgets/home-faq/ui/HomeFaq.tsx
index 59428efb..1edd441a 100644
--- a/frontend/src/widgets/home-faq/ui/HomeFaq.tsx
+++ b/frontend/src/widgets/home-faq/ui/HomeFaq.tsx
@@ -1,3 +1,5 @@
+import { Reveal } from '@/shared/ui'
+
const faqs = [
{
q: 'Stack Up은 무료 크레딧을 제공하나요?',
@@ -9,74 +11,66 @@ const faqs = [
},
{
q: 'GitHub 외 다른 로그인은 가능한가요?',
- a: '개발자를 위한 서비스이고 레포 분석이 핵심 기능이라, MVP단계에선 GitHub OAuth만 제공합니다.',
+ a: '개발자를 위한 서비스이고 레포 분석이 핵심 기능이라, MVP 단계에선 GitHub OAuth만 제공합니다.',
},
{
q: '꼬리질문은 얼마나 빨리 받을 수 있나요?',
- a: '평균 3초 이내 응답을 목표로 합니다. Flash 모델과 사전 구축 RAG 인덱스로 지연을 최소화해요.',
+ a: '평균 3초 이내 응답을 목표로 합니다. 저지연 모델과 사전 구축한 검색 인덱스로 지연을 최소화해요.',
},
{
q: '음성 분석은 어떻게 동작하나요?',
- a: 'WebRTC로 마이크·웹캠 스트림을 받아 말 속도(WPM)·간투어를 분석합니다. 권한을 거부하면 텍스트 입력으로 진행할 수 있어요.',
+ a: '마이크 스트림을 받아 말 속도(어절/분)·무음 구간·간투어를 측정합니다. 권한을 거부하면 텍스트 입력으로 진행할 수 있어요.',
},
]
export function HomeFaq() {
return (
-
-