/* =====================================================================
   wife-smut · base.css
   Legacy STRUCTURE + COMPONENTS for surfaces that have not migrated yet.
   Geometry, type, and palette defaults come only from tokens.css; themes.css
   may replace semantic colours. New components belong in components.css.
   ===================================================================== */



/* ---- Reset / base ---------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--fg);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1, h2, h3, h4 { font-family: var(--font-display); margin: 0 0 .5em; font-weight: 700; letter-spacing: -.01em; }
h1 { font-size: var(--fs-h1); line-height: var(--lh-h1); }
h2 { font-size: var(--fs-h2); line-height: var(--lh-h2); }
h3 { font-size: var(--fs-h3); line-height: var(--lh-h3); }
p { margin: 0 0 var(--space-3); }
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; display: block; }
code, kbd { font-family: var(--font-mono); font-size: .92em; background: var(--bg-sunken); padding: .1em .4em; border-radius: 6px; }
::selection { background: var(--accent-weak); }
:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--radius-sm); }

/* ---- Forms & buttons (generic) --------------------------------------- */
/* One field geometry everywhere (owner report 2026-10-03: fields of several heights and
   radii side by side). Height comes from the tap floor — 44px on touch, 36px on a mouse —
   so the padding is horizontal only and the text centres on the line-height. */
input, select, textarea {
  font: inherit;
  color: var(--fg);
  background: var(--bg-elev);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0 var(--space-3);
  line-height: var(--lh-input);
  transition: border-color var(--transition), box-shadow var(--transition);
}
:where(input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="hidden"]), select) {
  min-block-size: var(--tap-min);
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); box-shadow: var(--ring); outline: none; }
textarea { width: 100%; min-height: 84px; resize: vertical; padding-block: var(--space-2); }
label { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--fg-muted); font-size: .92rem; }

/* A plain <button> and a `.btn` share one geometry: --radius corners (DESIGN-SYSTEM.md §3.3:
   "cards, buttons, sheets, inputs"), the same padding and type, height from the tap floor.
   Before this, plain buttons were 6px-radius/.45em, `.btn` was a pill at .55em, and
   utilities.css redefined `.btn` again — three shapes on one screen. */
button {
  font: inherit;
  cursor: pointer;
  color: var(--fg);
  background: var(--bg-elev-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-4);
  line-height: 1.2;
  transition: background var(--transition), border-color var(--transition), color var(--transition), transform var(--transition);
}
button:hover { border-color: var(--accent); color: var(--accent); }
button:active { transform: translateY(1px); }
button:disabled { cursor: default; opacity: .65; }

/* Primary call-to-action buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .4em;
  padding: var(--space-2) var(--space-4); border-radius: var(--radius);
  font-size: var(--fs-body); font-weight: 600; border: 1px solid transparent; line-height: 1.2;
  text-align: center;
}
.btn:hover { text-decoration: none; transform: translateY(-1px); }
.btn-request, .btn-request-series { background: var(--accent); color: var(--accent-fg); }
.btn-request:hover, .btn-request-series:hover { color: var(--accent-fg); filter: brightness(1.06); }
.btn-checkout { background: var(--success); color: #fff; }
.btn-checkout:hover { color: #fff; filter: brightness(1.05); }
.btn-retry { background: var(--warning); color: #fff; }
.btn-progress {
  background: var(--bg-elev-2); color: var(--fg-muted); cursor: progress;
  border-color: var(--border-strong);
  animation: ws-pulse 1.3s ease-in-out infinite;
}
@keyframes ws-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }

/* ---- App shell: header / nav ----------------------------------------- */
.app-header {
  position: sticky; top: 0; z-index: 50;
  background: var(--bg-elev);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}
.app-header__inner {
  max-width: var(--maxw); margin: 0 auto;
  min-height: var(--header-h);
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-2) var(--space-4);
}
.brand {
  font-family: var(--font-display); font-weight: 800; font-size: 1.15rem;
  color: var(--fg); letter-spacing: -.02em; white-space: nowrap;
  display: inline-flex; align-items: center; gap: .3em;
}
.brand:hover { text-decoration: none; color: var(--accent); }
/* R-547: the brand glyph (app/static/favicon.svg, the one source for it and the whole
   PWA icon set) beside the "smut" wordmark, in place of a Seerr-style ocular orb. */
.brand__glyph { inline-size: 28px; block-size: 28px; flex-shrink: 0; }
/* The always-visible user cluster (.app-header__user) takes margin-left:auto to pin the
   avatar + bell to the far right (#102). `.nav-toggle` (the old kebab drawer trigger) was
   removed by #658 along with the drawer it opened — its CSS lived on until R-170, which
   found `.app-header__actions` (the omnibox + admin ticket meter) still carrying THAT
   drawer's collapse rule below with no button left to open it, rendering the phone header
   search as a real DOM node at a permanent 0×0. */

.app-header__actions {
  display: flex; align-items: center; gap: var(--space-4);
  margin-left: auto; flex: 1; justify-content: flex-end;
}
.app-header__actions a { color: var(--fg-muted); font-weight: 500; white-space: nowrap; }
.app-header__actions a:hover { color: var(--accent); text-decoration: none; }
/* Scope the compass spacing to the header link: `.discover` is also the page surface. */
.header-nav__item.discover { gap: .35em; }
.discover__compass { display: block; flex: 0 0 auto; }


/* search omnibox */
/* Phone header nav links are hidden on the controls themselves, not only by the
   ancestor nav, so zero-size-control checks do not see live-looking dead targets. */
.header-nav__item { display: none; }
/* ONE component (`_omnibox.html`), two placements: the app header (every page except Shelf
   Home) and the top of Shelf Home. From 600px the field is simply visible; below that the
   header placement collapses to a magnifying-glass button (see the phone block in
   "Responsive"). Phone-only controls are display:none ON THEMSELVES, so the phone gate
   never counts them as live 0x0 targets. */
