/* Book Keeper — a dark ground for artwork, and four colours that mean
 * something.
 *
 * The library is a hundred and fifty pieces of cover art, and they are the
 * content. The interface is therefore built to disappear behind them: a
 * neutral ground so no jacket is tinted by the page it sits on, and chroma
 * held back everywhere the art is not.
 *
 * The status palette is the load-bearing part: agreed / one-source /
 * contested / absent are the four answers the merge can give, and they are the
 * only place a *control* is allowed to carry meaning in colour. The one other
 * place colour means anything is the ground of a book's own page, which is
 * washed with a hue taken from its jacket — see `--book-tint`. Everything
 * else is ground, ink and one rule width.
 *
 * Three scales are declared here and nothing outside them is allowed:
 * six type sizes, one 4px spacing step, and two control heights. Sizes like
 * 12.5px and paddings like 9px are how a stylesheet stops being a system, so
 * a new value belongs in :root or nowhere. Vertical rhythm in particular is
 * set here rather than by inline styles at the JS call sites.
 *
 * There are two kinds of class here and the difference matters:
 *
 *   - A *component* is styled on its own — `.panel`, `.destination`,
 *     `.queue-row` — so its name has to be unmistakable, because any element
 *     anywhere carrying that word gets these rules.
 *   - A *part* is only ever styled under its component — `.source .name`,
 *     `.upload-row .state`. Parts may be ordinary words precisely because
 *     they are scoped.
 *
 * A part name promoted to a global rule is the failure to avoid. `.bar` was
 * one: a three-pixel upload progress bar, styled unscoped, silently claimed
 * the sticky filing bar the moment it was given the class `destination bar`,
 * which rendered it 24px tall and pale instead of dark. It is `.progress-bar`
 * now, and styles_test.mjs refuses any new global rule named after a common
 * word. */

:root {
  /* What the browser paints for itself: the scrollbars, the dropdown a select
     opens, the checkboxes, the caret. Told once here, or the parts of the
     interface this stylesheet does not draw stay light against everything it
     does. */
  color-scheme: dark;

  /* Ground for artwork. Neutral and a shade cool, so a warm jacket and a cold
     one both read as themselves rather than as a tint of the page — which is
     the whole reason this is the ground the covers sit on. Not black: a jacket
     with a black field needs an edge to sit against. */
  --paper: #121316;
  /* Recessed, for what the machine says back: a path, a filing destination. */
  --well: #0c0d0f;
  --surface: #191b1f;
  --ink: #e9eaec;
  /* Text on a control filled with --ink. The fill inverts with the ground and
     the label on it has to invert too. */
  --on-ink: #121316;
  --ink-strong: #ffffff;
  --mute: #a4a7ae;
  --faint: #8e9199;
  --pale: #84878f;
  --line: #2a2d34;
  --line-strong: #3d424b;
  --hover: #20232a;
  --active: #272b33;
  --hairline: #212429;
  --rule: #24272d;

  /* Merge statuses. Held well back in chroma: these mark a field in a form,
     not a warning light, and beside cover art anything saturated competes with
     the one thing on the page worth looking at. */
  --agreed: #83b795;
  --single: #8aa8ca;
  --contested: #d9917c;
  --absent: #8a8d95;
  --danger: #d9817c;

  /* Tints, used only to back their own status. */
  --agreed-bg: #17211b;
  --agreed-edge: #2e4436;
  --single-bg: #171c24;
  --single-edge: #2d3b4d;
  --contested-bg: #241a16;
  --contested-edge: #4a332b;
  --contested-row: #1c1512;
  --contested-field: #6d463b;
  --danger-bg: #231816;
  --on-dark-warn: #f0a89b;

  /* Format marks. Two families told apart by hue, backed by their own tint. */
  --mark-plain-bg: #1b242a;
  --mark-plain-ink: #9dc0d0;
  --mark-audio-bg: #221d2b;
  --mark-audio-ink: #b3a3c8;

  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --sans: Archivo, system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* Type. Six whole-pixel sizes: a title, a heading, a lead, the body, a
     label and a mark. Half-pixel steps are what let the old scale grow to
     fifteen sizes nobody could tell apart. */
  --t-title: 28px;
  --t-head: 20px;
  --t-lead: 16px;
  --t-body: 14px;
  --t-label: 13px;
  --t-mark: 11px;

  /* Space. One 4px step, used for every padding, gap and margin. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;

  /* One radius for every box in the interface. Cover art is the deliberate
     exception at 2px: it is a physical object, not a control. */
  --radius: 3px;
  --h-control: 34px;
  --h-small: 30px;

  /* Measures. Every view is held to what its content can use: the shelf takes
     the widest, being cards rather than prose; `--w-wide` is a three-column
     screen's, summed below; and a form measure and a field cap keep text from
     sprawling when a column is wider than a line wants to be. */
  --w-page: 1020px;
  --w-rail: 400px;
  /* Summed rather than chosen, so the rail ends where the blocks above it
     end: slack between the two sits at the end of the row. */
  --w-wide: calc(var(--w-form) + var(--s4) + var(--w-rail) + var(--s6) * 2);
  --w-shelf: 1600px;
  --w-form: 720px;
  --w-field: 520px;
  --w-short: 132px;

  /* Fixed columns. Named because they repeat: the label column is set in
     three places and has to stay one width, or entering edit mode moves every
     value sideways. */
  --w-label: 96px;
  --w-rowhead: 104px;
  --w-state: 76px;
  --w-key: 62px;
  --w-thumb: 172px;
  /* How tall a cover is drawn, wherever one is drawn at size. One value, so a
     book is the same size on the page that identifies it as in the row it was
     picked from. Width follows the art; only this is fixed. */
  --shelf-cover-h: 200px;
  /* A ceiling for the widest shape the aspect clamp admits. */
  --w-jacket: 300px;
  /* The measure prose is read at, which is narrower than any column here. */
  --w-blurb: 62ch;
  /* Tracks hang under the format badge rather than under the row, so the list
     reads as belonging to it. Badge plus its gap, measured once. */
  --indent-tracks: 78px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: var(--t-body)/1.5 var(--sans);
}

a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }
a:hover { color: var(--contested); }
/* Form controls do not inherit letter-spacing from the UA stylesheet, which
   left every label and its own input on different tracking. */
