@charset "utf-8";
/* ══════════════════════════════════════════════════════════════════════════
   layout.css — **배치의 소유자** (신규 파일 · 원본 css 4종은 무수정)

   원본 4종보다 뒤에 로드되므로 같은 특이도(`.tws-main`)로 덮어쓴다.
   `!important`가 한 곳도 없는 게 정상이다 — 인라인 스타일과 싸울 일이 없어야 한다.
   `!important`를 쓰고 싶어지면 JS 어딘가에 인라인 폭이 남아 있다는 신호다.

   ── 왜 float가 아니라 grid인가 ────────────────────────────────────────────
   원본 카드는 `float:left` + 폭 합계로 줄바꿈을 만드는 모자이크였다. 배치가 **폭 산수의
   부산물**이라 칸 하나를 바꾸면 같은 줄 전부를 손으로 다시 나눠 줘야 했고, 그 재배분이
   JS 인라인 스타일 7군데에 박혀 있었다(InfoPanels의 W 객체 5개 + App의 카드 폭 +
   NoticeClock의 공지 폭 + 키보드 폭). 트랙으로 옮기면서 그 7군데를 전부 지웠다.

   ── 트랙 수치의 출처 ──────────────────────────────────────────────────────
   눈대중이 아니라 **원본 배치의 실측값**이다(헤드리스 크롬, 2026-08-26).
   셀 사이 검은 틈 0.2rem은 원본에서 칸마다 붙은 margin이었는데 여기서는 `gap` 하나로
   통일한다 — 그래서 `.tws-info`의 폭이 42.3 → 42.1rem이다(안쪽 첫 칸의 margin-left가
   gap으로 빠져나갔다). **칸의 좌표는 그대로**고 컨테이너 박스만 그만큼 줄어든다.

   ⚠ 두 줄의 칸 경계가 서로 맞지 않는다. PC 1줄은 96|211|110, 2줄은 96|84|80|80|73인데
   211의 오른쪽 끝이 2줄의 어느 경계와도 만나지 않는다. 그래서 트랙을 **잘게 쪼개고
   span으로 합친다**(6열). 원본 주석이 부르던 "벽돌 그리드"가 실은 격자가 아니었다는
   증거이고, 폭을 손으로 재배분할 수밖에 없었던 이유이기도 하다.
   ══════════════════════════════════════════════════════════════════════════ */

/* ══ 타이핑 글자 크기 → 줄 박스 → 문장 행 ═══════════════════════════════════
   설정의 '글자 크기'(15~25px)가 여기로 들어와 **두 값이 차례로 파생된다.**

   ── 왜 CSS 변수인가 ───────────────────────────────────────────────────────
   글자 크기를 바꾸면 줄 박스가 커지고, 줄 박스가 커지면 문장 상자가 커져야 한다. 그 배치는
   전부 이 파일이 갖고 있으므로(규칙 12) 값이 CSS까지 와야 한다. 컴포넌트가 인라인으로만
   들고 있으면 상자는 그대로인 채 글자만 커져 `overflow: hidden`이 자른다 — 실제로
   줄바꿈 모드에서 윗줄이 잘렸다(2026-08-31 지적).

   ── ⛔ 카드 높이는 글자 크기와 **무관하다** (2026-08-31 지시) ──────────────
   문장 행이 자라는 몫은 카드가 아니라 **타이핑 영역의 마지막 행(`1fr`)**이 낸다.
   그 행은 NEXT 아래 남는 자리이고(PC 3.45rem / 태블릿 8.25rem) `1fr`이라 나머지를 받으므로,
   2행이 커진 만큼 저절로 줄어든다 — 카드 행(PC 18.6rem)도 카드 높이(34.4rem)도 그대로다.
   그래서 여기서 카드 높이에 `--tws-band`를 더하지 말 것: 글자를 키웠다고 카드가 커지면
   화면 전체가 재배치된다.
   그 몫을 내는 순서도 정해져 있다: **바와 NEXT 사이(`--tws-gap`)가 먼저** 0까지 줄고,
   모자라는 만큼만 `1fr`이 낸다(2026-08-31 지시). 최대 크기(25px)에서 `--tws-gap`이 0이 되고
   `1fr`은 3.45 → 3.05rem으로 0.4rem만 준다 — 조절 범위를 넓히려면 **먼저 그 여유부터
   계산할 것.**

   ── 왜 `:root`가 아니라 `main`인가 ────────────────────────────────────────
   App이 `--tws-font`를 **`main`의 인라인 스타일로** 쓴다(선언적이라 JS가 DOM에 직접
   쓰지 않는다 — 규칙 6). 파생값을 `:root`에 두면 그쪽의 `--tws-font`(아래 폴백)로
   먼저 계산돼 버리고 그 결과가 상속되므로, **파생도 같은 요소에서** 해야 한다.

   ── 줄 박스에 바닥이 있는 이유 ────────────────────────────────────────────
   2.8rem은 이 앱의 줄 박스다(NEXT·이웃 상자·모바일 제목이 같은 값을 쓴다). 글자를
   17px보다 키우면 그 박스로는 글자가 안 담겨 위아래가 잘리므로 따라 커지고, 줄이면
   **박스는 그대로 두고 글자만 작아진다** — 같이 줄이면 문장 상자가 작아져 그 자리에
   빈 줄이 남는다. 1.6은 지금 값(28/17 = 1.65)에서 왔고, 기본 크기 둘(17·15px)에서
   정확히 2.8rem으로 떨어져 **아무것도 안 고른 사람의 화면은 픽셀이 그대로다.**

   문장 행 = 줄 둘 + 줄 사이 0.1rem + 하프리딩 보정 0.2rem = 기본에서 정확히 5.9rem.   */
main {
	/* App이 늘 인라인으로 덮으므로 이 값은 JS가 죽었을 때만 보인다(PC 기본과 같은 값) */
	--tws-font: 1.7rem;
	--tws-line: max(2.8rem, calc(var(--tws-font) * 1.6));
	--tws-band: calc(var(--tws-line) * 2 + 0.3rem);
	/* 바와 NEXT 사이의 빈 자리 — **문장 행이 자란 몫을 여기가 먼저 낸다**(2026-08-31 지시).
	   `1fr`(NEXT 아래)이 먼저 줄면 NEXT가 카드 바닥에 달라붙는데, 그 아래는 카드의 여백이라
	   눈에 띈다. 반면 바와 NEXT 사이는 **아무것도 없는 자리**라 좁아져도 잃는 게 없다.
	   7.9rem = 기본 문장 행(5.9) + 기본 여백(2). 기본 크기에서는 정확히 2rem이고,
	   글자를 키운 만큼 줄다가 0에서 멈춘다 — 그 뒤로는 `1fr`이 마저 낸다(25px에서 4px). */
	--tws-gap: max(0rem, calc(7.9rem - var(--tws-band)));
	/* 장문의 위·아래 여백. **세 곳(최대 규칙·고정 규칙·고정의 calc)이 같은 값을 봐야 해서**
	   상수로 둔다 — 손으로 적으면 한쪽만 고쳐져 «고정에서만 어긋나는» 배치가 남는다.
	   위(3.5)와 아래(4.3)가 다른 까닭은 그 규칙 옆에 적어 두었다(잉크와 상자의 차이). */
	--tws-long-top: 3.5rem;
	--tws-long-bottom: 4.3rem;
}

/* clearfix의 :after는 그리드 컨테이너에서 **셀 하나를 차지한다** — 반드시 지운다.
   (darkmode.css의 `.clearfix:after { content:""; display:block; clear:both }`)
   `.tws-info`는 모바일에서 float 그대로라 그쪽은 아래 ≥601px 블록에서만 끈다. */
.tws-main:after { display: none; }

/* ══ 카드의 세로 위치 — 창이 짧아져도 윗변이 잘리지 않게 ══════════════════════
   원본(`twstb.css:14-18`)은 `top: 40%` + `translate(-50%, -50%)`로 자리를 잡는데,
   `top`의 퍼센트가 `main`의 콘텐츠 높이에 걸린다(`darkmode.css:126-131`의
   `height: 100%` + `padding: 3.7rem 0 0 0`). 정리하면

     카드 윗변 = 3.7rem + 40% × (창높이 − 3.7rem) − 카드높이 / 2

   **이 식에는 하한이 없다.** 창을 줄이면 그대로 음수로 내려가 윗부분이 화면 위로 잘리고,
   잘린 부분은 스크롤로도 못 올라온다 — PC는 창높이 360에서 6px, 320에서 22px,
   태블릿(카드가 더 크다)은 420에서 3px, 320에서 43px이 사라졌다(2026-08-26 실측).
   설정·로그인 페이지는 `CardPage`가 여백을 남겨 두므로 **카드만 안 지켜지고 있었다.**

   ── 고치는 방법: 계산을 브라우저에 넘긴다 ─────────────────────────────────
   `top` 값을 직접 깎지 않고 **flex 중앙 정렬로 바꾼다.** 두 가지가 공짜로 따라온다.

     ① 카드 높이를 우리가 몰라도 된다. `h / 2`를 브라우저가 한다 — CSS는 계산된 높이를
        못 읽으므로, `top`을 손으로 깎으려면 높이를 여기 한 번 더 적어야 했다
        (그러면 원본 높이를 바꿀 때 한쪽만 고쳐져 조용히 다시 잘린다).
     ② **`margin: auto`는 자리가 모자라면 0으로 떨어진다.** 그게 곧 하한이다.
        음수 여백을 만들지 않으므로 카드가 `main`의 패딩 안쪽(4.44rem)에 붙는다.

   `padding-bottom: 20vh`가 아래쪽을 그만큼 먹어 중앙이 위로 밀린다. 이 offset이
   **상수가 아니라 창높이에 비례**해야 원본과 같아진다 — `top: 40%`의 치우침이
   0.1 × 창높이라서, 고정 px로 주면 900에서만 맞고 560에서 34px 어긋났다(실측).
   `padding-top`을 3.7 → 4.44rem으로 올린 건 그 과정에 남는 3.7px 오차를 지우려는 것이고,
   결과 식은 `0.4 × 창높이 + 22.2 − 카드높이/2`로 **원본과 완전히 같다.**
   PC는 창높이 486까지, 태블릿은 560까지 옛 값과 0px 차이다.

   ⚠ `main`의 흐름상 자식이 **카드 하나뿐**이라서 성립한다(결과 카드·로그인·설정·dev는
   전부 absolute/fixed라 flex 아이템이 되지 않는다). 흐름상 자식을 새로 추가하면 그 순간
   나란히 놓이며 배치가 깨진다.
   ⚠ `.tws-main`의 `position: relative`는 **그대로 둔다** — 인포 칸 안의 절대배치 요소들이
   이걸 기준으로 잡는다.

   모바일(~600px)은 `.tws-main`에 `top`이 없어 원래 안 잘린다. 그래서 건드리지 않는다. */
@media (min-width: 601px) {
	main {
		display: flex;
		padding: 4.44rem 0 20vh 0;
	}
	.tws-main {
		top: auto;
		left: auto;
		margin: auto;
		/* ⚠ 지우면 카드 안 글자의 안티에일리어싱이 전부 미세하게 달라진다(기하는 동일,
		   RMSE 0.69%, 눈으로는 구분 불가). 원인은 합성 레이어가 아니라 원본
		   `translate(-50%, -50%)`의 **0.5px 소수 오프셋**이다 — 카드 폭이 899라 −50%가
		   −449.5px이고, 글자가 그 반 픽셀 위에서 그려지고 있었다. `translateZ(0)` 같은
		   항등 변환으로는 재현되지 않는다(오히려 차이가 커졌다).
		   타건 비용은 그대로다 — p50 1.4~1.6ms 동일, p95는 오히려 개선(실측 3회). */
		transform: none;
	}
}

/* ══ PC·태블릿 공통 ════════════════════════════════════════════════════════ */
@media (min-width: 601px) {
	.tws-info:after { display: none; }

	/* 칸들이 스스로 자리를 정하던 수단을 내린다. 자리는 이제 트랙이 정한다.
	   `.tws-info section`까지 잡는 이유: info-5·6이 래퍼 div 안에 한 겹 더 들어 있다. */
	.tws-main > *,
	.tws-info > *,
	.tws-info section {
		float: none;
		margin: 0;
		width: auto;
		height: auto;
		min-width: 0;
		min-height: 0;
	}


	/* ── 인포 셀의 자리 (PC·태블릿 공통) ───────────────────────────────────
	   트랙 폭만 브레이크포인트마다 다르고 **자리는 같다.** float 시절엔 이걸 표현할
	   수단이 없어서 폭 숫자를 뷰포트마다 따로 계산해야 했다. */
	.tws-info-1  { grid-area: 1 / 1 / 3 / 2; }
	.tws-info-2  { grid-area: 1 / 2 / 3 / 5; }
	/* 「결과까지」는 두 줄 높이를 다 쓴다 — 2026-09-04 ~ 10-07 에는 아래 줄(2행)이 「결과 보기」 스위치 칸(`.tws-info-3b`)이었다
	   (지시로 없앴다 — 결과는 늘 뜬다). 4.5 + 0.2 + 2.8 = 7.5rem 이라 1 · 2번 칸과 같은 높이다 */
	.tws-info-3  { grid-area: 1 / 5 / 3 / 7; }
	.tws-info-4  { grid-area: 3 / 1 / 6 / 2; }
	.tws-info-5  { grid-area: 3 / 2 / 4 / 3; }
	.tws-info-6  { grid-area: 4 / 2 / 6 / 3; }
	.tws-info-7  { grid-area: 3 / 3 / 5 / 4; }
	.tws-info-8  { grid-area: 3 / 4 / 5 / 6; }
	.tws-info-9  { grid-area: 3 / 6 / 5 / 7; }
	.tws-info-10 { grid-area: 5 / 3 / 6 / 7; }
	/* ── 연습 ↔ 랭크 토글 (docs/RANK-IMPL.html 11-1, 2026-10-02) ───────────────────────
	   **기록 칸(`.tws-info-2`)의 오른쪽 아래**에 선다(지시: 「폰에서 저렇게 놓을 거면 기록 전광판 우측 하단에」 — 폰은 기록
	   막대 오른쪽이다). 칸 **안**에 두지 않고 **같은 격자 자리에 겹쳐** 세운다 — 그 칸은 스크롤 칸이라 안에 두면 줄과 함께
	   굴러간다(자리를 재지 않는다 — 묶음 프롬프트가 쓰는 그 수법, 규칙 12). 칸 오른쪽 위의 「전체 보기」(1.8rem · 0.5rem
	   안쪽)와 **같은 크기 · 같은 간격**으로 아래에 맞춘다. 줄은 오른쪽 2.1rem 을 늘 비워 두므로(`InfoPanels.ROW` — 「전체
	   보기」 자리) 토글이 줄 글자를 안 덮는다. (0,2,0)이라 위의 `.tws-info > *`(margin 0)를 이긴다 */
	.tws-info > .tws-info-toggle {
		grid-area: 1 / 2 / 3 / 5;
		align-self: end;
		justify-self: end;
		margin: 0 0.5rem 0.5rem 0;
		z-index: 2;
		/* 랭크면 토글 왼쪽에 순위표 · 내 기록이 차례로 선다(2026-10-02 지시) — 토글은 맨 오른쪽 그 자리 그대로 */
		display: flex;
		gap: 0.4rem;
	}

	/* 인포 행: (4.5 · 2.8) / (2.8 · 1.5 · 2.8)
	   첫 줄 7.5rem 을 **둘로 쪼갰다**(2026-09-04) — 그때 아래 칸이 「결과 보기」 스위치였다(2026-10-07 에 없앴다 — 지금은
	   `.tws-info-3` 이 두 행을 합쳐 쓴다). 행을 그대로 둔 것은 4.5 + 0.2(틈) + 2.8 = 7.5 라 **1·2번 칸의 높이가 그대로**이고
	   다른 칸이 그 분할에 기대지 않아서다(되돌릴 일이 생기면 행부터 다시 본다).
	   2줄 안에서 5·6은 28:45로, 7~9와 10은 45:28로 갈린다 — 두 분할선이 다르므로
	   행도 잘게 쪼개고 span으로 합친다. 열과 같은 사정이다. */
	.tws-info {
		display: grid;
		gap: 0.2rem;
		grid-template-rows: 4.5rem 2.8rem 2.8rem 1.5rem 2.8rem;
	}

	/* 이 칸은 누르는 곳이 아니다 — 원본의 `:hover{cursor:pointer}`를 되돌린다 */
	.tws-info-3:hover { cursor: default; }
}

/* ══ PC (1025px~) ══════════════════════════════════════════════════════════
   카드 3열 × 3행. 오른쪽 열의 공지(5rem)/키보드(10rem) 분할 때문에 1행을 둘로 쪼갰고,
   로고·인포가 그 둘을 span한다. 감싸는 div가 없어서 이렇게 푼다.                */
@media (min-width: 1025px) {
	.tws-main {
		display: grid;
		gap: 0.2rem;
		/* 로고 | 인포 | 공지·키보드 */
		grid-template-columns: 15.4rem 42.1rem 31.6rem;
		/* 공지 | 키보드 | 타이핑 */
		grid-template-rows: 5rem 10rem 18.6rem;
		/* 15.4 + .2 + 42.1 + .2 + 31.6 + padding .4 = 89.9rem.
		   원본은 90.7rem이고 vkb 좌우 여백 축소분(-0.8rem)을 App이 인라인으로 빼고
		   있었다 — 이제 **여기서 한 번 선언하고 끝난다.** */
		width: 89.9rem;
		height: 34.4rem;
	}
	.tws-logo-box        { grid-area: 1 / 1 / 3 / 2; }
	.tws-info            { grid-area: 1 / 2 / 3 / 3; }
	.tws-notice          { grid-area: 1 / 3 / 2 / 4; }
	.tws-keyboard-effect { grid-area: 2 / 3 / 3 / 4; }
	.tws-typing-area     { grid-area: 3 / 1 / 4 / 4; }

	/* 1줄 96|237|84, 2줄 96|84|80|80|73 을 동시에 만족하는 최소 트랙.
	   4·5열을 4.3|3.5 에서 **6.9|0.9 로 다시 나눴다**(2026-09-04): 합(4.3+0.2+3.5)은
	   그대로라 `.tws-info-8`(4~5열)의 폭 8rem 이 안 바뀌고, 5~6열인 `.tws-info-3`만
	   11 → 8.4rem 으로 좁아진다. 그만큼 2~4열인 `.tws-info-2`가 21.1 → 23.7rem 으로 넓어진다. */
	.tws-info {
		grid-template-columns: 9.6rem 8.4rem 8rem 6.9rem 0.9rem 7.3rem;
	}

	/* 시계의 점판(캔버스) — 원본 글자와 **같은 오른쪽 끝**에 세운다.
	   `.tws-notice>p:nth-child(3)`의 text-align:right는 블록 자식에 안 걸리므로
	   왼쪽 여백으로 민다. 크기는 점 격자에서 나오므로 컴포넌트가 갖는다
	   (여기서 폭을 정하면 격자와 상자가 갈라진다 — DotClock.tsx 참고). */
	#clock > canvas {
		display: block;
		margin-left: auto;
	}

	/* 공지 줄의 「자세히」 — **손을 올렸을 때만** 나타난다(2026-09-08 지시).
	   「전체보기」(.tws-info-2)와 **같은 안무**다: 자리는 컴포넌트가 인라인으로 갖고
	   (NoticeClock 의 MORE_SLOT — 그 파일이 안 실렸을 때 흐름에 끼는 것을 막는다),
	   여기는 나타나고 사라지는 것만 맡는다. 200ms 는 이 앱의 토글 어휘다(규칙 8).
	   ⚠ 기준 상자를 만들려고 `position` 을 여기서 준다 — 원본 twspc.css 는 안 준다.
	   ⚠ 버튼은 `#ntc` **안**에 산다. `.tws-notice` 의 자식을 늘리면
	      `twspc.css` 의 `>p:nth-child(3)` 이 어긋나 시계 정렬이 빠진다(규칙 12). */
	.tws-notice { position: relative; }
	.tws-notice-more { opacity: 0; transition: opacity 200ms; }
	.tws-notice:hover .tws-notice-more,
	.tws-notice-more:focus-within { opacity: 1; }
}

/* ── 전광판 줄 바뀜 — 옛 줄은 위로 올라가 나가고 새 줄은 밑에서 올라온다(2026-10-14 지시: 「팍 변경되는게 아닌 1번 공지가 위로
   올라가면서 2번 공지가 밑에서 올라와서 변경되는 식으로」) ────────────────────────────────────────────────────────────
   창(한 줄 높이 · `overflow: hidden`)과 나가는 줄이 겹치는 자리는 `NoticeClock` 이 인라인으로 갖고, 여기는 움직임만 맡는다. 거리는 제 높이
   (= 창의 높이)라 두 줄이 맞붙어 한 칸 올라간다. 시간은 `session/notice.NOTICE_SLIDE_MS`(500 — 모션 어휘 «정착», 규칙 8)를 변수로 받는다
   (값이 두 곳에 적히지 않게 · 못 받으면 0 이라 곧바로 바뀐다). 이 칸은 PC(1025px~)에서만 보인다.
   ⚠ 미끄러지는 동안만 `will-change: transform` — 제 층에서 합성 단계가 민다(흐르는 긴 줄과 같은 처방). 다 올라가면 클래스를 뗀다 —
     머무는 줄은 층이 안 생겨 픽셀이 그대로다.
   ⚠ 움직임 줄이기를 켠 사람에게는 이 클래스가 아예 안 붙는다(곧바로 바뀐다 — `NoticeClock` 이 정한다). */
.tws-notice-in {
	animation: tws-notice-in var(--tws-notice-slide-ms, 0ms) ease both;
	will-change: transform;
}
.tws-notice-out {
	animation: tws-notice-out var(--tws-notice-slide-ms, 0ms) ease both;
	will-change: transform;
}
@keyframes tws-notice-in {
	from { transform: translateY(100%); }
}
@keyframes tws-notice-out {
	to { transform: translateY(-100%); }
}

/* ══ 태블릿 (601~1024px) ═══════════════════════════════════════════════════
   공지·키보드가 없어 2열 2행. 셀 자리는 PC와 같고 트랙 폭만 다르다.             */
@media (min-width: 601px) and (max-width: 1024px) {
	.tws-main {
		display: grid;
		gap: 0.2rem;
		grid-template-columns: 15.4rem 42.1rem;
		grid-template-rows: 15.2rem 22.8rem;
		width: 58.1rem;
		height: 38.6rem;
	}
	.tws-logo-box    { grid-area: 1 / 1 / 2 / 2; }
	.tws-info        { grid-area: 1 / 2 / 2 / 3; }
	.tws-typing-area { grid-area: 2 / 1 / 3 / 3; }

	/* 1줄 110|223|84, 2줄 110|84|73|73|73 — PC와 같은 방식으로 4·5열을 3.6|3.5 →
	   6.2|0.9 로 다시 나눈다(합 7.3 유지 → `.tws-info-8` 폭 그대로). */
	.tws-info {
		grid-template-columns: 11rem 8.4rem 7.3rem 6.2rem 0.9rem 7.3rem;
	}
}

/* ══ 모바일 (~600px) ═══════════════════════════════════════════════════════
   카드가 **보이는 화면을 통째로 쓴다**. 1열 3행 — 상단 줄(5rem) / 타이핑(1fr) /
   하단 상태 줄(5rem). 위아래는 제 높이를 지키고 **남는 높이는 전부 가운데가 받는다.**

     ┌──────────────┐  로고 · 로그인 · 설정        5rem
     ├──────────────┤
     │              │  타이핑 영역                 1fr  ← 키보드가 내려간 만큼 자란다
     ├──────────────┤
     ├──────────────┤  목표 속도 · 속도 · 정확도    5rem
     └──────────────┘  문장 제목 / 출처            2.8rem ← 키보드 윗변에 붙는다

   원본의 `.tws-info` 10칸은 폰에서 렌더하지 않는다(App 참고). 그 자리를 대신하는 것이
   `.tws-mobile-info`이고, `.tws-info-*` 규칙과 겹치지 않게 **새 클래스**를 쓴다 —
   `twsm.css`는 미디어쿼리 밖이라 그 이름들이 모든 폭에서 살아 있다.                */
