www.git

www

~ Personal site — projects, posts, about. Built with sqzass.
git clone https://github.com/debuman/www.git
commit ee2330fa699f09ba969f3e21e6b5e75aac5395bb
parent 1d31969a52cd2c0a49990857a1566c2ed3c57a13
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Tue, 4 Aug 2026 21:07:37 +0900

Fix the reading rail: it was 400px from where a hand goes

Measured on the live page at 1440x757, the rail drew a 266x484 block in the
right margin and only 14% of it responded to a mouse. The only live surface was
a 39px strip around three 1px lines; everything else, including the entire area
the labels appear in, was pointer-events: none. Worse, the labels were reachable
only from the 28% of a segment's height that overlapped the label's own band —
enter the tick anywhere else, move toward the name you just revealed, and it
deleted itself before you arrived. An interaction that works about a third of
the time reads worse than one that never works.

Four things changed.

The rail is pinned to the window edge instead of the text column. Anchoring it
to the column put it 311px from the edge at 1440 and 551px at 1920 — nowhere a
hand goes. The strip a hand crosses reaching for the scrollbar is the most
travelled empty space on a page, and the rail now lives in it. The whole box is
the trigger, so the labels appear when the cursor enters the margin and stay up
while it travels to any of them.

Ticks moved outboard and labels inboard, right-aligned against them. Names now
run toward the text instead of away from it, and four right-aligned lines hug
the rail as one list instead of floating as three orphan phrases.

There is a first segment for the introduction, from the top of the prose to the
first heading, labelled with the post's title and linking to #content, the
anchor the skip link already uses. Without it, 34% of the article belonged to no
segment and the rail was inert for the first 750px of scroll — you arrived, you
started reading, and nothing moved. It also gives the proportional map something
to say: with three near-equal sections the segment heights varied by 22% and the
map was indistinguishable from a tick list.

The accent is out of the rail. As drawn, the current section was 2.1:1 and the
sections already read were 4.2:1 — the past twice as loud as the present, and
the one mark carrying "you are here" the faintest thing in the widget. It is one
ordered luminance ramp now: unread hairline, read ink, current full ink, and the
current segment is still the only partially filled one. Accent goes back to
hover, which is where this site's rule says it belongs.

Kept, because measuring the two side by side said to: grid rows rather than
absolutely positioned labels, so a label can never end up beside the wrong tick
and nothing desyncs during a resize; the reading line derived from the heading's
own scroll-margin-top, so "you are here" and "clicking goes here" come from one
declaration; getElementById on the raw href, which does not throw on a Korean
id; and the inline list that keeps working with no JavaScript at all. Still
refused: hiding the native scrollbar, and the drag-scrub that only makes sense
once you have.

Diffstat:
Mstatic/css/main.css | 110+++++++++++++++++++++++++++++++------------------------------------------------
Mstatic/js/ui.js | 102++++++++++++++++++++++++++++++++++++++++---------------------------------------
2 files changed, 95 insertion(+), 117 deletion(-)

diff --git a/static/css/main.css b/static/css/main.css
@@ -974,63 +974,56 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
 
 /* ---------- reading rail ---------- */
 
