# Step 4 — table-of-contents sidebar for signed-in issue pages Worktree and branch: given in your launch prompt. Read `00-shared.md`, `handoff-step1.md` (legacy world/behind availability) and `handoff-step2.md` (design system). Item 4 of the shared brief: on every signed-in issue page — `/issues/{date}`, `/issues/{date}/articles/{id}`, `/issues/{date}/world`, `/issues/{date}/behind` — a sidebar lists the issue's chapters, marks where the reader is, and lets them jump anywhere. Public pages get nothing. ## Data (Rust, `src/web/issue.rs`) One helper `issue_toc(&IssueView, current: TocPosition) -> Toc` used by all four handlers, passed to their templates as `toc`: ```rust struct Toc { date: Date, display_date: String, issue_number: i64, items: Vec, position: usize, total: usize, issue_href: String } enum TocKind { Brief, Section, Chapter, World, Behind, Colophon } struct TocItem { kind: TocKind, label: String, href: String, number: Option, minutes: Option, current: bool } ``` Chapters are the picks in issue order (numbered 1..n across sections, section labels interleaved as non-links), followed by World Briefing and Behind the paper when available (`has_world`/`has_behind` as computed after step 1) and a Colophon anchor (`/issues/{date}#colophon`). `position`/`total` count navigable chapters only (articles + world + behind), so the article page can say "Chapter 4 of 11"; on the issue page position is 0 ("Front page"). ## Markup and behaviour - Desktop (`lg:` and up): a two-column grid, `aside` on the left `w-64 xl:w-72`, `sticky top-4`, `max-h-[calc(100vh-2rem)] overflow-y-auto`, thin scrollbar. Header: issue number + date (link to the issue), a progress line "Chapter 4 of 11" and a 2px progress bar (width = position/total). Then the list: section labels as tracked uppercase sans, chapters as numbered links in Newsreader `text-[0.95rem]` with the read-time in muted sans, the current one bold with a 2px accent bar on the left and `aria-current="page"`; World Briefing / Behind the paper / Colophon after a hairline. - Mobile (below `lg:`): the aside becomes a sticky bar under the site header: a hamburger `