@media (max-width: 600px) {
	/* ⚠ 여기만 `gap`이 없다. 모바일은 셀 사이 검은 틈이 **균일하지 않다** — 상단 줄과
	   상태 줄은 붙어 있고(0), 타이핑 영역만 위로 0.2rem 떨어져 있다(`twsm.css:194`의
	   `margin: 0.2rem 0 0 0`). 균일한 gap을 주면 상태 줄이 2px 밀린다(실측).
	   그래서 간격은 원본 margin에 그대로 맡긴다. */
	.tws-main {
		display: grid;
		grid-template-columns: 1fr;
		/* 원본은 `0 1.3rem`이라 위아래가 0이었다(`twsm.css:24-27`) — 카드가 화면 맨 위·아래에
		   닿아 있었다. 사방 0.5rem으로 바꿔 어느 변에서도 같은 만큼 띄운다(2026-08-26 지시).
		   덮개(설정·로그인)의 여백도 같은 값이다 — `CardPage`의 `MOBILE_PAD`. */
		padding: 0.5rem;
		/* 카드가 **보이는 화면을 통째로 쓴다**(2026-08-27 지시). 그래야 아래 상태 줄이
		   화면 바닥(=키보드 윗변)에 붙고, 남는 높이가 전부 타이핑 영역으로 간다. */
		height: 100%;
		/* ⚠ **가로로 새는 것을 여기서 막는다.** 드래그용 이웃 상자가 화면 밖(+100%)에
		   서 있는데, `visibility: hidden`은 **그림만 지우고 자리는 남긴다** — 그래서 문서의
		   스크롤 폭이 360 → 707px이 되고, 안드로이드 크롬이 오른쪽 끝에 스크롤바를 그렸다
		   (2026-08-27 실기기 지적: "평소에 고스트 보더가 보인다"). 실제로 옆으로 밀리기도 했다.
		   `clip`은 `hidden`과 달리 스크롤 컨테이너를 만들지 않고, `overflow-y`는 그대로
		   `visible`로 둘 수 있다(`hidden`을 쓰면 y도 강제로 auto가 된다). */
		overflow-x: clip;
		/* 상단 줄 / 타이핑 / 상태 줄 / 제목 줄.
		   **둘째가 `1fr`인 것이 이 구조의 전부다** — 키보드가 내려가 화면이 커지면
		   그 차이를 타이핑 영역만 받아 가고, 나머지 셋은 제 높이를 지킨다.
		   나머지가 `auto`인 이유: 높이를 여기 또 적으면 각 칸의 값과 두 곳에서 같은 값을
		   세게 된다. 칸이 제 높이를 정하고 트랙은 그걸 받는다.

		   3행이 **기록 줄**(`#twsInfo2`)이다 — 타이핑 영역과 상태 줄 사이(2026-09-03 지시).
		   여기에 끼우면서 아래 둘의 행 번호가 하나씩 밀렸다. */
		grid-template-rows: auto 1fr auto auto auto;
	}
	/* 그리드 아이템의 기본 `min-width: auto`는 **내용보다 작아지지 않겠다**는 뜻이라,
	   문장이 긴 순간 1fr 트랙이 카드 밖으로 자란다(실측 +27.89px). 원본 float 배치엔
	   없던 성질이므로 명시적으로 꺼 준다. */
	.tws-main > * { min-width: 0; }

	/* ── 상단 줄 = 로고 칸 ──────────────────────────────────────────────────
	   원본은 `.tws-logo-box`를 모바일에서 통째로 감추고(`twsm.css:28-30`) 별도 `<header>`가
	   작은 로고만 들고 있었다. 그 바람에 **로그인·설정 진입 버튼이 같이 숨어서 모바일에서
	   설정 페이지에 들어갈 방법이 없었다**(2026-08-26 실측). header를 걷어내고 이 칸이
	   그 줄을 대신한다 — 진입점이 한 곳으로 모이고 로고 구현도 하나가 된다.

	   `main`의 위쪽 패딩(`darkmode.css:126`의 3.7rem)은 원래 그 header 자리였다. 이제
	   그 줄이 카드 안으로 들어왔으므로 패딩을 걷어 화면 맨 위부터 쓴다.

	   좌우 패딩 0.4rem은 카드의 1.3rem에 더해져 1.7rem이 된다 — 옛 header의
	   `padding: 1rem 1.7rem`과 좌우 여백을 맞추기 위한 값이다.
	   위아래 1.3rem은 줄 높이를 **50px**로 만든다 — 버튼이 2.4rem이라 24 + 26 = 50.
	   50은 antws의 모바일 헤더 높이다(`Index/template/Normal.style.js`의 `'head' 50px`,
	   데스크톱은 116px). 원본 tws의 옛 header는 37px이었는데 그건 로고만 있던 줄이고,
	   여기엔 누를 것이 둘 들어가므로 antws 쪽 값을 따른다(2026-08-26 지시).
	   위아래 1.3rem이 카드의 좌우 패딩과 같은 값이라 여백이 사방으로 맞아떨어진다.

	   ── 왜 flex가 아니라 3열 grid인가 ────────────────────────────────────
	   로고는 **줄의 한가운데**에 있어야 한다. `space-between`으로는 로고가 왼쪽 끝에
	   붙고, `justify-content: center`로는 오른쪽 버튼 폭만큼 왼쪽으로 밀린다.
	   양쪽에 같은 `1fr`을 두면 버튼이 몇 개든 가운데가 흔들리지 않는다. */
	main {
		padding-top: 0;
		/* ── 키보드가 올라온 만큼만 쓴다 ─────────────────────────────────────
		   `height: 100%`도 `100dvh`도 **소프트 키보드를 모른다** — 키보드가 뜨면
		   visual viewport만 줄고 layout viewport는 그대로다(`dvh`는 주소창 접힘용).
		   그 상태로 하단 줄을 붙이면 줄이 키보드 뒤로 숨는다.
		   `--app-h`는 `src/lib/appHeight.ts`가 `window.visualViewport`를 재서 내려놓는
		   값이고, 없으면(구형 브라우저·JS 실패) 예전과 같은 `100%`로 떨어진다.

		   `top`은 iOS 몫이다. Safari는 키보드를 띄우면서 layout viewport 안에서 화면을
		   아래로 밀어(`visualViewport.offsetTop`) 입력창을 보여 준다 — 높이만 맞추면
		   그 밀린 만큼 위가 잘린다. `main`은 원래 `position: relative`라(darkmode.css:126)
		   `top` 하나로 되민다.
		   ⚠ **`transform`으로 옮기지 않는다** — 변환된 조상은 자손 `position: fixed`의
		   기준이 되어 덮개(설정·로그인·dev)가 통째로 어긋난다. */
		height: var(--app-h, 100%);
		top: var(--app-top, 0px);
	}
	.tws-logo-box {
		grid-area: 1 / 1 / 2 / 2;
		display: grid;
		grid-template-columns: 1fr auto 1fr;
		align-items: center;
		/* 위아래 1.2rem + 버튼 2.4rem + 테두리 0.2rem = 5rem(50px).
		   테두리를 더하면서 세로 패딩을 1.3 → 1.2로 내렸다 — 겉높이가 antws의 50px 그대로여야 한다.

		   가로는 **1.3rem**: 아래 타이핑 영역이 쓰는 값과 같다(`twsm.css:193`의
		   `padding: 0.8rem 1.3rem`). 그래서 헤더의 버튼과 타이핑 영역의 글자가 **같은
		   세로선**에서 시작하고 끝난다 — 카드 안에 상자가 둘이면 안쪽 내용의 선도 하나여야
		   한다. 0.4rem이었을 때는 버튼이 테두리에서 4px 떨어져 있어 상자에 눌린 느낌이었고,
		   글자 선과도 9px 어긋나 있었다(2026-08-26 지적). */
		padding: 1.2rem 1.3rem;
		/* 모바일의 상자 언어는 타이핑 영역이 갖고 있다(`twsm.css:187-198`): 1px 테두리 +
		   0.5rem 라운드 + boxbg + 2px 오프셋 그림자. 헤더도 같은 상자여야 한 화면에 두 종류의
		   칸이 생기지 않는다. 데스크톱에서 이 칸이 `--boxbg`를 쓰는 것과도 같은 규칙이다. */
		background: var(--boxbg);
		border: solid 1px #0c0c0c;
		border-radius: 0.5rem;
		box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.7);
	}
	/* 위 줄 글자만 남긴 로고(`TwsLogo compact`). viewBox가 글자의 실제 bbox라 비율이
	   6.62:1이고, 폭은 그 비율로 따라와야 한다 — 어긋나면 SVG가 상자 안에서 레터박스로
	   놓여 글자만 작아진다. 가운데 열이라 양옆 1fr이 알아서 밀어 준다.
	   ⚠ 2026-10-15 에 글자가 antws_origin 타이틀의 Poppins 로 바뀌며 비율이 6.21 → 6.62 가 됐다 — 높이는 그대로 두고
	   폭만 9.43 → 10.06rem 으로 늘렸다(1.52 × 6.62).

	   높이 1.52rem은 **페이지 제목의 글자 크기와 맞춘 값**이다(2026-08-26 지시).
	   제목은 모바일에서 17.25px인데, 그건 font-size라 글자가 실제로 차지하는 높이와 다르다 —
	   같은 글꼴로 'typing.works'를 그렸을 때의 **잉크 높이가 15.18px**이다
	   (canvas `measureText`의 actualBoundingBox로 실측). 로고는 글자를 패스로 그린 것이라
	   그 잉크 높이가 곧 상자 높이다. font-size(1.725rem)를 그대로 쓰면 로고만 커 보인다. */
	.tws-logo-box > .tws-logo-mark {
		grid-column: 2;
		width: 10.06rem;
		height: 1.52rem;
	}
	/* 폰 메뉴(햄버거 — `MobileNav`, 2026-10-22 지시: 「헤더 좌측에 햄버거 버튼 만들고 모바일 전용 메뉴를 만들자」)는 **왼쪽 칸**이다.
	   그전에는 셋째 칸(오른쪽)에 아이콘 무리(업적 · 계정 · 설정 · 문의 — `> div { grid-column: 3 }`)가 섰는데 넷이 다 메뉴 안으로 들어갔다.
	   오른쪽 칸은 비고, 로고는 양옆 1fr 사이 가운데 그대로다.
	   ⚠ `grid-row: 1` 을 못 박는다 — 줄 번호 없이 열만 주면 자동 배치가 «앞 아이템(로고 · 2열)보다 앞 열»을 **다음 줄**로 보낸다.
	   JSX 에서 로고보다 앞에 두었지만 순서가 바뀌어도 상단 줄이 두 줄로 자라지 않게 한다. */
	.tws-logo-box > .tws-nav { grid-column: 1; grid-row: 1; justify-self: start; }
	/* margin은 원본 그대로 둔다 — 위 주석대로 이게 유일한 세로 간격이다.
	   **높이를 내린다.** 원본은 `16.2rem`으로 못박혀 있었는데(`twsm.css:189`) 그러면
	   1fr 트랙을 줘도 상자가 안 자란다. `auto`로 두면 그리드가 stretch로 행을 채운다 —
	   자라는 몫은 이 상자 안의 **바 아래 1fr 밴드**가 받는다(아래 타이핑 영역 절 참고).
	   문장과 진행도 바의 y는 그대로다: 키보드를 여닫을 때 치던 글자가 움직이면 안 된다. */
	.tws-typing-area { grid-area: 2 / 1 / 3 / 2; height: auto; }

	/* ── 기록 줄 (#twsInfo2) — 타이핑 영역과 상태 줄 사이 (2026-09-03 지시) ──
	   데스크톱의 `#twsInfo2`는 끝낸 문장을 전부 쌓고 칸이 스크롤한다. 폰에서는 세로
	   `1fr`을 타이핑 영역이 받아야 하므로 **방금 끝낸 한 줄만** 눕히고, 나머지는
	   눌러서 여는 전체 보기가 맡는다.

	   ⛔ **스크롤하지 않는다**(지시). 줄이 하나라 넘칠 일이 없지만, `.tws-info-2`의
	   `overflow-y: auto`(이 파일 위쪽)가 미디어쿼리 밖이라 클래스를 물려받으면 딸려
	   온다 — 그래서 클래스 이름부터 다르고, 넘치면 자른다.

	   ⚠ 클래스가 `.tws-info-2`가 아니다: `twsm.css:50`의 `display: none`이 미디어쿼리
	   밖이라 모든 폭에서 살아 있어(규칙 12) 그 이름을 쓰면 줄이 통째로 사라진다.

	   **새로 정한 값이 없다.** 테두리·라운드·그림자·`--boxbg`는 위아래 두 상자의 것,
	   좌우 1.3rem은 상단 줄·타이핑 영역이 쓰는 값(글자의 세로선이 하나로 맞는다),
	   위쪽 틈 0.2rem은 카드가 쓰는 그 값이다.

	   ⚠ 높이를 못박는다(규칙 10). 기록이 없는 동안 글자에 맡기면 첫 문장을 끝내는
	   순간 줄이 자라고, 그만큼 위의 타이핑 영역이 줄어 치던 글자가 움직인다. */
	.tws-mobile-records {
		grid-area: 3 / 1 / 4 / 2;
		display: grid;
		grid-template-columns: 1fr auto;
		/* 「랭크」 입구가 서면 아이템이 셋이다 — 넘치는 것을 새 «열»로(새 행이면 `overflow: hidden` 에 잘린다).
		   아이템이 둘일 때는 한 픽셀도 안 바뀐다(둘 다 이미 명시된 두 열에 앉는다) */
		grid-auto-flow: column;
		align-items: center;
		gap: 0.8rem;
		height: 2.8rem;
		padding: 0 1.3rem;
		margin: 0.2rem 0 0 0;
		border: solid 1px #0c0c0c;
		border-radius: 0.5rem;
		background: var(--boxbg);
		box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.7);
		overflow: hidden;
	}
	/* 숫자 칸의 폭은 데스크톱 `.tws-info-2>p>span`(twstb.css:148-163)의 비율 그대로다 —
	   값이 바뀔 때 줄이 흔들리지 않게 하는 것이 그 폭의 일이고, 화면이 달라도 같다.
	   세로 구분선(2·3번째 칸의 오른쪽 테두리)도 원본이 쓰던 그 문법이다. */
	.tws-mobile-records > p {
		/* 그리드 아이템의 기본 min-width는 내용 크기다 — 0으로 내려야 말줄임이 걸린다 */
		min-width: 0;
		font-size: 1.2rem;
		font-weight: 700;
		line-height: 1.4rem;
		color: var(--font);
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
	}
	.tws-mobile-records > p > span {
		display: inline-block;
		margin: 0 0.3rem 0 0;
	}
	.tws-mobile-records > p > span:nth-child(1) {
		width: 1.5rem;
	}
	.tws-mobile-records > p > span:nth-child(2),
	.tws-mobile-records > p > span:nth-child(3) {
		width: 2.9rem;
		margin: 0 0.8rem 0 0;
		border-right: 0.1rem solid;
	}
	/* 버튼색(`--btn-ink`, theme.css 맨 끝)이다 — 막대의 맨 아이콘(전체 보기 · 랭크의 순위표 · 내 기록 · 모드 토글)도 버튼이다
	   (2026-10-12 지시: 버튼의 글자 · 아이콘을 한 색으로). 그전에는 본문색 62%(반투명)였다 — 값보다 한 단계 물러나는 것은
	   대부분의 팔레트에서 그대로다(본문이 원래 부드러운 몇 팔레트는 예외 — theme.css 의 그 절). ⚠ 반투명으로 되돌리지 않는다 — 획이 겹치는 자리에 점이 박힌다. */
	.tws-mobile-records-more {
		display: flex;
		color: var(--btn-ink);
	}

	/* ── 하단 상태 줄 (2026-08-27 지시) ─────────────────────────────────────
	   목표 속도 / 속도 / 정확도 셋만. 폰에서 `#twsInfo` 10칸을 걷어내며 "나중에 다른
	   방식으로 채운다"고 남겨 둔 자리다.

	   **새로 정한 값이 없다.** 상자는 타이핑 영역의 것(`twsm.css:187-198`), 셋을 가르는
	   틈은 카드가 쓰는 `0.2rem`, 겉높이 5rem은 상단 줄과 같은 값이다 — 위아래 두 줄의
	   높이가 같아야 가운데 타이핑 영역이 화면의 가운데에 앉는다.

	   ⚠ 배경이 `--boxbg`가 아니라 **`#0c0c0c`(테두리와 같은 검정)**인 것이 요점이다.
	   이 앱은 요소를 회색 외곽선이 아니라 **검은 틈**으로 가른다(CLAUDE.md 8) — 칸 셋을
	   `--boxbg`로 깔고 그 사이로 이 검정을 2px 드러내면 데스크톱 카드와 같은 문법이 된다.
	   구분선을 새로 그리면 원본 어디에도 없는 회색 선이 하나 생긴다. */
	.tws-mobile-info {
		grid-area: 4 / 1 / 5 / 2;
		display: grid;
		grid-template-columns: 1fr 1fr 1fr;
		gap: 0.2rem;
		height: 5rem;
		/* 상단 줄↔타이핑 영역과 같은 간격. 타이핑 영역이 위쪽 margin으로 갖고 있는 그 값이다 */
		margin: 0.2rem 0 0 0;
		border: solid 1px #0c0c0c;
		border-radius: 0.5rem;
		/* 칸 사이로 드러날 검정 */
		background: #0c0c0c;
		box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.7);
		/* 라운드 안쪽으로 칸을 잘라 낸다 — 없으면 네 모서리에서 칸이 곡선을 비집고 나온다 */
		overflow: hidden;
	}
	.tws-mobile-info > div {
		display: grid;
		align-content: center;
		justify-items: center;
		background: var(--boxbg);
	}
	/* ── 문장 제목 / 출처 ───────────────────────────────────────────────────
	   **상태 줄 안이 아니라 그 아래 자기 칸이다**(2026-08-27 지적). PC·태블릿에서도
	   출처는 속도·정확도와 같은 칸에 얹히지 않고 `.tws-info-10`이라는 **제 칸**을 갖는다
	   — 폰만 다르게 만들 이유가 없다. 상자 언어(테두리·라운드·그림자·`--boxbg`)도 위의
	   두 상자와 같은 것을 쓴다.

	   ⚠ 높이를 못박는다. 출처가 없는 문장이 섞여 있어서 글자에 맡기면 문장을 넘길 때마다
	   이 칸이 사라졌다 나타나고, 그만큼 위의 타이핑 영역이 늘었다 줄었다 한다(규칙 10).
	   좌우 1.3rem은 상단 줄·타이핑 영역이 쓰는 그 값이라 글자의 세로선이 하나로 맞는다. */
	.tws-mobile-title {
		grid-area: 5 / 1 / 6 / 2;
		display: grid;
		align-content: center;
		height: 2.8rem;
		margin: 0.2rem 0 0 0;
		padding: 0 1.3rem;
		border: solid 1px #0c0c0c;
		border-radius: 0.5rem;
		background: var(--boxbg);
		box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.7);
	}
}

/* ══ 타이핑 영역 — 네 밴드 그리드 ═══════════════════════════════════════════
   원본은 `padding`으로 상하좌우 여백을 만들고 나머지를 흐름에 맡겼다
   (`twsm.css:193` 0.8/1.3 · `twstb.css:425` 4.3/4.5/0 · `twspc.css:69` 4.3/4.3/0).
   그래서 **NEXT의 자리가 위 내용의 높이에서 나왔다** — 문장 상자가 한줄(3.3rem)로
   짧아지는 순간 NEXT가 28px 딸려 올라간다. 밴드를 트랙으로 선언하면 그 종속이 끊긴다.

     ┌──────────────┐ 1  상단 영역    4.3rem  ← 원본 padding-top
     │  타이핑 영역   │ 2  문장    --tws-band  ← 뷰포트. 줄바꿈·세로 스크롤이 여기서 일어난다
     │              │                   (기본 5.9rem, 글자 크기를 키우면 따라 자란다)
     ├──────────────┤ 3  진행도 바     0.4rem  ← **고정 눈금자**
     │              │ 4  바~NEXT 사이 --tws-gap  ← **여기가 먼저 내준다** (기본 2rem)
     ├──────────────┤ 5  NEXT         auto
     └──────────────┘ 6  하단 영역     1fr  ← 4행이 다 내주고도 모자랄 때만 준다

   ── 진행도 바는 문장의 형제다 (2026-08-26 지시) ───────────────────────────
   바를 문장 상자 **안**에 두면 두 번 깨진다.
     ① 문장이 여러 줄이 되면 바가 밀려 내려간다 — 바의 y가 글자 수에 따라 달라진다.
     ② 그 상자에 스크롤을 걸면 바가 **스크롤 내용에 딸려 들어가** 맨 아래까지 내려야 보인다.
   형제로 두고 자기 행을 주면 둘 다 구조적으로 불가능해진다. 그래서 문장 상자는 순수한
   뷰포트가 되고, 줄바꿈/흐름 전환은 그 안에서만 일어난다 — 이게 이 구조의 목적이다.

   바는 **두줄 모드 자리에 고정**한다(4.3rem + 문장 행 = 기본 10.2rem). 한줄 모드에서
   글자에 붙이지 않는다 — 붙이면 다시 ①이 된다.

   ── 트랙 값의 출처 ────────────────────────────────────────────────────────
   지어낸 값이 하나도 없다. **원본이 이 자리에서 쓰던 세 조각을 그대로 행으로 편 것**이다:
     문장 상자 6.1rem − 상단 보정 0.2rem = 5.9rem   (마진 박스, 기본 글자 크기에서)
     진행도 바                          = 0.4rem
     바의 아래 여백 (`twstb.css:455`)    = 2rem   (기본 글자 크기에서)
   합이 8.3rem이고 4.3 + 8.3 = 12.6rem — **NEXT가 원본과 같은 y에 선다**(PC·태블릿 모두
   126 실측). 6행 `1fr`도 원본이 NEXT 아래 남기던 자리와 같다(PC 34.5px / 태블릿 82.5px).
   바의 margin은 4행이 됐으므로 0으로 내린다 — 안 내리면 같은 간격을 두 번 센다.

   ⚠ **문장 상자의 높이를 컴포넌트에서 선언하지 않는다.** 2행을 기본 `align-self: stretch`로
   채운다 — stretch는 **마진 박스**를 행에 맞추므로 -0.2rem 보정이 있는 상자는 6.1rem이 되고
   글자가 원본과 같은 y에 앉는다. 모드마다 height를 적으면 바가 그 높이에 딸려 다닌다.

   ── 좌우 여백을 트랙으로 ──────────────────────────────────────────────────
   `padding`을 지우고 같은 값을 1열·3열에 둔다. 값이 옮겨 갔을 뿐 **글자의 세로선은
   그대로**다(PC 4.3rem / 태블릿 4.5rem / 모바일 1.3rem). 패딩으로 남겨 두면 나중에
   "여백까지 쓰는 표시"(페이드가 상자 끝까지 닿는 식)를 만들 수 없다 —
   패딩은 자식이 넘어갈 수 없는 벽이지만 트랙은 span 하나로 넘어간다.

   ⚠ `h1.hidden`은 `position: absolute`(darkmode.css:111)라 그리드 아이템이 되지 않는다 —
   트랙을 하나 먹지 않는다. 그래서 아래 선택자에 포함하지 않는다. */
.tws-typing-area {
	display: grid;
	padding: 0;
	/* **안쪽 층은 안쪽 일이다.** 타이핑 영역 안에는 제 나름의 층이 있다 —
	   글자(0) < 좌우 페이드(5) < 안내문(6) < 떠 있는 커서(10). 이 영역은 쌓임 맥락을
	   만들지 않으므로 그 숫자들이 **바깥으로 새어** 형제와 직접 겨룬다: 그래서 묶음
	   프롬프트(z-index 5) 위로 안내문(6)이 떠올라 "위에 보이는 문장을 따라…"가
	   «결과보기» 창 안에 찍혔다(2026-09-03 지적).
	   `isolation: isolate`로 안쪽 층을 가둔다 — 프롬프트 쪽 숫자를 올려서 막으면
	   안에 6보다 큰 것이 하나 생길 때마다 같은 버그가 다시 난다(커서가 이미 10이다).
	   ⚠ 배치는 하나도 바뀌지 않는다. 쌓임만 가둔다. */
	isolation: isolate;
}

/* ── 묶음이 끝난 자리 (2026-09-02) ───────────────────────────────────────────
   단문 5문장·장문 1편을 끝내면 타이핑 영역이 물러나고 그 위에 «결과보기 / 계속치기»가
   뜬다. **자리를 재지 않는다** — 위 세 폭에서 `.tws-typing-area`와 **같은 grid-area**를
   주면 그리드가 정확히 같은 칸에 겹쳐 준다(배치의 소유자는 이 파일이다, CLAUDE.md 12).
   JS로 좌표를 재서 따라붙이면 폭이 바뀔 때마다 어긋난다.

   `z-index`가 필요한 이유: 같은 칸의 두 아이템은 **DOM 순서**로 쌓이는데, 타이핑 영역이
   opacity 전환 중에 쌓임 맥락을 만들어 순서가 뒤집혀 보일 수 있다. 카드를 덮는 페이지는
   40이므로 그보다 낮게 둔다 — 설정·성적표는 이 창 위에 떠야 한다.

   ⚠ **이 5는 타이핑 영역 '안'의 숫자들과 겨루지 않는다** — 그쪽은 위 `.tws-typing-area`의
   `isolation: isolate`가 가둔다. 이 값을 올려서 안쪽을 이기려 들지 말 것(안내문 6을 피하려
   올려 봐야 커서 10이 남는다). 여기 숫자가 상대하는 것은 **형제들**뿐이다. */