.header-search { position: relative; flex: 1; max-width: 360px; min-width: 150px; }
.omnibox__field { display: flex; align-items: center; gap: var(--space-1); }
.header-search input[type=search] {
  flex: 1 1 auto; min-width: 0; width: 100%;
  border-radius: var(--radius-pill); padding-inline: .9em;
}
.omnibox__toggle, .omnibox__submit, .omnibox__close { display: none; }
.omnibox[data-placement="page"] { flex: none; max-width: 640px; min-width: 0; margin: 0 0 var(--space-4); }
.omnibox-results { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 60; }
/* Header placement, 600px and up: the field is only ~185px wide at tablet widths, and a
   dropdown inheriting that width clips the type pill and the closing quote. The dropdown
   anchors to the header row instead of the field (`.omnibox` stops being the containing
   block), right-aligned to the header's content edge, comfortably wide, and never wider
   than the row less a gutter each side, so it stays on screen at every width. The field
   is at most 360px, so the dropdown is never narrower than it. The phone block below
   overrides this with its own open-field inset. */
@media (min-width: 600px) {
  .app-header__inner { position: relative; }
  .omnibox[data-placement="header"] { position: static; }
  .omnibox[data-placement="header"] .omnibox-results {
    left: auto; right: var(--space-4);
    width: min(30rem, 100% - 2 * var(--space-4));
  }
}
.omnibox-dropdown {
  list-style: none; margin: 0; padding: var(--space-1);
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
  max-height: 60vh; overflow: auto;
}
.omnibox-dropdown:empty { display: none; }
.ac-hit a, .ac-full-search a, .ac-empty {
  display: flex; align-items: center; gap: var(--space-2);
  padding: .5em .6em; border-radius: var(--radius-sm); color: var(--fg);
}
.ac-hit a:hover, .ac-full-search a:hover { background: var(--accent-weak); text-decoration: none; }
.ac-label { flex: 1; }
.ac-authors { font-size: .78rem; color: var(--fg-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 40%; }
.ac-type { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-faint); border: 1px solid var(--border); border-radius: var(--radius-pill); padding: .1em .5em; }
.ac-empty { color: var(--fg-faint); }
.ac-full-search { border-top: 1px solid var(--border); margin-top: var(--space-1); padding-top: var(--space-1); font-weight: 600; }
/* "Closest we have": the nearest few entries when nothing clears the match bar. */
.ac-closest { margin-top: var(--space-1); }
.ac-closest__heading {
  margin: 0; padding: .4em .6em .1em;
  font-size: .78rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--fg-faint);
}
.ac-closest__list { list-style: none; margin: 0; padding: 0; }
/* Try harder (ADR-0053): the row between the matches (or "Closest we have") and "See all
   results"; its control wraps inside the dropdown's width. */
.ac-try-harder { margin-top: var(--space-1); padding: var(--space-1) var(--space-2); }

/* tickets meter */
.tickets-meter-host { white-space: nowrap; }
.tickets-meter, .tickets-total {
  display: inline-flex; align-items: center; gap: .35em;
  font-size: .82rem; color: var(--fg-muted);
  background: var(--bg-elev-2); border: 1px solid var(--border);
  padding: 0 .7em; border-radius: var(--radius-pill);
  block-size: 32px; box-sizing: border-box; /* the header's one round-control height */
}
.tickets-meter strong, .tickets-total strong { color: var(--accent); }
.tickets-meter .icon { inline-size: 16px; block-size: 16px; }
.tickets-zero { color: var(--danger); }

/* notification nav badge */
.nav-unread, .unread-badge, .notifications-unread {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 4px;
  font-size: .72rem; font-weight: 700; line-height: 1;
  background: var(--danger); color: #fff; border-radius: var(--radius-pill);
  margin-left: .3em; vertical-align: middle;
}

/* ---- Main content area ----------------------------------------------- */
.app-main {
  max-width: var(--maxw); margin: 0 auto;
  padding: var(--space-6) var(--space-4) var(--space-8);
}
.app-main > section { margin-bottom: var(--space-6); }
.empty-state {
  color: var(--fg-faint); background: var(--bg-elev);
  border: 1px dashed var(--border-strong); border-radius: var(--radius);
  padding: var(--space-6); text-align: center;
}

/* generic surface tables (library list, admin, cancelled) */
table { border-collapse: collapse; width: 100%; background: var(--bg-elev); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-sm); }
/* #340: a table too wide for the viewport scrolls INSIDE this wrapper instead of dragging
   the document sideways — the shape tests/e2e/test_phone_budget.py asserts for the body.
   #417: `max-height` + a sticky thead keeps the column labels visible while scrolling a
   long table. A job row is mostly opaque short strings, so an unlabelled column is
   unreadable — the header is the only thing making the row mean anything. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-scroll--tall { max-height: 70vh; overflow-y: auto; }
.table-scroll--tall thead th { position: sticky; top: 0; z-index: 1; }
/* The generic `table` rule sets overflow:hidden for its rounded corners, which would clip
   a sticky header into invisibility — so a scrolling table opts out of the clipping. */
.table-scroll--tall table { overflow: visible; }
th, td { text-align: left; padding: .6em .8em; border-bottom: 1px solid var(--border); }
thead th { background: var(--bg-elev-2); font-size: .8rem; text-transform: uppercase; letter-spacing: .03em; color: var(--fg-muted); }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--accent-weak); }

/* report-issue (a <details> appended into main). Scoped to that block: as a bare
   `.app-main details` it gave every disclosure on every page a top rule and 40px of space. */
.app-main details.report-issue { margin-top: var(--space-6); border-top: 1px solid var(--border); padding-top: var(--space-4); }
.app-main details.report-issue summary { cursor: pointer; color: var(--fg-muted); font-weight: 600; }

/* =====================================================================
   SHELF (home + /shelf): horizontal poster rows
   ===================================================================== */
.shelf-page > h1, .library-page > h1 { margin-bottom: var(--space-4); }
.shelf-section { margin-bottom: var(--space-5); }

.shelf-row-wrap {
  position: relative; display: flex; align-items: center; gap: 0;
}

