/* JSB Books — starter stylesheet.
 *
 * Minimal on purpose. It exists so the cache-busting path (?v=<mtime> stamped
 * by jsb_books_render._asset_href) has a real file to point at from day one.
 *
 * Any NEW css/js file must be referenced through _asset_href() too — a bare
 * URL is the stale-asset bug this arrangement prevents.
 */

/* Text size, set on the settings page and applied before first paint.

   --txt-floor  nothing on the site renders smaller than this. Every font-size
                is written as max(its own size, the floor), so raising it lifts
                the small print -- author lines, notes, chips -- while leaving
                anything already larger alone. The hierarchy survives; only the
                squinting goes.
   --ui-scale   multiplies the lot. Every size in this file is in rem and no
                root size was ever set, so this is one honest multiplier rather
                than a hundred edits.

   Both default to no change, so an untouched browser sees the original design. */
html { font-size: calc(100% * var(--ui-scale, 1)); }

:root {
  --txt-floor: 0rem;
  --ui-scale: 1;
  --bg: #fdfdfb;
  --fg: #1e1c19;
  --muted: #6b6660;
  --rule: #e2ddd5;
  --accent: #6b4f2a;
  --ok: #2f6b3a;
  --err: #9a2b2b;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16150f;
    --fg: #eae6df;
    --muted: #a29a8f;
    --rule: #332f28;
    --accent: #c9a56a;
    --ok: #7fbf8c;
    --err: #e08a8a;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.6 Georgia, "Iowan Old Style", "Times New Roman", serif;
}

.wrap {
  max-width: 62rem;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 4rem;
}

h1, h2, h3 { font-weight: 600; line-height: 1.25; }
h1 { font-size: max(1.9rem, var(--txt-floor, 0rem)); margin: 0 0 .4rem; }
h2 { font-size: max(1.25rem, var(--txt-floor, 0rem)); margin: 2rem 0 .5rem; padding-bottom: .3rem; border-bottom: 1px solid var(--rule); }

.lede { color: var(--muted); margin-top: 0; }
.note { color: var(--muted); font-size: max(.875rem, var(--txt-floor, 0rem)); text-align: center; padding: 2rem 1rem; }

code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85em;
  background: color-mix(in srgb, var(--rule) 45%, transparent);
  padding: .1em .35em;
  border-radius: 3px;
}

a { color: var(--accent); }

.ok  { color: var(--ok); }
.err { color: var(--err); }

/* Log in / Log out corner control */

/* Persistent quick link in the top bar, beside the brand. A chip rather than a
   bare word so it reads as somewhere to GO, not as part of the site's name —
   "JSB Books Want" would be a sentence, and a wrong one. */
.quicklink {
  margin-left: .75rem;
  font: 600 .66rem/1 var(--sans, system-ui, sans-serif); font-size: max(.66rem, var(--txt-floor, 0rem));
  letter-spacing: .09em;
  text-transform: uppercase;
  text-decoration: none;
  padding: .26rem .6rem;
  border-radius: 999px;
  white-space: nowrap;
  color: var(--brass, #C89A4E);
  border: 1px solid currentColor;
  opacity: .82;
}
.quicklink:hover { opacity: 1; background: rgba(200, 154, 78, .12); }
/* No "you are here" fill. The filled state washed the chip out against the
   amber, and John can tell which page he is on from the page. One appearance
   everywhere is also one less thing to look at. (It filled the gear as well as
   Want on want pages, which was wrong anyway.) */

.top-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* WRAPS, because it must. On a 390px iPhone the brand, four quicklinks and
     the OWNER / Log out block come to 460px, and a nowrap flex row cannot
     fold — so the auth block sat 74px off the right edge, unreachable, and
     the whole PAGE scrolled sideways to reach it. That is what made the site
     feel unpinned: the horizontal scroll was the header's, and every other
     surface came along with it. */
  flex-wrap: wrap;
  gap: .35rem .75rem;
  padding: .5rem 1rem;
  border-bottom: 1px solid var(--rule);
  font-size: max(.8rem, var(--txt-floor, 0rem));
}
/* On a phone the auth block takes its own row rather than fighting for space
   on the first one; margin-left:auto keeps it right-aligned on wide screens. */
.top-bar .auth-bar { margin-left: auto; }
.auth-bar { display: flex; align-items: center; gap: .5rem; }
.brand {
  text-decoration: none; color: var(--muted);
  text-transform: uppercase; letter-spacing: .08em; font-size: max(.78rem, var(--txt-floor, 0rem));
}
.brand:hover { color: var(--accent); }
.auth-state { color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
/* The deployed release, left of OWNER. Quieter than the state word beside it:
   it is a fact to glance at, not a control. Not uppercased — it is a tag name
   with a dot in it, and shouting RELEASE_1.0 reads like a warning. */
.build-tag { color: var(--muted); opacity: .75; font-size: .85em; letter-spacing: .02em; }

/* Login page */
.books-login { display: grid; place-items: center; min-height: 100vh; }
.login-card {
  width: min(24rem, 92vw);
  padding: 2rem;
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: color-mix(in srgb, var(--bg) 90%, var(--fg) 3%);
}
.login-card h1 { font-size: max(1.3rem, var(--txt-floor, 0rem)); }
.login-lede { color: var(--muted); font-size: max(.9rem, var(--txt-floor, 0rem)); }
.login-card label { display: block; margin-top: 1rem; font-size: max(.85rem, var(--txt-floor, 0rem)); color: var(--muted); }
.login-card input[type=password] {
  width: 100%; padding: .55rem .7rem; margin-top: .3rem;
  border: 1px solid var(--rule); border-radius: 6px;
  background: var(--bg); color: var(--fg); font-size: max(1rem, var(--txt-floor, 0rem));
}
.login-card button {
  width: 100%; margin-top: 1.1rem; padding: .6rem;
  border: 0; border-radius: 6px; cursor: pointer;
  background: var(--accent); color: var(--bg); font-size: max(1rem, var(--txt-floor, 0rem));
}
.login-error { color: var(--err); font-size: max(.9rem, var(--txt-floor, 0rem)); }
.login-back { text-align: center; font-size: max(.85rem, var(--txt-floor, 0rem)); margin-bottom: 0; }

/* Rendered private documents (jsb_books_doc.py) ------------------------- */
.doc h3 { font-size: max(1.02rem, var(--txt-floor, 0rem)); margin: 1.6rem 0 .2rem; }
.doc h3 + p { margin-top: .2rem; color: var(--muted); font-size: max(.9rem, var(--txt-floor, 0rem)); }
/* h4 was emitted by two published documents and styled by nothing, so it fell
   through to the browser's defaults — bold, but SMALLER than the h3 above it,
   which reads as a level going the wrong way. Given weight and colour here
   rather than left to inherit: this palette is scoped to a body class, and an
   element with no rule of its own is how three UI bugs started. */
.doc h4 {
  font-size: max(.95rem, var(--txt-floor, 0rem));
  font-weight: 600; color: var(--accent);
  margin: 1.4rem 0 .2rem;
}
.doc h4 + p { margin-top: .2rem; color: var(--muted); font-size: max(.9rem, var(--txt-floor, 0rem)); }
.doc hr { border: 0; border-top: 1px solid var(--rule); margin: 2.5rem 0; }
.doc blockquote {
  margin: .9rem 0;
  padding: .5rem .9rem;
  border-left: 3px solid var(--accent);
  background: color-mix(in srgb, var(--rule) 30%, transparent);
  font-size: max(.92rem, var(--txt-floor, 0rem));
}
.doc blockquote p { margin: 0; }
.doc ul, .doc ol { padding-left: 1.3rem; }
.doc li { margin: .25rem 0; }
.doc pre {
  background: color-mix(in srgb, var(--rule) 45%, transparent);
  padding: .8rem 1rem; border-radius: 6px; overflow-x: auto;
}
.doc pre code { background: none; padding: 0; font-size: .82em; }

/* Wide tables scroll inside their own box — the page body must never scroll
 * sideways on a phone. */
.table-scroll { overflow-x: auto; margin: .8rem 0; }
.doc table { border-collapse: collapse; width: 100%; font-size: max(.84rem, var(--txt-floor, 0rem)); }
.doc th, .doc td {
  border: 1px solid var(--rule);
  padding: .35rem .55rem;
  text-align: left;
  vertical-align: top;
}
.doc th {
  background: color-mix(in srgb, var(--rule) 40%, transparent);
  font-weight: 600; white-space: nowrap;
}
.doc td code { font-size: .8em; }

/* JSB Reading (/Books/reading/) -----------------------------------------
 *
 * Palette taken from Cowork's mockup (jsb-reading-mockup.html), which John
 * designed with it. It is a committed DARK scheme — cool slate chrome, brass
 * accent, and a warm panel for the private notes — so it is defined on the
 * page's own class rather than inside a prefers-color-scheme block. It must
 * look the same whatever the visitor's OS theme is; this is the design, not a
 * dark variant of one.
 *
 * The two note fields are deliberately different surfaces. Public notes sit on
 * cool slate like the rest of the chrome; personal notes sit on the warm panel.
 * That warmth is the signal that the field is private — it is doing work, so
 * do not flatten the two into one style.
 */
.books-reading {
  --ink:       #0F131B;   /* page */
  --slate:     #171D28;   /* shelf, public-note panel */
  --slate2:    #1E2634;   /* raised */
  --edge:      #2A3444;
  --edge2:     #3A4759;
  --warmpanel: #1B1710;   /* private notes */
  --warmink:   #EFE2C6;
  --warmmute:  #8A7A5C;
  --brass:     #C89A4E;
  --brass-dim: #8A6B33;
  --patina:    #4F8C7C;   /* read / saved */
  --rose:      #B4645F;   /* orphaned */
  --text:      #E4E9F1;
  --mute:      #818FA6;
  --mute2:     #5C6880;
  --r: 3px;

  background: var(--ink);
  color: var(--text);
  font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.books-reading h1, .books-reading h2 {
  font-family: Fraunces, "Iowan Old Style", Georgia, serif;
  font-weight: 500;
}
.books-reading em { color: var(--brass); font-style: italic; }
.books-reading a { color: var(--brass); }
.books-reading :focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }

.books-reading .top-bar {
  background: var(--slate); border-bottom: 1px solid var(--edge);
}
.books-reading .brand { color: var(--mute); }
.books-reading .brand:hover { color: var(--brass); }
.books-reading .auth-state,
.books-reading .build-tag { color: var(--mute2); }
.books-reading .auth-link { color: var(--brass); }

.reading {
  display: grid;
  grid-template-columns: minmax(15rem, 18.3rem) 1fr;
  gap: 0;
  min-height: calc(100vh - 2.4rem);
  align-items: stretch;
}

.shelf {
  background: var(--slate);
  border-right: 1px solid var(--edge);
  padding: 1rem .9rem 1.5rem;
  display: flex; flex-direction: column; min-height: 0;
  position: sticky; top: 0; max-height: 100vh;
}
.find input {
  width: 100%; padding: .45rem .6rem; font: inherit; font-size: max(.85rem, var(--txt-floor, 0rem));
  /* 2px and brass-tinted BEFORE focus. At 1px in --edge2 the box read as part
     of the background and John could not find it to click into — a search
     field nobody can see is a search field nobody uses. Half-strength so it
     invites rather than shouts; focus takes it to full. */
  border: 2px solid rgba(200, 154, 78, .45); border-radius: var(--r);
  background: var(--ink); color: var(--text);
}
.find input:hover { border-color: rgba(200, 154, 78, .7); }
.find input:focus { border-color: var(--brass, #C89A4E); outline: none; }
.find input::placeholder { color: var(--mute2); }
/* Scoped: the front page has its own sort control with its own palette, and
   these variables (--mute2, --ink, --edge2) only exist inside .books-reading. */
.books-reading .sortby {
  display: flex; align-items: center; gap: .4rem; margin-top: .45rem;
  font-size: max(.6875rem, var(--txt-floor, 0rem)); color: var(--mute2);
  text-transform: uppercase; letter-spacing: .07em;
}
.books-reading .sortby select {
  flex: 1; min-width: 0; font: inherit; font-size: max(.78rem, var(--txt-floor, 0rem));
  text-transform: none; letter-spacing: 0;
  padding: .28rem .4rem; border: 1px solid var(--edge2); border-radius: var(--r);
  background: var(--ink); color: var(--text);
}
.chips { display: flex; flex-wrap: wrap; gap: .3rem; margin: .7rem 0 .5rem; }
/* The kind row sits directly under the filter row and is a second group of the
   same control, not a new block. .shelf is a flex column so the two margins do
   not collapse: left alone they read 1.2rem apart, further than the gap above
   the first row. Only the top margin is overridden — everything else is
   inherited from .chips on purpose. */
.chipkind { margin-top: 0; }
/* The First-editions row is a THIRD group and the same argument applies: same
   control, no new block, so only the top margin changes. Its note explains
   what the filter did to the rows below — that they are now a particular COPY
   and not the usual representative — which is not something a chip label has
   room to say. */
.chipfirst { margin-top: 0; align-items: center; }
.chipnote {
  font-size: max(.625rem, var(--txt-floor, 0rem)); color: var(--mute);
  letter-spacing: .02em; font-style: italic;
}
.chip {
  font-size: max(.6875rem, var(--txt-floor, 0rem)); padding: .22rem .6rem; border-radius: 999px;
  border: 1px solid var(--edge2); color: var(--mute); text-decoration: none;
  letter-spacing: .02em;
}
.chip:hover { color: var(--text); border-color: var(--mute2); }
.chip.on { background: var(--brass); border-color: var(--brass); color: #1A1204; }
.count { font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--mute2); margin: .1rem 0 .6rem;
         text-transform: uppercase; letter-spacing: .07em; }

.bklist { list-style: none; margin: 0; padding: 0; overflow-y: auto; flex: 1; }
/* The jacket on each row. 2.2rem, so the list is still something to run down
   rather than a shelf to browse — that is what the shelf view is for. */
/* ---- the reading page's two views -------------------------------------
   List and Shelves do different jobs — the list is for running down quickly,
   the shelf for looking — so the toggle marks the current one rather than
   removing it: a control that vanishes as you use it makes the row jump. */
.viewsw { display: flex; gap: .3rem; margin: 0 0 .6rem; }
.vw { font-size: max(.7rem, var(--txt-floor, 0rem)); letter-spacing: .06em;
  text-transform: uppercase; padding: .3rem .8rem; border-radius: 999px;
  text-decoration: none; border: 1px solid var(--edge2); color: var(--mute);
  min-height: 2.1rem; display: inline-flex; align-items: center; }
.vw:hover { color: var(--brass); border-color: var(--brass); }
.vw.on { background: var(--brass); border-color: var(--brass); color: #1A1204; }

/* Shelf view: no left panel, so the controls sit above the boards. */
.readingboards { display: block; padding: 1rem 1.2rem 3rem; }
.boardbar { max-width: 70rem; margin: 0 auto .6rem; }
.boardfind { max-width: 26rem; margin-bottom: .5rem; }
.boards { max-width: 70rem; margin: 0 auto; }
/* THE OPEN BOOK SHARES THE BOARDS' LEFT EDGE, not the window's.
   .boardbar and .boards are centred at 70rem; .detail is their sibling with a
   reading measure of its own and, until now, no centring at all — so on a wide
   screen the shelf sat in the middle while the book it opened was stranded
   against the far left, with a third of the window lying empty between them.

   Aligned by margin rather than by widening .detail to 70rem, because the 58rem
   cap is a MEASURE: stretching it to match the furniture would run the binding
   and provenance notes past a comfortable line length to no purpose. Under
   70rem the max() collapses to zero and nothing changes, which is the width
   John reported already reads correctly. */
.readingboards > .detail {
  margin-left: max(0px, calc((100% - 70rem) / 2));
  margin-right: auto;
}
.boardrow { margin-bottom: .6rem; }
.boardpage { display: flex; align-items: center; gap: .9rem; flex-wrap: wrap;
  margin: .2rem 0 1rem; }
.boardpage .count { margin: 0; }
/* The selected book, marked on the board it came from. */
.boards .slot.sel img, .boards .slot.sel .jacket { outline: 2px solid var(--brass);
  outline-offset: 2px; }

.bkart { flex: 0 0 2.2rem; width: 2.2rem; aspect-ratio: 2/3; object-fit: cover;
  border-radius: 2px; border: 1px solid var(--edge2); background: var(--slate2); }
.bkartnone { border-style: dashed; }
.bk {
  /* A grid, not a block, now that a jacket sits beside the text: the cover
     spans both rows in column one and the title and its meta line stack in
     column two. Rows with no art keep the same columns, so the titles stay
     aligned down the list — a ragged left edge is worse than a dashed box. */
  display: grid; grid-template-columns: 2.2rem 1fr;
  column-gap: .55rem; align-items: center;
  padding: .42rem .55rem; border-radius: var(--r);
  text-decoration: none; color: var(--text);
  border-left: 2px solid transparent;
}
.bk > .bkart { grid-row: 1 / span 2; }
.bk > .bkt, .bk > .bkm { grid-column: 2; min-width: 0; }
.bk:hover { background: var(--slate2); }
.bk.sel { background: var(--edge); border-left-color: var(--brass); }
.bk.noted { border-left-color: var(--patina); }
.bkt { display: block; font-size: max(.875rem, var(--txt-floor, 0rem)); line-height: 1.35; }
.bkm { display: block; font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--mute2); margin-top: .1rem; }

/* Content is capped rather than filling the window: measure matters more than
 * using the space, and an unconstrained column stranded the cover in the far
 * corner. */
.detail { min-width: 0; padding: 1.6rem 2rem 4rem; max-width: 58rem; }
.detail.empty { color: var(--mute); }

/* Cover LEFT of the identifying text, as one block. */
.bookhead { display: flex; gap: 1.5rem; align-items: flex-start; margin-bottom: .4rem; }
.bookhead .cover {
  flex: 0 0 9.5rem; width: 9.5rem; height: auto; align-self: flex-start;
  border-radius: var(--r); border: 1px solid var(--edge);
  background: var(--slate2);
}
.bookhead .cover.nocover { aspect-ratio: 2 / 3; border-style: dashed; border-color: var(--edge2); }
.coverbtn {
  flex: 0 0 9.5rem; padding: 0; border: 0; background: none; cursor: zoom-in;
  line-height: 0; border-radius: var(--r);
}
.coverbtn .cover { width: 100%; }
.coverbtn:hover .cover { border-color: var(--brass); }

/* Lightbox. Deliberately not a new window — it is one image, and a tab is a
 * heavier thing to close than a keypress. */
.lb {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(7, 9, 13, .92);
  display: grid; place-items: center; padding: 2rem;
}
.lb[hidden] { display: none; }
.lb figure { margin: 0; display: grid; justify-items: center; gap: .6rem; }
.lb img {
  max-height: calc(100vh - 7rem); height: auto; width: auto;
  border-radius: var(--r); border: 1px solid var(--edge);
  box-shadow: 0 12px 48px rgba(0,0,0,.6);
}
.lb figcaption { font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--mute2); letter-spacing: .06em; }
.lbclose {
  position: absolute; top: 1rem; right: 1.4rem;
  background: none; border: 0; color: var(--mute); cursor: pointer;
  font-size: max(2rem, var(--txt-floor, 0rem)); line-height: 1; padding: .2rem .5rem;
}
.lbclose:hover { color: var(--brass); }
.bookid { min-width: 0; flex: 1 1 auto; }
.detail h1 { font-size: max(1.7rem, var(--txt-floor, 0rem)); margin: 0 0 .15rem; line-height: 1.2; }
.detail h2 {
  font-size: max(.72rem, var(--txt-floor, 0rem)); text-transform: uppercase; letter-spacing: .1em;
  color: var(--mute); border: 0; padding: 0; margin: 1.9rem 0 .4rem;
  font-family: "IBM Plex Sans", sans-serif; font-weight: 600;
}
.detail .sub { color: var(--mute); font-style: italic; margin: 0 0 .5rem;
               font-family: Fraunces, Georgia, serif; font-size: max(1.05rem, var(--txt-floor, 0rem)); }
.detail .by { font-size: max(.95rem, var(--txt-floor, 0rem)); margin: .2rem 0; color: var(--text); }
.detail .facts { font-size: max(.78rem, var(--txt-floor, 0rem)); color: var(--mute2); margin: .15rem 0; }
.detail .plot { font-size: max(.9rem, var(--txt-floor, 0rem)); color: var(--mute); max-width: 44rem; }

.notes { clear: right; margin: 1.4rem 0; max-width: 46rem; }
.hint { font-size: max(.7rem, var(--txt-floor, 0rem)); color: var(--mute2); margin: 0 0 .4rem;
        letter-spacing: .03em; }
.hint.private { color: var(--warmmute); }
.hint.private::before { content: "\1F512  "; }

textarea.edit {
  width: 100%; font: inherit; font-size: max(.92rem, var(--txt-floor, 0rem)); line-height: 1.6;
  padding: .8rem .9rem; border-radius: var(--r); resize: vertical;
  background: var(--slate); border: 1px solid var(--edge); color: var(--text);
}
textarea.edit::placeholder { color: var(--mute2); font-style: italic; }
/* Private notes get the warm panel. The colour IS the privacy signal. */
textarea.edit[data-field="personal_notes"] {
  background: var(--warmpanel); color: var(--warmink);
  border-color: #2E2618;
  font-family: Fraunces, "Iowan Old Style", Georgia, serif;
}
textarea.edit[data-field="personal_notes"]::placeholder { color: var(--warmmute); }

.statusrow { display: flex; align-items: center; gap: .7rem; margin-top: .9rem; }
.statusrow label { font-size: max(.7rem, var(--txt-floor, 0rem)); color: var(--mute2);
                   text-transform: uppercase; letter-spacing: .08em; }
.statusrow select {
  font: inherit; font-size: max(.82rem, var(--txt-floor, 0rem)); padding: .3rem .45rem;
  border: 1px solid var(--edge2); border-radius: var(--r);
  background: var(--ink); color: var(--text);
}
#save {
  font: inherit; font-size: max(.8rem, var(--txt-floor, 0rem)); font-weight: 600; letter-spacing: .04em;
  padding: .4rem 1.15rem; border: 0; border-radius: var(--r);
  background: var(--brass); color: #1A1204; cursor: pointer;
}
#save:hover { background: #D6A755; }
#save[disabled] { opacity: .55; cursor: default; }
.saved { font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--patina); }

