/* THE RUNNER'S DESK (js/maker/runner-desk.js). The set is Sonar's, as given in
   design/the-runners-desk-build.md §6; everything after "END OF SONAR'S SET"
   places it on the page and draws what is on its screen. */

/* ── LUX'S TELEVISION ─────────────────────────────────────────────────
   A 1950s set, cartoonish: chunky outlines, generous radii, flat colour.
   Walnut body, cream bezel and dials, rounded CRT, rabbit ears, stub legs.
   The amber is the product's one accent and is used ONLY for the live
   edge and the clock — never for the casing, or the set competes with
   the thing on its screen. */
.tv            { display: block; width: 100%; padding: 0; border: none;
                 background: none; cursor: pointer; font: inherit; }
.tv-ears       { display: block; height: 30px; position: relative; }
.tv-ear        { position: absolute; left: 50%; bottom: 0; width: 3px;
                 height: 30px; background: #6B4425; border-radius: 2px;
                 transform-origin: bottom center; }
.tv-ear.l      { transform: rotate(-26deg); }
.tv-ear.r      { transform: rotate(26deg); }
.tv-tip        { position: absolute; bottom: 26px; width: 8px; height: 8px;
                 border-radius: 999px; background: #E8DCC8;
                 border: 2px solid #6B4425; box-sizing: border-box; }

.tv-body       { display: flex; gap: 9px; padding: 11px; border-radius: 20px;
                 background: #A9703F; border: 3px solid #6B4425;
                 box-sizing: border-box; box-shadow: 0 12px 26px rgba(0,0,0,.5); }
.tv-bezel      { flex: 1 1 auto; min-width: 0; padding: 7px; border-radius: 16px;
                 background: #E8DCC8; border: 2px solid #6B4425;
                 box-sizing: border-box; }
.tv-screen     { position: relative; border-radius: 11px; background: #07090B;
                 padding: 9px; overflow: hidden;
                 box-shadow: inset 0 0 18px rgba(0,0,0,.9); }
.tv-screen::after { content: ""; position: absolute; inset: 0;
                 pointer-events: none;
                 background: repeating-linear-gradient(to bottom,
                   rgba(255,255,255,.045) 0 1px, transparent 1px 3px); }

.tv-side       { flex: none; width: 26px; display: flex; flex-direction: column;
                 align-items: center; gap: 7px; padding-top: 3px; }
.tv-dial       { width: 20px; height: 20px; border-radius: 999px;
                 background: #E8DCC8; border: 2px solid #6B4425;
                 box-sizing: border-box; }
.tv-grille     { display: flex; flex-direction: column; gap: 3px; width: 18px; }
.tv-grille i   { height: 2px; border-radius: 2px; background: #6B4425; }

.tv-legs       { display: flex; justify-content: space-between; padding: 0 26px; }
.tv-leg        { width: 7px; height: 13px; background: #6B4425;
                 border-radius: 0 0 4px 4px; }
.tv-leg.l      { transform: rotate(10deg); }
.tv-leg.r      { transform: rotate(-10deg); }

/* the shot strip on the screen */
.tv-strip      { display: flex; gap: 3px; margin-bottom: 7px; }
.tv-shot       { flex: 1 1 0; aspect-ratio: 16/9; border-radius: 2px; }
.tv-shot.done  { background: linear-gradient(150deg,#4A3B2A,#241C12); }
.tv-shot.live  { background: #0E1013; border: 1px solid #E9A94E; }
.tv-shot.wait  { background: #101317; border: 1px dashed #2B3038; }

@media (prefers-reduced-motion: reduce) { .tv * { animation: none !important; } }
/* ── END OF SONAR'S SET ─────────────────────────────────────────────── */

/* The spans inside the button are blocks (a button may only hold phrasing content). */
:where(.tv) span { display: block; }   /* zero class weight: every .tv-* rule below still decides its own display */
.tv-tip.l      { left: calc(50% - 16px); }
.tv-tip.r      { left: calc(50% + 10px); }
.tv:focus-visible { outline: none; }
.tv:focus-visible .tv-body { outline: 3px solid #E9A94E; outline-offset: 3px; }
.tv-sr         { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.tv-shot.fail  { background: #1A0F0E; border: 1px solid #8C3B2E; }

/* What is on the screen. Cream on black; amber only for the live edge and the clock. */
.tv-screen     { min-height: 78px; color: #E8DCC8; text-align: left; }
.tv-work       { display: flex; flex-direction: column; gap: 2px; font-size: 12px; line-height: 1.35; }
/* WORKING WITH A PICTURE (Raina, 2026-10-06: "it doesn't show thumbnails"): DONE's own box,
   different words. The latest picture full-bleed; strip, line, counts and clock sit in the
   caption on the bottom gradient, so the subject in the top of the picture stays bright
   (Sonar, REF-6F679AE7). The caption is a .tv-work too, so its rows keep their type. */
.tv-frame.has-work { height: 132px; }
/* The strip rides the top edge of the picture: opaque cells, no ground needed. */
.tv-frame.has-work > .tv-strip { position: absolute; left: 6px; right: 6px; top: 6px; }
/* The band under the words holds FULL STRENGTH across the text and puts the whole ramp in a
   short edge above it: every row reads at ~8:1 over a bright picture (Sonar measured the
   earlier wash at 1.2–3.9:1 over bright.png; AA is 4.5:1). A soft edge on a band, not a
   wash over the frame. */
.tv-frame.has-work .tv-frame-caption { display: flex; flex-direction: column; gap: 1px; padding-top: 20px;
                 background: linear-gradient(to top, rgba(7,9,11,.94) 0, rgba(7,9,11,.94) calc(100% - 18px), rgba(7,9,11,0) 100%); }
.tv-frame.has-work .tv-frame-caption.tv-work { position: absolute; }
/* The phone's small set: the frame fills the screen's content box (the shared 64px DONE
   height is (0,3,0) and would otherwise pin it, leaving the picture fully covered — Sonar's
   control went 64 → 104 px with this rule), and over a picture the ordinary "Making" row
   goes: the lit cell and the running clock already say it. The line stays for trouble. */
.runner-desk.is-phone .tv-frame.has-work { height: auto; align-self: stretch; flex: 1 1 auto; aspect-ratio: auto; }
.runner-desk.is-phone .tv-frame.has-work .tv-line:not(.is-trouble):not(.is-always) { display: none; }
.runner-desk.is-phone .tv-frame.has-work > .tv-strip { top: 5px; }
/* The phone caption gives the rows the whole frame width (139 px, as without a picture) and
   drops "about": the estimate needs 130 px and the band's 8 px sides left it 123 (Sonar). */
.runner-desk.is-phone .tv-frame.has-work .tv-frame-caption { padding-left: 0; padding-right: 0; }
.runner-desk.is-phone .tv-frame.has-work .tv-usual-about { display: none; }
.desk-full-picture { position: relative; height: min(40vh, 360px); border-radius: 10px; overflow: hidden; margin-bottom: 12px;
                 background: linear-gradient(150deg,#4A3B2A,#241C12); }
.desk-full-picture .tv-frame-img { position: absolute; inset: 0; width: 100%; height: 100%; }
.tv-line       { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
/* "Working · 12s made" (Raina, 2026-10-10): only the word may truncate, never the number (Sonar, REF-AF0555E1). */
.tv-line.has-made { display: flex; align-items: baseline; min-width: 0; }   /* a class, not :has() — Safari before 15.4 drops :has (Fathom) */
.tv-line-word  { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: pre; }
.tv-made       { flex: none; font-variant-numeric: tabular-nums; }
.tv-meta       { display: flex; justify-content: space-between; gap: 8px; color: #B9AE9A; }
.tv-meta .tv-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* "2:48 · usually about 5:00": the clock and the estimate never truncate; only the label
   between them may (Sonar, REF-AF0555E1 — the ellipsis once ate the estimate on the phone). */
.tv-usual      { color: #B9AE9A; display: flex; align-items: baseline; white-space: nowrap; min-width: 0; }
.tv-clock      { color: #E9A94E; font-variant-numeric: tabular-nums; }
.tv-usual > .tv-clock, .tv-usual-est { flex: none; }
.tv-usual-est  { font-variant-numeric: tabular-nums; }
.tv-usual-label { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: pre; }
/* Inside the label: `:where(.tv) span { display: block }` blockifies every span in the set, and
   this one would take its own line — "about" orphaned under "usually" (Sonar, REF-43D047E6). */
.tv-usual-about { display: inline; }
.tv-waiting    { color: #E8DCC8; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* THE TEST CARD, the station ident (placeholder drawing; Sonar owns the look):
   a circle, a crosshair, a bar strip, and the runner's name in Instrument Serif. */
.tv-card       { position: relative; display: flex; align-items: center; justify-content: center;
                 height: 78px; overflow: hidden; }
.tv-card-x     { position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: rgba(232,220,200,.35); }
.tv-card-y     { position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: rgba(232,220,200,.35); }
.tv-card-ring  { position: absolute; left: 50%; top: 50%; width: 70px; height: 70px; transform: translate(-50%, -50%);
                 border-radius: 999px; border: 2px solid rgba(232,220,200,.6); }
.tv-bars       { position: absolute; left: 12%; right: 12%; top: 50%; height: 16px; transform: translateY(-50%);
                 display: flex; border-radius: 2px; overflow: hidden; }
.tv-bars i     { flex: 1 1 0; }
/* WAKING UP: static over the test card. Two layers of fine noise (an SVG turbulence, drawn once) slide past each
   other in steps, with a slow roll bar; the bars flicker under it. Reduced motion keeps the still picture (rule above). */
.tv-card[data-mode="waking"] .tv-static {
  position: absolute; inset: 0; pointer-events: none; mix-blend-mode: screen; opacity: .34;
  background-image:
    linear-gradient(rgba(255,255,255,0) 0%, rgba(255,255,255,.12) 48%, rgba(255,255,255,0) 52%),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='120' height='120' filter='url(%23n)'/></svg>");
  background-size: 100% 40%, 120px 120px;
  animation: tv-roll 2.4s linear infinite, tv-snow .35s steps(6) infinite; }
.tv-card[data-mode="waking"] .tv-bars { top: auto; bottom: 12%; transform: none; height: 9px; animation: tv-flicker 1.7s steps(9) infinite; }
.tv-card[data-mode="waking"] .tv-card-name { z-index: 1; letter-spacing: .02em; }
@keyframes tv-roll    { from { background-position: 0 -40%, 0 0; } to { background-position: 0 140%, 0 0; } }
@keyframes tv-snow    { 0% { background-position-x: 0, 0; } 100% { background-position-x: 0, -120px; } }
@keyframes tv-flicker { 0%, 100% { opacity: 1; } 30% { opacity: .55; } 55% { opacity: .85; } 70% { opacity: .4; } }
/* Reduced motion wherever the card is drawn: the small set (.tv, above) AND the full-screen card (.desk-full, on body). */
@media (prefers-reduced-motion: reduce) { .tv-card[data-mode="waking"] .tv-static, .tv-card[data-mode="waking"] .tv-bars { animation: none !important; } }
.tv-card-name  { position: relative; padding: 0 10px; border-radius: 4px; background: #07090B; color: #E8DCC8;
                 font-family: "Instrument Serif", Georgia, serif; font-size: 22px; line-height: 1.15;
                 white-space: nowrap; max-width: 90%; overflow: hidden; text-overflow: ellipsis; }

/* DONE: the last frame held, the show's name, "3:03 · ready". */
.tv-frame      { position: relative; display: block; height: 78px; border-radius: 4px; overflow: hidden;
                 background: linear-gradient(150deg,#4A3B2A,#241C12); }
/* ⛔ CONTAIN, NOT COVER — the same defect the player had (#391). The TV's screen is a
   FIXED shape, so what it shows must be contained: a 16:9 film in it letterboxes the
   way an old set does, which is on-theme and honest. `cover` instead CROPS — measured,
   the phone frame is 139x64 (2.17:1), so a 16:9 still loses its top and bottom and a
   9:16 one becomes a thin horizontal slice of itself. With Orca's #389 making vertical
   films the default on a phone, cover would crop exactly the films the phone makes. */
.tv-frame-img  { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
.tv-frame-caption { position: absolute; left: 0; right: 0; bottom: 0; padding: 14px 8px 5px;
                 background: linear-gradient(to top, rgba(7,9,11,.92), rgba(7,9,11,0)); }
.tv-frame-title { font-size: 13px; font-weight: 600; color: #E8DCC8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tv-frame-ready { font-size: 11px; color: #B9AE9A; }
.tv-frame.is-behind .tv-frame-ready { color: #E9A94E; }   /* behind the timeline: the SAME amber as Waiting (one-chat.css .relay-pill) — both mean "your move"; the pill vs plain text carries the weight (Sonar). Never a BARS colour: those are the test card's drawing, not a state (bug-2a7df97a) */
.tv-frame.is-full { height: min(60vh, 560px); border-radius: 10px; }
.tv-frame.is-full .tv-frame-title { font-size: 24px; }
.tv-frame.is-full .tv-frame-ready { font-size: 15px; }
.tv-frame.is-full .tv-frame-caption { padding: 40px 18px 14px; }

/* ── WHERE THE SET SITS ── desktop: its own column beside the conversation. */
.runner-desk   { width: 248px; }
@media (min-width: 1041px) {
  body.has-desk #chat-host { margin-right: 280px; }
  body.has-desk > .runner-desk { position: fixed; right: 20px; top: 76px; z-index: 5; }
}
body.signed-out .runner-desk { display: none; }

/* Phone: above the composer. The antennas drop first, the dials second; the
   screen keeps its size.

   ── THE 390 HEIGHT (Sonar, 2026-10-05) ────────────────────────────────────
   Measured on the merged shots: the set was capped at 272px inside a 390px
   column — SEVENTY PIXELS OF MARGIN EACH SIDE — and its screen was carrying
   four lines of small type in 110px. So the TV was BOTH the tallest thing on
   the phone AND the hardest to read, which is the worst of both.
   The room comes from the WIDTH, not from shrinking the set: full column,
   same height, a screen half as cramped. Costs nothing vertical.
   ⛔ AND NOT BY DROPPING THE LEGS. They are 11px and they are the reason it
   reads as a television rather than a panel; the charm IS the feature here.
   What goes instead is the duplicated line (below). */
/* 264 − 32 padding = 232 cabinet → ~176 of screen → 132 tall at 4:3, and the
   whole set lands near 173px: the proportion of a television for about the
   height the stretched one cost. Wider and 4:3 eats the conversation;
   narrower and the type on the screen stops being readable. */
/* ── THE CABINET GIVES UP ITS SPACE ON A PHONE ──────────────────────────
   Measured 2026-10-08 at 390 (Kane's capture REF-KANE-PHONE-01, re-measured here):
   cabinet 232 · bezel 173 · screen 157 · frame 139 · PICTURE 139x64 — and 26px of
   that is the dial panel. On a desktop side column the cabinet is charm; at 390 it
   was 93px of chrome around the only thing she is looking at.
   ⛔ AND KANE'S READING OF IT, which is the part that decides it: the cabinet spends
   its space in the state where she has NOT SEEN the film yet and is deciding whether
   to. Rendered, the player gives the same film 358px on the same phone — 92% of the
   device against 36%. The worst place in the flow to spend width is the one place
   the picture has to do its only job.
   WHAT GOES: the dial panel, 26px plus its 9px gap. It is the cheapest character per
   pixel in the set — two circles and a grille. WHAT STAYS: the cabinet, the bezel
   and the legs. (The ears are already hidden on a phone, three rules below.)
   ⚠ THE WIDTH IS NOT CHANGED HERE AND THAT IS DELIBERATE — see the guard below, which
   is my own from #279 and which this pass walked straight into. Taking max-width off
   widens the SCREEN (4:3, so it grows in both directions) while `.tv-frame` keeps its
   fixed 64px height, so the picture gets wider in a mostly empty screen, and the set
   costs ~105px of the conversation it was designed to sit beside. That is a trade for
   her to see, not one to make in a commit. */
.runner-desk.is-phone { width: 100%; max-width: 264px; box-sizing: border-box; margin: 2px auto 4px; padding: 0 16px; flex: none; }
.runner-desk.is-phone .tv-side { display: none; }
.runner-desk.is-phone .tv-body { gap: 0; }
.runner-desk.is-phone .tv-ears { display: none; }
.runner-desk.is-phone .tv-body { padding: 9px; border-radius: 17px; }
.runner-desk.is-phone .tv-bezel { padding: 6px; border-radius: 13px; }
.runner-desk.is-phone .tv-dial { width: 18px; height: 18px; }
/* ⛔ THE SCREEN WAS TOO SHORT, NOT TOO NARROW — and I got it backwards once.
   Raina, 2026-10-06: "the mobile tv is stretched out". My previous pass took
   max-width off so the set used the whole 390 column; that bought legible type
   and cost the object its identity. A screen 300 wide and 64 tall is about
   4.7:1 — a letterbox slab in a wooden frame. A television is ~4:3.
   THE LESSON, because I will be tempted again: WHEN A THING HAS TO READ AS AN
   OBJECT, ITS PROPORTION IS A REQUIREMENT, NOT A VARIABLE TO SPEND. */
.runner-desk.is-phone .tv-screen { aspect-ratio: 4 / 3; min-height: 0; padding: 9px;
  display: flex; flex-direction: column; justify-content: center; }
/* A phone's vertical room is the conversation's: the strip keeps its shape as
   a row of short tiles, and the words stay at full size. */
.runner-desk.is-phone .tv-shot { aspect-ratio: auto; height: 12px; }
.runner-desk.is-phone .tv-strip { margin-bottom: 5px; }
.runner-desk.is-phone .tv-card, .runner-desk.is-phone .tv-frame { height: 64px; }
.runner-desk.is-phone .tv-card-ring { width: 56px; height: 56px; }
/* ⛔ A FOURTH LINE WAS WORTH 16px AND I LEFT IT. I had the queue count riding
   the elapsed row via `::after { content: attr(data-waiting) }`, which saves a
   line and puts real words in a pseudo-element, where a screen reader may not
   reliably reach them. "1 change waiting" is the only thing on that screen
   telling someone their words have not been picked up yet. The width is where
   the room was; this was never the place to take it from. */
@media (max-height: 560px) {
  .runner-desk.is-phone .tv-side, .runner-desk.is-phone .tv-legs { display: none; }
}

/* ── FULL SCREEN ── the shots fill the screen; the casing is dropped unless
   FULL_SCREEN_KEEPS_CASING (runner-desk.js) says otherwise. */
.desk-full     { position: fixed; inset: 0; z-index: 50; background: #07090B; color: #E8DCC8;
                 display: flex; flex-direction: column; }
.desk-full[hidden] { display: none; }
.desk-full-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid #1E232A; flex: none; }
.desk-full-bar h2 { flex: 1; margin: 0; font-size: 16px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.desk-full-back { min-height: 44px; padding: 0 16px; border-radius: 10px; border: 1px solid #3A4048; background: #14181D;
                 color: #E8DCC8; font: inherit; font-size: 15px; cursor: pointer; }
.desk-full-back:focus-visible { outline: 2px solid #E9A94E; outline-offset: 2px; }
.desk-full-body { flex: 1 1 auto; overflow: auto; padding: 20px 16px max(20px, env(safe-area-inset-bottom)); }
.desk-full-work { max-width: 1000px; margin: 0 auto; }
.desk-full-card { max-width: 560px; margin: 10vh auto 0; }
.desk-full-card .tv-card { height: 300px; }
.desk-full-card .tv-card-ring { width: 260px; height: 260px; }
.desk-full-card .tv-bars { height: 48px; }
.desk-full-card .tv-card-name { font-size: 56px; }
.desk-full-done { max-width: 1000px; margin: 0 auto; }
.desk-full-waiting { max-width: 1000px; margin: 12px auto 0; }
.tv-strip.is-full { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; margin-bottom: 16px; }
.tv-strip.is-full .tv-shot { position: relative; border-radius: 6px; }
.tv-shot-name  { position: absolute; left: 8px; right: 8px; bottom: 6px; font-size: 12px; color: #E8DCC8;
                 white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.desk-full-line { margin: 0 0 4px; font-size: 18px; font-weight: 600; }
.desk-full-meta { margin: 0; display: flex; gap: 16px; color: #B9AE9A; font-size: 15px; }
.desk-full-live, .desk-full-waiting { margin: 8px 0 0; color: #B9AE9A; font-size: 15px; }
.desk-full-live b { color: #E8DCC8; }
.desk-full.keeps-casing .desk-full-body > .tv { max-width: 1000px; margin: 0 auto; cursor: default; }
.desk-full p span, .desk-full p b { display: inline; }

/* ── THE SHELF, under the TV (js/maker/shelf.js; Sonar, design/the-shelf.md). A row of upright books on a walnut
   plank, the TV's own wood. Spines at least 64px wide, titles set HORIZONTALLY, width = how much has been added.
   Not drawn when empty. ~70px tall, so on a phone it takes no room from the conversation. */
.shelf         { margin-top: 10px; }
.shelf[hidden] { display: none; }
.shelf-row     { display: flex; align-items: flex-end; gap: 4px; height: 70px; box-sizing: border-box; padding: 6px 2px 0;
                 overflow-x: auto; overflow-y: hidden; border-bottom: 5px solid #6B4425; scrollbar-width: none; }
.shelf-row::-webkit-scrollbar { display: none; }
.shelf-spine   { flex: none; height: 58px; min-width: min-content; max-width: 170px; box-sizing: border-box; padding: 5px 6px; display: flex; align-items: flex-start;
                 border: 2px solid #6B4425; border-bottom: none; border-radius: 5px 5px 0 0; background: #E8DCC8; color: #1A1205;
                 font: 600 11px/1.25 inherit; text-align: left; cursor: pointer; overflow: hidden; }
.shelf-spine[data-kind="pictures"] { background: #D8C29C; }
.shelf-spine[data-kind="notes"]    { background: #C9D2BF; }
.shelf-spine:focus-visible { outline: 2px solid #E9A94E; outline-offset: 2px; }
/* A SPINE IS NEVER NARROWER THAN ITS WIDEST WORD. At the 64px floor an ordinary title broke mid-letter —
   "Hitchco / ck's", "Biolumin / escence". `min-width: min-content` above puts the constraint where the text is
   measured; spineWidth stays the floor and thickness still separates books above it. NOT `overflow-wrap: anywhere`:
   it makes min-content one character, which undoes the floor.
   ⛔ AND THE CAP LIVES ON THE TITLE, NOT ON THE SPINE, BECAUSE MIN-WIDTH BEATS MAX-WIDTH. The spine's own
   `max-width: 170px` does NOT hold against `min-width: min-content`: measured in chromium, a 54-character
   unbroken title (a pasted URL) made a 325px spine — five times the floor, in a row meant to hold several books.
   Capping the TITLE bounds what min-content can ask for, so the spine lands at 170. Found by Fathom on #458.
   BOTH HALVES ARE LOAD-BEARING AND FOR DIFFERENT REASONS, measured with each alone as its own control:
   max-width alone also caps at 170 but the URL is then CLIPPED HORIZONTALLY (a 309px line box in a 154px
   window, cut mid-character, no ellipsis); `break-word` makes it wrap to three lines inside the cap instead.
   `break-word` does not touch min-content, so an ordinary title is still never split: "Hitchcock's staircases"
   is 83px either way. */
.shelf-spine-title { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
                     max-width: 154px; overflow-wrap: break-word; }
.chat-row .shelf-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.chat-row .shelf-kept  { margin-top: 6px; }
/* The open book: the desk's full-screen look, under the shelf's own classes (no desk-full-* selector may match it). */
.shelf-full     { position: fixed; inset: 0; z-index: 50; background: #07090B; color: #E8DCC8; display: flex; flex-direction: column; }
.shelf-full[hidden] { display: none; }
.shelf-full-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid #1E232A; flex: none; }
.shelf-full-bar h2 { flex: 1; margin: 0; font-size: 16px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.shelf-book-back { min-height: 44px; padding: 0 16px; border-radius: 10px; border: 1px solid #3A4048; background: #14181D;
                   color: #E8DCC8; font: inherit; font-size: 15px; cursor: pointer; }
.shelf-book-back:focus-visible { outline: 2px solid #E9A94E; outline-offset: 2px; }
.shelf-full-body { flex: 1 1 auto; overflow: auto; padding: 20px 16px max(20px, env(safe-area-inset-bottom)); }
.shelf-book       { max-width: 720px; margin: 0 auto; }
.shelf-book-meta  { margin: 0 0 8px; color: #B9AE9A; font-size: 14px; }
.shelf-entry      { padding: 16px 0; border-bottom: 1px solid #1E232A; }
.shelf-entry-text { white-space: pre-wrap; font-size: 16px; line-height: 1.55; color: #E8DCC8; overflow-wrap: anywhere; }
.shelf-sources    { margin: 10px 0 0; padding-left: 22px; font-size: 13px; color: #B9AE9A; }
.shelf-sources a  { color: #E8DCC8; }
.shelf-entry-at   { margin: 8px 0 0; font-size: 12px; }
.shelf-book-actions { display: flex; flex-wrap: wrap; gap: 8px; padding: 16px 0 8px; }

/* ── INSIDE THE OPEN BOOK (js/maker/shelf.js; Sonar, design/the-shelf.md §2). Three things a book of accumulated
   research needs that one long scroll cannot give: a place to land for every addition, a way to reach the other
   books without closing this one, and a citation you can press. Same walnut-and-cream palette as the spines. */
.shelf-book-layout { display: flex; align-items: flex-start; gap: 28px; max-width: 980px; margin: 0 auto; }
.shelf-book-layout > .shelf-book { flex: 1 1 auto; min-width: 0; margin: 0; }

/* THE RAIL: one row per addition, the reading position marked. Sticky, so it stays reachable down a long book. */
.shelf-rail      { flex: none; width: 196px; position: sticky; top: 0; display: flex; flex-direction: column; gap: 2px;
                   padding: 2px 0 8px; border-right: 1px solid #1E232A; }
.shelf-rail-item { display: flex; flex-direction: column; gap: 2px; min-height: 44px; padding: 7px 12px 7px 11px; text-align: left;
                   border: 0; border-left: 3px solid transparent; background: none; color: #B9AE9A; font: inherit; cursor: pointer; }
.shelf-rail-item:hover { background: #14181D; color: #E8DCC8; }
.shelf-rail-item:focus-visible { outline: 2px solid #E9A94E; outline-offset: -2px; }
.shelf-rail-item[aria-current="true"] { border-left-color: #6B4425; background: #14181D; color: #E8DCC8; }
.shelf-rail-when  { font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; color: #8C8372; }
.shelf-rail-item[aria-current="true"] .shelf-rail-when { color: #E9A94E; }
.shelf-rail-brief { font-size: 13px; line-height: 1.35; overflow-wrap: break-word; }

/* THE SHELF, SEEN FROM INSIDE THE BOOK: the other books, one tap away. Thickness still means accumulation. */
.shelf-full-strip { flex: none; display: flex; align-items: flex-end; gap: 4px; height: 50px; padding: 0 16px;
                    border-bottom: 4px solid #6B4425; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
.shelf-full-strip[hidden] { display: none; }
.shelf-full-strip::-webkit-scrollbar { display: none; }
.shelf-strip-row   { display: flex; align-items: flex-end; gap: 4px; }
.shelf-strip-spine { flex: none; height: 40px; min-width: min-content; max-width: 170px; box-sizing: border-box; padding: 4px 5px; display: flex; align-items: flex-start;
                     border: 2px solid #6B4425; border-bottom: none; border-radius: 4px 4px 0 0; background: #E8DCC8; color: #1A1205;
                     font: 600 10px/1.2 inherit; text-align: left; cursor: pointer; overflow: hidden; opacity: 0.72; }
.shelf-strip-spine[data-kind="pictures"] { background: #D8C29C; }
.shelf-strip-spine[data-kind="notes"]    { background: #C9D2BF; }
.shelf-strip-spine:hover { opacity: 1; }
.shelf-strip-spine[aria-current="true"] { opacity: 1; height: 46px; cursor: default; }
.shelf-strip-spine:focus-visible { outline: 2px solid #E9A94E; outline-offset: 2px; }
.shelf-strip-title { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
                     max-width: 154px; overflow-wrap: break-word; }

/* A CITATION IS A CONTROL. The marker is an inline-block with a REAL box — the first pass used line-height:0 and
   vertical-align:super, which painted a number nothing could click. Its row is the 40px-tall thing a finger finds. */
.shelf-cite { display: inline-block; min-width: 17px; height: 17px; padding: 0 3px; margin: 0 1px; vertical-align: 2px;
              border: 1px solid #3A4048; border-radius: 4px; background: #14181D; color: #B9AE9A;
              font: 500 10.5px/15px ui-monospace, SFMono-Regular, Menlo, monospace; text-align: center; cursor: pointer; }
.shelf-cite:hover { border-color: #6B4425; color: #E8DCC8; }
.shelf-cite:focus-visible { outline: 2px solid #E9A94E; outline-offset: 1px; }
.shelf-cite.is-lit { background: #E9A94E; border-color: #E9A94E; color: #1A1205; }
.shelf-sources li  { box-sizing: border-box; min-height: 40px; padding: 9px 6px; margin-left: -6px; border-radius: 6px; cursor: pointer; }
.shelf-sources li.is-lit { background: #14181D; box-shadow: inset 3px 0 0 #E9A94E; color: #E8DCC8; }

/* ON A PHONE the rail lies down above the entries rather than disappearing: a book she cannot move around in on the
   device she reads on is the defect the rail exists to fix. */
@media (max-width: 760px) {
  .shelf-book-layout { flex-direction: column; gap: 12px; }
  .shelf-rail      { position: static; width: auto; align-self: stretch; flex-direction: row; gap: 6px; overflow-x: auto;
                     padding: 0 0 8px; border-right: 0; border-bottom: 1px solid #1E232A; scrollbar-width: none; }
  .shelf-rail::-webkit-scrollbar { display: none; }
  .shelf-rail-item { flex: none; max-width: 180px; border-left: 0; border-bottom: 3px solid transparent; border-radius: 8px 8px 0 0; }
  .shelf-rail-item[aria-current="true"] { border-left-color: transparent; border-bottom-color: #6B4425; }
  .shelf-rail-brief { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