input, button, textarea, select { font: inherit; color: inherit; letter-spacing: inherit; }
input:focus, textarea:focus { outline: none; }
::selection { background: #33465a; }
::placeholder { color: var(--pale); }

.app { display: flex; min-height: 100vh; }
.grow { flex: 1; min-width: 0; }

/* ── Sidebar ─────────────────────────────────────────────────────────── */

.sidebar {
  width: 224px;
  flex: none;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  padding: var(--s5) var(--s3);
}

.wordmark {
  padding: var(--s1) var(--s3) var(--s5);
  font-size: var(--t-lead);
  font-weight: 600;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: var(--s3);
  height: var(--h-control);
  padding: 0 var(--s3);
  border-radius: var(--radius);
  color: var(--mute);
  text-decoration: none;
  font-size: var(--t-label);
}
.nav-link:hover { background: var(--hover); color: var(--ink); }
.nav-link[aria-current='page'] { background: var(--active); color: var(--ink); font-weight: 500; }
.nav-link span:first-child { flex: 1; }
/* A count that has not loaded shows a middot, never a zero — "nothing here"
   and "not yet known" must not look the same. */
.nav-link .count { font-family: var(--mono); font-size: var(--t-mark); color: var(--faint); }
.nav-link[aria-current='page'] .count,
.nav-link:hover .count { color: var(--mute); }

/* The book's own colour, borrowed from its jacket and set here by the shell.
   A wash across the top of the page rather than a fill: it says which book
   this is before a word is read, and leaves every surface laid over it alone.
   On this element rather than on the view, which is capped to a reading
   measure — a backdrop drawn there stops mid-window with an edge down each
   side. Unset, which is every other screen and every cover with no colour in
   it, the gradient has nothing to draw and the page is the ordinary ground. */
.app-content {
  flex: 1;
  min-width: 0;
  background: radial-gradient(140% 340px at 50% 0, var(--book-tint, transparent), transparent 72%)
    no-repeat;
}
/* A view is as wide as its content can use, and centred past that. The shelf
   gets the widest measure, being a wrapping row of cards rather than anything
   anyone reads along. */
.view { padding: var(--s6) var(--s6) var(--s7); margin-inline: auto; }
.view-narrow { max-width: var(--w-page); }
.view-wide { max-width: var(--w-wide); }
.view-shelf { max-width: var(--w-shelf); }

/* Runs, and the two gaps that separate them.
   The gap between runs has to beat the gap between rows inside one, or the end
   of a run reads as one more row of it — and a run whose head was scrolled past
   is then indistinguishable from the one above. A head sits closer to its own
   covers than to anything else, which is what binds it to them.
   Declared on the containers rather than as margins on the parts, so the two
   gaps are stated once and a run carries none of its own spacing. */
.runs { display: flex; flex-direction: column; gap: var(--s7); }
.run { display: flex; flex-direction: column; gap: var(--s4); }

/* A rule under the name, because a block of covers is not a box and has no
   edge of its own — this is what says where the run above it ended. */
.run-head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
  font-size: var(--t-lead);
  font-weight: 600;
}
.run-name { color: var(--ink); text-decoration: none; }
/* A run with no page of its own — a letter, or the books a sort could not
   place. Named, but not as a thing you can open. */
.run-name.plain { color: var(--mute); font-weight: 500; }
.run-name:hover { color: var(--ink); text-decoration: underline; }
.run-count { font-family: var(--mono); font-size: var(--t-mark); color: var(--faint); font-weight: 400; }

/* ── Type ────────────────────────────────────────────────────────────── */

/* Blocks carry no margins of their own: spacing between them is the stack's
   job, so a paragraph cannot quietly add 14px nobody asked for. */
p { margin: 0; }
h1 { font-size: var(--t-title); font-weight: 600; letter-spacing: -0.02em; margin: 0; line-height: 1.15; text-wrap: pretty; }
h2 { font-size: var(--t-lead); font-weight: 600; margin: 0; line-height: 1.3; }
.lede { color: var(--mute); font-size: var(--t-label); margin: var(--s1) 0 0; }
.hint { color: var(--mute); font-size: var(--t-label); }
.meta { font-family: var(--mono); font-size: var(--t-mark); color: var(--faint); }
.mono { font-family: var(--mono); }
.warn { color: var(--contested); font-size: var(--t-label); }
.loading, .empty { color: var(--mute); }

.head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s5); }
.back { border: 0; background: none; padding: 0; cursor: pointer; font-size: var(--t-label); color: var(--mute); text-decoration: none; }
.back:hover { color: var(--ink); }

/* Every view opens the same way: where you came from, what this is, and one
   line saying how much of it there is. */
.view > .head { margin-bottom: var(--s6); }
.back + h1 { display: block; margin-top: var(--s2); }
.head .meta { margin-top: var(--s1); }

/* ── Controls ────────────────────────────────────────────────────────── */

button {
  height: var(--h-control);
  padding: 0 var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  font-size: var(--t-label);
  white-space: nowrap;
}
button:hover { border-color: var(--line-strong); }
button.primary { background: var(--ink); border-color: var(--ink); color: var(--on-ink); font-weight: 500; }
button.primary:hover { background: var(--ink-strong); border-color: var(--ink-strong); }
button.danger { background: var(--danger-bg); border-color: var(--contested-field); color: var(--danger); }
button.danger:hover { border-color: var(--danger); }
button.armed { background: var(--danger); border-color: var(--danger); color: var(--on-ink); }
button.link { border: 0; background: none; padding: 0; height: auto; color: var(--faint); font-size: var(--t-label); }
button.link:hover { color: var(--ink); }
button[disabled] { cursor: not-allowed; opacity: 0.55; }
/* Working, not broken. A control that reaches the network holds this until
   the answer comes back. */
button.busy { cursor: progress; opacity: 0.55; pointer-events: none; }
button.busy::after { content: '…'; }
button.small { height: var(--h-small); padding: 0 var(--s2); }
button.quiet { color: var(--mute); }

.actions { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }

input, textarea, select {
  width: 100%;
  height: var(--h-control);
  padding: 0 var(--s2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: var(--t-body);
}
textarea { height: auto; padding: var(--s2); resize: vertical; line-height: 1.5; }
input:focus, textarea:focus, select:focus { border-color: var(--ink); }
input[type='radio'], input[type='checkbox'], input[type='file'] { width: auto; height: auto; }
input.short { max-width: var(--w-short); }

/* Wraps, because the row cannot shrink: the box has a set width and the pills
   and selects size to their labels. */
.filters { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }
.pills { display: flex; flex-wrap: wrap; gap: var(--s1); }
.pill { color: var(--mute); }
.pill[aria-pressed='true'] { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
/* Wide enough for its own placeholder and no wider: the three controls share
   one line, and the box that can give width up without losing a reading is
   this one — a search field is sized by what is typed into it, not by what it
   can hold. */
.search { width: 180px; max-width: 100%; }
/* A select is full-width by default, which is right in a form and wrong in a
   row: as a flex child that width is a claim on the whole line, and everything
   sharing the row is wrapped past it. Sized to its longest option instead. */
.filters select { width: auto; }
/* The label and its control are one thing and wrap as one. */
.filters .sort { display: flex; align-items: center; gap: var(--s2); }
.filters .sort-label { font-size: var(--t-label); color: var(--mute); white-space: nowrap; }

/* ── Panels ──────────────────────────────────────────────────────────────
   One panel, one header, one body. The header names the panel and may carry
   marks on the right; the body owns the padding, so no call site has to
   invent its own. Panels whose rows draw their own separators (a queue, a
   file list, a comparison) leave the body off and let the rows fill the box. */

.panel { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
.panel-head {
  /* So a progress hairline can sit on its bottom edge without taking height. */
  position: relative;
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-size: var(--t-label);
  color: var(--mute);
}
.panel-title { font-size: var(--t-lead); font-weight: 600; color: var(--ink); line-height: 1.3; }
/* One line, so a head does not grow a second one while a reading sits in it
   and shrink back when it goes — a movement to report movement. The title
   yields instead: it is one known word per panel and never the part in
   doubt. */
.panel-head .aside { color: var(--faint); white-space: nowrap; }
.panel-head .panel-title { flex: 0 1 auto; min-width: 0; }
/* The rail's head reserves the height its Stop button needs whether or not the
   button is there. A control is taller than a line of text, so the head grew
   when a search started and shrank when it ended — the last thing on the page
   still moving for a reason that was only ever about reporting. */
.rail .panel-head { min-height: calc(var(--h-control) + var(--s3) * 2); }
.panel-body { padding: var(--s4); }

/* One gap between sibling sections, and the same one across a row of them.
   The page head keeps double, so the title is separated from the content
   rather than being one more section in the list. */
.stack { display: flex; flex-direction: column; gap: var(--s4); }
.view > .panel, .view > .stack, .view > .groups, .view > .runs, .view > .dropzone, .view > .settings { margin-top: 0; }

.blank {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--s7);
  text-align: center;
}
.blank strong { display: block; font-size: var(--t-lead); font-weight: 500; }
.blank span { color: var(--mute); font-size: var(--t-label); }
/* An empty state that has somewhere to send you says so with the same control
   the lists use, under the sentence explaining it. */
.blank .actions { justify-content: center; margin-top: var(--s5); }

.toast {
  position: sticky;
  top: 0;
  z-index: 20;
  margin: var(--s4) var(--s6) 0;
  display: flex;
  gap: var(--s3);
  align-items: flex-start;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--agreed);
  border-radius: var(--radius);
  padding: var(--s3) var(--s4);
}
.toast.bad { border-left-color: var(--danger); }
.toast .text { font-weight: 500; }
.toast .detail { font-family: var(--mono); font-size: var(--t-mark); color: var(--mute); margin-top: var(--s1); word-break: break-word; }
.toast button { border: 0; background: none; color: var(--faint); font-size: var(--t-lead); height: auto; padding: 0 var(--s1); }

.notice {
  display: flex;
  gap: var(--s3);
  align-items: center;
  border: 1px solid var(--line);
  border-left: 3px solid var(--contested);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--s3) var(--s4);
  font-size: var(--t-label);
}