/* Horizontally scrolling cover rail. Scrolls inside its own box so the page
 * body never scrolls sideways. */
.rail {
  display: flex; gap: .85rem; overflow-x: auto; padding: .3rem .2rem 1rem;
  scroll-snap-type: x proximity; scrollbar-width: thin;
  scrollbar-color: var(--edge2) transparent;
}
.rail::-webkit-scrollbar { height: 8px; }
.rail::-webkit-scrollbar-thumb { background: var(--edge2); border-radius: 4px; }
.railitem {
  flex: 0 0 6.5rem; width: 6.5rem; text-decoration: none; color: var(--text);
  scroll-snap-align: start;
}
.railitem img, .railitem .nocover {
  display: block; width: 6.5rem; height: 9.75rem; object-fit: cover;
  border-radius: var(--r); border: 1px solid var(--edge);
  background: var(--slate2);
}
.railitem .nocover {
  display: grid; place-items: center; padding: .4rem;
  border-style: dashed; border-color: var(--edge2);
}
.railitem .nocover span {
  font-size: max(.68rem, var(--txt-floor, 0rem)); line-height: 1.3; color: var(--mute2);
  text-align: center; overflow: hidden; display: -webkit-box;
  -webkit-line-clamp: 5; -webkit-box-orient: vertical;
}
.railitem:hover img, .railitem:hover .nocover { border-color: var(--brass); }
.rt {
  display: block; font-size: max(.74rem, var(--txt-floor, 0rem)); line-height: 1.3; margin-top: .35rem;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.ry { display: block; font-size: max(.68rem, var(--txt-floor, 0rem)); color: var(--mute2); }

.orphan { border-left: 2px solid var(--rose); padding: .1rem 0 .1rem .95rem; margin: 1.3rem 0; }
.orphan h2 { margin: 0 0 .15rem; font-size: max(1.05rem, var(--txt-floor, 0rem)); text-transform: none;
             letter-spacing: 0; color: var(--text);
             font-family: Fraunces, Georgia, serif; }
.orphan p { font-size: max(.88rem, var(--txt-floor, 0rem)); margin: .3rem 0; }

.priv { font-size: max(.82rem, var(--txt-floor, 0rem)); display: grid; grid-template-columns: auto 1fr;
        gap: .25rem .9rem; max-width: 34rem; }
.priv dt { color: var(--mute2); }
.priv dd { margin: 0; color: var(--warmink); }

/* A CLZ custom field's label. Subordinate to the "Private" h2 above it, so it
   is sized between that and the body — big enough to separate two custom
   fields from each other, quiet enough not to compete for the section. */
.udfhead { font-size: max(.92rem, var(--txt-floor, 0rem)); font-weight: 600;
           color: var(--mute2); margin: 1rem 0 .3rem; }

.books-reading .note { color: var(--mute2); text-align: left; padding: .6rem 0; }

@media (max-width: 52rem) {
  .reading { grid-template-columns: 1fr; }
  .shelf { position: static; max-height: none; border-right: 0;
           border-bottom: 1px solid var(--edge); }
  .bklist { max-height: 16rem; }
  .detail { padding: 1.2rem 1rem 3rem; }
  .bookhead { gap: 1rem; }
  .bookhead .cover { flex-basis: 6.5rem; width: 6.5rem; }
}

@media (max-width: 40rem) {
  h1 { font-size: max(1.5rem, var(--txt-floor, 0rem)); }
  .wrap { padding: 1rem .9rem 3rem; }
}

/* Latest reading entry, on the scaffold page ---------------------------- */
.latest { margin: 1.5rem 0 2rem; }
.latestcard {
  display: flex; gap: 1rem; align-items: flex-start;
  padding: .9rem; border: 1px solid var(--rule); border-radius: 8px;
  background: color-mix(in srgb, var(--rule) 18%, transparent);
}
.latestthumb { flex: 0 0 4.5rem; width: 4.5rem; line-height: 0; }
.latestthumb img {
  width: 100%; height: auto; border-radius: 4px; border: 1px solid var(--rule);
}
.latestthumb.nocover {
  display: block; aspect-ratio: 2 / 3; border: 1px dashed var(--rule);
  border-radius: 4px;
}
.latestmeta { min-width: 0; }
.latesttitle { font-size: max(1.05rem, var(--txt-floor, 0rem)); text-decoration: none; }
.latesttitle:hover { text-decoration: underline; }
.latest .facts { font-size: max(.8rem, var(--txt-floor, 0rem)); color: var(--muted); margin: .2rem 0 0; }

/* Working pages index, on the scaffold page ----------------------------- */
.working { margin: 1.5rem 0 2rem; }
.pages { list-style: none; padding: 0; margin: .4rem 0; }
.pages li { padding: .3rem 0; border-bottom: 1px solid var(--rule); }
.pages li:last-child { border-bottom: 0; }
.pages a { font-size: max(1rem, var(--txt-floor, 0rem)); text-decoration: none; }
.pages a:hover { text-decoration: underline; }
.pages .blurb { display: block; font-size: max(.8rem, var(--txt-floor, 0rem)); color: var(--muted); }

/* Work-level reading notes, shared across copies ------------------------ */
.books-reading textarea.workedit {
  background: var(--warmpanel); color: var(--warmink);
  border-color: #2E2618;
  font-family: Fraunces, "Iowan Old Style", Georgia, serif;
}
.books-reading textarea.workedit::placeholder { color: var(--warmmute); }
.shared {
  font-size: max(.76rem, var(--txt-floor, 0rem)); color: var(--mute); margin: .4rem 0 0;
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
}
.shared a { color: var(--brass); }
.shared select {
  font: inherit; font-size: max(.78rem, var(--txt-floor, 0rem)); padding: .22rem .35rem; max-width: 22rem;
  border: 1px solid var(--edge2); border-radius: var(--r);
  background: var(--ink); color: var(--text);
}
.linkbtn {
  font: inherit; font-size: max(.72rem, var(--txt-floor, 0rem)); padding: .18rem .6rem; cursor: pointer;
  background: none; color: var(--mute); border: 1px solid var(--edge2);
  border-radius: 999px;
}
.linkbtn:hover { color: var(--brass); border-color: var(--brass); }

/* CLZ's own notes — read-only, historical */
.clznote {
  white-space: pre-wrap;
  font-size: max(.88rem, var(--txt-floor, 0rem)); line-height: 1.55;
  color: var(--warmink); background: var(--warmpanel);
  border: 1px solid #2E2618; border-left: 3px solid var(--brass-dim);
  border-radius: var(--r); padding: .7rem .9rem; max-width: 46rem;
  max-height: 22rem; overflow-y: auto;
}

/* Public review, as a visitor sees it */
.review {
  white-space: pre-wrap; font-size: max(.95rem, var(--txt-floor, 0rem)); line-height: 1.6;
  max-width: 44rem; color: var(--text);
  border-left: 2px solid var(--brass); padding-left: .9rem;
}

/* What John owns of this book */
.owned { margin: .5rem 0 0; display: flex; flex-wrap: wrap; gap: .35rem; }
.ochip {
  display: inline-block;
  font-size: max(.68rem, var(--txt-floor, 0rem)); letter-spacing: .04em; padding: .16rem .6rem;
  border: 1px solid var(--edge2); border-radius: 999px; color: var(--mute);
  text-decoration: none;
}
/* A chip is a link to that copy, except the one you are already reading. The
   current chip has to look like a state and not a dead link, so it takes the
   brass fill the active filter chip uses. */
a.ochip:hover { color: var(--text); border-color: var(--brass); }
.ochip.on {
  background: var(--brass); border-color: var(--brass); color: #1A1204;
  cursor: default;
}
/* Per-copy notes under a single book */
.copynote { margin: .9rem 0; }
.copyhead {
  font-size: max(.72rem, var(--txt-floor, 0rem)); text-transform: uppercase; letter-spacing: .07em;
  color: var(--mute2); margin: 0 0 .25rem;
}
.copylink { font-size: max(.7rem, var(--txt-floor, 0rem)); text-transform: none; letter-spacing: 0; }

/* A note written on the site, as opposed to imported from CLZ — visibly
 * different so the two are not mistaken for each other. */
.copyown {
  white-space: pre-wrap; font-size: max(.88rem, var(--txt-floor, 0rem)); line-height: 1.55;
  color: var(--text); background: var(--slate);
  border: 1px solid var(--edge); border-left: 3px solid var(--patina);
  border-radius: var(--r); padding: .7rem .9rem; max-width: 46rem;
}

/* ======================================================================
   The bookshelf on the front page
   ----------------------------------------------------------------------
   Modelled on the CLZ web app's shelf view. The wood is DRAWN, not an
   image: CLZ's texture is their asset, and a gradient shelf re-flows to
   any row height or column count without shipping a single byte.

   The trick that makes it a shelf rather than a grid is one repeating
   background whose period equals the grid row height, so a board appears
   under every row no matter how many rows the viewport produces. Change
   --rowh and the boards follow by themselves.
   ====================================================================== */
/* Both knobs are CSS variables with a default baked into the fallback, so the
   page is correct with JavaScript off and shelf.js only ever overrides.
   
   The width knob resizes THE SHELF ONLY, never the wrapper. It used to set the
   wrapper's max-width, which meant the toolbar holding the slider was itself
   being resized as you dragged — the control slid out from under the cursor.
   Everything except the shelf and its pager now keeps a fixed, centred measure,
   so the slider stays put while the shelf grows past it or shrinks inside it. */
.wrap.wide { max-width: 100%; }
.wrap.wide > * {
  max-width: 84rem;          /* fixed: the toolbar must not move */
  margin-left: auto; margin-right: auto;
}
.wrap.wide > .woodshelf,
.wrap.wide > .pager {
  max-width: min(var(--shelf-max, 84rem), 100%);
}

.woodshelf {
  /* ONE knob: --col, the narrowest a column may be. Everything else derives.
     Row pitch and cover height MUST follow column width rather than be set
     beside it — a cover is bound by whichever of the two it reaches first, and
     when they drift apart the shelf either clips covers or grows a band of
     dead air above every row. 1.45 is just under a 2:3 cover's height, which
     keeps height the binding constraint and stands the books up to the shelf
     above them. */
  --col: var(--shelf-col, 102px);                   /* the size control */
  --board: 17px;                                    /* visible board front */
  --side: 18px;                                     /* the bookcase end panels */
  --art: calc(var(--col) * 1.45);                   /* tallest a cover may be */
  --rowh: calc(var(--art) + var(--board) + 14px);   /* board repeats at this */

  /* --------------------------------------------------------------------
     TIMBER. Light cherry is the default. Every colour below is sampled from
     the texture it belongs to — the light photo averages #b85023 and the dark
     #3d0e01 — so the shelves and end panels are cut from the same wood as the
     back rather than being a guess that half matches.

     The panels are OPAQUE, which is not a style choice. Translucent ones let
     the horizontal shelf lines show straight through, so every shelf appeared
     to carry on across the end boards. A real side plank is solid; making
     these solid removes the ghosting and also caps the texture's tile seam at
     the two edges the shelves no longer reach.
     -------------------------------------------------------------------- */
  --wood-tex:  url("shelf-wood-light.jpg");
  --edge-out:  #3A1004;   /* outside corner of the end panel, in shadow */
  --edge-body: #8D3610;
  --edge-lit:  #E79154;   /* inner edge, facing you, catching the light */
  --brd-lit:   #F0A061;   /* front edge of a shelf */
  --brd-up:    #B8551F;
  --brd-face:  #6E2A0C;   /* the shelf face, in the books' shadow */
  --brd-foot:  #2A0C02;   /* opaque: also caps the texture's tile seam */
  --wash1: rgba(30, 10, 0, .18);
  --wash2: rgba(20, 6, 0, .30);

  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--col), 1fr));
  grid-auto-rows: var(--rowh);
  gap: 0;
  margin: 1rem 0 1.5rem;
  /* Books stop clear of the end panels instead of running under them. Only
     LEFT and RIGHT: vertical padding would move the grid without moving the
     background, and the seam alignment below depends on the two staying
     locked together. */
  padding: 0 var(--side);
  border-radius: 6px;
  overflow: hidden;
  box-shadow: inset 0 0 60px rgba(0, 0, 0, .45);

  background-color: var(--edge-out);
  /* Woodgrain from Magnific, free licence, ATTRIBUTION REQUIRED — the credits
     are rendered on the page, not just noted here, because a comment does not
     satisfy a licence:
       light — "Cherry wood texture", creativepack
       dark  — "Black striped cardboard texture", kues1

     THE SEAM. Neither photo is tileable, so instead of hiding a seam it is
     PLACED: each tile is sized to exactly three shelf rows, which puts its one
     horizontal join on a row boundary — and a row boundary is where an opaque
     shelf foot is drawn. Three rows rather than one so the figure in the grain
     does not obviously repeat. No vertical seam, because the tile spans the
     full width. Both follow --rowh, so the size slider cannot break either.

     Layer order is paint order, first on top: end panels, shelves, wash, wood.
     The panels are outermost because that is how a bookcase is built — the
     shelves butt into the sides. */
  background-image:
    /* 1. left end panel — opaque, lit on its inner edge */
    linear-gradient(90deg,
      var(--edge-out) 0,
      var(--edge-body) 3px,
      var(--edge-body) calc(var(--side) - 6px),
      var(--edge-lit) calc(var(--side) - 2px),
      var(--edge-out) var(--side)),
    /* 2. right end panel — the same plank, mirrored */
    linear-gradient(270deg,
      var(--edge-out) 0,
      var(--edge-body) 3px,
      var(--edge-body) calc(var(--side) - 6px),
      var(--edge-lit) calc(var(--side) - 2px),
      var(--edge-out) var(--side)),
    /* 3. ONE shelf, tiled down by background-repeat rather than a repeating
          gradient, so it can be INSET: it now spans only the gap between the
          two end panels and stops cleanly at each, instead of running the full
          width and showing through them. */
    linear-gradient(to bottom,
      transparent 0,
      transparent calc(var(--rowh) - var(--board)),
      var(--brd-lit)  calc(var(--rowh) - var(--board)),
      var(--brd-up)   calc(var(--rowh) - var(--board) + 2px),
      var(--brd-face) calc(var(--rowh) - var(--board) + 6px),
      var(--brd-foot) calc(var(--rowh) - 4px),
      var(--brd-foot) var(--rowh)),
    /* 4. knock the photo back a little */
    linear-gradient(180deg, var(--wash1), var(--wash2)),
    /* 5. the wood itself */
    var(--wood-tex);
  background-size:
    var(--side) 100%,
    var(--side) 100%,
    calc(100% - var(--side) * 2) var(--rowh),
    auto,
    100% calc(var(--rowh) * 3);
  background-position: left top, right top, var(--side) top, 0 0, 0 0;
  background-repeat: no-repeat, no-repeat, repeat-y, repeat, repeat;
}

/* The dark option: a deep red-stained case, averaging #7d0102. Only the
   colours change — the geometry above is shared, so the inset shelves and
   opaque panels behave identically. The walnut this replaced averaged #3d0e01
   and was simply too dark to read as anything. */
:root[data-wood="dark"] .woodshelf {
  --wood-tex:  url("shelf-wood-dark.jpg");
  --edge-out:  #2A0001;
  --edge-body: #5E0001;
  --edge-lit:  #C4432E;
  --brd-lit:   #D4553C;
  --brd-up:    #8A0A08;
  --brd-face:  #480002;
  --brd-foot:  #1A0001;
  --wash1: rgba(20, 0, 0, .18);
  --wash2: rgba(10, 0, 0, .32);
}

/* Each cell is a slot on the board: books stand on it, bottom-aligned. */
.slot {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0 5px var(--board);
  text-decoration: none;
  min-width: 0;
}
.slot img {
  max-height: var(--art);
  max-width: 100%;
  width: auto; height: auto;
  border-radius: 1px 2px 2px 1px;
  /* Lit from the top-left like the shelf is, plus a page-edge highlight on
     the right so a pale cover does not dissolve into the next one. */
  box-shadow:
    0 5px 9px rgba(0, 0, 0, .6),
    1px 0 0 rgba(255, 255, 255, .10),
    inset 0 1px 0 rgba(255, 255, 255, .13);
  transition: transform .13s ease, box-shadow .13s ease;
}
.slot:hover img, .slot:focus-visible img {
  transform: translateY(-5px);
  box-shadow: 0 11px 16px rgba(0, 0, 0, .7), 0 0 0 2px var(--accent);
}
.slot:focus-visible { outline: none; }

/* No cover art: a plain jacket with the title on it. A gap on a shelf reads
   as a broken image; a blank jacket reads as a book, which is what it is. */
/* A book whose cover was never scanned. It used to be a dark brown board, and
   a visitor reported it "gets lost in the wood grain" — measured, he was
   right: the shelf photo averages luma 0.42 light and 0.15 dark, and the old
   jacket sat at 0.12–0.26, inside the noise of the dark one.

   So it is now buff cloth with dark ink, which is both what an unjacketed
   hardback actually looks like and far from either wood — 0.84 against 0.42
   and 0.15. Done in CSS rather than as a stock "cover not available" image so
   the title stays readable and it scales with the shelf slider. */
