www.git

www

~ Personal site — projects, posts, about. Built with sqzass.
git clone https://github.com/debuman/www.git

static/css/main.css (71313B)

      1 /* sqzass 테마 — marigold
      2  *
      3  * 이 사이트가 쓰는 테마이자 sqzass 로 만든 첫 테마다. 이름은 이 시트의 유일한
      4  * 색에서 따왔다. 두 번째 테마가 생기면 그것도 자기 색으로 불리면 된다.
      5  *
      6  * 차가운 니어블랙 위의 가운데 정렬 갤러리. 색은 평상시에 없고 손이 닿을 때만
      7  * 나온다 — 액센트가 브랜딩이 아니라 보상이다.
      8  *
      9  * hover 는 색만 바꾼다. 크기가 움직이면 시선이 그리로 끌려가는데, 이 사이트에서
     10  * 커서가 지나가는 자리는 대부분 읽는 중인 자리다.
     11  *
     12  * 목소리는 굵기로 만든다. 디스플레이는 230~350 의 초경량에 **양수** 트래킹이고,
     13  * 라벨은 작고 넓게 벌어진 대문자다. 큰 글자는 가늘고 작은 글자는 벌어진다.
     14  * 굵은 글씨는 이 사이트에 없다.
     15  *
     16  * 서체는 Noto Sans KR 가변 한 벌이다. 라틴과 한글을 unicode-range 로 갈라
     17  * 두어서 영문 페이지는 한글 파일을 받지 않는다. 서브셋은 tools/fonts.py 가
     18  * 만들고, 빠진 글자는 CI 가 빌드 전에 잡는다.
     19  *
     20  * 테마는 [data-theme] 이 정한다. 생성된 하이라이트 스타일시트가 같은 속성을
     21  * 보기 때문에, prefers-color-scheme 에 맡기면 둘이 어긋날 수 있다.
     22  */
     23 
     24 @font-face {
     25   font-family: "Display";
     26   src: url("/fonts/display-latin.woff2") format("woff2");
     27   font-weight: 100 900;
     28   font-display: swap;
     29   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20A0-20BF,
     30                  U+2122, U+2190-21BB, U+2212, U+2500-257F, U+25A0-25FF, U+2605-2606;
     31 }
     32 @font-face {
     33   font-family: "Display";
     34   src: url("/fonts/display-kr.woff2") format("woff2");
     35   font-weight: 100 900;
     36   font-display: swap;
     37   unicode-range: U+1100-11FF, U+3130-318F, U+AC00-D7A3, U+FF01-FF60;
     38 }
     39 @font-face {
     40   font-family: "JetBrains Mono";
     41   src: url("/fonts/jetbrains-mono-400.woff2") format("woff2");
     42   font-weight: 400;
     43   font-display: swap;
     44 }
     45 @font-face {
     46   font-family: "JetBrains Mono";
     47   src: url("/fonts/jetbrains-mono-700.woff2") format("woff2");
     48   font-weight: 700;
     49   font-display: swap;
     50 }
     51 
     52 /* ---------- tokens ---------- */
     53 
     54 :root {
     55   color-scheme: dark;
     56 
     57   /* 바닥에 색이 있다. 이 집 사이트들은 전부 그렇다 — ommp 는 청보라(#0A051E,
     58      앱의 BG_DARK 를 그대로), blog 는 청록, sqzass 문서는 청흑, 랜딩은 웜브라운.
     59      순수 중립 회색은 아무 말도 하지 않는다. */
     60   --bg-recess: #030405;
     61   --bg:        #07090a;
     62   --bg-2:      #0e1112;
     63   --bg-3:      #151819;
     64 
     65   --ink:   #eaeeec;
     66   --ink-2: #9ba39f;
     67   --ink-3: #737b77;
     68   --ink-4: #363c39;
     69 
     70   --rule:   #16191a;
     71   --rule-2: #232728;
     72   --rule-3: #363b3c;
     73 
     74   /* 색이 아니라 삼원소. 토큰 하나로 alpha 0.03~0.75 를 전부 낸다 —
     75      hex 였다면 매번 color-mix 를 거쳐야 했을 값들이다. */
     76   --ink-rgb: 234, 238, 236;
     77 
     78   /* 이 테마의 이름이자 유일한 색. 평상시에는 어디에도 없고 링크·포커스 링·
     79      현재 항목·손이 닿은 것에만 나온다 — 주황이면 갈 수 있는 것이거나 지금
     80      있는 자리다.
     81      지어낸 값이 아니라 About 페이지 초상의 배경에서 그대로 뽑았다
     82      (tools/icons.py 옆에서 샘플링, hsl 36° 75%). ommp 가 모든 색을 앱 소스에서
     83      가져오는 것과 같은 규칙이고, 차가운 바닥과는 보색이라 손이 닿는 자리가
     84      확실히 뜬다. 니어블랙 대비 9.07:1. */
     85   --accent: #e4a242;
     86   /* 액센트 위에 얹히는 잉크. 액센트가 밝은 주황이라 두 테마 모두 어둡다 —
     87      ::selection 처럼 액센트가 배경이 되는 자리에서 --bg 를 쓰면 라이트에서
     88      흰 글자가 주황 위에 놓여 읽히지 않는다. */
     89   --on-accent: #14140f;
     90 
     91   /* 선화 마크는 순수 흑 아니면 순수 백이다. 중간 회색은 비활성으로 읽힌다. */
     92   --mark-ink: #ffffff;
     93 
     94   --sans: "Display", -apple-system, BlinkMacSystemFont, "Segoe UI",
     95           "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", sans-serif;
     96   --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
     97 
     98   /* 굵기 사다리. 네 단이고 간격이 고르다 — 선택자마다 값을 적으면 사다리가
     99      아니라 우연의 모음이 된다. 굵은 글씨는 여전히 없다: 제일 무거운 단이 420 이다.
    100      트래킹은 둘뿐이다. 가는 글자를 크게 쓰면 자간이 필요하고, 두꺼워질수록
    101      그 필요가 줄어든다. */
    102   --w-display: 180;
    103   --w-section: 260;
    104   --w-item:    340;
    105   --w-label:   420;
    106   --track-air: 0.03em;
    107   --track-set: 0.014em;
    108 
    109   --radius: 2px;
    110   --masthead-h: 3.5rem;   /* sqzass 문서와 같은 높이. 내용이 아니라 크롬이라 얇아야 한다 */
    111   --gutter: clamp(1.25rem, 5vw, 3rem);
    112   --wide: 71rem;
    113   --read: 44rem;
    114 
    115   /* 한 박자. 값을 여기 한 번만 두면 모든 상호작용이 같은 속도로 반응하고,
    116      그게 "같은 물건"이라는 느낌을 만든다. hover 는 색만 바꾸므로 곡선도 하나면 된다. */
    117   --ease: cubic-bezier(0.16, 1, 0.3, 1);
    118   --tr-ink: 0.25s ease;
    119 }
    120 
    121 :root[data-theme="light"] {
    122   color-scheme: light;
    123 
    124   --bg-recess: #e9ecea;
    125   --bg:        #fafbfa;
    126   --bg-2:      #f2f4f3;
    127   --bg-3:      #eaeded;
    128 
    129   --ink:   #0e1211;
    130   --ink-2: #515856;
    131   --ink-3: #737a78;
    132   --ink-4: #bdc3c1;
    133 
    134   --rule:   #e4e8e6;
    135   --rule-2: #d5dad8;
    136   --rule-3: #bac0be;
    137 
    138   --ink-rgb: 14, 18, 17;
    139   /* 액센트는 두 테마가 같다. 밝은 바닥에서 대비가 낮아지지만, 이 색은
    140      평상시에 나오지 않고 hover·focus·현재 위치에만 쓰이며 그 자리에는 늘
    141      상태 변화가 함께 있다(밑줄, 테두리, 눈금). 색 하나로만 뜻을 전하는
    142      자리는 없다. */
    143   --accent: #e4a242;
    144   --mark-ink: #000000;
    145 }
    146 
    147 *, *::before, *::after { box-sizing: border-box; }
    148 
    149 /* 루트가 유동이라 모든 rem 이 이미 반응형이다. 무릎은 286px 과 1143px 이다. */
    150 html {
    151   font-size: clamp(16px, 15px + 0.35vw, 19px);
    152   -webkit-text-size-adjust: 100%;
    153   scroll-behavior: smooth;
    154 }
    155 
    156 body {
    157   margin: 0;
    158   background: var(--bg);
    159   color: var(--ink);
    160   font-family: var(--sans);
    161   /* 루트가 이미 유동이라(clamp 16→19px) 본문까지 vw 에 매달면 두 번 조정된다.
    162      1100px 위에서는 어차피 최대값에 고정돼 있던 값이다. */
    163   font-size: 1.0625rem;
    164   line-height: 1.7;
    165   /* 한국어는 어절 안에서 줄바꿈하지 않는다. */
    166   word-break: keep-all;
    167   overflow-wrap: anywhere;
    168   -webkit-font-smoothing: antialiased;
    169   text-rendering: optimizeLegibility;
    170 }
    171 
    172 img { max-width: 100%; height: auto; }
    173 a { color: inherit; text-decoration: none; }
    174 
    175 :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }
    176 ::selection { background: var(--accent); color: var(--on-accent); }
    177 
    178 * { scrollbar-width: thin; scrollbar-color: var(--rule-2) transparent; }
    179 ::-webkit-scrollbar { width: 8px; height: 8px; }
    180 ::-webkit-scrollbar-track { background: transparent; }
    181 ::-webkit-scrollbar-thumb { background: var(--rule-2); border-radius: 999px; }
    182 ::-webkit-scrollbar-thumb:hover { background: var(--ink-4); }
    183 
    184 
    185 .skip {
    186   position: absolute; left: -9999px; top: 0; z-index: 100;
    187   padding: 0.7rem 1.1rem; background: var(--bg-2);
    188   border: 1px solid var(--accent); border-radius: var(--radius);
    189 }
    190 .skip:focus { left: 0.5rem; top: 0.5rem; }
    191 
    192 /* ---------- layout ---------- */
    193 
    194 .wrap {
    195   width: 100%;
    196   max-width: var(--wide);
    197   margin-inline: auto;
    198   padding-inline: var(--gutter);
    199 }
    200 .wrap-read { max-width: var(--read); }
    201 /* 읽는 폭에 날짜 거터가 나가 앉을 자리를 더한 값. 숫자를 따로 고르지 않고
    202    --read 에서 유도하면 본문 폭을 바꿨을 때 원장이 따라온다.
    203    (ch 는 컨테이너에 쓰면 본문 글꼴 기준이라 절반밖에 안 나온다 — 한 번 틀렸다.) */
    204 .wrap-ledger { max-width: calc(var(--read) + 12rem); }
    205 
    206 main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
    207 
    208 /* ---------- masthead ---------- */
    209 
    210 .masthead {
    211   position: sticky; top: 0; z-index: 30;
    212   height: var(--masthead-h);
    213   background: color-mix(in srgb, var(--bg) 82%, transparent);
    214   backdrop-filter: blur(14px) saturate(1.5);
    215   border-bottom: 1px solid var(--rule);
    216 }
    217 
    218 /* 3열 격자. 양옆 열이 1fr 로 같으므로 가운데 열이 화면 축에 정확히 앉는다.
    219    flex 에 margin-inline:auto 를 쓰면 브랜드와 도구 덩이의 폭 차이만큼 내비가
    220    밀리는데, 페이지 전체가 가운데축인 사이트에서는 그 어긋남이 보인다. */
    221 .masthead-in {
    222   display: grid;
    223   grid-template-columns: 1fr auto 1fr;
    224   align-items: center; gap: clamp(0.75rem, 3vw, 2rem);
    225   height: 100%; max-width: var(--wide); margin-inline: auto;
    226   padding-inline: var(--gutter);
    227 }
    228 
    229 .brand { display: inline-flex; align-items: center; gap: 0.55rem; flex: none; }
    230 
    231 /* 마크는 잉크로 그린다. 원본이 흰 배경의 검은 선화라 <img> 로는 다크에서
    232    사라지는데, 마스크로 두면 배경색이 곧 선 색이 된다. 흑 아니면 백. */
    233 .brand-mark {
    234   width: 1.9em; aspect-ratio: 128 / 123; flex: none;
    235   background-color: var(--mark-ink);
    236   -webkit-mask: url("/images/mark.png") center / contain no-repeat;
    237   mask: url("/images/mark.png") center / contain no-repeat;
    238 }
    239 @supports not (mask-image: url("")) { .brand-mark { display: none; } }
    240 
    241 .brand-name {
    242   font-size: 0.82rem; font-weight: 500;
    243   letter-spacing: 0.22em; text-transform: uppercase;
    244   color: var(--ink);
    245 }
    246 
    247 .nav {
    248   display: flex; align-items: center; justify-content: center;
    249   gap: clamp(1rem, 2.5vw, 2rem);
    250   min-width: 0; overflow-x: auto; scrollbar-width: none;
    251 }
    252 .nav::-webkit-scrollbar { display: none; }
    253 
    254 /* 라벨은 작고 넓게 벌어진 대문자다. 상자도 배경도 없다. */
    255 .nav a {
    256   position: relative; flex: none; padding-block: 0.4rem;
    257   font-size: 0.72rem; font-weight: 400;
    258   letter-spacing: 0.18em; text-transform: uppercase;
    259   color: var(--ink-3);
    260   transition: color var(--tr-ink);
    261 }
    262 .nav a:hover { color: var(--ink); }
    263 .nav a[aria-current] { color: var(--ink); }
    264 /* 현재 위치는 1px 짧은 눈금. 밑줄은 링크의 표시이고 이건 상태의 표시다. */
    265 .nav a[aria-current]::after {
    266   content: ""; position: absolute; left: 50%; bottom: -0.15rem;
    267   width: 1.4rem; height: 1px; margin-left: -0.7rem;
    268   background: var(--accent);
    269 }
    270 
    271 .tools { display: flex; align-items: center; justify-content: flex-end; gap: 0.9rem; }
    272 
    273 /* 크롬 컨트롤에는 테두리가 없다. 상자를 두르면 편집물이 아니라 대시보드가 된다. */
    274 .chip {
    275   display: inline-flex; align-items: center; gap: 0.4rem;
    276   padding: 0; border: 0; background: none; cursor: pointer;
    277   font: inherit; font-size: 0.7rem; letter-spacing: 0.12em;
    278   text-transform: uppercase; color: var(--ink-3);
    279   /* 컨트롤 라벨은 낱말 하나이고, 낱말 하나는 줄바꿈하지 않는다. body 가
    280      overflow-wrap: anywhere 라 이걸 빼면 좁은 화면에서 "Search" 가 쪼개져
    281      마스트헤드 높이를 밀어 올린다. */
    282   white-space: nowrap;
    283   transition: color var(--tr-ink);
    284 }
    285 .chip:hover { color: var(--ink); }
    286 
    287 /* 키를 말하는 컨트롤 하나만 진짜 키캡이 된다 — 터미널에서 모는 도구라 얇은
    288    테두리에 작은 반경. 네 칩에 전부 테두리를 두르면 테두리 없는 타일·원장·
    289    액자와 싸워서 마스트헤드가 툴바가 된다. */
    290 .chip kbd {
    291   font-family: var(--mono); font-size: 0.66rem; line-height: 1; letter-spacing: 0;
    292   padding: 0.18rem 0.36rem;
    293   border: 1px solid var(--rule-2); border-radius: 3px;
    294   color: var(--ink-4);
    295   transition: border-color var(--tr-ink), color var(--tr-ink);
    296 }
    297 .chip:hover kbd { border-color: var(--rule-3); color: var(--ink-3); }
    298 /* 라벨이 `Ctrl /` 에서 `⌘ /` 로 바뀌어도 폭이 안 변한다 — 누르려던 것이
    299    도망가면 안 된다. */
    300 .chip-search { min-width: 4.2rem; justify-content: flex-end; }
    301 
    302 .i-moon { display: none; }
    303 :root[data-theme="light"] .i-sun { display: none; }
    304 :root[data-theme="light"] .i-moon { display: inline; }
    305 
    306 .chip-word { display: none; }
    307 
    308 @media (max-width: 46rem) {
    309   .brand-name { display: none; }
    310   /* 키캡은 키보드가 있을 때의 안내다. 없는 화면에서는 낱말이 대신 선다. */
    311   .chip kbd { display: none; }
    312   .chip-word { display: inline; }
    313   .chip-search { min-width: 0; }
    314   /* 가운데 축을 포기한다. 양옆을 1fr 로 묶어 두면 넓은 쪽(도구 열)의 폭이
    315      좁은 쪽에도 강제되고, 그 낭비만큼 내비가 잘려 나간다. 축보다 다 보이는
    316      게 낫다 — 축은 읽는 자리를 정하지만 잘린 라벨은 길을 지운다. */
    317   .masthead-in { grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.9rem; }
    318   .nav { gap: 1rem; }
    319 }
    320 
    321 /* 손안의 화면에서는 마스트헤드에 들어갈 자리가 모자란다. 나가는 것은 GitHub 다 —
    322    콜로폰에 같은 링크가 있고, 여기 남는 넷(갈래 셋과 검색)은 이 사이트 안을
    323    도는 데 필요한 전부다. 잘린 라벨을 남기느니 중복을 하나 뺀다. */
    324 @media (max-width: 34rem) {
    325   .chip-github { display: none; }
    326   .masthead-in { gap: 0.75rem; }
    327   /* 여기서부터 내비는 밀어서 본다. 가운데 정렬로 두면 넘칠 때 왼쪽으로 흘러나간
    328      부분에 손이 닿지 않는다 — 스크롤은 오른쪽으로만 되기 때문이다. */
    329   .nav { gap: 0.85rem; justify-content: flex-start; }
    330 }
    331 
    332 /* ---------- page head: 가운데 축 ---------- */
    333 
    334 .page-head {
    335   padding-block: clamp(3.5rem, 9vw, 7rem) clamp(2rem, 4vw, 3.5rem);
    336   text-align: center;
    337 }
    338 
    339 /* 프런트매터 펜스. 제목 블록이 끝나고 본문이 시작하는 자리에 한 페이지 한 번.
    340    소스 파일에서 `+++` 가 표시하는 것과 같은 경계이고, 이 사이트의 모든 페이지가
    341    실제로 `+++` 로 시작하는 sqzass 문서다. 장식처럼 보이는 유일한 것이며
    342    장식이 아니다. (sqzass 문서 시트가 같은 장치를 같은 이유로 쓴다.) */
    343 .fence {
    344   font-family: var(--mono);
    345   font-size: 1.0625rem;
    346   letter-spacing: 0.1em;
    347   color: var(--ink-4);
    348   text-align: center;
    349   margin: 0 0 clamp(2rem, 4vw, 2.75rem);
    350   user-select: none;
    351 }
    352 
    353 .page-title {
    354   margin: 0;
    355   font-size: clamp(2rem, 1.4rem + 2.4vw, 3.2rem);
    356   font-weight: var(--w-display);
    357   letter-spacing: var(--track-air);
    358   line-height: 1.14;
    359   text-wrap: balance;
    360 }
    361 
    362 .page-lede {
    363   margin: clamp(0.9rem, 2vw, 1.4rem) auto 0;
    364   max-width: 52ch;
    365   color: var(--ink-2);
    366   text-wrap: pretty;
    367 }
    368 
    369 /* 표제 아래 한 줄. 날짜와 언어가 여기 함께 선다 — 둘 다 이 문서에 대한 사실이고,
    370    따로 세우면 제목과 본문 사이에 줄만 하나 더 생긴다. 비어 있으면 gap 도 여백도
    371    없다(빈 flex 컨테이너라 높이가 0 이다). */
    372 .page-meta {
    373   display: flex; flex-wrap: wrap; align-items: baseline;
    374   justify-content: center; gap: 1.1rem;
    375   margin: 1.2rem 0 0;
    376   font-family: var(--mono); font-size: 0.74rem;
    377   letter-spacing: 0.08em; font-variant-numeric: tabular-nums;
    378   color: var(--ink-3);
    379 }
    380 .page-meta:empty { margin: 0; }
    381 
    382 /* ---------- language switch ---------- */
    383 
    384 /* 두 글자 코드 둘. 사이에 구분자를 두지 않는다 — 간격이 이미 나누고 있고,
    385    가운뎃점이나 슬래시를 넣으면 두 낱말이 한 낱말처럼 붙어 보인다.
    386 
    387    현재 언어가 제일 밝다. 크롬의 현재 항목이 밝아지는 것과 같은 규칙이고,
    388    반대로 두면 "지금 어디에 있는지"를 알려고 두 회색을 견줘야 한다.
    389    갈 수 있는 쪽에는 밑줄 자리가 비어 있다가 손이 닿으면 액센트로 채워진다 —
    390    색만으로 뜻을 전하지 않는다는 이 사이트의 규칙 그대로다. */
    391 /* 자기 크기를 스스로 갖는다. .page-meta 안에도 서고 프로젝트 히어로의 배지 줄
    392    아래에도 서는데, 부모마다 크기가 달라지면 같은 컨트롤이 페이지마다 다른
    393    물건으로 보인다. */
    394 .langs {
    395   display: inline-flex; gap: 0.55rem;
    396   font-family: var(--mono); font-size: 0.7rem;
    397   letter-spacing: 0.16em; text-transform: uppercase;
    398 }
    399 /* 히어로에서는 배지 줄 바로 아래 붙는다 — 둘 다 이 프로젝트에 대한 사실이다. */
    400 .hero-main > .langs { margin-top: 0.85rem; }
    401 .lang-cur { color: var(--ink); }
    402 /* 갈 수 있는 쪽은 이 사이트의 링크 단이다(--ink-3). --ink-4 는 날짜와 펜스의
    403    단이라 "있지만 누를 것은 아닌" 자리이고, 라이트 테마에서는 1.7:1 로 사실상
    404    안 보인다. 지금 판(--ink)과의 차이는 이 단에서도 충분히 크다. */
    405 .langs a {
    406   color: var(--ink-3);
    407   border-bottom: 1px solid transparent;
    408   transition: color var(--tr-ink), border-color var(--tr-ink);
    409 }
    410 .langs a:hover { color: var(--ink-2); border-bottom-color: var(--accent); }
    411 
    412 .eyebrow {
    413   margin: 0 0 0.9rem;
    414   font-size: 0.7rem; font-weight: 400;
    415   letter-spacing: 0.22em; text-transform: uppercase;
    416   color: var(--ink-3);
    417 }
    418 
    419 .crumbs { margin-bottom: 0.9rem; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; }
    420 .crumbs a { color: var(--ink-3); transition: color var(--tr-ink); }
    421 .crumbs a:hover { color: var(--accent); }
    422 
    423 /* ---------- wing title: 섹션 머리 ---------- */
    424 
    425 /* 가운데 제목 양옆으로 짧은 헤어라인. 페이지를 연으로 끊는 장치이고,
    426    위 여백이 넉넉해야 연이 연으로 읽힌다. */
    427 /* 갈래 머리. 옆줄은 없다 — 한 가지 일에 한 가지 장치이고, 위아래 여백이
    428    이미 연을 끊고 있다. 좁아지면 줄이는 게 아니라 뺀다. */
    429 /* 갈래 머리는 한 줄이다: 이름, 그 갈래로 가는 문까지 이어지는 헤어라인, 그리고
    430    문. 헤어라인은 장식이 아니라 둘을 한 줄의 양끝으로 묶는 것이고, 그래서 이름과
    431    문 사이에 무엇이 오든 둘은 같은 줄에 있다.
    432    아래 내용이 전부 왼쪽 정렬(카드의 라벨, 원장의 날짜 거터)이라 머리도 왼쪽에서
    433    시작한다. 가운데축은 히어로와 글 한 편의 것이고, 갈래의 머리는 목록의 것이다. */
    434 .room-head {
    435   display: flex; align-items: center;
    436   gap: clamp(0.9rem, 2vw, 1.5rem);
    437   margin: clamp(4.5rem, 11vw, 7rem) 0 clamp(1.5rem, 3.5vw, 2.25rem);
    438 }
    439 .room-head h2 {
    440   margin: 0; flex: none;
    441   font-size: clamp(1.05rem, 0.95rem + 0.55vw, 1.3rem);
    442   font-weight: var(--w-section); letter-spacing: var(--track-air); line-height: 1.2;
    443 }
    444 /* 남는 자리를 전부 먹는다. 문이 없는 갈래(프로젝트 묶음)에서는 끝까지 간다. */
    445 .room-rule { flex: 1 1 auto; min-width: 1rem; height: 1px; background: var(--rule); }
    446 .room-head-sub {
    447   margin: -0.75rem 0 clamp(1.75rem, 4vw, 2.5rem);
    448   max-width: 54ch;
    449   font-size: 0.92rem; color: var(--ink-3);
    450 }
    451 
    452 
    453 /* ---------- home hero ---------- */
    454 
    455 /* 첫 화면 전체를 쓴다. 스크롤 전에 사이트의 장르를 정하는 자리다. */
    456 .hero-home {
    457   min-height: calc(100svh - var(--masthead-h));
    458   display: grid; place-items: center; text-align: center;
    459   padding-block: clamp(2rem, 6vw, 4rem);
    460 }
    461 .hero-home > .wrap { display: grid; justify-items: center; }
    462 
    463 .hero-mark {
    464   width: clamp(3.4rem, 8vw, 5rem); aspect-ratio: 128 / 123;
    465   background-color: var(--mark-ink);
    466   -webkit-mask: url("/images/mark.png") center / contain no-repeat;
    467   mask: url("/images/mark.png") center / contain no-repeat;
    468   margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
    469 }
    470 
    471 .statement {
    472   margin: 0; max-width: 20ch;
    473   font-size: clamp(2.2rem, 1.3rem + 3.6vw, 4.2rem);
    474   font-weight: var(--w-display); letter-spacing: var(--track-air); line-height: 1.1;
    475   text-wrap: balance;
    476 }
    477 
    478 .statement-sub {
    479   margin: clamp(1.2rem, 3vw, 1.8rem) auto 0;
    480   max-width: 44ch; color: var(--ink-2); text-wrap: pretty;
    481 }
    482 
    483 /* 문. 채운 알약이 아니라 밑줄 그은 라벨이다. */
    484 .door {
    485   display: inline-block; margin-top: clamp(1.8rem, 4vw, 2.75rem);
    486   padding-bottom: 0.35rem;
    487   font-size: 0.72rem; letter-spacing: 0.22em; text-transform: uppercase;
    488   color: var(--ink-2);
    489   border-bottom: 1px solid var(--rule-3);
    490   transition: color var(--tr-ink), border-color var(--tr-ink);
    491 }
    492 .door:hover { color: var(--ink); border-color: var(--accent); }
    493 
    494 /* 페이지가 열릴 때 한 번. 스크롤에 반응해 숨었다 나타나지 않는다 —
    495    읽던 자리로 돌아왔을 때 사라지는 것은 고장으로 보인다. */
    496 @keyframes rise { from { opacity: 0; transform: translateY(0.75rem); } to { opacity: 1; transform: none; } }
    497 .js .hero-home [data-rise] { animation: rise 0.9s var(--ease) both; }
    498 .js .hero-home [data-rise="1"] { animation-delay: 0.12s; }
    499 .js .hero-home [data-rise="2"] { animation-delay: 0.28s; }
    500 .js .hero-home [data-rise="3"] { animation-delay: 0.42s; }
    501 
    502 /* ---------- home rooms ---------- */
    503 
    504 /* 홈의 갈래는 .wrap 보다 좁다. 1440 에서 1102px — 참조 카드(1123)와 같은 자리다.
    505    머리도 카드도 타일도 이 폭을 쓰므로 셋의 왼쪽 끝과 오른쪽 끝이 같다. */
    506 .room { max-width: calc(58rem + 2 * var(--gutter)); padding-block: 0; }
    507 
    508 /* 갈래의 위 여백은 그 갈래의 첫 물건이 진다. 전에는 언제나 머리가 졌는데,
    509    글 갈래에서는 머리가 카드 뒤로 갔다. */
    510 .room > .feature:first-child { margin-top: clamp(4.5rem, 11vw, 7rem); }
    511 /* 카드 뒤에 오는 머리는 갈래를 여는 것이 아니라 그 아래 목록을 여는 것이라
    512    덜 띄운다. 갈래 사이의 거리와 갈래 안의 거리는 달라야 한다. */
    513 .feature + .room-head { margin-top: clamp(2.5rem, 6vw, 3.5rem); }
    514 
    515 /* 문은 머리의 오른쪽 끝에 선다. 밑줄은 이 사이트의 문(.door)과 같은 물건이다. */
    516 .room-door {
    517   flex: none;
    518   padding-bottom: 0.25rem;
    519   font-size: 0.7rem; letter-spacing: 0.22em; text-transform: uppercase;
    520   color: var(--ink-3);
    521   border-bottom: 1px solid var(--rule-2);
    522   transition: color var(--tr-ink), border-color var(--tr-ink);
    523 }
    524 .room-door:hover { color: var(--ink); border-bottom-color: var(--accent); }
    525 
    526 /* ---------- featured frames ---------- */
    527 
    528 /* 액자는 테두리가 없다. 그림에만 테두리가 있고 캡션은 그 아래 떠 있다.
    529    flex 라 셋이든 넷이든 마지막 줄에 하나만 남지 않는다. */
    530 .frames {
    531   display: flex; flex-wrap: wrap; justify-content: center;
    532   gap: clamp(1.5rem, 3vw, 2.25rem);
    533 }
    534 
    535 .frame {
    536   flex: 1 1 220px; max-width: 400px;
    537   display: flex; flex-direction: column; align-items: center; text-align: center;
    538   background: transparent; border: 0;
    539 }
    540 
    541 .frame-media {
    542   width: 100%; aspect-ratio: 16 / 10;
    543   border: 1px solid var(--rule-2); border-radius: var(--radius);
    544   overflow: hidden; display: grid; place-items: center;
    545   background: var(--bg-2);
    546   transition: border-color var(--tr-ink);
    547 }
    548 .frame:hover .frame-media { border-color: var(--rule-3); }
    549 
    550 /* 그림은 제 색으로 쉰다. 탈색해 두었다가 손이 닿으면 돌려주는 건 다른 집의
    551    장치였다 — 여기서는 ommp 가 4K 컬러 스크린샷을 그대로 싣는 쪽이 기준이다. */
    552 .frame-media img { width: 100%; height: 100%; object-fit: cover; }
    553 
    554 .frame-mark {
    555   width: 42%; aspect-ratio: 1;
    556   background-color: var(--mark-ink);
    557   -webkit-mask: var(--mark-src) center / contain no-repeat;
    558   mask: var(--mark-src) center / contain no-repeat;
    559   opacity: 0.72; transition: opacity var(--tr-ink);
    560 }
    561 .frame:hover .frame-mark { opacity: 1; }
    562 
    563 .frame-title {
    564   display: flex; align-items: center; justify-content: center; gap: 0.5rem;
    565   margin: clamp(0.9rem, 2vw, 1.25rem) 0 0;
    566   font-size: 1.25rem; font-weight: var(--w-item); letter-spacing: var(--track-set);
    567 }
    568 .frame-title a { transition: color var(--tr-ink); }
    569 .frame:hover .frame-title a { color: var(--accent); }
    570 
    571 /* 이름 옆 작은 마크. 크기를 em 으로 잡아 제목이 커지면 같이 커진다. */
    572 .chip-mark {
    573   width: 1.55em; height: 1.55em; flex: none;
    574   display: grid; place-items: center;
    575   border: 1px solid var(--rule-2); border-radius: var(--radius);
    576   background: var(--bg-2);
    577 }
    578 .chip-mark::before {
    579   content: ""; width: 74%; height: 74%;
    580   background-color: var(--mark-ink);
    581   -webkit-mask: var(--mark-src) center / contain no-repeat;
    582   mask: var(--mark-src) center / contain no-repeat;
    583 }
    584 .chip-mark img { width: 74%; height: 74%; object-fit: contain; }
    585 
    586 .frame-blurb {
    587   margin: 0.6rem 0 0; max-width: 40ch;
    588   font-size: 0.9rem; color: var(--ink-3); text-wrap: pretty;
    589 }
    590 
    591 .frame-meta {
    592   display: flex; align-items: center; justify-content: center;
    593   gap: 0.9rem; margin-top: 0.9rem;
    594 }
    595 
    596 /* ---------- no image ---------- */
    597 
    598 /* 그림이 아직 없는 자리. 회색 상자로 때우지 않고 비었다고 말한다 —
    599    나중에 무엇을 채워야 하는지가 화면에 보여야 한다. */
    600 .noimg {
    601   width: 100%; height: 100%;
    602   display: grid; place-items: center;
    603   background:
    604     repeating-linear-gradient(45deg,
    605       transparent, transparent 7px,
    606       rgba(var(--ink-rgb), 0.022) 7px, rgba(var(--ink-rgb), 0.022) 14px);
    607 }
    608 .noimg::after {
    609   content: "no image";
    610   font-family: var(--mono); font-size: 0.62rem;
    611   letter-spacing: 0.2em; text-transform: uppercase;
    612   color: var(--ink-4);
    613   border: 1px solid var(--rule-2); border-radius: var(--radius);
    614   padding: 0.3rem 0.6rem;
    615 }
    616 
    617 /* ---------- project tiles ---------- */
    618 
    619 /* 카드다. 테두리와 바닥을 갖고, 글은 왼쪽에서 시작한다.
    620    테두리가 없던 때는 이 여섯 개가 페이지에서 유일하게 액자 없는 물건이라,
    621    위의 카드 하나만 상자로 떠 있고 아래는 글자만 떠다녔다. 갈래 머리가 왼쪽에서
    622    시작하는데 그 아래 내용이 가운데 정렬인 것도 같은 어긋남이었다. */
    623 /* 트랙에 상한을 두고 justify-content 로 가운데 모은다. `1fr` 로 두면 항목이
    624    두 개일 때 3열 격자의 왼쪽 두 칸에 붙어 페이지 축이 어긋난다 — 갈래마다
    625    개수가 다른 목록에서는 이게 항상 일어난다. */
    626 .tiles {
    627   display: grid;
    628   grid-template-columns: repeat(auto-fit, minmax(15rem, 21rem));
    629   justify-content: center;
    630   gap: clamp(0.5rem, 1.5vw, 1rem);
    631 }
    632 
    633 .tile {
    634   position: relative; isolation: isolate;
    635   display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start;
    636   gap: 0.5rem; text-align: left;
    637   padding: clamp(1.25rem, 3vw, 1.75rem);
    638   border: 1px solid var(--rule); border-radius: var(--radius);
    639   background: var(--bg-2);
    640   transition: border-color var(--tr-ink);
    641 }
    642 /* 손이 닿으면 테두리가 한 단 오른다 — 맨 위 카드와 같은 응답이다. */
    643 .tile:hover { border-color: var(--rule-2); }
    644 /* 누르면 눌린다. 빛이 번지는 대신 물건이 압축된다 — 이 집의 이름들이 전부
    645    모음을 눌러 뺀 것이고(sqzer, sqzass, ommp), 랜딩이 이미 같은 값을 쓴다.
    646    0.988 은 눈이 알아채되 글자가 흐려지지는 않는 지점이다. */
    647 
    648 .tile-title {
    649   display: flex; align-items: center; gap: 0.45rem;
    650   margin: 0; font-size: 1.05rem; font-weight: var(--w-item); letter-spacing: var(--track-set);
    651   color: var(--ink-2); transition: color var(--tr-ink);
    652 }
    653 .tile:hover .tile-title { color: var(--ink); }
    654 .tile-title .chip-mark { width: 1.4em; height: 1.4em; }
    655 
    656 .tile-blurb {
    657   margin: 0;
    658   font-size: 0.85rem; line-height: 1.6; color: var(--ink-3); text-wrap: pretty;
    659 }
    660 
    661 /* 사실 줄은 카드 아래쪽에 붙는다. margin-top:auto 가 그 위 여백을 전부 먹어서,
    662    설명 길이가 달라도 여섯 장의 사실 줄이 같은 높이에 선다. */
    663 .tile-meta {
    664   display: flex; align-items: center; gap: 0.7rem;
    665   margin-top: auto; padding-top: 0.35rem;
    666   font-family: var(--mono); font-size: 0.68rem; letter-spacing: 0.06em;
    667   color: var(--ink-4); font-variant-numeric: tabular-nums;
    668   transition: color var(--tr-ink);
    669 }
    670 .tile:hover .tile-meta { color: var(--ink-3); }
    671 
    672 .tile-links { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.1rem; }
    673 .tile-links a, .frame-meta a {
    674   position: relative; z-index: 2;
    675   display: inline-flex; color: var(--ink-4);
    676   transition: color var(--tr-ink);
    677 }
    678 .tile-links a:hover, .frame-meta a:hover { color: var(--accent); }
    679 
    680 /* 아이콘만 있는 링크에는 이름이 안 보인다. title 속성은 브라우저가 1초쯤 뒤에
    681    자기 모양으로 띄우는데, 그 지연과 모양이 사이트와 무관하다. 같은 값을
    682    data-tooltip 으로도 주고 우리가 그린다. aria-label 이 접근성을 이미 책임지므로
    683    이건 순수하게 눈에 보이는 쪽이고, 그래서 aria-hidden 이다. */
    684 [data-tooltip] { position: relative; }
    685 [data-tooltip]::after {
    686   content: attr(data-tooltip);
    687   position: absolute; bottom: calc(100% + 0.5rem); left: 50%;
    688   transform: translateX(-50%);
    689   padding: 0.28rem 0.5rem; white-space: nowrap; pointer-events: none;
    690   background: var(--bg-3); color: var(--ink-2);
    691   border: 1px solid var(--rule-2); border-radius: var(--radius);
    692   font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase;
    693   opacity: 0; transition: opacity var(--tr-ink);
    694 }
    695 [data-tooltip]:hover::after, [data-tooltip]:focus-visible::after { opacity: 1; }
    696 html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none; }
    697 
    698 /* 타일 전체가 상세 페이지로 가는 링크. 아이콘만 위로 올린다. */
    699 .tile-link::after { content: ""; position: absolute; inset: 0; }
    700 
    701 /* 언어 배지 — 작고 넓게 벌어진 대문자. */
    702 .lang {
    703   font-family: var(--mono); font-size: 0.62rem;
    704   letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-3);
    705 }
    706 .stars { font-variant-numeric: tabular-nums; }
    707 
    708 /* ---------- project mosaic ---------- */
    709 
    710 /* 첫 장이 크고 나머지 셋이 옆에 쌓인다. 대칭 격자는 여섯 개가 다 같은 무게라고
    711    말하는데, 갈래에는 앞에 세울 것이 있다.
    712 
    713    그림이 있는 프로젝트는 둘뿐이다. 없는 것은 자기 색으로 선다 — 프로젝트마다
    714    이미 색이 하나씩 정해져 있고(상세 페이지의 --accent 와 같은 값), 그 색이
    715    "그림 없음" 판보다 그 프로젝트에 대해 더 많이 말한다. 그림이 생기면 카탈로그의
    716    cover 한 줄로 들어온다. */
    717 .mosaic {
    718   display: grid;
    719   grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
    720   grid-template-rows: repeat(3, minmax(0, 1fr));
    721   gap: clamp(0.5rem, 1.5vw, 1rem);
    722 }
    723 /* 첫 장이 왼쪽 열을 통째로 쓴다. :first-child 라 개수가 넷이 아니어도 안 깨진다. */
    724 .mosaic > :first-child { grid-row: 1 / -1; }
    725 
    726 .mtile {
    727   position: relative; display: flex; align-items: flex-end;
    728   overflow: hidden;
    729   border: 1px solid var(--rule); border-radius: var(--radius);
    730   background: var(--bg-2);
    731   transition: border-color var(--tr-ink);
    732 }
    733 .mtile:hover { border-color: var(--rule-2); }
    734 /* 작은 장의 키를 여기서 정한다. 큰 장은 셋의 합만큼 자라므로 자기 비율이 없다. */
    735 .mosaic > :not(:first-child) { aspect-ratio: 2.1 / 1; }
    736 
    737 /* 바탕. 그림이 있으면 그림이, 없으면 그 프로젝트의 색이 깔린다.
    738 
    739    평상시에는 색이 빠져 있고 손이 닿으면 돌아온다. 이 시트의 첫 줄에 적힌
    740    전제가 그것이다 — "색은 평상시에 없고 손이 닿을 때만 나온다". 그림에까지
    741    그 규칙을 적용한 것이고, 크기나 자리는 그대로이므로 hover 가 색만 바꾼다는
    742    규칙도 지켜진다. 박자는 이 시트의 유일한 박자다.
    743    (한때 이 탈색을 걷어냈던 적이 있다. 그때는 참조에서 그대로 가져온 값이라
    744    뺐고, 지금은 이 사이트의 전제를 그림까지 밀고 간 것이라 뜻이 다르다.) */
    745 .mtile-art {
    746   position: absolute; inset: 0;
    747   display: grid; place-items: center;
    748   background: color-mix(in srgb, var(--tint) 30%, var(--bg-2));
    749   filter: grayscale(1);
    750   transition: filter var(--tr-ink);
    751 }
    752 .mtile:hover .mtile-art, .mtile:focus-visible .mtile-art { filter: grayscale(0); }
    753 .mtile-art img { width: 100%; height: 100%; object-fit: cover; }
    754 /* 마크는 잉크가 아니라 그 프로젝트의 색을 진하게 쓴 것이다 — 바탕과 같은 색의
    755    두 단이라 한 물건으로 읽힌다. */
    756 .mtile-mark {
    757   width: 42%; aspect-ratio: 1;
    758   background-color: color-mix(in srgb, var(--tint) 62%, var(--ink));
    759   -webkit-mask: var(--mark-src) center / contain no-repeat;
    760   mask: var(--mark-src) center / contain no-repeat;
    761 }
    762 
    763 /* 글은 바닥에 앉고, 그 아래에서 바탕색이 올라와 그림을 덮는다. 밝은 테마에서는
    764    밝은 장막이고 어두운 테마에서는 어두운 장막이라, 글은 두 테마 모두에서 --ink
    765    한 색으로 읽힌다. 참조 사이트는 다크 전용이라 흰 글자 한 벌로 끝난다. */
    766 .mtile-body {
    767   position: relative; z-index: 1; width: 100%;
    768   display: flex; flex-direction: column; gap: 0.3rem;
    769   padding: clamp(1rem, 2.4vw, 1.6rem);
    770   /* 글이 앉는 띠는 바탕색으로 꽉 차 있고 그 위에서 풀린다. 그라데이션이 글자
    771      높이 안에서 투명해지면 밝은 그림 위의 밝은 글자가 되는데, ommp 의 표지가
    772      정확히 그랬다. 두 테마 모두 --bg 로 가므로 글은 --ink 한 색이면 된다. */
    773   background: linear-gradient(to top,
    774     var(--bg) 0%,
    775     var(--bg) 42%,
    776     color-mix(in srgb, var(--bg) 72%, transparent) 72%,
    777     transparent 100%);
    778 }
    779 .mtile-title {
    780   font-size: clamp(0.95rem, 0.9rem + 0.3vw, 1.05rem);
    781   font-weight: var(--w-item); letter-spacing: var(--track-set);
    782   color: var(--ink);
    783 }
    784 .mtile-blurb {
    785   max-width: 44ch; font-size: 0.8rem; line-height: 1.5;
    786   color: var(--ink-2); text-wrap: pretty;
    787 }
    788 .mtile-stars {
    789   margin-top: 0.15rem;
    790   font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.08em;
    791   font-variant-numeric: tabular-nums; color: var(--ink-3);
    792 }
    793 /* 큰 장은 글도 한 단 크다 — 크기가 순서를 말한다. */
    794 .mosaic > :first-child .mtile-title { font-size: clamp(1.1rem, 1rem + 0.5vw, 1.35rem); }
    795 
    796 /* 여기서부터 작은 장의 키는 글이 정한다. 비율을 고정해 두면 폭이 줄면서 상자가
    797    글보다 먼저 작아져 잘린다 — 카드에서 겪은 것과 같은 어긋남이다. 그림은 어차피
    798    잘리는 물건이므로 여기서는 글이 이기게 둔다. 큰 장은 셋의 합만큼 자란다. */
    799 @media (max-width: 72rem) {
    800   .mosaic > :not(:first-child) { aspect-ratio: auto; }
    801 }
    802 
    803 /* 좁아지면 한 열로 쌓고 전부 같은 비율이 된다. 두 열로 두면 작은 장이 글을
    804    담기에 좁아진다. */
    805 @media (max-width: 46rem) {
    806   .mosaic { grid-template-columns: 1fr; grid-template-rows: none; }
    807   .mosaic > :first-child { grid-row: auto; }
    808   .mosaic > * { aspect-ratio: 2 / 1; }
    809 }
    810 
    811 /* ---------- ledger ---------- */
    812 
    813 .ledger { list-style: none; margin: 0; padding: 0; }
    814 
    815 .ledger-year {
    816   margin-top: clamp(2.5rem, 6vw, 4rem);
    817   text-align: center;
    818   font-family: var(--mono); font-size: 0.68rem;
    819   letter-spacing: 0.22em; color: var(--ink-4);
    820 }
    821 .ledger-year:first-child { margin-top: 0; }
    822 
    823 /* 행 사이에 선이 없다. 간격이 나눈다.
    824 
    825    손이 닿아도 바닥이 안 바뀐다. 회색 판이 깔리면 그 행만 다른 재질이 된 것처럼
    826    보이고, 목록을 훑는 동안 판이 행에서 행으로 따라다닌다 — 읽는 자리에서
    827    움직이는 것이 하나 생기는 셈이다. 이 사이트의 hover 는 색만 바꾼다는 규칙에
    828    맞춰, 여기서도 바뀌는 것은 날짜와 제목의 잉크뿐이다.
    829 
    830    가로 패딩과 같은 크기의 음수 마진이 남아 있는 것은 판을 깔기 위해서가 아니라
    831    누를 수 있는 자리를 글자 폭보다 넓게 두기 위해서다. 글은 제자리에 선다.
    832    키보드 포커스는 전역 :focus-visible 링이 진다. */
    833 .ledger-link {
    834   display: grid;
    835   grid-template-columns: clamp(5rem, 11vw, 8rem) minmax(0, 1fr);
    836   align-items: baseline; gap: clamp(1rem, 2.5vw, 2rem);
    837   padding: 1.1rem 1rem; margin-inline: -1rem;
    838 }
    839 
    840 .ledger-date {
    841   font-family: var(--mono); font-size: 0.72rem;
    842   letter-spacing: 0.12em; text-transform: uppercase;
    843   font-variant-numeric: tabular-nums;
    844   color: var(--ink-4); transition: color var(--tr-ink);
    845 }
    846 .ledger-link:hover .ledger-date { color: var(--accent); }
    847 
    848 .ledger-body { display: block; min-width: 0; }
    849 .ledger-title {
    850   display: block;
    851   font-size: clamp(1.15rem, 1rem + 0.7vw, 1.55rem);
    852   font-weight: var(--w-item); letter-spacing: var(--track-set); line-height: 1.25;
    853   color: var(--ink-2); transition: color var(--tr-ink); text-wrap: balance;
    854   /* 홈의 글 갈래가 넓은 방으로 옮겨 가면서 이 열이 1080px 까지 열렸다. 제목은
    855      한 줄로 읽는 물건이라 폭에 상한이 필요하다 — 설명(58ch)에는 이미 있었다. */
    856   max-width: 42ch;
    857 }
    858 .ledger-link:hover .ledger-title { color: var(--ink); }
    859 .ledger-desc {
    860   display: block; margin-top: 0.3rem; max-width: 58ch;
    861   font-size: 0.88rem; color: var(--ink-3);
    862 }
    863 
    864 @media (max-width: 40rem) {
    865   .ledger-link { grid-template-columns: 1fr; gap: 0.5rem; padding-inline: 0; margin-inline: 0; }
    866 }
    867 
    868 /* 홈의 맨 위 글. 그림과 이름표가 테두리 하나 안에 같이 있다 — 미술관 라벨과
    869    작품의 관계다. 전에는 그림을 가운데 세우고 글을 그 아래 따로 세웠는데, 폭도
    870    정렬도 달라서 두 물건으로 읽혔다.
    871 
    872    /projects/ 의 액자는 테두리를 그림에만 두르고 캡션을 아래에 띄운다. 그건 여러
    873    점이 나란히 걸릴 때의 문법이고, 여기는 한 점뿐이라 라벨이 붙어 있어야 한 작품이
    874    된다.
    875 
    876    호버는 색만 바꾼다: 테두리가 한 단, 날짜가 액센트로, 제목이 잉크로. 원장의
    877    행과 같은 응답이다. */
    878 .feature {
    879   display: grid;
    880   grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
    881   /* 카드의 크기는 글이 정하지 않는다. 어느 글이 맨 위에 오든 같은 상자다.
    882      8/3 은 지어낸 값이 아니라 두 열에서 따라 나온다: 그림이 카드 폭의 0.6 을
    883      차지하고 그 그림이 16:10 이므로 카드 높이 = 0.6W ÷ 1.6 = W ÷ 2.667 이다.
    884      여기 비율을 걸어 두면 이름표가 행을 늘리지 못하고, 그림은 자기 칸을 채우기만
    885      하면 되므로 aspect-ratio 를 따로 갖지 않는다. */
    886   aspect-ratio: 8 / 3;
    887   /* 이 카드 안의 크기는 창이 아니라 **카드**를 따른다. 아래 cqw 가 그 단위다.
    888      창에 매달았더니 카드는 좁아지는데 글자는 그만큼 안 줄어서, 920px 근처에서
    889      제목의 디센더와 설명 둘째 줄이 상자 밖으로 밀려 잘렸다. 상자와 글자가 같은
    890      수에 매여 있으면 그 어긋남이 생길 자리가 없다. */
    891   container-type: inline-size;
    892   border: 1px solid var(--rule);
    893   border-radius: var(--radius);
    894   overflow: hidden;
    895   background: var(--bg-2);
    896   text-align: left;
    897   transition: border-color var(--tr-ink);
    898 }
    899 .feature:hover { border-color: var(--rule-2); }
    900 /* 미리보기가 없는 글은 한 칸이다. 빈 칸을 남기면 그 글만 뭔가 빠진 것처럼 보인다. */
    901 .feature:not(:has(.feature-media)) { grid-template-columns: 1fr; }
    902 
    903 /* 그림은 자기 칸을 채운다. 높이는 이름표가 정하고 그림이 거기에 맞춰 잘린다 —
    904    16:9 를 고정하면 제목이 길어질 때 두 칸의 키가 어긋난다.
    905    손이 닿으면 안에서 돌아간다. 상자는 자리도 크기도 안 바꾸므로, 호버에
    906    레이아웃이 움직이지 않는다는 규칙은 그대로다.
    907    poster 는 영상의 첫 프레임이다. 시작할 때도 되감을 때도 같은 그림이라 이음매가
    908    없다. */
    909 .feature-media {
    910   display: block; margin: 0;
    911   /* 높이 바닥을 따로 두지 않는다. width/height 속성이 16:9 를 알려 주므로 그
    912      비율이 그대로 행 높이의 바닥이 되고, 라벨이 더 길면 라벨이 이긴다. 예전의
    913      clamp 는 어느 폭에서도 이 값을 넘지 못해서 아무 일도 하지 않았다. */
    914   width: 100%; height: 100%;
    915   /* 16:10 은 이 사이트의 그림 비율이다 — /projects/ 의 .frame-media 가 같은 값을
    916      쓴다. 이 카드만 <video> 의 width/height 속성이 주는 16:9 를 그대로 행 높이로
    917      써서, 카드가 3.09:1 로 납작했다(참조는 2.66:1). CSS 비율이 내재 비율을
    918      덮으므로 행은 이제 max(이름표의 자연 높이, 그림 폭 ÷ 1.6) 이고, 제목이
    919      길어지면 이름표가 이긴다.
    920      좌우가 잘리는데 이 포스터는 만다라도 인물도 가운데 있어서 잘려 나가는 것은
    921      배경이다. 반대로 납작하게 두면 세로가 잘리고, 그때 잘리는 것은 머리다. */
    922   aspect-ratio: 16 / 10;
    923   object-fit: cover;
    924   border-right: 1px solid var(--rule);
    925   background: var(--bg-3);
    926 }
    927 
    928 .feature-label {
    929   display: flex; flex-direction: column; justify-content: center;
    930   gap: clamp(0.7rem, 1.9cqw, 1.2rem); min-width: 0;
    931   /* 상자가 고정이므로 넘칠 수 있는 쪽은 글이다. 마지막 방어선. */
    932   overflow: hidden;
    933   padding: clamp(1.1rem, 3.8cqw, 2.4rem);
    934 }
    935 .feature-date {
    936   font-family: var(--mono); font-size: 0.7rem;
    937   letter-spacing: 0.14em; font-variant-numeric: tabular-nums;
    938   color: var(--ink-4); transition: color var(--tr-ink);
    939 }
    940 .feature:hover .feature-date { color: var(--accent); }
    941 .feature-title {
    942   margin: 0;
    943   /* 카드 폭의 3.7% — 1102px 카드에서 40.8px 이고, 이름표의 측정폭 353px 중
    944      제목이 약 306px 을 쓴다. 제목이 자기 열을 거의 채우는 것이 이 카드가
    945      헤드라인처럼 읽히는 이유다(참조는 353 중 353).
    946      cqw 라 카드가 좁아지면 글자도 같은 비율로 좁아진다. rem 하한은 아주 좁은
    947      카드에서 읽히는 크기의 바닥이다. */
    948   font-size: clamp(1.25rem, 3.7cqw, 2.3rem);
    949   font-weight: var(--w-section); letter-spacing: var(--track-air); line-height: 1.2;
    950   color: var(--ink-2); transition: color var(--tr-ink);
    951   /* 세 줄까지. 1440 에서 이름표 안쪽이 372px 이고 날짜·설명·문·간격이 125px 을
    952      쓰므로 제목에 247px 이 남는다 — 세 줄이 178px 이라 어떤 제목이 와도 들어온다.
    953 
    954      -webkit-line-clamp 를 쓰지 않는다. 이 조합에서 display 가 flow-root 로
    955      계산돼 clamp 자체가 죽고 overflow:hidden 만 남았는데, 그러면 상자 높이가
    956      line-height(1.2em)라 글자의 실제 줄상자(1.44em)가 6px 잘린다 — "Beginning"
    957      의 g 아래가 그렇게 날아갔다. 줄 수는 max-height 로 세고, padding-bottom 이
    958      디센더가 앉을 자리를 만든다. 둘 다 em 이라 크기를 바꿔도 따라온다. */
    959   padding-bottom: 0.25em;
    960   max-height: calc(3 * 1.2em + 0.25em);
    961   overflow: hidden;
    962   /* 이 시트의 모든 제목이 balance 다. 한때 여기만 pretty 로 바꿔 두고 "balance 는
    963      제목을 한 줄로 붙인다"고 적었는데, 재 보니 39자 제목에서 둘 다 세 줄이고
    964      balance 쪽이 줄 길이만 고르게 맞춘다. 틀린 이유로 예외를 만들 뻔했다. */
    965   text-wrap: balance;
    966 }
    967 .feature:hover .feature-title { color: var(--ink); }
    968 .feature-desc {
    969   margin: 0; max-width: 38ch; color: var(--ink-3); text-wrap: pretty;
    970   font-size: clamp(0.85rem, 1.65cqw, 1rem);
    971   /* 두 줄까지. 줄 간격이 1.65em 이라 글자 줄상자(1.44em)보다 넉넉해서 여기는
    972      따로 자리를 만들 필요가 없다. */
    973   line-height: 1.65;
    974   max-height: calc(2 * 1.65em);
    975   overflow: hidden;
    976 }
    977 
    978 /* 글 덩이의 아래 끝. 문(.door)과 같은 물건이다 — 밑줄 그은 마이크로캡스.
    979    align-self 가 없으면 flex 열에서 가로로 늘어나 밑줄이 라벨 폭을 가로지른다. */
    980 .feature-cta {
    981   align-self: start;
    982   padding-bottom: 0.3rem;
    983   font-size: 0.7rem; letter-spacing: 0.22em; text-transform: uppercase;
    984   color: var(--ink-3);
    985   border-bottom: 1px solid var(--rule-2);
    986   transition: color var(--tr-ink), border-color var(--tr-ink);
    987 }
    988 /* 밑줄은 잉크 사다리에서 한 단만 오른다. 액센트는 이 카드에 하나뿐이고 날짜가
    989    그걸 갖는다 — 위 규칙에 그렇게 적어 놓고 여기서 두 번째를 켰었다. */
    990 .feature:hover .feature-cta { color: var(--ink-2); border-bottom-color: var(--rule-3); }
    991 
    992 /* 좁아지면 그림이 위로 올라간다. 옆으로 두면 두 칸 다 못 쓸 만큼 좁아진다.
    993    64rem 인 이유: 그 아래에서는 고정 8/3 상자에 네 줄이 안 들어간다. 상자를
    994    늘리는 대신 쌓는다 — 쌓인 카드는 글이 크기를 정해도 되고, 그게 이 폭에서
    995    맞는 답이다. */
    996 @media (max-width: 56rem) {
    997   .feature { grid-template-columns: 1fr; aspect-ratio: auto; }
    998   .feature-media {
    999     aspect-ratio: 16 / 10;
   1000     border-right: 0; border-bottom: 1px solid var(--rule);
   1001   }
   1002 }
   1003 
   1004 /* ---------- buttons ---------- */
   1005 
   1006 .btn-row { display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; margin-top: 1.75rem; }
   1007 
   1008 .btn {
   1009   display: inline-flex; align-items: center; gap: 0.5rem;
   1010   height: 2.5rem; padding-inline: 1.15rem;
   1011   border: 1px solid var(--rule-2); border-radius: var(--radius);
   1012   font-size: 0.72rem; letter-spacing: 0.16em; text-transform: uppercase;
   1013   color: var(--ink-2);
   1014   transition: color var(--tr-ink), border-color var(--tr-ink), background var(--tr-ink);
   1015 }
   1016 /* 채운 버튼은 잉크로 채운다. 액센트로 채우면 그게 첫 화면에서 제일 큰 색이 된다. */
   1017 .btn-primary { background: var(--ink); border-color: var(--ink); color: var(--bg); }
   1018 .btn-primary:hover { background: var(--ink); filter: brightness(1.08); }
   1019 .btn-ghost:hover { color: var(--ink); border-color: var(--rule-3); }
   1020 .btn-text { border-color: transparent; padding-inline: 0.35rem; }
   1021 .btn-text:hover { color: var(--accent); }
   1022 
   1023 .back {
   1024   display: inline-flex; align-items: center; gap: 0.45rem;
   1025   margin-top: 2rem;
   1026   font-size: 0.7rem; letter-spacing: 0.18em; text-transform: uppercase;
   1027   color: var(--ink-4);
   1028   transition: color var(--tr-ink);
   1029 }
   1030 .back:hover { color: var(--ink-2); }
   1031 
   1032 /* ---------- project detail ---------- */
   1033 
   1034 /* 프로젝트마다 자기 색조를 갖는다. 전역 --accent 를 이 페이지 안에서만
   1035    덮으므로 eyebrow 대시·특징 번호·링크·포커스가 한꺼번에 따라간다. 값이 없는
   1036    프로젝트는 인라인 style 이 아예 안 붙어서 사이트 기본값을 쓴다.
   1037    대비 범위는 좁게 잡는다 — 중간 명도, 낮은 채도. 니어블랙 위에서 읽히면서도
   1038    페이지를 자기 색으로 물들이지는 않는 띠다. */
   1039 .project { padding-bottom: clamp(3rem, 8vw, 6rem); }
   1040 
   1041 /* 브리프는 읽는 폭 안에 살고 제목도 한 단계 작다. 등급 차이가 열 개수가
   1042    아니라 타이포로 보여야 한다 — 그러지 않으면 "같은 페이지, 더 짧은 것"이다. */
   1043 .project[data-tier="brief"] .hero-name { font-size: clamp(2rem, 1.6rem + 1.6vw, 2.8rem); }
   1044 .project[data-tier="brief"] .hero { border-bottom: 0; padding-bottom: clamp(1.5rem, 3vw, 2rem); }
   1045 
   1046 .hero {
   1047   display: grid; grid-template-columns: 1.35fr 1fr;
   1048   gap: clamp(1.5rem, 5vw, 4rem); align-items: center;
   1049   padding-block: clamp(2rem, 5vw, 3.5rem) clamp(2.5rem, 6vw, 4rem);
   1050   border-bottom: 1px solid var(--rule);
   1051 }
   1052 .hero-main { min-width: 0; }
   1053 
   1054 /* eyebrow 앞의 액센트 대시. 제품 명패의 시작을 표시한다. */
   1055 .hero .eyebrow { display: flex; align-items: center; gap: 0.75rem; }
   1056 .hero .eyebrow::before {
   1057   content: ""; width: 1.75rem; height: 1px; background: var(--accent); flex: none;
   1058 }
   1059 
   1060 .hero-name {
   1061   display: flex; align-items: center; gap: 0.6rem; margin: 0;
   1062   font-size: clamp(2.6rem, 1.9rem + 2.6vw, 3.8rem);
   1063   font-weight: var(--w-display); letter-spacing: var(--track-air); line-height: 1.05;
   1064 }
   1065 .hero-name .chip-mark { width: 1.05em; height: 1.05em; }
   1066 
   1067 .hero-tagline { margin: 1.1rem 0 0; max-width: 46ch; color: var(--ink-2); text-wrap: pretty; }
   1068 
   1069 .badges { display: flex; flex-wrap: wrap; gap: 1.25rem; margin: 1.5rem 0 0; padding: 0; list-style: none; }
   1070 .badges li {
   1071   font-family: var(--mono); font-size: 0.66rem;
   1072   letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-4);
   1073 }
   1074 
   1075 .hero .btn-row { justify-content: flex-start; }
   1076 
   1077 .hero-aside { display: grid; place-items: center; }
   1078 .medallion {
   1079   display: grid; place-items: center;
   1080   width: min(100%, 20rem); aspect-ratio: 1;
   1081   border: 1px solid var(--rule); border-radius: var(--radius);
   1082   background: var(--bg-2); overflow: hidden;
   1083 }
   1084 .medallion img { width: 76%; height: 76%; object-fit: contain; }
   1085 .medallion-mark::before {
   1086   content: ""; width: 72%; height: 72%;
   1087   background-color: var(--mark-ink);
   1088   -webkit-mask: var(--mark-src) center / contain no-repeat;
   1089   mask: var(--mark-src) center / contain no-repeat;
   1090 }
   1091 
   1092 @media (max-width: 52rem) {
   1093   .hero { grid-template-columns: minmax(0, 1fr); text-align: center; }
   1094   .hero .eyebrow, .hero-name, .hero .btn-row { justify-content: center; }
   1095   .hero-tagline, .badges { margin-inline: auto; justify-content: center; }
   1096   /* 좁을 때 옆칸은 지우지 않고 위로 올린다. 지우면 그 프로젝트만 그림이 없는
   1097      것처럼 보인다. */
   1098   .hero-aside { order: -1; }
   1099   .medallion { width: min(60%, 13rem); }
   1100 }
   1101 
   1102 /* ---------- facts ---------- */
   1103 
   1104 .facts {
   1105   display: flex; flex-wrap: wrap; justify-content: center;
   1106   margin-block: clamp(2rem, 5vw, 3rem);
   1107   padding-block: clamp(1.5rem, 3vw, 2rem);
   1108   border-bottom: 1px solid var(--rule);
   1109 }
   1110 .fact {
   1111   flex: 1 1 9rem; display: flex; flex-direction: column; gap: 0.3rem;
   1112   align-items: center; text-align: center; padding-inline: 1rem;
   1113   border-left: 1px solid var(--rule);
   1114 }
   1115 .fact:first-child { border-left: 0; }
   1116 .fact-value {
   1117   font-size: clamp(1.9rem, 1.5rem + 1.6vw, 2.9rem);
   1118   font-weight: var(--w-display); letter-spacing: 0.005em; line-height: 1;
   1119   font-variant-numeric: tabular-nums;
   1120 }
   1121 .fact-label {
   1122   font-size: 0.64rem; letter-spacing: 0.22em; text-transform: uppercase;
   1123   color: var(--ink-4);
   1124 }
   1125 
   1126 /* ---------- lattice ---------- */
   1127 
   1128 .block { padding-block: clamp(2.5rem, 6vw, 4rem); border-bottom: 1px solid var(--rule); }
   1129 .block:last-child { border-bottom: 0; }
   1130 .block-head { margin-bottom: clamp(1.75rem, 4vw, 2.5rem); text-align: center; }
   1131 .block-title {
   1132   margin: 0; font-size: clamp(1.5rem, 1.25rem + 1.1vw, 2.1rem);
   1133   font-weight: var(--w-section); letter-spacing: var(--track-air); text-wrap: balance;
   1134 }
   1135 .block-intro { margin: 0.8rem auto 0; max-width: 56ch; color: var(--ink-3); }
   1136 
   1137 /* 선은 음수 마진으로 겹친 1px 이다. 개수와 열수가 무엇이든 맞아 들어가고
   1138    :nth-child 규칙이 필요 없다. 20rem 은 임의값이 아니다 — 71rem 안에서 열이
   1139    3개가 되고, 특징을 여섯 개로 쓰면 두 줄이 정확히 찬다. */
   1140 .lattice {
   1141   display: grid;
   1142   grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
   1143   border-top: 1px solid var(--rule);
   1144   border-left: 1px solid var(--rule);
   1145 }
   1146 .cell {
   1147   position: relative; isolation: isolate;
   1148   padding: clamp(1.25rem, 3vw, 1.75rem) clamp(1rem, 2vw, 1.5rem);
   1149   min-width: 0;
   1150   margin: 0 -1px -1px 0;
   1151   border-right: 1px solid var(--rule);
   1152   border-bottom: 1px solid var(--rule);
   1153 }
   1154 /* hover 에서 위로 올린다. 그러지 않으면 이웃에 가려 두 변만 밝아진다. */
   1155 .cell:hover { z-index: 1; border-color: var(--rule-2); }
   1156 .cell { transition: border-color var(--tr-ink); }
   1157 
   1158 .cell-index {
   1159   display: block; font-family: var(--mono); font-size: 0.66rem;
   1160   letter-spacing: 0.18em; color: var(--ink-4); margin-bottom: 0.7rem;
   1161   transition: color var(--tr-ink);
   1162 }
   1163 .cell:hover .cell-index { color: var(--accent); }
   1164 .cell-title { margin: 0; font-size: 1rem; font-weight: var(--w-label); letter-spacing: var(--track-set); }
   1165 .cell-desc { margin: 0.5rem 0 0; font-size: 0.86rem; color: var(--ink-3); text-wrap: pretty; }
   1166 
   1167 /* ---------- terminal ---------- */
   1168 
   1169 .term {
   1170   border: 1px solid var(--rule-2); border-radius: var(--radius);
   1171   overflow: hidden; background: var(--bg-recess);
   1172   max-width: 52rem; margin-inline: auto;
   1173 }
   1174 .term-bar {
   1175   display: flex; align-items: center; gap: 0.7rem;
   1176   padding: 0.55rem 0.85rem;
   1177   border-bottom: 1px solid var(--rule); background: var(--bg-2);
   1178 }
   1179 .term-dots { display: inline-flex; gap: 0.32rem; }
   1180 .term-dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--rule-3); }
   1181 .term-dots i:first-child { background: var(--ink-4); }
   1182 .term-name { font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.1em; color: var(--ink-4); }
   1183 
   1184 .term-body {
   1185   margin: 0; padding: 1.2rem 1.3rem; overflow-x: auto;
   1186   font-family: var(--mono); font-size: 0.8rem; line-height: 1.85; white-space: pre;
   1187 }
   1188 .term-body code { font: inherit; background: none; padding: 0; border: 0; }
   1189 .term-body span { display: block; }
   1190 .term-cmd { color: var(--ink); }
   1191 /* 주석은 기울이고, 출력은 중립 회색이다. 출력에 색을 주면 그게 결과가 아니라
   1192    상태로 읽힌다. */
   1193 .term-comment { color: var(--ink-4); font-style: italic; }
   1194 .term-out { color: var(--ink-3); }
   1195 .term-caption {
   1196   margin: 0.8rem auto 0; max-width: 52rem; text-align: center;
   1197   font-family: var(--mono); font-size: 0.68rem; letter-spacing: 0.06em; color: var(--ink-4);
   1198 }
   1199 
   1200 /* ---------- cta ---------- */
   1201 
   1202 .cta { margin-top: clamp(3rem, 7vw, 5rem); padding-block: clamp(2.5rem, 6vw, 5rem); text-align: center; }
   1203 .cta-inner { max-width: 46ch; margin-inline: auto; }
   1204 .cta-title {
   1205   margin: 0; font-size: clamp(1.6rem, 1.3rem + 1.4vw, 2.4rem);
   1206   font-weight: var(--w-display); letter-spacing: var(--track-air); text-wrap: balance;
   1207 }
   1208 .cta-text { margin: 0.9rem 0 0; color: var(--ink-3); }
   1209 
   1210 /* ---------- about ---------- */
   1211 
   1212 .about-head { text-align: center; }
   1213 .portrait { margin: 0 auto clamp(1.5rem, 4vw, 2.25rem); width: min(100%, 12rem); }
   1214 .portrait img {
   1215   display: block; width: 100%; aspect-ratio: 4 / 5; object-fit: cover;
   1216   border: 1px solid var(--rule-2); border-radius: var(--radius);
   1217 }
   1218 
   1219 /* ---------- toc & pager ---------- */
   1220 
   1221 .toc { margin: 2rem auto 2.5rem; max-width: var(--read); padding: 1rem 1.25rem; border-left: 1px solid var(--rule-2); }
   1222 .toc-label {
   1223   margin: 0 0 0.5rem; font-size: 0.66rem;
   1224   letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-4);
   1225 }
   1226 .toc ul { list-style: none; margin: 0; padding: 0; }
   1227 .toc ul ul { padding-left: 1rem; }
   1228 .toc li { margin: 0.3rem 0; font-size: 0.88rem; }
   1229 .toc a { color: var(--ink-3); transition: color var(--tr-ink); }
   1230 .toc a:hover { color: var(--accent); }
   1231 
   1232 /* ---------- reading rail ---------- */
   1233 
   1234 /* 목차의 두 번째 모습. 넓은 화면에서 오른쪽 가장자리에 서고, 절마다 세로
   1235    헤어라인 한 칸이며 칸의 높이가 그 절이 글에서 차지하는 몫이다. 목록은 절이
   1236    몇 개인지 말하고 지도는 어느 절이 긴지 말한다.
   1237 
   1238    레일이 실제로 만들어졌을 때만 목록이 물러난다(.has-rail 은 ui.js 가 붙인다).
   1239    스크립트가 못 오면 목록이 그대로 남고, 그게 이 장치의 폴백이다.
   1240 
   1241    처음 판은 본문 칼럼에 붙여 놓고 눈금 자체만 손을 받게 했다. 재 보니 레일이
   1242    차지한 넓이의 14% 만 반응했고, 눈금에서 이름으로 커서를 옮기면 이름이
   1243    대부분 사라졌다 — 이름과 세로로 겹치는 28% 구간에서만 건너갈 수 있었다.
   1244    가끔 되는 조작은 안 되는 조작보다 나쁘다. 지금은 여백 전체가 손을 받는다. */
   1245 
   1246 .rail { display: none; }
   1247 
   1248 /* 이 시트의 첫 min-width 다. 나머지는 전부 max-width — 넓게 시작해서 좁아지면
   1249    뺀다는 자세이고, 이것만 예외인 이유는 레일이 없던 자리를 요구하는 유일한
   1250    요소이기 때문이다. */
   1251 @media (min-width: 80rem) {
   1252   .rail {
   1253     display: grid;
   1254     /* 이름이 안쪽, 눈금이 바깥쪽이다. 눈금은 스크롤 막대 옆에 서는 스크롤
   1255        가구이고 이름은 글 쪽으로 흐른다 — 반대로 두면 눈이 글에서 멀어지는
   1256        방향으로 읽어 나가야 한다. */
   1257     grid-template-columns: minmax(0, 1fr) 2px;
   1258     column-gap: 0.9rem;
   1259     row-gap: 0.3rem;
   1260     /* 행 높이는 ui.js 가 grid-template-rows 로 쓴다. 겹침 풀이를 격자에 맡기면
   1261        참조 구현이 손으로 짠 두 번의 밀어내기 패스가 통째로 없어지고, 이름이
   1262        엉뚱한 눈금 옆에 가는 일이 구조적으로 불가능해진다. */
   1263 
   1264     position: fixed;
   1265     /* 창 가장자리에 붙는다. 본문 칼럼에 매달았더니 1920px 에서 레일이 창 끝에서
   1266        551px 떨어진 자리에 섰다 — 손이 가는 자리가 아니다. 스크롤 막대를 향해
   1267        뻗는 손이 지나가는 띠가 이 화면에서 가장 자주 밟히는 빈 자리이고,
   1268        여기 두면 찾으려 하지 않아도 걸린다.
   1269        폭은 본문과 겹치지 않는 만큼까지만 — 1280px 에서 28px 이 남는다. */
   1270     right: 0;
   1271     width: min(16rem, calc(50vw - var(--read) / 2 + var(--gutter) - 1.5rem));
   1272     /* 눈금과 창 끝 사이. 스크롤 막대 바로 옆이 이 물건의 자리다 — 멀어질수록
   1273        본문 여백에 떠 있는 장식으로 보이고, 붙을수록 스크롤 가구로 읽힌다.
   1274        포커스 링이 2px 테두리에 3px 오프셋이라 그만큼은 남긴다. */
   1275     padding-right: 0.3rem;
   1276     top: 50%; transform: translateY(-50%);
   1277     height: min(64vh, 34rem);
   1278     /* 마스트헤드(30) 아래. 올라온 이름이 크롬을 덮으면 안 된다. */
   1279     z-index: 20;
   1280   }
   1281 
   1282   /* 눈금은 세 단의 밝기다: 아직 안 읽은 것, 읽은 것, 지금 읽는 것. 색조는 한
   1283      가지뿐이라 2px 에서도 순서가 읽힌다. 한때 읽은 자리를 잉크로, 지금 자리를
   1284      주황으로 칠한 적이 있는데 그때는 과거가 현재보다 두 배 진했다 — 두 색을
   1285      섞으면 어느 쪽이 앞인지 정할 방법이 없다. 색은 하나고, 순서는 길이가 짓는다. */
   1286   /* 아직 안 읽은 자리. 중립 헤어라인이라 주황이 지나간 자리와 안 지나간 자리가
   1287      색조로 갈린다. */
   1288   .rail-seg { background: var(--rule-3); }
   1289 
   1290   /* 읽은 만큼. 이 사이트의 유일한 색이고, 여기가 그 색이 상시로 서는 유일한
   1291      자리다 — 다른 데서는 손이 닿을 때만 나온다. 진행은 한 가지 색이 단조롭게
   1292      자라는 것으로만 말한다: 위는 다 차고, 지금 절만 덜 찼고, 아래는 비어 있다.
   1293      "지금"을 다른 색으로 칠하지 않는 이유가 그거다. 덜 찬 칸이 이 열에 하나뿐이라
   1294      색을 더 쓰면 읽는 사람이 순서를 매길 수 없는 두 번째 변수가 생긴다.
   1295 
   1296      라이트에서 이 주황은 바탕 대비 2.12:1 이다. 낮다 — 그래서 이 레일은 보조
   1297      표시이고, 실제 목차는 좁은 화면과 스크립트가 없을 때 그대로 나오는 본문의
   1298      목록이 진다. 이름의 현재 표시는 색이 아니라 aria-current 와 잉크가 진다.
   1299 
   1300      높이는 ui.js 가 스크롤에서 직접 쓴다 — 전환을 걸면 화면보다 한 박자 늦게
   1301      따라와 거짓말을 한다. 이미 연속값에 매인 것은 애니메이션하지 않는다. */
   1302   .rail-fill {
   1303     display: block; width: 100%; height: 0;
   1304     background: var(--accent);
   1305   }
   1306 
   1307   .rail-label {
   1308     align-self: start;
   1309     /* 오른쪽 정렬이라 이름들이 눈금에 붙어 한 덩이로 읽힌다. 왼쪽 정렬로 두면
   1310        길이가 제각각인 세 구절이 빈 자리에 흩어져 목록으로 안 보인다. */
   1311     text-align: right;
   1312     /* 두 줄까지. 칸의 아래끝 2.2rem 이 정확히 두 줄 높이라, 넘치면 옆 칸을
   1313        침범한다. -webkit-line-clamp 를 쓰지 않는 이유는 카드 제목과 같다 — 이
   1314        조합에서 display 가 flow-root 로 계산돼 clamp 가 죽고 overflow 만 남으면,
   1315        상자가 line-height 만큼이라 글자 줄상자(1.44em)의 아래가 잘린다.
   1316        ponytail: 여기 걸리는 제목이 잦아지면 breakpoint 를 84rem 으로 올려 이름
   1317        열을 넓히는 쪽이 먼저다. 글자 크기를 줄이는 건 그다음. */
   1318     max-height: calc(2 * 1.35em + 0.12em);
   1319     padding-bottom: 0.12em;
   1320     overflow: hidden;
   1321     font-size: 0.72rem; line-height: 1.35;
   1322     /* 13.7px 글자다. --ink-3 은 라이트에서 4.23:1 로 AA 아래이고, 여기서 쓰던
   1323        hover 액센트는 2.12:1 이라 손을 얹으면 더 안 읽혔다 — 이 열에서 액센트가
   1324        두 번째로 틀린 자리다. 밝기로만 간다. */
   1325     color: var(--ink-2);
   1326     opacity: 0; pointer-events: none;
   1327     transition: opacity var(--tr-ink), color var(--tr-ink);
   1328   }
   1329   .rail-label[aria-current] { color: var(--ink); }
   1330 
   1331   /* 드러나는 것은 숨어 있던 것이지 자리를 옮긴 것이 아니다. 이름은 제자리에서
   1332      밝아진다 — 굵어지지도, 밀려나지도 않는다. 여백 전체가 방아쇠라, 한 번 나온
   1333      이름까지 커서가 가는 동안 사라지지 않는다. 박자는 이 시트의 유일한 박자다.
   1334 
   1335      눈금은 hover 에 반응하지 않는다. --rule-3 에서 --ink-4 로 옮겼더니 라이트에서
   1336      1.78:1 → 1.72:1 로 오히려 흐려졌고 다크에서는 1.76 → 1.77 로 아무 일도
   1337      일어나지 않았다 — 두 토큰의 밝기가 두 테마 모두에서 거의 같다. 사이에 낄
   1338      단이 없고, 이름 넷이 한꺼번에 나타나는 것이 이미 충분한 대답이다.
   1339      좁아지면 줄이는 게 아니라 뺀다. */
   1340   .rail:hover .rail-label,
   1341   .rail:focus-within .rail-label { opacity: 1; pointer-events: auto; }
   1342 
   1343   /* 손이 얹힌 이름은 밝아진다. 지금 절과 같은 잉크가 되지만 둘을 가르는 것은
   1344      색이 아니라 눈금이다 — 지금 절만 덜 찬 칸을 갖는다. 내비도 같은 규칙이다. */
   1345   .rail-label:hover { color: var(--ink); }
   1346 
   1347   .has-rail .toc { display: none; }
   1348 }
   1349 
   1350 /* ---------- prose ---------- */
   1351 
   1352 .prose { font-size: 1.02rem; line-height: 1.78; }
   1353 .prose-intro { margin-bottom: 2.5rem; color: var(--ink-2); text-align: center; }
   1354 .project .prose, .prose-intro { max-width: var(--read); margin-inline: auto; }
   1355 
   1356 /* 여백은 형제 사이에만. 타입 셀렉터로 초기화하면 특정성이 높아 아래 규칙을
   1357    이기고 문단 사이 여백이 통째로 사라진다. */
   1358 .prose > * { margin: 0; }
   1359 .prose > * + * { margin-top: 1.2rem; }
   1360 .prose p { text-wrap: pretty; }
   1361 
   1362 .prose h2, .prose h3, .prose h4 {
   1363   margin: 2.75rem 0 0; line-height: 1.25; letter-spacing: 0.012em;
   1364   text-wrap: balance; scroll-margin-top: calc(var(--masthead-h) + 1rem);
   1365 }
   1366 .prose h2 { font-size: clamp(1.3rem, 1.15rem + 0.7vw, 1.6rem); font-weight: var(--w-section); }
   1367 .prose h3 { font-size: 1.12rem; font-weight: var(--w-item); }
   1368 .prose h4 { font-size: 1rem; font-weight: var(--w-label); }
   1369 .prose h2 + *, .prose h3 + *, .prose h4 + * { margin-top: 0.8rem; }
   1370 
   1371 .prose a { color: var(--ink); text-decoration: underline; text-underline-offset: 0.2em; text-decoration-color: var(--rule-3); }
   1372 .prose a:hover { text-decoration-color: var(--accent); }
   1373 
   1374 .prose ol { margin-top: 1.2rem; padding-left: 1.35rem; }
   1375 .prose ul { margin-top: 1.2rem; list-style: none; padding-left: 1.1rem; }
   1376 .prose li { margin: 0.4rem 0; }
   1377 .prose ul > li { position: relative; }
   1378 .prose ul > li::before {
   1379   content: ""; position: absolute; left: -1.1rem; top: 0.85em;
   1380   width: 0.45rem; height: 1px; background: var(--ink-4);
   1381 }
   1382 
   1383 .prose blockquote {
   1384   margin-top: 1.8rem; padding-left: 1.25rem;
   1385   border-left: 1px solid var(--rule-3); color: var(--ink-2);
   1386 }
   1387 
   1388 .prose code {
   1389   font-family: var(--mono); font-size: 0.86em;
   1390   padding: 0.12em 0.35em; border-radius: var(--radius); background: var(--bg-3);
   1391 }
   1392 .prose pre {
   1393   position: relative; margin-top: 1.5rem; padding: 1.1rem 1.2rem; overflow-x: auto;
   1394   border: 1px solid var(--rule); border-radius: var(--radius);
   1395   background: var(--bg-recess); font-size: 0.82rem; line-height: 1.75;
   1396 }
   1397 .prose pre code { padding: 0; background: none; font-size: inherit; }
   1398 
   1399 .copy-btn {
   1400   position: absolute; top: 0.5rem; right: 0.5rem;
   1401   padding: 0.18rem 0.5rem;
   1402   border: 1px solid var(--rule-2); border-radius: var(--radius);
   1403   background: var(--bg-2); color: var(--ink-4);
   1404   font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em;
   1405   text-transform: uppercase; cursor: pointer; opacity: 0;
   1406   transition: opacity var(--tr-ink), color var(--tr-ink), border-color var(--tr-ink);
   1407 }
   1408 .prose pre:hover .copy-btn, .prose pre:focus-within .copy-btn { opacity: 1; }
   1409 .copy-btn:hover { color: var(--ink); border-color: var(--rule-3); }
   1410 .copy-btn.is-done { opacity: 1; color: var(--accent); border-color: var(--accent); }
   1411 
   1412 .prose table {
   1413   width: 100%; margin-top: 1.75rem; border-collapse: collapse;
   1414   font-size: 0.9rem; font-variant-numeric: tabular-nums;
   1415 }
   1416 .prose th, .prose td { padding: 0.55rem 0.75rem; text-align: left; border-bottom: 1px solid var(--rule); }
   1417 .prose th {
   1418   font-size: 0.64rem; font-weight: 500;
   1419   letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-4);
   1420   border-bottom-color: var(--rule-2);
   1421 }
   1422 .prose td { color: var(--ink-2); }
   1423 .prose hr { margin-top: 2.5rem; border: 0; border-top: 1px solid var(--rule); }
   1424 
   1425 .prose .anchor { opacity: 0; color: var(--ink-4); text-decoration: none; transition: opacity var(--tr-ink); }
   1426 .prose h2:hover .anchor, .prose h3:hover .anchor, .prose h4:hover .anchor,
   1427 .prose .anchor:focus-visible { opacity: 1; }
   1428 
   1429 /* ---------- search palette ---------- */
   1430 
   1431 .palette {
   1432   width: min(38rem, calc(100vw - 2rem)); margin-top: 12vh; padding: 0;
   1433   border: 1px solid var(--rule-2); border-radius: var(--radius);
   1434   background: var(--bg-2); color: var(--ink);
   1435   box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55);
   1436 }
   1437 .palette::backdrop { background: rgba(1, 1, 2, 0.66); backdrop-filter: blur(4px); }
   1438 .palette-form { margin: 0; }
   1439 .palette-head {
   1440   display: flex; align-items: center; gap: 0.7rem;
   1441   padding: 0.85rem 1rem; border-bottom: 1px solid var(--rule); color: var(--ink-4);
   1442 }
   1443 .palette-input {
   1444   flex: 1; min-width: 0; border: 0; background: none; color: var(--ink);
   1445   font: inherit; font-size: 0.98rem; outline: none;
   1446 }
   1447 .palette-close { border: 0; background: none; cursor: pointer; padding: 0; color: var(--ink-4); }
   1448 .palette-close kbd { font-family: var(--mono); font-size: 0.64rem; }
   1449 .palette-results { list-style: none; margin: 0; padding: 0.4rem; max-height: 56vh; overflow-y: auto; }
   1450 .palette-results:empty { display: none; }
   1451 .palette-results a { display: block; padding: 0.6rem 0.7rem; border-radius: var(--radius); }
   1452 .palette-results li[aria-selected="true"] a, .palette-results a:hover { background: var(--bg-3); }
   1453 .palette-title { display: block; font-size: 0.94rem; font-weight: 400; }
   1454 .palette-crumb {
   1455   display: block; margin-top: 0.15rem;
   1456   font-size: 0.66rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-4);
   1457 }
   1458 .palette-results mark { background: none; color: var(--accent); }
   1459 .palette-status {
   1460   margin: 0; padding: 0.55rem 1rem; border-top: 1px solid var(--rule);
   1461   font-family: var(--mono); font-size: 0.66rem; color: var(--ink-4);
   1462 }
   1463 .palette-status:empty { display: none; }
   1464 
   1465 /* ---------- colophon ---------- */
   1466 
   1467 /* 여백은 세 줄을 서로 떼어 놓을 만큼만. 크롬은 읽는 물건이 아니라 나가는
   1468    길이라, 본문만 한 자리를 차지하면 페이지가 거기서 한 번 더 시작하는 것처럼
   1469    보인다. */
   1470 .colophon { border-top: 1px solid var(--rule); padding-block: 2rem 2.5rem; }
   1471 .colophon-in {
   1472   max-width: var(--wide); margin-inline: auto; padding-inline: var(--gutter);
   1473   display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
   1474   gap: 0.9rem 2rem; text-align: center;
   1475 }
   1476 .colophon-nav { display: flex; flex-wrap: wrap; justify-content: center; gap: 1.75rem; }
   1477 .colophon-nav a {
   1478   font-size: 0.66rem; letter-spacing: 0.22em; text-transform: uppercase;
   1479   color: var(--ink-4); transition: color var(--tr-ink);
   1480 }
   1481 .colophon-nav a:hover { color: var(--ink-2); }
   1482 /* 갈래 링크와 소셜은 다른 줄에 선다 — 안을 도는 것과 밖으로 나가는 것이다. */
   1483 .colophon-social { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.6rem; flex-basis: 100%; }
   1484 /* 아이콘만 남긴다. GitHub 와 Instagram 은 마크만으로 읽히는 몇 안 되는 표지라
   1485    낱말이 하는 일이 없다 — 이름은 aria-label 이 진다. */
   1486 .colophon-social a {
   1487   display: inline-flex; align-items: center; justify-content: center;
   1488   width: 1.9rem; height: 1.9rem;
   1489   font-size: 1rem; color: var(--ink-3);
   1490   transition: color var(--tr-ink);
   1491 }
   1492 .colophon-social a:hover { color: var(--accent); }
   1493 .colophon-social svg { flex: none; }
   1494 
   1495 .colophon-note { margin: 0; flex-basis: 100%; font-size: 0.7rem; letter-spacing: 0; color: var(--ink-4); }
   1496 .colophon-note a { color: var(--ink-3); transition: color var(--tr-ink); }
   1497 .colophon-note a:hover { color: var(--accent); }
   1498 
   1499 /* ---------- Korean ---------- */
   1500 
   1501 /* 한국어는 라틴 타이포 장치 셋을 깨뜨린다. letter-spacing 은 음절 블록을 벌리고,
   1502    text-transform 은 아무 일도 하지 않으며, monospace 는 한글이 프로포셔널로
   1503    폴백하는데 공백만 mono 폭이라 어절 사이가 두 배로 벌어진다.
   1504 
   1505    여기 걸리는 것은 **한국어가 들어가는 자리만**이다. 크롬(내비·푸터·컨트롤·
   1506    문 링크)은 두 언어에서 영어로 고정이므로 대문자와 자간을 그대로 둔다 —
   1507    되돌리면 한국어 페이지에서만 라벨이 소문자로 나와 두 언어가 달라 보인다. */
   1508 html[lang="ko"] .eyebrow,
   1509 html[lang="ko"] .fact-label,
   1510 html[lang="ko"] .prose th,
   1511 html[lang="ko"] .crumbs,
   1512 html[lang="ko"] .palette-crumb,
   1513 html[lang="ko"] .btn,
   1514 html[lang="ko"] .back,
   1515 html[lang="ko"] .badges li {
   1516   font-family: var(--sans);
   1517   letter-spacing: 0;
   1518   text-transform: none;
   1519 }
   1520 
   1521 /* 초경량은 한글에서도 나오지만 아주 큰 크기에서만 버틴다. 한글은 같은 굵기에서
   1522    획이 훨씬 많아 라틴보다 먼저 뭉개지고, 작아지면 획이 끊어져 보인다.
   1523    사다리 전체를 한 단씩 올린다 — 선택자마다 예외를 적으면 라틴 쪽 사다리를
   1524    고칠 때 이쪽이 조용히 어긋난다. */
   1525 html[lang="ko"] {
   1526   --w-display: 300;
   1527   --w-section: 350;
   1528   --w-item:    400;
   1529   --w-label:   500;
   1530   /* 자간도 여기서 끈다. letter-spacing 은 한글에서 음절 블록 사이를 벌려
   1531      한 낱말이 여러 낱말로 보이게 만든다 — 라틴에서 숨을 주는 그 값이
   1532      한글에서는 정확히 반대로 작동한다. */
   1533   --track-air: 0;
   1534   --track-set: 0;
   1535 }
   1536 
   1537 /* 숫자와 날짜는 한국어에서도 mono 다. 숫자에는 위 문제가 없고 자릿수가 맞아야
   1538    목록이 줄을 이룬다. */
   1539 html[lang="ko"] .ledger-date,
   1540 html[lang="ko"] .feature-date,
   1541 html[lang="ko"] .ledger-year,
   1542 html[lang="ko"] .term-body,
   1543 html[lang="ko"] .stars { font-family: var(--mono); }
   1544 
   1545 /* ---------- reduced motion ---------- */
   1546 
   1547 @media (prefers-reduced-motion: reduce) {
   1548   html { scroll-behavior: auto; }
   1549   *, *::before, *::after {
   1550     animation-duration: 0.01ms !important;
   1551     animation-iteration-count: 1 !important;
   1552     transition-duration: 0.01ms !important;
   1553   }
   1554   /* 전환만 끄면 변형된 자리에 멈춘다. 종점도 같이 중화한다. */
   1555   .js .hero-home [data-rise] { animation: none; opacity: 1; transform: none; }
   1556   .frame:hover .frame-media,
   1557   .frame:hover .frame-media img,
   1558   .tile-links a:hover { transform: none; }
   1559 }