/* ── Status vocabulary ───────────────────────────────────────────────────
   Two shapes, and only two. A *status* is a dot and a word: the answer the
   merge gave, or the state a book is in. A *mark* is a short mono credit —
   who said it, how many, how big. Everything that used to be a tag, a flag or
   a badge is now one of these. */

.status { display: inline-flex; align-items: center; gap: var(--s2); font-size: var(--t-label); white-space: nowrap; }
.dot { width: 7px; height: 7px; border-radius: 50%; flex: none; border: 1px solid transparent; }
.status-agreed { color: var(--agreed); } .status-agreed .dot { background: var(--agreed); }
.status-single { color: var(--single); } .status-single .dot { background: var(--single); }
.status-contested { color: var(--contested); } .status-contested .dot { background: var(--contested); }
/* The record holding a value the sources contradict. It asks for a decision
   the way a contested field does, so it looks like one. */
.status-differs { color: var(--contested); } .status-differs .dot { background: var(--contested); }
.status-absent { color: var(--absent); } .status-absent .dot { background: transparent; border-color: var(--absent); }

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  font-size: var(--t-label);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1px var(--s2);
  white-space: nowrap;
  color: var(--mute);
}
.chip-agreed { color: var(--agreed); background: var(--agreed-bg); border-color: var(--agreed-edge); }
.chip-single { color: var(--single); background: var(--single-bg); border-color: var(--single-edge); }
.chip-contested { color: var(--contested); background: var(--contested-bg); border-color: var(--contested-edge); }

.mark {
  font-family: var(--mono);
  font-size: var(--t-mark);
  padding: 1px var(--s1);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--mute);
  white-space: nowrap;
}
/* Only in the matrix, where a mark stands for a source that was asked and had
   nothing to say. Everywhere else a mark is a fact and is legible. */
.mark-off { border-color: var(--line); color: var(--absent); }
.mark.match { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
.mark-agreed { background: var(--agreed-bg); border-color: var(--agreed-edge); color: var(--agreed); }
.mark-plain { background: var(--mark-plain-bg); border-color: transparent; color: var(--mark-plain-ink); }
.mark-audio { background: var(--mark-audio-bg); border-color: transparent; color: var(--mark-audio-ink); }

/* ── The destination ─────────────────────────────────────────────────────
   Where a book will live on disk. The same block whether it is being filed
   for the first time or already has a directory, because it is the same
   fact — only the label and whether it sticks to the bottom differ. */

.destination {
  background: var(--well);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s3) var(--s4);
  display: flex;
  align-items: center;
  gap: var(--s4);
}
.destination .label { font-size: var(--t-label); color: var(--faint); }
.destination .path { font-family: var(--mono); font-size: var(--t-body); margin-top: 2px; word-break: break-word; }
.destination .path .missing { color: var(--on-dark-warn); }
.destination .note { font-size: var(--t-label); color: var(--on-dark-warn); margin-top: 2px; }
.destination button { background: none; border-color: var(--line-strong); color: var(--ink); }
.destination button:hover { border-color: var(--ink); }
.destination button.primary { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
.destination button.primary[disabled] { background: none; border-color: var(--line); color: var(--pale); }

/* ── Library: the shelf ──────────────────────────────────────────────── */

/* Fixed height, width from the art — the same rule the cover picker uses, and
   the one holodeck's tile grid is built on. A row of covers then shares a
   baseline the way books on a shelf do, and a square audiobook cover is
   legible as square instead of being fitted into a novel's proportions.
   Flex rather than grid: equal-width columns are exactly what this is not.
   The height is the same at every width: a cover that grows with the window is
   a book that is one size on the laptop and another on the monitor. What a
   wider window buys is more books in the row. */
.shelf {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--s6) var(--s5);
}
/* Bounded by the row as well as by the art. A wide enough cover asks for more
   width than a narrow window has, and the row is the one bound that cannot
   give. */
.shelf .book-card {
  width: calc(var(--shelf-cover-h) * var(--card-aspect, 0.6667));
  max-width: 100%;
  flex: 0 0 auto;
}
/* The height follows the width rather than being set beside it, so the one
   card that hits that bound comes down whole. Given both, the box stops
   matching the art and `contain` letterboxes it inside a border drawn round
   the box — which is the cropping-and-letterboxing this shelf exists not to
   do. Unclamped the two are identical: the card is already the height times
   the aspect. */
.shelf .cover { width: 100%; height: auto; aspect-ratio: var(--card-aspect, 0.6667); }

.book-card { display: flex; flex-direction: column; gap: var(--s2); text-align: left; text-decoration: none; color: inherit; }
.book-card:hover { opacity: 0.86; color: inherit; }
/* The first line under a cover is always the title, whether or not the cover
   is real art. It read as the author on stand-in covers, which put a name and
   a title in the same place and left no way to tell which was which. */
/* One line each for the title and the author, so every card is the same
   height and nothing is ever spaced apart to make room. What does not fit is
   on the tooltip. */