.tws-typing-area > .tws-typing-view,
.tws-typing-area > .tws-goal-bar,
.tws-typing-area > .tws-next-slot {
	grid-column: 2;
}
.tws-typing-area > .tws-typing-view { grid-row: 2; }
/* 위 주석대로 바의 아래 여백은 4행 트랙이 됐다. (0,2,0)이라 twstb의 (0,1,0)을 이긴다 */
.tws-typing-area > .tws-goal-bar    { grid-row: 3; margin: 0; }
.tws-typing-area > .tws-next-slot   { grid-row: 5; }

/* ── 명령줄 — 문장 자리를 덮는다 (2026-09-08 지시) ──────────────────────────
   `#`을 눌렀는데 그 자리에 `#`이 쳐야 할 글자가 아니면 App이 명령 모드로 갈리고
   (`isCommandStart`), 이 줄이 문장 자리에 얹힌다.

   **자리를 재지 않는다** — `.tws-typing-view`와 같은 grid-area를 주면 그리드가 정확히
   같은 칸에 겹쳐 준다(묶음 프롬프트가 쓰는 그 수법, 규칙 12). JS로 좌표를 재서 붙이면
   폭이 바뀔 때마다 어긋난다.

   ⚠ **불투명해야 한다.** 아래 문장은 언마운트하지 않고 그대로 두므로(Esc로 접을 때 장문
   span 수천 개를 다시 짓지 않으려고) 반투명이면 문장이 비친다.

   ⚠ **`z-index`는 11이다 — 떠 있는 커서(10)보다 위여야 한다.** 10으로 두었더니 문장의
   커서가 명령줄을 뚫고 나와 **캐럿이 둘 보였다**(실측). 이 영역 안의 층은
   글자(0) < 좌우 페이드(5) < 안내문(6) < 떠 있는 커서(10) < **명령줄(11)** 이고,
   `.tws-typing-area`의 `isolation: isolate`가 그 숫자들을 안에 가두므로 바깥 형제
   (묶음 프롬프트 5)와는 겨루지 않는다 — 묶음이 끝난 자리는 여전히 위다. */
.tws-typing-area > .tws-cmd-line {
	grid-column: 2;
	grid-row: 2;
	z-index: 11;
	background: var(--boxbg);
	display: flex;
	flex-direction: column;
	justify-content: center;
	gap: 0.6rem;
	overflow: hidden;
	min-height: 0;
	/* **왼쪽 0.2rem 은 문장 글자의 그 0.2rem 이다** — 문장 칸은 모든 모드가 글자를 0.2rem
	   들여 앉힌다(`TypingArea` 의 `margin … 0.2rem`·`left: 0.2rem`). 안 주면 명령이 문장보다
	   2px 왼쪽에서 시작해 «같은 자리»가 아니게 된다(2026-09-29 실측: 219.5 대 221.5). */
	padding-left: 0.2rem;
	/* **위쪽 -0.2rem 도 문장 칸의 그 값이다**(줄 박스 하프리딩 보정, `TypingArea` 의 모든 분기).
	   같은 칸에 얹는 것만으로는 모자라다 — 문장 칸은 이 보정으로 2px 위에서 시작하므로, 안 주면
	   명령줄 상자가 문장 상자보다 2px 낮고 짧다. 주면 **두 상자가 같은 마진 박스**다. */
	margin-top: -0.2rem;
}
/* 친 명령. **문장과 같은 크기·같은 색**이다 — 같은 자리에서 같은 손으로 치는 것이라
   크기가 달라지면 «다른 화면»으로 읽힌다. */
.tws-cmd-text {
	font-size: var(--tws-font);
	line-height: var(--tws-line);
	color: var(--font);
	white-space: nowrap;
}
/* 캐럿 — 연습의 명령줄과 랭크의 명령 창(`CommandPanel`)이 같이 쓴다. 굵기 · 색은 문장 쪽 `.tws-caret`(index.html)과 같다.
   ⚠️ **자리는 문장 쪽과 다르다**(2026-10-28 지적: 「캐럿 위치 잘 잡아주고」). 처음(2026-09-08)에는 문장 쪽 규격을 그대로 썼는데(높이 1.15em · 밑이
   글꼴 아래 끝 `text-bottom` · 왼쪽 -0.1rem) 그건 «글자와 글자 사이 경계 한가운데»에 서는 캐럿의 값이라, 줄 끝에 서는 이 캐럿에서는 ① 마지막 글자(«#»)의
   오른쪽 획에 1px 겹치고 ② 글꼴의 아래 여백까지 덮어 글자보다 아래로 6px 가까이 처졌다(실측 · 17px 굵은 Apple SD Gothic Neo — 기준선 위로 «#» 잉크
   +1 ~ +12.4px · «설» −1.2 ~ +13.8px · 캐럿 −5 ~ +15px).
   지금은 높이 1em · 밑이 기준선 0.12em 아래(−2 ~ +15px — «#» 위아래로 2.6 · 3px, 한글은 1px 안팎 넘는다) · 마지막 글자에서 0.08em 떨어진다.
   ⚠ 2026-09-08 의 «높이 1em 이면 `#`보다 낮게 앉고 짧다»는 밑을 `text-bottom`(−5px)에 둔 탓이었다 — 밑을 올려 그 일이 없다.
   **손이 멈추면 깜빡인다**(같은 날 지적: 「에니메이션이 빠진듯」) — 문장 쪽 `.tws-cursor-blink` 와 같은 1초 박자다. 치는 동안은 켜져 있다: 쓰는 쪽이 친
   글이 바뀔 때마다 이 요소를 새로 세워(`key`) 박자를 처음(켜짐)부터 다시 센다. */
.tws-cmd-caret {
	display: inline-block;
	width: 0.2rem;
	height: 1em;
	margin-left: 0.08em;
	vertical-align: -0.12em;
	background: var(--caret);
	animation: tws-caret-blink 1s steps(1) infinite;
}
/* «지금 어느 쪽이 손을 쥐고 있는가» — dev 팔레트가 같은 말을 같은 이유로 적는다 */
.tws-cmd-hint {
	font-size: 1.2rem;
	color: var(--npt);
}

/* ── 폰: 명령줄은 **문장이 있던 그 줄**에 선다 (2026-09-29 지시) ─────────────────
   「폰에서 명령이나 검색입력 모드가 되면 문장 위치에서 벗어나는 것을 확인했어 … 문장 위치
   그대로 명령줄로 바뀌는 자연스러움이 있어야해. 입력하는 곳은 여기다.라는 인식이 있을테니까」.

   까닭: 폰은 문장 칸(2행)이 **`1fr`** 이라 남는 높이를 다 받고(아래 폰 규칙), 문장 글자는
   그 칸의 **위쪽**에 앵커돼 있다. 그런데 명령줄은 칸 안에서 **세로 가운데**(`center`)라, 칸이
   650px 쯤 되는 폰에서는 명령이 **문장보다 300px 아래**에 떴다(실측: 문장 y69 · 명령 y369).
   데스크톱·태블릿은 칸이 한 줄 높이(`--tws-band`)라 가운데가 곧 문장 자리여서 안 드러났다.

   그래서 폰에서만 **위에 붙인다.** 상단 보정(-0.2rem)은 위 기본 규칙이 모든 폭에 이미 준다 —
   그래서 명령 글자가 문장 첫 글자와 **같은 픽셀**에 앉는다(실측 폰: 문장·명령 둘 다 (21, 69)).
   ⛔ 데스크톱까지 위로 붙이지 말 것 — 거기서는 문장이 칸 안에 가운데로 앉아 있어서 위로 붙이면
      오히려 어긋난다. */
@media (max-width: 600px) {
	.tws-typing-area > .tws-cmd-line {
		justify-content: flex-start;
	}
}

/* ── 문장을 받는 중 · 연결이 필요하다 (2026-10-01, docs/SENTENCE-IMPL.html 8절) ───────────
   로그인한 사람은 문장을 서버에서 받는다 — 처음이거나 응답이 늦어 **칠 문장이 아직 없을 때**의 모습이다.
   지시(같은 날): 「문장을 보여주는 구역에 NEXT 하고 진행 표시줄, 문장 작성, 문장 보여주는 걸 아무것도 없다고
   가정하고 시작하자. 한 묶음으로 페이드되서 같이 뜨는 걸로」 · 「로딩 가상 키보드는 가운데 그 밑에 텍스트 문구」.

   ① 그동안 구역의 넷(문장 · 입력 줄 · 진행 표시줄 · NEXT)은 **보이지만 않는다**(`opacity: 0`) — 자리는 그대로
      잡고 있어서 문장이 와도 카드 높이가 안 움직인다. ② 로더와 문구는 **구역 전체**의 가운데에 선다(모든 행 ·
      열을 덮는다). ③ 문장이 오면 덮개가 빠지고 넷이 **같은 200ms 로 같이** 떠오른다 — NEXT 칸이 원래 갖고 있던
      불투명도 전이(아래 `.tws-next-slot`)와 같은 값이다(모션 어휘 «전환» 200, CLAUDE.md 8).
   ⚠ 전이는 클래스가 바뀔 때만 돈다 — 처음부터 받는 중이면 첫 그림에 아무것도 안 움직이고(첫 배치 전이 없음),
     비로그인은 이 클래스가 영영 안 붙는다. 문장 칸이 다시 마운트돼도(표시 방식 전환) 전이는 안 돈다.
   ⚠ 명령줄(11)이 이 덮개(10)보다 위다 — 받는 중에도 `#` 로 명령을 친다. */
.tws-typing-area > .tws-typing-view,
.tws-typing-area > .tws-goal-bar {
	transition: opacity 200ms ease;
}
.tws-typing-area.tws-waiting > .tws-typing-view,
.tws-typing-area.tws-waiting > .tws-goal-bar,
.tws-typing-area.tws-waiting > .tws-next-slot {
	opacity: 0;
}
.tws-typing-area > .tws-wait-line {
	grid-column: 1 / -1;
	grid-row: 1 / -1;
	z-index: 10;
	/* 랭크 결정전 결과의 등장 순서가 이 덮개 한가운데에 선다(`RankViews.ResultMain` 의 `INTRO_STAGE` — 2026-10-08). 자리는 안 바뀐다 */
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 0.6rem;
	min-height: 0;
	/* 글자 기준을 안내문 크기로 — «다시 시도»(`CONTROL` 의 1.2em)가 그 1.2배가 된다. 토스트와 같은 비율이다.
	   안 주면 버튼이 문장 칸의 큰 글자를 기준으로 커져 안내문의 1.5배가 됐다(실측). */
	font-size: 1.2rem;
	text-align: center;
	/* **조금 늦게 뜬다** — 새로고침하면 남겨 둔 문장을 이 기기에서 읽는 데 수 ms 가 걸리는데(8-3), 로더를 곧바로 띄우면
	   그 사이 한두 프레임 번쩍였다 사라진다. 150ms 안에 끝나면 로더는 아예 안 보이고, 정말 기다릴 때만 200ms 로 떠오른다
	   (모션 어휘 «전환» 200). ⚠ 같은 덮개 안에서 «받는 중 → 연결 필요»로 바뀔 때는 다시 안 돈다(새로 마운트될 때만). */
	animation: tws-wait-in 200ms ease 150ms both;
}
@keyframes tws-wait-in {
	from {
		opacity: 0;
	}
}
.tws-wait-text {
	color: var(--npt);
}
/* «비로그인 상태로 타이핑하기» 앞의 안내(2026-10-15 — `TypingArea` 의 `onGuest`) — 좁은 화면에서 두 줄로 접힐 때 고르게 · 낱말 안에서
   안 끊는다(안 주면 폰에서 «않 / 습니다»로 끊겼다, 실측) */
.tws-wait-line > .tws-wait-note {
	max-width: 36em;
	text-wrap: balance;
	word-break: keep-all;
}
@media (prefers-reduced-motion: reduce) {
	.tws-typing-area > .tws-typing-view,
	.tws-typing-area > .tws-goal-bar {
		transition: none;
	}
	.tws-typing-area > .tws-wait-line {
		animation: none;
	}
}

/* ── 계정 설정을 받아 화면을 바꾸는 0.4초 (2026-10-14 지시: 「새 기기에서 처음 열 때 페이드로 스르륵 변경되도록 가능?」) ──────────
   로그인한 사람의 설정은 계정이 기준이다(`src/session/prefsSync.ts`). 첫 화면은 이 기기 사본으로 **바로** 그리고(서버를 기다리면 첫
   문장이 늦다), 계정 값이 다르면 **카드를 200ms 흐리게 → 바꾸고 → 200ms 다시 선명하게** 한다 — `app/usePrefsSync` 가 클래스를 단다
   (시간은 그 파일의 `PREFS_FADE_MS` 와 같이 고친다 · 모션 어휘 «전환» 200, CLAUDE.md 8).
   팔레트만 다르면 이 클래스가 안 붙는다 — 팔레트의 화면 전체 색 페이드(theme.css `data-theme-fading`)가 맡는다.
   ⚠ 전이는 **이 클래스가 붙어 있는 동안에만** 있다 — 평소의 카드에는 전이가 없어 첫 그림 · 다른 일에 안 낀다.
   ⚠ 늘 쓰던 기기는 사본과 계정 값이 같아 이 일이 안 생긴다(새 기기 · 다른 기기에서 바꾼 직후 처음 열 때만).
   ⚠ 움직임 줄이기를 고른 사람에게는 흐리게 하지 않는다 — 그냥 바뀐다(흐린 채 0.2초를 기다리면 빈 화면이 번쩍인다). */
/* ⚠ **전환(transition)이 아니라 애니메이션이다** — 계정 값을 얹는 순간 팔레트도 바뀌면 theme.css 의 색 페이드 규칙
     (`:root[data-theme-fading] *` — 200ms 동안 **모든 요소**의 `transition-property` 를 `!important` 로 덮는다)이 카드의
     불투명도 전환을 지워, 돌아오는 쪽이 페이드 없이 툭 켜졌다(실측: `in` 이 되자마자 불투명도 1). 애니메이션은 그 규칙을 안 탄다.
   ⚠ `both` — 흐려진 뒤 0 에 머문다(바꾸는 동안 안 보이게). 돌아오는 쪽은 0 에서 시작한다. */
.tws-main.tws-prefs-out {
	animation: tws-prefs-out 200ms ease-out both;
}
.tws-main.tws-prefs-in {
	animation: tws-prefs-in 200ms ease-in both;
}
@keyframes tws-prefs-out {
	to {
		opacity: 0;
	}
}
@keyframes tws-prefs-in {
	from {
		opacity: 0;
	}
}
@media (prefers-reduced-motion: reduce) {
	.tws-main.tws-prefs-out,
	.tws-main.tws-prefs-in {
		animation: none;
	}
}

/* ── 장문 ↔ 단문 전환을 부드럽게 (2026-09-08 지시) ──────────────────────────
   두 모드는 트랙 값이 통째로 다르다(위 여백·문장 창·NEXT 자리·아래 여백). 그대로 두면
   **한 프레임에 30px씩 튄다**(실측: 진행바 702→699→669→667→665).

   Chrome은 `grid-template-rows`를 보간한다 — **단 길이↔길이만.** `auto`와 `fr` 자리는
   그대로 튄다. 그래서 전이 한 줄로는 거의 안 듣는다(실측: 가장 큰 걸음 30 → 28px).
   진짜 처방은 아래 둘이고, 이 선언은 그 둘이 있어야 뜻이 생긴다:

     ① NEXT 줄이 **스스로 줄어든다**(`.tws-next-slot`, 바로 아래) — 5행은 `auto`로 남고
        트랙이 래퍼의 지금 높이를 따라온다. 자리를 통째로 없애면 그 순간 `1fr`이 그 몫을
        먹어 바가 25.5px 튄다.
     ② 장문 규칙이 **단문과 같은 «모양»**을 쓴다 — 어느 칸이 `fr`인지까지 맞춘다(아래).

   **200ms**는 이 앱의 모션 어휘 셋 중 «토글·노브·흐름·페이지 전환»의 값이다(규칙 8).

   ⚠ **첫 페인트에 돌지 않는 것은 `tws-nonext`가 첫 렌더부터 확정이기 때문이다.**
   한때 그 클래스를 «`nextText`가 비었나»로 정했는데, 첫 렌더에는 문장 목록이 아직 없어
   단문에서도 비어 있다 — 한 프레임 장문으로 그려졌다가 뒤집히면서 **로드할 때마다 바가
   745 → 708로 흘렀다.** 지금은 저장값 `short` 하나로 정한다(App의 그 주석). 그 조건을
   «화면에 실제로 글자가 있나»로 되돌리면 이 미끄러짐이 그대로 돌아온다.
   ⚠ 비용: 전환하는 동안 프레임마다 배치를 다시 잡는다. 실측으로 **최악 프레임이 나빠지지
   않았다**(장문→단문 154ms → 77ms, 단문→장문 32 → 31ms) — 그 154ms는 애니메이션이 아니라
   span 4,431개짜리 트리를 헐고 새로 짓는 비용이라 어차피 든다. */
.tws-typing-area {
	transition: grid-template-rows 200ms ease;
}

/* NEXT가 앉는 칸. **접히는 것은 이 래퍼이고 NEXT 자신이 아니다** —
   원본 CSS가 `.tws-next-typing > span`으로 라벨을 잡으므로 안쪽에 한 겹만 끼워도
   직계 자식이 아니게 되어 폰트가 날아간다(TypingArea의 그 주석).

   `1fr → 0fr`은 **높이를 몰라도 접히는 유일한 수단**이다(`RecordsPage`의 `Collapse`가
   쓰는 그것). `auto`↔`0`은 보간되지 않아 그냥 튄다. */
.tws-next-slot {
	display: grid;
	grid-template-rows: 1fr;
	transition: grid-template-rows 200ms ease, opacity 200ms ease;
}
/* `0fr`에서 자식이 삐져나오지 않게 하는 짝 — 이 둘이 없으면 안 접힌다 */
.tws-next-slot > .tws-next-typing {
	overflow: hidden;
	min-height: 0;
}
/* 장문 — 없어질 것은 **줄어들면서** 없어진다(지시) */
.tws-nonext .tws-next-slot {
	grid-template-rows: 0fr;
	opacity: 0;
}
/* ── 진행 표시줄 · NEXT 줄을 끈 사람(2026-10-16 지시: 「진행줄 보기 끄기 / 기본: 보기」 · 「NEXT 보기 끄기 / 기본: 보기」 — 설정 › 타이핑) ──
   클래스는 App 이 카드에 단다(`tws-bar-off` · `tws-next-off` — 상태 표시일 뿐 배치가 아니다, `tws-nonext` 와 같은 계약).
   · NEXT — 장문과 **같은 모양으로** 줄어들며 접힌다(바로 위 규칙). 행(`grid-template-rows`)은 안 건드린다 — 단문의 행이 그대로라 문장 · 진행
     표시줄은 한 픽셀도 안 움직이고, NEXT 가 비운 높이는 그 아래 남는 행(`1fr`)이 받는다. 줄바꿈 · 최대(`tws-wrap`)에서는 문장 띠가 그만큼 자란다.
     App 이 연습에서만 단다 — 랭크의 그 칸은 문장 결과의 시계가 도는 자리라 접으면 안 된다.
     ⚠ 2026-10-27 몇 시간은 «문장 처음부터» 단추가 이 줄 안에 있어 끈 NEXT 를 접지 않고 라벨 · 글만 감췄다 — 단추가 진행 표시줄 아래로 옮겨 가며
     (아래 `.tws-next-reset`) 되돌렸다.
   · 진행 표시줄 — **자리는 남기고 안 보이게만** 한다(`visibility`). 행(0.4rem)을 걷으면 그 아래 NEXT 가 올라간다. `opacity` 로 감추지 않는 것은
     랭크 판의 짙어지기 · 흐려지기(`.tws-rank-count ~ .tws-goal-bar` 의 애니메이션)가 `opacity` 를 덮어 다시 보이게 하기 때문이다 */
.tws-next-off .tws-next-slot {
	grid-template-rows: 0fr;
	opacity: 0;
}
/* ── «문장 처음부터»(Esc 와 같은 일 — `TypingArea.NextReset`, 2026-10-27) — 진행 표시줄 오른쪽 끝 바로 아래 ─────────────────────
   지시: 「NEXT 위치한 요소 우측 끝에 문장 초기화 버튼」 → 같은 날 「진행표시줄은 안없어지니까 진행표시줄 밑에 2~3px정도 간격을 넣고 … 진행표시줄 꺼도
   나올 수 있게끔」. NEXT 줄은 장문 · NEXT 끔에서 접혀 그 안에 두면 같이 사라졌다 — 진행 표시줄의 행(3행)은 어느 모드에서나 남는다(끈 막대도
   `visibility` 로만 감춘다 — 위 규칙). 그래서 단추를 **3행의 오른쪽 끝에 세우고 막대 아래로 0.25rem(2.5px) 내린다** — 행은 0.4rem 그대로고
   단추는 그 밑(바와 NEXT 사이 `--tws-gap` · 장문 줄바꿈이면 아래 여백)으로 비어져 나온다. 막대와 따로 된 요소라 끈 막대에 같이 숨지 않는다.
   ⚠ 글자를 키우면 `--tws-gap` 이 0 까지 줄어 단추가 NEXT 줄의 오른쪽 끝에 걸린다 — 그래서 NEXT 줄은 그만큼 오른쪽을 비운다(아래 `padding-right` —
   단추 둘 1.8rem × 2 + 사이 0.6rem + 틈 0.8rem). 랭크 · 폰에는 단추가 없다(App · `TypingArea` 가 안 그린다).
   ⚠️ **오른쪽에 «#»(명령 창 — 2026-11-02 부터 · 그전에는 문장 자리의 명령줄)이 같이 선다**(2026-10-28 지시: 「연습 모드에 문장 처음부터 버튼 오른쪽에
   # 버튼 넣자」 — 창은 아래 «명령 버튼 «#»과 명령 창» 절의 `.tws-mini-slot`) — 둘이 한 감싸개라 오른쪽 끝(막대의
   끝)은 «#»이고 «문장 처음부터»는 그만큼(1.8rem + 0.6rem) 왼쪽으로 갔다. 사이는 결과의 리스트 · 그래프 단추 사이와 같은 0.6rem */
.tws-typing-area > .tws-next-reset {
	grid-row: 3;
	grid-column: 2;
	justify-self: end;
	align-self: start;
	position: relative;
	top: calc(0.4rem + 0.25rem);
	z-index: 1;
	line-height: 0;
	display: flex;
	gap: 0.6rem;
}
.tws-typing-area:not(.tws-rank) > .tws-next-slot > .tws-next-typing {
	padding-right: calc(1.8rem * 2 + 0.6rem + 0.8rem);
}
.tws-bar-off .tws-goal-bar {
	visibility: hidden;
}

