/* 홈 — 밝은 판 하나 위에 제목과 명함 뭉치가 함께 놓인 화면.
 *
 * **이 화면의 원칙**
 *   화면 위의 조판 규칙은 명함이 이미 쓰고 있는 규칙에서 가져온다.
 *   이 화면은 인쇄물이 나오는 자리이지, 인쇄물을 소개하는 자리가 아니다.
 *
 * 그래서:
 *  1. **화면에서 가장 검은 것은 제목이 아니라 명함 뒷면이다.** 제목은 한 단 연하다 —
 *     이 도구의 주인공은 UI 가 아니라 인쇄물이다.
 *  2. 색은 주황 한 계열뿐이고, **배경과 주변 요소에만** 쓴다. 명함 자체는 흑백에 주황
 *     한 점이라, 배경이 화려해지면 주인공이 밀린다.
 *  3. 물리적 객체는 명함뿐. 그림자와 둥근 모서리는 카드에만 쓴다(버튼·아이콘에는 없다).
 *  4. 화면은 **한 화면에서 끝난다.** 어떤 폭·높이에서도 스크롤이 생기지 않아야 한다.
 */

:root {
    /* ── 밝은 판 두 단 ────────────────────────────────────────────────
       크림(#F4F1EA 계열)이 아니다 — 인쇄물 주제에 흔한 선택이고 채도가 그만큼 오르면
       흰 명함이 «누런 종이 위의 흰 종이» 가 된다.

       대신 **카드가 놓인 가운데는 중성으로 두고 가장자리로 갈수록만 따뜻해진다.**
       그래서 주인공(흰 종이)의 배경은 끝까지 깨끗하고, 온도는 화면 둘레에만 남는다. */
    --ground: #F5F6F7;        /* 카드 뒤 — 가장 밝은 곳. 여기만 중성이다 */
    --ground-edge: #E5E2DE;   /* 가장자리. 중앙과 명도차 약 5%, 그리고 **조금 따뜻하다** */

    /* ── 검정 네 단 ──────────────────────────────────────────────────
       가장 진한 값(--ink)은 **명함 뒷면**의 것이다. 제목은 그보다 한 단 연하다. */
    /* 순검정. **검은 띠(하단 25%)의 면**이다 — 화면에서 가장 진한 것은 이제 명함
       뒷면이 아니라 이 면이다. 뒷면 카드(--ink)는 밝은 판 위의 «가장 진한 한 점» 이고,
       띠는 판 자체가 검은 층이라 둘의 역할이 다르다: 카드가 띠보다 한 단 밝아도
       주인공 자리를 잃지 않는다(카드는 밝은 판 위에 놓여 있으므로). */
    --ink-0: #000;

    --ink: #17181A;           /* 검은 뒷면 카드 = 밝은 판 위에서 가장 진한 한 점 */
    --ink-title: #26282B;     /* 제목 */
    /* 서브헤딩 단. 한 단 밝혔다(#55585C → #5C6064, 판 위 대비 6.61 → 5.86:1) — 제목이
       화면을 이미 다 말하므로 설명 줄은 «읽히되 먼저 읽히지 않는» 쪽이 맞다. 다음 단
       (--ink-3 #7A7E83)까지 가면 3.78:1 로 떨어져 본문 크기에서 눈이 애쓴다.
       이 단을 쓰는 곳은 설명 줄(.sub)과 상단 이름 줄(.who__name) 둘이고, 둘 다 같은
       역할이라 값을 따로 두지 않는다. */
    --ink-2: #5C6064;
    --ink-3: #7A7E83;         /* 보조 (밝은 판) */

    /* ── 검은 판 위의 글자 두 단 ─────────────────────────────────────
       밝은 판의 잉크를 그대로 쓸 수 없다. 특히 보조 단(--ink-3 #7A7E83)은 검정 위에서
       대비 4.0:1 로 «읽히긴 하는데 눈이 애쓰는» 값이다 — 설명이 안 읽히는 문제가 바로
       그 자리에서 났었다. 그래서 검은 판에서는 두 단을 **따로** 둔다(9.2:1 / 7.1:1). */
    --on-ink: #F2F3F4;        /* 검은 판 위의 제목 */
    --on-ink-2: #A9AEB4;      /* 검은 판 위의 설명 */

    /* ── 주황 두 단 ──────────────────────────────────────────────────
       tokens.css 의 --accent(#ED6E00)는 **인쇄물 위에서만** 쓴다(명함 SVG 안의 직함 등).
       그 값은 실제로 인쇄되는 색이라 손대지 않는다. 아래 둘은 **화면 쪽**의 단계다.

       **역할을 단계에 고정한다.** 같은 성격의 요소는 항상 같은 단으로 간다 — 요소마다
       톤이 다르면 색이 «위계» 를 말하기 시작하고, 그러면 동등한 것들이 동등해 보이지 않는다.

         mid   표시하는 것 — 포커스 링. (기능 아이콘과 브랜드 줄은 여기서 떠났다:
               아이콘은 검은 띠 위로 옮겨 가며 제 값(#DC7936)을 갖게 됐고, 브랜드 줄은
               명함 위 직함과 같아야 해서 --accent 를 읽는다.)
         glow  빛 — 배경의 온기. 잉크가 아니라 «빛» 이라 밝고, 항상 알파와 함께 쓴다

       밝은 바탕에서 #ED6E00 은 채도가 튀어 **제목보다 먼저 읽힌다**. 그래서 화면의 기준
       오렌지(mid)는 인쇄용보다 한 단 깊다 — 액센트가 액센트 자리에 머물게.

       한때 여기 단이 넷이었다. peach 는 구분선을 회색으로 되돌리며, amber 는 «빛» 이
       알파와 함께만 쓰여 --glow 로 합치며, burnt 는 주 버튼을 검정으로 정하며 각각
       쓰는 곳이 없어져 지웠다 — 쓰이지 않는 단을 남겨 두면 다음 사람이 «있는 줄 알고»
       되살릴 자리가 된다. 지금 화면에서 주황이 잉크로 나타나는 곳은 제목 위 브랜드 줄과
       아이콘의 획뿐이고, 나머지는 배경의 빛이다. */
    --o-mid: #D45F00;


    /* 빛은 hex 가 아니라 rgb 삼원으로 둔다. 배경 글로우 세 겹과 검은 카드의 림라이트가
       **같은 빛**이어야 하는데 전부 알파를 달리해 쓰기 때문이다(색을 따로 적으면
       여러 곳이 조용히 갈린다). 색 자체는 앰버(#EC903E)다. */
    --glow: 236, 144, 62;

    /* 제도 잉크 — 배경의 격자. **회색이 아니라 주황이다.** 색상각 25° 로 브랜드 오렌지
       (--o-mid, 27°)와 같은 계열이라, 판이 오른쪽으로 갈수록 따뜻해져도 선이 혼자
       차갑게 뜨지 않고 배경의 온기와 한 벌로 읽힌다.

       **명도는 옛 웜그레이(122,98,80)와 같게 맞춰 둔 값이다** — L 103 → 104. 색만
       옮기고 밝기를 유지했으므로 아래 격자 두 겹의 알파(0.145 / 0.035)와 `.room::before`
       주석의 보강량 계산이 그대로 성립한다. 채도를 올리더라도 L 을 이 근처에 두면
       «격자가 먼저 보이는» 실패를 피할 수 있다.

       항상 알파와 함께 쓰므로 rgb 삼원으로 둔다. */
    --plan-ink: 178, 84, 16;

    /* 그림자의 «잉크». 순수 검정으로 그리면 밝은 판 위에서 회색이 아니라 «구멍» 이 된다.
       **차가운 회색도 아니다** — 실제 종이 그림자는 주변 색을 머금으므로 아주 조금
       따뜻한 쪽으로 옮겨 둔다(23,24,26 → 34,28,24). 버튼 테두리·구분선도 같은 값을 쓰므로
       화면 전체의 «회색» 이 한 번에 같은 온도가 된다. */
    --shade: 34, 28, 24;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

/* 100vh 가 아니라 100dvh 다 — 모바일 브라우저의 주소창이 접히면 100vh 는 화면보다
   커져서 «스크롤 없음» 이 깨진다. dvh 를 모르는 브라우저를 위해 vh 를 먼저 적는다. */
body.home {
    height: 100vh;
    height: 100dvh;
    margin: 0;
    overflow: hidden;
    font-family: var(--sans);
    font-weight: 400;
    font-size: 15px;
    line-height: 1.55;
    color: var(--ink-title);
    background: var(--ground);
}

:focus-visible {
    outline: 2px solid var(--o-mid);
    outline-offset: 2px;
}

/* ── 판 ──────────────────────────────────────────────────────────────
 *
 * 카드가 놓인 자리 뒤가 가장 밝고 가장자리로 갈수록 아주 조금 어둡다. **조명이지
 * 장식이 아니다** — 비네팅으로 보이면 실패이고, 공간감만 남으면 된다.
 *
 * 원근은 여기 하나에만 있다. 요소마다 따로 주면 각자의 소실점이 생겨 셋이 서로 다른
 * 방에 있는 것처럼 보인다.
 *
 * overflow 를 자른다: 카드가 화면 **위쪽 바깥**에서 떨어져 내려오므로, 자르지 않으면
 * 낙하 시작 지점 때문에 스크롤 막대가 생긴다.
 */
.room {
    position: relative;
    height: 100%;
    overflow: hidden;

    /* 격자 한 칸. **두 겹이 이 값을 함께 쓴다**(`.plan` 과 따뜻한 쪽 보강 `::before`) —
       각자 적으면 두 겹의 칸이 조용히 어긋나 격자가 두 벌로 보인다.

       칸 하나가 **명함 판형**이다(1.7407:1 — tokens.css 의 --card-ratio 와 같은 비율).
       임의의 정사각 모눈이 아니라 이 화면이 다루는 물건의 비율이라, 카드가 격자 위에
       얹히면 칸에 맞아떨어진다. 칸 폭은 화면을 따라 자라고 높이는 **폭에서 판형으로
       나온다** — 두 값을 따로 적으면 화면 크기가 바뀔 때 칸이 명함 비율을 잃는다. */
    --cell-w: clamp(58px, 5.2vw, 86px);
    --cell-h: calc(var(--cell-w) / 1.7407);

    /* 격자의 **초점** — 카드 셋의 무게중심이다(.stack 56/55 · .fly--b 65/38 ·
       .fly--a 77/23 의 평균). 격자는 여기서 가장 진하고 **멀어질수록 옅어진다**.
       카드 자리를 옮기면 여기도 함께 옮긴다 — 어긋나면 «격자가 진한 자리» 와 «카드가
       놓인 자리» 가 갈리고, 그러면 격자가 카드를 받치는 눈금이 아니라 딴 무늬가 된다.
       (좁은 화면에서 카드 배치가 바뀌는 두 구간은 아래 미디어쿼리에서 다시 잡는다.) */
    --grid-focus: 66% 39%;
    perspective: 1600px;
    perspective-origin: 58% 42%;
    background:
        /* 빛은 **오른쪽**에 있고 왼쪽은 깨끗한 흰 면으로 남는다.
           카드가 오른쪽에 있으므로 주인공이 따뜻한 면 위에 놓이는데, 그것이 오히려
           갈라 준다: 카드의 면은 순백(#FFF)과 순검정(#17181A) 그대로이고 **주변만**
           따뜻해지므로 둘의 차이가 더 벌어진다.

           **덩어리 셋으로 흩어 놓는다.** 하나만 쓰거나 linear-gradient 로 그으면 왼쪽에서
           오른쪽으로 «자로 그은» 띠가 되어, 색이 아니라 그라디언트가 먼저 보인다.
           크기·자리·세기가 다른 셋이 겹치면 경계가 서로를 흩어 어디서 시작하는지 모르게
           된다.

           **자리는 화면이 아니라 «밝은 판» 을 기준으로 잡는다.** 아래 25% 가 검은 띠가
           된 뒤로 세 겹의 중심(88% · 104%)이 전부 띠 뒤로 들어가, 화면에 남은 것은 빛의
           **바깥 꼬리**뿐이었다 — 온기가 거의 사라졌다. 중심을 판 안으로 끌어올리고
           (88%→62%, 104%→76%) 세기를 한 단 올려(0.16→0.20) 되살린다. 판이 좁아졌으므로
           같은 세기로는 같은 온기가 나오지 않는다.

           **띠 비율을 고치면 여기 자리도 함께 고친다** — 세 겹은 «화면 아래» 가 아니라
           «판 아래» 를 맡는 것이다. */
        radial-gradient(74% 58% at 82% 62%,
                        rgba(var(--glow), 0.20) 0%, rgba(var(--glow), 0.08) 46%,
                        rgba(var(--glow), 0) 80%),
        /* 둘째 겹은 **카드가 놓인 오른쪽 위**를 맡는다. 카드가 따뜻한 면 위에 놓여야
           «빛 안에 있는 종이» 가 되고, 순백인 면과의 차이도 그만큼 벌어진다. */
        radial-gradient(62% 66% at 95% 24%,
                        rgba(var(--glow), 0.155) 0%, rgba(var(--glow), 0.055) 48%,
                        rgba(var(--glow), 0) 82%),
        /* 셋째 겹은 **띠 바로 위**의 온기다. 검은 면과 밝은 판이 맞닿는 선에서 판이
           식어 있으면 경계가 «잘린 자리» 로 보인다 — 그 선 위에 빛을 조금 남긴다. */
        radial-gradient(88% 44% at 48% 76%,
                        rgba(var(--glow), 0.11) 0%, rgba(var(--glow), 0.035) 46%,
                        rgba(var(--glow), 0) 76%),
        /* 바탕. **가장 밝고 가장 중성인 곳이 왼쪽**이고, 오른쪽으로 갈수록 조금 어둡고
           조금 따뜻해진다. 중심을 왼쪽 위(16% 32%)에 두어 이 흐름 자체도 대각이다 —
           수평으로 두면 위 세 겹까지 합쳐 결국 «좌→우 띠» 로 보인다. */
        radial-gradient(108% 88% at 16% 27%,
                        #FDFDFD 0%, #FAFAFA 32%, #F0EDE9 66%, var(--ground-edge) 100%);
}

/* 미세한 그레인. 넓은 면에 걸친 옅은 그라디언트는 8비트 화면에서 **띠(밴딩)** 로
   보이는데, 픽셀 단위의 잡음이 그 경계를 흩어 없앤다. 무늬로 읽히면 안 되므로 농도는
   2.6% 다 — 밝은 판에서는 어두운 판보다 잡음이 더 잘 보여 세기를 낮춘다.
   움직이지 않는다. 판 위의 모든 것 위에 얹혀 종이 질감을 한 벌로 만든다. */
/* 따뜻한 쪽의 격자 보강. 목표는 «오른쪽을 더 진하게» 가 아니라 **밝은 쪽과 따뜻한 쪽의
   세기가 눈에 같게** 만드는 것이다.

   보강량은 **명도차 계산이 아니라 실측**에서 나온다. 한때 여기 L(ink)=102 기준의
   명도 계산이 있었고 «따뜻한 쪽은 0.90 배로만 떨어진다» 고 봐서 +0.035 를 얹었다.
   잉크가 웜그레이에서 주황(--plan-ink)으로 바뀌며 그 전제가 깨졌다 — 주황 선은 흰
   바탕에서 «어둡다» 에 더해 «색이 다르다» 로도 보이지만, 주황 바탕에서는 그 색차가
   사라지고 명도차만 남는다. 명도만 재면 안 보이는 손실이라 ΔL 계산으로는 잡히지 않는다.

   그래서 렌더한 화면을 격자 끈 판과 픽셀로 빼서 ΔE2000(명도+색차)으로 쟀다.
   알파 1 단위가 만드는 ΔE 는
     흰 바탕(b*≈0)   k = 33.8
     주황 바탕(b*≈14) k = 23.9      ← 같은 잉크가 29% 덜 보인다
   보강 겹의 마스크 실효값은 주황 쪽 0.95, 흰 쪽 0.29(0 이 아니다 — 이 겹은 흰 쪽에도
   조금 걸린다). 두 쪽이 같은 ΔE 4.7 이 되는 연립해가 A=0.114 / B=0.087 이다.
   실측 편차폭 22% → 12%.

   **흰 쪽 마스크가 0.29 라는 게 핵심이다.** 이걸 0 으로 보고 B 만 올리면 흰 쪽도 같이
   올라가 «흰 쪽을 낮춘다» 는 목적이 상쇄된다. B 를 만지면 A 도 함께 다시 풀어야 한다.
   재측정: `.plan` 만 켠 판·둘 다 켠 판·둘 다 끈 판 세 장을 찍어 k 와 마스크를 다시 뽑는다.

   **`.plan` 의 자식이 될 수 없다.** 마스크는 요소와 그 자손에 함께 걸리므로, `.plan`
   안에 두면 `.plan` 의 거리 페이드에 이 겹까지 한 번 더 깎인다. 그래서 형제로 둔다.

   칸 값은 `.room` 에서 읽고 두 겹 다 `inset: 0` 이라 **위상이 같다** — 선이 겹쳐 진해질
   뿐 두 벌로 보이지 않는다.

   마스크는 두 겹을 **교차**시킨다: ① 카드에서 멀어지는 거리(`.plan` 과 **같은 원**) —
   보강도 함께 옅어져야 «멀리 있는데 오른쪽만 진한» 자리가 생기지 않는다. ② 판이
   따뜻한 자리(배경 빛 중 가장 센 82% 62% 에 맞춤). 빛이 옮겨가면 여기도 함께 옮긴다. */
.room::before {
    /* 격자를 뺀 뒤로 보강할 대상이 없다. 규칙은 남겨 둔다 — 격자를 되살리면
       여기 display 한 줄만 지우면 된다. */
    display: none;
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background-image:
        repeating-linear-gradient(to right,
            rgba(var(--plan-ink), 0.087) 0 0.5px, transparent 0.5px var(--cell-w)),
        repeating-linear-gradient(to bottom,
            rgba(var(--plan-ink), 0.087) 0 0.5px, transparent 0.5px var(--cell-h));
    -webkit-mask-image:
        radial-gradient(circle 52vw at var(--grid-focus),
            #000 0%, rgba(0, 0, 0, 0.5) 55%, transparent 92%),
        radial-gradient(74% 80% at 84% 62%,
            #000 0%, rgba(0, 0, 0, 0.5) 56%, transparent 90%);
    mask-image:
        radial-gradient(circle 52vw at var(--grid-focus),
            #000 0%, rgba(0, 0, 0, 0.5) 55%, transparent 92%),
        radial-gradient(74% 80% at 84% 62%,
            #000 0%, rgba(0, 0, 0, 0.5) 56%, transparent 90%);
    -webkit-mask-composite: source-in;
    mask-composite: intersect;
}

.room::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 9;
    pointer-events: none;
    opacity: 0.026;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── 판 위의 격자 ────────────────────────────────────────────────────
 *
 * 인쇄 조판·제도 도면의 시각 언어다. 빈 자리가 «비었다» 가 아니라 **«작업 영역»** 으로
 * 읽히게 하는 것이 목적이고, 그래서 장식 도형이나 추상 무늬가 아니라 눈금이다.
 *
 * **눈에 띄면 실패다.** 화면을 처음 봤을 때 격자가 먼저 보이면 진한 것이고, 카드·글자
 * 자리에서는 더 옅어지거나 가려져야 한다.
 *
 * 층은 `.room` 안에서 z 0 — 판의 조명 **위**에 얹히고 글자(z 5)·카드(z 2~4) **아래**로
 * 들어간다. 카드가 지나가는 자리를 덮는 것이 곧 «가려짐» 이다.
 */
.plan {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;

    /* 0.5px. 1px 로 두면 이 밀도에서 «모눈종이» 가 되어 배경이 먼저 보인다. DPR 2 화면
       에서는 물리 1픽셀이라 또렷하고, DPR 1 에서는 브라우저가 알아서 흐리게 깔아 준다 —
       둘 다 원하는 세기다.

       **알파(0.114)는 단독으로 정한 값이 아니다.** 판의 왼쪽·가운데는 거의 순백이라 같은
       잉크가 가장 잘 보이는 자리다. 따뜻해지는 오른쪽·아래에서는 같은 값이 배경에 묻히는데,
       그것을 이 겹에서 올려 해결하지 않는 것은 그대로다 — 올리면 밝은 쪽이 먼저
       «모눈종이» 가 된다. 그쪽은 **따뜻한 쪽을 보강하는 겹**(`.room::before`)이 맡는다.

       두 겹은 «밝은 쪽과 따뜻한 쪽의 ΔE 를 같게» 라는 한 조건의 연립해라서 **짝으로 움직인다**
       (0.114 / 0.087). 한쪽만 바꾸면 균형이 깨진다 — 근거와 재측정 방법은 `.room::before` 주석. */
    background-image:
        repeating-linear-gradient(to right,
            rgba(var(--plan-ink), 0.114) 0 0.5px, transparent 0.5px var(--cell-w)),
        repeating-linear-gradient(to bottom,
            rgba(var(--plan-ink), 0.114) 0 0.5px, transparent 0.5px var(--cell-h));

    /* 페이드는 **카드에서 멀어지는 거리**로만 정한다 — `circle` 이라 어느 방향으로든
       같은 거리에서 같은 세기다(타원으로 두면 가로·세로 페이드가 달라, 카드 오른쪽과
       아래가 서로 다른 세기로 옅어진다).

       한때 중심이 화면 가운데(54% 46%)였고 왼쪽 글 자리에 «구멍» 을 하나 더 뚫어 뒀다.
       그 조합의 부작용이 **오른쪽에서 격자가 깎이는 것**이었다 — 카드가 오른쪽에 있는데
       페이드는 화면 가운데를 기준으로 하니, 카드 주변이 가장 옅어졌다(실측: 오른쪽에서
       선-배경 명도차가 가운데의 절반). 초점을 카드로 옮기면 글 자리는 «구멍» 없이도
       거리만으로 옅어진다(1440×900 에서 마스크 계수 0.05). 그래서 마스크는 한 겹이다. */
    -webkit-mask-image:
        radial-gradient(circle 52vw at var(--grid-focus),
            #000 0%, rgba(0, 0, 0, 0.5) 55%, transparent 92%);
    mask-image:
        radial-gradient(circle 52vw at var(--grid-focus),
            #000 0%, rgba(0, 0, 0, 0.5) 55%, transparent 92%);
}

/* ── 글 덩어리 ───────────────────────────────────────────────────────
 *
 * 제목·한 줄 설명·버튼이 **한 흐름**에 있다. 셋에 각자 좌표를 주면 화면 크기가 바뀔
 * 때마다 사이 간격이 따로 놀고(제목은 vh 로 자라는데 간격은 % 라 어긋난다), 진입
 * 연출도 셋으로 쪼개진다.
 *
 * 세로 가운데에 선다. 가운데로 당기는 것은 `transform` 이 아니라 독립 속성 `translate`
 * 다 — `transform` 은 진입 연출이 쓴다.
 */
.lede {
    position: absolute;
    left: 6%;
    /* 화면의 가운데가 아니라 **밝은 판의 가운데**다. 아래 25% 는 검은 띠가 가져갔으므로
       글이 놓일 판은 위쪽 75% 이고, 그 가운데는 37.5% 다 — 화면 기준 42% 에 두면 글이
       판 안에서 아래로 처져 버튼이 띠에 가까워 보인다. */
    top: 38%;
    z-index: 5;
    translate: 0 -50%;
    /* 한 줄 제목이 들어갈 폭이다. 카드 기둥의 왼쪽 끝(.stack 53%)까지가 글의 몫이라
       여기가 상한이고, 제목 글자 크기는 이 폭에서 거꾸로 계산된다(아래 .title). */
    max-width: min(52vw, 680px);
}

/* ── 제목 ────────────────────────────────────────────────────────────
 *
 * 화면에서 가장 강한 요소다. **한 줄이다** — 「명함 발급 시스템」은 이름 하나이고,
 * 한때 「명함 발급 / 시스템」으로 끊어 두었더니 끊긴 자리가 두 덩어리로 읽혔다.
 *
 * 한 줄이라 **글자 크기가 폭에 종속된다.** 이 문구는 자간까지 넣어 약 7.65em 폭이므로
 * (한글 7 자 + 공백 둘), 글 덩어리 폭 W 에 대해 크기는 W / 7.65 를 넘을 수 없다.
 * vw 계수 5.6 과 상한 84px 이 그 계산의 결과다 — 상한을 올리려면 .lede 폭을 먼저 넓혀야
 * 하고, 반대로 폭을 줄이면 여기 두 값도 함께 내려야 한다. nowrap 이라 남으면 넘친다.
 */
.title {
    margin: 0;
    font-size: clamp(34px, min(5.6vw, 9.5vh), 84px);
    font-weight: 700;
    /* 접히지 않게 묶는다. 저절로 접히게 두면 폭이 조금 모자란 화면에서 「명함 발급 /
       시스템」이 되살아나고, 끊는 자리도 화면마다 달라진다. */
    white-space: nowrap;
    /* 자간은 0.005em. 큰 글자를 바짝 붙여 놓으면 획이 서로 붙어 «덩어리» 로 보이는데,
       이 글꼴은 굵기 700 에서 특히 그렇다(-0.035em → 0.005em). 행간은 한 줄이 된
       뒤로는 줄 사이가 아니라 제목↔설명 사이 여백으로만 작용한다. */
    line-height: 1.16;
    letter-spacing: 0.005em;
    color: var(--ink-title);
}

/* 제목 위의 브랜드 줄. 기능 아이콘과 **같은 단**(mid)이다 — 둘 다 «표시하는 것» 이라
   같은 톤이어야 한다.

   로고타입이 아니라 **글자**다. 이 자리는 제목과 한 문장으로 읽혀야 하는 자리여서,
   글꼴이 다른 마크가 끼면 문장이 끊긴다(마크는 상단에 따로 있다).

   크기·자간은 «먼저 읽히지 않을 만큼» 으로 잡는다: 14px 에 자간 0.14em. 자간을 좁히면
   대문자 일곱 자가 한 덩어리로 뭉친다. */
.lede__brand {
    display: block;
    margin-bottom: clamp(10px, 1.6vh, 16px);
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.14em;
    /* **오른쪽 명함 위의 직함(PM·FDE)과 같은 값**이다 — 둘 다 tokens.css 의 --accent
       (#ED6E00)를 읽는다. 화면에 브랜드 이름이 두 번(제목 위 줄 · 명함 위 조판) 나오는데
       색이 한 단 달라 «다른 주황» 으로 갈리던 것을 한 값으로 모았다.

       이 값은 **인쇄되는 색**이라(명함 SVG 안의 직함) 여기서 고치지 않는다 — 브랜드
       줄을 다시 떼어내고 싶으면 --o-mid 로 되돌린다. 위 「주황 두 단」 주석이 경고하듯
       밝은 바탕에서 --accent 는 --o-mid 보다 채도가 높아 더 앞으로 나온다. */
    color: var(--accent);
}

/* ── 서브헤딩 · 버튼 ─────────────────────────────────────────────────
   제목이 화면을 이미 다 말했으므로 여기서는 작게 한 줄이면 된다. */
.sub {
    margin: clamp(14px, 2.2vh, 22px) 0 0;
    /* **고정 18px.** 한때 clamp(14 … 17px) 로 화면에 따라 줄였는데, 이 줄은 제목이
       말하지 않은 것(무엇부터 무엇까지 하는 도구인지)을 유일하게 적는 자리라 좁은
       화면에서 먼저 작아지면 안 된다. 제목은 34px 까지 줄어들어도 여기와 겹치지 않는다. */
    font-size: 18px;
    color: var(--ink-2);
}

.actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    /* 제목 덩어리와 «누르는 것» 사이는 넉넉히 띄운다 — 붙어 있으면 버튼이 제목의 일부처럼
       읽히고, 이 화면에서 실제로 할 일 하나가 눈에 덜 띈다. */
    margin-top: clamp(28px, 4.4vh, 46px);
}

/* 이 화면에서 할 수 있는 일은 사실상 하나(로그인)다. 그래서 주 동작은 면·굵기로 앞에
   서고 보조는 테두리만 남는다 — 발급 화면의 «면을 가진 것은 하나» 와 같다.
   밝은 판이므로 명암이 뒤집힌다: 면을 가진 쪽이 **어둡다**. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 46px;
    padding: 0 20px;
    font-family: var(--sans);
    font-size: 16px;
    font-weight: 500;
    line-height: 1;
    white-space: nowrap;
    text-decoration: none;
    color: var(--ink-title);
    /* **흰 면**이다(투명 아님). 판이 오른쪽·아래로 갈수록 따뜻해지므로 투명한 버튼은
       놓인 자리의 색을 그대로 입어 «면이 없는 테두리» 로 보였다. 흰 면을 깔면 버튼이
       판 위에 놓인 물건이 되고, 주 동작(검은 면)과 같은 «면 대 면» 대비가 된다. */
    background: #FFF;
    border: 1px solid rgba(var(--shade), 0.22);
    cursor: pointer;
    transition: border-color 140ms ease, background-color 140ms ease;
}

.btn:hover { border-color: rgba(var(--shade), 0.48); }

.btn--go {
    padding: 0 30px;
    font-weight: 600;
    color: var(--ground);
    background: var(--ink);
    border-color: transparent;
}

.btn--go:hover { background: #000; }

/* ── 할 수 있는 일 넷 — 검은 띠 ──────────────────────────────────────
 *
 * 화면 **바닥에 붙은 검은 블록**이고 좌우로 꽉 찬다(full-bleed). 항목 사이에는 여전히
 * 선도 상자도 없다 — 두는 순간 «카드 격자» 가 되고, 그러면 순서가 사라지고 넷이 제목과
 * 같은 무게로 올라선다. 네 열의 폭이 같아서 아이콘이 한 눈금 위에 선다.
 *
 * **경계를 만드는 것이 선에서 면으로 바뀌었다.** 한때 줄 위에 1px 회색 실선을 그어
 * «여기부터는 다른 층» 을 말했는데, 검은 면 자체가 그 말을 더 분명하게 한다 — 선은
 * 지웠다(두면 검정 위의 회색 실선이 되어 면의 윗변과 이중으로 그어진다).
 *
 * 좌우 여백은 `left/right` 가 아니라 **padding** 으로 준다. 면은 화면 끝까지 가야 하고
 * 글은 다른 층과 같은 눈금(6%)에 서야 하므로, 그 둘을 한 요소에서 나누는 방법이 이것뿐이다.
 *
 * **검은 판에서는 색을 전부 다시 잡는다.** 밝은 판의 잉크를 그대로 옮기면 설명 단이
 * 특히 어둡게 잠긴다(--ink-3 는 검정 위에서 4.0:1) — 아래 세 값은 그래서 따로 있다.
 */
.feats {
    position: absolute;
    left: 0;
    right: 0;
    /* 바닥에 **붙인다**. 아래에 밝은 판이 한 줄이라도 남으면 «띠» 가 아니라 «떠 있는
       검은 상자» 가 된다. */
    bottom: 0;
    z-index: 5;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0 clamp(16px, 2.4vw, 40px);
    margin: 0;
    /* 띠는 화면의 **25%** 를 갖는다. 여기서 높이는 «내용에 맞춘 값» 이 아니라 판형이다 —
       화면을 위(밝은 판 75%)와 아래(검은 판 25%)로 가르는 비율 그 자체다. min-height 로
       두는 이유는 좁은 화면에서 넷이 2열 2행으로 접히며 내용이 25vh 를 넘을 수 있기
       때문이다(height 로 두면 그때 글이 면 밖으로 넘친다). */
    min-height: 25vh;
    /* 내용은 **위쪽에 붙인다.** 남는 공간은 아래에 둔다 — 30% 를 세로 가운데로 맞추면
       글이 화면 바닥에서 멀어져 띠가 «비어 있는 판» 으로 보인다. */
    align-content: start;
    /* 가로 6% 는 제목·버튼이 서는 눈금과 같은 값이다. 위 여백은 글이 윗변에 붙지 않을
       만큼만 — 아래는 남는 공간이 알아서 받친다. */
    padding: clamp(28px, 4.2vh, 52px) 6% clamp(20px, 3vh, 36px);
    /* **순검정**이다(--ink 아닌 --ink-0). 같은 화면에 검은 카드 뒷면이 있어 둘을 나란히
       보게 되는데, 띠가 카드와 같은 #17181A 면 «큰 카드» 처럼 읽힌다 — 띠는 카드가 아니고
       판이다. 한 단 더 내려 순검정으로 두면 면과 물건이 갈린다. */
    background: var(--ink-0);
    list-style: none;
}

.feat {
    display: grid;
    grid-template-columns: 26px 1fr;
    align-items: start;
    gap: 0 12px;
    min-width: 0;
    /* hover 는 **이 항목 하나만** 움직인다. ul 이나 부모에 걸면 다섯이 함께 반응해서
       «줄 전체가 눌린» 것처럼 보인다. */
    transition: transform 160ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.feat:hover { transform: translateY(-2px); }

/* 아이콘은 **오렌지**다. 넷이 같은 한 값을 쓴다 — 항목마다 톤이 다르면 색이 «위계» 를
   말하기 시작하고, 그러면 동등한 넷이 동등해 보이지 않는다.

   값은 **#DC7936**(L* 60.9 · C 61.8 · h 57.1°, 순검정 대비 6.83:1)이다.
   이것은 명함 직함 색(--accent #ED6E00)과 **밝기·색조가 같고 채도만 덜어낸 값**이다
   (L* 60.9 · h 57.1° 동일, C 82.3 → 61.8). 그래서 브랜드 줄(--accent)·명함 위 직함·
   아이콘 넷이 한 계열로 정렬되고, 아이콘만 한 단 차분하다.

   한때 브랜드 오렌지를 흰 바탕에 얹어 스포이드한 단색을 썼는데(#D45F00 70% → #E18F4D),
   **흰색으로 흐리면 색조가 오히려 노래진다** — LCh 색조가 56.1°(원색) → 61.9° 로 올라간다.
   흰색으로 흐리는 갈래를 버리고 LCh 로 직접 잡게 된 것이 그때의 결론이고, 지금 값도
   그 방식으로 골랐다.

   **색조 50° 아래는 주황이 아니라 «살구·산호» 로 읽히기 시작한다** — 이 자리의 하한이
   거기다. 위로는 채도가 상한을 만든다: 밝기를 유지한 채 채도를 올리면 R 채널이 255 에
   닿아(#FF8E3D 에서 C 70.1) 더 못 올라가고, 그 언저리부터 가는 획이 형광처럼 뜬다.

   값은 `/sandbox/icon-color/` 의 실시간 조절기에서 눈으로 고른 것이다. 바꿀 일이
   생기면 그 화면에서 홈 전체 미리보기를 보며 고르고 CSS 한 줄을 여기로 옮기면 된다.

   **한때 흰색이었고, 그전에는 주황이었다가 흰색으로 되돌린 이력이 있다** — 순검정 위의
   주황 넷이 강한 색 덩어리가 되어 띠가 «주황 줄» 로 읽혔기 때문이다. 지금 다시 주황으로
   두는 근거는 그때와 조건이 다르다는 것이다:
     · 제목(.feat__name)은 흰색(--on-ink)으로 남는다. 색을 지는 것은 **획뿐**이라
       덩어리가 아니라 점 넷으로 찍힌다.
     · 앰버(--glow 236,144,62, LCh 색조 63.2°)보다 **붉다**(57.1°). 이 자리에서 «누렇다»
       를 가르는 것은 밝기가 아니라 색조다 — 밝기는 --glow 와 비슷해도 색조가 내려가면
       노랑기가 빠진다.

   **배경 그라디언트와 «같은 색조» 가 아니다.** 판의 따뜻한 빛은 LCh 색조 70~74° 로 이
   아이콘(53.3°)보다 노란 쪽이다. 배경이 복숭아빛으로 부드럽게 보이는 것은 색조 때문이
   아니라 아주 밝고(L* 87~92) 채도가 낮기(C 5~13) 때문이라, 그 색조를 아이콘에 그대로
   옮기면 채도가 높은 획에서는 «노란 주황» 이 된다.
   띠가 다시 «주황 줄» 로 읽히기 시작하면 먼저 되돌릴 곳이 여기다. */
.feat__ico {
    display: block;
    margin-top: 1px;
    color: #DC7936;
}

.feat__ico svg { display: block; width: 26px; height: 26px; }

.feat__text { min-width: 0; }

.feat__name {
    display: block;
    font-size: 16px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--on-ink);
    transition: color 160ms ease;
}

/* hover 에서 한 단 더 밝아진다 — 밝은 판에서 «한 단 더 진해지던» 것의 반전이다. */
.feat:hover .feat__name { color: #FFF; }

/* **네 항목의 줄 수가 같아야 한다.** 하나만 줄 수가 다르면 그 칸의 아이콘만 위로 뜨고
   줄 전체가 어긋난 것처럼 보인다. 그래서 끊는 자리를 템플릿의 `<br>` 로 못 박았다 —
   폭에 맡기면 화면 크기마다 끊는 자리가 달라져 넷의 줄 수가 갈린다. */
.feat__note {
    display: block;
    margin-top: 3px;
    /* **고정 14px.** 좁은 폭에서 한 단 내리던 것을 없앴다 — 이 줄은 900px 이하에서
       아예 사라지므로(설명을 빼고 이름만 남긴다) 「조금 작게 해서 버티는」 구간이 실은
       필요하지 않았다. 넷의 줄 수(각 두 줄)는 템플릿의 <br> 가 잡는다. */
    font-size: 14px;
    line-height: 1.45;
    /* 제목과 위계는 두되 **확실히 읽히는** 명도다(7.1:1). 밝은 판의 보조 단(--ink-3)을
       그대로 옮기면 4.0:1 로 떨어져 «읽히긴 하는데 눈이 애쓰는» 글이 된다. */
    color: var(--on-ink-2);
}

/* ── 명함 세 덩어리 ──────────────────────────────────────────────────
 *
 * 바닥의 더미 → 검은 뒷면 → 흰 앞면 순으로 쌓인다. 조금씩 어긋나 있어 아래 것이
 * 가려지되 무엇인지는 알아볼 수 있다.
 *
 * 자리는 **중심점**으로 준다(셋의 크기가 달라 모서리 기준으로는 가운데가 어긋난다).
 * 가운데로 당기는 것은 `transform` 이 아니라 독립 속성 `translate` 다 — `transform` 은
 * 낙하 애니메이션이 쓰기 때문이다. 한 속성에 둘을 몰면 애니메이션이 자리 값을 덮어써서
 * 카드가 화면 왼쪽 위로 튄다.
 */
.stack, .fly {
    position: absolute;
    translate: -50% -50%;
    transform-style: preserve-3d;
    aspect-ratio: var(--card-ratio);
}

/* 세로 자리는 **검은 띠가 정한다.** 띠가 화면의 25% 를 가지므로 카드가 놓일 밝은 판은
   위쪽 75% 뿐이고, 셋의 중심을 그 안에서 잡는다 — 셋 다 한때 3% 아래에 있었는데
   그때는 더미와 검은 뒷면이 띠 안으로 들어갔다(실측). 셋의 **서로 간 간격은 그대로**
   두고 덩어리째 올린다: 어긋난 대각선이 이 장면의 조판이다.

   띠 비율을 고치면 여기 세 값도 함께 고친다 — 30% 였을 때는 각각 4% 더 위였다.

   판 안에서는 **위로 붙인다.** 셋의 중심이 판의 가운데(37.5%)를 지나 아래로 처지면
   카드가 띠에 눌린 것처럼 보이는데, 위쪽은 (상단 마크를 지운 뒤로) 비어 있어 그만큼
   여유가 있다 — 지금 셋의 중심은 55/38/23% 로, 가운데 카드가 판의 가운데에 선다. */
/* 카드 셋의 자리·크기·면내 회전. **그림자 겹(.cast)이 같은 값을 읽는다** — 그림자는
   이제 카드 안이 아니라 카드 **아래 층**(z 1)에 따로 있어서, 한쪽만 고치면 그림자가 제
   카드를 놓친다. 좁은 화면 규칙도 이 변수만 덮어쓴다(아래 미디어쿼리). */
.room {
    --s-l: 56%; --s-t: 55%; --s-w: min(22vw, 30vh, 304px);     --s-rz: -1deg;
    --b-l: 65%; --b-t: 38%; --b-w: min(27.5vw, 38.5vh, 386px); --b-rz:  5deg;
    --a-l: 77%; --a-t: 23%; --a-w: min(34vw, 46vh, 469px);     --a-rz: -4deg;
}
.stack,  .cast--s { left: var(--s-l); top: var(--s-t); width: var(--s-w); --pose-z: var(--s-rz); }
.fly--b, .cast--b { left: var(--b-l); top: var(--b-t); width: var(--b-w); --pose-z: var(--b-rz); }
.fly--a, .cast--a { left: var(--a-l); top: var(--a-t); width: var(--a-w); --pose-z: var(--a-rz); }

.stack  { z-index: 2;
          /* **X 는 크게, Y·Z 는 거의 0.** 셋을 한 덩어리로 «완만하게» 줄이는 문제가
             아니었다 — 축마다 하는 일이 다르다.

             `rotateX`(26deg)는 «위에서 내려다보는 각도» 다. 두께가 화면에 나타나는 양이
             이 각도로 정해진다(법선으로 민 두께 × sin) — 이것이 클수록 더미가 «책상에
             놓인 물건» 으로 읽힌다. 실측 아래 띠: 13deg 5.9px · 18deg 9.9px · 24deg
             14.8px · 26deg 16.4px · 30deg 19.5px. **띠를 조절하는 손잡이가 이 값이다.**
             떠 있는 두 장(11·9deg)보다 훨씬 큰 것이 맞다 — 그 둘은 공중에 있고 더미는
             바닥에 누워 있다.

             반대로 `rotateY`·`rotateZ` 는 두께를 **보여 주지 않으면서** 원근 오류만
             만든다. 법선으로 쌓인 아래 장들을 옆으로 밀어 **한쪽 모서리만 두껍게**
             하는 것이 그 값들이다(실측 왼쪽/오른쪽 들여쓰임: Y -4deg 8.3/0px ·
             -2deg 6.7/0.8px · -1deg 6.0/1.6px). 그래서 둘은 거의 지웠다.

             남은 좌우 차이는 소실점이 더미보다 오른쪽 위에 있어 생기는 **원근이 맞는
             비대칭**이다(지우려면 판 전체의 perspective-origin 을 옮겨야 하고, 그러면
             큰 카드 둘의 자세가 함께 바뀐다). X 를 올리면 띠가 두꺼워지는 만큼 이
             차이는 상대적으로 작아진다 — 13deg 에서 6.0/5.9 였던 «들여쓰임 대 띠» 가
             26deg 에서 5.7/16.4 다. */
          --pose-x: 26deg; --pose-y: -1deg; }
.fly--b { z-index: 3; --pose-x: 9deg;  --pose-y: 13deg; }
.fly--a { z-index: 4; --pose-x: 11deg; --pose-y: -13deg; }

/* 기울기 한 곳. 기본 자세(--pose-*)와 마우스 시차(--rx/--ry)를 **더해서** 쓴다 —
   두 겹으로 나누면 층이 하나 더 늘고, JS 가 자세 값을 알아야 한다(지금은 CSS 만 안다).
   translateZ 는 hover 로 카드가 앞으로 나오는 거리다. */
.stack__drop, .fly__drop { height: 100%; transform-style: preserve-3d; }

.stack__tilt, .fly__tilt {
    height: 100%;
    transform-style: preserve-3d;
    transform:
        translateZ(var(--lift, 0px))
        rotateX(calc(var(--pose-x) + var(--rx, 0deg)))
        rotateY(calc(var(--pose-y) + var(--ry, 0deg)))
        rotateZ(var(--pose-z));
    /* 마우스를 따라올 때의 «여운». 커서가 창 밖으로 나가면 JS 가 값을 지우고, 이
       transition 이 원래 자세로 되돌린다. */
    transition: transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ── 종이 ────────────────────────────────────────────────────────────
   큰 그림자는 여기 없다 — 바닥의 `__shade` 가 진다. 뒤집기(preserve-3d)를 하는 요소에
   box-shadow 를 크게 주면 뒷면의 그림자까지 함께 돌아 «두 겹으로 겹친 얼룩» 이 된다.
   여기 남는 것은 **종이 두께**(1px 경계)뿐이고, 밝은 판에서 흰 종이를 띄우는 것이
   그 한 겹이다. */
/* 가장자리는 **종이의 모서리**이지 «빛나는 테두리» 가 아니다.
   한때 여기에 주황 빛(--glow)을 한 겹 더 둘렀는데, 흰 카드 둘레에 주황 윤곽선이 생겨
   카드가 «테두리를 두른 물건» 으로 보였다. 온기는 --shade 자체가 이미 따뜻한 회색이라
   그 1px 로 충분하다 — 주변 빛은 **배경이** 말하고 카드는 종이로만 있는다. */
.paper {
    position: absolute;
    inset: 0;
    border-radius: 2px;
    overflow: hidden;
    background: var(--paper);
    box-shadow: 0 0 0 1px rgba(var(--shade), 0.11);
}

/* 뒷면은 검은 종이다. 밝은 판에서 **가장 진한 한 점**이라 면을 가르는 경계선이 필요
   없다. 가장자리의 림라이트는 «있다는 것을 눈치채지 못할» 세기까지만 남긴다 — 검은
   면 위에서는 같은 농도라도 훨씬 뚜렷하게 보인다(0.16 에서는 주황 테두리였다). */
.paper--black {
    background: var(--ink);
    box-shadow: 0 0 0 1px rgba(var(--glow), 0.06);
}

/* ── 종이처럼 보이게 — 조명 · 질감 ───────────────────────────────────
 *
 * 카드가 «벡터 도형» 으로 읽히던 이유는 셋이었다: 면이 완전히 평평하고, 표면에 질감이
 * 없고, 그림자가 고르게 흐렸다. 아래 두 겹이 앞의 둘을 맡고 그림자는 `__shade` 가 맡는다.
 *
 * **두께(옆면)는 넣지 않았다.** 시안 비교(/sandbox/paper-a)에서 단독 효과가 가장 컸지만
 * — 검은 카드 아랫변이 배경 176 대비 211 로 튀어 «흰 속심» 이 분명히 보였다 — 채택에서
 * 빠졌다. 되살릴 일이 생기면 그 시안의 box-shadow 두 줄이 전부다.
 *
 * 세기는 **세 겹이 함께 얹히는 것**을 전제로 잡았다. 하나만 쓸 때의 값(시안 B·C)보다
 * 낮고, 넷을 다 쓰던 값(시안 E)보다는 높다 — 가장 센 겹이 빠진 만큼 나머지가 올라간다.
 *
 * 겹 순서: .paper 안에는 그림(.paper__art)과 조판(.paper__layer)이 있다. 덧씌우는 겹은
 * 그 **위**로 와야 하므로 ::before 에도 z-index 를 준다 — ::before 는 첫 자식이라
 * z-index 가 없으면 그림 **아래**로 깔려 아무것도 안 보인다(실제로 그렇게 만들어 봤다).
 */

/* 조명. 판의 광원은 왼쪽 위다(그림자가 오른쪽 아래로 흐르는 것과 같은 전제). */
.paper::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    background: linear-gradient(var(--lit-angle, 150deg),
        rgba(255, 255, 255, var(--lit-hi, 0.17)) 0%,
        rgba(255, 255, 255, 0) 34%,
        rgba(var(--shade), calc(var(--lit-lo, 0.078) * 0.45)) 68%,
        rgba(var(--shade), var(--lit-lo, 0.078)) 100%);
}

/* 검은 면은 **균일한 검정이면 종이가 아니라 «구멍» 이다.** 밝은 면보다 변화 폭을 크게
   준다 — 검정 위에서는 같은 알파의 명암차가 훨씬 작게 보인다. */
.paper--black::before {
    background: linear-gradient(var(--lit-angle, 150deg),
        rgba(255, 255, 255, var(--lit-hi-b, 0.082)) 0%,
        rgba(255, 255, 255, 0.012) 38%,
        rgba(0, 0, 0, calc(var(--lit-lo-b, 0.27) * 0.5)) 70%,
        rgba(0, 0, 0, var(--lit-lo-b, 0.27)) 100%);
}

/* 표면 질감. **눈에 띄면 실패다** — 무늬로 읽히는 순간 종이가 오히려 더 가짜가 된다.
   알갱이가 개별로 보이지 않고 «면이 완전히 매끈하지는 않다» 는 인상만 남는 세기다.

   feTurbulence 출력은 색이 있는 잡음이라 그대로 쓰면 컬러 노이즈가 된다 — feColorMatrix
   로 채도를 0 으로 만들어 회색 결로 쓴다. 판 전체의 그레인(.room::after)과는 별개다:
   그쪽은 화면 전체에 깔린 망점이고 이건 카드 표면에만 붙는다. 결이 판보다 굵어야
   («baseFrequency» 가 낮아야) 카드가 «다른 재질» 로 갈린다. */
.paper::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 4;                 /* 조명 겹(z 3) 위 — 질감은 빛보다 위에 있는 표면이다 */
    pointer-events: none;
    opacity: var(--grain, 0.044);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.78' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23p)'/%3E%3C/svg%3E");
}

/* 검은 면에서는 회색 결이 «밝히는» 쪽으로만 작용해 같은 값이라도 더 도드라진다.
   한 단 낮춘다 — 안 낮추면 검은 카드만 먼지 앉은 것처럼 보인다. */
.paper--black::after { opacity: var(--grain-black, 0.034); }

.paper__art { display: block; width: 100%; height: 100%; }

/* 바탕 그림 위에 얹히는 조판. 좌표는 서버가 판형(pt) 그대로 준다 — viewBox 가 같은
   판형이라 변환이 없다(발급 화면의 .card__layer 와 같은 방식). */
.paper__layer {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

.paper__layer text { font-family: var(--sans); white-space: pre; }

/* ── 그림자 ──────────────────────────────────────────────────────────
 *
 * 광원은 **하나**다: 왼쪽 위, 고도 약 60°. 그래서 세 덩어리의 그림자가 전부 오른쪽
 * 아래로 지고, **밀려나는 양은 높이에 비례한다**(높이 뜬 카드일수록 더 멀리).
 *
 * 오른쪽의 --glow 겹을 «빛» 으로 읽지 말 것 — 그것은 밝기가 아니라 **온기**다(같은
 * 밝기에서 색만 따뜻해진다). 판이 실제로 가장 밝은 곳은 왼쪽 위(16% 27%)이고,
 * 그래서 광원도 왼쪽 위다. 한때 배경 주석은 «빛은 오른쪽», 그림자 주석은 «왼쪽 위»
 * 라 적혀 둘이 모순이었다.
 *
 * **방향은 --sx/--sy 한 곳에서만 나온다.** 예전에는 그림자 겹마다 좌우 인셋을 달리
 * 줘서 방향을 만들었는데, 카드마다 값이 어긋나 실측에서 fly--a 는 +3.5px(오른쪽),
 * fly--b 는 -4.6px(왼쪽), .stack 은 +26.4px 로 **방향도 크기도 제각각**이었다 —
 * 게다가 가장 높이 뜬 카드가 가장 덜 밀려 순서까지 거꾸로였다. 인셋은 좌우 대칭으로
 * 되돌리고(모양만 맡는다) 방향은 아래 --sx/--sy 가 혼자 맡는다.
 *
 * 한 덩어리에 두 겹이다 — 가까운 곳에 **짧고 진한 것**, 멀리 **넓고 옅은 것**.
 * 한 겹짜리 회색 그림자는 종이를 «스티커» 로 만든다.
 *
 * 카드에 box-shadow 로 붙이지 않고 **바닥에 따로 둔다.** 이유가 둘이다:
 *   ① 낙하·부유에서 그림자가 카드와 **다르게** 움직여야 한다(카드는 내려오고 그림자는
 *      좁아진다). box-shadow 로는 그 둘을 분리할 수 없다.
 *   ② 애니메이션은 transform·opacity 만 쓴다. box-shadow 는 그 둘이 아니다.
 *
 * **그림자는 카드 안에 두지 않는다.** 한때 각 카드 안(.fly__shade)에 있었는데, 그러면
 * 그 카드의 z-index(2~4) 안에 갇혀 **아래 카드 위에 그려진다** — 고길동(.fly--b)의
 * 그림자가 도우너(.stack) 종이의 82.8% 를 평균 20/255 로 덮고 있었다(실측). 카드가
 * 겹쳐 있는 구도라 값을 줄여서 피할 수 있는 문제가 아니다: 뜬 카드 바로 아래 바닥이
 * 곧 아래 카드가 놓인 자리다.
 *
 * 그래서 셋의 그림자를 **카드 전부보다 아래 층**(.casts, z 1)으로 내린다. 아래 카드에
 * 가려지는 부분은 그냥 가려지고, 판에 떨어지는 부분만 보인다.
 *
 * 층이 갈리면서 translateZ 도 필요 없어졌다 — 그것은 «3D 에서 기울어진 카드와 평평한
 * 그림자가 서로를 뚫는» 문제를 피하려던 것인데, 이제 그림자는 카드와 다른 평면 층에
 * 있어 애초에 만나지 않는다. 원근 축소분(≈0.94)을 상쇄하던 --shade-fix 도 그만큼
 * 낮춘다(1.08 → 1.01: 0.9357 × 1.08 = 1.011).
 */
.casts {
    position: absolute;
    inset: 0;
    z-index: 1;                 /* .bgfx(z 1)보다 뒤에 오므로 그 위, 카드(z 2~4) 아래 */
    pointer-events: none;
    /* 진입은 **여기 한 곳**에 건다. 각 .cast 에 걸면 키프레임의 transform 이 그림자의
       평상시 transform(--sx/--sy 이동)을 덮어써 그림자가 제자리를 잃는다. */
    animation: text-in 260ms ease-out backwards;
}

.cast {
    position: absolute;
    translate: -50% -50%;
    aspect-ratio: var(--card-ratio);
    transform: translate(var(--sx, 0), var(--sy, 0)) scale(var(--shade-fix));
    transition: transform 260ms ease, opacity 260ms ease;
}

.shade { position: absolute; border-radius: 7px; }

/* 멀리 · 넓고 옅게. 좌우 **대칭**이다 — 오른쪽으로 미는 일은 --sx 가 한다. */
.shade--far {
    left: -8%;
    right: -8%;
    top: 40%;
    bottom: -48%;
    background: rgba(var(--shade), var(--far-a));
    filter: blur(var(--far-b));
}

/* 가까이 · 짧고 진하게. 카드 바로 아래에 붙어 «닿아 있는 지점» 을 만든다. */
.shade--near {
    left: 0;
    right: 0;
    top: 62%;
    bottom: -18%;
    background: rgba(var(--shade), var(--near-a));
    filter: blur(var(--near-b));
}

/* 접지(接地). 기존 두 겹은 둘 다 «넓고 고르게 흐린 타원» 이라 **부유물의 그림자**로
   읽혔다. 실물의 그림자는 성질이 둘로 갈린다: 닿은 쪽은 좁고 진하고 **덜 흐리며**,
   먼 쪽은 넓고 옅고 많이 흐리다. 그래서 겹을 하나 더 두고, 기존 둘은 «먼 쪽» 을 맡아
   더 흐려졌다(아래 --far-b / --near-b 가 그만큼 커진 값이다).

   **비대칭이어야 한다.** 카드가 기울어 있으니 한쪽 변이 바닥에 가깝고 반대쪽은 멀다.
   좌우 대칭 타원은 «카드가 바닥과 평행하다» 고 말해 버려서, 기울어진 카드에서는 그
   자체가 어긋난 단서가 된다. --skew 는 각 카드의 rotateY 부호를 따른다. */
.cast::before {
    content: "";
    position: absolute;
    left: var(--ct-l, 10%);
    right: var(--ct-r, 2%);
    top: var(--ct-t, 70%);
    bottom: var(--ct-b, -6%);
    border-radius: 5px;
    background: rgba(var(--shade), var(--ct-a, 0.24));
    filter: blur(var(--ct-blur, 5px));
    transform: rotate(var(--pose-z, 0deg)) skewX(var(--skew, 0deg));
}

.shade--far  { transform: rotate(var(--pose-z, 0deg)) skewX(var(--skew, 0deg)); }
.shade--near { transform: rotate(var(--pose-z, 0deg)) skewX(calc(var(--skew, 0deg) * 0.6)); }

/* 높이가 그림자로 드러난다: 맨 위 흰 카드는 **넓고 옅게**, 바닥 스택은 **좁고 진하게**. */
/* 빛 각도는 **카드** 에 남는다 — 조명 겹(.paper::before)이 카드 안에 있기 때문이다.
   (그림자 값들은 위 .cast--* 로 옮겼다: 그림자가 카드 밖 층으로 나갔으므로.) */
.fly--a { --lit-angle: 154deg; }
.fly--b { --lit-angle: 145deg; }
.stack  { --lit-angle: 153deg; }

/* 빛 각도(--lit-angle)도 여기서 갈린다. 셋의 rotateX/Y/Z 가 달라 같은 빛을 다른
   각도로 받는다 — 셋에 같은 그라디언트를 주면 «같은 무늬를 세 번 붙인 것» 이 되어
   오히려 도형처럼 보인다.

   **값은 자세에서 역산한다.** 조명 겹은 .paper 안(= 회전하는 좌표계 안)에 그려지므로,
   화면에서 빛 방향이 고정되려면 로컬 각도가 자세를 상쇄해야 한다:

       local = atan2(x, -y),  (x, y) = diag(1/cos β, 1/cos α) · R(-γ) · d
       d = 화면에서 고정할 빛 방향(150deg = 왼위→오른아래),  α,β,γ = rotateX/Y/Z

   → fly--a(11/-13/-4) 154deg · fly--b(9/13/5) 145deg · .stack(26/-1/-1) 153deg.

   --sx/--sy 도 실측으로 푼다. 목표는 «가로 0.20·h · 세로 0.16·h»(고도 74°)다.

   **--sx 값 자체가 곧 화면 오프셋은 아니다.** 각 그림자는 제 겹 구성(far/near/접지의
   인셋·농도·흐림)에서 오는 기준 오프셋을 이미 갖고 있고, --sx/--sy 는 그 위에 얹는
   보정이다. 게다가 --sx 1px 이 무게중심을 몇 px 움직이는지가 카드마다 다르다 —
   뜬 카드 둘은 ≈1.0 인데 더미는 **2.3** 이다(접지 겹이 좁고 진해 무게중심을 혼자
   끌고 간다). 그래서 두 점을 재서 기울기를 구하고 역산했고, 맞는지는 값이 아니라
   아래 «실측» 표로 판단한다.
   .stack 만 예외다 — 바닥에 **놓인** 것이라 «아랫변 아래로 얼마» 라는 목표 자체가
   맞지 않는다(식대로면 --sy 가 -33px, 즉 그림자를 위로 당기게 된다). 접지는 붙어
   있는 것이 맞으므로 세로는 손대지 않고 기준값(+18px)을 그대로 쓴다.

   **기울기는 카드마다 재야 한다.** .stack 은 --sx 1px 이 화면에서 1.44px 로 움직여
   뜬 카드 둘(0.92 / 0.94)의 1.5배가 넘는다 — 같은 값을 주면 바닥에 놓인 더미가
   가장 멀리 밀려 순서가 거꾸로 된다(실제로 그렇게 나왔다: 더미 +36.9 > 검정 +28.0).

   측정에는 함정이 하나 더 있다. 카드를 다 띄운 채로 재면 **겹친 그림자끼리 알파
   합성**되어 한 겹만 껐을 때의 차이가 그 겹의 모양과 다르다 — 더미 구간은 검은
   카드의 그림자가 덮고 있어 기울기가 2.24 로 잘못 나왔다. 카드를 하나만 남기고
   재야 맞다.

   한때 148 / 118 / 166 이었다. 손으로 고른 값이라 셋의 폭이 48° 나 벌어졌는데,
   위 식으로 계산하면 실제 폭은 **9°** 다 — 특히 검은 카드(118)는 27° 어긋나 혼자
   다른 조명 아래 있었다. 자세(--pose-*)를 고치면 이 각도도 다시 계산한다. */
.cast--a { --shade-fix: 1.011;
          --far-a: 0.085; --far-b: 42px; --near-a: 0.085; --near-b: 20px;
          --skew: 7deg;  --ct-l: 7%; --ct-r: 7%;  --ct-t: 72%; --ct-b: -4%;
          --ct-a: 0.14;  --ct-blur: 7px;
          --sx: 27px; --sy: 38px; }
.cast--b { --shade-fix: 1.010;
          --far-a: 0.09; --far-b: 34px; --near-a: 0.10; --near-b: 15px;
          --skew: -6deg; --ct-l: 7%;  --ct-r: 7%; --ct-t: 73%; --ct-b: -3%;
          --ct-a: 0.15; --ct-blur: 6px;
          --sx: 21px; --sy: 30px; }
/* 더미는 바닥에 **놓여** 있다 — 접지가 가장 좁고 진하며 흐림이 가장 적다. */
.cast--s { --shade-fix: 1.008;
          --far-a: 0.12; --far-b: 18px; --near-a: 0.17; --near-b: 9px;
          --skew: 2deg;  --ct-l: 4%;  --ct-r: 4%;  --ct-t: 84%; --ct-b: -2%;
          --ct-a: 0.26;  --ct-blur: 3px;
          --sx: 7px;  --sy: -6px; }

/* 바닥에 놓인 더미는 그림자가 카드에 바짝 붙는다. */
.stack .shade--far  { left: -3.5%; right: -3.5%; top: 62%; bottom: -22%; }
.stack .shade--near { left: 0.5%;  right: 0.5%;  top: 78%; bottom: -8%; }

/* ── 떠 있는 카드 ────────────────────────────────────────────────────
 *
 * 두 면을 다 갖고 있고, 누르면 **실제로 돌아서** 반대 면이 온다. 뒷면을 따로 숨기지
 * 않고 backface-visibility 로 처리한다 — opacity 로 바꿔치기하면 도는 도중에 두 면이
 * 겹쳐 비친다.
 */
.fly { cursor: pointer; }

.fly:focus-visible { outline: 2px solid var(--o-mid); outline-offset: 16px; }

.fly__float { height: 100%; transform-style: preserve-3d; }

.fly__turn {
    height: 100%;
    transform-style: preserve-3d;
    transform: rotateY(var(--flip, 0deg));
    transition: transform 620ms cubic-bezier(0.4, 0, 0.2, 1);
}

.fly[data-flipped="true"] .fly__turn { --flip: 180deg; }

.face {
    position: absolute;
    inset: 0;
    backface-visibility: hidden;
}

.face--b { transform: rotateY(180deg); }

/* hover — **그 카드만** 앞으로 나오고 그림자가 커진다. .room 에 걸면 셋이 함께
   반응해서 «장면 전체가 눌린» 것처럼 보인다. */
.fly:hover .fly__tilt { --lift: 46px; }

/* hover 로 카드가 앞으로 나오면 그림자는 넓어지고 옅어진다. 그림자가 카드의 자손이
   아니게 됐으므로 :has() 로 잇는다 — 값은 translateZ 를 뺀 환산값이다(1.19 → 1.11). */
.room:has(#fly-a:hover) .cast--a { transform: translate(var(--sx), var(--sy)) scale(1.11); opacity: 0.82; }
.room:has(#fly-b:hover) .cast--b { transform: translate(var(--sx), var(--sy)) scale(1.11); opacity: 0.82; }

/* ── 바닥의 더미 ─────────────────────────────────────────────────────
 *
 * 같은 크기의 종이 여러 장이 아주 조금씩 어긋나 쌓여 있다. 아래 장은 위 장에 가려
 * **아래 끝 1px 남짓만** 보이는데, 그 줄무늬가 곧 종이 단면이다. 밝은 판에서는 이 단면이
 * 어두운 판에서보다 잘 보인다 — 종이의 흰 면과 사이의 그림자 선이 둘 다 살아난다.
 *
 * **한 장의 두께가 이 그림의 전부다.** 한때 3px 씩 일곱 장이었는데, 3px 는 이 크기에서
 * 종이가 아니라 골판지다(명함 용지 0.3mm ≈ 여기서 1px). 두께를 1/3 로 줄이고 장수를
 * 열여덟로 늘렸다 — 눈에 남는 실루엣 높이는 거의 같은데, 결이 굵은 여섯 줄에서
 * 촘촘한 열여덟 줄로 바뀐다.
 *
 * **완벽히 정렬하지 않는다.** 사람이 쌓은 종이는 가로로 미세하게 어긋나고 조금씩 돌아가
 * 있다. 다만 어긋남을 크게 주면 «부채처럼 펼친 카드» 가 되고 «쌓인 더미» 가 아니게 된다 —
 * 여기 값은 ±0.7px · ±0.14deg 로, 보이는 것이 아니라 «완벽하지 않다는 느낌» 만 남는다.
 */
.stack__body {
    position: relative;
    height: 100%;
    transform-style: preserve-3d;
    /* 종이 한 장의 두께. **카드 면의 법선(Z) 방향**이다 — 면 안쪽(로컬 Y)이 아니다.
       이 차이가 이 더미의 전부다: Y 로 밀면 열여덟 장이 **같은 평면 위에서** 아래로
       미끄러진 것이라, 단면 선(각 장의 아래 모서리)은 카드 면과 나란한데 띠의 외곽선은
       화면 아래로 곧게 내려가 **두 기울기가 어긋난다**(카드가 휘어 보이고, rotateZ 와
       겹쳐 왼쪽 아래가 오른쪽보다 두꺼워진다 — 실측). Z 로 밀면 더미가 **하나의 3D
       물체**가 되어 네 모서리의 두께가 원근에서 저절로 나온다.

       값은 실물보다 두껍다(명함 용지 0.3mm ≈ 1px). 법선으로 민 두께는 화면에 sin(자세 X)
       배만 나타나므로 — 26deg 에서 0.44 배 — 1px 로 두면 열여덟 장이 8px 띠가 되어 종이
       여러 장으로 읽히기엔 얇다. 2.6px 에서 띠가 16.4px 다(실측).

       **이 값과 자세 X 는 함께 움직인다.** 한쪽만 고치면 띠 두께가 따라 바뀐다 —
       띠 = 열일곱 칸 × 이 값 × sin(X) − 원근이 끌어올리는 몫. */
    --sheet: 2.6px;
}

.stack__sheet, .stack__top {
    position: absolute;
    inset: 0;
    border-radius: 2px;
    transition: transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* 단면 한 장. 아래 그림자 선이 장과 장을 가른다 — 이것이 없으면 열여덟 장이 한 덩어리의
   두꺼운 회색 판으로 뭉친다.

   **농도는 장수에 반비례한다.** 일곱 장 시절의 0.20 을 그대로 두면 1px 간격으로 겹친
   열여덟 줄이 서로를 덮어 더미 전체가 회색 판이 된다. 줄은 «있는지 모를 만큼» 옅게
   두고(0.085), 결은 줄의 진하기가 아니라 **줄의 개수**가 만든다. */
.stack__sheet {
    background: var(--paper);
    box-shadow: 0 0.5px 0 rgba(var(--shade), 0.085);
}

/* 자리 = **위에서 몇 번째인지**(--i) × 한 장 두께를 **Z 로** 내려보낸 것. 마크업에는
   번호만 있고 좌표는 없다.

   `.stack__body` 가 `preserve-3d` 라 이 Z 는 **카드 면의 법선**을 따른다 — 그래서 열여덟
   장이 한 물체로 쌓이고, 아래 장이 더 멀어 원근으로 조금 작게·조금 아래로 보인다(모서리
   두께가 저절로 나오는 이유). 렌더 순서도 Z 가 정하므로 아래 장이 위 장에 가려진다.

   어긋남(--jx/--jr)과 hover 여분(--hx/--hy/--hr)은 **면 안쪽**이다 — 사람이 쌓은 종이가
   가로로 미세하게 어긋나는 것과, 위에서부터 집어 벌어지는 것은 둘 다 평면 안의 일이다. */
.stack__sheet {
    transform:
        translate3d(calc(var(--jx, 0px) + var(--hx, 0px)),
                    var(--hy, 0px),
                    calc(var(--i, 0) * var(--sheet) * -1))
        rotate(calc(var(--jr, 0deg) + var(--hr, 0deg)));
}

/* 어긋남·회전은 **다섯 장 주기**로 돈다. 열여덟은 5 의 배수가 아니라 주기가 더미
   안에서 딱 맞아떨어지지 않는다 — 반복이 눈에 잡히지 않는 이유다.

 */
.stack__sheet:nth-child(5n+1) { --jx: -0.6px; --jr:  0.12deg; }
.stack__sheet:nth-child(5n+2) { --jx:  0.4px; --jr: -0.09deg; }
.stack__sheet:nth-child(5n+3) { --jx: -0.2px; --jr:  0.05deg; }
.stack__sheet:nth-child(5n+4) { --jx:  0.7px; --jr: -0.14deg; }
.stack__sheet:nth-child(5n+5) { --jx: -0.4px; --jr:  0.08deg; }

/* 맨 윗장은 인쇄면을 보인다 — **앞면이 보이는 카드는 전부 채운다**(`_card_front.html`).
   한때 여기만 값 없는 원지였는데, «아직 아무것도 적히지 않은 표준 명함» 이라는 의도는
   화면에서 읽히지 않고 «만들다 만 카드» 로 보였다. */
.stack__top { box-shadow: 0 0 0 1px rgba(var(--shade), 0.09); }

/* ── 넘겨보기 (클릭) ─────────────────────────────────────────────────
 *
 * 더미를 누르면 **위에서 아래로 한 장씩 아주 조금 들렸다 내려앉는다** — 엄지로 카드 덱을
 * 살짝 튕겼을 때의 그 물결이다. 화면에서 실제로 바뀌는 것은 없다(연출뿐이다).
 *
 * **움직임의 크기가 이 연출의 전부다.** 들리는 높이는 2px 이고 회전은 0.9deg 다.
 * 기준은 **화면에 보이는 단면 한 칸**이다 — 띠 16.4px 를 열일곱 칸으로 나눈 0.96px 이
 * 그 칸이고, 2px 은 그 **두 칸**이다. 들린 장이 이웃 두 장의 모서리를 넘어서는 것이
 * 보이면서도(그래서 «한 장이 움직였다» 로 읽힌다) 띠의 8분의 1 이라 더미의 실루엣을
 * 흔들지 않는다. 더 주면 «튕긴다» 가 아니라 «흩어진다» 가 된다.
 *
 * 시차는 **20ms**다. 요청 범위(20~40ms)의 아래쪽 끝인데, 장수 때문이다 — 열여덟 장에
 * 40ms 면 그것만 720ms 로 «전체 0.5초 이내» 를 못 지킨다. 20ms × 18 + 135ms = 495ms.
 *
 * 자리(--i)·어긋남·hover 가 `transform` 을 이미 쓰고 있으므로 여기서도 **독립 속성
 * `translate`/`rotate`** 로 움직인다(진입 연출과 같은 이유). 끝 값이 평상시 값(none)과
 * 같아 **시작 상태로 정확히 돌아오고**, 그래서 몇 번이든 다시 누를 수 있다.
 */
.stack {
    /* JS 가 «애니메이션이 끝나는 시각» 을 이 두 값과 단면 개수로 **계산한다** —
       총 길이를 상수로 또 적으면 여기 값을 고칠 때 조용히 어긋난다. */
    --riffle-dur: 135ms;
    --riffle-step: 20ms;
    /* 누를 수 있다는 표시. 더미는 role=button 이 아니다 — 눌러도 화면 내용이 바뀌지
       않는 연출이라, 스크린리더에 «버튼» 으로 알리면 하지 않는 약속을 하는 셈이다.
       (뒤집히는 큰 카드 둘은 실제로 반대 면을 보여 주므로 role=button 이 맞다.) */
    cursor: pointer;
}

/* 빠르게 시작해 감속한다 — 튕기는 힘은 처음에 들어가고 종이는 스스로 내려앉는다. */
@keyframes riffle {
    0%   { translate: 0 0; rotate: 0deg; }
    42%  { translate: 0.8px -2px; rotate: 0.9deg; }
    100% { translate: 0 0; rotate: 0deg; }
}

/* 그림자도 함께 움직여야 «들렸다» 로 읽힌다. 넓어지는 폭은 3% — 카드가 2px 들리는
   동안의 그림자 변화이므로 이보다 크면 카드보다 그림자가 먼저 보인다. */
@keyframes riffle-shade {
    0%   { transform: translate(var(--sx), var(--sy)) scale(var(--shade-fix)); }
    40%  { transform: translate(var(--sx), var(--sy)) scale(calc(var(--shade-fix) * 1.03));
           opacity: 0.95; }
    100% { transform: translate(var(--sx), var(--sy)) scale(var(--shade-fix)); opacity: 1; }
}

/* 위에서 아래로. `--i` 는 맨 위가 1 이므로 그대로 시차가 된다(맨 윗장은 --i 가 없어
   따로 적고 0ms 에 시작한다). 아래 장이 늦게 움직이는 것이 곧 «물결» 이다. */
.stack[data-riffle="true"] .stack__top {
    animation: riffle var(--riffle-dur) cubic-bezier(0.18, 0.72, 0.3, 1);
}

.stack[data-riffle="true"] .stack__sheet {
    animation: riffle var(--riffle-dur) cubic-bezier(0.18, 0.72, 0.3, 1)
               calc(var(--i) * var(--riffle-step));
}

/* 그림자는 장마다가 아니라 **더미 하나**로 반응한다 — 물결이 지나는 동안 한 번
   넓어졌다 좁아진다. 길이는 물결의 앞부분(0~380ms)에 걸친다. */
.room:has(.stack[data-riffle="true"]) .cast--s {
    animation: riffle-shade 380ms cubic-bezier(0.18, 0.72, 0.3, 1);
}

/* hover — 맨 위 세 장이 부채처럼 벌어진다. 아래 장들은 그대로다: 더미는 «위에서부터»
   집어지는 물건이다. 단면은 `--h*` 로 더해지므로 제 높이를 유지한 채 벌어진다.

   `.stack__top` 이 `.stack__body` 의 마지막 자식이라, 맨 위 단면은 nth-last-child(2)
   (= `--i: 1`)이고 그 아래가 (3) 이다. */
.stack:hover .stack__top { transform: translate(2.5px, -3.5px) rotate(-1.7deg); }
.stack:hover .stack__sheet:nth-last-child(2) { --hx: -2.5px; --hy: 0.5px; --hr:  1.4deg; }
.stack:hover .stack__sheet:nth-last-child(3) { --hx:  3px;   --hy: 3.5px; --hr: -1deg; }

/* **누르는 순간 부채를 접는다.** 벌어진 채로 물결이 지나면, 물결이 드는 높이(2px)가
   벌어진 틈(2.5~3.5px)보다 작아 그 틈에 묻힌다 — 정확히 맨 위 세 장에서, 물결이 가장
   먼저 지나는 자리에서 그렇다.

   접히는 것은 `transform`(부채)이고 물결은 `translate`/`rotate` 이므로 **서로 덮지 않고
   동시에** 일어난다. 접는 시간은 90ms 로 평상시(220ms)보다 짧다 — 물결의 첫 봉우리가
   77ms(맨 위 장: delay 20 + 135×0.42)에 오므로, 그때 이미 거의 접혀 있어야 한다.
   물결을 늦춰서 접기를 기다리지 않는 이유: 누른 뒤 반응까지의 시간은 늘리지 않는다.

   물결이 끝나 플래그가 지워지면 hover 규칙이 다시 걸려 평상시 속도로 벌어진다 —
   «접고 · 넘기고 · 다시 벌어짐» 이 한 흐름이 된다.

   선택자에 `:hover` 를 그대로 달아 둔다. 특정도가 위 세 줄보다 한 단 높아야 덮을 수 있고
   (`[data-riffle]` 이 그 한 단이다), 애초에 부채가 없는 상태에서는 접을 것도 없다. */
.stack[data-riffle="true"]:hover .stack__top {
    transform: none;
    transition-duration: 90ms;
}

.stack[data-riffle="true"]:hover .stack__sheet:nth-last-child(2),
.stack[data-riffle="true"]:hover .stack__sheet:nth-last-child(3) {
    --hx: 0px; --hy: 0px; --hr: 0deg;
    transition-duration: 90ms;
}

/* ── 진입 — 한 번에, 살짝 올라오며 ───────────────────────────────────
 *
 * 새로고침하면 **한 화면이 한 번에** 나타난다. 글도 카드도 같은 시각(0ms)에, 같은
 * 움직임으로 — 8px 아래에서 올라오며 밝아진다(260ms). 순서도 시차도 없다.
 *
 * 여기 한때 있던 것: 카드가 위에서 수직으로 떨어지는 낙하 → 오른쪽 위에서 호를 그리며
 * 들어오는 활강 → 더미를 «아래 덩어리 + 얹히는 넷» 으로 나눈 조립. 세 벌 다 만들어
 * 보고 지웠다. 진입 연출은 **매 새로고침마다 같은 것을 다시 보게 되는 비용**이고,
 * 이 화면의 주인공은 움직임이 아니라 놓여 있는 인쇄물이다. 남긴 8px 은 «연출» 이
 * 아니라 화면이 갑자기 튀어나오지 않게 하는 정도다.
 *
 * 되살릴 일이 있으면 히스토리에서 꺼낸다(`3e3c531` 이 마지막 활강 판이다). 쓰지 않는
 * 키프레임을 남겨 두지 않는 이유는, 남기면 다음 사람이 «있는 줄 알고» 되살릴 자리가
 * 되기 때문이다 — 색 단을 지울 때와 같은 규칙이다.
 *
 * **카드는 부모(`.stack`/`.fly`)에 건다.** 개별 그림자(`.cast`)에 직접 걸어도 안 된다 —
 * 그림자의 평상시 transform 이 --sx/--sy 이동인데 진입 키프레임의 translateY 가 그것을
 * 덮어써 그림자가 제자리를 잃는다. 그림자 셋은 부모인 `.casts` 한 곳에 걸어 함께 든다.
 *
 * 같은 이유로 **그림자를 움직이는 키프레임은 평상시 transform 을 그대로 다시 적어야**
 * 한다(riffle-shade). var 하나라도 정의가 없으면 transform 이 통째로 무효가 되어
 * `none` 이 된다 — translateZ 를 걷어내며 실제로 그렇게 만들었다가 실측에서 잡았다.
 *
 * 검은 띠만 **자리를 옮기지 않고 밝아지기만** 한다(`band-in`). 8px 을 주면 화면 바닥에
 * 붙은 면의 윗변이 함께 움직여, 판을 가르는 선이 제자리를 찾는 과정이 보인다.
 */
@keyframes text-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

@keyframes band-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* `backwards` 라 시작 전에는 opacity 0 이지만 **버튼은 그때도 눌린다** — 투명한 요소도
   포인터 이벤트를 받는다. */
.lede,
.stack, .fly--a, .fly--b { animation: text-in 260ms ease-out backwards; }

.feats { animation: band-in 260ms ease-out backwards; }

/* ── 부유 ────────────────────────────────────────────────────────────
 *
 * 떠 있는 두 장이 아주 조금 오르내린다. **화면이 뜨는 순간부터 돈다**(delay 없음) —
 * 한때 «착지 시각» 을 기다렸는데, 기다릴 낙하가 없어졌다.
 *
 * 시작 값이 0 이라 평상시 자세에서 **끊김 없이** 이어진다 — from 을 0 이 아닌 값으로
 * 두면 첫 프레임에 그만큼 튄다.
 *
 * 두 장의 진폭·주기가 다르다. 같은 박자면 둘이 한 덩어리로 붙어 보여 «두 장» 이 아니게 된다.
 */
@keyframes fly-float-a {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(0, -5px, 0); }
}

@keyframes fly-float-b {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(0, 3.5px, 0); }
}

/* 그림자는 `.cast` 가 아니라 그 **안쪽 두 겹**이 숨쉰다 — 바깥은 제자리 값과
   hover 가 이미 쓰고 있고, 한 요소의 transform 을 둘이 나눠 가질 수 없다. */
@keyframes shade-breathe {
    from { transform: scale(1); opacity: 1; }
    to   { transform: scale(1.07); opacity: 0.74; }
}

.fly--a .fly__float { animation: fly-float-a 5.4s ease-in-out infinite alternate; }
.cast--a .shade     { animation: shade-breathe 5.4s ease-in-out infinite alternate; }

.fly--b .fly__float { animation: fly-float-b 4.4s ease-in-out infinite alternate; }
.cast--b .shade     { animation: shade-breathe 4.4s ease-in-out infinite alternate; }

/* ── 좁은 화면 ───────────────────────────────────────────────────────
 *
 * 구조는 그대로다 — 제목이 위, 카드 뭉치가 가운데, 글과 다섯 줄이 아래. 화면이 좁아질수록
 * 제목의 두 줄을 **한 축으로 모으고**(들여쓰기를 줄이고) 카드를 가운데로 당긴다.
 * 여전히 한 화면에서 끝난다.
 */
/* 기능 줄은 다섯 열이라 폭에 가장 먼저 쫓긴다. 카드보다 앞선 폭에서 먼저 조인다 —
   좌우 여백·열 간격·아이콘·글자를 한 단씩 줄여 **설명이 잘리지 않게** 유지한다
   (다섯이 전부 한 줄이어야 아이콘이 한 눈금에 선다). */
@media (max-width: 1150px) {
    .feats { padding-inline: 4%; gap: 0 clamp(12px, 1.6vw, 26px); }
    .feat { grid-template-columns: 22px 1fr; gap: 0 10px; }
    .feat__ico svg { width: 22px; height: 22px; }
}

@media (max-width: 1100px) {
    /* 이 구간은 카드가 커지는 만큼 기둥을 당겨 오는 자리다. 한 줄 제목이 되면서
       당기는 방향이 문제가 됐다 — 왼쪽으로만 당기면 제목 끝과 검정 카드가 3px 거리로
       붙는다(측정값). 그래서 **왼쪽보다 아래로** 당긴다: 제목은 가로를 쓰고 카드는
       세로를 쓴다. 제목 계수도 한 단 내려(5.6 → 5vw) 둘이 같은 폭을 다투지 않게 한다. */
    .title { font-size: clamp(30px, min(5vw, 9.5vh), 84px); }

    /* 카드 무게중심이 옮겨졌으므로 격자 초점도 함께 옮긴다. */
    .room { --grid-focus: 63% 41%; }

    .room {
        --s-l: 49%; --s-t: 53%; --s-w: min(35vw, 31vh, 313px);
        --b-l: 64%; --b-t: 42%; --b-w: min(40.5vw, 36vh, 359px);
        --a-l: 76%; --a-t: 29%; --a-w: min(44vw, 40.5vh, 405px);
    }
}

/* 여기부터는 네 열에 설명이 들어가지 않는다 — 두 줄짜리 설명이 세 줄·네 줄로 접히며
   칸마다 높이가 갈린다. 두 열 두 줄로 접고 설명을 뺀다: 항목 이름만으로도 넷이
   무엇인지 전해지고, 이름 넷은 어느 폭에서도 한 줄이다. */
@media (max-width: 900px) {
    .feats { grid-template-columns: repeat(2, 1fr); gap: 12px clamp(10px, 3vw, 24px); }
    .feat__note { display: none; }
}

@media (max-width: 780px) {
    /* 글 덩어리가 화면 전폭을 쓰므로 vw 계수가 커진다(5.6 → 10.5). 10.5vw × 7.65 ≈
       80vw — 좌우 여백을 남기고 한 줄이 들어가는 상한이다. */
    .title { font-size: clamp(30px, min(10.5vw, 8vh), 72px); }

    /* 글 덩어리가 위로 올라가고 카드는 그 아래를 쓴다 — 좁은 화면에서 둘을 나란히
       두면 글의 폭이 읽을 수 없을 만큼 좁아진다. 검은 띠가 아래 25% 를 가져간 뒤로도
       글은 한 단 더 올라가 있다(26% → 24%): 그만큼이 카드가 버튼을 피할 여유가 된다. */
    .lede { top: 24%; max-width: none; }

    /* 위아래로 접힌 판이 가장 빡빡하다. 글(위) · 카드(가운데) · 검은 띠(아래 30%)가
       한 화면을 나눠 쓰는데, 카드만 올리면 흰 앞면이 「회원가입」 버튼을 덮는다(실측:
       띠가 30% 였을 때 -13px). 그래서 카드는 **덜** 올리고(5~6%) 세로 상한(vh)을 한 단
       낮춰 트리오 자체를 조금 작게 만든다 — 여기서는 카드 크기가 곧 여백이다. */
    .room { --grid-focus: 55% 54%; }

    .room {
        --s-l: 40%; --s-t: 62%; --s-w: min(52vw, 24vh, 300px);
        --b-l: 56%; --b-t: 55%; --b-w: min(58vw, 28vh, 340px);
        --a-l: 70%; --a-t: 46%; --a-w: min(62vw, 31vh, 380px);
    }
}

/* 세로가 아주 짧은 화면(가로로 눕힌 휴대폰 등). 글자·간격을 함께 조인다. */
@media (max-height: 620px) {
    .btn { height: 40px; }
    /* 바닥에 붙어 있으므로 «띄우는 값» 이 아니라 **안쪽 세로 여백**을 조인다. 설명이
       빠져 내용이 한 줄뿐이라, 여백을 더 줄이면 띠가 «잘린 막대» 로 보인다. */
    .feats { padding-block: clamp(20px, 3.2vh, 32px); }
    .feat__note { display: none; }
}

/* ── 모션 최소화 ─────────────────────────────────────────────────────
 *
 * 부유·시차·hover 의 움직임이 없다. 진입 연출은 애초에 없으므로(위 «진입 — 없다»)
 * 여기서 껄 것도, 대신 넣을 페이드도 없다 — 두 모드의 차이는 «놓인 뒤에 움직이는지» 뿐이다.
 *
 * 선택자는 **걸어 둔 것과 같은 형태로** 되짚어 적는다. 부유가 `.fly--a .fly__float` 로
 * 걸려 있어서 `.fly__float { animation: none }` 만으로는 꺼지지 않는다 — 미디어쿼리는
 * 특정도를 올려 주지 않는다(실측: reduce 를 켜도 카드가 계속 숨을 쉬었다).
 */
@media (prefers-reduced-motion: reduce) {
    .feat, .feat__name, .btn { transition: none; }
    .feat:hover { transform: none; }

    /* 남은 것은 부유와 넘겨보기뿐이다(진입 연출은 아예 없다). 선택자를 **걸어 둔 것과
       같은 형태로** 되짚어 적는다 — 부유가 `.fly--a .fly__float` 로 걸려 있어서
       `.fly__float { animation: none }` 만으로는 꺼지지 않는다.

       넘겨보기는 JS 가 애초에 걸지 않지만(`reduceMotion`) 여기서도 끈다 — 사용자가
       세션 중에 설정을 바꾸면 JS 가 읽은 값은 이미 낡았고, 그때 기준이 되는 것은
       미디어쿼리다. */
    .fly--a .fly__float, .fly--b .fly__float,
    .cast--a .shade, .cast--b .shade,
    .stack[data-riffle="true"] .stack__top,
    .stack[data-riffle="true"] .stack__sheet,
    .casts, .room:has(.stack[data-riffle="true"]) .cast--s { animation: none; }

    .stack__tilt, .fly__tilt, .fly__turn, .cast,
    .stack__sheet, .stack__top { transition: none; }

    .fly:hover .fly__tilt { --lift: 0px; }
    .room:has(#fly-a:hover) .cast--a,
    .room:has(#fly-b:hover) .cast--b { transform: translate(var(--sx), var(--sy)) scale(1.01); opacity: 0.86; }

    /* 단면은 transform 을 지우지 않는다 — 그러면 --i 로 얻은 제 높이까지 잃고 열여덟
       장이 한 자리에 겹친다. 벌어짐(`--h*`)만 0 으로 돌린다.

       선택자를 **벌어짐 규칙과 똑같이** 적는다. `.stack__sheet` 만 쓰면 특이도가 한 단
       낮아 `:nth-last-child` 쪽이 이기고, 저감 모션에서도 부채가 그대로 벌어진다(실측). */
    .stack:hover .stack__top { transform: none; }
    .stack:hover .stack__sheet:nth-last-child(2),
    .stack:hover .stack__sheet:nth-last-child(3) { --hx: 0px; --hy: 0px; --hr: 0deg; }
}

/* ── 카드 뒤 배경 — 실루엣 한 장 ─────────────────────────────────────
 *
 * 격자가 있던 자리다. 시안 비교(/sandbox/bg-a ~ bg-c8)에서 A7 «한 장» 이 뽑혔다.
 *
 * 층은 z 1 — 판의 조명(z 0) **위**, 카드(z 2~4) **아래**다. 카드가 주인공이므로
 * 배경 요소는 반드시 카드 뒤로 들어가야 한다.
 */
.bgfx {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    overflow: hidden;           /* 화면 밖은 잘린다 — «이어진다» 는 느낌의 근거 */
}

/* 지면. 카드 각각의 __shade 와 다른 겹이다 — 그쪽은 «이 카드가 띄워져 있다» 를 말하고
   이건 **바닥 자체**를 말한다.

   **왼쪽 끝을 40% 에서 끊는다.** 시안에서는 26% 였는데 그 값이면 꼬리가 x 519 까지
   닿아 밝은 왼쪽을 함께 가라앉혔다 — 판을 밝게 한 것과 정면으로 어긋난다. 40% 면
   영향이 x 748 에서 끝나 글 덩어리(오른쪽 끝 617) 근처에 아무것도 오지 않는다. */
.bgfx__floor {
    position: absolute;
    left: 40%;
    right: -8%;
    top: 42%;
    bottom: 4%;
    background: radial-gradient(56% 54% at 56% 44%,
        rgba(var(--shade), 0.115) 0%,
        rgba(var(--shade), 0.052) 46%,
        rgba(var(--shade), 0) 78%);
    filter: blur(26px);
}

/* 실루엣 전용 무대. `.room` 과 **같은 카메라 값**을 복제하므로 같은 pose 를 주면
   카드와 똑같이 일그러진다 — 평면 `rotate:` 로는 원근 사다리꼴 왜곡이 없어
   «비슷하게 기울인 사각형» 으로 갈린다.

   `.bgfx` 에 바로 못 주는 이유: `overflow:hidden` 이 3D 를 평면으로 눌러 버린다
   (grouping property — preserve-3d 와 공존 불가). 자르는 겹과 카메라 겹을 나눈다. */
.rig {
    position: absolute;
    inset: 0;
    perspective: 1600px;
    perspective-origin: 58% 42%;
}

/* left/top 이 **중심**이다 — `.stack`/`.fly` 와 같은 좌표계를 쓴다. */
.g3 {
    position: absolute;
    translate: -50% -50%;
    aspect-ratio: var(--card-ratio);
    border-radius: 3px;
    background: rgba(var(--shade), var(--g-a));
    filter: blur(var(--g-b));
    transform: rotateX(var(--px)) rotateY(var(--py)) rotateZ(var(--pz));
}

/* 폭은 카드와 **같은 형식**(min(vw,vh,px))이다. 실루엣만 px 고정이면 좁은 화면에서
   실루엣이 카드보다 커져 앞뒤가 뒤집힌다. 비율은 .stack(22vw/30vh/304px) 기준. */
.g3--one { left: 98%; top: 5%; width: min(31.9vw, 43.4vh, 440px);
           --px: 11deg; --py: -13deg; --pz: -4deg; --g-a: 0.099; --g-b: 6px; }

/* 실루엣의 그림자. 실루엣은 «뒤에 있는 카드» 이지 그림자가 아니므로, 그것도 바닥에
   그림자를 떨어뜨려야 카드 셋과 같은 조명 아래 있게 된다.

   자리는 카드들과 **같은 규칙**으로 잡았다: 광원이 왼쪽 위이므로 중심에서 오른아래로
   밀고, 미는 양은 높이에 비례한다. 실루엣은 트리오보다 높으므로(아직 떠 오는 중) h≈150
   으로 보아 +54px · +42px — %로 옮기면 중심이 (101.75%, 9.7%) 다.

   **농도는 실루엣 자체보다 옅다(0.05 < 0.099).** 배경의 흐릿한 형체가 자기 그림자보다
   연해 보이면 «그림자가 주인» 이 되어 거꾸로 읽힌다. 흐림도 크게 준다 — 높이 뜬 물체의
   그림자일수록 윤곽이 풀린다. */
.g3__shade {
    position: absolute;
    left: 101.75%;
    top: 9.7%;
    width: 30%;
    height: 11%;
    translate: -50% -50%;
    border-radius: 50%;
    background: rgba(var(--shade), 0.05);
    filter: blur(20px);
}