.book-card .card-title {
  font-size: var(--t-body);
  font-weight: 600;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.book-card .byline {
  font-size: var(--t-label);
  font-weight: 400;
  color: var(--mute);
  line-height: 1.3;
  margin-top: 2px;
  min-height: 1.3em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Everything on a card starts at its left edge. Cards are deliberately
   unequal in width, so anything set against the right edge lines up with
   nothing on the card beside it. */
.book-card .sub { display: flex; align-items: center; gap: var(--s2); margin-top: var(--s1); font-size: var(--t-mark); color: var(--faint); }

/* A book has cover art only if one was picked when it was filed. Without it,
   the cover is drawn from what is known: the title, the year, and whether it
   is read aloud. The shadow, rather than a hairline border, is what makes it
   read as an object on a shelf instead of a box in a form. */
.cover {
  position: relative;
  aspect-ratio: 2 / 3;
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.07), 0 2px 4px rgba(0, 0, 0, 0.45), 0 10px 24px rgba(0, 0, 0, 0.4);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: var(--s3) var(--s3) var(--s3) var(--s5);
  overflow: hidden;
  background: #31382f;
  color: #f4f2ec;
}
.cover.audio { background: #2b3340; }
.cover .spine { position: absolute; left: 0; top: 0; bottom: 0; width: 7px; background: #232820; }
.cover.audio .spine { background: #1d232d; }
.cover .cover-title { font-size: var(--t-head); font-weight: 600; letter-spacing: -0.015em; line-height: 1.2; text-wrap: pretty; }
.cover .cover-title.long { font-size: var(--t-lead); }
.cover .cover-note { font-family: var(--mono); font-size: var(--t-mark); color: rgba(244, 242, 236, 0.55); margin-top: var(--s2); }

/* Real art keeps its own proportions: the box is set to the cover's aspect
   inline, so there is nothing left to crop. Cards are therefore as wide as
   their art, which is what makes a shelf of them read like books rather than
   a grid of equal boxes. */
.cover.art { padding: 0; background: var(--line); aspect-ratio: auto; }
/* A hairline at the art's own edge: the shadow alone is enough for a dark
   jacket and nothing at all for a pale one against this paper. An outline
   rather than a border, so it takes no space and cannot change the box the
   art was measured into. */
.cover .cover-art {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  outline: 1px solid rgba(255, 255, 255, 0.08);
  outline-offset: -1px;
}

/* The pager is the only way past the first page of a shelf, so it is given a
   rule and the middle of the page rather than being left to trail off the last
   row. */
.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  margin-top: var(--s7);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
}
.pager-pages { display: flex; align-items: center; gap: var(--s1); }
/* Given the shape a control has here rather than a link's underline. */
.pager-step,
.pager-page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--h-control);
  padding: 0 var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink);
  text-decoration: none;
  font-size: var(--t-label);
  white-space: nowrap;
}
.pager-step:not(.spent):hover,
.pager-page:hover { border-color: var(--line-strong); color: var(--ink); }
/* Square, so a row of numbers reads as a row of numbers rather than as words
   of different lengths. */
.pager-page { min-width: var(--h-control); padding: 0 var(--s2); }
.pager-page[aria-current='page'] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--on-ink);
  font-weight: 500;
}
/* A step with nowhere left to go: kept so the row does not close up around it,
   but offering none of what a live one does. */
.pager-step.spent { color: var(--pale); cursor: default; }
.pager-gap { color: var(--faint); padding: 0 var(--s1); }

/* ── Choosing a cover ─────────────────────────────────────────────────────
   Sources never agree on a cover, so these are compared rather than
   merged. The tiles are deliberately large: the whole point is to see which
   scan is sharp, and a thumbnail of a thumbnail decides nothing.

   A row of fixed-height tiles whose widths follow their own art, rather than
   a grid of equal boxes each art is fitted into. A square audiobook cover and
   a 2:3 novel then appear as the shapes they actually are — which is the
   difference being judged. */
/* One row, always. Wrapped, the panel was 98px while it waited and 816px once
   nine covers had landed, which shoved the fields 682px down the page at the
   moment they became worth reading. A row is the height of one tile whatever
   it holds, so the panel that appears is the size of the panel that fills, and
   nothing below it moves. It is also the rule the shelf and the picker already
   follow: fix the height, let the width vary.
   More than fits scrolls sideways, and `Compare` is there for seeing them all
   at once. */
.cover-grid {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  gap: var(--s3);
  overflow-x: auto;
  /* Room for the selected cover's ring, which is drawn outside its edge and
     would otherwise be clipped away on the first and last tiles. */
  padding: 2px;
}
/* The art is the tile. A card around it — its own surface, border, radius and
   padding — drew a second rectangle a few pixels outside every cover, so a row
   of them read as a row of boxes that happen to contain pictures. Nine covers
   of nine different shapes is already busy enough. What is left is the picture,
   its credit and its measurements, and the state lives on the art's own edge. */
.cover-option {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  text-align: left;
  height: auto;
  font: inherit;
}
.cover-option:hover .cover-thumb { border-color: var(--pale); }
/* Outside the edge rather than inset: a ring drawn over the art covers the
   millimetre of it nearest the border, on every cover in the row that is not
   the one being judged. */
.cover-option.selected .cover-thumb { border-color: var(--ink); box-shadow: 0 0 0 2px var(--ink); }
.cover-option.selected .cover-name { color: var(--ink); font-weight: 600; }
/* Height is fixed and width follows from the aspect-ratio set inline, so
   nothing is cropped and nothing is letterboxed: the tile is the art's own
   shape. object-fit stays `fill` — with the box already at the source's
   proportions there is nothing to fit, and a mismatch would mean the server's
   measurement was wrong, which is worth seeing rather than hiding. */
.cover-option .cover-thumb {
  height: var(--w-thumb);
  width: auto;
  /* A placeholder is a div, and a div in a stretching column takes the width of
     the longest caption under it rather than the one its aspect asks for. */
  align-self: flex-start;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 2px;
  display: block;
}
.cover-option .cover-thumb.empty { border-style: dashed; background: transparent; }
/* A cover that cannot be drawn: nothing was fetched, or the bytes are in a
   format no browser renders. It holds the box at the shape the cover was
   measured at, so the row neither shifts nor hands the caption underneath — the
   one thing that says what happened — to a broken-image mark. */
.cover-option .cover-thumb.blank {
  border-style: dashed;
  background: repeating-linear-gradient(
    -45deg,
    transparent 0 6px,
    rgba(255, 255, 255, 0.05) 6px 12px
  );
}
/* The cover already on disk. It is an option like the others so that going
   back to it is one click, and it reads as one — only the label differs. */
.cover-option.current .cover-thumb { border-color: var(--line-strong); }
/* The panel while the art is still being fetched, so the picker reports the
   wait rather than appearing from nowhere once it is over. */
/* The same height a row of tiles will take, so the panel does not grow when
   the art arrives: the thumb itself, the two labels under it, the gaps between
   them and the tile's own padding. Built from the tile's parts rather than
   from a number, so it follows if any of them changes. */
.cover-waiting {
  color: var(--mute);
  font-size: var(--t-label);
  min-height: calc(
    var(--w-thumb) + var(--s1) * 2 + var(--t-label) + var(--t-mark) + var(--s4)
  );
}

/* The crossfade between one rebuilt view and the next. Short, because it sits
   between a click and its result: long enough to read as a change rather than
   a flash, short enough that nothing waits for it. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 120ms;
}
/* Elements given an identity by `moving()` slide and grow between where they
   were and where they now are, rather than crossfading in place. Slightly
   longer than the crossfade because movement needs to be followed by the eye,
   and eased out so it settles rather than stopping dead. */
::view-transition-group(*) {
  animation-duration: 220ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
/* The nav does not change between frames, so it should not be part of what
   fades — a sidebar that pulses on every answer is the jumpiness, not the
   cure. */
.sidebar { view-transition-name: nav; }
::view-transition-group(nav) { animation: none; }

/* How far a lookup has got, drawn along the bottom edge of the head it belongs
   to. Named `lookup` rather than `progress` because `.bar` is already the
   upload's, and a second global word for the same shape is how that one quietly
   took over the filing bar.
   Out of flow on purpose: laid out, it made the head taller while a search ran
   and shorter when it stopped — a movement to report movement. The reading
   goes in the aside, where a panel already says how much of it there is. */
.lookup-track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  overflow: hidden;
}
/* The bar slides between answers rather than snapping: with four sources
   there are only four steps, and a step that jumps reads as a redraw rather
   than as progress. The movement comes from its `view-transition-name`, not
   from a CSS transition — the element is rebuilt on every answer, so a fresh
   one starts at its new width with nothing to move from. */
.lookup-fill {
  height: 100%;
  background: var(--ink);
}