@media (max-width: 600px) {
	.tws-typing-area {
		grid-template-columns: 1.3rem 1fr 1.3rem;
		/* 폰에는 NEXT가 없다(`twsm.css:262` display:none → 5행이 0으로 접힌다).
		   바 아래는 통째로 남는 자리고, 마지막 0.8rem은 원본 `padding-bottom` 그대로.
		   문장 행은 다른 폭과 같으므로(기본 5.9rem) 바가 원본과 같은 y(0.8+5.9)에 선다.
		   폰만 2행이 `1fr`이라 `--tws-band`를 쓰지 않는다 — 이미 남는 높이를 다 받는다. */
		/* **문장 뷰포트(2행)가 1fr이다** — 상자가 자라면 그 몫을 여기가 받고, 진행도 바는
		   상자 바닥에 남아 아래 테두리 노릇을 한다. 반대로 두면(바 아래를 1fr) 바가 화면
		   위쪽 1/4에 뜬 채 그 밑이 통째로 비어 상자가 미완성으로 읽힌다(2026-08-27 실측).
		   글자는 이 뷰포트의 top에 앵커돼 있으므로 **키보드를 여닫아도 y가 안 변한다**
		   (실측: 문장 y64 · 상자 730↔400 어느 쪽에서도 동일). 치는 손 밑에서 글이
		   움직이면 안 된다 — 자리를 옮기는 건 바 하나뿐이다.
		   NEXT는 폰에서 `display: none`이라 5행이 0으로 접힌다. 4행을 0으로 둔 것은
		   그 사이 여백(2rem)이 바를 바닥에서 띄우기 때문이다. */
		grid-template-rows: 0.8rem 1fr 0.4rem 0 auto 0.8rem;
	}
	/* ── 폰의 랭크 — 맨 위 띠(docs/RANK-IMPL.html 11-4 · 11-6 폰) ─────────────────────────
	   폰에는 NEXT 줄이 없어(위 주석) 5·4·3·2·1 · 「1/5 · 상대 5.8초」 · 문장 결과를 띄울 데가 없었다. 랭크일 때만(`tws-rank`)
	   1행(위 여백 0.8rem)을 띠로 키워 **판의 줄(NEXT 칸을 그 자리로 옮긴다)**을 둔다. 모드 토글은 기록 막대에 있다(아래).
	   ⚠ 맨 위라 **키보드가 올라와도 보인다**(문장 · 띠가 뷰포트 위에 앵커돼 있다 — 위 주석의 실측).
	   ⚠ 판의 줄은 대기 · 결과에서 덮개 밑에 숨는다(`tws-waiting`).
	   ⚠ 연습은 문장 자리가 한 픽셀도 안 바뀐다 — 1행이 그대로 0.8rem 이다(토글은 기록 막대 오른쪽에 선다).
	   ⚠ 원본(`twsm.css`)이 폰에서 `.tws-next-typing` 을 `display: none` 으로 숨긴다 — (0,3,0) 으로 되살린다. */
	/* ⚠️ **2026-10-08 — 문장 자리를 위아래로 쪼갰다**(지시: 「1,2칸은 몇번째 나타내는게 있어야하고 2,2칸을 상하로 쪼개서 밑에다가 상대
	   놓으라니까? 폰 키보드 올라올거 생각해서」): 1 맨 위 띠(3rem — 칸 다섯 · 판의 이름) · 2 내 문장(`1fr`) · 3 상대 칸(`1fr`) · 4 진행 막대
	   (감춘다 — 데스크톱 랭크와 같다) · 5 NEXT(비었다 — 칸 다섯이 승패를 칠하므로 문장 결과 글도 안 쓴다) · 6 바닥.
	   ⚠ 키보드: 타이핑 영역이 보이는 높이(`--app-h`)를 따라 줄어들면 2 · 3행이 **같이 반씩** 준다 — 상대 칸이 키보드 밑으로 안 숨는다.
	   ⚠ 그전 몇 분은 띠를 4.6rem 으로 키워 그 안에 상대를 두 줄로 넣었다 — 지시를 잘못 읽은 것이었다 */
	.tws-typing-area.tws-rank {
		grid-template-rows: 3rem 1fr 1fr 0 0 0.8rem;
		--tws-rank-name-size: 1.2rem;
	}
	.tws-typing-area.tws-rank > .tws-goal-bar {
		display: none;
	}
	.tws-typing-area.tws-rank > .tws-next-slot {
		grid-row: 5;
		align-self: center;
		min-width: 0;
	}
	/* 타이핑 1,2 — 몇 번째 문장(칸 다섯)은 **오른쪽 끝**, 판의 이름(결정전)은 왼쪽 끝, 신호등은 가운데(2026-10-19 지시: 「문장 개수 막대를 기존 위치에서
	   우측 정렬로 변경」 — 그전에는 칸 다섯이 왼쪽 · 이름이 오른쪽이었다. 이름은 칸 다섯과 자리를 맞바꿨다).
	   칸 다섯은 띠의 바닥에서 판의 이름 글자 높이의 1/3 만큼 띄운다(2026-10-08 지시: 「1,2칸의 몇 개를 나타내는 막대가 데스크탑 처럼 여백을 갖고
	   좌측 하단 정렬하도록」 — 그 몇 분은 띠의 세로 가운데였다). 여백 식은 2026-10-18 까지의 데스크톱과 같다(데스크톱은 10-19 에 신호등이 들어오며
	   칸 다섯이 올라갔다 — 폰은 띠가 3rem 이라 신호등이 칸과 같은 줄에 선다, 아래) */
	.tws-typing-area.tws-rank > .tws-rank-steps {
		grid-column: 2;
		grid-row: 1;
		justify-self: end;
		align-self: end;
		margin-bottom: calc(var(--tws-rank-name-size) / 3);
	}
	/* **신호등** — 맨 위 띠의 **가운데**, 띠의 세로 가운데(2026-10-19 — 판의 이름(결정전 · 왼쪽 끝)과 같은 줄). 데스크톱의 자리(칸 다섯 아래)는 띠가
	   3rem 이라 칸 위로 3px 밖에 안 남는다. ⚠ 처음에는 칸 다섯과 같은 세로 가운데(띠 바닥 쪽)였는데 상대 화살표(▼ — 내 문장 맨 위 줄에 선다)가 띠 바로
	   밑이라 상대가 가운데쯤 글자를 칠 때 누운 불과 겹쳤다(같은 날 실측 — 불 아래 끝 31px · 화살표 윗변 29px). 띠 가운데로 올려 4px 를 비운다 */
	.tws-typing-area.tws-rank > .tws-rank-lights {
		grid-column: 2;
		grid-row: 1;
		justify-self: center;
		align-self: center;
	}
	.tws-typing-area.tws-rank > .tws-rank-name {
		grid-column: 2;
		grid-row: 1;
		justify-self: start;
		align-self: center;
	}
	/* 한 줄이다 — 라벨(「시작」 · 「1/5」)과 글을 나란히, 넘치면 글 쪽이 말줄임. 폰에서는 `TypingArea` 가 이 줄에 인라인
	   배치를 안 붙이므로(원본이 숨기는 줄이었다) 여기서 세운다. 색 · 굵기는 데스크톱 NEXT 줄(`twstb.css` 의 그 규칙)과 같다 */
	.tws-typing-area.tws-rank > .tws-next-slot > .tws-next-typing {
		display: flex;
		align-items: baseline;
		gap: 0.8rem;
		white-space: nowrap;
		overflow: hidden;
	}
	.tws-typing-area.tws-rank > .tws-next-slot > .tws-next-typing > span {
		flex: none;
		font-size: 1.2rem;
		font-weight: bold;
		color: var(--npt);
	}
	.tws-typing-area.tws-rank > .tws-next-slot > .tws-next-typing > #npt {
		position: static;
		min-width: 0;
		margin: 0;
		padding: 0;
		overflow: hidden;
		text-overflow: ellipsis;
		font-size: 1.2rem;
		color: var(--npt);
	}
	/* **폰의 상대 칸 — 타이핑 3,2**(문장 자리의 아래 반 — 위 행 주석). 진행(글자 · 막대)은 너비 전부다(「상대줄을 좌우반으로 가르지말고 너비를
	   다 사용하자」). 칸 안의 배치(아래에 붙은 두 줄)는 아래 «상대 칸» 절이 데스크톱과 한 벌로 정한다 — 여기는 자리와 위 여백만 */
	.tws-typing-area.tws-rank > .tws-rank-rival {
		grid-column: 2;
		grid-row: 3;
		padding-top: 0.8rem;
		/* 벽 — 데스크톱 · 태블릿보다 넓다(같은 날 지시: 「휴대폰은 1/5제한으로 해보자」 · 「모바일은 2/3 사용」 — 막대) */
		--tws-rival-text-w: 80%;
		--tws-rival-bar-w: calc(100% * 2 / 3);
	}
	/* 폰의 모드 토글은 **기록 막대의 오른쪽**이다(`MobileRecordBar` 의 `modeToggle` — 연습 · 랭크 어느 쪽에서나 같은 자리).
	   기록 칸의 것(`.tws-info-toggle`)은 폰에서 숨긴다 — 폰의 `#twsInfo` 는 메뉴로만 펼쳐진다 */
	.tws-info > .tws-info-toggle {
		display: none;
	}
}
@media (min-width: 601px) and (max-width: 1024px) {
	.tws-typing-area {
		grid-template-columns: 4.5rem 1fr 4.5rem;
		grid-template-rows: 4.3rem var(--tws-band) 0.4rem var(--tws-gap) auto 1fr;
	}
}
@media (min-width: 1025px) {
	.tws-typing-area {
		grid-template-columns: 4.3rem 1fr 4.3rem;
		grid-template-rows: 4.3rem var(--tws-band) 0.4rem var(--tws-gap) auto 1fr;
	}
}

/* ══ 줄바꿈 모드 — 카드가 '페이지 크기'로 자란다 ════════════════════════════
   문장을 접으면 한 줄이 여러 줄이 되므로 두 줄짜리 문장 행(기본 5.9rem)으로는 두 줄밖에
   못 본다.
   그래서 **카드째 키운다.** 목표 높이는 사용자가 정했다(2026-08-28): 로그인·설정
   페이지와 같은 높이.

   ── 그 높이를 여기서 다시 계산하지 않는다 ─────────────────────────────────
   `CardPage.expand()`가 쓰는 식을 **그대로** 옮긴 것이다:
       max(카드 높이, min(카드 높이 × GROW_RATIO, 창높이 − MIN_TOP − MIN_BOTTOM))
   `GROW_RATIO = 2`, `MIN_TOP = 44.4px = 4.44rem`, `MIN_BOTTOM = 24px = 2.4rem`(루트 10px는
   원본 CSS가 고정한다) → 창에서 빼는 값이 6.84rem.
   여기서 '카드 높이'는 **평소(흐름) 카드**다: 1440×900에서 흐름 카드 344 → 페이지 688,
   그래서 줄바꿈 카드도 688이다(태블릿 386 → 772, 실측 일치).
   ⚠ 글자 크기를 바꿔도 이 숫자는 **안 움직인다** — 카드 높이는 글자와 무관하다(맨 위 참고).
   ⚠ 저쪽 상수가 바뀌면 여기도 같이 바꿔야 한다 — CSS는 그 모듈을 읽을 수 없다.

   ⚠ **줄바꿈 상태에서는 덮개가 더 커지지 않는다** — 카드와 같은 자리·같은 크기다.
   `CardPage`의 `fitCard`가 그 경우 `expand()`를 통째로 건너뛰고 카드 rect를 그대로 쓰기
   때문이다(App이 `fitCard={wrapOn}`으로 알려 준다). 한동안 여기 "688 → 852로 한 번 더
   커진다"고 적혀 있었는데, 그건 `fitCard`가 생기기 전 이야기였다 — 그 글을 믿고 기하를
   계산하면 있지도 않은 회귀를 쫓게 된다(2026-08-31 검증에서 걸렸다).

   JS로 재서 인라인으로 쓰지 않는 이유는 규칙 12 그대로다 — 배치의 소유자는 하나다.
   `min()`/`max()`는 이 앱이 지원하는 모든 브라우저에 있다.

   ── 자란 몫은 문장 행이 받는다 ────────────────────────────────────────────
   2행을 `1fr`로 바꾼다(모바일이 이미 쓰는 방식). 6행은 **원래 크기로 못박는다** —
   `1fr`로 두면 늘어난 높이를 2행과 나눠 가져 NEXT 아래가 같이 벌어진다.
   3.45rem / 8.25rem은 그 `1fr`이 실제로 잡던 값이다(위 인포 주석의 실측 34.5px / 82.5px).
   진행도 바(3행)는 문장 상자 바로 아래에 그대로 붙어 아래 테두리 노릇을 계속한다.

   ── `.tws-fixed` — 자라지 **않기**를 고른 상태 (2026-09-01 지시) ───────────
   설정의 '메인 높이'가 `고정`이면 App이 카드에 이 클래스를 더한다 — **그게 기본값이라
   줄바꿈의 평소 모습이 이쪽이고, 자라는 쪽(`최대`)이 고르는 쪽이다.** 아래 세 곳(카드 높이·
   카드 행·타이핑 행)과 `main`의 대칭 여백이 **전부 `:not(.tws-fixed)`로 걸려 있어**
   그대로 안 걸리고, 위쪽의 평소 규칙(`.tws-main`, `.tws-typing-area`)이 그대로 이긴다.

   **고정 쪽 값을 여기에 다시 적지 않는 이유**가 그것이다. 34.4rem·`5rem 10rem 18.6rem`·
   `4.3rem var(--tws-band) …`를 여기 한 벌 더 쓰면 평소 카드를 고칠 때 두 곳을 고쳐야 하고,
   한쪽만 고치면 **줄바꿈+고정에서만 어긋나는** 배치가 남는다(이 프로젝트가 정렬·지표에서
   세 번 데인 것과 같은 종류다). '안 자란다'는 규칙의 **부재**로 적는 게 정확하다.

   ⚠ **접기 자체는 이 클래스와 무관하다.** 글이 접히는 것은 `TypingArea`가 `textFlow`로
   판단하고, 이웃 고스트의 `pre-wrap`(맨 아래)도 `.tws-wrap`만 본다 — 고정은 **카드가
   자라는 것만** 끈다. 그래서 고정 + 줄바꿈은 '두 줄짜리 창으로 접힌 글을 세로로 넘기는'
   상태가 되고, 커서 줄은 맨 윗줄에 앉는다(`wrapAnchor`가 보이는 줄 수로 정한다).      */
@media (min-width: 601px) and (max-width: 1024px) {
	.tws-main.tws-wrap:not(.tws-fixed) {
		height: max(38.6rem, min(77.2rem, calc(100vh - 6.84rem)));
		/* 카드의 타이핑 행도 함께 풀어 준다 — 아래 ⚠ 참고 */
		grid-template-rows: 15.2rem 1fr;
	}
	.tws-wrap:not(.tws-fixed) .tws-typing-area {
		grid-template-rows: 4.3rem 1fr 0.4rem 2rem auto 8.25rem;
	}
}
@media (min-width: 1025px) {
	.tws-main.tws-wrap:not(.tws-fixed) {
		height: max(34.4rem, min(68.8rem, calc(100vh - 6.84rem)));
		grid-template-rows: 5rem 10rem 1fr;
	}
	.tws-wrap:not(.tws-fixed) .tws-typing-area {
		grid-template-rows: 4.3rem 1fr 0.4rem 2rem auto 3.45rem;
	}
}
/* ── NEXT가 비었을 때 — 그 자리를 문장 창이 받는다 (2026-09-08 지시) ─────────
   장문에는 NEXT가 없다(App이 `nextText`를 비운다). 그런데 줄은 `visibility: hidden`이라
   **자리는 그대로 먹는다** — 바 아래가 `2rem + NEXT 25.5 + 나머지`로 80px이 되어,
   문장 위(4.3rem = 43px)와 견주면 아래만 두 배 가까이 넓었다(실측 위 49 / 아래 82).

   그래서 NEXT가 없으면 **4·5행을 0으로 접고 마지막 행을 4.3rem으로 둔다.**

   ⚠ **위(3.5rem)와 아래(4.3rem)의 숫자가 일부러 다르다**(2026-09-08 지시). 상자 기준으로
   재면 어긋나 보이지만 **눈에 닿는 것은 상자가 아니라 잉크**다 — 1행 아래에는 첫 줄의
   half-leading 6px이 더 붙어 글자가 그만큼 늦게 시작한다(실측 43 → 잉크까지 49). 반면
   아래는 진행도 바 밑의 순수한 빈칸이라 43이 그대로 43이다. 그래서 위를 8px 줄여
   **35 + 6 = 41 ≈ 43**으로 맞춘다. 두 숫자를 같게 되돌리면 위가 다시 6px 넓어 보인다.

   ⚠ **되찾은 45px은 문장 행(2행)이 받는다**(2026-09-08 지시). `1fr`이라 남는 높이가
   전부 그쪽으로 흐른다 — 고정에서도 카드는 그대로이고 **보이는 줄만 2 → 3으로** 는다.
   그 자리는 어차피 아무것도 없던 곳이다. 최대는 2행이 이미 `1fr`이라 값이 같아진다.

   ⚠ **줄바꿈에서만 건다.** 흐름(`.tws-wrap`이 없는 상태)도 장문이면 같은 80px이 남지만,
   거기는 문장이 한 줄이고 이웃 고스트가 같은 칸에 서 있어 행을 풀면 영향이 다르다 —
   지시 범위(고정/최대 = 줄바꿈)를 넘지 않는다.

   특이도는 (0,3,0)으로 위 `.tws-wrap:not(.tws-fixed) .tws-typing-area`와 **같다** —
   그래서 반드시 그 뒤에 와야 이긴다. 미디어쿼리 안에 또 적지 않는 이유는 두 폭의 값이
   같아졌기 때문이다(폰은 `.tws-wrap`이 붙어도 위 폰 규칙이 (0,1,0)이라 지는데, 폰에는
   NEXT가 `display: none`이라 애초에 5행이 0이고 바 아래가 이미 통째로 남는 자리다 —
   그 규칙을 여기서 덮으면 안 된다). */
@media (min-width: 601px) {
	/* **최대** — 단문(`4.3rem 1fr 0.4rem 2rem auto 3.45rem`)과 같은 모양이다:
	   2행이 `1fr`, 6행이 길이. 그래서 여섯 트랙이 전부 보간된다. */
	.tws-wrap:not(.tws-fixed).tws-nonext .tws-typing-area {
		grid-template-rows: var(--tws-long-top) 1fr 0.4rem 0 auto var(--tws-long-bottom);
	}
	/* **고정** — 단문(`4.3rem var(--tws-band) 0.4rem var(--tws-gap) auto 1fr`)은 2행이
	   길이고 6행이 `1fr`이다. 값이 같아도 «어느 칸이 `fr`인가»가 다르면 그 칸은 보간되지
	   않고 튀므로, 이쪽도 **2행을 길이로 적는다.** 남는 높이를 직접 계산하는 셈이라
	   `100%`(= 타이핑 영역의 높이, 카드 그리드가 정해 둔 확정값)에서 나머지를 뺀다.
	   6행 `1fr`이 남은 것을 받아 `--tws-long-bottom`이 된다. */
	.tws-wrap.tws-fixed.tws-nonext .tws-typing-area {
		grid-template-rows:
			var(--tws-long-top)
			calc(100% - var(--tws-long-top) - 0.4rem - var(--tws-long-bottom))
			0.4rem 0 auto 1fr;
	}
}

/* ── 랭크전의 데스크톱 · 태블릿 — 상태 줄(2026-10-06) ─────────────────────────────────────────────────────
   타이핑 영역의 격자는 **연습과 같다** — 문장 띠 · 진행 바 · NEXT 칸(상대 · 문장 결과)이 연습과 같은 자리에 선다(같은 날 지시:
   「연습과 같은 영역으로 다시 돌리자. 그 상태에서 옮겨야겠음」). 그사이 세 띠 · 다섯 칸으로 나눠 봤는데 문장 띠가 연습보다
   19px 아래로 내려가 모드를 오갈 때 문장이 뛰었다(실측 — 띠 윗변 41 → 60px).
   랭크가 더하는 것은 **맨 위 줄(1행)** 둘이다 — 몇 번째 문장(`.tws-rank-steps` — 칸 다섯, **모든 판** · 2026-10-07 지시:
   「랭크전도 결정전과 같은 위치로 n/5」 — 그전 상대가 있는 판은 대결 줄 끝의 「1/5」 글자였다 · 2026-10-19 부터 오른쪽)와 신호등(왼쪽 — 아래).
   판의 이름(`.tws-rank-name` — 「랭크 결정전」, 상대가 없는 판에만)은 2026-10-22 부터 타이핑 3,2 의 오른쪽 아래다(상대 칸의 이름 줄 자리 — 아래).
   셋 다 2열(문장 띠와 같은 열)이라 끝이 문장 띠의 끝과 같은 세로선이다.
   ⚠ 2026-10-07 에 자리가 여러 번 바뀌었다(지시): 「1/5」 글자 + 이름이 NEXT 자리(5행)에 한 줄 → 이름만 맨 위 왼쪽 → 다시 5행 오른쪽
     (「1/5」는 맨 위 왼쪽 칸 다섯으로) → **이름이 맨 위 오른쪽**(「티켓 아이콘이 있던 자리에 '랭크 결정전' 텍스트를 넣어보자」 — 티켓은
     정보 패널의 「결과까지」 칸 아래로 갔다). 5행에 서던 동안에는 NEXT 칸(«내 기록 n초»)을 6행으로 내렸는데, 이제 5행이 비어 그 규칙도
     걷었다 — NEXT 칸은 연습 · 랭크 판과 같은 5행이다.
   ⚠ 연습은 한 픽셀도 안 바뀐다 — 둘 다 랭크일 때만 그려지고(`RankViews.rankNext` 의 `split`) 자리도 `tws-rank` 로만 건다.
   ⚠ 이름의 글자는 NEXT 줄(원본 `twspc.css` · `twstb.css` 의 `.tws-next-typing>span` · `#npt`)과 같은 값이다 — 새로 정하지 않는다.
   ⚠ 폰은 따로다(위 «폰의 랭크 — 맨 위 띠» — 둘 다 안 그린다). */