.shelf-scroll-btn {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  /* Was a fixed 36x36 (below --tap-min) here, bumped to the floor only on shelf/home
     routes by surfaces/shelf-browse.css's own override — a real gap once a SECOND
     surface (the book page, U5/R-236) started reusing this exact wrap+button pattern
     via the shared kit. The shelf-browse.css override becomes a same-value no-op, kept
     rather than removed to avoid touching a surface stylesheet outside this unit. */
  width: var(--tap-min); height: var(--tap-min);
  padding: 0; border-radius: 50%;
  background: var(--bg-elev); border: 1px solid var(--border-strong);
  color: var(--fg-muted); box-shadow: var(--shadow-sm);
  z-index: 2; transition: color var(--transition), background var(--transition), opacity var(--transition);
  flex-shrink: 0;
}
.shelf-scroll-btn:hover { color: var(--accent); background: var(--bg-elev-2); border-color: var(--accent); }
.shelf-scroll-btn:disabled { opacity: .35; pointer-events: none; }
.shelf-scroll-btn[hidden] { display: none; }

.shelf-row {
  list-style: none; margin: 0; padding: 0 0 var(--space-2);
  display: flex; gap: var(--space-4); overflow-x: auto; scroll-snap-type: x proximity;
  scrollbar-width: thin; flex: 1; min-width: 0;
}
.shelf-item {
  flex: 0 0 auto; width: var(--shelf-item-w); position: relative;
  display: flex; flex-direction: column; gap: 2px; scroll-snap-align: start;
}
.shelf-item__cover {
  width: 100%; aspect-ratio: var(--cover-ratio); object-fit: cover;
  border-radius: var(--cover-radius); background: var(--bg-elev-2);
  box-shadow: var(--cover-shadow); margin-bottom: var(--space-1);
  transition: transform var(--transition), box-shadow var(--transition);
}
.shelf-item:hover .shelf-item__cover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
.shelf-item__link {
  font-weight: 600; font-size: .9rem; color: var(--fg); line-height: 1.25;
  /* #105: clamp the title to 2 lines and break long words, so a long/over-tagged title
     never spills past its fixed-width card onto a neighbouring card's caption. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
  overflow: hidden; overflow-wrap: normal; word-break: normal;
}
.shelf-item__author, .shelf-item__series, .shelf-item__sub {
  font-size: .78rem; color: var(--fg-faint);
  /* #105: hold each secondary line to a single, ellipsised line — never overlaps a neighbour. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.shelf-sections { list-style: none; margin: 0; padding: 0; }

/* Hidden-sections tray: the unhide (Show) controls for sections the viewer disabled (#160). */
.shelf-hidden { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--border); }
.shelf-hidden__title { font-size: .85rem; font-weight: 600; color: var(--fg-faint); margin: 0 0 var(--space-3); }
.shelf-hidden__list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.shelf-hidden__item { display: inline-flex; align-items: center; gap: var(--space-2); background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-pill); padding: .25em .25em .25em .9em; }
.shelf-hidden__label { font-size: .82rem; color: var(--fg-faint); }
.shelf-hidden__show button { font-size: .78rem; padding: .25em .7em; border-radius: var(--radius-pill); background: transparent; color: var(--fg-faint); border-color: transparent; }
.shelf-hidden__show button:hover { color: var(--accent); border-color: var(--border); }

/* =====================================================================
   LIBRARY (Plex-style grid + list)
   ===================================================================== */
/* #784: the controls are a bare row — the old card wrapper (panel + border + shadow)
   spent ~90px framing one input and one button. Layout lives in surfaces/library.css. */
.library-controls input[type=search] { flex: 1; min-width: 0; border-radius: var(--radius-pill); }
.library-view-toggle { border: 0; margin: 0; padding: 0; display: inline-flex; gap: var(--space-3); }
.library-view-toggle label { color: var(--fg-muted); }
/* #161: the reading dates under the status control — history, not a headline. */
.reading-status__dates { color: var(--fg-muted); font-size: .85rem; margin: 0 0 var(--space-2); font-variant-numeric: tabular-nums; }
/* #80: the result count sits above the listing at .jobs-count's weight — it is
   orientation, not a heading, and must not compete with the group headings below it. */
.library-count {
  color: var(--fg-muted); font-size: .9rem; margin: 0 0 var(--space-3);
  font-variant-numeric: tabular-nums;
}
/* #109: the column picker. A <details> rather than a permanently-open panel — it is set
   once and then rarely touched, and it must not push the listing down the page. */
.column-picker { margin: 0 0 var(--space-4); }
.column-picker summary { cursor: pointer; color: var(--fg-muted); font-size: .9rem; }
.column-picker__body {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-3) var(--space-4);
  margin-top: var(--space-2);
}
.column-picker__col { display: inline-flex; align-items: center; gap: 4px; color: var(--fg-muted); }
.column-picker__body form { display: contents; }
/* The resize grip. Sized in the header cell's own padding so it needs no layout space of
   its own; `touch-action: none` is what lets a pointerdrag work on a touch screen instead
   of being swallowed as a scroll gesture. */
.library-list thead th { position: relative; }
.col-grip {
  position: absolute; top: 0; right: 0; width: 8px; height: 100%;
  cursor: col-resize; touch-action: none; user-select: none;
}
.col-grip:hover { background: var(--accent); opacity: .35; }
/* #494: a byline the evidence does not support. Deliberately quiet — a dotted underline
   and muted colour, not a warning: the claim is "we are not sure", not "this is wrong",
   and 3,290 books carry it, so anything louder would shout across the whole Library. */
.author-unconfirmed {
  color: var(--fg-muted);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.author-unconfirmed:hover { color: var(--accent); }
/* #487: the scope label beside a series' derived-status badge. Quiet — it is a caveat on
   the badge, not a competing claim, and it appears on 1,402 of 3,420 blocks. */
.series-scope { color: var(--fg-faint); font-size: .8rem; font-variant-numeric: tabular-nums; }

/* #448: content warnings read differently from tropes because they ARE different — the
   warning block is bordered and warning-coloured, the hint is quiet, and the "N more"
   disclosure covers only ordinary tropes (a warning behind a toggle is not a warning). */
.facts-heading { font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-faint); margin: var(--space-4) 0 var(--space-2); }
.facts-hint { color: var(--fg-muted); font-size: .85rem; margin: 0 0 var(--space-2); }
.content-warnings {
  border-left: 3px solid var(--warning); padding-left: var(--space-3);
  margin: var(--space-4) 0;
}
.content-warnings .facts-heading { color: var(--warning); margin-top: 0; }
.trope-chip--warning a { background: var(--warning-weak); color: var(--warning); }
.trope-more summary { cursor: pointer; color: var(--fg-muted); font-size: .85rem; margin-top: var(--space-2); }
.trope-more .trope-list { margin-top: var(--space-2); }