/* Where a panel head sends you on — the rest of a list it is only showing the
   start of. Right-aligned like the head's other control, and a link rather than
   a button because it goes somewhere. */
.panel-head .panel-link {
  margin-left: auto;
  font-size: var(--t-label);
  color: var(--mute);
}
.panel-head .panel-link:hover { color: var(--ink); }

/* Full Size sits in the panel head rather than on each tile: it opens all of
   them at once, which is what comparing two scans of the same art needs. */
.cover-full {
  margin-left: auto;
  font-size: var(--t-label);
}

/* A sheet over the whole window, scrolling in both directions. Nothing is
   scaled down — a scan too big for the window is the fact being judged. */
.cover-zoom {
  position: fixed;
  inset: 0;
  z-index: 30;
  overflow: auto;
  background: var(--well);
  padding: var(--s5);
  /* Dragged rather than scrollbarred: at actual size the sheet is several
     windows wide, and a selection started by the drag would fight it. */
  cursor: grab;
  user-select: none;
}
.cover-zoom:active { cursor: grabbing; }
.cover-zoom-strip {
  display: flex;
  align-items: flex-start;
  gap: var(--s5);
  width: max-content;
  /* Full height so a fitted cover has the window to fill. */
  height: 100%;
}
.cover-zoom-item {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin: 0;
  min-height: 0;
}
.cover-zoom-art {
  display: block;
  background: var(--paper);
  border: 1px solid var(--line-strong);
}
/* Fitted: the window bounds the art and its own aspect-ratio, set inline,
   keeps the shape. Both bounds are needed — a tall cover is limited by the
   height and a wide one by the width, and a square audiobook cover by
   whichever is shorter.
   It bounds and never fills. Scaling a 309px scan up to the window would make
   it look like art it is not, and telling a real scan from an upscaled
   thumbnail is the judgement this view exists for — so a cover smaller than
   the window is shown small, which is itself the answer. */
.cover-zoom.fit .cover-zoom-item { height: 100%; }
.cover-zoom.fit .cover-zoom-art {
  max-height: 100%;
  max-width: 100%;
  height: auto;
  width: auto;
  min-height: 0;
}
/* Actual size: the file's own pixels, whatever they come to. */
.cover-zoom.actual .cover-zoom-art { max-width: none; }

/* Fixed to the window rather than the sheet, so panning does not carry it off
   the screen. */
.cover-zoom-bar {
  position: fixed;
  top: var(--s5);
  right: var(--s5);
  z-index: 1;
  display: flex;
  gap: var(--s1);
  cursor: auto;
}
.cover-zoom-mode,
.cover-zoom-close { font-size: var(--t-label); }
.cover-zoom-mode[aria-pressed='true'] { border-color: var(--ink); background: var(--surface); }
.cover-zoom-caption { display: flex; flex-direction: column; gap: 0; }
.cover-zoom-name { font-size: var(--t-label); color: var(--ink); }
.cover-zoom-mark {
  font-family: var(--mono);
  font-size: var(--t-mark);
  color: var(--faint);
}
/* A caption sits under art of unknown width, so it is bounded by the widest a
   tile ever gets rather than by its own tile: clamped to the picture above it,
   a narrow cover truncated the measurement that is the whole reason the caption
   is there — `1800 × 3228 · 3.5 MB` came out as `1800 × 3228 ·…`. The reading
   is worth more than the ragged edge. */
.cover-option .cover-source,
.cover-option .cover-detail {
  max-width: var(--w-thumb);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cover-option .cover-source {
  font-size: var(--t-label);
  color: var(--ink);
  display: flex;
  align-items: baseline;
  gap: var(--s2);
}
.cover-option .cover-name { overflow: hidden; text-overflow: ellipsis; }
.cover-option .cover-detail { font-family: var(--mono); font-size: var(--t-mark); color: var(--mute); }
.cover-option.broken .cover-detail { color: var(--contested); }
/* Shown so the absence is explained, but not a choice: there are no bytes
   behind it to file. */
.cover-option.unavailable { cursor: default; }
.cover-option.unavailable:hover .cover-thumb { border-color: var(--line); }
/* Beside the credit, not over the picture. It is a mark — a reading about the
   cover — so it is set like every other one rather than as a filled badge. */
.cover-option .cover-flag {
  flex: 0 0 auto;
  color: var(--agreed);
  font-family: var(--mono);
  font-size: var(--t-mark);
}

/* ── Upload ──────────────────────────────────────────────────────────── */

.dropzone {
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--s7);
  text-align: center;
}
.dropzone.over { border-color: var(--ink); background: var(--hover); }
.dropzone strong { display: block; font-size: var(--t-lead); font-weight: 500; }
.dropzone .hint { margin-top: var(--s1); }
.dropzone input[type='file'] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.file-button {
  display: inline-block;
  margin-top: var(--s5);
  height: var(--h-control);
  line-height: 32px;
  padding: 0 var(--s4);
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  background: var(--ink);
  color: var(--on-ink);
  font-size: var(--t-label);
  font-weight: 500;
  cursor: pointer;
  text-decoration: none;
}
.file-button:hover { background: var(--ink-strong); color: var(--on-ink); }
/* Picking a folder is the second way in, not the main one: a book on its own is
   still a file. */
.file-button.quiet { background: none; border-color: var(--line-strong); color: var(--ink); }
.file-button.quiet:hover { background: var(--surface); color: var(--ink); border-color: var(--ink); }
.dropzone .actions { justify-content: center; }
input[type='file']:focus + .file-button { outline: 2px solid var(--ink); outline-offset: 2px; }

