diff --git a/apps/site/design.md b/apps/site/design.md
index 9cf957a..83e4f4c 100644
--- a/apps/site/design.md
+++ b/apps/site/design.md
@@ -213,14 +213,35 @@ WebGL 게이트: `prefers-reduced-motion` 이면 **아예 시작하지 않는다
| 레이어 | 무엇 | 어디 | 근거 |
|---|---|---|---|
-| `.bg-grid` | 간격 눈금 격자. 셀이 `--grid-cell = --space-6` | 파이프라인 · 수치 | "모든 값이 토큰에서 나온다"가 배경에서도 성립해야 한다. 계기판의 눈금이기도 하다 |
-| `.bg-glow` | 광원 블롭 두 개. 섹션마다 `--glow-x` 로 자리를 옮긴다 | 재료 · 설치 | 히어로의 빛이 페이지를 관통해 마지막 CTA 에서 돌아온다 |
+기준면은 `BgPhoto` 컴포넌트가 만든다(아래 표).
-격자는 가장자리를 마스크로 지운다 — 화면 끝까지 가면 배경이 아니라 **표**가 된다.
-배경이 가장 적게 움직인다. 앞의 것보다 크게 움직이면 앞으로 튀어나와 보인다.
+**CSS 격자를 한동안 같이 깔았다가 걷어냈다.** 사진이 들어온 뒤로 배경이 두 겹이 되어
+서로 경쟁했고, 격자는 배경이라기보다 **표**로 읽혔다. 배경도 한 섹션에 하나다.
-**실측 3층** (스크롤 1000 → 1600): 격자 **+14.9px** · 산출물 **-14px** · 단계명 **0**.
-한 층은 반드시 고정이다. 전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.
+한 층은 반드시 고정이다 — 파이프라인의 단계명은 움직이지 않는다.
+전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.
+
+### 패럴랙스가 세 번 실패한 경위
+
+**1. 이동량이 모자랐다.** 여유를 28% 잡아놓고 `±46px`(총 92px)만 움직였다.
+스크롤 1800px 구간에서 이동 비율 5% — 아무도 시차를 느끼지 못했다.
+공식은 **이미지를 컨테이너보다 크게 만들고 그 초과분 전체를 움직이는 것**이다.
+`depth 0.45` → 이미지 145%, 이동 `translateY(-31.03%)`. 같은 구간에서 **374px**.
+
+**2. `overflow: hidden` 이 `view()` 타임라인을 죽이고 있었다.**
+`overflow: hidden` 은 스크롤 컨테이너를 만들고, 그러면 자식의 `view()` 가 문서가 아니라
+그 상자를 기준으로 잡힌다. 이미지는 상자 안에서 늘 같은 자리라 진행도가 고정된다.
+**콘솔 에러도 없고 computed 값도 정상이라** 두 스크롤 위치에서 `transform` 을 재보고서야
+이동 0px 인 것을 알았다. 잘라내는 일은 섹션의 `overflow: clip` 에 맡겼다 —
+`clip` 은 스크롤 컨테이너를 만들지 않는다.
+
+**3. `opacity` 로 눌러 이미지를 지웠다.** 어두운 광원 사진을 `opacity: 0.5` 로 깔면
+검정도 빛도 같이 죽어 **있는지조차 보이지 않는다.** `mix-blend-mode: screen` 으로 바꿨다 —
+검정은 투명해지고 밝은 곳만 더해지므로 불투명도를 0.85~1.0 으로 유지해도 본문 대비가 안전하다.
+밝은 종이만 `normal` 이다.
+
+**생성 프롬프트에도 같은 함정이 있었다.** "본문 뒤에 깔리니 어둡게"를 너무 강하게 넣어서
+안전한 이미지가 아니라 **없는 이미지**를 만들었다. 대비는 합성 방식으로 풀 문제였다.
### 배경 사진 넉 장 (gpt-image-2 로 생성)
diff --git a/apps/site/src/components/BgPhoto.astro b/apps/site/src/components/BgPhoto.astro
index 1af18c3..ee6f01f 100644
--- a/apps/site/src/components/BgPhoto.astro
+++ b/apps/site/src/components/BgPhoto.astro
@@ -18,60 +18,108 @@ const SOURCES = { refraction, paper, split, align } as const;
interface Props {
variant?: keyof typeof SOURCES;
- /** 0~1. 본문 뒤에 깔리므로 기본값을 낮게 둔다 */
+ /**
+ * 패럴랙스 강도. 이미지를 컨테이너보다 이 비율만큼 크게 만들고,
+ * 스크롤 구간 동안 정확히 그 초과분만큼 움직인다.
+ * 0.45 면 섹션 높이가 2000px 일 때 900px 을 이동한다.
+ */
+ depth?: number;
+ /** 0~1 */
opacity?: number;
/** 좌우 반전. 같은 그림을 두 섹션에 쓸 때 되풀이로 안 읽히게 한다 */
flip?: boolean;
}
-const { variant = "refraction", opacity = 0.45, flip = false } = Astro.props;
+const { variant = "refraction", depth = 0.45, opacity = 0.9, flip = false } = Astro.props;
const src = SOURCES[variant];
+
+// 밝은 종이만 그대로 얹고, 어두운 광원 사진은 더하기 합성한다.
+const additive = variant !== "paper";
+
+// 이미지 높이 = (1 + depth) 배. 이동량은 이미지 자기 높이 기준 비율로 환산한다.
+// 예: depth 0.45 -> 높이 145%, 이동 -(0.45 / 1.45) = -31.03%
+const scale = 1 + depth;
+const shift = -(depth / scale) * 100;
---
{/*
- `loading="lazy"` — 초기 번들에 들어가지 않는다. CSS background-image 로 깔면
이 통제권을 잃는다(항상 받고, 최적화도 안 된다). 그래서
를 절대배치한다
- `aria-hidden` + 빈 alt — 장식이다. 보조기술이 읽을 것이 없다
- - opacity 기본 0.45 — 본문 대비를 지키는 선. 올리려면 그 위 글자를 다시 재라
*/}
-
+
diff --git a/apps/site/src/components/Install.astro b/apps/site/src/components/Install.astro
index c92f8b2..d85bc7d 100644
--- a/apps/site/src/components/Install.astro
+++ b/apps/site/src/components/Install.astro
@@ -9,7 +9,7 @@ const { c, ui } = Astro.props;
-
+
{c.h2}
diff --git a/apps/site/src/components/Materials.astro b/apps/site/src/components/Materials.astro
index f8861de..69b9c4b 100644
--- a/apps/site/src/components/Materials.astro
+++ b/apps/site/src/components/Materials.astro
@@ -12,7 +12,7 @@ const { c } = Astro.props;
둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다.
*/}
-
+
{c.h2}
{c.lead}
diff --git a/apps/site/src/components/Metrics.astro b/apps/site/src/components/Metrics.astro
index 64aac25..4957d1e 100644
--- a/apps/site/src/components/Metrics.astro
+++ b/apps/site/src/components/Metrics.astro
@@ -19,7 +19,7 @@ const rest = c.items.filter((m) => !m.feature);
흔들리면 그 값도 흔들리는 것처럼 읽힌다. 계기판의 바늘은 떨지 않는다.
움직임이 의미를 만드는 자리가 있으면, 움직이지 않는 것이 의미인 자리도 있다.
*/}
-
+
{c.eyebrow}
{c.h2}
diff --git a/apps/site/src/components/Pipeline.astro b/apps/site/src/components/Pipeline.astro
index b65203b..7325346 100644
--- a/apps/site/src/components/Pipeline.astro
+++ b/apps/site/src/components/Pipeline.astro
@@ -14,7 +14,7 @@ const { c } = Astro.props;
③ 의미 —
— 화면에 번호는 없지만 보조기술은 "6개 중 n번째"로 읽는다
role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다
*/}
-
+
{/* 제목·리드·브리프·스파인을 한 컨테이너에 담는다.
섹션 기본 그리드에 두면 제목은 main 칼럼의 음수 마진으로 x=371,
리드는 x=435, 스파인은 x=143 에 서서 정렬선이 셋이 된다. */}
diff --git a/apps/site/src/components/Preflight.astro b/apps/site/src/components/Preflight.astro
index 0eeb24f..8db8944 100644
--- a/apps/site/src/components/Preflight.astro
+++ b/apps/site/src/components/Preflight.astro
@@ -12,7 +12,7 @@ const { c } = Astro.props;
왼쪽 세로선이 로그라는 형식을 만든다.
*/}
-
+
{c.h2}
{c.lead}
diff --git a/apps/site/src/components/Showcase.astro b/apps/site/src/components/Showcase.astro
index 9b79dff..96a14ba 100644
--- a/apps/site/src/components/Showcase.astro
+++ b/apps/site/src/components/Showcase.astro
@@ -12,7 +12,7 @@ const shots = [shotCoffee, shotSaas];
{/* 레이아웃 패밀리: Z축 캐스케이드. 두 판이 서로 겹치고 각도가 다르다 */}
-
+
{c.h2}
{c.lead}
diff --git a/apps/site/src/components/Typography.astro b/apps/site/src/components/Typography.astro
index f3d2c77..ce27c82 100644
--- a/apps/site/src/components/Typography.astro
+++ b/apps/site/src/components/Typography.astro
@@ -9,7 +9,7 @@ const { c } = Astro.props;
---
-
+
{c.eyebrow}
{c.h2}
{c.lead}
diff --git a/apps/site/src/styles/base.css b/apps/site/src/styles/base.css
index 8f83f76..a93ffce 100644
--- a/apps/site/src/styles/base.css
+++ b/apps/site/src/styles/base.css
@@ -419,67 +419,19 @@ a:hover { color: var(--accent); }
패럴랙스는 깊이 차가 보여야 성립한다. 배경이 단색이면 앞의 것이
몇 px 어긋나 봐야 아무도 못 느낀다 — 기준면이 없기 때문이다.
- 그래서 배경을 깐다. 다만 사 온 이미지가 아니라 이 페이지가 주장하는 것,
- 곧 간격 눈금 자체를 격자로 그린다. --grid-cell 은 --space-5 다. */
-.bg-grid,
-.bg-glow,
+ 기준면은 `BgPhoto` 컴포넌트가 만든다.
+
+ CSS 로 그린 격자를 한동안 같이 깔았는데 걷어냈다. 사진이 들어온 뒤로는
+ 배경이 두 겹이 되어 서로 경쟁했고, 격자는 배경이라기보다 표로 읽혔다.
+ 배경도 한 섹션에 하나여야 한다. */
.has-bg {
position: relative;
isolation: isolate;
- /* 배경 레이어는 위아래로 14% 삐져나온다(패럴랙스 여유).
- clip 하지 않으면 그 삐져나온 부분이 이웃 섹션 위에 얹힌다 —
- 조판 섹션의 종이가 위 섹션 하단에 밝은 띠로 나타났다. */
+ /* 배경 이미지는 컨테이너보다 크고 패럴랙스로 움직인다.
+ clip 하지 않으면 그 초과분이 이웃 섹션 위로 넘친다. */
overflow: clip;
}
-/* 섹션은 subgrid 이고 `.section > *` 이 전부 main 칼럼(561px)에 배치된다.
- 절대배치라도 그리드 영역이 잡히면 그게 컨테이닝 블록이 되므로,
- 배경이 화면 가운데 561px 띠로만 깔렸다. 전체 폭으로 되돌린다. */
-.bg-photo { grid-column: full; }
-.bg-grid::before,
-.bg-glow::before {
- content: "";
- position: absolute;
- inset: -12% 0;
- z-index: -1;
- pointer-events: none;
-}
-
-.bg-grid::before {
- background-image:
- repeating-linear-gradient(to right,
- var(--line-strong) 0 1px, transparent 1px var(--grid-cell)),
- repeating-linear-gradient(to bottom,
- var(--line-strong) 0 1px, transparent 1px var(--grid-cell));
- /* 가장자리에서 지운다. 화면 끝까지 격자가 가면 표가 되어버린다 */
- -webkit-mask-image: radial-gradient(135% 95% at 50% 45%, black 22%, transparent 92%);
- mask-image: radial-gradient(135% 95% at 50% 45%, black 22%, transparent 92%);
- opacity: 0.7;
-}
-
-/* 히어로의 두 광원이 페이지를 관통한다. 섹션마다 자리가 다르다 */
-.bg-glow::before {
- background:
- radial-gradient(42% 46% at var(--glow-x, 78%) var(--glow-y, 32%),
- color-mix(in oklab, var(--accent) 15%, transparent), transparent 70%),
- radial-gradient(38% 42% at calc(100% - var(--glow-x, 78%)) 78%,
- color-mix(in oklab, var(--glow-2) 13%, transparent), transparent 68%);
-}
-
-@media (prefers-reduced-motion: no-preference) {
- @supports (animation-timeline: view()) {
- /* 배경이 가장 느리다. 콘텐츠보다 적게 움직여야 뒤에 있는 것으로 읽힌다 */
- .bg-grid::before { --par-from: -34px; --par-to: 34px; }
- .bg-glow::before { --par-from: -58px; --par-to: 58px; }
- .bg-grid::before,
- .bg-glow::before {
- animation: par linear both;
- animation-timeline: view();
- animation-range: cover 0% cover 100%;
- }
- }
-}
-
/* ---- 유틸 -------------------------------------------------- */
.visually-hidden {
position: absolute;
diff --git a/packages/skill/references/motion.md b/packages/skill/references/motion.md
index cb706d1..85d6c4e 100644
--- a/packages/skill/references/motion.md
+++ b/packages/skill/references/motion.md
@@ -448,6 +448,81 @@ JS 쪽 판정도 한 곳에 모은다. 설정이 도중에 바뀌면 반영한
안 움직이는 섹션이 하나도 없으면 움직임에 의미가 없다는 뜻이고,
`design.md` 에 "여기는 의도적으로 정지"라고 적어두지 않으면 다음 사람이 "빠뜨린 것"으로 보고 채운다.
+### 이동량은 절대값이 아니라 컨테이너 대비 비율이다
+
+**패럴랙스가 "안 보인다"의 대부분은 이동량이 모자란 것이다.**
+
+공식은 하나다. **배경을 컨테이너보다 크게 만들고, 그 초과분 전체를 움직인다.**
+
+```
+이미지 높이 = 컨테이너 × (1 + depth)
+이동량 = 컨테이너 × depth
+ = 이미지 높이의 depth / (1 + depth)
+```
+
+`depth: 0.45` 면 이미지는 145%, 이동은 이미지 자기 높이의 **31%**.
+섹션이 2000px 이면 **900px** 을 움직인다.
+
+```css
+.bg img {
+ height: 145%; /* 1 + depth */
+ animation: bg-par linear both;
+ animation-timeline: view();
+ animation-range: cover 0% cover 100%;
+}
+@keyframes bg-par {
+ from { transform: translateY(0); }
+ to { transform: translateY(-31.03%); } /* depth / (1 + depth) */
+}
+```
+
+`translateY(%)` 는 **자기 크기 기준**이라 컨테이너 높이를 몰라도 정확히 맞는다.
+
+**실측 사례**: 여유를 28% 잡아놓고 `±46px`(총 92px)만 움직였더니 스크롤 1800px 구간에서
+이동 비율이 5% 였다. 아무도 시차를 느끼지 못했다. 같은 구간에서 **374px** 로 올리자 보였다.
+CSS-Tricks 의 예제도 `background-position: bottom 0px → bottom -400px`, 즉 400px 이다.
+
+> 참고: `background-position` 을 애니메이션하는 방법도 있다(CSS-Tricks). 다만 그러면
+> `
` 의 지연 로딩과 반응형 `srcset` 을 잃는다. 무거운 사진이면 `
` 를
+> 절대배치하고 `transform` 을 쓰는 쪽이 낫다.
+
+### `overflow: hidden` 이 view() 타임라인을 죽인다
+
+**이것 하나에 패럴랙스가 통째로 멈춘다. 그리고 조용히 멈춘다.**
+
+`overflow: hidden` 은 **스크롤 컨테이너를 만든다.** 그러면 그 안에 있는 자식의
+`animation-timeline: view()` 는 문서가 아니라 **그 상자**를 기준으로 잡힌다.
+자식은 상자 안에서 위치가 변하지 않으므로 진행도가 고정되고, 애니메이션은
+어중간한 한 프레임에서 굳는다.
+
+- 콘솔 에러 없음. `animation-name`·`animation-timeline` computed 값도 전부 정상
+- `getComputedStyle(el).transform` 을 두 스크롤 위치에서 재봐야 알 수 있다. **실측에서 이동 0px**
+
+**잘라내야 한다면 `overflow: clip` 을 쓰거나 조상 섹션에서 처리해라.**
+`clip` 은 스크롤 컨테이너를 만들지 않는다.
+
+### 어두운 배경 위 광원 이미지는 opacity 로 누르지 마라
+
+`opacity` 는 이미지 전체를 균일하게 죽인다 — 검정도, 빛도 같이. 그래서 어두운 광원 사진을
+`opacity: 0.5` 로 깔면 **있는지조차 안 보인다.**
+
+```css
+.bg img { mix-blend-mode: screen; opacity: 0.9; }
+```
+
+`screen` 은 **검정을 투명하게 만들고 밝은 곳만 더한다.** 그래서 불투명도를 높게 유지해도
+본문 대비를 해치지 않는다. 밝은 이미지(종이·질감)는 반대로 `normal` 이 맞다.
+
+**생성형 이미지를 배경으로 쓸 때 특히 조심해라.** 프롬프트에 "본문 뒤에 깔리니 어둡게"를
+너무 강하게 넣으면 안전한 이미지가 아니라 **없는 이미지**가 나온다.
+대비는 합성 방식으로 풀고, 이미지 자체는 볼 수 있게 만들어라.
+
+### 배경도 한 섹션에 하나다
+
+CSS 격자와 사진을 같이 깔았다가 걷어냈다. 두 배경이 서로 경쟁했고,
+격자는 배경이라기보다 **표**로 읽혔다. 앞에서 "한 층은 고정"이라고 한 것과 같은 이야기다 —
+층을 늘리는 것과 층이 싸우게 두는 것은 다르다.
+
### 패럴랙스에는 기준면이 필요하다
**배경이 단색이면 패럴랙스는 성립하지 않는다.** 앞의 것이 몇 px 어긋나 봐야