.jacket {
  display: flex; flex-direction: column; justify-content: space-between;
  width: calc(var(--art) * 0.66); height: var(--art);
  padding: .5rem .45rem;
  background: linear-gradient(145deg, #e8e0cf 0%, #dbd1bc 55%, #cabf a8 100%);
  background: linear-gradient(145deg, #e8e0cf 0%, #dbd1bc 55%, #c9bda6 100%);
  border-left: 3px solid #8a7a5c;
  border-radius: 1px 2px 2px 1px;
  box-shadow: 0 5px 9px rgba(0, 0, 0, .6), inset 0 0 0 1px rgba(60, 48, 30, .35);
  transition: transform .13s ease;
}
.slot:hover .jacket { transform: translateY(-5px); }
.jacket .jt {
  font-size: max(.62rem, var(--txt-floor, 0rem)); line-height: 1.25; color: #2b2417;
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
  overflow: hidden;
}
.jacket .ja { font-size: max(.55rem, var(--txt-floor, 0rem)); color: #5d5340;
              letter-spacing: .03em; }
/* Says why it is blank, so it reads as a known gap rather than a broken image. */
.jacket .jn { font-size: max(.5rem, var(--txt-floor, 0rem)); color: #7b6f57;
              letter-spacing: .09em; text-transform: uppercase; }

/* How many copies stand behind this one tile. */
.copies {
  position: absolute; right: 7px; bottom: calc(var(--board) + 4px);
  min-width: 15px; padding: 0 3px;
  font: 600 .58rem/15px system-ui, sans-serif; font-size: max(.58rem, var(--txt-floor, 0rem)); text-align: center;
  color: #1a1008; background: var(--accent);
  border-radius: 999px; box-shadow: 0 1px 3px rgba(0, 0, 0, .6);
}

/* Toolbar above the shelf, and the shelf-to-shelf pager below it. */
/* Three stacked rows, each left-justified: count, then search, then the
   display controls. One line held all of it and had become unreadable. */
.shelfbar { display: flex; flex-direction: column; align-items: stretch;
            gap: .4rem; margin: 1rem 0 .3rem; }
.sbrow { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; }
.shelfbar input[type=search] {
  /* Capped, not greedy. Stretching to fill the row pushed Sort and Comics to
     the far right while the other two rows grouped left; all three should
     start at the same edge and end where their content ends. */
  flex: 0 1 26rem; min-width: 0;
  padding: .42rem .6rem; font: inherit; font-size: max(.9rem, var(--txt-floor, 0rem));
  /* Same reasoning as .find on the reading page, and worse here: --rule is
     #332f28 against a #16150f background, which is a border in name only. */
  border: 2px solid rgba(201, 165, 106, .45); border-radius: 4px;
  background: var(--bg); color: var(--fg);
}
.shelfbar input[type=search]:hover { border-color: rgba(201, 165, 106, .7); }
.shelfbar input[type=search]:focus {
  border-color: var(--accent, #c9a56a); outline: none;
}
/* The two size sliders. They carry no name attribute, so changing one never
   submits the search form they sit in. */
.shelfsize {
  display: flex; flex-wrap: wrap; align-items: center; gap: .45rem;
  font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--muted);
  text-transform: uppercase; letter-spacing: .07em;
}
.shelfsize .ctl { display: inline-flex; align-items: center; gap: .25rem; }
.shelfsize .ctll { margin-right: .1rem; }
.shelfsize input[type=range] { width: 6.5rem; accent-color: var(--accent); }
.shelfsize .nudge {
  font: 600 .95rem/1 system-ui, sans-serif; font-size: max(.95rem, var(--txt-floor, 0rem));
  width: 1.25rem; height: 1.25rem; padding: 0; cursor: pointer;
  color: var(--fg); background: transparent;
  border: 1px solid var(--rule); border-radius: 3px;
}
.shelfsize .nudge:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.shelfsize .nudge:disabled { opacity: .3; cursor: default; }
.shelfsize .across {
  min-width: 4.6rem; font-variant-numeric: tabular-nums; text-align: right;
}
.shelfsize a { text-decoration: none; }
/* The SIZE controls go on a phone — the shelf sizes itself there and two
   sliders plus a reset is a row of things that do nothing useful. The WOOD
   toggle stays: it is a preference like the text size, not a desktop tool,
   and it was only ever hidden because it happened to share this row. John
   went looking for it on his iPhone and reasonably assumed it had gone. */
@media (max-width: 900px) {
  .shelfsize > *:not(.woodctl) { display: none; }
  .shelfsize { justify-content: flex-start; margin-top: .1rem; }
  /* 44px, per the gesture failures in CLAUDE.md. At its desktop size this
     is a 25px target, which is a miss waiting to happen on a phone. */
  .shelfsize .woodbtn { min-height: 2.75rem; padding-inline: 1rem; }
}

.shelfbar .sortby {
  flex: 0 0 auto; display: flex; align-items: center; gap: .4rem;
  font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--muted);
  text-transform: uppercase; letter-spacing: .07em;
}
.shelfbar .sortby select {
  font: inherit; font-size: max(.85rem, var(--txt-floor, 0rem)); text-transform: none; letter-spacing: 0;
  padding: .35rem .45rem; border: 1px solid var(--rule); border-radius: 4px;
  background: var(--bg); color: var(--fg);
}
.shelfcount {
  /* Left-justified now that it has its own row — it used to be pushed right
     with margin-left:auto to get out of the way on the single line. */
  flex: 0 0 auto;
  font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--muted);
  text-transform: uppercase; letter-spacing: .07em;
}
.pager {
  display: flex; align-items: center; gap: 1rem;
  margin: 0 0 2.5rem; font-size: max(.85rem, var(--txt-floor, 0rem));
}
.pager .pageno { color: var(--muted); font-size: max(.72rem, var(--txt-floor, 0rem));
                 text-transform: uppercase; letter-spacing: .07em; }
.shelfempty {
  padding: 3rem 1rem; text-align: center; color: var(--muted);
  background: #2c1c0e; border-radius: 6px; margin: 1rem 0;
}

/* The old scaffold diagnostics, kept but folded away. */
.diag { margin: 2.5rem 0 0; border-top: 1px solid var(--rule); padding-top: 1rem; }
.diag summary {
  cursor: pointer; font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--muted);
  text-transform: uppercase; letter-spacing: .09em;
}
.diag[open] summary { margin-bottom: .5rem; }

@media (max-width: 640px) {
  /* On a phone the saved preference is ignored: a 220px cover would give one
     book per row. The floor is the narrow default, not whatever the desktop
     slider was left at. */
  .woodshelf { --col: 80px; }
}

/* The public review on a Reading card: three lines, then a toggle. Only the
   REVIEW appears here — it is the one field on the reading page written to be
   read by someone else. */
.rev {
  margin: .45rem 0 0; font-size: max(.86rem, var(--txt-floor, 0rem)); line-height: 1.45;
  white-space: pre-line;          /* keep the paragraph breaks as typed */
}
.rev.clamped {
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
.revmore {
  margin-top: .15rem; padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--accent);
  text-transform: uppercase; letter-spacing: .07em;
}
.revmore:hover { text-decoration: underline; }

/* "Reading" on the front page: as many books as are marked, not one. */
.nowreading {
  display: grid; gap: .75rem;
  grid-template-columns: repeat(auto-fill, minmax(min(21rem, 100%), 1fr));
}
.nowreading .latestcard { margin: 0; }

/* Wood switch, and the licence credits it makes necessary. */
.woodbtn {
  font: inherit; font-size: max(.72rem, var(--txt-floor, 0rem)); text-transform: uppercase;
  letter-spacing: .07em; padding: .15rem .5rem; cursor: pointer;
  color: var(--fg); background: transparent;
  border: 1px solid var(--rule); border-radius: 3px;
}
.woodbtn:hover { border-color: var(--accent); color: var(--accent); }
.credits {
  margin: 1.5rem 0 0; font-size: max(.7rem, var(--txt-floor, 0rem)); color: var(--muted);
}
.credits a { color: var(--muted); }

/* Comics toggle in the shelf toolbar. */
.comicsbox {
  display: inline-flex; align-items: center; gap: .35rem; flex: 0 0 auto;
  font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--muted);
  text-transform: uppercase; letter-spacing: .07em; cursor: pointer;
}
.comicsbox input { accent-color: var(--accent); }

/* Format chips above the search box. */
.fmtchips { gap: .35rem; }
.fmtchips .chip {
  font-size: max(.68rem, var(--txt-floor, 0rem)); padding: .2rem .65rem; border-radius: 999px;
  border: 1px solid var(--rule); color: var(--muted);
  text-decoration: none; text-transform: uppercase; letter-spacing: .06em;
}
.fmtchips .chip:hover { color: var(--fg); border-color: var(--accent); }
.fmtchips .chip.on {
  background: var(--accent); border-color: var(--accent); color: var(--bg);
}

/* ======================================================================
   EPUB reader (/Books/read/)
   ----------------------------------------------------------------------
   A committed dark scheme like the reading site, defined on the page's own
   class rather than in a prefers-color-scheme block: a reader should look
   the same whatever the OS theme is.

   epub.js renders the book into an iframe it sizes itself, so the viewer
   needs a REAL height — a percentage of a parent that is itself auto-height
   collapses to nothing and the book renders zero pixels tall.
   ====================================================================== */