.upload-row { padding: var(--s3) var(--s4); border-bottom: 1px solid var(--hairline); display: flex; gap: var(--s4); align-items: center; }
.upload-row:last-child { border-bottom: 0; }
.upload-row .state { width: var(--w-state); flex: none; font-size: var(--t-label); color: var(--faint); }
.upload-row .state.ok { color: var(--agreed); }
.upload-row .state.bad { color: var(--danger); }
.upload-row .name { font-family: var(--mono); font-size: var(--t-label); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.upload-row .note { font-size: var(--t-label); color: var(--mute); margin-top: 2px; }
.upload-row .note.bad { color: var(--danger); }
.upload-row .size { font-family: var(--mono); font-size: var(--t-mark); color: var(--faint); white-space: nowrap; }
.progress-bar { height: 3px; border-radius: var(--radius); background: var(--rule); margin-top: var(--s2); overflow: hidden; }
.progress-bar > div { height: 100%; background: var(--ink); transition: width 0.2s; }

/* ── Queue ───────────────────────────────────────────────────────────── */

.queue-row {
  display: flex;
  gap: var(--s5);
  align-items: center;
  width: 100%;
  padding: var(--s4);
  border: 0;
  border-bottom: 1px solid var(--hairline);
  border-left: 3px solid var(--line);
  background: none;
  text-align: left;
  text-decoration: none;
  color: inherit;
  height: auto;
  white-space: normal;
  border-radius: 0;
}
.queue-row:last-child { border-bottom: 0; }
.queue-row:hover { background: var(--hover); color: inherit; border-color: var(--line); }
.queue-row.contested { border-left-color: var(--contested); }
.queue-row.agreed { border-left-color: var(--agreed); }
.queue-row .title { display: block; font-size: var(--t-lead); font-weight: 600; color: inherit; text-decoration: none; }
.queue-row .title:hover { text-decoration: underline; }
.queue-row .actions { flex: none; display: flex; align-items: center; gap: var(--s2); }
.queue-row .file { display: flex; align-items: center; gap: var(--s2); font-family: var(--mono); font-size: var(--t-mark); color: var(--faint); margin-top: var(--s1); }
/* The filename is what gives way when the row is too narrow, not the mark. */
.queue-row .file .mark { flex: none; }
.queue-row .file > span:not(.mark) { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-row .verdict { width: 260px; flex: none; font-size: var(--t-label); }
.queue-row .verdict .sources { color: var(--faint); margin-top: 2px; }
/* A link that acts like a button: it goes somewhere rather than doing
   something, so it stays an anchor and is dressed as the control it sits
   beside. Shared with the settled-import card, where the way on is also a
   destination. */
.queue-row .go,
.blank .go { flex: none; height: var(--h-control); padding: 0 var(--s3); display: flex; align-items: center; border: 1px solid var(--line); border-radius: var(--radius); font-size: var(--t-label); color: inherit; text-decoration: none; }
.queue-row .go:hover,
.blank .go:hover { border-color: var(--line-strong); }
.blank .go.primary { background: var(--ink); border-color: var(--ink); color: var(--on-ink); font-weight: 500; }
.blank .go.primary:hover { background: var(--ink-strong); border-color: var(--ink-strong); }

/* ── Review ──────────────────────────────────────────────────────────── */

.progress { display: flex; align-items: center; gap: var(--s2); height: var(--h-control); padding: 0 var(--s3); border: 1px solid var(--agreed-edge); border-radius: var(--radius); background: var(--agreed-bg); color: var(--agreed); font-size: var(--t-label); white-space: nowrap; }
.progress.open { border-color: var(--contested-edge); background: var(--contested-bg); color: var(--contested); }
.progress .dot { background: currentColor; }

/* Covers span the width above both columns, because a row of them is the one
   thing here that wants every pixel. Below it the form keeps a measure: a field
   stretched to a wide panel is not easier to read. */
.review-body { display: flex; gap: var(--s4); align-items: flex-start; }
.review-fields { flex: 1; min-width: 0; max-width: var(--w-form); display: flex; flex-direction: column; gap: var(--s4); }
/* A fixed rail and a fluid form rather than an even split: the form is the
   column being read and typed into. */
.review-body .rail { width: var(--w-rail); }
.rail { width: 340px; flex: none; position: sticky; top: var(--s3); }

/* A control that opens a region below it. Two on the review screen — the
   query in the rail and the absent fields in the editor — and they are the
   same control, so they open the same way.

   The caret is the whole affordance. Drawn in borders rather than a glyph,
   because this interface has no icons and one triangle is not a reason to
   start. */
.disclosure {
  display: flex;
  width: 100%;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border: 0;
  background: none;
  text-align: left;
  height: auto;
}
.disclosure:hover { background: var(--hover); }
.disclosure .spacer { flex: 1; }
.disclosure-caret {
  flex: none;
  width: 0;
  height: 0;
  border-left: 5px solid var(--faint);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 120ms ease;
}
.disclosure[aria-expanded='true'] .disclosure-caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
  .disclosure-caret { transition: none; }
}
.disclosure-label { font-size: var(--t-label); color: var(--ink); font-weight: 500; }
.disclosure-summary { flex: 1; font-size: var(--t-label); color: var(--faint); white-space: normal; }

/* The query, folded away above the answers to it. Open, it is set on the page
   ground inside a white panel, so the boxes read as one form rather than as
   loose inputs above the results. Title takes the full width because it is the
   one always typed; the two that qualify it share the row below. */
.lookup-fold { border-bottom: 1px solid var(--hairline); }
.lookup {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s2) var(--s3);
  margin: 0 var(--s4) var(--s4);
  padding: var(--s3);
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}
/* `display` here would otherwise beat the browser's own rule for `hidden`,
   which is how a folded-away form stays on the screen. */