@media (min-width: 601px) {
	/* 판의 이름 — **타이핑 3,2 의 오른쪽 아래**(2026-10-22 지시: 「랭크 결정전 텍스트를 우측 하단으로 넣자」). 상대가 있는 판이면 그 자리에 상대의
	   이름 · 티어 · 속도 줄이 선다(아래 «상대 칸» — 칸의 바닥 · 오른쪽 끝) — 결정전은 상대가 없으니 그 자리에 판의 이름이 선다. 바닥 여백 0.4rem 은
	   상대 칸의 위아래 여백과 같은 값이다(규칙 8 — 이름의 바닥이 상대 이름 줄의 바닥과 같은 선). 오른쪽 끝은 문장 띠의 끝 = 칸 다섯의 끝과 같은 세로선.
	   ⚠ 그전(2026-10-07 ~ 10-22)에는 **맨 위 오른쪽**이었다(2026-10-07 지시: 「티켓 아이콘이 있던 자리에 '랭크 결정전' 텍스트를 넣어보자」) — 1행의
	     바닥(2026-10-08 지시: 「랭크 결정전도 하단 정렬하자」), 10-19 부터는 칸 다섯이 그 위 오른쪽 끝으로 와서 칸 다섯 바로 밑이었다(칸의 그림자 끝과
	     이름 윗변 사이 5px). 같은 날(10-07) 그 전에는 NEXT 자리 오른쪽 끝 · 맨 위 왼쪽이었다.
	   ⚠ 폰은 그대로 맨 위 띠의 왼쪽 끝이다(위 «폰의 랭크») */
	.tws-typing-area.tws-rank > .tws-rank-name {
		grid-column: 2;
		grid-row: 3;
		justify-self: end;
		align-self: end;
		margin-bottom: 0.4rem;
	}
	/* 몇 번째 문장 — **칸 다섯**(2026-10-07 지시: 「n/5 삭제 → 현재 '랭크 결정전' 위치에 _ _ _ _ _ 5개의 진행바와 높이와 색상이
	   같은걸 만들고 개수 표시」). 자리는 1행(문장 띠 위 여백)의 **오른쪽** — 문장 띠의 오른쪽 끝과 같은 세로선(2열)이다(2026-10-19 지시: 「문장
	   개수 막대를 기존 위치에서 우측 정렬로 변경」 — 왼쪽은 신호등의 자리다). 높이는 아래 `margin-bottom`.
	   ⚠ 그전(2026-10-08 ~ 10-19)에는 **왼쪽 아래**였다 — 문장 띠의 왼쪽 끝과 같은 세로선, 칸의 바닥에 붙어 문장 띠 바로 위(2026-10-08 지시: 「아까
	     있던 그리드 영역 내에서 좌측하단정렬」 — 그 하루는 1행의 세로 가운데였다).
	   ⚠ 「타이핑 영역의 왼쪽 아래(6행)」로 옮긴 것은 지시를 잘못 읽은 것이었다(같은 날 — 「타이핑 영역아니라니까?」).
	   ⚠ 값은 새로 정하지 않는다(규칙 8): 색 높이 · 색은 진행 바(원본 `twsm.css` 의 `.tws-goal-bar` 0.4rem · `--gb` / `#goalSpdBar` `--gbs`),
	     칸 폭 · 사이는 티켓 칸(`RankViews` 의 원 버튼 1.8rem · `TICKET` 의 gap 0.6rem) — 지시가 그 둘을 맞추라고 했다(티켓이 이 줄의 반대쪽에
	     섰던 때다 · 같은 날 티켓은 정보 패널로 갔다).
	   ⚠ 껍데기는 버튼의 것(같은 날 지시: 「아이콘 버튼처럼 모서리를 살짝 둥글게하고 보더와 그림자를 넣어줘」) — 테두리 0.1rem 검정 · 그림자
	     `TWS_SHADOW`(`twsButton.ts`). 테두리는 색 띠 **바깥**이다(색 띠는 진행 바와 같은 0.4rem 으로 남는다) — 그래서 폭은 1.6rem + 테두리 = 1.8rem.
	     모서리만 버튼(0.3rem)보다 작다: 이 칸은 0.6rem 높이라 0.3rem 이면 양 끝이 반원인 알약이 된다(「살짝」이 아니다).
	   ⚠ 켜지는 것은 200ms 전환(모션 어휘 «전환») — 문장이 넘어갈 때 한 칸이 차오른다 */
	.tws-typing-area.tws-rank > .tws-rank-steps {
		grid-column: 2;
		grid-row: 1;
		justify-self: end;
		align-self: end;
		/* 바닥에서 **신호등 한 줄만큼 위**다(2026-10-19 지시: 「B로 하자」 — 칸 다섯을 올리고 그 자리에 신호등 · 같은 날 칸 다섯이 오른쪽으로 갈 때
		   「기존 위치에서」 — 높이는 그대로 뒀다). 신호등의 아래 여백 0.2rem + 불 1.3rem + 사이 0.7rem + 칸의 그림자 0.2rem = 2.4rem — 1행 43px 안에 칸 위로
		   13px 이 남는다. 이 아래는 비었다 — 판의 이름(결정전)은 2026-10-22 에 타이핑 3,2 의 오른쪽 아래로 갔다(위 `.tws-rank-name` — 그전에는 이 아래
		   오른쪽 끝이었다 · 칸의 그림자 끝과 이름 윗변 사이 5px).
		   ⚠ 그전(2026-10-08 ~ 10-18)에는 바닥에서 판의 이름 글자 높이의 1/3 이었다(지시: 「하단에 딱 붙이지 말고 '랭크 결정전'의 글자높이의 1/3 정도의
		   여백」 — 그때는 칸 다섯이 왼쪽 · 이름이 오른쪽으로 한 줄이었다) */
		margin-bottom: 2.4rem;
	}
	/* **신호등** — 문장 띠 바로 위 · 문장과 같은 왼쪽 선(2026-10-19 지시: 「B로 하자」 — 칸 다섯을 1rem 올리고 그 자리에 막대를 두는 안이었다가 같은 날
	   막대가 불 셋이 됐고, 칸 다섯은 그 높이 그대로 오른쪽으로 갔다). 불의 그림자 끝이 문장 띠의 윗변이다 — 불에서 문장 글자 윗선까지 9px 남짓(실측).
	   캐럿이 나오는 자리(문장 왼쪽 끝) 바로 위라, 왼쪽부터 초록 · 노랑 · 빨강인 불(`TypingArea`)이 오른쪽에서 왼쪽으로 옮겨 가다 거기서 끝난다 */
	.tws-typing-area.tws-rank > .tws-rank-lights {
		grid-column: 2;
		grid-row: 1;
		justify-self: start;
		align-self: end;
		/* 문장 띠보다 1rem 위 — **상대 화살표(▼)의 자리를 비운다**(2026-10-19 지시: 「상대를 화살표로 했을 때 겹치는데 … 문장과의 간격도 좀 띄우는게
		   낫나?」). 화살표는 내 문장 바로 위 6px 에 서고(`RivalMark` — 치는 동안만) 상대가 첫 몇 글자를 치는 동안 불 밑을 지나간다 — 0.2rem 이던 때는
		   누운 불의 막대 · 그림자에 붙어 보였다(실측: 막대 아래 끝 41px = 화살표 윗변 41px). 지금은 그림자 끝과 화살표 사이가 6px 이다. 상대 표시 설정과
		   상관없이 한 자리다(설정마다 다르면 바꿀 때 불이 뛴다) */
		margin-bottom: 1rem;
		/* 초록 원의 왼쪽 끝이 **문장 첫 글자의 왼쪽 끝**과 같은 선이다(2026-10-19 지시: 「초록원의 좌측 끝에 첫번째 글자와 동일한 선상에 있도록」) — 문장은
		   어느 표시 방식에서나 문장 띠 안에서 0.2rem 들어가 있다(`TypingArea` 의 «좌측 0.2rem», 2026-07-23). 그 값이 바뀌면 여기도 같이 */
		margin-left: 0.2rem;
	}
	/* 랭크전의 한줄 — 두줄치기의 **입력 줄** 자리에 선다(2026-10-07 지시: 「연습 모드와 다르게 랭크전에서 한줄을 두줄에서의 입력줄에 맞추자」).
	   두줄치기는 원문 줄(줄 박스 `--tws-line`) 아래 0.1rem 띄우고 입력 줄을 놓는다(`TypingArea` 의 classic 분기) — 같은 식이다.
	   랭크의 한줄은 늘 흐름 스트립이라(`stripOnly`) 이 값을 읽는 것은 그 줄 하나다. 연습은 변수가 없어 0(원문 줄 자리)이다.
	   ⚠ 폰은 안 건다 — 한줄뿐이라 맞출 «입력 줄»이 없다 */
	/* 랭크전 — **진행 막대를 걷고 타이핑 3,2 · 4,2 · 5,2 를 한 칸으로 합친다**(2026-10-08 지시: 「랭크전에서만 따로 하기 · 진행바를 없애고
	   타이핑 3,2 / 타이핑 4,2 / 타이핑 5,2 를 합쳐봐」 → 「칸만 비어있고 영역은 그대로 둔건가? 노란색 줄이 변경된게 없는데」 — 칸 번호는 «행,열»).
	   **격자의 행 자체를 넷으로 줄인다**: 1 맨 위 여백 · 2 문장 띠 · 3 합친 칸(NEXT 칸이 받는다) · 4 바닥. 처음엔 행 여섯을 둔 채 NEXT 칸만
	   세 행에 걸쳤는데 그건 격자 선이 그대로라 «합친» 것이 아니었다.
	   ⚠ 합친 칸의 크기는 셋의 합이다 — 막대(0.4rem) + 틈(`--tws-gap`) + NEXT(내용만큼, 랭크의 데스크톱에서는 비었다). 그래서 문장 띠와 바닥은
	     한 픽셀도 안 움직인다. 열과 1 · 2행은 위 «타이핑 영역의 행»(태블릿 · PC)과 같은 값이다.
	   ⚠ 막대는 지우지 않고 감춘다 — 카운트다운의 짙어지기(`.tws-rank-count ~ .tws-goal-bar`)와 연습의 막대가 같은 요소라서다.
	   ⚠ 행 번호를 쓰는 다른 것은 1 · 2행과 `1 / -1`(덮개 · 카운트다운 숫자)뿐이라 넷으로 줄어도 자리가 안 바뀐다. 연습은 그대로다. */
	/* ⚠️ 3 · 4행의 높이를 맞바꿨다(같은 날 지시: 「타이핑 3,2와 4,2의 높이를 서로 바꿔봐」) — 3행이 남는 자리(`1fr`, 지금 60px)를 받고
	   4행이 «막대 + 틈»(0.4rem + `--tws-gap`, 24px)이다. 합이 같아 문장 띠 · 타이핑 영역 높이는 그대로다 */
	.tws-typing-area.tws-rank {
		grid-template-rows: 4.3rem var(--tws-band) 1fr calc(0.4rem + var(--tws-gap));
	}
	.tws-typing-area.tws-rank > .tws-goal-bar {
		display: none;
	}
	/* ⚠️ 열은 연습과 같은 셋이다 — 문장 열(2열)을 좌우 반으로 갈라(「타이핑 3,2칸만 반으로 나누자」) 3,3 을 상대 칸으로 쓰던 것을 같은 날
	   걷었다(지시: 「기존 3,3칸을 없애고 3,2칸을 하나로 넓힌 다음」). 그동안은 열이 넷이라 다른 행의 것이 전부 두 반에 걸쳐(`2 / 4`) 있었다.
	   NEXT 칸과 상대 칸이 **같은 칸(타이핑 3,2)**에 겹친다 — 랭크에서 NEXT 칸은 늘 빈 글이다(문장 결과의 시계 `RankTicker` 만 그 안에서
	   돈다 — 그래서 감추지 않고 둔다). 상대 칸이 DOM 에서 뒤라 위에 그려진다 */
	.tws-typing-area.tws-rank > .tws-next-slot {
		grid-row: 3;
	}
	/* **상대 칸 — 타이핑 3,2**(2026-10-08 지시: 「우측상단에 상대의 닉네임, 티어, 평균속도 표시 · 좌측하단에 상대의 타이핑 모습 표시하고 글자 /
	   막대 진행으로 옵션에서 선택 · 글자가 넘칠때는 흐름으로 표시하고 막대일 때는 너비 100%로 비율로 진행」 → 같은 날 3,3 에서 문장 열 전부로).
	   내용은 `RankViews.RivalCell`. 칸 안의 배치는 아래 «상대 칸» 절이 폰과 한 벌로 정한다 — 여기는 자리 · 위아래 여백과 **벽**(진행이 차지할
	   수 있는 폭 — 오른쪽에서부터, 같은 날 지시: 「오른쪽 2/3이 벽인거지 거기에 닿으면 멈추고 좌에서 우로 흐르면서 타이핑」 · 「막대 표시는
	   1/2 사용」). 폰의 값은 위 «폰의 랭크» 가 따로 든다 */
	.tws-typing-area.tws-rank > .tws-rank-rival {
		grid-column: 2;
		grid-row: 3;
		padding: 0.4rem 0;
		--tws-rival-text-w: calc(100% * 2 / 3);
		--tws-rival-bar-w: 50%;
	}
	/* **한줄이면 문장을 따라 내려간다**(2026-10-19 지시: 「불과 상대(화살표) 문장과의 간격을 일정하게 두자. 두줄 모드는 괜찮아보이는데 한줄 모드는 서로의
	   간격이 너무 떨어짐」) — 랭크의 한줄은 문장(과 그 위의 상대 화살표)이 두줄의 입력 줄 자리, 곧 `--tws-strip-top` 만큼 아래에 서서 같은 자리의 불과
	   사이가 8px → 37px 이었다(실측). 불도 그만큼 내린다 — 두 모드 모두 불 → 화살표 8px · 화살표 → 글자 잉크 3.3px · 불 → 글자 17.3px 다(1280 실측).
	   한줄치기인지는 타이핑 영역의 상태 클래스 `tws-one-line`(`TypingArea`)이 말한다 */
	.tws-typing-area.tws-rank.tws-one-line > .tws-rank-lights {
		margin-bottom: calc(1rem - var(--tws-strip-top));
	}
	.tws-typing-area.tws-rank {
		--tws-strip-top: calc(var(--tws-line) + 0.1rem);
		/* 판의 이름(「랭크 결정전」)의 글자 크기. 넓은 화면은 1.7rem(맨 아래 1025px~). 칸 다섯의 아래 여백이 이 값에서 나오는 것은 이제 폰뿐이다
		   (위 «폰의 랭크» — 데스크톱 · 태블릿의 칸 다섯은 2026-10-19 부터 2.4rem 이다) */
		--tws-rank-name-size: 1.3rem;
	}
}
/* **태블릿의 랭크 — 3행이 데스크톱의 «2행 : 3행» 비율**(2026-10-08 지시 둘: 「테블릿에서 타이핑 4행 칸들의 높이를 연습 모드의 6행 칸들의
   높이만큼 올려보자」 → 「데스크탑에서 2행과 3행의 높이 비율로 테블릿의 3행의 높이를 적용하자. 그만큼 4행이 줄겠지」).
   데스크톱의 기본 글자 크기에서 2행(문장 띠)은 5.9rem, 3행(`1fr`)은 6rem 이다(타이핑 영역 18.6rem − 4.3 − 5.9 − 막대 0.4 − 틈 2, 실측 59 : 60px).
   태블릿의 3행을 **2행 × 6/5.9** 로 적고, 4행이 남는 자리(`1fr`)를 받는다 — 실측 820px: 43 · 59 · 60 · 66(바로 전의 «연습 6행만큼»은
   43.5 · 82.5 였다 — 그때 3행이 좁아 상대 칸의 위아래 여백을 걷었는데, 이제 데스크톱과 같은 높이라 여백도 데스크톱과 같다).
   ⚠ 비율은 문장 띠(`--tws-band`)에 곱한다 — 글자를 키워 띠가 자라면 3행도 같은 비율로 자라고 4행이 그만큼 준다 */
@media (min-width: 601px) and (max-width: 1024px) {
	.tws-typing-area.tws-rank {
		grid-template-rows: 4.3rem var(--tws-band) calc(var(--tws-band) * 6 / 5.9) 1fr;
	}
}
@media (min-width: 1025px) {
	.tws-typing-area.tws-rank {
		--tws-rank-name-size: 1.7rem;
	}
}

/* ── 랭크전 맨 위 줄의 모양 — 칸 다섯(몇 번째 문장) · 판의 이름(「랭크 결정전」) ─────────────────────────────────────────────
   자리는 «랭크전의 데스크톱 · 태블릿»(타이핑 1행 · 문장 열의 양 끝, 바닥 정렬)과 «폰의 랭크»(타이핑 1,2 — 맨 위 띠의 양 끝)가 정하고, 모양은
   여기 한 벌이다(2026-10-08 — 폰에도 칸 다섯이 서면서 모았다: 「1,2칸은 몇번째 나타내는게 있어야하고」).
   칸 다섯의 값은 새로 정하지 않았다(규칙 8): 색 높이 · 색은 진행 바(원본 `twsm.css` 의 `.tws-goal-bar` 0.4rem · `--gb` / `#goalSpdBar` `--gbs`),
   칸 폭 · 사이는 티켓 칸(`RankViews` 의 원 버튼 1.8rem · `TICKET` 의 gap 0.6rem), 껍데기는 버튼의 것(테두리 0.1rem 검정 · 그림자 `TWS_SHADOW` —
   테두리는 색 띠 바깥이라 폭이 1.6rem + 테두리 = 1.8rem · 모서리만 0.2rem: 0.6rem 높이에 0.3rem 이면 알약이 된다). 켜지는 것은 200ms 전환 */
.tws-typing-area.tws-rank > .tws-rank-name {
	z-index: 11;
	font-weight: bold;
	color: var(--npt);
	font-size: var(--tws-rank-name-size);
	/* 줄 높이를 글자 크기로 — 기본 줄 높이(1.5)면 글자 아래 반행간(4px)만큼 글자가 칸 다섯보다 떠 보인다(바닥은 같은데 글자가 위에 있다) */
	line-height: 1;
	animation: tws-wait-in 200ms ease both;
}
.tws-typing-area.tws-rank > .tws-rank-steps {
	z-index: 11;
	display: flex;
	gap: 0.6rem;
	animation: tws-wait-in 200ms ease both;
}
.tws-typing-area.tws-rank > .tws-rank-steps > span {
	box-sizing: content-box;
	width: 1.6rem;
	height: 0.4rem;
	border: 0.1rem solid #000;
	border-radius: 0.2rem;
	box-shadow: 0.1rem 0.2rem 0 0 rgba(0, 0, 0, 0.3);
	background: var(--gb);
	transition: background-color 200ms ease, border-color 200ms ease;
}
/* 결정전(상대 없는 판)의 켜짐 — «지금 문장까지». 진행 바의 색(`--gbs`)이다 */
.tws-typing-area.tws-rank > .tws-rank-steps > span.tws-on {
	background: var(--gbs);
}
/* 상대가 있는 판(2026-10-07 지시: 「칸 다섯 승패 색으로 칠하자」) — **이긴 문장은 «이김» 색(`--rank-win` 연두), 진 문장은 «짐» 색(`--wrong`)**:
   승급전 · 결과의 ✓ · ✕ 칸(`RankViews` 의 `MARK_WIN` · `MARK_LOSS`)과 같은 색이다(2026-10-13 지시: 「승 패 색상을 지정해놨었잖아. 그걸로 변경해줘」 —
   그전 엿새는 이긴 칸이 진행 바 색(`--gbs`)이라 판 표시의 ✓ 칸과 색이 달랐다). **지금 치는 문장은 바탕을 비워 두고(남은 칸과 같은 색) 테두리만
   강조색(`--timebtn`)이다**(같은 날 지시: 「현재 치고있는 색상을 강조색으로」 → 「치는 칸은 색을 채우는 게 아닌 보더의 색으로만 표시하자」 — 바탕을
   강조색으로 채웠더니 강조색이 빨간 팔레트 넷(한지 · 단청 · horizon · dawn)에서 진 칸과 같아 보였다(색 차 ΔE 11~19, 계산값). 그전에는 «아직 안 친
   글자»의 색 `--npt` 로 채웠다). 테두리 두께는 그대로라 칸 크기가 안 변한다. 넷 다 이미 있는 값이다(규칙 8) */
.tws-typing-area.tws-rank > .tws-rank-steps > span.tws-win {
	background: var(--rank-win);
}
.tws-typing-area.tws-rank > .tws-rank-steps > span.tws-loss {
	background: var(--wrong);
}
.tws-typing-area.tws-rank > .tws-rank-steps > span.tws-now {
	border-color: var(--timebtn);
}

/* ── 상대 칸(`RankViews.RivalCell` · 진행은 `RaceLine` — 데스크톱 · 태블릿 · 폰의 타이핑 3,2 가 같이 쓴다, 2026-10-08) ──────
   자리(격자 칸 · 바깥 여백)는 위 두 곳(«랭크전의 데스크톱 · 태블릿» · «폰의 랭크»)이 정하고, 칸 안의 배치와 진행의 모양은 여기 한 벌이다
   (두 곳에 적으면 한쪽만 고쳐진다 — 실제로 같은 규칙 열두 줄이 두 벌이었다).
   **두 줄이 칸의 바닥에 붙고, 상대 문장이 위 · 이름 · 티어 · 속도가 그 아래에 여백을 두고 선다**(2026-10-14 지시: 「닉네임하고 속도 표시부분 <>
   문장이 나오는 부분 위치 바꿔줘」 — 순서는 `RankViews.RivalCell` 이 그리는 차례다). 그전(2026-10-08 지시: 「모바일에서 상대 문장 하단 정렬」 · 「모바일
   테블릿 데스크탑 공통으로 문장의 상단의 여백을 두고 상대의 정보 배치」)에는 이름 줄이 상대 문장 바로 위였고, 그보다 전에는 데스크톱이 위 · 아래 끝으로
   갈랐다(`space-between` — 정보가 칸 꼭대기에 홀로 떠 있었다). 여백 0.6rem 은 칸 다섯 · 티켓 칸의 사이와 같은 값이다(규칙 8).
   이름 줄은 오른쪽 끝이고 **진행도 오른쪽 끝에 붙는다**(2026-10-08 지시: 「상대 문장 기본적으로 우측정렬」).
   **진행이 차지할 수 있는 폭에 벽이 있다** — 칸 오른쪽에서부터 `--tws-rival-text-w`(글자) · `--tws-rival-bar-w`(막대)까지(값은 위 두 곳:
   데스크톱 · 태블릿 2/3 · 1/2, 폰 4/5 · 2/3). 상대 문장이 그보다 짧으면 오른쪽 끝에 붙어 서고, 길면 벽에 닿아 멈추고 그 안에서 흐른다
   (「거기에 닿으면 멈추고 좌에서 우로 흐르면서 타이핑」). 막대는 벽까지 늘 꽉 찬다(비율의 자이므로).
   ⚠ 색은 NEXT 줄과 같은 `--npt`. **이름 줄은 상대 문장의 딸린 말이라 한 단 작고 가늘다** — 1rem · 400 으로 상대 문장(1.3rem · 400)의
     0.77 배다(2026-10-08 지시: 「상대의 정보가 상대가 치는 문장의 폰트 크기가 비슷해보여. 두께도 두꺼워. 알맞은 비율로」). 그전 1.2rem ·
     굵게는 13px 문장 옆에서 12px 굵은 글이라 크기가 같아 보였다. 1.1rem · 400 도 S22 에서 재 봤는데 여전히 비슷해 보였다. 1rem 은
     앱에 이미 있는 값이고(규칙 8), 속도 아이콘은 글자 크기를 따라 같이 줄어든다(칸 사이 0.8rem 은 그대로).
   ⚠ `min-width: 0` · `overflow: hidden` — 글이 칸보다 길면 «흐름»으로 옆으로 미는데(`RaceLine` 의 `TextPane`), 안 막으면 격자가 늘어난다 */
.tws-typing-area.tws-rank > .tws-rank-rival {
	display: grid;
	grid-template-rows: auto auto;
	align-content: end;
	row-gap: 0.6rem;
	min-width: 0;
	overflow: hidden;
	font-weight: bold;
	color: var(--npt);
	animation: tws-wait-in 200ms ease both;
	/* 진행의 벽이 칸 폭의 % 라 — 열이 내용에 끌려 자라면 그 % 가 칸 밖을 잰다 */
	grid-template-columns: minmax(0, 1fr);
}
.tws-rank-rival > .tws-rival-info {
	justify-self: end;
	display: flex;
	align-items: center;
	gap: 0.8rem;
	font-size: 1rem;
	font-weight: 400;
	white-space: nowrap;
}
/* 진행 칸이 곧 벽이다 — 오른쪽에 붙고 폭이 벽까지. 글자는 그 안에서 다시 오른쪽에 붙는다(아래 `.tws-rival-text`) */
.tws-rank-rival > .tws-rival-feed {
	justify-self: end;
	width: var(--tws-rival-text-w);
	min-width: 0;
	font-size: 1.3rem;
}
.tws-rank-rival > .tws-rival-feed.tws-rival-feed-bar {
	width: var(--tws-rival-bar-w);
}
/* 글자: 한 줄 · 넘치면 옆으로 민다(전환 160ms). 두께는 내가 치는 문장과 같은 400 이다(2026-10-08 지시: 「상대 폰트 두께 줄여」) —
   원본이 `body` 를 통째로 굵게 두어(`twsm.css:7`) 그대로 두면 700 을 물려받는다. 문장 쪽은 `TypingArea` 가 400 을 준다.
   흐름 자리를 재는 숨은 사본도 이 안이라 같은 두께로 잰다.
   ⚠ 폭은 **원문의 폭**이다(`RaceLine.TextPane` 이 숨은 사본에서 재어 준다 · 벽을 넘으면 `max-width` 가 벽에서 멈춘다) — 오른쪽에 붙은 줄이
     «상대가 친 글»의 폭을 따르면 조합 중인 낱자 · 틀린 글자로 폭이 매 타 바뀌어 **줄의 시작점까지 좌우로 떨린다.** 재기 전 첫 그림은
     `fit-content`(그 글의 폭)다 */
/* `position: relative` — 흐를 때 끝을 흐리는 덮개(`FlowFades` — 절대배치)가 이 상자에 붙는다 */
.tws-rank-rival .tws-rival-text {
	position: relative;
	display: block;
	width: fit-content;
	max-width: 100%;
	margin-left: auto;
	overflow: hidden;
	white-space: nowrap;
	font-weight: 400;
}
/* **상대 칸을 왼쪽에**(설정 › 랭크전 › 「상대 칸 자리」 — 2026-10-13 지시: 「우측하단에 있는 상대방을 좌측하단으로 옮길 수 있는 기능」). 칸은 그대로
   타이핑 3,2 이고 이름 줄 · 진행 칸(벽)이 왼쪽에 붙고 글자도 그 안에서 왼쪽에 붙는다 — 오른쪽 정렬 셋(위 `justify-self: end` 둘 · 바로 위
   `margin-left: auto`)을 뒤집는 것뿐이다. 막대는 원래 왼쪽부터 찬다 */
.tws-rank-rival.tws-rival-left > .tws-rival-info,
.tws-rank-rival.tws-rival-left > .tws-rival-feed {
	justify-self: start;
}
.tws-rank-rival.tws-rival-left .tws-rival-text {
	margin-left: 0;
	margin-right: auto;
}
.tws-rank-rival .tws-rival-line {
	display: inline-block;
	position: relative;
	transition: transform 160ms linear;
}
/* 막대: 진행 막대(원본 `.tws-goal-bar` 0.4rem · `--gb` / `#goalSpdBar` `--gbs`)와 같은 높이 · 색, 폭은 칸 전부. 비율은 `scaleX` */
.tws-rank-rival .tws-rival-bar {
	display: block;
	height: 0.4rem;
	background: var(--gb);
	overflow: hidden;
}
.tws-rank-rival .tws-rival-fill {
	display: block;
	height: 100%;
	background: var(--gbs);
	transform-origin: left;
	transition: transform 160ms linear;
}