/* One fused capsule, not four side-by-side pills: DESIGN-SYSTEM §5.9 calls N adjacent
   pills a "control farm", and the pill-density lint counts each one (the Library's
   desktop filter row measured 7 against a limit of 6). `.segmented` owns the border,
   radius and hairlines; only the muted quick-pick voice stays here.
   The compound `.added-quickpick.segmented > button` is deliberate: plain
   `.added-quickpick button` ties components.css's `.segmented > label, > a, > button`
   on specificity and loses on source order, so the muted colour and the small size
   silently did nothing. The picks also wrap two-per-row in the filter grid's narrow
   column, so each segment grows to fill its row — dead space beside "Clear" reads as
   a broken capsule. */
.added-quickpick { vertical-align: middle; }
.added-quickpick.segmented > button {
  flex: 1 0 auto; justify-content: center;
  font-size: .8rem; color: var(--fg-muted);
  transition: background var(--transition), color var(--transition);
}
.added-quickpick.segmented > button:hover { color: var(--accent); background: var(--accent-weak); }
.added-quickpick.segmented > button[data-range="clear"] { color: var(--fg-faint); }

.lib-group { margin: var(--space-6) 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 2px solid var(--border); }
.lib-series-head { margin: var(--space-4) 0 var(--space-3); color: var(--fg-muted); font-size: .95rem; text-transform: uppercase; letter-spacing: .03em; }

.library-grid {
  display: grid; gap: var(--space-5);
  grid-template-columns: repeat(auto-fill, minmax(var(--lib-card-min), 1fr));
}
.lib-card { display: flex; flex-direction: column; gap: var(--space-1); position: relative; }
.lib-cover {
  width: 100%; aspect-ratio: var(--cover-ratio); object-fit: cover;
  border-radius: var(--cover-radius); background: var(--bg-elev-2);
  box-shadow: var(--cover-shadow); margin-bottom: var(--space-2);
  transition: transform var(--transition), box-shadow var(--transition);
}
.lib-card:hover .lib-cover { transform: translateY(-4px) scale(1.01); box-shadow: var(--shadow-lg); }
.lib-title { font-weight: 600; font-size: .92rem; color: var(--fg); line-height: 1.25; }
.lib-author { font-size: .8rem; color: var(--fg-muted); }
.lib-series, .lib-added { font-size: .74rem; color: var(--fg-faint); }

/* Stretched link (#103): a card link with `card-link` projects an invisible
   full-bleed ::after over its nearest positioned ancestor (the .lib-card /
   .shelf-item, both `position: relative`), so the whole card — cover + body —
   is one click target while staying a single, accessible anchor. Nested
   interactive controls (forms/buttons, e.g. bookmark/watch) opt back out by
   sitting above the overlay, so they still receive their own clicks. */
.card-link::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.lib-card form, .lib-card button,
.shelf-item form, .shelf-item button { position: relative; z-index: 2; }

/* #109: column widths are now emitted per <col> by _library_listing.html — the saved
   width if the reader has dragged one, otherwise app.rows.COLUMNS[key].default_px. The
   five percentage rules that used to live here summed to 100% across one specific set of
   six columns, so any picked seventh column overflowed the table. */
/* The scroll box (#109). `width: 100%` keeps the old behaviour on a wide screen — with
   table-layout: fixed, columns whose pixel widths total less than the container are scaled
   up proportionally, which is what the percentage rules used to do. When the widths total
   MORE (nine columns picked, or one dragged wide), the table overflows this box and the
   box scrolls, instead of the whole page scrolling sideways under a sticky header. */
.library-list-scroll { overflow-x: auto; }
.library-list { table-layout: fixed; width: 100%; }
.library-list td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.library-list td a { font-weight: 600; }

/* =====================================================================
   BOOK / SERIES / AUTHOR content pages
   ===================================================================== */
/* #335: the type pill (_page_type_pill.html, included from base.html above
   {% block content %} whenever a page sets `page_type`) — colour-coded purely from
   existing :root tokens, so every theme (base.css loads first, themes.css redefines
   the tokens after) and both light/dark modes inherit readable colours with no
   per-theme override. The word (BOOK/SERIES/AUTHOR) is always printed — colour is
   never the only cue. */
.page-type-pill {
  display: inline-block; margin-bottom: var(--space-3);
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  border-radius: var(--radius-pill); padding: .2em .75em;
}
.page-type-pill[data-page-type="book"] { background: var(--accent-weak); color: var(--accent); }
.page-type-pill[data-page-type="series"] { background: var(--success-weak); color: var(--success); }
.page-type-pill[data-page-type="author"] { background: var(--warning-weak); color: var(--warning); }

.book-header { display: grid; grid-template-columns: minmax(180px, 240px) 1fr; gap: var(--space-6); align-items: start; }
.book-cover, .author-photo {
  width: 100%; aspect-ratio: var(--cover-ratio); object-fit: cover;
  border-radius: var(--radius); box-shadow: var(--shadow-lg); background: var(--bg-elev-2);
}
.author-photo { aspect-ratio: 1; max-width: 160px; border-radius: var(--radius-pill); }
.book-meta > h1 { margin-bottom: var(--space-2); }
.book-author { color: var(--fg-muted); font-size: 1.02rem; }
.book-series { color: var(--fg-muted); }
.book-date { color: var(--fg-faint); font-size: .88rem; }
/* #437: the enrichment-backfilled description — real body prose (up to ~2800 chars),
   so it gets readable line-height and a capped measure rather than stretching the full
   width of the wide grid column; unlike its muted metadata neighbours (.book-author,
   .book-series, .book-date) this is page CONTENT, so it takes the full-strength --fg. */