.lookup[hidden] { display: none; }
.lookup-field { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.lookup-field .field-label { font-size: var(--t-label); color: var(--mute); }
.lookup input { width: 100%; }
.lookup-title, .lookup-actions { grid-column: 1 / -1; }
.lookup-actions { display: flex; justify-content: flex-end; }

.field-row { padding: var(--s4); border-bottom: 1px solid var(--hairline); }
.field-row:last-of-type { border-bottom: 0; }
.field-row.open { background: var(--contested-row); }
/* Label, status, who-said-what and Clear, on one line where there is room for
   one. The book page runs this column beside both a cover and the rail, which
   is narrow enough that the marks would otherwise overflow the panel and clip
   the Clear button against its edge. */
.field-row .field-head { display: flex; align-items: center; gap: var(--s2) var(--s3); flex-wrap: wrap; }
.field-row .field-label { width: var(--w-label); flex: none; font-size: var(--t-label); color: var(--mute); }
.field-row .spacer { flex: 1; }
.field-row .field-body { display: flex; gap: var(--s3); margin-top: var(--s2); }
.field-row .field-body > .field-label { visibility: hidden; }
.field-row .field-input { flex: 1; min-width: 0; }
.field-row.open input, .field-row.open textarea { border-color: var(--contested-field); background: var(--contested-row); }
.provenance { font-size: var(--t-label); color: var(--faint); margin-top: var(--s1); }
.provenance.contested { color: var(--contested); }
.provenance.own { color: var(--ink); }

/* Who said what, at a glance. A mark is filled when that source's value is
   the one currently in the box. */
.matrix { display: flex; gap: var(--s1); flex: none; }

.choices { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: var(--s2); }
.choice {
  display: flex;
  align-items: center;
  gap: var(--s2);
  max-width: 100%;
  height: var(--h-small);
  padding: 0 var(--s2);
}
.choice .label { overflow: hidden; text-overflow: ellipsis; }
.choice .who { font-family: var(--mono); font-size: var(--t-mark); color: var(--faint); }
.field-row.open .choice { border-color: var(--contested-edge); }
.choice[aria-pressed='true'] { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
.choice[aria-pressed='true'] .who { color: rgba(18, 19, 22, 0.6); }

.disclosure .more { font-size: var(--t-label); color: var(--ink); }
.absent-fields { display: flex; flex-direction: column; gap: var(--s2); padding: 0 var(--s4) var(--s4); }
.absent-fields .row { display: flex; gap: var(--s3); align-items: center; }
.absent-fields .field-label { width: var(--w-label); flex: none; font-size: var(--t-label); color: var(--mute); }

.source { padding: var(--s3) var(--s4); border-bottom: 1px solid var(--hairline); }
.source:last-child { border-bottom: 0; }
.source .top { display: flex; justify-content: space-between; align-items: center; gap: var(--s2); }
.source .who { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.source .name { font-size: var(--t-body); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.source .state { font-size: var(--t-label); color: var(--faint); white-space: nowrap; }
/* One card per result a source returned. */
.record { border-top: 1px solid var(--hairline); padding-top: var(--s2); margin-top: var(--s2); }
.source .record:first-of-type { border-top: 0; }
.record-head { display: flex; align-items: baseline; gap: var(--s2); }
.record-title { flex: 1; min-width: 0; font-size: var(--t-label); font-weight: 500; line-height: 1.3; }
.record-summary { color: var(--faint); font-size: var(--t-label); margin-top: 2px; line-height: 1.4; }
.record-actions { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: var(--s2); }
.record.best .record-title { font-weight: 600; }

.source .rows { margin-top: var(--s2); display: flex; flex-direction: column; gap: 2px; }
.source .rows .row { display: flex; gap: var(--s2); font-size: var(--t-label); line-height: 1.4; }
.source .rows .k { width: var(--w-key); flex: none; color: var(--pale); }
.source .rows .v { flex: 1; min-width: 0; }
.source .rows .v.used { color: var(--agreed); }
.source .none { margin-top: var(--s1); font-size: var(--t-label); color: var(--pale); }
/* Never asked, rather than asked and silent. The greyed name and state carry
   it; a dashed border on a near-white rule would not be visible. */

/* ── Duplicates: a comparison, not a list ────────────────────────────── */

.groups { display: flex; flex-direction: column; gap: var(--s4); }

.compare { display: flex; border-bottom: 1px solid var(--hairline); }
.compare:last-child { border-bottom: 0; }
.compare .rowhead { width: var(--w-rowhead); flex: none; padding: var(--s2) var(--s4); font-size: var(--t-label); color: var(--mute); }
.compare .cell { flex: 1; min-width: 0; padding: var(--s2) var(--s4); border-left: 1px solid var(--hairline); font-family: var(--mono); font-size: var(--t-label); line-height: 1.45; overflow-wrap: anywhere; }
.compare .cell.head { padding: var(--s3) var(--s4); }
.compare .cell.keeper { background: var(--agreed-bg); }
/* The directory is a link, so it breaks between path segments rather than
   mid-word: an underline running through a broken word is unreadable. */
.compare .cell .path { font-family: var(--mono); font-size: var(--t-mark); color: var(--faint); margin-top: var(--s2); overflow-wrap: break-word; line-height: 1.45; }
.compare .same { flex: 1; padding: var(--s2) var(--s4); border-left: 1px solid var(--hairline); font-size: var(--t-label); color: var(--pale); }

.keep { display: flex; align-items: center; gap: var(--s2); height: var(--h-small); padding: 0 var(--s2); color: var(--mute); }
.keep .dot { border-color: var(--line); }
.keep[aria-pressed='true'] { border-color: var(--agreed-edge); color: var(--agreed); }
.keep[aria-pressed='true'] .dot { background: var(--agreed); border-color: var(--agreed-edge); }

.group-foot { display: flex; gap: var(--s4); align-items: center; padding: var(--s4); flex-wrap: wrap; }
.group-foot .why { flex: 1; min-width: 240px; font-size: var(--t-label); color: var(--mute); }
.suggested { display: flex; align-items: center; gap: var(--s2); }
button.suggested { background: var(--ink); border-color: var(--ink); color: var(--on-ink); font-weight: 500; }
button.danger.suggested { background: var(--danger-bg); border-color: var(--contested-field); color: var(--danger); }
button.suggested .mark { background: rgba(18, 19, 22, 0.14); border-color: transparent; color: var(--on-ink); }
button.danger.suggested .mark { background: var(--contested-bg); border-color: var(--contested-edge); color: var(--danger); }

/* ── Book ────────────────────────────────────────────────────────────── */

/* The book presents itself: jacket to the left, billing beside it, the way a
   book lies face up on a table.
   Recessed, so it separates from the page without an edge drawn round it and
   the jacket has something to stand out from. Darkened rather than filled: the
   page's own wash reaches its brightest right here, and an opaque band would
   cover the one part of it worth seeing. Neutral, and it takes the wash down
   rather than adding to it, because a jacket cannot be judged against a
   surface mixed from itself. */
.book-head {
  display: flex;
  gap: var(--s5);
  align-items: center;
  /* One step all round, and the bleed is that same step: the band reaches out
     by exactly what it pads back in, so its contents stand on the line the
     record below them is set to. Change one and the other follows. */
  margin-inline: calc(var(--s5) * -1);
  padding: var(--s5);
  border-radius: var(--radius);
  /* Taking light out rather than filling is what leaves the wash underneath
     legible at all. */
  background: rgba(0, 0, 0, 0.28);
}
.book-head > .cover { height: var(--shelf-cover-h); width: calc(var(--shelf-cover-h) * var(--card-aspect, 0.6667)); }
/* The art to its own edge. A hairline keeps a pale jacket off the ground and a
   drop shadow stands it off the page; anything wider reads as a frame around a
   picture rather than as a book lying on a surface. */
.book-head > .cover.art {
  height: var(--shelf-cover-h);
  width: auto;
  /* The clamp already bounds the shape, so this only catches a cover measured
     before the clamp existed. */
  max-width: var(--w-jacket);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.07),
    0 2px 4px rgba(0, 0, 0, 0.45), 0 16px 36px rgba(0, 0, 0, 0.5);
}
.book-head > .cover.art .cover-art { width: auto; height: 100%; }
/* The padding belongs to the drawn stand-in, which sets a title inside the
   cover space. Real art fills its box, so it must not inherit that inset —
   the box is already at the art's own aspect, and padding would shrink the
   content area to a different shape and letterbox it. */
.book-head > .cover { padding: var(--s4) var(--s3) var(--s3) var(--s5); }
.book-head > .cover.art { padding: 0; }
.book-head > .cover .spine { width: 9px; }

/* Centred against the jacket, so the two sides read as one block whatever the
   art's shape. */
.book-billing {
  /* Takes the room left beside the jacket rather than sizing to its own
     contents. Shrink-wrapped, the column resolved to exactly the width of its
     widest child — the row of actions — so that row sat at its wrapping point
     with nothing to spare, and a font measuring a few pixels wider than the one
     it was laid out in put the last button on a line of its own. */
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--s4);
}
.book-billing-lead { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s2); }
.book-title { font-size: var(--t-title); font-weight: 600; letter-spacing: -0.02em; line-height: 1.1; }
.book-subtitle { font-size: var(--t-head); color: var(--mute); line-height: 1.25; }
.book-author { font-size: var(--t-lead); }
.book-series { font-size: var(--t-label); color: var(--mute); }

/* A row, because these act on the book they sit beside. */
.book-actions { display: flex; flex-wrap: wrap; gap: var(--s2); }
/* Right-aligned in the head, like every other control that acts on the panel
   under it rather than on one row of it. */
.panel-head .head-danger { margin-left: auto; }

.book-blurb { max-width: var(--w-blurb); font-size: var(--t-lead); line-height: 1.65; }
/* The prose's measure, so the record reads as one column rather than as prose
   with a wider table under it. */
.book-facts { max-width: var(--w-blurb); display: flex; flex-direction: column; gap: var(--s3); }
.book-facts .fact { display: flex; gap: var(--s3); align-items: baseline; }
.book-facts .field-label { width: var(--w-label); flex: none; font-size: var(--t-label); color: var(--mute); }
.book-facts .value { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.book-facts .fact.missing .value { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center; color: var(--pale); }

.book-body { display: flex; gap: var(--s4); align-items: flex-start; margin-top: var(--s6); }
.book-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--s4); }

.edit-row { display: flex; gap: var(--s3); align-items: center; padding: var(--s2) 0; }
.edit-row:first-child { padding-top: 0; }
.edit-row:last-child { padding-bottom: 0; }
.edit-row .field-label { width: var(--w-label); flex: none; font-size: var(--t-label); color: var(--mute); }
/* The box is sized to what goes in it. A year in a 700px field reads as a
   mistake, and the column is not a reason to make one. */
.edit-row input, .edit-row textarea { max-width: var(--w-field); }
/* A short field is short on every screen. Stated after the column rule above,
   which is otherwise equally specific and would win by being later. */
.edit-row input.short, .field-row input.short { max-width: var(--w-short); }

/* A book at rest. The same label column as the editor, so entering and
   leaving edit mode does not move the values sideways. */
