패럴랙스에 기준면을 준다 — 배경 격자와 광원 레이어

배경이 단색이면 패럴랙스는 성립하지 않는다. 앞의 것이 몇 px 어긋나도
비교할 대상이 없다. 히어로 말고는 기준면이 없어 시차가 안 보였다.

배경 이미지를 사 오는 대신 이 페이지가 이미 주장하는 것을 그렸다
  .bg-grid  간격 눈금 격자. 셀이 --grid-cell = --space-6
            파이프라인 · 수치. "모든 값이 토큰에서 나온다"가 배경에서도 성립
  .bg-glow  광원 블롭. 섹션마다 --glow-x 로 자리를 옮긴다
            재료 · 설치. 히어로의 빛이 마지막 CTA 에서 돌아온다

실측 3층 (스크롤 1000 -> 1600)
  격자 +14.9px  ·  산출물 -14px  ·  단계명 0
  한 층은 반드시 고정이다. 전부 움직이면 그냥 흔들리는 페이지다

그리드 wide 트랙을 좁은 화면에서 닫는다
  고정 10rem 이라 모바일에서도 좌우 80px 이 살아남아, 390px 에서 본문이
  x=112 부터 시작하고 폭이 278px 로 줄어 제목이 네 줄로 접혔다.
  --wide-pad 를 clamp 로 바꿔 닫았다. 이제 전부 x=32 에서 시작하고 두 줄이다

파이프라인 정렬선을 하나로
  제목 x=371 / 리드 x=435 / 스파인 x=143 이던 것을 전부 x=183 으로.
  제목·리드·브리프를 스파인과 같은 컨테이너에 넣었다

히어로 높이를 콘텐츠에 맞춘다
  모바일에서 콘텐츠 390px 에 736px 을 씌워 여백이 화면의 41% 였고
  다음 섹션이 화면 밖으로 밀려 스크롤 단서까지 사라졌다.
  모바일 min-height 제거(736 -> 554), 데스크톱 88dvh -> 78dvh
  배경 마스크 62% -> 82% 로 늘려 하단의 죽은 띠를 없앴다

스킬 motion.md 에 "패럴랙스에는 기준면이 필요하다" 를 추가.
페이지가 주장하는 것을 배경으로 그리는 표와, 한 층은 고정이라는 규칙.

390px / 1440px 실렌더: 가로 스크롤 0, 대비 실패 0. 테스트 22개 통과
This commit is contained in:
Yun Chan 2026-08-20 14:25:02 +09:00
parent 9c9ab2de7e
commit 0e03bbafac
9 changed files with 158 additions and 19 deletions

View file

@ -55,7 +55,7 @@ const { c, ui } = Astro.props;
position: relative;
isolation: isolate;
overflow: clip;
min-height: min(88dvh, 52rem);
min-height: min(78dvh, 46rem);
display: grid;
align-content: center;
padding-block: calc(var(--header-h) + var(--space-6)) var(--space-7);
@ -72,8 +72,8 @@ const { c, ui } = Astro.props;
/* 아래로 갈수록 지운다. black 은 색이 아니라 마스크 알파다.
없으면 히어로 하단에서 배경이 가로선으로
뚝 끊겨 다음 섹션과 두 장의 종이처럼 보인다. */
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 62%, transparent 96%);
mask-image: linear-gradient(to bottom, black 0%, black 62%, transparent 96%);
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
mask-image: linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
}
.hero-fallback,
.hero-canvas,
@ -143,7 +143,10 @@ const { c, ui } = Astro.props;
}
@media (max-width: 900px) {
.hero { min-height: min(94dvh, 46rem); }
/* min-height 를 걸지 않는다. 콘텐츠가 390px 인데 736px 을 씌우면
여백이 화면의 41% 를 먹고, 다음 섹션이 화면 밖으로 밀려나
스크롤할 것이 있다는 단서까지 사라진다. */
.hero { min-height: 0; }
.hero-grid { grid-template-columns: 1fr; gap: var(--space-5); }
.hero-title { max-width: 100%; }
/* 세로 화면에서는 글자가 위쪽 절반을 다 쓴다. 가로 방향 스크림으로는

View file

@ -7,7 +7,7 @@ const { c, ui } = Astro.props;
---
<section id="install" class="full install" aria-labelledby="install-h">
<section id="install" class="full install bg-glow" style="--glow-x: 30%" aria-labelledby="install-h">
<div class="install-grid">
<div class="install-head">
<h2 id="install-h">{c.h2}</h2>

View file

@ -10,7 +10,7 @@ const { c } = Astro.props;
배경은 three.js 셰이더 플레인, 전경은 SVG 필터를 실제로 먹인 타일 3장.
둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다.
*/}
<section class="section materials" aria-labelledby="mat-h">
<section class="section materials bg-glow" style="--glow-x: 70%" aria-labelledby="mat-h">
<h2 id="mat-h" class="reveal">{c.h2}</h2>
<p class="lead mat-lead reveal">{c.lead}</p>

View file