.book-blurb { margin-top: var(--space-3); color: var(--fg); line-height: 1.55; max-width: 65ch; }
/* #634: other readers' aggregate — rating, how many rated it, page count. Styled as a
   METADATA line (the muted register of .book-author/.book-date), deliberately NOT as the
   .book-rating control further down the page: this is a fact you read, and anything that
   borrowed the control's weight would invite a click that does nothing. The number itself
   takes --fg so the line still has one thing to land on. */
.book-community { color: var(--fg-faint); font-size: .88rem; margin: var(--space-2) 0 0; }
.book-community strong { color: var(--fg); font-weight: 600; }
.tickets-hint { margin: var(--space-2) 0; }

.book-meta > .acquire,
.book-meta > .bookmark-control,
.book-meta > .reading-status,
.book-meta > .book-rating,
.book-meta > .tags { margin-top: var(--space-4); }

/* series & author shared book rows — structural rules live in components.css §5 now
   (#784: the legacy flex definition here fought the kit's grid and leaked padding into
   the card/carousel variants). */
.series-books, .standalone-books { list-style: none; margin: var(--space-3) 0; padding: 0; }
/* #487: a series member on an author's page that is NOT by that author. The block lists
   the series' full membership so the run reads as a run; this is what stops the page
   asserting authorship of the rest, which was #476's actual bug. Dimmed rather than hidden
   or greyed to unreadability — the reader must still be able to read the title, click
   through and request it, they simply must not read it as this author's work. Restored to
   full strength on hover and focus-within, so interacting with one is never a fight with
   the styling and keyboard focus is never dimmer than the mouse. */
.book-row--other-author { opacity: .55; }
.book-row--other-author .book-title { font-weight: 400; }
.book-row--other-author:hover,
.book-row--other-author:focus-within { opacity: 1; }
/* The reader's own contrast preference outranks our de-emphasis: at that setting a dimmed
   row is a legibility problem, so the distinction falls back to weight alone. */
@media (prefers-contrast: more) {
  .book-row--other-author { opacity: 1; }
}
.book-seq { font-variant-numeric: tabular-nums; }
.book-flag { font-size: .72rem; font-weight: 700; padding: .15em .6em; border-radius: var(--radius-pill); text-transform: uppercase; letter-spacing: .03em; }
.book-flag[data-testid="flag-owned"] { background: var(--success-weak); color: var(--success); }
.book-flag[data-testid="flag-future"] { background: var(--warning-weak); color: var(--warning); }
.book-flag[data-testid="flag-missing"] { background: var(--bg-elev-2); color: var(--fg-faint); }

/* /discover's queue (#570). The list needs its own rules for one measured reason: a bare
   <ul> keeps the UA stylesheet's 40px indent, and with the dismiss checkbox beside it that
   pushed the shared .book-row 56px past a 390px viewport — the only page in the app whose
   DOCUMENT scrolled sideways on a phone. Everywhere else wide rows sit in a container that
   scrolls on its own (.table-scroll, .library-list-scroll); here the row is meant to fit,
   so the fix is to stop stealing width rather than to add a scrollbar. */
.discover-list { list-style: none; margin: var(--space-3) 0; padding: 0; }
/* An annotated row carries a checkbox and/or a sentence, so it is the one .book-row that
   can need two lines. Scoped to that modifier on purpose: the author and series pages sit
   in wider containers and must keep their single-line rows. The acquire control has a hard
   min-content width it will not shrink past, so on a phone the row WRAPS rather than
   pushing the document sideways. */
.book-row--annotated { flex-wrap: wrap; }
.book-row-pick { flex: 0 0 auto; margin: 0; }
/* The sentence takes the full width under the row rather than competing with it: it is the
   part the reader is meant to be able to disagree with (design section 6), so it must never
   be the thing that truncates.
   The indent is PADDING, not margin: a `flex-basis: 100%` item is already the full width of
   its line, and a left margin is added on top of that — which pushed the sentence 15px past
   a 390px viewport. Padding is inside the box under this sheet's global `border-box`. */
.book-row-note {
  flex: 1 0 100%; margin: .15em 0 0;
  color: var(--fg-muted); font-size: .85rem;
}

.series-header, .author-header { margin-bottom: var(--space-5); }
.series-author, .author-engagement, .series-engagement { color: var(--fg-muted); }
.series-stats { display: inline-flex; gap: var(--space-2); color: var(--fg-muted); font-size: .9rem; }
.series-engagement, .author-engagement { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; margin: var(--space-3) 0; }
.request-series { margin: var(--space-4) 0; }
.ticket-count { font-weight: 700; }

.series-block { background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); margin-bottom: var(--space-4); box-shadow: var(--shadow-sm); }
.series-toggle, .standalones-toggle { background: transparent; border: 0; font-size: 1em; padding: .1em .4em; color: var(--fg-muted); }

.series-link { font-weight: 700; }
.metadata-editor { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px dashed var(--border-strong); }
.md-edit, .md-lock { margin-bottom: var(--space-3); }
.md-field { display: grid; gap: var(--space-2); margin-bottom: var(--space-4); padding-bottom: var(--space-3); border-bottom: 1px solid var(--border); }
.md-field__label { font-weight: 600; }
.md-field input:not([type="checkbox"]), .md-field select { width: 100%; min-height: var(--tap-min); }
.md-field__hint, .md-field__state { margin: 0; font-size: .85rem; }
.md-field__state { color: var(--fg-muted); }
.md-field__state--locked { color: var(--success); }
.md-field__lock { display: flex; align-items: center; gap: var(--space-2); min-height: var(--tap-min); }
.md-field__lock input { width: 1.25rem; height: 1.25rem; }
.md-field .btn { min-height: var(--tap-min); justify-self: start; }
.md-error { color: var(--danger); font-weight: 600; margin: 0 0 var(--space-3); }

/* =====================================================================
   Reading status / ratings / tags / bookmarks / watch
   ===================================================================== */