.books-reader {
  background: #14120E; color: #E8E2D6;
  font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
}
.books-reader .top-bar { background: #1B1813; border-bottom: 1px solid #2C2720; }
.books-reader a { color: #C89A4E; }

.readwrap {
  display: flex; flex-direction: column;
  /* dvh, with vh only as the fallback for browsers that lack it. On a phone
     100vh is the height the viewport WOULD have with the browser chrome
     retracted — taller than what you can actually see. Sizing the reader by it
     made every column taller than the screen, so the last lines sat below the
     fold and each chapter came out as ONE page: the arrows then had nothing to
     page through and jumped straight to the next chapter. */
  height: calc(100vh - 2.4rem);
  height: calc(100dvh - 2.4rem);
  max-width: 62rem; margin: 0 auto; padding: 0 1rem 1rem;
}
.readbar {
  display: flex; align-items: center; gap: 1rem;
  padding: .6rem .2rem; flex: 0 0 auto;
  border-bottom: 1px solid #2C2720;
}
.rdback { font-size: max(.8rem, var(--txt-floor, 0rem)); text-decoration: none; white-space: nowrap; }
.rdtitle {
  flex: 1 1 auto; min-width: 0; font-size: max(.85rem, var(--txt-floor, 0rem)); color: #9A9184;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rdnav { display: flex; align-items: center; gap: .5rem; flex: 0 0 auto; }
.rdnav button {
  font: 600 1.1rem/1 system-ui, sans-serif; font-size: max(1.1rem, var(--txt-floor, 0rem)); width: 2rem; height: 2rem;
  cursor: pointer; color: #E8E2D6; background: transparent;
  border: 1px solid #3A342B; border-radius: 4px;
}
.rdnav button:hover { border-color: #C89A4E; color: #C89A4E; }
.rdloc { font-size: max(.72rem, var(--txt-floor, 0rem)); color: #9A9184; min-width: 2.6rem; text-align: center;
         font-variant-numeric: tabular-nums; }

/* The page itself: paper, because a book is easier to read dark-on-light and
   this is the one surface where that beats matching the chrome. */
.rdviewer {
  flex: 1 1 auto; min-height: 0; margin-top: .8rem;
  background: #F6F1E6; border-radius: 4px;
  box-shadow: 0 2px 18px rgba(0, 0, 0, .5);
  overflow: hidden;
}
.rdmsg {
  margin: 2rem auto; max-width: 34rem; padding: 1.2rem 1.4rem;
  background: #1B1813; border: 1px solid #2C2720; border-radius: 6px;
  font-size: max(.92rem, var(--txt-floor, 0rem));
}
.rdmsg .hint { color: #9A9184; font-size: max(.84rem, var(--txt-floor, 0rem)); margin-bottom: 0; }
.rdmsg code { background: #251F18; color: #E8E2D6; }

/* Way in to the reader from a book page. */
/* a.readbtn, not .readbtn. The reading site sets `.books-reading a { color:
   var(--brass) }`, which is a class AND a type selector and therefore beats a
   bare class however far down the file it sits. The result was brass text on a
   brass button: the label was emitted correctly and rendered invisible, on
   that page only — the front page carries no such rule, so the same chip read
   fine there. Qualifying with the element name matches the specificity, and
   being later in the file settles it. */
a.readbtn {
  display: inline-block; margin-left: .4rem;
  font-size: max(.68rem, var(--txt-floor, 0rem)); padding: .2rem .7rem; border-radius: 999px;
  text-decoration: none; letter-spacing: .06em; text-transform: uppercase;
  background: var(--brass, #C89A4E); color: #1A1204; border: 1px solid var(--brass, #C89A4E);
}
a.readbtn:hover { filter: brightness(1.12); color: #1A1204; }

/* Reader: contents dropdown, and a page counter rather than a chapter one. */
.rdtocwrap { flex: 0 1 16rem; min-width: 0; display: flex; }
.rdtocwrap select {
  width: 100%; min-width: 0; font: inherit; font-size: max(.78rem, var(--txt-floor, 0rem));
  padding: .28rem .4rem; border: 1px solid #3A342B; border-radius: 4px;
  background: #1B1813; color: #E8E2D6;
}
.rdtocwrap select:disabled { opacity: .5; }
.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}
/* The book is paginated now, so nothing inside may scroll. */
.rdviewer { overflow: hidden; }

/* Status chips sit after the format chips, with a little air between. */
.chipsep { flex: 0 0 .6rem; }
.fmtchips .chip.clear { border-color: transparent; color: var(--muted); padding: .2rem .4rem; }
.fmtchips .chip.clear:hover { color: var(--fg); }

/* Publisher plot, on the front-page Reading cards. Same clamp as a review;
   dimmer, because it is not John's writing. */
.latestcard .plot { color: var(--muted); font-size: max(.82rem, var(--txt-floor, 0rem)); }

/* Plot in the reading page header, under the chips. Clamped so the header
   cannot grow past the cover beside it — the cover is the thing that sets the
   header's height, and a 975-character blurb would otherwise treble it. */
.books-reading .plotwrap { margin: .55rem 0 0; max-width: 44rem; }
.books-reading .plot {
  margin: 0; font-size: max(.84rem, var(--txt-floor, 0rem)); line-height: 1.5; color: var(--mute);
  white-space: pre-line;
}
.books-reading .plot.clamped {
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
  overflow: hidden;
}
.books-reading .plotmore {
  margin-top: .2rem; padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: max(.68rem, var(--txt-floor, 0rem)); color: var(--brass);
  text-transform: uppercase; letter-spacing: .07em;
}
.books-reading .plotmore:hover { text-decoration: underline; }

/* Keep-on-server toggle, beside Read on a book page. */
.pinbtn {
  margin-left: .35rem; font: inherit; font-size: max(.68rem, var(--txt-floor, 0rem));
  padding: .2rem .7rem; border-radius: 999px; cursor: pointer;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--mute); background: transparent; border: 1px solid var(--edge2);
}
.pinbtn:hover { color: var(--text); border-color: var(--brass); }
.pinbtn.on { background: var(--patina); border-color: var(--patina); color: #0C1614; }
.pinbtn:disabled { opacity: .5; cursor: default; }

/* Image lightbox in the reader. Lives in the parent document so it gets the
   whole window — inline, an image is bounded by the ~430px column, which is
   what makes maps unreadable however large the browser is. */
.rdlb {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(10, 8, 5, .94);
  display: flex; align-items: center; justify-content: center;
  overflow: auto; padding: 3rem 1rem 1rem;
}
.rdlb img.fit {
  max-width: 100%; max-height: calc(100vh - 4rem);
  width: auto; height: auto; cursor: zoom-in;
  box-shadow: 0 4px 40px rgba(0, 0, 0, .7);
}
.rdlb img.actual {
  max-width: none; max-height: none;      /* pan by scrolling the overlay */
  cursor: zoom-out;
  box-shadow: 0 4px 40px rgba(0, 0, 0, .7);
}
.rdlbbar {
  position: fixed; top: 0; left: 0; right: 0;
  display: flex; justify-content: flex-end; gap: .5rem;
  padding: .6rem .8rem; background: rgba(10, 8, 5, .9);
}
.rdlbbar button {
  font: inherit; font-size: max(.72rem, var(--txt-floor, 0rem)); text-transform: uppercase;
  letter-spacing: .07em; padding: .3rem .8rem; cursor: pointer;
  color: #E8E2D6; background: transparent;
  border: 1px solid #3A342B; border-radius: 4px;
}
.rdlbbar button:hover { border-color: #C89A4E; color: #C89A4E; }

/* "Back" in the reader — returns to where a link was followed from. Hidden
   until there is somewhere to go back to. */
.rdbackbtn {
  flex: 0 0 auto; font: inherit; font-size: max(.72rem, var(--txt-floor, 0rem));
  padding: .28rem .7rem; cursor: pointer; white-space: nowrap;
  color: #C89A4E; background: transparent;
  border: 1px solid #3A342B; border-radius: 4px;
}
.rdbackbtn:hover { border-color: #C89A4E; }

/* Second facts line: the bibliographic detail CLZ carries. Dimmer than the
   first, which holds the things you look for at a glance. */
.books-reading .facts.biblio { color: var(--mute2); font-size: max(.78rem, var(--txt-floor, 0rem)); }

/* ----------------------------------------------------------------------
   The reader on a phone.
   There were no rules here at all, so a 390px screen got the desktop bar:
   a 16rem contents dropdown, a title, and two 2rem arrows crushed against
   the edge. With no keyboard either, there was effectively no way to turn
   a page. Tapping the left or right third of the page also works — see
   reader.js — but a visible control should exist regardless.
   ---------------------------------------------------------------------- */
@media (max-width: 700px) {
  .readbar { flex-wrap: wrap; row-gap: .45rem; }
  .rdtitle { order: 3; flex: 1 1 100%; }
  .rdtocwrap { order: 4; flex: 1 1 100%; }

  /* Page turning moves to a fixed bar at the bottom, where a thumb is. */
  .rdnav {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
    gap: 0; padding: .3rem .5rem calc(.3rem + env(safe-area-inset-bottom));
    background: #1B1813; border-top: 1px solid #2C2720;
  }
  .rdnav button {
    flex: 1 1 0; height: 2.9rem; font-size: max(1.5rem, var(--txt-floor, 0rem)); border-radius: 6px;
  }
  /* Wider than it was: it now carries "214 / 1,340 · 16%" rather than
     "12/17". */
  .rdloc { flex: 0 0 9.5rem; font-size: max(.74rem, var(--txt-floor, 0rem)); }
  /* Immersive mode: the whole screen is the page, and the controls are
     summoned by tapping the centre.

     The page-turn bar must survive it — a tap zone nobody can see is not a
     control, and hiding everything left John on a screen of text with nothing
     to press. But .rdnav lives INSIDE .readbar, and a child cannot show
     through a parent set to display:none, so "hide the bar, keep the nav" was
     never going to work: it just reserved an empty black strip where the bar
     had been.

     So the bar is not hidden — it MOVES. In immersive mode it becomes the
     bottom page-turn bar, carrying the only two children that still matter. */
  html.rd-bare .top-bar { display: none; }
  html.rd-bare .readbar {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    height: 3.5rem;
    margin: 0; padding: 0 .5rem;
    display: flex; align-items: center; justify-content: center; gap: .75rem;
    background: var(--bg, #14110d);
    border-top: 1px solid var(--rule, #2c2620);
    border-bottom: 0;
    z-index: 5;
  }
  html.rd-bare .readbar > *:not(.rdnav):not(.rdver) { display: none; }
  html.rd-bare .rdnav { display: flex; }
  html.rd-bare .readwrap {
    height: calc(100vh - 3.5rem); height: calc(100dvh - 3.5rem);
    padding: 0; max-width: none;
  }
  html.rd-bare .rdviewer { margin-top: 0; border-radius: 0; }

  /* Clear of the fixed bar, or the last line of every page sits under it. */
  .readwrap {
    height: calc(100vh - 2.4rem - 3.5rem);
    height: calc(100dvh - 2.4rem - 3.5rem);
  }
}

/* Which reader.js build the page is running. Deliberately quiet — it is a
   diagnostic, not furniture — but readable when John needs to tell a fix from
   a cached copy of the bug. */
.rdver {
  font: 600 0.62rem/1 var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: max(0.62rem, var(--txt-floor, 0rem));
  letter-spacing: 0.04em;
  color: var(--muted, #8a8175);
  opacity: 0.55;
  padding: 0.15rem 0.3rem;
  border: 1px solid currentColor;
  border-radius: 3px;
  align-self: center;
}

/* Transparent gesture layer over the book frame, phone only.

   The book iframe is sandboxed without allow-scripts, and WebKit does not
   dispatch touch events into a document where scripting is off — so taps and
   swipes never reached the reader on iOS while the toolbar buttons worked.
   Catching them out here costs nothing and does not weaken the sandbox. */
#rdtap {
  position: absolute;
  inset: 0;
  z-index: 4;
  background: transparent;
  touch-action: pan-y;
}

/* Shown while a long press has stood the gesture layer down for text
   selection. It carries the way back: with the layer down the parent document
   cannot see taps, so paging has to be resumed by a real button rather than by
   a gesture nobody would find. */
#rdselbar {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  /* Low, so it hides as little of the text being selected as possible. In
     immersive mode it has to clear the page-turn bar. */
  bottom: calc(env(safe-area-inset-bottom, 0px) + .5rem);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: .7rem;
  padding: .45rem .7rem;
  border-radius: 999px;
  background: var(--bg, #14110d);
  border: 1px solid var(--edge2, #3A4759);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
  font-size: max(.74rem, var(--txt-floor, 0rem));
  color: var(--muted, #cfc6b6);
}
#rdselbar[hidden] { display: none; }
html.rd-bare #rdselbar {
  bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px) + .5rem);
}
#rdselbar button {
  font: inherit;
  font-weight: 600;
  padding: .2rem .8rem;
  border-radius: 999px;
  cursor: pointer;
  color: var(--brass, #C89A4E);
  background: none;
  border: 1px solid currentColor;
}

/* ---- reader type and colour panel ------------------------------------- */

.rdtypebtn {
  font: 600 .85rem/1 var(--sans, system-ui, sans-serif); font-size: max(.85rem, var(--txt-floor, 0rem));
  padding: .3rem .6rem;
  border-radius: 6px;
  cursor: pointer;
  color: var(--ink, #e8e2d6);
  background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
  /* A finger's worth, and no smaller. Apple's own guidance is 44px, and the
     play glyph is a thin triangle inside it — the visible mark is not the
     target. touch-action and user-select stop iOS treating a press as text
     selection, which is what made the old long-press produce a magnifier. */
  min-width: 2.75rem; min-height: 2.75rem;
  display: inline-flex; align-items: center; justify-content: center;
  touch-action: manipulation;
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}
/* The triangle and the stop square are small glyphs in a large button, so they
   are sized up rather than left to the label's font size. */
#rdspeak { font-size: max(1.05rem, var(--txt-floor, 0rem)); line-height: 1; }
.rdtypebtn[aria-expanded="true"] {
  color: var(--brass, #C89A4E);
  border-color: var(--brass, #C89A4E);
}

#rdtypepanel {
  position: fixed;
  z-index: 70;
  top: 4.2rem;
  right: .8rem;
  width: min(21rem, calc(100vw - 1.6rem));
  padding: .7rem .8rem;
  border-radius: 10px;
  background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .5);
  font-size: max(.78rem, var(--txt-floor, 0rem));
  color: var(--ink, #e8e2d6);
  /* The face list is as long as the device has faces, so the panel scrolls
     rather than running off the screen. */
  max-height: min(72vh, 32rem);
  overflow-y: auto;
}
#rdtypepanel[hidden] { display: none; }

/* The panel's own way out. It is opened from the toolbar, and a tap in the
   middle of the screen hides the toolbar — so the panel needs a control that
   does not live somewhere that can disappear. */
/* Qualified under the panel's id: the unqualified class lost to
   "#rdtypepanel button", so the X sat in the text flow at the top left
   instead of the corner. The same specificity lesson as the Listen
   button, one rule later. */
#rdtypepanel .rdtypeclose {
  position: absolute;
  top: .3rem;
  right: .4rem;
  width: 2rem;
  height: 2rem;
  line-height: 1;
  font-size: max(1.3rem, var(--txt-floor, 0rem)) !important;
  padding: 0 !important;
  border: 0 !important;
  background: none !important;
  color: var(--muted, #8a8175) !important;
  cursor: pointer;
}
#rdtypepanel .rdtypeclose:hover { color: var(--ink, #e8e2d6) !important; }

#rdtypepanel .rdrow:first-of-type { padding-right: 2rem; }
#rdtypepanel .rdrow {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .35rem 0;
}
#rdtypepanel .rdrow + .rdrow { border-top: 1px solid var(--edge, #2A3444); }
#rdtypepanel .rdlabel {
  flex: 0 0 3.4rem;
  color: var(--muted, #8a8175);
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: max(.64rem, var(--txt-floor, 0rem));
}
#rdtypepanel button {
  font: inherit;
  cursor: pointer;
  color: var(--ink, #e8e2d6);
  background: none;
  border: 1px solid var(--edge2, #3A4759);
  border-radius: 6px;
  padding: .25rem .55rem;
}
#rdtypepanel button.sel {
  color: var(--brass, #C89A4E);
  border-color: var(--brass, #C89A4E);
}

#rdtypepanel .rdsize { display: flex; align-items: center; gap: .5rem; }
#rdtypepanel .rdsize span { min-width: 3rem; text-align: center; }

#rdtypepanel .rdfaces { display: flex; flex-wrap: wrap; gap: .35rem; }

#rdtypepanel .rdthemes { display: flex; flex-wrap: wrap; gap: .35rem; }
/* Each swatch is set in the colours it selects, so the choice is legible as
   itself rather than as a word. */
#rdtypepanel .rdtheme {
  width: 2.1rem;
  height: 1.9rem;
  font-weight: 600;
  border-color: var(--edge2, #3A4759);
}
#rdtypepanel .rdtheme.sel { outline: 2px solid var(--brass, #C89A4E); }

#rdtypepanel .rdcustom label {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  color: var(--muted, #8a8175);
}
#rdtypepanel input[type="color"] {
  width: 1.9rem;
  height: 1.6rem;
  padding: 0;
  border: 1px solid var(--edge2, #3A4759);
  border-radius: 5px;
  background: none;
  cursor: pointer;
}

/* On a phone the panel is a sheet: the toolbar is too narrow to anchor to. */
@media (max-width: 700px) {
  #rdtypepanel {
    top: auto;
    left: .5rem;
    right: .5rem;
    bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px) + .5rem);
    width: auto;
  }
}

/* ---- audiobook player -------------------------------------------------- */

.listenwrap {
  max-width: 46rem;
  margin: 0 auto;
  padding: 1rem 1rem 4rem;
}
.lsnhead {
  display: flex;
  gap: 1.1rem;
  align-items: flex-start;
  margin: .8rem 0 1.2rem;
}
.lsncover {
  flex: 0 0 auto;
  width: 150px;
  height: auto;
  border-radius: 4px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .45);
}
.lsnmeta { min-width: 0; }
.lsnmeta h1 { margin: 0 0 .2rem; font-size: max(1.35rem, var(--txt-floor, 0rem)); line-height: 1.2; }
.lsnwho { margin: 0 0 .3rem; color: var(--muted, #8a8175); }
.lsnfacts { margin: 0 0 .6rem; font-size: max(.8rem, var(--txt-floor, 0rem)); color: var(--muted, #8a8175); }

/* Keep is the only way a file reaches the server, so it reads as a switch
   rather than as a link. */
.lsnkeep {
  font: 600 .72rem/1 var(--sans, system-ui, sans-serif); font-size: max(.72rem, var(--txt-floor, 0rem));
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: .3rem .8rem;
  border-radius: 999px;
  cursor: pointer;
  color: var(--muted, #8a8175);
  background: none;
  border: 1px solid currentColor;
}
.lsnkeep.on {
  color: var(--bg, #14110d);
  background: var(--brass, #C89A4E);
  border-color: var(--brass, #C89A4E);
}

.lsnplayer { margin: 1rem 0; }
.lsnplayer audio { width: 100%; display: block; }
.lsnnow { margin: .5rem 0 .1rem; font-size: max(.9rem, var(--txt-floor, 0rem)); }
.lsnpos { font-size: max(.78rem, var(--txt-floor, 0rem)); color: var(--muted, #8a8175); }

.lsnrates, .lsnskip { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .6rem; }
.lsnrates button, .lsnskip button {
  font: inherit;
  font-size: max(.74rem, var(--txt-floor, 0rem));
  padding: .25rem .6rem;
  border-radius: 6px;
  cursor: pointer;
  color: var(--ink, #e8e2d6);
  background: none;
  border: 1px solid var(--edge2, #3A4759);
}
.lsnrates button.sel { color: var(--brass, #C89A4E); border-color: var(--brass, #C89A4E); }

.lsnresume {
  margin: .8rem 0;
  padding: .5rem .8rem;
  border-radius: 8px;
  background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
  font-size: max(.82rem, var(--txt-floor, 0rem));
}
.lsnresume[hidden] { display: none; }
.lsnresume button {
  font: inherit;
  margin-left: .5rem;
  padding: .2rem .7rem;
  border-radius: 999px;
  cursor: pointer;
  color: var(--brass, #C89A4E);
  background: none;
  border: 1px solid currentColor;
}

.lsnh2 {
  font-size: max(.78rem, var(--txt-floor, 0rem));
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted, #8a8175);
  margin: 1.6rem 0 .5rem;
}

/* An 82-chapter book is normal, so the list scrolls in place rather than
   burying the notes field under it. */
.lsnchapters {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 26rem;
  overflow-y: auto;
  border: 1px solid var(--edge, #2A3444);
  border-radius: 8px;
}
.lsnchapters li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .45rem .7rem;
  cursor: pointer;
  border-bottom: 1px solid var(--edge, #2A3444);
  font-size: max(.84rem, var(--txt-floor, 0rem));
}
.lsnchapters li:last-child { border-bottom: 0; }
.lsnchapters li:hover { background: var(--panel, #1c1a15); }
.lsnchapters li.current {
  color: var(--brass, #C89A4E);
  background: var(--panel, #1c1a15);
}

.lsnnote textarea {
  width: 100%;
  font: inherit;
  font-size: max(.9rem, var(--txt-floor, 0rem));
  padding: .6rem;
  border-radius: 8px;
  color: var(--ink, #e8e2d6);
  background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
  resize: vertical;
}
.lsnnoterow { display: flex; align-items: center; gap: .7rem; margin-top: .5rem; }
.lsnnoterow button {
  font: inherit;
  font-size: max(.78rem, var(--txt-floor, 0rem));
  padding: .3rem .9rem;
  border-radius: 999px;
  cursor: pointer;
  color: var(--brass, #C89A4E);
  background: none;
  border: 1px solid currentColor;
}
.lsnstate { font-size: max(.76rem, var(--txt-floor, 0rem)); color: var(--muted, #8a8175); }

@media (max-width: 560px) {
  .lsnhead { flex-direction: column; }
  .lsncover { width: 120px; }
}

/* Cross-format handoff. Offered, never applied: the ebook-to-audio mapping is
   approximate, and moving someone off the page they opened without asking is
   worse than a line of text they can ignore. */
#rdhand, #rdhand2 {
  position: fixed;
  /* A BAR, not a pill. Centred with shrink-to-fit, the sentence wrapped onto
     four lines inside a rounded capsule on a phone and looked broken. Spanning
     the width gives the text somewhere to go. */
  left: .75rem;
  right: .75rem;
  bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px) + .6rem);
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .6rem;
  padding: .55rem .8rem;
  border-radius: 12px;
  background: var(--panel, #1c1a15);
  border: 1px solid var(--brass, #C89A4E);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .5);
  font-size: max(.78rem, var(--txt-floor, 0rem));
  color: var(--ink, #e8e2d6);
}
#rdhand[hidden], #rdhand2[hidden] { display: none; }
#rdhand span, #rdhand2 span { flex: 1 1 auto; min-width: 0; }
#rdhand button, #rdhand2 button {
  flex: 0 0 auto;
  font: inherit;
  font-weight: 600;
  padding: .2rem .75rem;
  border-radius: 999px;
  cursor: pointer;
  color: var(--brass, #C89A4E);
  background: none;
  border: 1px solid currentColor;
}
#rdhand button.dismiss, #rdhand2 button.dismiss {
  color: var(--muted, #8a8175);
  font-weight: 400;
}

/* The one-tap switch between an ebook and its narration. Reads as a sibling of
   the Read/Listen chips on the book page, so it is recognisable as the same
   idea in a different place. */
/* Qualified past ".books-reader a", which sets every link in the reader to
   brass — brass text on a brass button is an invisible label, which is exactly
   what John saw. The same trap caught the Read chip earlier in this project:
   a single-class selector loses to an element-plus-class one. */
.books-reader a.rdswitchbtn,
.books-listen a.rdswitchbtn,
.rdswitchbtn {
  font: 600 .72rem/1 var(--sans, system-ui, sans-serif); font-size: max(.72rem, var(--txt-floor, 0rem));
  letter-spacing: .06em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  padding: .3rem .8rem;
  border-radius: 999px;
  color: var(--bg, #14110d);
  background: var(--brass, #C89A4E);
  border: 1px solid var(--brass, #C89A4E);
}
.books-reader a.rdswitchbtn:hover,
.books-listen a.rdswitchbtn:hover,
.rdswitchbtn:hover { filter: brightness(1.08); }
html.rd-bare .rdswitchbtn { display: none; }

/* ---- audiobook: optional big controls, and the chapter picker ---------- */

/* The browser's own player stays exactly as it was — it scrubs well and John
   asked for it kept. This is an ADDITION for listening away from a desk, where
   the native controls are too small to hit. Off unless asked for. */
.lsnbig { display: none; }
html.lsn-big .lsnbig {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .9rem;
  margin: 1rem 0 .4rem;
  flex-wrap: wrap;
}
/* The small skip row would duplicate the big one. */
html.lsn-big .lsnskip { display: none; }

html.lsn-big .lsnbig button {
  font: 600 1.05rem/1 var(--sans, system-ui, sans-serif); font-size: max(1.05rem, var(--txt-floor, 0rem));
  width: 3.6rem;
  height: 3.6rem;
  border-radius: 50%;
  cursor: pointer;
  color: var(--ink, #e8e2d6);
  background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
  display: flex;
  align-items: center;
  justify-content: center;
}
/* The 30-second buttons carry a numeral inside a circular arrow, the way every
   player draws them. */
html.lsn-big #back30b span,
html.lsn-big #fwd30b span { font-size: max(.95rem, var(--txt-floor, 0rem)); }
html.lsn-big #back30b::before { content: "\21ba"; margin-right: .1rem; font-size: max(1.3rem, var(--txt-floor, 0rem)); }
html.lsn-big #fwd30b::after   { content: "\21bb"; margin-left: .1rem; font-size: max(1.3rem, var(--txt-floor, 0rem)); }

html.lsn-big .lsnplay {
  width: 5.2rem !important;
  height: 5.2rem !important;
  font-size: max(1.7rem, var(--txt-floor, 0rem)) !important;
  color: var(--bg, #14110d) !important;
  background: var(--brass, #C89A4E) !important;
  border-color: var(--brass, #C89A4E) !important;
}
html.lsn-big .lsnbig button:active { filter: brightness(1.15); }

/* Rate buttons deserve a thumb too, once the rest has grown. */
html.lsn-big .lsnrates button {
  font-size: max(.9rem, var(--txt-floor, 0rem));
  padding: .5rem .8rem;
}

/* The chapter picker. A native select is a far bigger tap target than a row in
   a list, and on a phone it opens as a full-height wheel. */
.lsnchapwrap { display: block; margin: .2rem 0 .6rem; }
.lsnchapwrap select {
  width: 100%;
  font: inherit;
  font-size: max(.9rem, var(--txt-floor, 0rem));
  padding: .6rem .7rem;
  border-radius: 8px;
  color: var(--ink, #e8e2d6);
  background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
}
html.lsn-big .lsnchapwrap select { font-size: max(1rem, var(--txt-floor, 0rem)); padding: .85rem .7rem; }

/* Five round targets have to share a phone's width. At 390px the default sizes
   pushed the last one onto its own row, which looks like a mistake and puts
   "next chapter" somewhere the thumb does not expect. */
@media (max-width: 460px) {
  html.lsn-big .lsnbig { gap: .5rem; flex-wrap: nowrap; }
  html.lsn-big .lsnbig button { width: 3.05rem; height: 3.05rem; font-size: max(.95rem, var(--txt-floor, 0rem)); }
  html.lsn-big .lsnplay { width: 4.3rem !important; height: 4.3rem !important;
                          font-size: max(1.4rem, var(--txt-floor, 0rem)) !important; }
  html.lsn-big #back30b span, html.lsn-big #fwd30b span { font-size: max(.8rem, var(--txt-floor, 0rem)); }
  html.lsn-big #back30b::before, html.lsn-big #fwd30b::after { font-size: max(1.05rem, var(--txt-floor, 0rem)); }
}

/* ---- want lists -------------------------------------------------------- */

.wantwrap { max-width: 54rem; margin: 0 auto; padding: 1rem 1rem 4rem; }
.wanth1 { font-size: max(1.5rem, var(--txt-floor, 0rem)); margin: .6rem 0 .2rem; }
.wantsub { margin: 0 0 1.2rem; color: var(--muted, #8a8175); font-size: max(.85rem, var(--txt-floor, 0rem)); }

.wantlists { list-style: none; margin: 0 0 1.4rem; padding: 0; }
.wantlists li { border-bottom: 1px solid var(--edge, #2A3444); }
.wantlists a {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; padding: .8rem .2rem; text-decoration: none;
}
.wantname { font-size: max(1.05rem, var(--txt-floor, 0rem)); }
.wantcount { color: var(--muted, #8a8175); font-size: max(.8rem, var(--txt-floor, 0rem)); white-space: nowrap; }
.wantdef {
  font-size: max(.62rem, var(--txt-floor, 0rem)); text-transform: uppercase; letter-spacing: .06em;
  padding: .1rem .4rem; margin-left: .5rem; border-radius: 999px;
  color: var(--bg, #14110d); background: var(--brass, #C89A4E);
}

.wantnew { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
.wantnew input {
  flex: 1 1 14rem; font: inherit; font-size: max(.9rem, var(--txt-floor, 0rem)); padding: .5rem .6rem;
  border-radius: 8px; color: var(--ink, #e8e2d6);
  background: var(--panel, #1c1a15); border: 1px solid var(--edge2, #3A4759);
}

/* The quick-add row. Author and title are the only required fields, so they
   take the room; everything else shrinks. */
.wantadd {
  display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
  margin: .4rem 0 1.2rem; padding: .7rem; border-radius: 10px;
  background: var(--panel, #1c1a15); border: 1px solid var(--edge, #2A3444);
}
.wantadd input, .wantadd select {
  font: inherit; font-size: max(.9rem, var(--txt-floor, 0rem)); padding: .5rem .6rem; border-radius: 8px;
  color: var(--ink, #e8e2d6); background: var(--bg, #14110d);
  border: 1px solid var(--edge2, #3A4759);
}
#f_author, #f_title { flex: 1 1 11rem; min-width: 0; }
#f_notes { flex: 1 1 9rem; min-width: 0; }
.wantadd button, .wantnew button {
  font: inherit; font-weight: 600; font-size: max(.82rem, var(--txt-floor, 0rem)); padding: .5rem 1.1rem;
  border-radius: 999px; cursor: pointer;
  color: var(--bg, #14110d); background: var(--brass, #C89A4E);
  border: 1px solid var(--brass, #C89A4E);
}
.wantstate { font-size: max(.76rem, var(--txt-floor, 0rem)); color: var(--muted, #8a8175); }
.wantstate.bad { color: #d98b7a; }

.wantitems { list-style: none; margin: 0; padding: 0; }
.wantitems li {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; padding: .7rem .2rem; border-bottom: 1px solid var(--edge, #2A3444);
}
.wantitems li.got, .wantitems li.dropped { opacity: .5; }
.wantitems li.got .wanttitle { text-decoration: line-through; }
.wantmain { min-width: 0; }
.wanttitle { display: block; font-size: max(1rem, var(--txt-floor, 0rem)); }
.wantauthor { display: block; font-size: max(.82rem, var(--txt-floor, 0rem)); color: var(--muted, #8a8175); }
.wantmeta { display: block; font-size: max(.78rem, var(--txt-floor, 0rem)); color: var(--muted, #8a8175); margin-top: .15rem; }

/* The reason for keeping a want list beside a catalogue. */
/* A LABEL, not a pill. The 999px radius the other badges use is right for
   "LOW" -- one short word on one line, where the capsule ends are the shape.
   This one holds a sentence ("You have 2: 6th - 2008 - Paperback | 5th ed.,
   rev. and expanded - 2002 - Paperback"), and once that wraps to two lines the
   full radius bows the sides into a lozenge with the text sitting oddly inside
   it. A small radius reads the same at one line or four.

   Also a little more padding, because the text now has room to breathe on a
   narrow column where it previously never wrapped at all. */
.wantowned {
  display: inline-block; margin-top: .3rem; font-size: max(.72rem, var(--txt-floor, 0rem));
  padding: .2rem .55rem; border-radius: 6px;
  color: #8fbf8a; border: 1px solid currentColor;
  line-height: 1.45;
}
/* It is a link now, to the copy on the shelf. Keeping the pill exactly as it
   was and only adding the pointer: the colour already means "you own this",
   and underlining it or turning it brass would make it read as a different
   kind of thing than the one John has learned. */
a.wantowned { text-decoration: none; cursor: pointer; }
a.wantowned:hover { background: rgba(143, 191, 138, .14); }

.wantacts { display: flex; gap: .4rem; flex: 0 0 auto; }
.wantacts button {
  font: inherit; font-size: max(.76rem, var(--txt-floor, 0rem)); padding: .3rem .7rem; border-radius: 999px;
  cursor: pointer; color: var(--ink, #e8e2d6); background: none;
  border: 1px solid var(--edge2, #3A4759);
}
/* Quiet until touched. Delete sits next to "Got it" all day and must not
   compete with it, but on hover it stops being brass like everything else --
   it is the one control on the page doing something the server cannot give
   back. Written as .wantacts .wantdel, two classes, because a bare .wantdel
   loses on specificity to the .wantacts button rule below it. That trap has
   been sprung four times in this stylesheet already. */
.wantacts .wantdel { padding: .3rem .55rem; color: var(--muted, #8a8175); }
.wantacts button:hover { border-color: var(--brass, #C89A4E); color: var(--brass, #C89A4E); }
.wantacts .wantdel:hover { border-color: #a8452f; color: #a8452f; }
.wantacts .wantcopy { padding: .3rem .55rem; color: var(--muted, #8a8175); }

/* Anchored to the page, not to the row, so a row near the bottom of a long
   list does not have its menu clipped by anything with overflow set.

   SAME SURFACE AS THE EDIT DIALOG -- var(--panel) with var(--ink) on it, not
   a hardcoded paper colour. The first version set a light background and let
   the text colour inherit, which on John's dark theme produced pale text on
   pale paper: a menu with nothing legible in it. Any floating surface here
   has to name BOTH halves of the pair from the same tokens the rest of the
   page uses, or it only works in whichever theme it was written under. */
.wantcopymenu { position: absolute; z-index: 40; width: 208px;
                background: var(--panel, #1c1a15); color: var(--ink, #e8e2d6);
                border: 1px solid var(--edge2, #3A4759); border-radius: 8px;
                box-shadow: 0 20px 60px rgba(0, 0, 0, .6); padding: .4rem; }
.wantcopymenu .wcmhead { margin: .15rem .35rem .35rem; font-size: max(.72rem, var(--txt-floor, 0rem));
                         text-transform: uppercase; letter-spacing: .04em;
                         color: var(--muted, #8a8175); }
.wantcopymenu button { display: block; width: 100%; text-align: left;
                       background: none; border: 0; border-radius: 5px;
                       padding: .42rem .5rem; cursor: pointer; color: inherit;
                       font-size: max(.9rem, var(--txt-floor, 0rem)); }
.wantcopymenu button:hover:not(:disabled) { background: rgba(200, 154, 78, .16); }
.wantcopymenu button:disabled { opacity: .5; cursor: default; }
.wantcopymenu .wcmstate { margin: .3rem .35rem .1rem;
                          font-size: max(.78rem, var(--txt-floor, 0rem));
                          color: var(--brass, #C89A4E); }


/* On the book page the pair sits under the facts line, where it reads as part
   of the record rather than as an action bar. */
.booklinks { margin: .5rem 0 0; display: flex; gap: .5rem; flex-wrap: wrap; }

/* Outbound chips. Deliberately quieter than anything that changes data: they
   leave the site, and on a 150-row page two coloured buttons per row would be
   the loudest thing on it. They gain their colour on hover, like the row's own
   controls do. */
.wantlinks { display: inline-flex; gap: .4rem; margin-left: .6rem;
             vertical-align: middle; }
.linkchip { font-size: max(.72rem, calc(var(--txt-floor, .72rem) * .9));
            letter-spacing: .02em; text-transform: uppercase;
            text-decoration: none; padding: .18rem .45rem; border-radius: 4px;
            border: 1px solid rgba(140, 130, 115, .35);
            color: var(--muted, #8a8175); white-space: nowrap; }
.linkchip:hover { border-color: var(--brass, #C89A4E); color: var(--brass, #C89A4E);
                  background: rgba(200, 154, 78, .08); }
.linkchip.gr:hover { border-color: #7a6a52; color: #7a6a52;
                     background: rgba(122, 106, 82, .08); }

/* ---- the shelf view ---------------------------------------------------- */

/* The tiles are BUTTONS here, not links, so the browser's button styling has
   to be undone before .slot can do its work. */
.wantshelf .wantslot {
  background: none; border: 0; font: inherit; color: inherit; cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.wantshelf .wantslot:focus-visible { outline: 2px solid var(--brass, #C89A4E);
                                     outline-offset: -2px; }
/* A small tick on a book he already owns. The shelf shows only covers, so
   without it the one fact that changes what he does with an entry — that he
   has it already — would be the one fact invisible in this view. */
.wanthave {
  position: absolute; top: 4px; right: 8px; z-index: 2;
  font-size: max(.7rem, var(--txt-floor, 0rem)); line-height: 1;
  color: #d8f0d4; background: rgba(40, 90, 45, .85);
  border-radius: 999px; padding: .16rem .3rem;
}

/* Room to breathe. These two controls arrived at different times and each
   had a margin tuned for whatever sat above it then -- the sort's was
   NEGATIVE, to pull it up under the paste disclosure. Stacked together they
   collapsed into one crowded block. Now they are spaced as the pair they
   actually are. */
/* Which format THIS copy of a repeated book is. Only rendered when the same
   book appears twice on the page, because otherwise the shelf would carry a
   label on every spine to disambiguate nothing. Sits along the bottom of the
   cover, where a real book's format is usually printed. */
.wantfmt {
  position: absolute; left: 5px; right: 5px; bottom: calc(var(--board) + 3px);
  z-index: 2; text-align: center;
  font-size: max(.6rem, calc(var(--txt-floor, .6rem) * .8));
  text-transform: uppercase; letter-spacing: .04em;
  color: #f0e6d2; background: rgba(20, 17, 13, .82);
  border-radius: 3px; padding: .1rem .2rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The inline format picker inside the card. */
/* [hidden] only sets display:none in the UA sheet, so ANY display rule here
   overrides it and the box renders empty. On the phone that dashed strip sat
   between the notes and the buttons and pushed Save off the card. A rule that
   sets display on something toggled with [hidden] must say so explicitly. */
.wantdupfmt[hidden] { display: none; }
.wantdupfmt { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center;
              padding: .5rem; border-radius: 8px;
              border: 1px dashed var(--edge2, #3A4759); }
.wdfhead { font-size: max(.7rem, var(--txt-floor, 0rem)); text-transform: uppercase;
           letter-spacing: .05em; color: var(--muted, #8a8175); margin-right: .2rem; }
.wantdupfmt .wdf {
  font: inherit; font-size: max(.82rem, var(--txt-floor, 0rem));
  padding: .3rem .6rem; border-radius: 999px; cursor: pointer;
  background: none; color: var(--brass, #C89A4E);
  border: 1px solid rgba(200, 154, 78, .45);
}
.wantdupfmt .wdf:hover:not(:disabled) { background: rgba(200, 154, 78, .12); }
.wantdupfmt .wdf:disabled { opacity: .5; cursor: default; }

.wantviews { display: flex; gap: .4rem; justify-content: flex-end;
             align-items: center; margin: 1.1rem 0 .55rem; }
.wantview { font-size: max(.72rem, var(--txt-floor, 0rem)); text-transform: uppercase;
            letter-spacing: .05em; text-decoration: none; padding: .25rem .6rem;
            border-radius: 6px; color: var(--muted, #8a8175);
            border: 1px solid transparent; }
.wantview:hover { color: var(--brass, #C89A4E); }
.wantview.on { color: var(--brass, #C89A4E); border-color: rgba(200, 154, 78, .45); }

/* ---- the card's new head ------------------------------------------------ */

.wantdlgtop { display: flex; gap: .9rem; align-items: flex-start; }
.wantdlgcover { width: 92px; height: auto; border-radius: 2px; flex: 0 0 auto;
                box-shadow: 0 4px 14px rgba(0, 0, 0, .5); }
.wantdlgfacts { min-width: 0; display: flex; flex-direction: column; gap: .2rem; }
.wantdlgtitle { margin: 0; font-family: var(--serif, Georgia, serif);
                font-size: max(1.05rem, var(--txt-floor, 0rem)); line-height: 1.25; }
.wantdlgby { margin: 0; color: var(--muted, #8a8175);
             font-size: max(.85rem, var(--txt-floor, 0rem)); }
.wantdlgmeta { margin: 0; color: var(--muted, #8a8175);
               font-size: max(.78rem, var(--txt-floor, 0rem)); text-transform: capitalize; }
a.wantdlghave { text-decoration: none; }
.wantdlgalso { margin: 0; color: #7f9bbf;
               font-size: max(.76rem, var(--txt-floor, 0rem)); }
/* Collapsed by default. It is reference material, not something John needs
   every time he opens a card, and on a phone several hundred words of
   publisher copy would push everything else off the screen. */
.wantdlgblurb { margin: .2rem 0 0; }
.wantdlgblurb > summary { cursor: pointer; list-style: none;
  font-size: max(.72rem, var(--txt-floor, 0rem)); text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted, #8a8175); }
.wantdlgblurb > summary::-webkit-details-marker { display: none; }
.wantdlgblurb > summary::before { content: "\25B8 "; }
.wantdlgblurb[open] > summary::before { content: "\25BE "; }
.wantdlgblurb > summary:hover { color: var(--brass, #C89A4E); }
.wantdlgblurb p { margin: .35rem 0 0; line-height: 1.5;
  font-size: max(.85rem, var(--txt-floor, 0rem));
  color: var(--muted, #8a8175); font-family: var(--serif, Georgia, serif); }

.wantdlglinks { margin: .15rem 0 0; display: flex; gap: .4rem; }
.wantdlgacts { display: flex; gap: .5rem; flex-wrap: wrap; flex: 0 0 auto; }
.wantdlgfoot, .wantdlgtop, .wantdupfmt { flex: 0 0 auto; }
.wantdlgacts .e_act {
  font: inherit; font-size: max(.85rem, var(--txt-floor, 0rem));
  padding: .4rem .7rem; border-radius: 8px; cursor: pointer;
  background: none; color: var(--muted, #8a8175);
  border: 1px solid var(--edge2, #3A4759);
}
.wantdlgacts .e_act:hover:not(:disabled) { border-color: var(--brass, #C89A4E);
                                           color: var(--brass, #C89A4E); }

.wantsearch { display: flex; gap: .5rem; align-items: center; margin: .9rem 0 .35rem;
              flex-wrap: wrap; }
.wantsearch input[type="search"] {
  flex: 1 1 16rem; min-width: 0; font: inherit;
  font-size: max(.95rem, var(--txt-floor, 0rem));
  padding: .5rem .7rem; border-radius: 8px;
  color: var(--ink, #e8e2d6); background: var(--bg, #14110d);
  border: 1px solid var(--edge2, #3A4759);
}
.wantsearch button { font: inherit; font-size: max(.9rem, var(--txt-floor, 0rem));
                     padding: .5rem .9rem; border-radius: 8px; cursor: pointer;
                     background: none; color: var(--brass, #C89A4E);
                     border: 1px solid rgba(200, 154, 78, .5); }
.wantsearch button:hover { background: rgba(200, 154, 78, .1); }
.wantclear { color: var(--muted, #8a8175); text-decoration: none;
             font-size: max(.85rem, var(--txt-floor, 0rem)); }
.wantclear:hover { color: var(--brass, #C89A4E); }

/* Same shape as the "you have it" line and deliberately a different colour:
   they answer neighbouring questions — one is about the shelves, one is about
   the other lists — and telling them apart at a glance is the point. */
.wantalso { display: block; font-size: max(.76rem, var(--txt-floor, 0rem));
            color: #7f9bbf; margin-top: .25rem; }

/* Paging. Only rendered on a list long enough to need it, so on every list
   John made by hand this is not on the page at all. Prev and next are pushed
   to the edges with the count between them: on a phone they are then as far
   apart as the screen allows, which is the whole trick to not tapping the
   wrong one. */
.wantpager { display: flex; align-items: center; justify-content: space-between;
             gap: 1rem; margin: 1.4rem 0 .5rem; padding-top: 1rem;
             border-top: 1px solid rgba(140, 130, 115, .25); }
.wantpager a { color: var(--brass, #C89A4E); text-decoration: none;
               padding: .45rem .8rem; border: 1px solid rgba(200, 154, 78, .4);
               border-radius: 6px; white-space: nowrap; }
.wantpager a:hover { background: rgba(200, 154, 78, .1); }
.wantpagenum { color: var(--muted, #8a8175);
               font-size: max(.85rem, var(--txt-floor, 0rem)); }

/* The corpse. Dimmed so a row waiting on Undo can never be mistaken for a live
   entry, but still sitting in its old place in the list -- an accidental
   delete should be visible exactly where the eye already is. */
.wantitems li.wantgone { opacity: .6; }
.wantitems li.wantgone .wantauthor { color: #a8452f; }
.wantempty { color: var(--muted, #8a8175); font-size: max(.9rem, var(--txt-floor, 0rem)); }

@media (max-width: 560px) {
  .wantitems li { flex-direction: column; gap: .5rem; }
  .wantacts { align-self: flex-end; }
}

/* The editor. A dialog rather than a strip inside the row: editing an entry
   is when the NOTES get written — a pasted review, or why the book caught his
   eye — and a one-line input is the wrong shape for that. Notes take most of
   the card and everything else sits above them. */
.wantmain { cursor: pointer; border-radius: 6px; }
.wantmain:hover { background: rgba(200, 154, 78, .06); }
.wantmain:focus-visible { outline: 2px solid var(--brass, #C89A4E); outline-offset: 2px; }

/* THE PHONE CASE. Three things went wrong together here, none of them
   visible on the iPad, and each would have been enough on its own.

   1. align-items: center clips upward. When a flex child is taller than its
      container, centring pushes its top ABOVE the container and there is no
      way to scroll back to it -- the overflow is simply unreachable. That is
      why John saw the top of the card cut off. `margin: auto` on the child
      centres it when it fits and refuses to clip when it does not.
   2. 90vh is not 90% of what you can see on iOS. vh is the LAYOUT viewport,
      which includes the space under the address bar, so a card sized to it
      is taller than the visible area. dvh is the dynamic one that actually
      matches, with vh kept as the fallback for older browsers.
   3. The drag chained to the page behind. Once the card could not scroll,
      the gesture went to the document and Safari answered with a
      pull-to-refresh -- which is the "it reloads the previous page" he saw.
      overscroll-behavior: contain keeps the gesture inside the dialog. */
.wantdlg {
  position: fixed; inset: 0; z-index: 120;
  display: flex; justify-content: center;
  padding: 1rem;
  background: rgba(0, 0, 0, .6);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.wantdlgcard {
  width: min(42rem, 100%);
  margin: auto;                     /* centres, but never clips the top */
  /* NO TOP PADDING. The sticky header supplies it. A card that keeps its own
     leaves a strip above the header that the header cannot cover, and
     scrolled content shows through it. */
  padding-top: 0;
  padding-bottom: 1.3rem;           /* the last row should not touch the edge */
  max-height: min(90vh, 46rem);
  max-height: min(90dvh, 46rem);
  overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: .7rem;
  padding: 1rem 1.1rem 1.1rem;
  border-radius: 12px;
  background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
  box-shadow: 0 20px 60px rgba(0, 0, 0, .6);
  overflow: auto;
}
/* STICKY, like the footer, and for the same reason: the card scrolls on a
   phone, and the close X lives in here. Scrolled down, the header went away
   and took the only visible way out of the card with it — Escape is not on an
   iPad keyboard, and the backdrop deliberately does not dismiss. Cancel in the
   footer was the sole remaining exit, which is not obvious when the thing you
   are looking for is an X.

   Now the card has a fixed frame: title and X at the top, Save and Cancel at
   the bottom, and the entry scrolls between them. */
.wantdlghead {
  display: flex; align-items: center; justify-content: space-between;
  position: sticky; top: 0; z-index: 2;
  margin: 0 -1.1rem 0; padding: .95rem 1.1rem .6rem;
  background: var(--panel, #1c1a15);
  border-bottom: 1px solid rgba(140, 130, 115, .18);
}
.wantdlghead h2 { margin: 0; font-size: max(1rem, var(--txt-floor, 0rem)); }
.wantdlghead .e_close {
  font-size: max(1.4rem, var(--txt-floor, 0rem)); line-height: 1; padding: 0 .3rem;
  background: none; border: 0; cursor: pointer; color: var(--muted, #8a8175);
}
.wantdlghead .e_close:hover { color: var(--ink, #e8e2d6); }

.wantdlgrow { display: flex; flex-wrap: wrap; gap: .7rem; }
.wantdlgcard label {
  display: flex; flex-direction: column; gap: .25rem;
  flex: 1 1 12rem; min-width: 0;
  font-size: max(.7rem, var(--txt-floor, 0rem)); letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted, #8a8175);
}
.wantdlgcard input, .wantdlgcard select, .wantdlgcard textarea {
  font: inherit; font-size: max(.92rem, var(--txt-floor, 0rem)); text-transform: none; letter-spacing: normal;
  padding: .5rem .6rem; border-radius: 8px;
  color: var(--ink, #e8e2d6); background: var(--bg, #14110d);
  border: 1px solid var(--edge2, #3A4759);
}
/* The point of the dialog. */
.wantdlgnotes { flex: 1 1 auto; }
/* flex-shrink is the fix here, not a smaller number. The card is a flex
   column with a hard max-height; a textarea with min-height: 11rem cannot
   give any of it back, so every row BELOW it -- the actions, the format
   picker, Save and Cancel -- gets pushed past the bottom edge. Now the notes
   field is the part that yields when the card is short, which is right: it
   scrolls, and the buttons do not. */
/* The notes field is the one part that absorbs a short card, so it has to be
   the flexible one all the way down: the label is a flex column that may
   shrink to nothing, and the textarea inside it grows and shrinks to fill
   whatever is left.

   A FIXED height here was worse than the min-height it replaced. The label
   shrank, the textarea kept its 11rem, and the overflow painted straight over
   the buttons underneath -- text sitting on top of Delete and Save. Anything
   with a hard height inside a squeezed flex column will do that; the fix is
   to let it flex, with a floor. */
/* THE FLOOR BELONGS TO THE LABEL, NOT THE TEXTAREA. Both are flex items --
   the label inside the card, the textarea inside the label -- and a
   min-height on the inner one is precisely what breaks: the label shrinks as
   the card asks it to, the textarea refuses, and the difference is drawn on
   top of whatever comes next. That is how the notes text ended up written
   across the action buttons.

   So the label carries the floor and the textarea flexes freely inside it.
   When the card runs out of room the pair shrink together, and the card
   scrolls rather than anything overlapping. */
.wantdlgnotes { flex: 1 1 auto; min-height: 6.5rem; }
/* The label is a flex column, so the caption and its button need a row of
   their own or the button lands under the word "Notes". */
/* ---- the card in its two moods ----------------------------------------- */

/* READING is the default, so the form is what gets hidden, not the other way
   round: the fields, the notes textarea and Save are all edit-only. Cancel
   stays put and just changes its word, because in reading mode the only thing
   it does is close the card, and a second button for that would be one more
   thing at the bottom of a phone screen. */
.wantdlgcard:not(.editing) .wantdlgrow,
.wantdlgcard:not(.editing) .wantdlgnotes,
.wantdlgcard:not(.editing) .wantdlgfoot [type="submit"] { display: none; }
.wantdlgcard.editing .wantdlgread { display: none; }
.wantdlgcard.editing .e_edit { color: var(--brass, #C89A4E);
                               border-color: rgba(200, 154, 78, .5); }

.wantdlgread { display: flex; flex-direction: column; gap: .3rem; min-height: 0; }
.wantreadlab { margin: 0; font-size: max(.7rem, var(--txt-floor, 0rem));
               letter-spacing: .06em; text-transform: uppercase;
               color: var(--muted, #8a8175); }
/* Clamped, not scrolled. A scrollbar inside a card that already scrolls is
   two nested scrolls and a bad time on a touch screen. */
.wantreadnote { max-height: 11rem; overflow: hidden; }
.wantreadnote.open { max-height: none; }
.wantreadnote p { margin: 0 0 .55rem; line-height: 1.5;
                  font-size: max(.92rem, var(--txt-floor, 0rem));
                  font-family: var(--serif, Georgia, serif);
                  /* A pasted URL is one unbreakable word and will otherwise
                     push the card sideways. */
                  overflow-wrap: anywhere; }
.wantnotelink { color: var(--brass, #C89A4E); text-decoration: none;
                border-bottom: 1px solid rgba(200, 154, 78, .4); }
.wantnotelink:hover { border-bottom-color: var(--brass, #C89A4E); }
.wantreadempty { color: var(--muted, #8a8175); font-style: italic; }
.wantreadmore {
  align-self: flex-start; font: inherit;
  font-size: max(.72rem, var(--txt-floor, 0rem)); text-transform: uppercase;
  letter-spacing: .05em; background: none; border: 0; cursor: pointer;
  padding: .1rem 0; color: var(--brass, #C89A4E);
}
.wantreadmore:hover { text-decoration: underline; }

.wantnoteshead { display: flex; align-items: baseline; justify-content: space-between;
                 gap: .6rem; }
.wantnoteshead .e_room {
  font: inherit; font-size: max(.68rem, calc(var(--txt-floor, .68rem) * .85));
  text-transform: uppercase; letter-spacing: .05em;
  background: none; border: 1px solid rgba(200, 154, 78, .4); border-radius: 999px;
  color: var(--brass, #C89A4E); padding: .12rem .5rem; cursor: pointer;
}
.wantnoteshead .e_room:hover { background: rgba(200, 154, 78, .12); }

/* Everything the notes borrow their room from. Not display:none on the
   details block alone -- the action row and the cover are the two biggest
   things above the fold, and giving back only one of them is not worth the
   jump on screen. */
.wantdlgcard.notesfull .wantdlgtop,
.wantdlgcard.notesfull .wantdlgacts,
.wantdlgcard.notesfull .wantdupfmt { display: none; }
/* With the top gone the notes should take the room, not leave it empty. */
.wantdlgcard.notesfull .wantdlgnotes { min-height: 12rem; }
.wantdlgcard textarea {
  flex: 1 1 auto; min-height: 0; resize: vertical; line-height: 1.5;
  font-family: var(--serif, Georgia, serif);
}
/* On a short screen the notes box is what pushes the card past the viewport,
   and it is the one part that can give -- it grows when typed into, and the
   card scrolls. The cover shrinks with it so the head stays proportionate. */
@media (max-height: 720px) {
  .wantdlgnotes { min-height: 4.5rem; }
  .wantdlgcover { width: 66px; }
  .wantdlgcard { gap: .5rem; padding: 0 .9rem .9rem; }
  .wantdlghead { margin: 0 -.9rem 0; padding: .8rem .9rem .5rem; }
  .wantdlgfoot { margin: 0 -.9rem -.9rem; padding: .6rem .9rem .8rem; }
}
.wantdlghave {
  margin: 0; font-size: max(.76rem, var(--txt-floor, 0rem)); color: #8fbf8a;
  padding: .4rem .6rem; border-radius: 8px;
  border: 1px solid currentColor;
}
/* STUCK TO THE BOTTOM OF THE CARD. On a phone the card is taller than the
   screen and scrolls, and Save sat below the fold -- John changed a format on
   the iPhone and reported, correctly, that there was no way to save it. A
   primary action that requires discovering the card scrolls is a missing
   button. Sticky keeps Save, Cancel and Delete in view at every scroll
   position; the card scrolls behind them.

   The background is not decoration here: without it the notes text would run
   underneath the buttons as it scrolls past. */
.wantdlgfoot {
  display: flex; align-items: center; justify-content: flex-end; gap: .6rem;
  position: sticky; bottom: 0; z-index: 1;
  margin: 0 -1.1rem -1.3rem; padding: .7rem 1.1rem 1rem;
  background: var(--panel, #1c1a15);
  border-top: 1px solid rgba(140, 130, 115, .18);
}
.wantdlgfoot button {
  font: inherit; font-size: max(.82rem, var(--txt-floor, 0rem)); font-weight: 600;
  padding: .5rem 1.1rem; border-radius: 999px; cursor: pointer;
  color: var(--bg, #14110d); background: var(--brass, #C89A4E);
  border: 1px solid var(--brass, #C89A4E);
}
/* NOT the amber pill. .wantdlgfoot button fills every button with brass and
   dark text, which is right for Save -- it is the affirmative action. Delete
   inherited the fill and only had its text colour changed, so it came out
   muted-grey on amber: a button John could see but not read. It gets the same
   unfilled treatment as Cancel, in rust rather than grey, and it keeps its
   place hard left, away from Save. */
.wantdlgfoot .e_delete {
  margin-right: auto; background: none; font-weight: 400;
  color: #b5705f; border-color: rgba(181, 112, 95, .5);
}
.wantdlgfoot .e_delete:hover { color: #d08a76; border-color: #d08a76;
                               background: rgba(181, 112, 95, .12); }
.wantdlgfoot .e_cancel {
  color: var(--muted, #8a8175); background: none; font-weight: 400;
  border-color: var(--edge2, #3A4759);
}
.wantdlgfoot .wantstate { margin-right: auto; }

/* Phone. The 9rem floor that used to be here is gone: it was written when the
   card held a form and nothing else, and once the card grew a head, an action
   row and a format picker, a textarea that could not shrink below 144px
   overflowed its own label and painted its text across the buttons. On a
   screen this size the notes field is exactly the thing that must give.
   94vh also becomes 94dvh, for the same reason as the card's own max-height. */
@media (max-width: 560px) {
  .wantdlg { padding: .5rem; }
  .wantdlgcard { max-height: 94vh; max-height: 94dvh; }
  .wantdlgnotes { min-height: 4.5rem; }
}

/* A looser match — same title, same surname, but not the same full identity.
   Amber rather than green, because "looks like" is not "does". */

/* Sort control, priority and pin. */
.wantsort { display: flex; justify-content: flex-end; margin: 0 0 1.1rem; }
.wantsort label {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: max(.7rem, var(--txt-floor, 0rem)); letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted, #8a8175);
}
.wantsort select {
  font: inherit; font-size: max(.82rem, var(--txt-floor, 0rem)); text-transform: none; letter-spacing: normal;
  padding: .35rem .5rem; border-radius: 8px;
  color: var(--ink, #e8e2d6); background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
}

/* Only medium and low are marked. High is the default, and a badge on nearly
   every row would say nothing. */
.wantpri {
  margin-left: .5rem; font-size: max(.62rem, var(--txt-floor, 0rem)); letter-spacing: .06em;
  text-transform: uppercase; padding: .1rem .45rem; border-radius: 999px;
  border: 1px solid currentColor; vertical-align: .1em;
}
.wantpri.medium { color: #c9a86a; }
.wantpri.low { color: #7f8c98; }

.wantpin {
  font-size: max(.9rem, var(--txt-floor, 0rem)) !important; line-height: 1;
  padding: .28rem .5rem !important;
  color: var(--muted, #8a8175) !important;
}
.wantpin.on {
  color: var(--brass, #C89A4E) !important;
  border-color: var(--brass, #C89A4E) !important;
}
/* A pinned row should be findable without reading the star. */
.wantitems li[data-pinned="1"] {
  border-left: 2px solid var(--brass, #C89A4E);
  padding-left: .5rem;
}

/* ---- display settings -------------------------------------------------- */

.setwrap { max-width: 44rem; margin: 0 auto; padding: 1rem 1rem 4rem; }
.seth1 { font-size: max(1.5rem, var(--txt-floor, 0rem)); margin: .6rem 0 .2rem; }
.setsub { margin: 0 0 1.6rem; color: var(--muted, #8a8175);
          font-size: max(.85rem, var(--txt-floor, 0rem)); }
.setsec { margin: 0 0 1.6rem; }
.setsec h2 {
  font-size: max(.72rem, var(--txt-floor, 0rem));
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted, #8a8175); margin: 0 0 .3rem;
}
.sethint { margin: 0 0 .7rem; font-size: max(.8rem, var(--txt-floor, 0rem));
           color: var(--muted, #8a8175); }

.setopts { display: flex; flex-wrap: wrap; gap: .5rem; }
.setopts button {
  display: flex; flex-direction: column; align-items: flex-start; gap: .1rem;
  font: inherit; text-align: left; cursor: pointer;
  padding: .55rem .8rem; border-radius: 10px;
  color: var(--ink, #e8e2d6); background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
}
.setopts button.sel { border-color: var(--brass, #C89A4E); color: var(--brass, #C89A4E); }
/* Getting out again. Two fixed links always, plus one back to wherever you
   came from when the browser tells us. */
.setback { display: flex; gap: 1rem; flex-wrap: wrap; margin: 0 0 .8rem;
  font-size: max(.8rem, var(--txt-floor, 0rem)); }
.setback a { color: var(--accent, #c9a56a); text-decoration: none; }
.setback a:hover { text-decoration: underline; }
.setname { font-size: max(.88rem, var(--txt-floor, 0rem)); font-weight: 600; }
.setwhy { font-size: max(.72rem, var(--txt-floor, 0rem)); color: var(--muted, #8a8175); }
.setopts button.sel .setwhy { color: inherit; opacity: .8; }

/* The sample is built from the real want-list classes on purpose: it moves
   with the setting because it IS the thing being set. */
.setsample {
  padding: .8rem; border-radius: 10px;
  background: var(--panel, #1c1a15); border: 1px solid var(--edge, #2A3444);
}
.setsample .wanttitle { display: inline; }
.setsample span { display: block; }
.setsample .wantpri { display: inline-block; }

.setnote {
  font-size: max(.8rem, var(--txt-floor, 0rem));
  color: var(--muted, #8a8175);
  padding: .7rem .8rem; border-radius: 10px;
  border: 1px solid var(--edge, #2A3444);
}
.setfoot { display: flex; align-items: center; gap: .8rem; margin-top: 1rem; }
.setfoot button {
  font: inherit; font-size: max(.8rem, var(--txt-floor, 0rem));
  padding: .45rem 1rem; border-radius: 999px; cursor: pointer;
  color: var(--muted, #8a8175); background: none;
  border: 1px solid var(--edge2, #3A4759);
}
.quicklink.gear { margin-left: .35rem; padding: .26rem .5rem; }

/* Bringing a list in from elsewhere. Folded away: used once per list, then
   never again. */
.wantpaste { margin: 0 0 1rem; }
.wantpaste summary {
  cursor: pointer; font-size: max(.78rem, var(--txt-floor, 0rem));
  color: var(--brass, #C89A4E); padding: .3rem 0;
}
.wantpaste textarea {
  width: 100%; font: inherit; font-size: max(.86rem, var(--txt-floor, 0rem));
  padding: .6rem; border-radius: 8px; resize: vertical;
  color: var(--ink, #e8e2d6); background: var(--panel, #1c1a15);
  border: 1px solid var(--edge2, #3A4759);
}
.wantpaste code {
  font-size: max(.76rem, var(--txt-floor, 0rem));
  padding: .05rem .3rem; border-radius: 4px; background: var(--panel, #1c1a15);
}
.wantpasterow { display: flex; align-items: center; gap: .6rem; margin-top: .5rem; }
.wantpasterow button {
  font: inherit; font-size: max(.8rem, var(--txt-floor, 0rem)); font-weight: 600;
  padding: .45rem 1rem; border-radius: 999px; cursor: pointer;
  color: var(--bg, #14110d); background: var(--brass, #C89A4E);
  border: 1px solid var(--brass, #C89A4E);
}
.wantpasterow #pastecheck {
  color: var(--ink, #e8e2d6); background: none; border-color: var(--edge2, #3A4759);
}
.wantpastepreview {
  margin-top: .6rem; padding: .6rem .8rem; border-radius: 8px;
  font-size: max(.8rem, var(--txt-floor, 0rem));
  background: var(--panel, #1c1a15); border: 1px solid var(--edge, #2A3444);
}
.wantpastepreview ul { margin: .4rem 0 0; padding-left: 1.1rem; }
.wantpastepreview li { margin: .15rem 0; }

/* ---- CLZ records to fix — the checklist ---------------------------------
   A working page, not a document: the point is to tick things off while
   moving between CLZ and this list, so a row is a big tap target and the
   done ones stay visible rather than vanishing — John needs to see that he
   did them, and to be able to untick a mistake. */
.fixwrap { max-width: 60rem; margin: 0 auto; padding: 1rem 1.1rem 3rem; }
.fixwrap h1 { font-size: max(1.5rem, var(--txt-floor, 0rem)); margin: .6rem 0 .2rem; }
.fixsub { margin: 0 0 .8rem; color: var(--muted, #8a8175);
          font-size: max(.85rem, var(--txt-floor, 0rem)); }
.fixnote { margin: 0 0 1.6rem; line-height: 1.55; color: var(--muted, #8a8175);
           font-size: max(.88rem, var(--txt-floor, 0rem));
           border-left: 2px solid rgba(200, 154, 78, .4); padding-left: .8rem; }
.fixcat { font-size: max(1.05rem, var(--txt-floor, 0rem)); margin: 1.8rem 0 .2rem;
          display: flex; align-items: baseline; gap: .5rem; }
.fixcat span { font-size: max(.72rem, var(--txt-floor, 0rem));
               color: var(--muted, #8a8175); border: 1px solid currentColor;
               border-radius: 999px; padding: 0 .45rem; }
.fixwhy { margin: 0 0 .7rem; color: var(--muted, #8a8175); line-height: 1.5;
          font-size: max(.84rem, var(--txt-floor, 0rem)); }
/* "Also available" — the rest of a series John does not own. Deliberately
   plain: no cover, no jacket placeholder. /Books/awards/ established that a
   gap should look like a gap, and giving these art would make them read as
   books on the shelf. */
.books-reading .alsolist { list-style: none; margin: .2rem 0 1rem; padding: 0; }
.books-reading .alsoitem {
  display: flex; align-items: center; gap: .7rem;
  padding: .45rem .2rem; border-bottom: 1px solid var(--edge);
}
/* A real cover where Open Library had one. Small on purpose — this is a list
   to scan, and the shelf view is what the want list is for. The no-art rule
   John gave applies to /Books/awards/ only, where a cover MEANS ownership. */
.books-reading .alsocover {
  flex: 0 0 2.4rem; width: 2.4rem; aspect-ratio: 2 / 3; object-fit: cover;
  border-radius: 2px; border: 1px solid var(--edge2); background: var(--slate2);
}
.books-reading .alsocover.nocover { border-style: dashed; }

/* A row with a blurb is a <details>: the whole summary line is the tap
   target, and it opens before any script loads. Rows without one keep the
   plain layout rather than growing a control that does nothing. */
.books-reading .alsoitem.has-blurb { align-items: flex-start; }
.books-reading .alsodet { flex: 1; min-width: 0; }
.books-reading .alsodet > summary {
  display: flex; align-items: center; gap: .7rem; cursor: pointer;
  list-style: none; padding: .1rem 0;
}
.books-reading .alsodet > summary::-webkit-details-marker { display: none; }
.books-reading .alsodet > summary::after {
  content: "\25B6"; color: var(--brass-dim); font-size: .7em;
  margin-left: auto; transition: transform .12s ease;
}
.books-reading .alsodet[open] > summary::after { transform: rotate(90deg);
  color: var(--brass); }
.books-reading .alsodet > summary:hover .alsotitle { color: var(--brass); }
.books-reading .alsoblurb {
  margin: .5rem 0 .6rem 3.1rem; color: var(--mute);
  font-size: max(.82rem, var(--txt-floor, 0rem)); line-height: 1.55;
}
.books-reading .alsoyr { flex: 0 0 3rem; color: var(--mute2);
  font-size: max(.75rem, var(--txt-floor, 0rem)); }
.books-reading .alsotitle { flex: 1; color: var(--mute); }
.books-reading .wantbtn {
  font: inherit; font-size: max(.7rem, var(--txt-floor, 0rem));
  letter-spacing: .06em; text-transform: uppercase;
  padding: .25rem .75rem; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--brass-dim); background: none; color: var(--brass);
  /* 44px of tappable height on a phone, per the gesture failures in CLAUDE.md */
  min-height: 2.1rem;
}
.books-reading .wantbtn:hover { background: var(--brass); color: #1A1204; }
.books-reading .wantbtn.on { background: var(--patina); border-color: var(--patina);
  color: #0F131B; }
.books-reading .wantbtn:disabled { cursor: default; }

/* ---- /Books/cleanup/ : the consolidated checklist ----------------------
   Layout carried over from Cowork's CLZ cleanup page, which John asked to
   keep: progress bar, lettered amber badges, chips that match the letters.

   THE VARIABLES HERE ARE THE :root ONES — --accent, --rule, --muted, --fg.
   The brass palette (--brass, --edge2, --brass-dim, --mute2) is defined on
   .books-reading ONLY, and this page's body class is .books-fixes. Written
   against those, every `border: 1px solid var(--edge2)` was invalid at
   computed-value time and the WHOLE declaration was dropped — so the chips had
   no outline and ran together, and the letters had no box. Literal fallbacks
   on every one, so a future palette change cannot silently erase them again.

   Scoped to .books-fixes so it cannot reach the shelf chips on the front page,
   which share the .chip class name. */
.books-fixes .pw { display: flex; align-items: center; gap: .6rem;
  margin: .6rem 0 .2rem; font-size: max(.72rem, var(--txt-floor, 0rem));
  color: var(--accent, #c9a56a); letter-spacing: .04em; }
.books-fixes .pbar { flex: 1; height: 3px; background: var(--rule, #332f28);
  border-radius: 2px; overflow: hidden; }
.books-fixes .pbar i { display: block; height: 100%;
  background: var(--accent, #c9a56a); transition: width .3s; }

.books-fixes .chips { display: flex; gap: .35rem; flex-wrap: wrap;
  margin: .6rem 0 .3rem; }
.books-fixes .chip { font: inherit;
  font-size: max(.7rem, var(--txt-floor, 0rem));
  padding: .25rem .7rem;
  border: 1px solid var(--rule, #332f28); border-radius: 20px;
  color: var(--accent, #c9a56a); background: none; cursor: pointer;
  white-space: nowrap; display: inline-flex; align-items: center; gap: .35rem; }
.books-fixes .chip b { color: var(--muted, #a29a8f); font-weight: 400; }
.books-fixes .chip:hover { border-color: var(--accent, #c9a56a); }
.books-fixes .chip.sel { background: var(--accent, #c9a56a);
  border-color: var(--accent, #c9a56a); color: #1A1204; }
.books-fixes .chip.sel b { color: #5A4415; }
.books-fixes .chip.sel .k { color: #5A4415; border-color: #5A4415; }
/* A finished section dims rather than vanishing — the page must not reshuffle
   under him while he is working down it. */
.books-fixes .chip.done { opacity: .45; }

/* The lettered square, on the chip and on the heading it belongs to, so the
   two carry the same handle. */
.books-fixes .k {
  border: 1px solid var(--accent, #c9a56a); border-radius: 2px;
  padding: 0 .32rem; font-size: .8em; line-height: 1.5;
  color: var(--accent, #c9a56a); flex: 0 0 auto; }

.books-fixes .sub { margin: 1.7rem 0 .1rem;
  font-size: max(.68rem, var(--txt-floor, 0rem)); letter-spacing: .11em;
  text-transform: uppercase; color: var(--muted, #a29a8f); }

.books-fixes .fixsearch { font: inherit;
  font-size: max(.75rem, var(--txt-floor, 0rem));
  padding: .35rem .8rem; border-radius: 999px; min-width: 12rem;
  border: 1px solid var(--rule, #332f28); background: transparent;
  color: var(--fg, #eae6df); }
.books-fixes .fixsearch:focus { outline: none;
  border-color: var(--accent, #c9a56a); }

/* Section headings amber, like the page this replaced. */
.books-fixes .fixsec > summary { color: var(--accent, #c9a56a); }
.books-fixes .fixcatname { font-weight: 600; }
.books-fixes .fixcount { margin-left: auto; color: var(--muted, #a29a8f);
  font-size: max(.7rem, var(--txt-floor, 0rem)); letter-spacing: .07em;
  text-transform: uppercase; }
.books-fixes .fixsec > summary::before { color: var(--accent, #c9a56a); }

/* Hide-done: visibility only. The rows stay in the DOM and the ticks stay in
   the database — a save sends every checked box, so removing them would be a
   way to lose work, not a way to tidy.
   .fixwrap carries the class, not <body>: ".books-fixes.hidedone" would mean
   one element with both, which never matches and silently does nothing. */
.books-fixes .fixwrap.hidedone .fixitems li.done { display: none; }
.books-fixes .fixitems li.nomatch { display: none; }
.books-fixes details.fixsec.nomatch { display: none; }

/* Each fix category is a <details>, collapsed on load.
   Colours are the :root ones with literal fallbacks — the brass palette is
   defined on .books-reading only, and an undefined var() drops the whole
   declaration, which is how the section borders and the marker vanished. Same shape as .awyear
   on /Books/awards/ — the summary is the whole hit area, not a triangle,
   because these get tapped on a phone and a 12px glyph is not a target. */
.fixsec { margin: 0; border-top: 1px solid var(--rule, #332f28); }
.fixsec > summary {
  display: flex; align-items: baseline; gap: .6rem; cursor: pointer;
  padding: .8rem .2rem; list-style: none;
  font-size: max(1.05rem, var(--txt-floor, 0rem)); letter-spacing: .02em;
}
.fixsec > summary::-webkit-details-marker { display: none; }
/* U+25B6, the solid triangle — the "small" one at U+25B8 is invisible on a
   phone. ::before rather than a real element keeps it out of the a11y tree. */
.fixsec > summary::before {
  content: "\25B6"; color: var(--accent, #c9a56a); font-size: 1em;
  width: 1em; flex: 0 0 1em; text-align: center;
  transition: transform .12s ease; display: inline-block;
}
.fixsec[open] > summary::before { transform: rotate(90deg); color: var(--accent, #c9a56a); }
.fixsec > summary:hover, .fixsec > summary:hover::before { color: var(--accent, #c9a56a); }
.fixsec[open] > summary { border-bottom: 1px solid var(--rule, #332f28); }
.fixcatname { font-weight: 600; }
/* What is LEFT leads, because that is what John picks a batch by. Pushed
   right so a column of twelve reads as a list of sizes. */
.fixcount { margin-left: auto; font-size: max(.72rem, var(--txt-floor, 0rem));
  color: var(--mute2); letter-spacing: .06em; text-transform: uppercase; }
.fixsec .fixwhy { margin: .6rem 0 .7rem; }
.fixtoggle { margin: 1.2rem 0 .2rem; }
.fixallbtn {
  font: inherit; font-size: max(.72rem, var(--txt-floor, 0rem));
  letter-spacing: .07em; text-transform: uppercase;
  padding: .35rem .8rem; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--edge2); background: none; color: var(--mute);
}
.fixallbtn:hover { color: var(--brass); border-color: var(--brass); }

.fixitems { list-style: none; margin: 0; padding: 0; }
.fixitems li { border-bottom: 1px solid rgba(140, 130, 115, .16); }
.fixitems label { display: grid; grid-template-columns: auto 4.2rem 1fr; gap: .3rem .7rem;
                  align-items: baseline; padding: .7rem .2rem; cursor: pointer; }
.fixitems input { width: 1.1rem; height: 1.1rem; accent-color: #6f9c63;
                  align-self: center; }
.fixidx { color: var(--muted, #8a8175); font-variant-numeric: tabular-nums;
          font-size: max(.8rem, var(--txt-floor, 0rem)); }
.fixtitle { font-family: var(--serif, Georgia, serif);
            font-size: max(.98rem, var(--txt-floor, 0rem)); }
.fixsays { grid-column: 3; color: var(--muted, #8a8175);
           font-size: max(.78rem, var(--txt-floor, 0rem)); }
/* The link out of a row, for items where knowing is not the same as being able
   to act. It sits outside the <label> — inside, the label would swallow the
   click and tick the box instead — so it needs its own indent to line up under
   the title rather than under the checkbox. */
.fixgo { display: inline-block; margin: 0 0 .7rem calc(1.1rem + 4.2rem + 1.4rem);
         font-size: max(.78rem, var(--txt-floor, 0rem)); color: var(--brass, #C8A45C);
         text-decoration: none; border-bottom: 1px solid currentColor; }
.fixgo:hover { color: var(--text, #EDE6DA); }
@media (max-width: 700px) { .fixgo { margin-left: calc(1.1rem + 1.4rem); } }

/* Done stays legible. Struck through and dimmed says "handled"; hiding it
   would lose the evidence and make an accidental tick unrecoverable. */
.fixitems li.done .fixtitle { text-decoration: line-through; opacity: .5; }
.fixitems li.done .fixidx, .fixitems li.done .fixsays { opacity: .45; }
.fixstate { min-height: 1.2rem; margin: 1.2rem 0 0; color: var(--brass, #C89A4E);
            font-size: max(.8rem, var(--txt-floor, 0rem)); }
.fixstate.bad { color: #d08a76; }

@media (max-width: 560px) {
  .fixitems label { grid-template-columns: auto 1fr; }
  .fixidx { grid-row: 1; }
  .fixtitle, .fixsays { grid-column: 2; }
}

/* ---- read aloud ---------------------------------------------------------
   The same surface as the type panel, because it is the same kind of control
   and appears from the button beside it. Both halves of the colour pair are
   named explicitly: this is a floating panel, and the one time that was left
   to inheritance it came out invisible on a dark theme. */
#rdvoice { flex: 1 1 auto; min-width: 0; font: inherit;
           font-size: max(.8rem, var(--txt-floor, 0rem));
           padding: .3rem .4rem; border-radius: 6px;
           color: var(--ink, #e8e2d6); background: var(--bg, #14110d);
           border: 1px solid var(--edge2, #3A4759); }
#rdrate { flex: 1 1 auto; accent-color: var(--brass, #C89A4E); }
#rdtypepanel .rdface { font: inherit;
  font-size: max(.8rem, var(--txt-floor, 0rem));
  padding: .3rem .7rem; border-radius: 999px; cursor: pointer;
  background: none; color: var(--brass, #C89A4E);
  border: 1px solid rgba(200, 154, 78, .5); }
#rdtypepanel .rdface:hover { background: rgba(200, 154, 78, .12); }
.rdspeaknote { margin: .5rem 0 0; line-height: 1.45;
               font-size: max(.7rem, var(--txt-floor, 0rem));
               color: var(--muted, #8a8175); }
/* Speaking is a state, so the button shows it rather than relying on the icon
   alone — the glyph is small and the bar is busy. */
.rdaloudhead { margin-top: .8rem; padding-top: .7rem;
               border-top: 1px solid rgba(140, 130, 115, .22); }
#rdspeak[aria-pressed="true"] { color: var(--brass, #C89A4E);
                                border-color: var(--brass, #C89A4E); }

/* ---------------------------------------------------------------- awards
   /Books/awards/ — the Hugo and Nebula histories against the collection.
   The row's job is to answer "have I got this" at a glance, so a matched
   book gets a cover and warm ink and an unmatched one stays quiet. */
.awwrap { max-width: 60rem; margin: 0 auto; padding: 1.4rem 1.1rem 4rem; }
.awwrap h1 { margin: .6rem 0 .2rem; }
.awwrap .lede { color: var(--mute2); margin: 0 0 1rem; }
.awsub { font-size: max(.8rem, var(--txt-floor, 0rem)); color: var(--mute2);
         margin: .5rem 0 1.4rem; }

.awtabs { display: flex; gap: .5rem; margin: 1rem 0 .7rem; }
.awtab {
  font-size: max(.95rem, var(--txt-floor, 0rem)); padding: .35rem .95rem;
  border: 1px solid var(--edge2); border-radius: var(--r);
  color: var(--mute); text-decoration: none; letter-spacing: .02em;
}
.awtab[aria-current] { background: var(--brass); border-color: var(--brass); color: #1A1204; }

.awcats { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 .4rem; }
.awcat {
  font-size: max(.6875rem, var(--txt-floor, 0rem)); padding: .22rem .6rem;
  border-radius: 999px; border: 1px solid var(--edge2); color: var(--mute);
  text-decoration: none; letter-spacing: .02em;
}
.awcat span { color: var(--mute2); }
.awcat.on { background: var(--brass); border-color: var(--brass); color: #1A1204; }
.awcat.on span { color: #1A1204; opacity: .7; }

/* Each year is a <details>. The summary is the whole hit area — 88 of these
   get tapped on a phone, so it spans the row rather than being a triangle. */
.awyear { margin: 0; border-top: 1px solid var(--edge); }
.awyear > summary {
  display: flex; align-items: baseline; gap: .6rem; cursor: pointer;
  padding: .75rem .2rem; list-style: none;
  font-size: max(1.05rem, var(--txt-floor, 0rem)); letter-spacing: .04em;
}
.awyear > summary::-webkit-details-marker { display: none; }
/* Own marker, so it can sit on the left and rotate. ::before on the summary
   rather than a real element keeps it out of the accessibility tree. */
.awyear > summary::before {
  /* U+25B6, the SOLID triangle, not U+25B8 — the latter is named "small" and
     renders like it. At .8em in --mute2 it was invisible on a phone. Sized
     and coloured to be found without looking for it. */
  content: "\25B6"; color: var(--brass-dim); font-size: 1em;
  width: 1em; flex: 0 0 1em; text-align: center;
  transition: transform .12s ease; display: inline-block;
}
.awyear[open] > summary::before { transform: rotate(90deg); }
.awyear > summary:hover { color: var(--brass); }
.awyear > summary:hover::before { color: var(--brass); }
/* Open is the state worth seeing at a glance down a column of 80. */
.awyear[open] > summary::before { color: var(--brass); }
.awyear[open] > summary { border-bottom: 1px solid var(--edge); }
.awyr { font-weight: 600; }
/* A year with none of them owned still says so — but quietly, so a column of
   eighty reads as "where are the ones I have" rather than eighty reproaches. */
.awgot.none { color: var(--mute2); }
.awyear > *:not(summary):last-child { margin-bottom: 1.2rem; }

.awtoggle { margin: .2rem 0 1rem; }
.awtoggle button { cursor: pointer; font: inherit; background: none; }
.awgot { font-size: max(.7rem, var(--txt-floor, 0rem)); color: var(--brass-dim);
         letter-spacing: .06em; text-transform: uppercase; }
.awcatname { font-size: max(.75rem, var(--txt-floor, 0rem)); color: var(--mute2);
             text-transform: uppercase; letter-spacing: .08em;
             margin: .9rem 0 .3rem; font-weight: 600; }

.awlist { list-style: none; margin: 0 0 .4rem; padding: 0; }
/* The row is a grid so the marker, the cover and the text stay in three
   columns whatever the title length. A row with no cover keeps the column,
   or the titles of a year would not line up with each other. */
.awrow {
  display: grid; grid-template-columns: 1.1rem auto 1fr; align-items: start;
  gap: .1rem .6rem; padding: .3rem 0;
}
.awmark { color: var(--edge2); line-height: 1.5; }
.awrow.win .awmark { color: var(--brass); }
.awrow.win .awtitle { color: var(--warmink); font-weight: 600; }
.awtext { display: flex; flex-direction: column; gap: .05rem; min-width: 0; }
.awtitle { line-height: 1.35; }
.awby { font-size: max(.8rem, var(--txt-floor, 0rem)); color: var(--mute2); }
.awown {
  font-size: max(.7rem, var(--txt-floor, 0rem)); color: var(--brass);
  text-decoration: none; letter-spacing: .04em; margin-top: .12rem;
}
.awown:hover { text-decoration: underline; }

/* Covers only where a book was matched. Best Novel gets the taller one; the
   other categories are a long list and a big cover each would make the page
   a scroll rather than a table. */
.awcover { width: 2rem; height: 3rem; object-fit: cover; border-radius: 2px;
           border: 1px solid var(--edge2); display: block; }
.awrow.big .awcover { width: 3.2rem; height: 4.8rem; }
.awcover.nocover { background: var(--slate2); }
/* The blank keeps the COLUMN so titles line up down the year, but takes no
   height: reserving a full cover's height on every unmatched row turned a
   list of six novels into a page of gaps. */
.awcover.blank, .awrow.big .awcover.blank {
  border-color: transparent; background: none; height: 0;
}   /* beats .awrow.big .awcover, which sets a height */
.awhit:hover .awcover { border-color: var(--brass); }
.awrow.big { padding: .45rem 0; }
.awrow.big .awtitle { font-size: max(1rem, var(--txt-floor, 0rem)); }

@media (max-width: 34rem) {
  .awrow.big .awcover { width: 2.6rem; height: 3.9rem; }
  .awcats { gap: .3rem; }
}

/* Award chips on a want list. Reuses .chip, so they match the filters on the
   reading page rather than inventing a third pill style. */
.wantawards { display: flex; flex-wrap: wrap; gap: .4rem; margin: .6rem 0 .2rem; }
.wantawards .chip span { opacity: .65; }
.wantawards .chip.on span { opacity: .75; }

/* The working-pages directory, now its own page rather than a section under
   the shelf. Reuses .pages, which already styles the list it inherited. */
.workwrap { max-width: 48rem; margin: 0 auto; padding: 1.4rem 1.1rem 4rem; }
.workwrap h1 { margin: .6rem 0 .2rem; }
.workwrap .lede { color: var(--muted, #8a8175); margin: 0 0 1.4rem; }

/* The cover column. Normally one jacket; two when a single award covers two
   volumes, as Blackout/All Clear does. Wrapping them keeps the row a
   three-column grid instead of growing a fourth on that row alone. */
.awcovers { display: flex; gap: .3rem; align-items: flex-start; }

/* ---------------------------------------------------------------------------
   Feature pages — one book that earns its own page.
   
   Wider measure than the doc pages and a serif body, because this is meant to
   be read rather than worked through. The facts list is a dealer's entry, not
   a form: label and value on one line, hairline between, no boxes.
   
   Palette comes from :root like everything else, with literal fallbacks — an
   undefined var() drops the WHOLE declaration, which is how the kind chips
   once shipped with no border at all.
   --------------------------------------------------------------------------- */
.feature { max-width: 54rem; }
/* A feature link on a book page: the page's own name, and an x that removes it
   only when removing it affects this book alone. */
.featon { display: inline-flex; align-items: baseline; gap: .3rem;
  margin-right: .7rem; }
.featon .hint { font-size: max(.72rem, var(--txt-floor, 0rem)); }
.featx { padding: 0 .3rem; line-height: 1; }
#featsel, #featscope { max-width: 15rem; }

/* The series toggle: a standing rule, so it says what it will keep doing and
   how many books it speaks for before it is turned on. */
.fttag { margin: 2.6rem 0 0; padding: 1rem 1.1rem;
  border: 1px solid rgba(140,130,115,.22); border-radius: var(--r, 4px); }
.fttagq { margin: 0 0 .6rem; font-size: max(.92rem, var(--txt-floor, 0rem)); }
.fttaga { margin: 0; display: flex; align-items: baseline; gap: .5rem;
  flex-wrap: wrap; }
.fttaga .hint { flex: 1 1 18rem; font-size: max(.76rem, var(--txt-floor, 0rem)); }
.ftyes.on, .ftno.on { background: var(--brass, #C8A45C);
  border-color: var(--brass, #C8A45C); color: #1A1204; }

/* One command that clears a whole section. Wraps rather than scrolls: an ssh
   line with two paths in it is long, and a command you cannot see all of is one
   you retype wrongly. */
.fixcmd {
  margin: .2rem 0 .9rem; padding: .6rem .8rem; border-radius: var(--r, 4px);
  background: rgba(0,0,0,.28); border: 1px solid rgba(140,130,115,.22);
  color: var(--brass, #C8A45C); font-size: max(.78rem, var(--txt-floor, 0rem));
  white-space: pre-wrap; overflow-wrap: anywhere; user-select: all;
}

/* Verdict buttons on the cleanup page: the action IS the fix here, not a
   record that it was done elsewhere, so these are buttons and not a tick. */
.fixbtn { font: inherit; font-size: max(.76rem, var(--txt-floor, 0rem));
  padding: .18rem .55rem; border-radius: 999px; cursor: pointer;
  background: none; color: var(--brass, #C8A45C);
  border: 1px solid var(--brass, #C8A45C); }
.fixbtn:hover:not(:disabled) { background: var(--brass, #C8A45C); color: #1A1204; }
.fixbtn:disabled { opacity: .45; cursor: default; }
li.ruled { opacity: .6; }
li.ruled .fixbtn { display: none; }

/* The Featured toggle, beside Status. A checkbox rather than a chip because it
   saves with everything else on that button, and a chip that looked pressed
   before Save had run would be lying. */
.featbox { display: inline-flex; align-items: center; gap: .35rem;
  margin-left: .9rem; color: var(--mute2, #b3a894);
  font-size: max(.82rem, var(--txt-floor, 0rem)); cursor: pointer; }

.ftback { margin: 0 0 1.4rem; font-size: max(.8rem, var(--txt-floor, 0rem)); }
.ftback a { color: var(--mute, #8a8175); text-decoration: none; }
.ftback a:hover { color: var(--brass, #C8A45C); }
/* The header: the book beside its own name, the shape /Books/reading/ uses.
   Grid rather than float so the facts list below stays inside the text column
   instead of wrapping under the image.

   THE JACKET COLUMN IS OPT-IN, and that direction matters. It was written the
   other way first — two columns always — and a feature with no cover put its
   only child in the 13rem slot, so On Discworld opened with its title broken
   over two lines and "Terry Pratchett" wrapping inside a strip. The bare page
   is now the default and the picture adds a column, so a page that forgets the
   modifier is merely plain instead of unreadable. */
.fthead { display: grid; grid-template-columns: 1fr; gap: 0 2rem;
          align-items: start; margin: 0 0 1.4rem; }
.fthaspic { grid-template-columns: 13rem 1fr; }
.fthcover { padding: 0; border: 0; background: none; cursor: zoom-in;
            line-height: 0; width: 100%; }
.fthcover img { width: 100%; height: auto; border-radius: 2px;
                box-shadow: 0 3px 18px rgba(0,0,0,.5); }
/* A mirror thumbnail, shown at its own size. 185px is all there is. */
.fthflat { cursor: default; display: block; max-width: 11.6rem; }
.fthtext { min-width: 0; }
/* Under 700px the jacket goes above the title rather than shrinking into a
   stamp: a 6rem-wide binding photograph shows nothing worth seeing. */
@media (max-width: 700px) {
  .fthead { grid-template-columns: 1fr; gap: 1.1rem 0; }
  .fthcover { max-width: 13rem; }
}
.feature h1 { margin: 0 0 .2rem; line-height: 1.15; }
.ftsub {
  margin: 0 0 1.8rem; color: var(--mute, #8a8175); font-style: italic;
  font-size: max(1rem, var(--txt-floor, 0rem));
}

/* Label / value pairs. Grid rather than a table so a long issue-point note
   wraps under its own column instead of stretching the label one. */
.ftfacts {
  display: grid; grid-template-columns: 8.5rem 1fr; gap: 0 1.4rem;
  margin: 0 0 1.6rem; padding: 1rem 0; border-top: 1px solid rgba(140,130,115,.22);
  border-bottom: 1px solid rgba(140,130,115,.22);
}
.ftfacts dt {
  color: var(--mute, #8a8175); font-size: max(.74rem, var(--txt-floor, 0rem));
  letter-spacing: .06em; text-transform: uppercase; padding: .35rem 0;
}
.ftfacts dd {
  margin: 0; padding: .35rem 0;
  font-size: max(.92rem, var(--txt-floor, 0rem));
}
@media (max-width: 620px) {
  .ftfacts { grid-template-columns: 1fr; gap: 0; }
  .ftfacts dt { padding-bottom: 0; }
  .ftfacts dd { padding-top: .1rem; padding-bottom: .7rem; }
}
.ftlot { margin: -.8rem 0 2rem; font-size: max(.82rem, var(--txt-floor, 0rem)); }
.ftlot a, .ftbody a { color: var(--brass, #C8A45C); }

.ftbody { font-family: var(--serif, Georgia, serif); line-height: 1.65; }
.ftbody p { margin: 0 0 1.1rem; font-size: max(1rem, var(--txt-floor, 0rem)); }
.ftbody h2 {
  margin: 2.2rem 0 .9rem; font-size: max(1.15rem, var(--txt-floor, 0rem));
  letter-spacing: .01em;
}
/* The restorer's letter. Quoted rather than paraphrased because the list of
   five operations is the whole decision, and a summary would lose which parts
   of the book were kept. */
.ftbody blockquote {
  margin: 0 0 1.3rem; padding: .2rem 0 .2rem 1.2rem;
  border-left: 2px solid var(--brass, #C8A45C);
  color: var(--mute2, #b3a894); font-style: italic;
}
.ftbody blockquote p { margin: 0 0 .7rem; }
.ftbody blockquote p:last-child { margin-bottom: 0; }

/* Plates. Before and after are separate sets, because for this book the
   pairing IS the story. */
.plateset { margin: 2.4rem 0 0; }
.plateset h3 {
  margin: 0 0 .9rem; font-size: max(.76rem, var(--txt-floor, 0rem));
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--mute, #8a8175); font-weight: 600;
}
.plates {
  display: grid; gap: 1.1rem;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
}
.plate { margin: 0; }
.platebtn {
  display: block; width: 100%; padding: 0; border: 0; background: none;
  cursor: zoom-in; line-height: 0;
}
.platebtn img {
  width: 100%; height: auto; border-radius: 2px;
  box-shadow: 0 2px 14px rgba(0,0,0,.45);
}
.plate figcaption {
  margin: .5rem 0 0; line-height: 1.4;
  font-size: max(.76rem, var(--txt-floor, 0rem)); color: var(--mute, #8a8175);
}
.platenote {
  margin: 2rem 0 0; padding: .9rem 1.1rem; border: 1px dashed rgba(140,130,115,.3);
  border-radius: 3px; color: var(--mute, #8a8175);
  font-size: max(.82rem, var(--txt-floor, 0rem));
}

/* Lightbox. Its own, not the reading page's — that one is wired to .coverbtn
   and to covers.js, and sharing it would mean two pages depending on one
   script's assumptions about what an image on the page means. */
.plb {
  position: fixed; inset: 0; z-index: 90; display: flex;
  align-items: center; justify-content: center; padding: 2rem;
  background: rgba(8, 7, 5, .93);
}
.plb[hidden] { display: none; }
/* The frame scrolls; the backdrop does not. Two elements rather than one so a
   click on the empty area still closes, which it would not if the image simply
   overflowed the backdrop. */
.plbscroll { max-width: 100%; max-height: 100%; overflow: auto; display: flex;
             align-items: center; justify-content: center; }
.plb figure { margin: 0; max-width: 100%; max-height: 100%; text-align: center; }
.plb img { max-width: 100%; max-height: 82vh; width: auto; height: auto; }

/* FULL SIZE: the image stops being constrained and the frame gains bars. The
   flex centring has to go with it, or a picture wider than the frame is pinned
   by its centre and the left edge becomes unreachable. */
.plb.plbfull .plbscroll { display: block; width: 100%; height: 100%; }
.plb.plbfull img { max-width: none; max-height: none; }
.plb.plbfull figure { max-width: none; max-height: none; }

/* BRIGHTEN. A viewing filter, never written to the file — the plates vary by
   1.7x in the ORIGINAL photographs and the archive stays faithful to them. */
.plb.plblit img { filter: brightness(1.32) contrast(1.06); }

.plbbar { position: absolute; top: .5rem; right: .8rem; z-index: 2;
          display: flex; gap: .5rem; align-items: center; }
.plbtool {
  min-height: 2.25rem; padding: .35rem .9rem; cursor: pointer;
  background: rgba(20,17,12,.82); color: var(--mute2, #b3a894);
  border: 1px solid rgba(140,130,115,.4); border-radius: 999px;
  font-family: inherit; font-size: max(.78rem, var(--txt-floor, 0rem));
}
.plbtool:hover { color: var(--text, #EDE6DA); border-color: var(--brass, #C8A45C); }
.plbtool[hidden] { display: none; }
.plb figcaption {
  margin: .8rem 0 0; color: var(--mute2, #b3a894);
  font-size: max(.82rem, var(--txt-floor, 0rem));
}
.plbx { font-size: 1.3rem; line-height: 1; padding: .1rem .7rem; }
.plbx:hover { color: var(--text, #EDE6DA); }

/* ---------------------------------------------------------------------------
   Add a book. Desktop first — John said he will fill these in at a desk —
   collapsing to one column on a phone rather than shrinking a two-column grid
   into something neither.
   --------------------------------------------------------------------------- */
.addwrap { max-width: 52rem; }
.addform fieldset {
  border: 0; border-top: 1px solid rgba(140,130,115,.22);
  margin: 1.8rem 0 0; padding: 1.2rem 0 0;
}
.addform legend {
  padding: 0 .7rem 0 0; color: var(--brass, #C8A45C);
  font-size: max(.74rem, var(--txt-floor, 0rem));
  letter-spacing: .09em; text-transform: uppercase;
}
.agrid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem 1.4rem; }
@media (max-width: 700px) { .agrid { grid-template-columns: 1fr; } }
.afield { display: flex; flex-direction: column; gap: .3rem; }
.afield.awide { grid-column: 1 / -1; margin-top: 1rem; }
.addform label {
  color: var(--mute2, #b3a894);
  font-size: max(.8rem, var(--txt-floor, 0rem));
}
.addform input[type=text], .addform input[type=number], .addform input[type=date],
.addform select, .addform textarea {
  /* 44px minimum, and 16px text so iOS does not zoom the page on focus —
     the same reason the search boxes are sized the way they are. */
  min-height: 2.75rem; padding: .5rem .7rem; font-size: max(1rem, var(--txt-floor, 0rem));
  background: rgba(0,0,0,.22); color: var(--text, #EDE6DA);
  border: 1px solid var(--edge2, rgba(140,130,115,.35)); border-radius: 3px;
  font-family: inherit;
}
/* A SELECT NEEDS AN OPAQUE BACKGROUND, and the rule above gives every control
   a translucent one. That is fine for the closed control — the dark card shows
   through — and wrong for the POPUP, which the operating system draws on its
   own light surface while keeping the light `color` set above. John found it
   as "white text on a light gray background" on the Image Type menu; it was
   true of Format and Binding too, and had been since the form was written.
   The new dropdown only made it worth looking at.

   Both halves are needed. Some browsers paint the popup from the select's own
   background, others from the option's, and neither inherits a translucent
   value usefully. This is the "overriding one property and inheriting the
   rest" fault §6 lists three times over — colour set without a background. */
.addform select { background-color: var(--bg, #14110d); }
.addform select option {
  background-color: var(--bg, #14110d);
  color: var(--text, #EDE6DA);
}
.addform textarea { min-height: 5rem; line-height: 1.5; resize: vertical; }
.addform input:focus, .addform select:focus, .addform textarea:focus {
  outline: none; border-color: var(--brass, #C8A45C);
}
.addform input[type=file] {
  padding: .55rem 0; color: var(--mute, #8a8175);
  font-size: max(.85rem, var(--txt-floor, 0rem));
}
.ahint {
  color: var(--mute, #8a8175); line-height: 1.4;
  font-size: max(.72rem, var(--txt-floor, 0rem));
}
.ahint code { font-size: .95em; }
.acheck { justify-content: flex-end; }
.acheck label { display: flex; align-items: center; gap: .5rem; min-height: 2.75rem;
                color: var(--text, #EDE6DA); cursor: pointer; }
.acheck input { width: 1.15rem; height: 1.15rem; accent-color: #6f9c63; }

.aactions { display: flex; align-items: center; gap: 1.2rem; margin: 2rem 0 0; }
.abtn {
  min-height: 2.75rem; padding: .6rem 1.4rem; cursor: pointer;
  background: var(--brass, #C8A45C); color: #1A1204; border: 0; border-radius: 999px;
  font-family: inherit; font-size: max(.9rem, var(--txt-floor, 0rem)); font-weight: 600;
}
.abtn:hover { filter: brightness(1.08); }
.acancel { color: var(--mute, #8a8175); text-decoration: none;
           font-size: max(.85rem, var(--txt-floor, 0rem)); }
.acancel:hover { color: var(--text, #EDE6DA); }

.asaved, .aerror {
  margin: 0 0 1.4rem; padding: .8rem 1.1rem; border-radius: 3px;
  font-size: max(.88rem, var(--txt-floor, 0rem));
}
.asaved { border: 1px solid rgba(111,156,99,.5); background: rgba(111,156,99,.12); }
.aerror { border: 1px solid rgba(190,90,70,.55); background: rgba(190,90,70,.12); }
.asaved a { color: var(--brass, #C8A45C); }

/* The panel that answers "do I have to type this?" before it is asked. */
.aderived { margin: 2.4rem 0 0; }
.aderived summary {
  cursor: pointer; color: var(--mute, #8a8175);
  font-size: max(.82rem, var(--txt-floor, 0rem));
}
.aderived dl {
  display: grid; grid-template-columns: 12rem 1fr; gap: .5rem 1.2rem;
  margin: .9rem 0 0; padding: .9rem 0 0;
  border-top: 1px solid rgba(140,130,115,.2);
}
.aderived dt { color: var(--mute2, #b3a894);
               font-size: max(.78rem, var(--txt-floor, 0rem)); }
.aderived dd { margin: 0; color: var(--mute, #8a8175); line-height: 1.5;
               font-size: max(.78rem, var(--txt-floor, 0rem)); }
@media (max-width: 620px) {
  .aderived dl { grid-template-columns: 1fr; gap: .15rem; }
  .aderived dd { margin-bottom: .7rem; }
}

/* The live character counter. Only turns colour at the cap, because a number
   that changes colour every keystroke is noise. */
.acount { color: var(--mute, #8a8175); font-variant-numeric: tabular-nums; }
.acount.afull { color: #d98b6a; font-weight: 600; }
.aerror.akept { margin-top: -.9rem; border-color: rgba(140,130,115,.3);
                background: transparent; color: var(--mute, #8a8175); }

/* Prose facts for a book this site catalogues itself — binding, provenance,
   condition, where it lives. A label/value pair rather than a run-on line,
   because these are sentences and the biblio line above is not. */
.localfact { display: flex; gap: .8rem; margin: .35rem 0; line-height: 1.5;
             font-size: max(.86rem, var(--txt-floor, 0rem)); }
.lfl { flex: 0 0 8rem; color: var(--mute, #8a8175);
       font-size: max(.72rem, var(--txt-floor, 0rem));
       letter-spacing: .06em; text-transform: uppercase; padding-top: .15rem; }
.lfv { color: var(--mute2, #b3a894); }
.lffeat { color: var(--brass, #C8A45C); text-decoration: none;
          border-bottom: 1px solid currentColor; }
/* "FEATURE ARTICLE" is two words and wraps in the 8rem column, which looked
   like a mistake beside BINDING and PROVENANCE sitting on one line each.
   
   THE WHOLE COLUMN WIDENS, not just that row. Each fact is its own flex line,
   so the "column" is only a column because every label shares one basis —
   widening one label alone would step its value out of line with the others.
   
   AND ONLY ON A BOOK THAT HAS ONE. Most books have no feature, and there is no
   reason for them to pay for a label they do not show; the class is emitted by
   the renderer, which already knows.
   
   Measured in ch rather than rem because the label's size is
   max(.72rem, --txt-floor) and John can raise that floor in Settings — a fixed
   rem that fits at the default wraps again two notches up. ch tracks the font,
   and max() keeps it from ever being narrower than it is today. */
.featwide .lfl { flex: 0 0 max(8rem, 20ch); white-space: nowrap; }

@media (max-width: 620px) {
  .localfact { flex-direction: column; gap: .1rem; }
  .lfl, .featwide .lfl { flex: none; }
}

/* Maintenance — copies that have left a master. Deliberately plain: this is
   the one page whose button cannot be undone, so it gets no flourishes that
   might make the button feel routine. */
.maintwrap { max-width: 54rem; }
.maintempty {
  margin: 1.6rem 0 0; padding: 1.4rem 1.6rem; border-radius: 4px;
  border: 1px solid rgba(140,130,115,.22); color: var(--mute, #8a8175);
  line-height: 1.6; font-size: max(.9rem, var(--txt-floor, 0rem));
}
.maintempty strong { color: var(--text, #EDE6DA); }
.maintlist { list-style: none; margin: 1.4rem 0 0; padding: 0; }
.maintlist li { border-bottom: 1px solid rgba(140,130,115,.16); }
.maintlist label {
  display: grid; grid-template-columns: auto 1fr auto; gap: .3rem .9rem;
  align-items: baseline; padding: .8rem .2rem; cursor: pointer;
}
.maintlist input { width: 1.1rem; height: 1.1rem; accent-color: #be5a46;
                   align-self: center; }
.mtitle { font-family: var(--serif, Georgia, serif);
          font-size: max(.98rem, var(--txt-floor, 0rem)); }
.mmeta  { grid-column: 2; color: var(--mute, #8a8175);
          font-size: max(.76rem, var(--txt-floor, 0rem)); }
.mholds { grid-row: 1; grid-column: 3; color: #d9a05f;
          font-size: max(.76rem, var(--txt-floor, 0rem)); }
/* A copy carrying nothing is the easy case and should not shout. */
.mholds.mnone { color: var(--mute, #8a8175); }
/* One that carries your writing gets an edge, so the eye finds it first. */
.maintlist li.holds { border-left: 2px solid #d9a05f; padding-left: .7rem; }
.mopen { margin: 0 0 .8rem calc(1.1rem + 1.2rem); display: inline-block;
         color: var(--brass, #C8A45C); text-decoration: none;
         font-size: max(.78rem, var(--txt-floor, 0rem));
         border-bottom: 1px solid currentColor; }
.abtn.mdanger { background: #be5a46; color: #fff; }
.abtn.mdanger:hover { filter: brightness(1.08); }
@media (max-width: 700px) {
  .maintlist label { grid-template-columns: auto 1fr; }
  .mholds { grid-row: auto; grid-column: 2; }
}


/* A series listing on a feature page. Number, jacket, then title and blurb —
   the shape a reader scans down rather than reads across. */
.serieslist { list-style: none; margin: 1.4rem 0 0; padding: 0; counter-reset: none; }
.sbook {
  display: grid; grid-template-columns: 2.2rem 4.5rem 1fr; gap: 0 1.1rem;
  align-items: start; padding: 1rem 0;
  border-bottom: 1px solid rgba(140,130,115,.16);
}
.snum { color: var(--mute, #8a8175); font-variant-numeric: tabular-nums;
        font-size: max(.8rem, var(--txt-floor, 0rem)); padding-top: .2rem;
        text-align: right; }
.sart { line-height: 0; }
.scover { width: 100%; height: auto; border-radius: 2px;
          box-shadow: 0 2px 10px rgba(0,0,0,.4); }
/* A book with no art gets a blank jacket, not a broken image — the same
   decision covers.js makes on the shelf. */
.scover.snone { display: block; aspect-ratio: 2 / 3;
                border: 1px dashed rgba(140,130,115,.3); border-radius: 2px;
                box-shadow: none; }
.sbody { min-width: 0; }
.stitle { font-family: var(--serif, Georgia, serif);
          font-size: max(1rem, var(--txt-floor, 0rem)); }
.sbody a { color: var(--brass, #C8A45C); text-decoration: none; }
.sbody a:hover .stitle { text-decoration: underline; }
.smeta { display: block; margin: .15rem 0 .45rem; color: var(--mute, #8a8175);
         font-size: max(.76rem, var(--txt-floor, 0rem)); }
.sblurb { margin: 0; color: var(--mute2, #b3a894); line-height: 1.55;
          font-size: max(.86rem, var(--txt-floor, 0rem)); }
/* Four lines, and the toggle is only added when it really overflows. */
.sblurb.clamped { display: -webkit-box; -webkit-line-clamp: 4;
                  -webkit-box-orient: vertical; overflow: hidden; }
/* A book he does not own is present but recessive: the gap is the point of a
   series page, and a gap that shouts is a gap that reads as a fault. */
.sbook.snot .stitle, .sbook.snot .sblurb { opacity: .62; }
.sbook.snot .scover { opacity: .5; filter: saturate(.4); }
@media (max-width: 620px) {
  .sbook { grid-template-columns: 1.8rem 3.4rem 1fr; gap: 0 .8rem; }
}


/* /Books/run/ ------------------------------------------------------------
   A page of buttons that start things. The visual job is to make the state
   of each job readable at a glance from a phone: what it does, when it last
   ran, and whether it is in hand right now. Colour carries none of that on
   its own — a busy row also says "waiting…" on its button and a failed one
   says FAILED in words, because three of this project's UI bugs came from
   overriding one property and inheriting the rest. */
.runwrap { max-width: 60rem; }
.runnotice {
  margin: 1rem 0; padding: .6rem .9rem;
  border-left: 3px solid var(--accent); background: var(--panel);
  border-radius: 0 6px 6px 0;
}
.joblist { list-style: none; margin: .4rem 0 1.8rem; padding: 0; }
.jobrow {
  display: flex; gap: 1rem; align-items: center; justify-content: space-between;
  flex-wrap: wrap;
  padding: .7rem .9rem; margin-bottom: .5rem;
  background: var(--panel); border: 1px solid var(--rule); border-radius: 8px;
}
.jobrow.busy { border-color: var(--accent); }
.jobrow.bad  { border-color: var(--err); }
.jobtext { flex: 1 1 22rem; min-width: 0; }
.jobrow h3 { margin: 0 0 .15rem; font-size: max(1rem, var(--txt-floor, 0rem)); }
.jobrow .hint { margin: 0 0 .2rem; }
.jobwhen { margin: 0; color: var(--muted); font-size: max(.85rem, var(--txt-floor, 0rem)); }
/* 44px, because a hover chip and a long-press have both failed on John's
   devices. A plain tappable button, and big enough to hit. */
.runbtn {
  min-height: 44px; min-width: 6.5rem; padding: .5rem 1.1rem;
  border: 0; border-radius: 6px; cursor: pointer;
  background: var(--accent); color: var(--bg);
  font-size: max(.95rem, var(--txt-floor, 0rem)); font-weight: 600;
}
.runbtn[disabled] { background: var(--rule); color: var(--muted); cursor: default; }
.uploadform { display: flex; gap: .8rem; align-items: center; flex-wrap: wrap;
              margin-bottom: 1.8rem; }
.uploadform input[type=file] { color: var(--fg); flex: 1 1 18rem; }
.joblog {
  background: var(--panel); border: 1px solid var(--rule); border-radius: 8px;
  padding: .8rem; white-space: pre-wrap; word-break: break-word;
  max-height: 34rem; overflow: auto;
  font-size: max(.82rem, var(--txt-floor, 0rem));
}
.runtable { width: 100%; border-collapse: collapse; }
.runtable th, .runtable td {
  text-align: left; padding: .35rem .6rem;
  border-bottom: 1px solid var(--rule);
  font-size: max(.88rem, var(--txt-floor, 0rem));
}
.runtable th { color: var(--muted); font-weight: 600; }

/* The CLZ upload, inside the job card that consumes it. It began under its own
   heading at the foot of the page and John could not find it — a control that
   answers a question belongs where the question is asked. Full width of the
   card, below the text and the Run button, so the reading order is: what this
   does, what is waiting, how to add one. */
.uploadbox {
  flex: 1 1 100%;
  margin-top: .6rem; padding-top: .6rem;
  border-top: 1px dashed var(--rule);
}
.waitlist {
  list-style: none; margin: .2rem 0 .5rem; padding: 0;
  color: var(--fg); font-size: max(.85rem, var(--txt-floor, 0rem));
}
.waitlist li { padding: .15rem 0; }
/* Quieter than Run: uploading is the preparation, not the action. */
.runbtn.alt { background: transparent; color: var(--accent);
              border: 1px solid var(--accent); }

/* CLZ's My Images on a book page. The plate classes do the work — same
   markup, same lightbox, one stylesheet — so this only sets the section
   apart from a feature page, where plates are the whole point rather than
   a note beside the facts. Smaller thumbnails for the same reason. */
.copyplates { margin: 1.4rem 0 0; }
.copyplates h2 { margin-bottom: .3rem; }
.copyplates .plates { gap: .5rem; }
.copyplates .plate { max-width: 9rem; }
.copyplates figcaption {
  font-size: max(.78rem, var(--txt-floor, 0rem));
  color: var(--muted);
}

/* The My Images panel on /Books/add/. A card per photograph, laid out the way
   CLZ lays its own out — picture, description, type — because John works in
   both and a different arrangement for the same job is one more thing to hold
   in mind. Wraps rather than scrolls: there is no cap on how many. */
.imgcards { display: flex; flex-wrap: wrap; gap: .8rem; margin-top: .6rem; }
.imgcard {
  flex: 0 1 15.5rem; display: flex; flex-direction: column; gap: .3rem;
  padding: .6rem; border: 1px solid var(--rule); border-radius: 8px;
  background: var(--panel);
}
.imgcard img { width: 100%; height: auto; border-radius: 4px; }
.imgcard-none {
  padding: 1.4rem .4rem; text-align: center; color: var(--muted);
  border: 1px dashed var(--rule); border-radius: 4px;
}
.imgcard label { display: flex; flex-direction: column; gap: .15rem;
                 font-size: max(.82rem, var(--txt-floor, 0rem)); color: var(--muted); }
.imgcard input[type=text], .imgcard select { width: 100%; }
/* Dashed, like CLZ's "drop or click to add" slot, so it reads as an empty
   place for something rather than a card with its picture missing. */
.imgnew { border-style: dashed; }
/* .imgcard label.imgdel, NOT .imgdel — and this is the third time this exact
   trap has been written down in this file. `.imgcard label` is a class plus an
   element, (0,1,1); a bare `.imgdel` is (0,1,0) and LOSES, so flex-direction
   stayed `column` and the checkbox sat on top of its own words instead of
   beside them. John read it as the column being too narrow; it was not, and
   widening alone would have produced a wider column with the same stack.
   See the note at .books-reader a.rdswitchbtn for the same fault twice over. */
.imgcard label.imgdel {
  flex-direction: row; align-items: center; gap: .45rem;
  margin-top: .2rem;
}
/* The checkbox itself must not be stretched by align-items on the row, and
   must not shrink away either — both have happened elsewhere in this file. */
.imgcard label.imgdel input[type=checkbox] { flex: 0 0 auto; margin: 0; }
.imgro { margin: 0; font-size: max(.82rem, var(--txt-floor, 0rem)); }

/* The drop zone. CLZ's says "Drop, paste or click to add a new image" and
   John works in both, so this says the same in the same place — plus the one
   thing worth spelling out, that dragging straight out of another browser
   window works without downloading first. */
.imgnew .dropzone-hint {
  margin: 0 0 .3rem; color: var(--muted);
  font-size: max(.8rem, var(--txt-floor, 0rem));
}
.imgnew.dropping {
  border-color: var(--accent); border-style: solid;
  background: color-mix(in srgb, var(--accent) 12%, var(--panel));
}
.droppreview { width: 100%; height: auto; border-radius: 4px; margin: .2rem 0; }
.dropnote { margin: .2rem 0 0; font-size: max(.8rem, var(--txt-floor, 0rem)); }
.dropnote.ok  { color: var(--accent); }
.dropnote.bad { color: var(--err); }

/* The cover already on the book, above its file input. A file input cannot be
   pre-filled, so an edit form otherwise has no way to say "there is already a
   photograph here" — and John read the empty control as the cover having gone.
   Small, because it is an answer to a question rather than the subject of the
   page. */
.acurrent {
  display: block; max-width: 7rem; height: auto; margin: .1rem 0 .35rem;
  border: 1px solid var(--edge2, rgba(140,130,115,.35)); border-radius: 3px;
}

/* The long help under each job. Folded away by default: fifteen jobs each with
   a paragraph of prose is a wall, and the point of the page is to press a
   button. A native <details> so it needs no JavaScript and behaves on the
   phone, where John is most likely to be reading it. */
.jobhelp { margin: .35rem 0 0; }
.jobhelp summary {
  cursor: pointer; color: var(--accent);
  font-size: max(.78rem, var(--txt-floor, 0rem));
  /* 44px of tappable height, because a summary is a control and this project
     has lost three arguments to targets that were too small on an iPad. */
  padding: .5rem 0; min-height: 2.75rem; display: flex; align-items: center;
}
.jobhelp[open] summary { margin-bottom: .2rem; }
.jobhelp p {
  margin: 0 0 .5rem; max-width: 46rem;
  color: var(--muted); line-height: 1.5;
  font-size: max(.8rem, var(--txt-floor, 0rem));
}

.jobhelp ol { margin: 0 0 .6rem; padding-left: 1.4rem; max-width: 46rem; }
.jobhelp li {
  margin: 0 0 .35rem; color: var(--muted); line-height: 1.5;
  font-size: max(.8rem, var(--txt-floor, 0rem));
}