.edit-row input.dirty { border-color: var(--contested-field); }
.save-row { display: flex; gap: var(--s3); align-items: center; }
.save-row .note { font-size: var(--t-label); color: var(--faint); }

.file-group { border-bottom: 1px solid var(--hairline); }
.file-group:last-child { border-bottom: 0; }
.file-group .top { padding: var(--s3) var(--s4); display: flex; gap: var(--s3); align-items: center; }
.file-group .name { font-family: var(--mono); font-size: var(--t-label); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-group .sub { font-size: var(--t-label); color: var(--faint); margin-top: 2px; }
.tracks { padding: 0 var(--s4) var(--s3) var(--indent-tracks); display: flex; flex-direction: column; gap: 2px; max-height: 240px; overflow-y: auto; }
.tracks .track { display: flex; gap: var(--s3); font-family: var(--mono); font-size: var(--t-mark); color: var(--mute); }
.tracks .n { width: 24px; color: var(--pale); }
.tracks .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Settings ────────────────────────────────────────────────────────── */

/* A form measure rather than a page one: these are fields to fill in, and a
   1020px text box is not easier to read for being wider. */
.settings { max-width: var(--w-form); display: flex; flex-direction: column; gap: var(--s4); }
/* No margin of its own: it sits in a stack that already spaces its
   children. */
.example { font-family: var(--mono); font-size: var(--t-label); color: var(--agreed); word-break: break-word; }
.providers { list-style: none; margin: 0; padding: 0; display: flex; gap: var(--s1); flex-wrap: wrap; }
.providers li { font-family: var(--mono); font-size: var(--t-mark); border: 1px solid var(--line); border-radius: var(--radius); padding: 2px var(--s2); color: var(--mute); }
.providers.muted li { border-style: dashed; color: var(--pale); }
.entries { list-style: none; margin: var(--s1) 0 0; padding: 0; font-size: var(--t-label); color: var(--mute); }
.entries li { font-family: var(--mono); padding: 1px 0; word-break: break-word; }

/* ── Narrow ──────────────────────────────────────────────────────────── */

/* Below this the nav lies down, and the width it was holding goes to the view.
   The sidebar is `flex: none`, so it takes as much out of a narrow window as a
   wide one and the shelf lives on the remainder. Collapsing it returns that
   width in one step, so a row gains covers as the window crosses this line
   whatever line is chosen — the breakpoint decides where the nav stops being
   worth its width, not whether that step exists. */
@media (max-width: 1000px) {
  .app { flex-direction: column; }
  .sidebar { width: 100%; flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--line); padding: var(--s2); }
  .wordmark { display: none; }
  /* Stacked, the cross axis is the width — see styles_test.mjs, which holds
     every such rule to restating it. */
  .review-body { flex-direction: column; align-items: stretch; }
  /* Named with the same reach as the rule that widens it beside the form, or
     that one wins here and holds a fixed column inside a narrower window. */
  .rail, .review-body .rail { width: 100%; position: static; }
  /* Stacked, these read as one column, so the rail takes the measure of what
     sits above it — which is the form here and is not capped on the book
     screen. A rail that stops short of the panels above it is the same ragged
     edge as one that overruns them. */
  .review-body .rail { max-width: var(--w-form); }
  /* Stacked only when there are three columns to stack. The cover is a fixed
     column: laying it above the record spends a whole line without widening
     the record, and beside the record the same width reads as the book it
     belongs to. The rail is the one column that cannot share the row. */
  .book-body:has(.rail) { flex-direction: column; align-items: stretch; }
}

@media (max-width: 720px) {
  .view { padding: var(--s5) var(--s4) var(--s7); }
  /* The bleed cannot exceed the view's own padding here or the band runs off
     the screen, so both come down a step together. */
  .book-head { margin-inline: calc(var(--s4) * -1); padding: var(--s4); }
  /* Stacked, for the reason the review and book bodies are: `flex-start` on
     the cross axis is a width, and it sizes the filter row to its content. */
  .head { flex-direction: column; align-items: stretch; }
  .destination { flex-wrap: wrap; }
  .queue-row { flex-wrap: wrap; }
  /* Its own line, not its own width. This column never shrinks, and the panel
     around it clips rather than scrolls, so any width it asks for beyond the
     row's is taken out of sight rather than off the end. */
  .queue-row .verdict { width: 100%; }
  .shelf { gap: var(--s5) var(--s4); }
}

/* Where the cover finally gives way: the point at which the record beside it
   can no longer hold a label and its value on one line. Later than the window
   first feels narrow, for the reason the rule above gives. */
@media (max-width: 560px) {
  .book-body { flex-direction: column; align-items: stretch; }
  /* The jacket goes above the billing rather than beside it: side by side, the
     title is left a column too narrow to be one.
     `flex-start` rather than `stretch`: the art's box is sized from its height
     and takes whatever width its shape asks for, so stretching the column
     widens the box without widening the picture and `contain` letterboxes the
     jacket inside it. */
  .book-head { flex-direction: column; align-items: flex-start; }
}

/* ── Settings: sources ────────────────────────────────────────────── */

/* Named apart from the rail's `.source`, which is a different component. Two
   components on one global class is invisible until a page looks wrong. */
.source-setting { display: flex; flex-direction: column; gap: var(--s3); padding: var(--s3) 0; border-bottom: 1px solid var(--hairline); }
.source-setting:first-of-type { padding-top: 0; }
.source-setting:last-of-type { border-bottom: 0; padding-bottom: 0; }
.source-setting .switch { display: flex; align-items: center; gap: var(--s2); cursor: pointer; width: fit-content; }
.source-setting .switch input[disabled] { cursor: not-allowed; }
.source-setting .name { font-size: var(--t-body); font-weight: 500; }
/* Muted while the switch cannot be used, not while the row is marked locked:
   typing a key releases the switch, and the name has to follow the control. */
.source-setting .switch input[disabled] + .name { color: var(--mute); }
.source-setting .key { display: flex; flex-direction: column; gap: var(--s2); padding-left: var(--s5); }
.source-setting .key-state { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2); }
.settings-form { display: flex; flex-direction: column; gap: var(--s4); }

/* ── Sign-in ─────────────────────────────────────────────────────────── */

/* The one page drawn before there is a session, so it carries none of the
   app's chrome: a heading, a sentence and the link that starts the flow. */
.signin {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  min-height: 100vh;
  padding: var(--s5);
  text-align: center;
}
.signin h1 { font-size: var(--t-title); }
.signin p { color: var(--mute); font-size: var(--t-body); }
.signin .signin-button {
  display: inline-flex;
  align-items: center;
  height: var(--h-control);
  padding: 0 var(--s4);
  border-radius: var(--radius);
  background: var(--ink);
  color: var(--on-ink);
  font-size: var(--t-label);
  font-weight: 500;
  text-decoration: none;
}
.signin .signin-button:hover { background: var(--ink-strong); }

/* ── Reading devices ─────────────────────────────────────────────────── */

.devices .catalog { display: flex; flex-direction: column; gap: var(--s1); }
.devices .create { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s2); }
.devices .create .field { flex: 1 1 var(--w-rail); }
.devices .secret { display: flex; flex-direction: column; gap: var(--s1); }
.devices .secret .field-label { display: flex; align-items: baseline; gap: var(--s2); }
.devices .copy { display: flex; gap: var(--s2); }
.devices .copy input { flex: 1; min-width: 0; }
.devices .rows { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.devices .row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s3);
  padding: var(--s2) 0;
  border-top: 1px solid var(--hairline);
}
.devices .row .name { flex: 1 1 auto; font-size: var(--t-body); }