/* ── 랭크전의 신호등 — 빨강 → 노랑 → 초록이 차례로 넘어지고, 초록이 넘어지면 캐럿(2026-10-19 지시: 「신호등처럼 빨간불 > 노란불 > 초록불 > 프롬포트가
   나타나면 시작하는걸로 하고 5초 3초 옵션은 없애자 · 숫자 없애자」 → 「원형 아이콘(빨강, 노랑, 초록): 랭크 티켓 크기로 · 빨강 켜짐(1초) > 빨강 꺼지면서
   노랑 켜짐(1초) > 노랑 꺼지면서 초록 켜짐(1초) > 초록 꺼지면서 캐럿 나오면 타이핑 가능」 → 「원형 아이콘의 색상이 다 켜져있고 아이콘이 넘어지는
   에니메이션으로 변경(3d처럼 보이도록 넘어졌을때의 단면이 보임) · 빨강이 넘어지면서 문장이 0으로 나오고 초록이 넘어질 때 100으로 자연스러운 페이드 효과」) ──
   `.tws-rank-count` 는 신호등을 세는 시계 위젯(`RankTicker`)이 낸 **표지**다 — 아무것도 안 그리고 서 있기만 한다. 표지가 서는 순간(문장마다) 형제인
   불 셋(`.tws-rank-lights` — 판 내내 서 있는 켜진 불)과 문장 띠 · 상대 칸에 애니메이션이 걸린다. 길이는 `TypingArea` 가 영역에 싣는 변수다
   (`--tws-lamp-red` · `--tws-lamp` — `RankViews.lampTimes`, 지금 1초씩 · 넘어지는 시간 `--tws-fall` 0.5초는 이 파일):
     0초에 셋이 일어선다(0.2초) · 빨강이 1초에 · 노랑이 2초에 · 초록이 3초에 바닥에 닿는다(각각 0.5초 전부터 넘어진다) · 3초에 표지가 내려가며
     (입력이 열린다) 캐럿이 선다. 문장은 빨강이 넘어지기 시작할 때(0.5초) 0 에서 초록이 닿을 때(3초) 1 까지 한 번에 짙어진다.
   ⚠ 같은 날 몇 시간은 **불이 켜지고 꺼졌다** — 꺼진 불 셋 중 제 차례(1초)의 하나만 켜지고, 문장은 노랑 1초 동안 짙어졌다(뒤에 «노랑이면 흐리게 ·
     초록이면 다» 두 계단).
   ⚠ 그전(2026-10-06 ~ 10-18)에는 표지가 **큰 숫자**였다(«5 · 4 · 3 · 2 · 1» — 설정에서 5초 · 3초, 10-16 부터 «5 · 4 · 3.9 … 0.1»). 문장은 숫자가
     서는 순간부터 «카운트다운 − 1초» 동안 ease-in 으로 짙어졌다. 표지의 바탕 모양(아래 `.tws-rank-count` — 큰 글자 · 한가운데)은 문장 결과의
     스코어가 그대로 쓴다.
   ⚠ 상태를 App 에 올리지 않는다(규칙 4 · 5) — 시계가 돌아도 문장 span 들이 다시 안 그려진다. 불을 켜는 것도 CSS 다.
   ⚠ 알림(「그만둘까요?」)으로 시계가 멈추면 표지가 `tws-hold` 를 단다 → 불 · 짙어지기도 멈춘다(풀리면 거기서 잇는다).
   ⚠ 애니메이션은 **표지가 서 있는 동안만** 걸린다 — 문장이 열리면(표지가 내려가면) 불은 넘어진 채, 문장은 원래 투명도(1)로 돌아간다. 다음 문장의
     신호등에서 표지가 다시 서면 처음부터 다시 돈다(`both` 라 첫 프레임부터 0 이다 — 새 문장이 한 번 비치지 않는다).
   ⚠ 형제 선택자(`~`)라 표지가 문장 띠 · 불 셋보다 **DOM 에서 앞**이어야 한다(`TypingArea` 가 그렇게 둔다).
   ⚠ 캐럿은 여기가 아니라 `TypingArea` 의 `caretOff` 가 끈다(포커스가 있어도 안 그린다) — 나온 뒤 2초는 안 깜빡인다(`CARET_FRESH_MS`).
   ⚠ 폰도 같다 — 불의 자리만 다르다(맨 위 띠 가운데 — «폰의 랭크»). */
/* 문장 결과의 스코어(`.tws-rank-count.tws-rank-verdict` — 2026-10-08 지시: 「'승리' '패'를 숫자처럼 보여줌」 → 「승리 대신에 1 : 0 > 1 : 1」)가
   이 모양을 쓴다 — 아래 «문장 결과». 신호등의 표지는 빈 요소라 그려지는 것이 없다 */
.tws-typing-area > .tws-rank-count {
	grid-column: 1 / -1;
	grid-row: 1 / -1;
	align-self: center;
	justify-self: center;
	z-index: 1;
	pointer-events: none;
	font-size: calc(var(--tws-font) * 2);
	font-weight: bold;
	line-height: 1;
	color: var(--font);
	/* 스코어가 **떠오른다**(2026-10-07 — 아래 «판을 여는 로더»). 서는 순간 한 번만 돈다 */
	animation: tws-wait-in 200ms ease both;
}
/* ⚠️ **상대 문장도 같이 짙어진다**(2026-10-08 지시: 「상대 문장이 5초부터 보임. 수정해줘」) — 그전에는 상대 칸이 신호등 처음부터 다음 문장을
   다 보여서 내 문장보다 먼저 읽혔다. 같은 애니메이션 · 같은 길이라 둘이 같은 투명도로 함께 떠오른다.
   ⚠️ **이름 · 티어 · 속도 줄도 같이 짙어진다**(2026-10-14 지시: 「닉네임/속도 표시가 5초부터 나오는데 문장이랑 같이 나오도록 바꿔줘」) — 그전에는
   «누구와 겨루는지는 처음부터 보인다»고 이 줄만 안 걸었다. 아래 «문장 결과»의 흐려짐 · «멈춤»도 같이 탄다(안 타면 문장 결과 동안 이 줄만 서 있다가
   다음 문장의 상대가 설 때 0 으로 툭 꺼졌다 떠오른다 — 상대는 문장마다 새로 선다) */
.tws-typing-area > .tws-rank-count ~ .tws-typing-view,
.tws-typing-area > .tws-rank-count ~ .tws-goal-bar,
.tws-typing-area > .tws-rank-count ~ .tws-rank-rival > .tws-rival-feed,
.tws-typing-area > .tws-rank-count ~ .tws-rank-rival > .tws-rival-info {
	/* **빨강이 넘어지기 시작할 때 0 → 초록이 바닥에 닿을 때 1**(2026-10-19 지시: 「빨강이 넘어지면서 문장이 0으로 나오고 초록이 넘어질 때 100으로
	   자연스러운 페이드 효과」) — 한 번에 이어지는 페이드다. 시작은 빨강의 끝 − 넘어지는 시간, 길이는 거기서 초록이 닿을 때(= 입력이 열리는 때)까지.
	   그 앞은 0 이다(`both`). 곡선은 **오래 흐리다가 끝에서 스르륵 오르는 4제곱 ease-in**(easeInQuart — 같은 날 지시: 「빨강이 넘어질 때 0.06 > 노랑이 넘어질 때
	   0.1 > 초록이 넘어지기 시작할 때 0.5 > 초록이 넘어지면 1 … 이런식으로 급격하게 해줘. 수치는 꼭 똑같이 안해도 됨」 → 「스르륵 나와야하는데 팍 팍 팍 나오게
	   하지 말라니까」): 빨강이 닿을 때 0 · 노랑 0.13 · 초록이 넘어지기 시작할 때 0.40 · 닿을 때 1(계산값). 2026-10-06 의 숫자 초읽기가 쓰던 바로 그 곡선이다
	   (「0에서 오래 있다가 1로 변경되는 곡선」).
	   ⛔ **점을 찍어 직선으로 잇지 않는다** — 그 네 점 그대로 잇자 빠르기가 2초 · 2.5초에서 갑자기 바뀌어 «팍 팍 팍» 나타났다(같은 날 지적). 하나로 이어지는
	   곡선이어야 한다.
	   ⚠ 같은 날 거쳐 간 곡선: S자(ease-in-out — 초록이 넘어지기 시작할 때 이미 0.92) → ease-in(0.06 · 0.43 · 0.69 · 1) → 네 점 직선(0.06 · 0.1 · 0.5 · 1).
	   ⚠ 같은 날 거쳐 간 둘(지시로 바꿨다): 노랑 1초 동안 0 → 1(ease — 노랑 한가운데서 이미 0.8 이라 «노랑에도 다 나타나 있다») · 노랑이면 흐리게(0.35)
	     초록이면 다(두 계단). 그전의 «0 근처에 오래 머물다 끝에서 오르는» 곡선(ease-in — 2026-10-06)은 5초 동안의 숫자 초읽기 것이다 */
	animation: tws-rank-reveal calc(var(--tws-lamp, 0ms) * 2 + var(--tws-fall, 500ms)) cubic-bezier(0.5, 0, 0.75, 0) calc(var(--tws-lamp-red, 0ms) - var(--tws-fall, 500ms)) both;
}
/* **불 셋** — 랭크 티켓 칸(정보 패널의 작은 칸 — `RankViews.TicketBadge` 의 `small`)과 같은 원이다(지시: 「랭크 티켓 크기로」): 1.3rem · 테두리
   0.1rem 검정 · 버튼 그림자. 사이는 0.9rem — 문장과의 사이를 띄운 만큼 좌우도 띄웠다(2026-10-19 지시: 「신호등 간격 좀 벌리고」 → 「문장과의 간격을 띄운만큼
   조화롭게 보이려면 아이콘끼리의 좌우 간격도 띄워야」 → 「좌우를 진짜 살짝만 줄여보자」 — 그 사이 몇 시간은 0.2rem · 0.6rem · 1rem 이었다 · 1.2rem 은 흩어져
   보였다). **늘 제 색으로 켜져 있다**
   (같은 날 지시: 「원형 아이콘의 색상이 다 켜져있고 아이콘이 넘어지는 에니메이션으로 변경(3d처럼 보이도록 넘어졌을때의 단면이 보임)」 — 그 몇 시간은
   꺼진 불(`--timebg`) 셋 중 제 차례의 하나만 켜졌다). 색은 신호등 색(`--lamp-*` — theme.css).
   자리는 «랭크전의 데스크톱 · 태블릿»(문장 띠 바로 위 왼쪽 — 칸 다섯은 그 위 오른쪽) · «폰의 랭크»(맨 위 띠 가운데)가 정한다. 칸 다섯과 같이 떠오른다
   (판의 맨 위 줄). 왼쪽부터 초록 · 노랑 · 빨강이다(2026-10-19 지시: 「요소 위치: 초록 노랑 빨강 순서」 — `TypingArea` 의 순서).
   **두께가 있는 원판**이다 — 불 하나 = 칸(`span` — 원근을 칸마다 따로 둬 셋이 같은 모양으로 넘어진다) + 원판(`i` — 앞면) + 옆면(`i::after` — 원판의 아래
   가장자리에서 뒤로 두께만큼). 원판은 **뒤쪽 아래 모서리**를 축으로 90° 뒤로 넘어진다 — 넘어지면 앞면은 위를 보고, 옆면이 앞을 봐 **단면**(지름 × 두께의
   막대)이 원래 바닥선 위에 선다. 판 동안(표지가 없을 때 — 치는 중 · 문장 결과)은 넘어진 채다 */
.tws-typing-area.tws-rank {
	/* 불 하나가 넘어지는 시간 — 모션 어휘 «정착» 500(CLAUDE.md 8). 불의 차례가 끝나는 순간(1 · 2 · 3초)에 바닥에 닿도록 그만큼 먼저 넘어지기 시작한다 */
	--tws-fall: 500ms;
	/* 원판의 두께(옆면의 높이) — 테두리를 넣어 0.5rem */
	--tws-lamp-depth: 0.5rem;
}
.tws-typing-area.tws-rank > .tws-rank-lights {
	z-index: 11;
	display: flex;
	gap: 0.9rem;
	pointer-events: none;
	animation: tws-wait-in 200ms ease both;
}
.tws-typing-area.tws-rank > .tws-rank-lights > span {
	display: block;
	width: 1.3rem;
	height: 1.3rem;
	/* 원근은 칸마다 — 눈높이는 **바닥선**이다(50% 100%). 그래야 서 있을 때 바닥의 옆면이 정확히 눈높이라 안 보이고(가운데에 두었더니 서 있는 원판 밑에
	   옆면이 가는 선으로 비쳤다 — 실측), 넘어지면 옆면만 앞을 본다 */
	perspective: 6rem;
	perspective-origin: 50% 100%;
}
.tws-typing-area.tws-rank > .tws-rank-lights > .tws-lamp-red {
	--tws-lamp-on: var(--lamp-red);
}
.tws-typing-area.tws-rank > .tws-rank-lights > .tws-lamp-yellow {
	--tws-lamp-on: var(--lamp-yellow);
}
.tws-typing-area.tws-rank > .tws-rank-lights > .tws-lamp-green {
	--tws-lamp-on: var(--lamp-green);
}
/* 원판(앞면) — 넘어진 채가 바탕이다(`rotateX(90deg)`) */
.tws-typing-area.tws-rank > .tws-rank-lights > span > i {
	position: relative;
	display: block;
	box-sizing: border-box;
	width: 100%;
	height: 100%;
	border: 0.1rem solid #000;
	border-radius: 50%;
	box-shadow: 0.1rem 0.2rem 0 0 rgba(0, 0, 0, 0.3);
	background-color: var(--tws-lamp-on);
	transform-style: preserve-3d;
	transform-origin: 50% 100% calc(var(--tws-lamp-depth) * -1);
	transform: rotateX(90deg);
	/* 누운 앞면은 눈보다 위라 뒷면이 보인다 — 안 그린다(옆면 위에 가는 선으로 남았다 — 실측) */
	backface-visibility: hidden;
}
/* 옆면(단면) — 원판의 아래 가장자리에서 뒤로 두께만큼 누운 판. 서 있을 때는 눈높이라 안 보이고, 원판이 넘어지면 앞을 본다. 앞면보다 조금 어둡다
   (같은 색 위에 검정 22% 를 한 겹 — 결과는 불투명이다 · `color-mix` 는 안 쓴다 — theme.css 의 그 까닭) */
.tws-typing-area.tws-rank > .tws-rank-lights > span > i::after {
	content: "";
	position: absolute;
	left: -0.1rem;
	top: calc(100% + 0.1rem);
	box-sizing: border-box;
	width: calc(100% + 0.2rem);
	height: var(--tws-lamp-depth);
	border: 0.1rem solid #000;
	border-radius: 0.2rem;
	box-shadow: 0.1rem 0.2rem 0 0 rgba(0, 0, 0, 0.3);
	background-color: var(--tws-lamp-on);
	background-image: linear-gradient(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.22));
	transform-origin: 50% 0;
	transform: rotateX(-90deg);
}
/* **표지가 서 있는 동안** — 0.2초 만에 일어서고(앞 문장에서 넘어진 채였다 · 첫 문장은 떠오르면서), 제 차례가 끝나는 순간 바닥에 닿게 넘어진다
   (빨강 1초 · 노랑 2초 · 초록 3초 — 초록이 닿는 순간 입력이 열린다). 넘어지는 것은 떨어지듯 점점 빨라진다(ease-in). 넘어지는 애니메이션은 채움이
   `forwards` 라 기다리는 동안은 일어선 채(앞 애니메이션의 끝)다. 문장 결과의 스코어는 같은 요소지만 `tws-rank-verdict` 라 안 건다 */
.tws-typing-area > .tws-rank-count:not(.tws-rank-verdict) ~ .tws-rank-lights > .tws-lamp-red > i {
	animation:
		tws-lamp-rise 200ms ease both,
		tws-lamp-fall var(--tws-fall, 500ms) cubic-bezier(0.55, 0, 1, 0.45) calc(var(--tws-lamp-red, 0ms) - var(--tws-fall, 500ms)) forwards;
}
.tws-typing-area > .tws-rank-count:not(.tws-rank-verdict) ~ .tws-rank-lights > .tws-lamp-yellow > i {
	animation:
		tws-lamp-rise 200ms ease both,
		tws-lamp-fall var(--tws-fall, 500ms) cubic-bezier(0.55, 0, 1, 0.45) calc(var(--tws-lamp-red, 0ms) + var(--tws-lamp, 0ms) - var(--tws-fall, 500ms)) forwards;
}
.tws-typing-area > .tws-rank-count:not(.tws-rank-verdict) ~ .tws-rank-lights > .tws-lamp-green > i {
	animation:
		tws-lamp-rise 200ms ease both,
		tws-lamp-fall var(--tws-fall, 500ms) cubic-bezier(0.55, 0, 1, 0.45) calc(var(--tws-lamp-red, 0ms) + var(--tws-lamp, 0ms) * 2 - var(--tws-fall, 500ms)) forwards;
}
/* 멈춤 — 위 셋보다 특이도가 높아야 한다(`animation` 묶음 속성이 멈춤을 «돌아감»으로 되돌린다) */
.tws-typing-area > .tws-rank-count.tws-hold:not(.tws-rank-verdict) ~ .tws-rank-lights > span > i {
	animation-play-state: paused;
}
@keyframes tws-lamp-rise {
	from {
		transform: rotateX(90deg);
	}
	to {
		transform: rotateX(0deg);
	}
}
@keyframes tws-lamp-fall {
	from {
		transform: rotateX(0deg);
	}
	to {
		transform: rotateX(90deg);
	}
}
/* 움직임을 줄이면 돌지 않는다 — 일어서기는 곧바로, 넘어지기는 바닥에 닿을 때(1 · 2 · 3초)에 한 번에. 시간을 알리는 일은 그대로다 */
@media (prefers-reduced-motion: reduce) {
	.tws-typing-area > .tws-rank-count:not(.tws-rank-verdict) ~ .tws-rank-lights > span > i {
		animation-timing-function: step-start, step-end;
	}
}
/* ── 문장 결과(2026-10-08 지시: 「1문장에서 치고 완료하고 문장이 지워지지 않고 > 페이드 되면서 '승리' '패'를 숫자처럼 보여줌 > 1초 보여주고
   1초 동안 페이드 되면서 5 시작 > 반복」) ──
   ① 끝낸 문장(원문 · 친 글)은 안 지워지고 **흐려진다** — 문장 결과의 승패(`.tws-rank-count.tws-rank-verdict` — 숫자와 같은 요소 · 같은
     자리)가 서는 순간 형제인 문장 띠에 애니메이션 하나가 걸린다. 길이는 카운트다운의 짙어지기와 같은 변수(`--tws-reveal-ms` — 문장
     결과에서는 승패가 서 있는 시간, 지금 1초 — `RankViews.VERDICT_FADE_MS`).
   ② 스코어는 서 있다가(`--tws-verdict-hold`) **다 흐려진 뒤에** 카운트다운이 선다(`--tws-verdict-fade` — 둘을 합치면 문장 결과의 길이다.
     값의 주인은 `RankViews` 의 `VERDICT_FADE_MS` 하나고 그 요소의 인라인 변수로 온다). 같은 날 «「1 : 0」 과 «5» 가 1초 겹치기»였다가
     «5» 가 가운데 «:» 위에 얹혀 «250» 으로 읽혀서 걷었다(지시: 「1로 하자」 — 겹치지 않기).
   ⚠ 이 규칙들은 아래 «멈춤»(`tws-hold`)보다 **앞**이어야 한다 — 특이도가 같고 `animation` 묶음 속성이 멈춤을 «돌아감»으로 되돌린다. */
/* 상대 문장도 내 문장과 함께 흐려진다 — 안 그러면 위 «짙어지기»가 문장 결과에서도 걸려 0 부터 다시 떠오른다(같은 방아쇠다) */
.tws-typing-area > .tws-rank-count.tws-rank-verdict ~ .tws-typing-view,
.tws-typing-area > .tws-rank-count.tws-rank-verdict ~ .tws-goal-bar,
.tws-typing-area > .tws-rank-count.tws-rank-verdict ~ .tws-rank-rival > .tws-rival-feed,
.tws-typing-area > .tws-rank-count.tws-rank-verdict ~ .tws-rank-rival > .tws-rival-info {
	animation: tws-rank-leave var(--tws-reveal-ms, 0ms) ease forwards;
}
.tws-typing-area > .tws-rank-count.tws-rank-verdict {
	animation:
		tws-wait-in 200ms ease both,
		tws-rank-leave var(--tws-verdict-fade, 0ms) ease var(--tws-verdict-hold, 0ms) forwards;
}
.tws-typing-area > .tws-rank-count.tws-rank-verdict.tws-hold {
	animation-play-state: paused;
}
.tws-typing-area > .tws-rank-count.tws-hold ~ .tws-typing-view,
.tws-typing-area > .tws-rank-count.tws-hold ~ .tws-goal-bar,
.tws-typing-area > .tws-rank-count.tws-hold ~ .tws-rank-rival > .tws-rival-feed,
.tws-typing-area > .tws-rank-count.tws-hold ~ .tws-rank-rival > .tws-rival-info {
	animation-play-state: paused;
}
/* 빨강이 넘어지기 시작할 때 0 → 초록이 바닥에 닿을 때 1 — 모양은 위 규칙의 곡선 하나다(«빨강이 넘어지기 시작할 때 0 → …» — 점을 찍지 않는다) */
@keyframes tws-rank-reveal {
	from { opacity: 0; }
	to { opacity: 1; }
}

/* ── 랭크전 — 판을 여는 로더 (2026-10-07 지시: 「시작 버튼을 누르면 로딩 표시 후 페이드 되면서 5,4,3,2,1 자연스럽게 넘어가기」) ──
   「시작」을 누르면 버튼 자리에 로더 · 문구가 **떠오르고**(200ms) 판이 열리면 **걷힌 뒤에**(200ms) 큰 숫자가 떠오른다(바로 위
   `.tws-rank-count` 의 200ms) — 한 자리에서 겹치지 않고 차례로 바뀐다. 맨 위 줄(칸 다섯 · 판의 이름)도 숫자와 같이 떠오른다.
   ⚠ 걷히는 시간은 `useRank` 의 `OPEN_FADE_MS` 와 짝이다 — 그 시간이 지나야 카운트다운이 선다. 로더가 적어도 얼마나 도는지도
     거기다(`OPEN_MIN_MS` — 답이 빨라도 로더가 번쩍이고 사라지지 않게).
   ⚠ 덮개(`.tws-wait-line`)는 대기 화면부터 이미 서 있어 제 애니메이션이 다시 안 돈다 — 그래서 로더를 한 겹 감싸 그 겹이 뜨고
     걷힌다. 감싼 겹의 간격은 덮개의 그것(0.6rem)과 같다 — 감싸도 로더와 문구 사이가 안 바뀐다. */
.tws-wait-line > .tws-rank-opening {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.6rem;
	animation: tws-wait-in 200ms ease both;
}
.tws-wait-line > .tws-rank-opening.tws-rank-leave {
	animation: tws-rank-leave 200ms ease forwards;
}
/* 강등 보호의 방패 — 속 색이 바뀌면 **흐려지며** 바뀐다(2026-10-08 지시: 「보호판이면 방패의 색상이 페이드 되는 에니메이션」 — 결과의 점수판에서
   쓴 보호 판의 방패가 꺼진다). 모션 어휘 «정착» 500(CLAUDE.md 8). 대기 화면은 값이 안 바뀌어 아무 일도 없다 */
.tws-rank-guards path {
	transition: fill 500ms ease;
}
@media (prefers-reduced-motion: reduce) {
	.tws-rank-guards path {
		transition: none;
	}
}
/* 결과 — 「확인 (3 · 2 · 1)」이 저절로 눌리면 걷힌다(2026-10-07 — `RankViews.ResultHold` · `RESULT_FADE_MS` 와 짝). 감싼 겹의 줄 간격은 덮개의
   그것(0.6rem)과 같아 감싸도 틀 · 버튼이 대기 화면과 같은 자리에 선다 */
.tws-wait-line > .tws-rank-result {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.6rem;
}
.tws-wait-line > .tws-rank-result.tws-rank-leave {
	animation: tws-rank-leave 200ms ease forwards;
}
@keyframes tws-rank-leave {
	to { opacity: 0; }
}
/* 랭크 대기 화면의 내용이 바뀔 때 — 새 내용이 떠오른다(2026-10-07 지시: 「글자들이 팍팍 바뀌는데 부드럽게 페이드되도록」).
   받는 중 → 채워짐 · 결정전 · 승급전 · 강등 보호 · 일반 사이 · 못 받음, 그리고 같은 상태에서 값이 바뀔 때(`RankViews` 의 `WaitFrame` ·
   `fadeKey`). 그 층이 **새로 그려지는 순간에만** 돈다(첫 그림의 덮개 자체는 위 `.tws-wait-line` 의 늦은 떠오름이 맡는다). 「시작」 버튼은
   자리를 지키고 색만 원래의 전이(200ms)로 바뀐다. 모션 어휘 «전환» 200(CLAUDE.md 8). */
.tws-wait-line .tws-rank-fade,
.tws-typing-area > .tws-corner > .tws-rank-fade {
	animation: tws-wait-in 200ms ease both;
}
/* 결과 화면에서 이번 판의 승급전 칸 하나가 떠오를 때 — 속도가 세어 오르는 동안 눈에 들게 «정착» 500 으로 천천히(2026-10-08 지시: 「이번판 승이나
   패가 되는 해당 아이콘만 패이드되서 나타나게」 — `RankViews` 의 `ResultSeries`) */
.tws-wait-line .tws-rank-settle {
	animation: tws-wait-in 500ms ease both;
}
/* 결과 막대 꼬리표 안의 두 얼굴 — **지금 점수 ↔ 바뀐 만큼**(2026-10-16 지시: 「떠 오르게 하지말고 숫자 상자 안에 상승시에는 이긴색 숫자 감소시에는 진색
   숫자와 현재 점수를 번갈아가면서 보여주자」 → 「1.5초로하고 페이드까지 넣어서 2초로 하자」 — `RankViews.KpBar` 의 `alt`). 하나가 1.5초 다 보이고 0.5초에
   바뀐다 — 물러나는 쪽이 먼저 흐려지고(앞 반) 들어오는 쪽이 그다음 떠오른다(뒤 반 · `--tws-kp-delay`). 길이 · 지연은 그 칸이 CSS 변수로 준다
   (`RankViews.RESULT_ALT_FADE_MS`). ⚠ 그전 몇 시간은 «+18» 이 꼬리표 위로 떠올라 사라졌다(`tws-rank-delta` — 지웠다) */