@ -19,7 +19,7 @@ const rest = c.items.filter((m) => !m.feature);
흔들리면 그 값도 흔들리는 것처럼 읽힌다. 계기판의 바늘은 떨지 않는다.
움직임이 의미를 만드는 자리가 있으면, 움직이지 않는 것이 의미인 자리도 있다.
*/}
<section id="metrics" class="section metrics" aria-labelledby="metrics-h">
<section id="metrics" class="section metrics bg-grid" aria-labelledby="metrics-h">
<div class="head">
<p class="eyebrow reveal">{c.eyebrow}</p>
<h2 id="metrics-h" class="reveal">{c.h2}</h2>

View file

@ -14,17 +14,20 @@ const { c } = Astro.props;
③ 의미 — <ol role="list"> — 화면에 번호는 없지만 보조기술은 "6개 중 n번째"로 읽는다
role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다
*/}
<section id="pipeline" class="section" aria-labelledby="pipeline-h">
<p class="eyebrow reveal">{c.eyebrow}</p>
<h2 id="pipeline-h" class="reveal">{c.h2}</h2>
<p class="lead pipeline-lead reveal">{c.lead}</p>
<div class="brief reveal"><div class="brief-core">
<span class="brief-label">{c.briefLabel}</span>
<p class="brief-text">{c.brief}</p>
</div></div>
<section id="pipeline" class="section bg-grid" aria-labelledby="pipeline-h">
{/* 제목·리드·브리프·스파인을 한 컨테이너에 담는다.
섹션 기본 그리드에 두면 제목은 main 칼럼의 음수 마진으로 x=371,
리드는 x=435, 스파인은 x=143 에 서서 정렬선이 셋이 된다. */}
<div class="full spine-wrap">
<p class="eyebrow reveal">{c.eyebrow}</p>
<h2 id="pipeline-h" class="reveal">{c.h2}</h2>
<p class="lead pipeline-lead reveal">{c.lead}</p>
<div class="brief reveal"><div class="brief-core">
<span class="brief-label">{c.briefLabel}</span>
<p class="brief-text">{c.brief}</p>
</div></div>
<ol role="list" class="spine">
{
c.stages.map((s) => (
@ -43,9 +46,13 @@ const { c } = Astro.props;
</section>
<style>
/* 컨테이너는 넓어도 읽는 것은 measure 안에 둔다 */
.pipeline-lead {
margin-bottom: var(--space-6);
max-width: var(--measure);
}
.brief { max-width: var(--measure); }
#pipeline-h { max-width: 24ch; }
/* 스파인은 페이지의 본체다. main 칼럼(561px)에 가두면
페이지에서 가장 긴 섹션이 가장 좁아진다.

View file

@ -139,9 +139,9 @@ section[id] { scroll-margin-top: calc(var(--header-h) + var(--space-4)); }
display: grid;
grid-template-columns:
[full-start] minmax(var(--gutter), 1fr)
[wide-start] minmax(0, var(--space-8))
[wide-start] minmax(0, var(--wide-pad))
[main-start] minmax(0, var(--measure)) [main-end]
minmax(0, var(--space-8)) [wide-end]
minmax(0, var(--wide-pad)) [wide-end]
minmax(var(--gutter), 1fr) [full-end];
}
.page > * { grid-column: main; }
@ -415,6 +415,61 @@ a:hover { color: var(--accent); }
}
}
/* ---- 배경 레이어 --------------------------------------------
패럴랙스는 깊이 차가 보여야 성립한다. 배경이 단색이면 앞의 것이
px 어긋나 봐야 아무도 느낀다 기준면이 없기 때문이다.
그래서 배경을 깐다. 다만 이미지가 아니라 페이지가 주장하는 ,
간격 눈금 자체를 격자로 그린다. --grid-cell --space-5 . */
.bg-grid,
.bg-glow {
position: relative;
isolation: isolate;
}
.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;

View file

@ -128,6 +128,18 @@
모바일 구간 32px 깔고 넓은 화면에서만 40px 까지 연다. */
--gutter: clamp(2rem, 1.4rem + 1.8vw, 2.5rem);
/* 배경 격자의 . 간격 스케일에서 가져온다
장식용 격자가 아니라 페이지가 실제로 쓰는 눈금이어야
"모든 값이 토큰에서 나온다" 주장이 배경에서도 성립한다. */
--grid-cell: var(--space-6);
/* main 칼럼 바깥의 여유 (wide 트랙). 넓은 화면에서 제목과 넓은 블록이
본문보다 밖으로 나갈 자리를 만든다.
값을 고정으로 두면 모바일에서도 좌우 80px 살아남아, 390px 화면에서
본문이 x=112 부터 시작하고 폭이 278px 줄어든다 제목이 줄로 접힌다.
좁은 화면에서는 0 으로 닫는다. */
--wide-pad: clamp(0rem, -3.7rem + 15.2vw, 10rem);
/* 헤더가 차지하는 높이. 있는 헤더라 문서 흐름에서 빠져 있고,
그래서 히어로 상단 여백과 앵커 목적지가 값을 알아야 한다.
간격 토큰을 clamp 바꾸는 순간 관계가 끊겨 h1 헤더 뒤로