-/* 목차의 두 번째 모습. 넓은 화면에서 오른쪽 여백에 서고, 절마다 세로 헤어라인
-   한 칸이며 칸의 높이가 그 절이 글에서 차지하는 몫이다. 목록은 절이 몇 개인지
-   말하고 지도는 어느 절이 긴지 말한다 — 같은 자료의 다른 사실이라 둘 다 있어도
-   중복이 아니다.
+/* 목차의 두 번째 모습. 넓은 화면에서 오른쪽 가장자리에 서고, 절마다 세로
+   헤어라인 한 칸이며 칸의 높이가 그 절이 글에서 차지하는 몫이다. 목록은 절이
+   몇 개인지 말하고 지도는 어느 절이 긴지 말한다.
 
    레일이 실제로 만들어졌을 때만 목록이 물러난다(.has-rail 은 ui.js 가 붙인다).
-   스크립트가 못 오면 목록이 그대로 남고, 그게 이 장치의 폴백이다. 문서 안에
-   목차는 언제나 한 벌이고 접근성 트리에도 한 벌만 남는다. */
+   스크립트가 못 오면 목록이 그대로 남고, 그게 이 장치의 폴백이다.
+
+   처음 판은 본문 칼럼에 붙여 놓고 눈금 자체만 손을 받게 했다. 재 보니 레일이
+   차지한 넓이의 14% 만 반응했고, 눈금에서 이름으로 커서를 옮기면 이름이
+   대부분 사라졌다 — 이름과 세로로 겹치는 28% 구간에서만 건너갈 수 있었다.
+   가끔 되는 조작은 안 되는 조작보다 나쁘다. 지금은 여백 전체가 손을 받는다. */
 
 .rail { display: none; }
 
 /* 이 시트의 첫 min-width 다. 나머지는 전부 max-width — 넓게 시작해서 좁아지면
    뺀다는 자세이고, 이것만 예외인 이유는 레일이 없던 자리를 요구하는 유일한
-   요소이기 때문이다. 좁은 화면에서는 줄이는 게 아니라 뺀다. */
+   요소이기 때문이다. */
 @media (min-width: 80rem) {
   .rail {
     display: grid;
-    /* 왼쪽이 눈금, 오른쪽이 이름. 선을 먼저 긋고 그 옆에 글을 두는 것은 .toc 의
-       border-left 와 blockquote 가 이미 쓰는 이 집의 곁줄 어법이다. 눈금을 창
-       끝에 붙이면 스크롤바 바로 옆에 세로줄이 하나 더 생긴다. */
-    grid-template-columns: 1px minmax(0, 1fr);
+    /* 이름이 안쪽, 눈금이 바깥쪽이다. 눈금은 스크롤 막대 옆에 서는 스크롤
+       가구이고 이름은 글 쪽으로 흐른다 — 반대로 두면 눈이 글에서 멀어지는
+       방향으로 읽어 나가야 한다. */
+    grid-template-columns: minmax(0, 1fr) 1px;
     column-gap: 0.9rem;
-    row-gap: 0.4rem;
+    row-gap: 0.3rem;
     /* 행 높이는 ui.js 가 grid-template-rows 로 쓴다. 겹침 풀이를 격자에 맡기면
-       참조 구현이 손으로 짠 두 번의 밀어내기 패스와 그 안의 상수 셋이 통째로
-       없어진다. */
+       참조 구현이 손으로 짠 두 번의 밀어내기 패스가 통째로 없어지고, 이름이
+       엉뚱한 눈금 옆에 가는 일이 구조적으로 불가능해진다. */
 
     position: fixed;
-    /* 창이 아니라 본문에 붙는다. right:0 으로 매달면 2560px 에서 레일만 500px
-       멀어져 무엇의 눈금인지 알 수 없게 된다. .wrap 은 --gutter 만큼 안쪽
-       패딩이 있으므로 글자가 끝나는 자리는 50vw + --read/2 - --gutter 다.
-       거기서 2.5rem 떨어뜨린다 — 이 부품의 유일한 임의값이고, 본문과 레일이
-       서로 다른 것으로 읽히는 최소 거리다. 폭도 같은 값들에서 끌어내므로
-       --read 를 고치면 레일이 따라온다(.wrap-ledger 가 같은 방법을 쓴다). */
-    left: calc(50vw + var(--read) / 2 - var(--gutter) + 2.5rem);
-    width: min(14rem, calc(50vw - var(--read) / 2 + var(--gutter) - 4rem));
+    /* 창 가장자리에 붙는다. 본문 칼럼에 매달았더니 1920px 에서 레일이 창 끝에서
+       551px 떨어진 자리에 섰다 — 손이 가는 자리가 아니다. 스크롤 막대를 향해
+       뻗는 손이 지나가는 띠가 이 화면에서 가장 자주 밟히는 빈 자리이고,
+       여기 두면 찾으려 하지 않아도 걸린다.
+       폭은 본문과 겹치지 않는 만큼까지만 — 1280px 에서 28px 이 남는다. */
+    right: 0;
+    width: min(16rem, calc(50vw - var(--read) / 2 + var(--gutter) - 1.5rem));
+    padding-right: 1rem;
     top: 50%; transform: translateY(-50%);
-    height: min(58vh, 30rem);
-    /* 마스트헤드(30) 아래. 올라온 이름이 크롬을 덮으면 안 된다. 검색은 modal
-       <dialog> 라 top layer 에 뜨므로 셈에 넣지 않는다. */
+    height: min(64vh, 34rem);
+    /* 마스트헤드(30) 아래. 올라온 이름이 크롬을 덮으면 안 된다. */
     z-index: 20;
-    /* 레일 자체는 손을 받지 않는다. 여백 전체를 투명한 판으로 덮으면 나중에 그
-       여백에 놓는 것이 전부 눌리지 않는다. */
-    pointer-events: none;
   }
 
-  .rail-seg {
-    position: relative;
-    background: var(--rule-2);
-    pointer-events: auto;
-    transition: background-color var(--tr-ink);
-  }
-  /* 1px 은 겨눌 수 없다. 보이는 선은 그대로 두고 손이 닿는 자리만 넓힌다.
-     오른쪽 1rem 은 column-gap(0.9rem)보다 넓다 — 눈금에서 이름으로 커서가
-     건너가는 중간에서 반응이 끊기면 이름이 도로 사라진다. 세로 -0.2rem 은
-     row-gap 의 절반이라 칸 사이에서도 안 끊긴다. */
-  .rail-seg::before { content: ""; position: absolute; inset: -0.2rem -1rem; }
+  /* 눈금은 세 단의 밝기다: 아직 안 읽은 것, 읽은 것, 지금 읽는 것. 색조는 한
+     가지뿐이라 1px 에서도 순서가 읽힌다 — 주황을 섞었더니 지금 자리가 이 열에서
+     제일 어두운 표시가 됐고(2.1:1), 읽은 자리가 그보다 두 배 진했다. 과거가
+     현재보다 크게 그려지면 그건 위치 표시가 아니다. */
+  .rail-seg { background: var(--rule-3); }
 
   /* 읽은 만큼. 높이는 ui.js 가 스크롤에서 직접 쓴다 — 전환을 걸면 화면보다 한
      박자 늦게 따라와 거짓말을 한다. 이미 연속값에 매인 것은 애니메이션하지
@@ -1040,18 +1033,17 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
     background: var(--ink-3);
     transition: background-color var(--tr-ink);
   }
-  /* 세 단이다: 헤어라인은 아직 안 읽은 것, 잉크는 읽은 것, 주황은 지금 읽는
-     것. 주황이 색 하나로만 말하지 않는다 — 지금 칸은 이 열에서 유일하게 덜 찬
-     칸이고(위는 다 차고 아래는 비어 있다), 그 이름은 유일하게 --ink 다.
-     클래스로 표시하는 이유: 왼쪽이 눈금이라 DOM 에서 눈금이 이름보다 앞에 오고,
-     CSS 에는 앞 형제를 짚는 선택자가 없다. */
-  .rail-fill.is-now { background: var(--accent); }
+  /* 지금 절이 이 열에서 제일 진하고, 유일하게 덜 찬 칸이다. 색 하나로만 말하지
+     않는다. */
+  .rail-fill.is-now { background: var(--ink); }
 
   .rail-label {
     align-self: start;
+    /* 오른쪽 정렬이라 이름들이 눈금에 붙어 한 덩이로 읽힌다. 왼쪽 정렬로 두면
+       길이가 제각각인 세 구절이 빈 자리에 흩어져 목록으로 안 보인다. */
+    text-align: right;
     /* 두 줄까지. 칸의 아래끝 2.2rem 이 정확히 두 줄 높이라, 넘치면 옆 칸을
-       침범한다. 지금 글의 제일 긴 제목이 30자이고 1280px 에서 한 줄이 26자쯤이라
-       두 줄에 들어온다.
+       침범한다.
        ponytail: 여기 걸리는 제목이 잦아지면 breakpoint 를 84rem 으로 올려 이름
        열을 넓히는 쪽이 먼저다. 글자 크기를 줄이는 건 그다음. */
     display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
@@ -1064,12 +1056,13 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
   .rail-label[aria-current] { color: var(--ink); }
 
   /* 드러나는 것은 숨어 있던 것이지 자리를 옮긴 것이 아니다. 이름은 제자리에서
-     밝아지고 눈금은 색이 한 단 오른다 — 굵어지지도, 밀려나지도 않는다. 박자는
-     이 시트의 유일한 박자 하나다. */
+     밝아지고 눈금은 색이 한 단 오른다 — 굵어지지도, 밀려나지도 않는다.
+     여백 전체가 방아쇠라, 한 번 나온 이름까지 커서가 가는 동안 사라지지 않는다.
+     박자는 이 시트의 유일한 박자 하나다. */
   .rail:hover .rail-label,
   .rail:focus-within .rail-label { opacity: 1; pointer-events: auto; }
   .rail:hover .rail-seg,
-  .rail:focus-within .rail-seg { background: var(--rule-3); }
+  .rail:focus-within .rail-seg { background: var(--ink-4); }
 
   /* 지금 절 위에 손이 올라가도 현재 표시를 지우지 않는다. hover 와 현재를 같게
      칠하면 목차에 커서를 얹는 것만으로 위치 표시가 사라진다. */
@@ -1078,23 +1071,6 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
   .has-rail .toc { display: none; }
 }
 
-.pager {
-  display: flex; gap: 1.5rem;
-  margin-top: clamp(3.5rem, 8vw, 5.5rem); padding-top: 1.75rem;
-  border-top: 1px solid var(--rule);
-}
-.pager-link { flex: 1; transition: color var(--tr-ink); }
-.pager-next { text-align: right; }
-.pager-next:only-child { margin-left: auto; flex: 0 1 22rem; }
-.pager-dir {
-  display: block; font-size: 0.64rem;
-  letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-4);
-  margin-bottom: 0.35rem;
-}
-.pager-title { display: block; font-size: 1rem; font-weight: var(--w-item); color: var(--ink-2); transition: color var(--tr-ink); }
-.pager-link:hover .pager-title { color: var(--ink); }
-@media (max-width: 40rem) { .pager { flex-direction: column; } }
-
 /* ---------- prose ---------- */
 
 .prose { font-size: 1.02rem; line-height: 1.78; }