.reading-status, .book-rating, .tags, .bookmark-control, .watch-control { background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-3) var(--space-4); }
.reading-status__current, .book-rating__current { color: var(--fg-muted); margin-bottom: var(--space-2); }
.reading-status__set, .book-rating__stars { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.status-pill, .star-pick { display: inline; }
.status-pill button, .star-pick button { border-radius: var(--radius-pill); padding: .3em .8em; font-size: .85rem; text-transform: capitalize; }
.status-pill button[aria-current="true"], .star-pick button[aria-current="true"] { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
.star-pick button[aria-current="true"] { background: var(--warning); border-color: var(--warning); }
.reading-status__return { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
/* R-267: the book page's comment box is now the shared, unstyled `_comment_editor.html`
   (byte-identical everywhere it is used, including the author/series pages, which carry
   no spacing rule of their own) — this caller-scoped margin replaces the old
   `.book-rating__comment` block's `margin-top` so the star row and the (now collapsed)
   comment disclosure keep the same breathing room they always had, without adding
   spacing to any OTHER caller of the shared editor. */
.book-rating .comment-editor { margin-top: var(--space-3); }

.tag-list { list-style: none; margin: 0 0 var(--space-2); padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.tag { display: inline-flex; align-items: center; gap: .3em; padding: .2em .3em .2em .7em; border-radius: var(--radius-pill); font-size: .8rem; }
.tag--own { background: var(--accent-weak); color: var(--accent); }
.tag--ghost { background: var(--bg-elev-2); color: var(--fg-faint); border: 1px dashed var(--border-strong); padding-right: .7em; }
/* R-273 (discoverability): the label is now a link to /library?tag=X&view=list (was a
   bare span) — without this, the global `a { color: var(--link) }` rule (base.css) would
   win over the .tag--own/.tag--ghost color set on the ANCESTOR <li>, since inheritance
   never beats a rule targeting the element itself. */
.tag__label { color: inherit; text-decoration: none; }
.tag__remove { display: inline; }
.tag__remove button { padding: 0 .35em; border-radius: var(--radius-pill); background: transparent; border: 0; color: inherit; line-height: 1; }
.tag__remove button:hover { color: var(--danger); }
.tag__ghost-badge { font-size: .62rem; text-transform: uppercase; letter-spacing: .04em; opacity: .8; }

/* Book-level Romance.io facts (ADR-0027, romanceio-tropes T6) — visually distinct from
   the viewer's editable .tag chips above: solid border, no remove control, plain link. */
.trope-list { list-style: none; margin: 0 0 var(--space-2); padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.trope-chip { display: inline-flex; }
.trope-chip a { display: inline-block; padding: .2em .7em; border-radius: var(--radius-pill); border: 1px solid var(--border-strong); font-size: .8rem; color: var(--fg); text-decoration: none; }
.trope-chip a:hover { border-color: var(--accent); color: var(--accent); }
.steam-indicator { display: inline-block; margin: 0 0 var(--space-2); font-size: .85rem; color: var(--warning); font-weight: 600; }

.bookmark-control, .watch-control { display: inline-flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); }
/* #113: the ACTIVE state of these toggles. Watching is green, Bookmarked is yellow, and
   both keep the colour on hover — the generic `button:hover` rule repaints to --accent,
   which would have made an active control turn indigo the moment you pointed at it, i.e.
   look inactive exactly when you were about to click it.

   Colour is the THIRD signal here, never the only one: the icon differs (eye vs check,
   outline vs filled ribbon) and so does the label. That is what keeps the state readable
   for a colourblind reader and under forced-colours, where these custom properties do not
   survive at all.

   The bookmark yellow is --warning, NOT a new --bookmark token. Every one of the ten
   theme blocks already tunes --warning to a mode-appropriate amber (#c9821f light,
   #f7b955 dark, and so on), and .steam-indicator already uses it as plain "the amber"
   rather than as a semantic. A fresh token would have had to be added to all ten or it
   would render one fixed colour on a dark background. The tradeoff, stated plainly: a
   theme that ever reddens its warnings reddens its bookmarks too. */
.watch-control button.is-active,
.watch-control button.is-active:hover { color: var(--success); border-color: var(--success); }
.bookmark-control button.is-active,
.bookmark-control button.is-active:hover { color: var(--warning); border-color: var(--warning); }
.bookmark-control button svg, .watch-control button svg { flex: none; }

/* =====================================================================
   Lists, bookmarks page, notifications, issues, search, auth, admin
   ===================================================================== */
.list-index, .bookmark-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.list-index-row, .bookmark-row {
  display: flex; align-items: center; gap: var(--space-3);
  background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--space-3) var(--space-4); box-shadow: var(--shadow-sm);
}
.list-index-row a, .bookmark-row a { flex: 1; font-weight: 600; }
.list-index-kind, .bookmark-kind, .list-kind { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-faint); border: 1px solid var(--border); border-radius: var(--radius-pill); padding: .12em .6em; }
.create-list, .add-item, .add-list-tag { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); }
.list-kind-toggle { border: 0; margin: 0; padding: 0; display: inline-flex; gap: var(--space-3); }
.list-members { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.list-member { display: flex; align-items: center; gap: var(--space-3); padding: .5em .7em; border-radius: var(--radius-sm); }
.list-member:hover { background: var(--bg-elev); }
.member-title { flex: 1; font-weight: 500; }
.member-author { color: var(--fg-faint); font-size: .82rem; }
.member-controls { display: inline-flex; gap: var(--space-1); }
.member-controls button { padding: .2em .55em; }
.list-tag-set { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.list-tag { display: inline-flex; align-items: center; gap: .3em; background: var(--accent-weak); color: var(--accent); border-radius: var(--radius-pill); padding: .2em .3em .2em .7em; font-size: .82rem; }
.list-candidate-results { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.list-candidate-result { background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.list-candidate-result label { display: flex; align-items: flex-start; gap: var(--space-2); width: 100%; min-width: 0; padding: var(--space-2) var(--space-3); }
.list-candidate-result input { flex: none; margin-top: .15em; }
.list-candidate-result label > span { min-width: 0; }
.list-candidate-title, .list-candidate-authors { display: block; }
.list-candidate-title { font-weight: 600; }
.list-candidate-authors { color: var(--fg-faint); font-size: .85rem; }
.list-candidate-source { display: inline-block; margin-top: .2em; color: var(--fg-faint); font-size: .72rem; letter-spacing: .04em; text-transform: uppercase; }

.notification-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.notification-list--nested { margin-top: var(--space-2); padding-left: var(--space-4); border-left: 2px solid var(--border); }
.notification {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  background: var(--bg-elev); border: 1px solid var(--border); border-left: 3px solid transparent;
  border-radius: var(--radius); padding: var(--space-3) var(--space-4);
  opacity: .72; /* read rows recede so an unread one is the thing the eye lands on */
}
/* W4/ADR-0033: unread must be UNMISTAKABLE, not just a faint tint — a heavier left
   accent bar and full-strength text/background, so a page of mixed read/unread rows
   sorts itself visually before the reader reads a single word. */
.notification.is-unread {
  border-left-width: 4px; border-left-color: var(--accent);
  background: var(--accent-weak); opacity: 1; font-weight: 500;
}
.notification__type { font-weight: 700; font-size: .8rem; text-transform: uppercase; letter-spacing: .03em; color: var(--accent); }
.notification__target { color: var(--fg-muted); font-size: .85rem; }
.notification__detail { flex: 1 1 100%; color: var(--fg-muted); font-size: .9rem; }
.notification__when { color: var(--fg-faint); font-size: .78rem; margin-left: auto; white-space: nowrap; }
.notification__action { flex-shrink: 0; }
.notification--empty { color: var(--fg-faint); list-style: none; }
.filter { color: var(--fg-muted); font-size: .9rem; }

/* A digest group reads like a notification row that happens to expand — same card
   language, plus a clear disclosure affordance on the summary line. */
.notification-group {
  background: var(--bg-elev); border: 1px solid var(--border); border-left: 3px solid transparent;
  border-radius: var(--radius); padding: var(--space-3) var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-2); opacity: .72;
}
.notification-group.is-unread { border-left-width: 4px; border-left-color: var(--accent); background: var(--accent-weak); opacity: 1; font-weight: 500; }
.notification-group summary {
  cursor: pointer; display: flex; align-items: center; gap: var(--space-2); list-style: none;
}
.notification-group summary::-webkit-details-marker { display: none; }
.notification-group summary::before { content: "▸"; color: var(--fg-muted); transition: transform .15s ease; }
.notification-group details[open] summary::before { transform: rotate(90deg); }
.notification-group .count {
  background: var(--accent-weak); color: var(--accent); border-radius: var(--radius-pill);
  padding: .1em .6em; font-size: .78rem; font-weight: 700;
}
.notification-group form { align-self: flex-start; }

.issue-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.issue { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-3) var(--space-4); }
.issue__category { font-weight: 700; }
/* R-009: the primary tap target on the row (its own comment calls it that) — was
   unstyled beyond inherited link defaults, which collapses an `<a>` to its text
   line-height (~22px). Never exercised at 402px before request-state-honesty made
   a reader's own request-linked auto-Issue appear here (previously /issues was
   always empty for the phone-gate's seeded reader). */
.issue__book { display: inline-flex; align-items: center; min-block-size: var(--tap-min); font-weight: 600; flex: 1 1 auto; }
.issue__status { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; padding: .12em .6em; border-radius: var(--radius-pill); }
.issue.is-open .issue__status { background: var(--warning-weak); color: var(--warning); }
.issue.is-closed .issue__status { background: var(--success-weak); color: var(--success); }
.issue__note { flex: 1 1 100%; color: var(--fg-muted); }

.search-authors, .search-books { list-style: none; margin: 0 0 var(--space-4); padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.search-author, .search-book { background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-3) var(--space-4); }
.search-book { display: flex; align-items: center; gap: var(--space-3); }
/* Guarded like .lib-cover et al in components.css (R-514): the height attribute
   cover_img() now sets is a presentational hint that would beat `aspect-ratio` and
   stretch the image, so block-size stays explicit auto with object-fit: cover. */
.search-book-cover {
  flex: 0 0 auto; width: 36px; aspect-ratio: var(--cover-ratio);
  block-size: auto; object-fit: cover;
  border-radius: var(--cover-radius); background: var(--bg-elev-2);
  box-shadow: var(--shadow-sm);
}
.search-book-meta { flex: 1; min-width: 0; }
.search-query { color: var(--fg-muted); }
.book-authors { color: var(--fg-faint); font-size: .85rem; }

/* Author page book rows with covers — geometry owned by components.css §5 (#784). */
.book-row-cover { background: var(--bg-elev-2); }

/* Library list row: small inline cover inside the Title cell. The cell must stay
   a normal table-cell — making it `display:flex` breaks `table-layout:fixed`
   column sizing (it shuffled the percentage widths so the Title column collapsed
   and clipped titles far earlier than the column allowed, #110). The cover sits
   inline so the cell's own overflow/ellipsis truncates the title only when it
   genuinely exceeds the 46% column. */
.lib-row-cover {
  display: inline-block; width: 24px; aspect-ratio: var(--cover-ratio);
  margin-right: var(--space-2); vertical-align: middle;
  border-radius: 3px; object-fit: cover; background: var(--bg-elev-2);
}

.error { color: var(--danger); background: var(--danger-weak); border-radius: var(--radius-sm); padding: .5em .7em; }

/* admin */
.attention-queue, .admin-accounts, .admin-jobs, .admin-tickets, .admin-landing { display: flex; flex-direction: column; gap: var(--space-4); }
/* #463: an operator alert reads sentence · when · Dismiss. The time is muted and pushed
   right so it never competes with the problem statement, and the whole row wraps on a
   phone rather than pushing the Dismiss button off-screen. */
.aq-alert { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
.aq-alert__text { flex: 1 1 18ch; }
.aq-alert__when { color: var(--fg-muted); font-size: .85rem; white-space: nowrap; }
.aq-alert__clear { margin: 0; }
.aq-alert__clear button { font-size: .85rem; padding: .2em .6em; }
/* #253: the jobs filter bar. Wraps rather than overflowing, so it survives a phone
   without needing its own media query. */
.jobs-filters { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: flex-end; }
.jobs-filters label { display: flex; flex-direction: column; gap: var(--space-1); font-size: .85rem; color: var(--fg-muted); }
.jobs-filters__clear { align-self: center; font-size: .85rem; }
.jobs-count { color: var(--fg-muted); font-size: .9rem; margin: 0; }
/* An error string is the one genuinely long cell here; cap it so one stack trace cannot
   set the width of every other column (#417). */
.jobs-error { max-width: 40ch; overflow-wrap: anywhere; }
.jobs-stamp { white-space: nowrap; font-variant-numeric: tabular-nums; }
/* #418: the "what it did" sentence. Summaries are truncated to 70 characters in Python, so
   this is a floor on readability rather than a guard against overflow — a short cell wraps
   a book title mid-word and makes the column unscannable. */
.jobs-did { min-width: 26ch; max-width: 46ch; }
.admin-landing__cards { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--space-3); }
.admin-card { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--space-3); background: var(--bg-elev); }
.admin-card a { font-weight: 600; }
.admin-card p { margin: var(--space-1) 0 0; color: var(--fg-faint); font-size: .9rem; }


/* error page (#353 surface, #410 image)
   The error surface shipped on plain base typography — these are its first styles, so
   there was no existing block to extend.
   The image is the owner's decorative 404 picture. It is bounded on BOTH axes on purpose:
   `max-width` keeps it from spanning a wide desktop, and `max-height: 40vh` is what
   guarantees the message and the "Back to the Library" action stay above the fold on a
   phone whatever the aspect ratio of the file dropped in. `height: auto` preserves that
   ratio, and `margin-inline: auto` centres it. */
.error-page__image {
  display: block; max-width: min(100%, 22rem); max-height: 40vh; height: auto;
  margin: var(--space-4) auto; border-radius: var(--radius-sm);
}

/* =====================================================================
   Responsive
   ===================================================================== */
@media (max-width: 860px) {
  .book-header { grid-template-columns: 1fr; }
  .book-cover { max-width: 200px; }
  /* #340: the Romance.io review queue. Its Actions cell holds three controls including a
     text input; inline at 390 px, Reject was clipped to a sliver and the Correct field
     showed about "https" — so Confirm became the only reliably usable action on a queue of
     LOW-CONFIDENCE matches, which is exactly the wrong thing to make easiest. Stacked, each
     control gets the full column width. */
  .review-actions { display: flex; flex-direction: column; gap: var(--space-2); align-items: stretch; }
  .review-actions form { display: flex; gap: var(--space-2); align-items: center; }
  .review-actions form.review-correct { flex-direction: column; align-items: stretch; }
  .review-actions label { display: flex; flex-direction: column; gap: var(--space-1); font-size: .85rem; }
  .review-actions input[type="text"] { width: 100%; min-width: 0; }
  .review-actions button { width: 100%; }
}
/* Phones. R-170 hid the header search here and made the Search tab the entry point; that is
   reversed: the header carries a magnifying-glass button at its top right, one tap opens the
   field beside it (omnibox.js puts the cursor in it), Enter or the field's magnifier
   submits, Close puts it away. Open, the field takes the whole header row — over the brand
   and title — and the dropdown spans the row under it. The Shelf Home (page) placement is
   never collapsed. */
@media (max-width: 599px) {
  .app-header__inner { position: relative; }
  /* order: after the tickets meter and the Guide chip, so the icon sits beside the bell, at the top right */
  .omnibox[data-placement="header"] { flex: 0 0 auto; min-width: 0; max-width: none; order: 1; }
  /* The input hides ON ITSELF, not only through its wrapper: the phone gate's and
     test_ui_shell's zero-size probes read an element's OWN computed display, and an input
     under a display:none ancestor would read as a live-looking 0x0 control (R-170's lesson). */
  .omnibox[data-placement="header"] .omnibox__field,
  .omnibox[data-placement="header"] input[type=search] { display: none; }
  .omnibox[data-placement="header"] .omnibox__toggle,
  .omnibox[data-placement="header"].is-open .omnibox__submit,
  .omnibox[data-placement="header"].is-open .omnibox__close {
    display: inline-flex; align-items: center; justify-content: center;
    inline-size: var(--tap-min); block-size: var(--tap-min); flex: 0 0 var(--tap-min);
    padding: 0; border: 0; background: none; color: var(--fg-muted);
  }
  .omnibox[data-placement="header"].is-open { position: static; }
  .omnibox[data-placement="header"].is-open .omnibox__toggle { display: none; }
  .omnibox[data-placement="header"].is-open .omnibox__field {
    display: flex; position: absolute; inset-block: 0; inset-inline: var(--space-3);
    z-index: 1; background: var(--bg-elev);
  }
  .omnibox[data-placement="header"].is-open input[type=search] { display: block; }
  .omnibox[data-placement="header"].is-open .omnibox-results { inset-inline: var(--space-3); }
  /* A late autocomplete response can land after Close/Escape; closed, nothing of it shows. */
  .omnibox[data-placement="header"]:not(.is-open) .omnibox-results { display: none; }
}
/* Narrow phones: the header row (brand, the Guide chip or the admin tickets meter, the search
   icon, the bell, the avatar) needs ~366px at its tightest, and the search icon added by the
   one search bar tipped 320px sideways by 34px. Below 380px the brand drops its wordmark and
   keeps the glyph (the link's aria-label still names it) in a full tap target. */
@media (max-width: 379px) {
  .brand__word { display: none; }
  .app-header .brand.shell-icon { min-inline-size: var(--tap-min); justify-content: center; }
}
@media (max-width: 520px) {
  .app-main { padding: var(--space-4) var(--space-3) var(--space-6); }
  .list-candidate-search label { align-items: stretch; flex-direction: column; width: 100%; }
  .list-candidate-search input[type=search] { width: 100%; min-width: 0; }
  /* The admin's live meter takes the compact header's centre. The page heading and
     More sheet retain the page identity and contextual Guide link. */
  .app-header__inner:has(.tickets-meter-host) .app-header__title { display: none; }
  .app-header__actions:has(.tickets-meter-host) .guide-context { display: none; }
}

/* The one sentence a recommendation card carries (#569, design §6). Explainability is a hard
   requirement there and bare scores are banned outright, so this is the only thing on the card
   that says WHY it was ranked — it must read as content, not as a tooltip or a footnote. */
.shelf-item__reason {
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