.tws-kp-face {
	transition: opacity var(--tws-kp-fade, 250ms) ease var(--tws-kp-delay, 0ms);
}
/* 「결과까지」 칸 경험치 막대의 숫자 상자 — 숫자를 상자 위아래 **가운데**에(2026-10-27 지시: 「상자안에서 상하여백도 안맞음. 그러니 더 커보이는듯」 —
   `RankViews.TicketShelf` 의 `EXP_TAG`). 줄 높이 1 에서 앱 글꼴(Apple SD Gothic Neo)은 위 · 아래 높이가 글자 크기의 0.94 · 0.35배라 기준선이 줄 아래쪽으로
   내려가, 숫자가 상자 위에 붙었다(실측 0.85rem: 위 틈 0.5px · 아래 틈 1.6px). 글 상자를 **대문자 높이 ~ 기준선**으로 자르고(`text-box`) 위아래 같은 여백을
   준다 — 숫자 자체가 가운데라 글꼴이 바뀌어도(윈도의 Roboto) 가운데다. 상자 높이는 그 전과 거의 같다(11.3px). 좌우 여백도 같은 0.2em 이다(같은 날
   지시: 「좌우 여백도 줄이자. 너무 넓음」 — 그 전 0.35em).
   ⚠ 못 자르는 브라우저는 위아래 여백만 그 전 그대로다(숫자가 조금 위). 여백을 인라인에 두지 않는다 — 인라인이 이겨 `@supports` 가 못 바꾼다 */
.tws-exp-tag {
	padding: 0.05em 0.2em;
}
@supports (text-box: trim-both cap alphabetic) {
	.tws-exp-tag {
		padding: 0.2em;
	}
	.tws-exp-tag > span {
		text-box: trim-both cap alphabetic;
	}
}
/* 랭크 전광판의 티어 · 점수 · 승패가 바뀔 때 — 결과를 보고 「확인」으로 대기에 오면 판 뒤 값으로 떠오른다(2026-10-08 지시: 「페이드 되서 변경되는걸로」 —
   `InfoPanels` 의 `tws-board-fade`). 대기 화면의 바뀜과 같은 «전환» 200 */
.tws-rank-board .tws-board-fade,
.tws-mobile-records > p.tws-board-fade {
	animation: tws-wait-in 200ms ease both;
}
/* 결과의 「확인」 뒤 걷힘(2026-10-14 지시: 「판이 끝나고 확인 버튼을 눌렀을 때 전광판, 대기실이 부드럽게 변경되지 않고 깜빡이는 현상 수정」) —
   전광판의 티어 · 점수 · 승패(폰은 한 줄)와 모서리의 리스트 · 그래프 버튼이 결과(`.tws-wait-line > .tws-rank-result.tws-rank-leave`)와 **같은 200ms 에**
   흐려지고(`useRankPlay` 의 `RESULT_FADE_MS`), 다 걷힌 뒤 대기 화면 · 새 값이 떠오른다. 그전에는 옛것이 한 프레임에 사라지고 새것이 0 에서 떠올라 그
   빈 프레임이 깜빡임으로 보였다. ⚠ 전광판은 **새로 그려질 조각만** 걷힌다(티어 이름이 바뀌면 이름과 KP — `InfoPanels.boardPartKeys`). 숫자만 바뀌는 KP · 승패는
   걷히지 않고 센다(`CountText`) */
.tws-rank-board .tws-board-fade.tws-board-leave,
.tws-mobile-records > p.tws-board-fade.tws-board-leave,
.tws-typing-area > .tws-corner > .tws-rank-leave {
	animation: tws-rank-leave 200ms ease forwards;
}
/* 랭크 덮개의 버튼 글자(`RankViews.LabelSwap`) — 바뀌면 새로 그려지며 떠오르고 「확인」 뒤에는 먼저 걷힌다(2026-10-14 지시: 「… 바 막대 … 고정하고
   숫자와 막대 내부, 버튼의 글자만 변경」). 결과의 「확인」과 대기의 「시작」이 **같은 버튼 요소**라 상자 · 손 올림 색은 그대로고 글자만 바뀐다 */
.tws-wait-line .tws-btn-label {
	animation: tws-wait-in 200ms ease both;
}
.tws-wait-line .tws-btn-label.tws-rank-leave {
	animation: tws-rank-leave 200ms ease forwards;
}
/* 「랭크전 도전」 테두리를 강조색 빛이 꼬리를 달고 돈다(2026-10-08 지시: 「버튼 보더가 강조색이 빛 꼬리를 달고 도는걸로하자. 숨쉬듯하는 에니메이션 빼고」 —
   `RecordsPage` 의 `RankChallenge`, 묶음이 차서 열린 결과 모달 · 티켓이 있을 때). 버튼을 감싼 칸의 `::before` 가 버튼 테두리 자리만 남기고(마스크) 그 위에
   원뿔 그러데이션(투명 → 강조색)을 돌린다 — 머리가 가장 밝고 뒤로 흐려지는 것이 꼬리다. 각도는 등록한 사용자 속성(`@property`)이라 부드럽게 돈다.
   ⚠ 누름을 안 받는다(아래 버튼이 그대로 눌린다). 버튼의 바탕 · 글자색은 안 건드린다.
   ⚠️ 소비자가 둘이다 — 위의 그 버튼과 **기록 칸의 랭크 입장 버튼**(연습 ↔ 랭크 토글 — `RankViews.ModeToggle`): 「결과까지」 칸의 티켓 자리에 손을
   올린 동안 돈다(2026-10-13 지시: 「기록 전광판의 랭크전 입장 버튼에 보더 빛 따라 움직이는 것만」) */
@property --tws-orbit-a {
	syntax: "<angle>";
	inherits: false;
	initial-value: 0deg;
}
@keyframes tws-orbit {
	to {
		--tws-orbit-a: 360deg;
	}
}
.tws-orbit::before {
	content: "";
	position: absolute;
	inset: -0.1rem;
	padding: 0.2rem;
	border-radius: 0.4rem;
	background: conic-gradient(from var(--tws-orbit-a), transparent 0turn 0.45turn, var(--timebtn) 1turn);
	-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
	-webkit-mask-composite: xor;
	mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
	filter: drop-shadow(0 0 0.3rem var(--timebtn));
	pointer-events: none;
	z-index: 1;
	animation: tws-orbit 2400ms linear infinite;
}
@media (prefers-reduced-motion: reduce) {
	.tws-orbit::before {
		display: none;
	}
	/* 움직임을 줄이면 꼬리표의 두 얼굴은 흐려지지 않고 그냥 바뀐다(번갈기는 그대로 — 값을 알리는 일이다) */
	.tws-kp-face {
		transition: none;
	}
	.tws-typing-area > .tws-rank-count.tws-rank-verdict,
	.tws-typing-area > .tws-rank-count.tws-rank-verdict ~ .tws-typing-view,
	.tws-typing-area > .tws-rank-count.tws-rank-verdict ~ .tws-goal-bar,
	.tws-typing-area > .tws-rank-count.tws-rank-verdict ~ .tws-rank-rival > .tws-rival-feed,
	.tws-typing-area > .tws-rank-count.tws-rank-verdict ~ .tws-rank-rival > .tws-rival-info,
	.tws-typing-area > .tws-rank-count,
	.tws-typing-area.tws-rank > .tws-rank-name,
	.tws-typing-area.tws-rank > .tws-rank-steps,
	.tws-typing-area.tws-rank > .tws-rank-lights,
	.tws-wait-line > .tws-rank-opening,
	.tws-wait-line > .tws-rank-opening.tws-rank-leave,
	.tws-wait-line > .tws-rank-result.tws-rank-leave,
	.tws-wait-line .tws-rank-fade,
	.tws-typing-area > .tws-corner > .tws-rank-fade,
	.tws-typing-area > .tws-cmd-corner,
	.tws-wait-line .tws-rank-settle,
	.tws-rank-board .tws-board-fade,
	.tws-rank-board .tws-board-fade.tws-board-leave,
	.tws-mobile-records > p.tws-board-fade,
	.tws-mobile-records > p.tws-board-fade.tws-board-leave,
	.tws-typing-area > .tws-corner > .tws-rank-leave,
	.tws-wait-line .tws-btn-label,
	.tws-wait-line .tws-btn-label.tws-rank-leave {
		animation: none;
	}
	.tws-typing-area.tws-rank > .tws-rank-steps > span,
	.tws-rank-rival .tws-rival-line,
	.tws-rank-rival .tws-rival-fill {
		transition: none;
	}
	.tws-typing-area.tws-rank > .tws-rank-rival {
		animation: none;
	}
}

/* ── 타이핑 영역의 오른쪽 위 모서리 — 도전권 표시(2026-10-06 지시: 「도전권 3/5 를 연습 모드에도 랭크 모드에도 타이핑 영역의
   우측 상단에 · 아이콘으로 · x3」) ──
   1행(문장 위의 여백)의 **오른쪽 끝** — 문장 띠의 오른쪽 끝과 같은 세로선(2열)이다. 연습 · 랭크 어느 격자에서나 1행은 비어 있다.
   글자는 NEXT 줄의 글(`#npt` — 1.5rem · 굵게 · `--npt`)과 같은 값이다(새로 정하지 않는다).
   ⚠ 랭크 대기 · 결과의 덮개(`.tws-wait-line`, z 10)보다 **위**다 — 덮개가 타이핑 영역 전체를 덮는다.
   ⚠ 모양은 `RankViews.TicketBadge`(칸 다섯 — 가진 수만큼 켜짐, 같은 날 지시로 «아이콘 + ×3» 에서 바뀌었고 손을 올렸을 때의 이름표는 뺐다).
   ⚠️ **지금은 랭크 결과의 리스트 · 그래프 단추가 쓴다**(2026-10-08 — App 의 `resultCorner`). 입장권은 여기를 떠났다:
     데스크톱 · 태블릿은 정보 패널의 「결과까지」 칸 아래로(2026-10-07 지시: 「결과까지 밑으로 티켓 아이콘을 옮겨보자」 — `InfoPanels` 의 `tickets` ·
     그 뒤 이 자리(1행 오른쪽 끝)는 랭크 결정전의 판 이름이 받았다가 2026-10-19 부터 칸 다섯이 쓴다), 폰은 **오른쪽 아래 모서리**로
     (2026-10-28 지시: 「모바일에서 입장권을 우측하단으로 옮기자」 — 아래 `.tws-corner-low`. 그전 3주는 랭크의 덮개가 선 동안 여기 섰다 — 2026-10-07
     지시: 「폰 화면에서 티켓의 위치를 우측 상단으로」). 그전에는 버튼 아래 「도전권 3/5」 줄 → 원 칸이었다. */
.tws-typing-area > .tws-corner {
	grid-column: 2;
	grid-row: 1;
	justify-self: end;
	align-self: center;
	z-index: 11;
	font-size: 1.5rem;
	font-weight: bold;
	color: var(--npt);
}
@media (min-width: 601px) and (max-width: 1024px) {
	.tws-typing-area > .tws-corner {
		font-size: 1.3rem;
	}
}
/* ── 오른쪽 아래 모서리 — 폰 랭크 대기실의 입장권(2026-10-28 지시: 「모바일에서 입장권을 우측하단으로 옮기자」 · `TypingArea` 의 `cornerLow`) ──
   위 모서리를 거울로 비춘 자리다 — 오른쪽 끝은 같은 세로선(문장 열의 끝 · 위 «#» 버튼의 오른쪽 끝)이고, 위의 것이 맨 위 띠(3rem)의 세로 가운데에
   서듯 영역 아래 끝의 3rem 의 세로 가운데에 선다. 감싸개가 `.tws-corner` 도 달아 덮개 위(z 11) · 설 때 떠오르기 · 명령 창이 뜨면 걷히기를 그대로 받는다.
   ⚠ 행이 아니라 영역 전체(1/-1)의 아래 끝에 붙인다 — 폰 랭크 격자의 맨 아래 행은 0.8rem 바닥이라 칸 다섯(1.8rem)이 안 든다.
   ⚠ 서는 때는 위 모서리에 있던 때 그대로다(App — 폰 · 랭크 모드 · 판의 문장이 안 선 동안 · 결과의 단추가 서기 전까지) */
.tws-typing-area > .tws-corner.tws-corner-low {
	grid-row: 1 / -1;
	align-self: end;
	display: flex;
	align-items: center;
	height: 3rem;
}

/* ── 명령 버튼 «#»과 명령 창 — 랭크 모드의 대기실 (2026-10-28) ─────────────────────────────────────────────────────
   지시: 「대기실 영역 좌측 상단에 # 네모 아이콘 버튼을 만들어서 키보드, 마우스, 터치로 할 수 있게 하자」 · 「명령줄은 대기실 영역 # 버튼이 확장되면서
   두번째 스크린샷 영역을 차지하는 미니 모달 … 메인 섹션 중 하나의 느낌으로 위에 떠있듯이」.
   버튼은 타이핑 영역의 **오른쪽 위 — 1행 · 문장 띠의 오른쪽 끝**(2026-10-28 지시: 「버튼을 우측 끝으로 옮기자」 — 처음 몇 시간은 왼쪽 위 모서리였다)이고
   덮개(z 10) 위다. **대기실에만 선다**(같은 날 지시: 「결과에서는 명령 버튼이 없게 하자」 — App 의 `cmdHere`) — 그래서 같은 자리의 위 모서리
   (`.tws-corner` — 결과의 리스트 · 그래프 단추)와 함께 서는 일이 없다. ⚠ 그날 몇 시간은 둘(과 폰의 입장권)이 한 줄에 서고 모서리가 버튼 폭 + 0.6rem
   왼쪽으로 비켰다(`tws-has-cmd` — 지웠다). 폰의 입장권은 같은 날 오른쪽 아래 모서리로 갔다(위 `.tws-corner-low`).
   창의 자리(`.tws-mini-slot`)는 타이핑 영역 **전체**(덮개와 같은 1/-1)이고 그 위(z 12)다 — 창(`MiniModal`)은 그 자리를 채우되 `--mini-inset` 만큼
   물러서 떠 있는 판으로 보인다. 닫힌 동안에는 자리가 누름을 안 받는다(`pointer-events: none` — 판만 받는다). 글자 크기는 판이 정한다(페이지 본문과
   같은 1rem · 폰 1.15rem).
   ⚠️ **연습 · 필사의 «#»(«문장 처음부터» 오른쪽 — `.tws-next-reset`)도 이 자리의 같은 창이다**(2026-11-02 지시: 「연습 모드에서 '#'을 입력하면 지금처럼
   '#'을 클릭하면 랭크 모드의 명령입력기처럼」) — 폭 규칙도 같다(PC 는 아래의 오른쪽 몫 · 가운데에 점수 영역은 없고 치던 문장이 왼쪽에 비친다). */
.tws-typing-area > .tws-cmd-corner {
	grid-column: 2;
	grid-row: 1;
	justify-self: end;
	align-self: center;
	z-index: 11;
	line-height: 0;
	/* 대기실이 설 때 같이 떠오른다 — 결과의 「확인」 뒤 리스트 · 그래프 단추가 걷히고 대기 화면이 떠오를 때 이 버튼만 툭 나타나지 않게(결과에서 빠지며
	   생긴 일 · 위 모서리의 `.tws-rank-fade` 와 같은 «전환» 200). 서 있는 동안은 다시 안 돈다(새로 설 때만) */
	animation: tws-wait-in 200ms ease both;
}
.tws-typing-area > .tws-mini-slot {
	grid-column: 1 / -1;
	grid-row: 1 / -1;
	position: relative;
	z-index: 12;
	pointer-events: none;
	--mini-inset: 0.6rem;
}
/* **PC — 창은 칸 가운데의 점수 영역(막대 · 속도 줄 · 「시작」) 옆에서 멈춘다**(2026-10-28 지시: 「3번째가 제일 나아보임」 — 폭 1/2 · 1/3 ·
   점수 영역 앞까지 셋을 띄워 고른 것). 점수 영역은 늘 칸 가운데라 그 둘레 20rem 을 비워 두고 한쪽 몫만 쓴다 — 가장 넓은 점수 영역(네 자리 속도 셋)이
   드는 폭이다(실측: 칸 895px · 대기 151px → 창 335.5px · 점수 영역과 30px — 창이 결과에도 서던 그날 몇 시간은 결과의 티어 이름 148~152px 도 그 안이었다).
   ⚠ **오른쪽 몫이다** — 버튼이 오른쪽 끝으로 가면서(같은 날 지시: 「버튼을 우측 끝으로 옮기자」) 창도 그 버튼에서 자라 나오게 오른쪽에 붙였다
   (`--mini-left` — `MiniModal` 이 자라고 줄어드는 길을 `left` 하나로 그리므로 오른쪽 붙임도 `left` 로 준다).
   ⛔ JS 로 재지 않는다(규칙 12) — 점수 영역은 서고 나서도 자란다(결과 화면에서 처음 1~2초 동안 가운데가 자라, 열 때 재 맞춘 폭이 다 선 뒤에 11px
   겹쳤다 — 실측).
   태블릿 · 폰은 칸이 좁아(577 · 380px) 그 몫이 200px 아래라 칸을 꽉 채운다(변수를 안 주면 `MiniModal` 이 꽉 채운다) */
@media (min-width: 1025px) {
	.tws-typing-area > .tws-mini-slot {
		--mini-width: calc(50% - 10rem - 2 * var(--mini-inset));
		--mini-left: calc(100% - var(--mini-inset) - var(--mini-width));
	}
}
/* 창이 떠 있는 동안 문장 칸이 밖을 안 잘라낸다 — 숨은 입력칸(`#ipt`)이 창의 캐럿 자리로 옮겨 가는데(2026-10-28 — 맥의 «한» 표시가 그 입력칸 자리에
   뜬다 · `TypingArea` 의 `inputAt`) 그 자리가 문장 칸 밖(위 · 왼쪽)이라 잘리면 입력기가 자리를 못 받는다. 그동안 문장 칸은 덮개 아래 `opacity: 0` 이라
   새로 보이는 것이 없다(대기실 · 결과 실측). */
.tws-typing-area.tws-cmd-open > .tws-typing-view {
	overflow: visible;
}
/* 창이 떠 있는 동안 모서리(폰의 입장권 — 오른쪽 아래 `.tws-corner-low`)를 걷는다 — 창이 그 자리를 덮는다(창은 칸 끝에서 `--mini-inset` 안쪽까지
   선다). 안 걷으면 물러서 떠 있는 창의 테두리 밖으로 그 끝이 비쳤다(폰 실측 — 그때는 위 모서리였다). 창이 자라고 줄어드는 시간(0.2초)에 맞춰 흐려졌다
   돌아온다. 위 모서리(결과의 단추)는 창과 함께 서는 일이 없다(창은 대기실에만 — 위 «명령 버튼» 절) */
.tws-typing-area > .tws-corner {
	transition: opacity 200ms ease;
}
.tws-typing-area.tws-cmd-open > .tws-corner {
	opacity: 0;
	pointer-events: none;
}

/* ── 필사의 화면 — 타이핑 영역 자리에 바로 선다 (2026-10-30 지시: 「버튼을 눌렀을 때 타이핑 영역 자체에 넣자. 모달 말고」) ───────────────────
   목록 넷 · 리스트 · 리스트 만들기 · 글 쓰기 · 붙여 넣기(`TypingArea` 의 `panel` · 안은 `TranscribePage.TranscribeWindow` · 여닫기 `app/useTrMenu`).
   **자리는 타이핑 영역의 문장 상자다** — 문장 · 진행 표시줄 · NEXT 가 서는 2열 × 2행 ~ 끝에서 둘째 행(같은 날 지시: 「타이핑 영역도 여백이 있는데 그 여백은
   무시하고 한다고?」 — 그 몇 시간은 영역 전체(1 / -1)를 덮어 머리 줄이 위 여백 띠에 서고 걸음이 아래 끝까지 내려갔다). 그래서 영역의 위 · 양옆 · 아래 여백이
   치는 화면과 같고, 머리 줄의 글자 · 카드의 바깥 선이 문장 글자 · 진행 표시줄의 양 끝과 같은 세로선이다(실측 — CLAUDE.md 의 필사 줄).
   문장 구역은 `tws-waiting` 으로 비운다(자리는 그대로라 카드가 안 움직인다). 덮개(`.tws-wait-line`)는 이 화면이 서는 동안 안 그린다(`TypingArea`).
   ⛔ **테두리 · 그림자 · 바탕을 안 준다** — 영역의 바탕 그대로라 «떠 있는 창»이 아니라 «타이핑 영역의 다른 화면»으로 읽힌다(그 하루의 작은 창
   (`MiniModal`)은 아이콘에서 자라 나온 테두리 상자였다). 꾸밈은 안의 카드 · 칸 · 가름선이 맡는다. 열릴 때만 «전환» 200 으로 떠오른다.
   ⚠ `contain: size` — 이 안의 긴 목록이 타이핑 영역의 행 크기에 끼지 않게(크기는 칸이 정하고 넘치는 것은 안에서 굴러간다).
   ⚠ 바깥을 안 잘라낸다 — 안의 굴러가는 칸이 카드의 그림자 · 켜짐 테두리 자리만큼 양옆으로 내민다(`TranscribePage.BLEED`).
   글자 기준은 페이지 본문과 같다(1rem · 폰 1.15rem — `CardPage` 의 `PAGE_TEXT` · `PAGE_TEXT_PHONE`). 아이콘 넷은 이 영역이 아니라 기록 칸(전광판)의
   오른쪽 아래다(`TranscribeTools` — 위 `.tws-info-toggle`). */
.tws-typing-area > .tws-tr-panel {
	grid-column: 2;
	grid-row: 2 / -2;
	z-index: 10;
	position: relative;
	display: flex;
	flex-direction: column;
	min-height: 0;
	min-width: 0;
	contain: size;
	font-size: 1rem;
	color: var(--font);
	animation: tws-wait-in 200ms ease both;
}
@media (max-width: 600px) {
	.tws-typing-area > .tws-tr-panel {
		font-size: 1.15rem;
	}
}
@media (prefers-reduced-motion: reduce) {
	.tws-typing-area > .tws-tr-panel {
		animation: none;
	}
}

/* ⚠ **카드를 키우는 것만으로는 문장 상자가 안 커진다.** 카드의 행이 전부 고정값이라
   (PC `5rem 10rem` + 고정 타이핑 행) 늘어난 344px이 트랙에 배분되지 않고 카드 바닥에 그대로
   고였다 — 카드만 두 배가 되고 문장 상자는 61px 그대로, NEXT가 바닥에서 381px 뜬 채로
   멈춰 있었다(2026-08-28 실측). 높이가 흐르려면 **카드 → 타이핑 영역 → 문장 행**
   세 단계가 전부 `1fr`이어야 한다. 한 곳만 빠져도 거기서 막힌다. */
/* 폰은 **이미 자라 있다** — 카드가 보이는 화면 높이(`--app-h`)고 문장 행이 `1fr`이라
   키보드를 내리면 그만큼 문장 상자가 커진다. 그래서 여기에 폰 규칙이 없다:
   줄바꿈 모드가 폰에서 바로 성립하는 이유이자, 이 기능을 폰에 넣을 수 있었던 이유다. */