diff --git a/static/js/ui.js b/static/js/ui.js
@@ -81,52 +81,68 @@
 
   /* ---------- reading rail ---------- */
 
-  /* 넓은 화면에서 오른쪽 여백에 서는 목차. 항목마다 세로 헤어라인 한 칸이고,
+  /* 넓은 화면에서 오른쪽 가장자리에 서는 목차. 항목마다 세로 헤어라인 한 칸이고,
      칸의 높이가 그 절이 글에서 차지하는 몫이다. 항목이 다 같은 크기인 목록은
      "가운데 절이 이 글의 절반"이라는 사실을 말하지 못한다.
 
      자료는 본문에 이미 있는 .toc 다. 이 파일이 못 오면 그 목록이 그대로 남는다 —
      목록을 감추는 CSS 가 여기서 붙이는 .has-rail 뒤에 있다.
 
+     첫 칸은 제목에서 첫 소제목까지다. 그게 없을 때 재 보니 이 글의 34% 가 어느
+     칸에도 안 들어갔고, 처음 750px 을 굴리는 동안 레일이 통째로 죽어 있었다.
+     도착해서 읽기 시작한 사람에게 아무 반응이 없는 진행 표시는 고장으로 보인다.
+
      넓은 화면인지는 여기서 묻지 않는다. 그 판단은 main.css 의 min-width 하나가
      갖고 있다. 두 곳에 두면 두 값이 조용히 어긋나고, 그때 나오는 증상은 "레일이
-     보이는데 안 찬다"라 숫자 문제로 보이지 않는다. 숨어 있을 때 여기가 하는 일은
-     짧은 배열 하나에 대한 산수뿐이라 값이 없다.
+     보이는데 안 찬다"라 숫자 문제로 보이지 않는다.
 
-     이른 반환이 여러 번 필요해서 안쪽 IIFE 다. 바깥 IIFE 에서 return 하면 나중에
-     이 아래에 절을 하나 더 붙이는 사람이 조용히 잃는다. */
+     이른 반환이 여러 번 필요해서 안쪽 IIFE 다. */
   (function () {
-    /* 읽는 줄. 마스트헤드에 가리지 않은 높이의 위에서 30%.
-       ponytail: 절이 이 거리보다 짧으면 그 절의 이름을 눌렀을 때 다음 절이
-       현재가 된다. 짧은 절이 많은 글이 생기면 0.2 쪽으로 내린다. */
+    /* 읽는 줄. 마스트헤드에 가리지 않은 높이의 위에서 30%. */
     var LINE = 0.3;
-    /* 이만큼도 못 구르는 문서에는 진행 표시를 그리지 않는다. 못 움직이는 진행
-       표시는 진행을 말하지 않는다. */
+    /* 이만큼도 못 구르는 문서에는 진행 표시를 그리지 않는다. */
     var FLOOR = 240;
 
     var toc = document.querySelector(".toc");
     var prose = document.querySelector(".prose");
+    var title = document.querySelector(".page-title");
     if (!toc || !prose) return;
     if (root.scrollHeight - window.innerHeight < FLOOR) return;
 
-    var host = document.createElement("nav");
-    host.className = "rail";
-    /* 이름은 목록이 이미 갖고 있다. JS 안에 문자열을 두면 번역도 안 되고
-       tools/fonts.py 의 눈에도 안 띈다. */
-    host.setAttribute("aria-label", toc.getAttribute("aria-label") || "");
-
     /* 목차의 링크를 전부 쓴다. 깊이별로 고르지 않는다 — 레일이 그리는 것은
        개요가 아니라 분량이고, 분량에는 깊이가 없다. 구조는 좁은 화면의 목록이
        들여쓰기로 이미 말한다. */
-    var rows = [];
+    var heads = [];
     toc.querySelectorAll("a[href^='#']").forEach(function (a) {
       var href = a.getAttribute("href");
       /* 한국어 제목은 id 가 한글이다(#택하지-않은-우회로). "#"+id 를
          querySelector 에 넘기면 CSS 선택자로 파싱돼 던진다. 소스에 적힌 값을
-         그대로 쓰므로 디코딩도 필요 없다 — decodeURIComponent 는 제목에 % 가
-         들어간 날 오히려 던진다. */
-      var head = document.getElementById(href.slice(1));
-      if (!head) return;
+         그대로 쓰므로 디코딩도 필요 없다. */
+      var el = document.getElementById(href.slice(1));
+      if (el) heads.push({ el: el, href: href, text: a.textContent });
+    });
+    /* 소제목이 하나뿐인 목차는 목차가 아니라 링크 하나다. */
+    if (heads.length < 2) return;
+
+    /* 첫 칸은 머리말이다. 제목을 이름으로 쓰고 <main id="content"> 로 간다 —
+       건너뛰기 링크가 이미 쓰는 진짜 앵커라, 가짜 "#" 도 클릭 핸들러도 필요
+       없다. */
+    var parts = [{ el: prose, href: "#content", text: (title && title.textContent) || "" }]
+      .concat(heads);
+
+    var host = document.createElement("nav");
+    host.className = "rail";
+    /* 이름은 목록이 이미 갖고 있다. JS 안에 문자열을 두면 번역도 안 되고
+       tools/fonts.py 의 눈에도 안 띈다. */
+    host.setAttribute("aria-label", toc.getAttribute("aria-label") || "");
+
+    var rows = parts.map(function (s) {
+      /* 격자가 두 열이라 넣는 순서대로 이름·눈금이 한 행이 된다. 이름이 앞이고
+         눈금이 뒤다 — 화면에서 이름이 안쪽, 눈금이 바깥쪽이다. */
+      var label = document.createElement("a");
+      label.className = "rail-label";
+      label.setAttribute("href", s.href);
+      label.textContent = s.text;
 
       var seg = document.createElement("span");
       seg.className = "rail-seg";
@@ -134,19 +150,10 @@
       fill.className = "rail-fill";
       seg.appendChild(fill);
 
-      var label = document.createElement("a");
-      label.className = "rail-label";
-      label.setAttribute("href", href);
-      label.textContent = a.textContent;
-
-      /* 격자가 두 열이라 넣는 순서대로 눈금·이름이 한 행이 된다. 행 번호를 셀
-         필요가 없고, 그래서 이름의 세로 위치가 곧 그 절의 위치다. */
-      host.appendChild(seg);
       host.appendChild(label);
-      rows.push({ fill: fill, label: label, head: head, f: -1 });
+      host.appendChild(seg);
+      return { fill: fill, label: label, el: s.el, f: -1 };
     });
-    /* 항목이 하나뿐인 목차는 목차가 아니라 링크 하나다. */
-    if (rows.length < 2) return;
 
     /* 목차 바로 뒤에 꽂는다. body 끝에 붙이면 화면에서는 본문 옆이지만 탭
        순서에서는 본문 뒤가 되고, 다 읽은 뒤에야 닿는 목차는 아무도 안 쓴다. */
@@ -164,8 +171,8 @@
       /* 읽는 줄이 마스트헤드에 가리는 만큼을 제목 자신의 scroll-margin-top 에서
          가져온다. main.css 가 앵커 점프를 위해 이미 선언해 둔 값이고, 브라우저가
          눌러서 뛸 때 쓰는 바로 그 값이다 — 한 군데서 나오므로 "여기 있다"와
-         "여기로 간다"가 어긋날 수 없다. --masthead-h 를 고쳐도 따라온다. */
-      var pad = parseFloat(getComputedStyle(rows[0].head).scrollMarginTop) || 0;
+         "여기로 간다"가 어긋날 수 없다. */
+      var pad = parseFloat(getComputedStyle(heads[0].el).scrollMarginTop) || 0;
       lead = pad + (window.innerHeight - pad) * LINE;
 
       tops = [];
@@ -173,18 +180,16 @@
       rows.forEach(function (r) {
         /* 앞 경계보다 위로는 못 간다. 서브픽셀이나 sticky 때문에 뒤 제목이 앞
            제목보다 위로 재어지면 높이가 음수인 칸이 생긴다. */
-        prev = Math.max(absTop(r.head), prev);
+        prev = Math.max(absTop(r.el), prev);
         tops.push(prev);
       });
-      /* 읽기는 본문이 끝나는 데서 끝난다. 페이저와 콜로폰은 글이 아니다. 레일의
-         좌표계는 첫 제목부터 본문 끝까지다 — 머리말은 목차에 없으므로 레일에도
-         없다. */
+      /* 읽기는 본문이 끝나는 데서 끝난다. 페이저와 콜로폰은 글이 아니다. */
       tops.push(Math.max(absTop(prose) + prose.offsetHeight, prev + 1));
 
       /* 칸의 높이가 그 절의 몫이다. 아래끝을 두면 아주 짧은 절도 사라지지 않고
-         두 줄짜리 이름이 옆 칸을 침범하지도 않는다 — 겹침 풀이를 격자에 맡긴다.
-         ponytail: 절이 열한 개를 넘으면 아래끝의 합이 상자보다 커져 넘친다.
-         그때 손댈 곳은 2.2rem 과 min(58vh, 30rem) 둘이다. */
+         두 줄짜리 이름이 옆 칸을 침범하지도 않는다.
+         ponytail: 절이 열두 개를 넘으면 아래끝의 합이 상자보다 커져 넘친다.
+         그때 손댈 곳은 2.2rem 과 min(64vh, 34rem) 둘이다. */
       var total = tops[tops.length - 1] - tops[0];
       var track = "";
       for (var i = 0; i < rows.length; i++) {
@@ -201,9 +206,9 @@
       /* 문서 끝에 닿으면 마지막 절은 끝난 것이다. 읽는 줄은 화면 아래 한 뼘을
          영영 못 지나가므로, 이게 없으면 마지막 칸은 절대 안 찬다. */
       var done = window.scrollY + window.innerHeight >= root.scrollHeight - 2;
-      /* 첫 제목 위에서는 아무것도 현재가 아니다. 아직 시작도 안 한 사람에게
-         "1절에 있다"고 말하면 그 표시는 남은 페이지 내내 못 믿는다. */
-      var now = -1;
+      /* 머리말 칸이 있으므로 첫 화면부터 현재가 있다. 도착한 사람은 실제로
+         머리말을 읽고 있다. */
+      var now = 0;
 
       for (var i = 0; i < rows.length; i++) {
         var f = done ? 1 : (line - tops[i]) / Math.max(1, tops[i + 1] - tops[i]);
@@ -215,8 +220,7 @@
         }
         /* 화면 상자가 아니라 줄 하나로 고른다. 제목 여럿이 같이 보일 때 누가
            이기는지 따질 일이 없다 — 줄 위에 있는 마지막 절이다. 관찰자를 쓰지
-           않으므로 부드러운 스크롤과 싸울 일도 없다: 눌러서 뛰는 동안 표시가
-           지나가는 절들을 훑는 것은, 실제로 그 절들을 지나가고 있어서다. */
+           않으므로 부드러운 스크롤과 싸울 일도 없다. */
         if (line >= tops[i]) now = i;
       }
       if (done) now = rows.length - 1;
@@ -226,10 +230,8 @@
         rows[at].label.removeAttribute("aria-current");
         rows[at].fill.classList.remove("is-now");
       }
-      if (now >= 0) {
-        rows[now].label.setAttribute("aria-current", "true");
-        rows[now].fill.classList.add("is-now");
-      }
+      rows[now].label.setAttribute("aria-current", "true");
+      rows[now].fill.classList.add("is-now");
       at = now;
     }