/* ── 자리도 '페이지 자리'여야 한다 ─────────────────────────────────────────
   크기를 페이지에 맞췄으면 **세로 위치도 페이지가 서던 자리**여야 한다
   (2026-08-28 지시: "흐름일 때 설정의 상단 y = 줄바꿈일 때 메인의 y").

   둘이 어긋나 있던 이유: 카드는 담는 상자(`main`)의 여백 안에서 `margin: auto`로 놓이는데
   원본 여백이 **위 4.44rem / 아래 18rem으로 비대칭**이다. 카드가 작을 때는 auto가 그 비대칭을
   흡수해 티가 안 나지만, 줄바꿈 카드처럼 커지면 auto가 0으로 떨어져 **위 여백에 그대로 붙는다**
   (창 900에서 y44). 반면 페이지는 `CardPage.expand()`가 **창 기준**으로 놓는다(y106).

   그래서 줄바꿈일 때만 여백을 대칭으로 되돌린다. 값은 `expand()`의 `MIN_TOP`과 같은 4.44rem —
   그러면 `margin: auto`가 남는 높이를 반씩 나눠 `max(MIN_TOP, (창 − 높이) / 2)`와 같은 답을
   낸다. 카드가 창을 넘길 만큼 커지면 auto가 0이 되어 4.44rem에서 멈추는 것까지 같다.

   ── 왜 2.4rem이 아니라 4.44rem인가 (2026-08-31 지시) ──────────────────────
   **창을 줄였을 때 윗변이 멈추는 자리를 셋이 같이 쓰게 하려는 것이다.** 흐름 카드는 위
   `main`의 패딩 4.44rem에서 멈추는데 줄바꿈 카드와 덮개만 2.4rem이었다 — 그래서 창높이
   700 이하에서 흐름 44 / 줄바꿈 24로 갈라졌고, 흐름 상태로 옵션을 열면 카드는 44에 있는데
   덮개가 24로 올라붙어 화면이 20.4px 튀었다(2026-08-31 실측).

   **위아래를 같은 값으로 두는 것이 핵심이다.** 위만 4.44로 올리고 아래를 2.4로 두면
   `margin: auto`가 그 비대칭을 절반씩 나눠 가져 넉넉한 창에서도 카드가 10.2px 내려간다 —
   창높이 900에서 106 → 116.2. 대칭이면 `max(4.44rem, (창 − 높이) / 2)`가 되어 **넉넉한
   창은 픽셀 단위로 그대로다**(900에서 106, 800에서 56. 실측 일치).

   아래쪽 24는 위 높이 식의 `6.84rem`(= 4.44 + 2.4)이 지킨다. 카드가 창을 넘겨 auto가 0이
   되면 윗변 44.4 + 높이(창 − 68.4) = 창 − 24로 아랫변이 정확히 24에서 끝난다. 아래까지
   4.44rem으로 벌리지 않는 이유는 줄바꿈 모드의 존재 이유가 **글을 한 줄이라도 더 보는 것**
   이라서다 — 위를 맞추는 값과 아래를 벌리는 값은 목적이 다르므로 같이 움직이지 않는다.
   ⚠ 폰은 제외한다 — 거기서는 카드가 보이는 화면 전체라 여백을 둘 자리가 없다.
   ⚠ 고정(`.tws-fixed`)이면 여기도 걸리지 않는다 — 카드가 평소 크기이므로 세로 위치도
   평소 자리(`margin: auto`가 비대칭 여백을 흡수한 그 자리)여야 흐름과 같은 y에 선다.   */
@media (min-width: 601px) {
	main.tws-wrap:not(.tws-fixed) {
		padding-top: 4.44rem;
		padding-bottom: 4.44rem;
	}
}

/* ══ 좌우 드래그로 문장 넘기기 — 이웃 상자 (폰) ═════════════════════════════
   손가락을 따라 타이핑 영역이 통째로 움직이고, 그 옆에 **이웃 문장 상자**가 같이 온다.
   놓으면 넘어가거나(상자 폭의 1/4 또는 튕김) 제자리로 돌아온다.

   ── 왜 그리드의 '같은 칸'인가 ─────────────────────────────────────────────
   이웃은 `.tws-typing-area`와 **같은 그리드 영역**(2행)에 놓인다. 그래서 폭·높이를
   우리가 한 글자도 적지 않아도 타이핑 영역과 같아진다 — 자리를 JS로 재서 맞추면
   그 순간 배치의 소유자가 둘이 된다(CLAUDE.md 12).
   쉬는 자리는 화면 밖이고 **그 자리도 CSS가 갖는다**: 끌기가 끝나면 컴포넌트가
   인라인 transform을 지우기만 하면 여기로 돌아온다.

   ── 왜 글자마다 span이 없나 ───────────────────────────────────────────────
   이웃 문장은 **언제나 '안 친 상태'**다 — 정타·오타·커서가 하나도 없다. 그래서 고스트
   색 한 덩어리로 그려도 보이는 것이 같고, 문단 장문에서 span 3,823개를 한 벌 더 만드는
   값(≈51ms)을 내지 않는다. 부호 하이라이트도 넣지 않는다: **착지한 뒤 진짜 컴포넌트가
   그린다**(2026-08-27 지시).

   상자 언어(테두리·라운드·그림자·`--boxbg`)는 타이핑 영역의 것 그대로다.
   ⚠ `pointer-events: none` — 이웃은 보이기만 한다. 손가락은 늘 진짜 상자를 쥔다.       */
@media (max-width: 600px) {
	/* 가로 손짓은 우리가 쓴다고 브라우저에 알린다 — 안 알리면 가장자리에서 시작한 끌기가
	   '뒤로 가기'로 새어 나간다. 세로는 그대로 넘겨준다(`pan-y`).
	   `touchmove`에서 `preventDefault`를 부르는 방법도 있지만 React는 이 이벤트를
	   패시브로 등록해서 먹히지 않는다 — CSS가 유일하게 확실한 수단이다. */
	/* **두 축 다 우리가 쓴다** — 가로는 문장 넘기기, 세로는 문장 훑어보기(2026-08-31 지시).
	   `pan-y`였을 때 브라우저에 넘겨 두던 세로 팬은 **하는 일이 없었다**: 폰에서 문서·body의
	   스크롤 여지가 0이라(실측) 손가락을 세로로 끌어도 아무 일도 안 일어났다.
	   ⚠ `pan-y`도 핀치 줌은 이미 막고 있었으므로(`pinch-zoom`이 빠진 값) `none`으로 바꿔도
	   새로 잃는 것이 없다. React가 `touchmove`를 passive로 붙여 `preventDefault`가 안 먹으니
	   막는 일은 CSS가 해야 한다. */
	.tws-typing-area { touch-action: none; }

	.tws-drag-ghost {
		grid-area: 2 / 1 / 3 / 2;
		position: relative;
		/* 타이핑 영역이 갖는 그 위쪽 간격(`twsm.css:194`) — 없으면 이웃만 2px 높이 뜬다 */
		margin: 0.2rem 0 0 0;
		border: solid 1px #0c0c0c;
		border-radius: 0.5rem;
		background: var(--boxbg);
		box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.7);
		overflow: hidden;
		pointer-events: none;
	}
	/* 쉬는 자리 = 화면 밖 한 칸. 0.2rem은 셀 사이 검은 틈과 같은 값이라, 끌면 두 상자
	   사이가 카드 안의 다른 틈과 같은 폭으로 벌어진다.
	   ⚠ **그런데 그것만으로는 화면 밖으로 다 나가지 않는다.** 카드 좌우 여백이 0.5rem이라
	   한 칸 + 0.2rem을 밀어도 테두리·그림자가 3px씩 가장자리에 걸린다(실기기 지적).
	   자리를 더 밀어내면 여백 값이 바뀔 때 같이 고쳐야 하므로, **평소엔 아예 안 보이게**
	   두고 끌기 시작할 때 컴포넌트가 켠다. 칠할 것도 없어진다. */
	.tws-drag-ghost { visibility: hidden; }
	.tws-drag-prev { transform: translateX(calc(-100% - 0.2rem)); }
	.tws-drag-next { transform: translateX(calc(100% + 0.2rem)); }

	/* 글자의 자리는 **진짜 문장과 같아야 한다** — 착지하는 순간 진짜 컴포넌트로 바뀌므로,
	   어긋나 있으면 그 프레임에 글자가 톡 튄다.
	   1.1rem = 상단 밴드 0.8rem **+** 문장 상자의 −0.2rem 마진 보정 + 테두리 1px.
	   부호를 조심할 것 —
	   음수 마진은 마진 박스를 위로 올리는 게 아니라 **테두리 박스를 아래로 내린다**
	   (stretch가 마진 박스를 행에 맞추므로). 처음에 0.6rem으로 뺐다가 글자가 5px 떠서
	   실측(Range로 첫 글자를 재서)으로 잡았다.
	   1.5rem = 좌우 트랙 1.3rem + 스트립의 left 0.2rem.
	   1.5rem/2.8rem = `TypingArea`가 폰에서 쓰는 글자 크기·줄 박스. */
	.tws-drag-ghost-text {
		position: absolute;
		top: 1.1rem;
		left: 1.5rem;
		/* ⚠ **글자가 잘리는 선이 진짜와 같아야 한다.** 진짜는 문장 뷰포트(좌우 트랙 1.3rem
		   안쪽)에서 잘리는데, 여기를 `right: 0`으로 두면 상자 테두리에서 잘려 **13px만큼
		   글자가 더 보인다** — 그 조각은 페이드 바깥이라 진하게 나오고, 착지하는 순간
		   사라진다("'가까'까지는 페이드인데 '운'이 진하게 보이다 없어짐", 2026-08-27 지적).
		   `overflow: hidden`이 있어야 이 상자가 실제로 자른다 — `white-space: pre`는
		   글자를 상자 밖으로 그대로 흘려보낸다. */
		right: 1.3rem;
		overflow: hidden;
		margin: 0;
		font-size: var(--tws-font);
		font-weight: 400;
		line-height: var(--tws-line);
		letter-spacing: normal;
		white-space: pre;
		color: color-mix(in srgb, var(--font) 50%, transparent);
	}

	/* 줄바꿈 모드에서는 이웃도 접힌다 — **착지하는 순간 진짜 컴포넌트로 바뀌므로**
	   한 줄짜리 이웃을 끌고 오면 놓는 순간 글이 여러 줄로 펼쳐지며 톡 튄다.
	   `pre-wrap`이다(`normal` 아님): 앞뒤 공백을 진짜와 똑같이 살려야 첫 글자의
	   가로 위치가 어긋나지 않는다. 아래 여백은 상자가 `overflow: hidden`으로 자른다. */
	.tws-wrap .tws-drag-ghost-text {
		white-space: pre-wrap;
		word-break: keep-all;
		bottom: 0;
	}
	/* 진행도 바도 같은 자리에 빈 채로 둔다 — 없으면 끌리는 동안 상자 하나만 바닥이 비어
	   두 상자가 다른 물건으로 보인다. 자리는 타이핑 영역의 3행과 같다(바 0.4 + 아래 0.8). */
	.tws-drag-ghost-bar {
		position: absolute;
		left: 1.3rem;
		right: 1.3rem;
		bottom: 0.8rem;
		height: 0.4rem;
		background: var(--gb);
	}

	/* ⚠ `prefers-reduced-motion`을 여기서 처리하지 않는다. 정착 시간은 컴포넌트가
	   **인라인 transition**으로 쓰는 값이라, CSS로 덮으려면 `!important`가 필요하다 —
	   이 파일에 그게 생기면 "JS 어딘가에 인라인이 남았다"는 신호다(맨 위 주석).
	   그래서 시간을 정하는 쪽(`TypingArea`의 SNAP_MS)이 직접 물어본다. */
}

/* ══ 문장 끝 페이드가 덮이는 동작 ═══════════════════════════════════════════
   긴 글의 잘린 끝을 흐리는 그라디언트는 **문장이 바뀔 때 스르르 덮인다.**
   그냥 켜 두면 새 문장이 앉는 순간 이미 다 켜져 있어 "탁" 나타난다(2026-08-27 지적).

   **500ms = 앱의 '정착' 시간**(CLAUDE.md 8: 100 점등 / 200 전환 / 500 정착). 이건 손짓에
   대한 답(200)이 아니라 **자리를 잡은 뒤 가라앉는 표현**이라 500이 맞다.

   ⚠ 키프레임에 `to`를 적지 않는다. 끝값이 **그 요소의 지금 값**(인라인 opacity)이 되므로,
   켜져야 할 쪽만 0 → 1로 덮이고 꺼져 있어야 할 쪽은 0 → 0이라 아무 일도 안 일어난다.
   방향마다 키프레임을 만들지 않아도 된다.
   ⚠ 다시 재생시키는 수단은 `animation-name`이 아니라 **React의 `key`**다 — 문장이 바뀌면
   요소가 다시 마운트되므로 애니메이션도 처음부터 돈다. */
@keyframes tws-fade-in {
	from {
		opacity: 0;
	}
}
.tws-fade-in {
	animation: tws-fade-in 500ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
	.tws-fade-in {
		animation: none;
	}
}

/* ══ 페이지 본문의 스크롤바 — 스크롤 중에만 보인다 ═══════════════════════════
   `CardPage`가 `.tws-page-scroll`을 달고, 손이 움직이는 동안만 `is-scrolling`을 더한다.

   왜 CSS인가: 스크롤바는 `::-webkit-scrollbar` 의사요소로만 손댈 수 있어 인라인 스타일로는
   못 그린다. CardPage는 원칙적으로 인라인만 쓰지만(원본 CSS가 맨 태그로도 걸려서), 이건
   클래스 훅 하나만 넘기고 그리기는 여기서 한다.

   ⚠ **트랙 폭은 항상 잡아 둔다.** 나타날 때 폭이 생기면 그 순간 본문이 6px 밀려 글자가
   흔들린다. 켜고 끄는 건 색뿐이다.

   색은 `--npt`(입력 안 한 글자용 흐린 색)를 쓴다 — 팔레트 29종을 그대로 따라가고,
   본문보다 확실히 뒤로 물러나는 자리의 색이라 스크롤바의 무게와 맞는다. */
.tws-page-scroll {
	scrollbar-width: thin;
	scrollbar-color: transparent transparent;
}
.tws-page-scroll.is-scrolling {
	scrollbar-color: var(--npt) transparent;
}
.tws-page-scroll::-webkit-scrollbar {
	width: 0.6rem;
}
.tws-page-scroll::-webkit-scrollbar-track {
	background: transparent;
}
.tws-page-scroll::-webkit-scrollbar-thumb {
	background: transparent;
	border-radius: 0.3rem;
	/* 200 = 앱의 '토글·전환' 시간. 나타날 땐 즉시, 사라질 땐 이 시간으로 물러난다 */
	transition: background 200ms ease;
}
.tws-page-scroll.is-scrolling::-webkit-scrollbar-thumb {
	background: var(--npt);
	transition: none;
}


/* ── 문장별 성적 칸(#twsInfo2)이 스크롤한다 ─────────────────────────────────
   예전에는 마지막 넷만 그렸다(`slice(-4)`) — 칸이 딱 네 줄 높이라 그 이상 그릴 데가
   없었기 때문인데, 그러면 다섯 번째 문장부터 앞 기록이 **화면에서 사라졌다**.
   이제 전부 그리고 칸이 스크롤한다(2026-09-02 지시).

   ⚠ **스크롤바는 감춘다**(같은 날 지시). 19.7rem 칸에 0.6rem 트랙이 생기면 성적 줄이
   그만큼 좁아지고, 원본에는 없던 세로선이 카드 한가운데에 서서 눈에 걸린다.
   휠·드래그·터치는 그대로 된다.

   ⚠ 높이·자리는 위 그리드가 이미 정했다 — 여기서는 넘치는 것을 어떻게 다룰지만 정한다. */
.tws-info-2 {
	overflow-y: auto;
	/* 끝까지 내렸을 때 뒤 페이지가 따라 움직이지 않게 */
	overscroll-behavior: contain;
	scrollbar-width: none;
	-ms-overflow-style: none;
}
.tws-info-2::-webkit-scrollbar {
	display: none;
}

/* '전체보기' — ⚠️ **늘 보인다**(2026-09-29 지시: 「호버해야 보였던 버튼들을 항상 보이게」).
   예전(2026-09-02~)에는 칸에 손을 올렸을 때만 떴는데, 그러면 손가락(호버가 없다)과
   처음 온 사람에게는 **버튼이 있다는 것 자체**가 안 보였다. 그래서 여기 남은 규칙이 없다 —
   자리 잡기는 처음부터 컴포넌트의 인라인 스타일이 갖는다(이 파일이 안 실린 채 그려지면
   아이콘이 왼쪽 위로 떨어지던 2026-09-02 의 그 까닭). */


/* 성적 줄의 간격을 좁힌다 — 오른쪽 위 '전체보기'와 «ACC: 100»이 맞닿았다(2026-09-02 지적).
   원본(twstb.css)은 칸마다 오른쪽 여백 1rem을 준다. 그때는 줄이 넷뿐이고 오른쪽에 아무것도
   없었지만, 이제 그 자리에 버튼이 선다. **칸 너비는 그대로 두고 사이만 줄인다** —
   숫자 폭(2.5rem)을 건드리면 세 자리 수에서 자리가 흔들린다.

   ⚠ 원본 CSS는 고치지 않는다. 이 파일이 뒤에 로드돼 같은 특이도로 덮는다. */
.tws-info-2>p>span {
	margin: 0 0.1rem 0 0;
}
.tws-info-2>p>span:nth-child(2),
.tws-info-2>p>span:nth-child(3) {
	/* 0.5 → 0.33rem: **지금 간격의 2/3**(2026-09-04 지시). 줄이 넷으로 고정되면서
	   세로가 짧아지자 가로 간격만 남아 헐거워 보였다. 칸 너비(2.5rem)는 그대로 둔다 —
	   건드리면 세 자리 수에서 자리가 흔들린다. */
	margin: 0 0.33rem 0 0;
}


/* 목록 줄에 손을 올리면 그 줄이 켜진다 — 어느 줄을 누르는지 보여야 한다(2026-09-02 지시).
   메인의 #twsInfo2와 전체보기 모달이 **같은 클래스**를 쓴다: 같은 뜻의 조작이 화면마다
   다르게 보이면 같은 앱으로 안 읽힌다.

   색은 **켜짐 표현(--timebtn)을 그대로 빌린다** — 원본에는 색이 바뀌는 hover가 없어서
   새 어휘를 늘리지 않는다는 규칙 그대로다(CLAUDE.md 8). 투명도를 섞어 글자를 덮지 않게
   하고, 팔레트 29종을 그대로 따라간다.

   앞 줄은 color-mix를 모르는 브라우저를 위한 자리다(그때는 켜짐 배경색이 그대로 깔린다). */
.tws-record-row {
	border-radius: 0.2rem;
	/* 200 = 앱의 '토글·전환' 시간. 불투명도는 아래 «정보 칸을 받는 동안» 의 것 — 한 줄에 같이 적는다
	   (따로 `transition` 을 적으면 특이도가 높은 쪽이 다른 하나를 지운다). */
	transition: background-color 200ms ease, opacity 200ms ease;
}

/* ── 정보 칸을 받는 동안 — 비우고, 한 묶음으로 페이드 (2026-10-01 지시) ─────────────────
   로그인하면 «결과까지» 숫자와 기록 줄 넷의 근거가 서버다. 이 조합을 이번에 처음 묻는 동안(켤 때 · 로그인 · 처음 가는
   언어/갈래) 둘을 **비우고**(자리는 그대로) 답이 오면 **같은 200ms 에 같이** 떠오른다 — 문장 구역(`tws-waiting`)과 같은
   방식이고 두 묶음은 따로 뜬다(치는 데 필요한 문장이 기록을 기다리지 않는다).
   ⚠ 전이는 클래스가 바뀔 때만 돈다 — 비로그인 · 한 번 본 조합 · 탭 복귀의 새로 묻기에는 안 붙는다.
   ⚠ 못 받아 오면(서버가 멈추면 목록 요청의 8초 시한 — `api/runs` 의 `LIST_DEADLINE_MS`) 비운 채로 두고 숫자 자리와 기록 줄
     칸에 새로고침 아이콘이 선다(`InfoPanels` 의 `infoState: "failed"`) — 이 기기 값으로 덮지 않는다. */
.tws-batch-count,
.tws-mobile-records > p {
	transition: opacity 200ms ease;
}
.tws-info-wait > p,
.tws-info-wait .tws-batch-count {
	opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
	.tws-record-row {
		transition: background-color 200ms ease;
	}
	.tws-batch-count,
	.tws-mobile-records > p {
		transition: none;
	}
}
.tws-record-row:hover {
	background-color: var(--timebg);
	background-color: color-mix(in srgb, var(--timebtn) 22%, transparent);
}


/* ── 랭크 모달의 누르는 줄 — 순위표의 사람 · 지난 판(2026-10-12) ─────────────────────
   누르면 그 칸 안에 작은 페이지(그 사람의 기록 · 판 하나)가 선다. 손을 올린 줄은 **기록 모달의 목록 줄과 같은 연한 칠**이다
   (`RecordsPage.LIST_CURSOR` — 본문색 10% · 켜짐색이 아니다: 훑는 것은 고르는 일이 아니라 지나가는 일이다). 키보드로 닿은 줄도 같다.
   ⚠ 내 줄의 칠(`RankPage.MINE_ROW` — 인라인)이 이것보다 이긴다 — 내 줄은 손을 올려도 내 줄 색 그대로다. */
.tws-rank-pick {
	cursor: pointer;
	transition: background-color 200ms ease;
}
.tws-rank-pick:hover,
.tws-rank-pick:focus-visible {
	background-color: color-mix(in srgb, var(--font) 10%, transparent);
	outline: none;
}

/* ── 지난 판의 재생 — 나 · 상대의 문장을 한 줄 흐름으로(2026-10-12 지시: 「리플레이에서는 상대와 나의 영역에서 텍스트를 한 줄로 표현하고 흐름 모드를
   적용」) ─────────────────────────────────────────────────────────────────────────────────────────────
   그리는 것은 대결 줄의 한 줄 흐름(`RaceLine.TextPane` — 같은 클래스 · 같은 흐림 `FlowFades`)이고 **자리만 다르다**: 대결 줄은 상대 칸 오른쪽에
   붙지만(`.tws-rank-rival .tws-rival-text` 의 `margin-left: auto`) 여기는 문장 상자의 **왼쪽에서 시작**한다. 치는 자리가 `FLOW_ANCHOR` 를
   넘으면 왼쪽으로 민다(전환 160ms — 대결 줄과 같은 값). 흐림은 `--boxbg` 라 문장 상자 바탕과 같다.
   ⚠ `overflow: hidden` · `max-width: 100%` — 안 막으면 긴 문장이 상자를 민다. */
.tws-replay-line .tws-rival-text {
	position: relative;
	display: block;
	width: fit-content;
	max-width: 100%;
	overflow: hidden;
	white-space: nowrap;
	font-weight: 400;
}
.tws-replay-line .tws-rival-line {
	display: inline-block;
	position: relative;
	transition: transform 160ms linear;
}

/* ── «편의성» 칸의 드롭다운(2026-10-20 지시: 「토글을 켜면 우측에 드롭다운이 나타나서 그 안에 부호, 숫자 건너뛰기 항목」 — `InfoPanels` 의 `#twsInfo9`)
   ─────────────────────────────────────────────────────────────────────────────────────────────
   칸 **오른쪽**에 펴진다 — 데스크톱은 그 자리가 점판(`.tws-keyboard-effect`)이고 드롭다운이 잠깐 그 위에 얹힌다(칸 위 끝과 맞춘다).
   태블릿은 이 칸이 카드 오른쪽 끝이라 오른쪽이 화면 밖이다 — 칸 **아래**에, 오른쪽 끝을 맞춘다. 폰은 이 칸이 없다(원본 `display: none`).
   ⚠ 칸이 `position: relative` 다(랭크 막의 자리) — 그 칸이 기준이다. 조상 중에 잘라 내거나(overflow) 쌓임 맥락을 만드는 것이 없다(실측). */
.tws-assist-menu {
	position: absolute;
	z-index: 5;
	top: 0;
	left: calc(100% + 0.4rem);
}
@media (max-width: 1024px) {
	.tws-assist-menu {
		top: calc(100% + 0.4rem);
		left: auto;
		right: 0;
	}
}

/* ══ 업적 추적 칸 — 카드 위 오른쪽 (PC · 태블릿 · 2026-10-22) ═════════════════
   자리(오른쪽 끝 · 카드 위 테두리에서 1rem 위)와 모양은 `AchieveTracker` 가 갖고, **폭만** 여기서 화면마다 정한다(규칙 12 — 폭은 이 파일의 몫).
   PC 는 안의 칸이 공지 칸과 같은 두 세로선 사이에 선다(공지 칸 31.6rem + 틀 0.2rem × 2 = 32rem) · 막대는 랭크 KP 막대와 같은 11.4rem.
   ⚠️ 태블릿은 **카드가 준 만큼 같이 준다**(지시: 「테블릿에서 추적 컴포넌트가 너무 길어. 테블릿 메인 너비 사이즈 줄어든 만큼 추적 컴포넌트도 줄이면
   될듯. 넘치는 텍스트는 ... 처리 / 진행바도 크기를 좀 줄이고」) — 카드 89.9 → 58.1rem(× 0.646)이라 칸 32 → 20.7rem · 막대 11.4 → 7.4rem. 이름은
   넘치면 «…»(그 칸이 남는 폭을 먹는다). 그만큼 짧아져 업적 토스트(왼쪽 위)와 덜 겹친다 — 태블릿 카드는 581px 라 토스트가 설 자리가
   366px(581 − 207 − 틈 8) 남고, 그보다 긴 토스트(이름이 긴 업적)가 뜨면 그동안 이 칸이 비켜선다(실측).
   폰에는 이 칸이 없다(지시: 「폰에서는 업적 추적을 빼자」 — App 이 안 그린다). */
.tws-ach-track { width: 32rem; }
.tws-ach-track-bar { flex: 0 0 11.4rem; }
@media (min-width: 601px) and (max-width: 1024px) {
	.tws-ach-track { width: 20.7rem; }
	.tws-ach-track-bar { flex-basis: 7.4rem; }
}
