/* Single-theme by decision, not omission: the airy white IS the brief.
     Every colour is painted explicitly so the page holds on any host ground. */
  :root {
    --ground:   #FBFAF8;
    --surface:  #FFFFFF;
    --sunk:     #F4F2ED;
    --ink:      #1B1A17;
    --ink-soft: #46433C;
    --muted:    #7A756B;
    --faint:    #A9A399;
    --rule:     #E4E0D8;
    --rule-mid: #CFC9BD;
    --brass:    #8A6A2F;
    --brass-lit:#C9A961;
    --brass-wash:#F6F0E2;
    --good:     #4C6B52;
    --warn:     #9A6B2F;
    --shadow:   0 1px 2px rgba(27,26,23,.06), 0 8px 24px -12px rgba(27,26,23,.18);

    --serif: "Newsreader", Georgia, serif;
    --sans:  "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    --mono:  "IBM Plex Mono", ui-monospace, Menlo, monospace;
  }

  * { box-sizing: border-box; }

  body {
    margin: 0;
    background: var(--ground);
    color: var(--ink);
    font-family: var(--sans);
    font-size: 15px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
  }

  button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
  a { color: var(--brass); }
  :focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; border-radius: 2px; }

  .lbl {
    font-size: 10.5px; font-weight: 600; letter-spacing: .12em;
    text-transform: uppercase; color: var(--muted);
  }
  .serif { font-family: var(--serif); }
  .mono { font-family: var(--mono); font-size: .86em; }
  .num { font-variant-numeric: tabular-nums; }

  /* ---------- chrome ---------- */

  header.bar {
    position: sticky; top: 0; z-index: 20;
    background: rgba(251,250,248,.94);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--rule);
  }
  .bar-inner {
    max-width: 1220px; margin: 0 auto; padding: 0 1.5rem;
    display: flex; align-items: center; gap: 1rem; height: 56px;
  }

  nav.main { display: flex; gap: .25rem; flex: 1; }
  /* Narrow screens: a hamburger, not a full-width select. The select filled
     the bar and read as a control belonging to the page rather than as
     navigation, and it hid the current page behind a tap. */
  .burger {
    display: none; width: 34px; height: 30px; padding: 7px 6px;
    border: 1px solid var(--rule-mid); border-radius: 3px;
    background: var(--surface); cursor: pointer;
    flex-direction: column; justify-content: space-between;
  }
  .burger span {
    display: block; height: 1.5px; width: 100%; background: var(--ink);
    border-radius: 1px;
  }
  .navpanel {
    display: none; position: absolute; top: 52px; left: .75rem; z-index: 30;
    min-width: 12rem; padding: .35rem;
    background: var(--surface); border: 1px solid var(--rule-mid);
    border-radius: 4px; box-shadow: var(--shadow);
  }
  .navpanel[data-open="true"] { display: block; }
  .navpanel a {
    display: block; padding: .5rem .6rem; border-radius: 3px;
    font-size: 14px; color: var(--muted);
  }
  .navpanel a:hover { background: var(--sunk); color: var(--ink); }
  .navpanel a[aria-current="page"] { color: var(--ink); font-weight: 600; }
  @media (max-width: 46rem) {
    nav.main { display: none; }
    .burger { display: flex; }
  }
  nav.main button {
    padding: .45rem .7rem; border-radius: 3px; color: var(--muted);
    font-size: 13.5px; white-space: nowrap;
  }
  nav.main button:hover { color: var(--ink); background: var(--sunk); }
  nav.main button[aria-current="page"] { color: var(--ink); font-weight: 600; }
  nav.main button[aria-current="page"]::after {
    content: ""; display: block; height: 2px; background: var(--brass); margin-top: .3rem;
  }

  .bar-right { display: flex; align-items: center; gap: .9rem; flex-shrink: 0; position: relative; }
  .langtog { display: flex; border: 1px solid var(--rule-mid); border-radius: 3px; overflow: hidden; }
  .langtog button { padding: .22rem .5rem; font-size: 11.5px; font-weight: 600; letter-spacing: .06em; color: var(--muted); }
  .langtog button[aria-pressed="true"] { background: var(--ink); color: var(--surface); }
  .usermenu {
    position: absolute; right: 0; top: 42px; width: 12.5rem; z-index: 30;
    background: var(--surface); border: 1px solid var(--rule-mid); border-radius: 3px;
    box-shadow: var(--shadow); padding: .8rem; display: none;
  }
  .usermenu[data-open="true"] { display: block; }
  .usermenu .um-name { font-family: var(--serif); font-size: 1.05rem; }
  .usermenu .um-row { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-top: .85rem; }
  .usermenu .um-link { display: block; width: 100%; text-align: left; font-size: 13px; color: var(--ink-soft); padding: .3rem 0; }
  .usermenu .um-link:hover { color: var(--brass); }
  .usermenu hr { border: none; border-top: 1px solid var(--rule); margin: .8rem 0 .2rem; }
  .avatar {
    width: 28px; height: 28px; border-radius: 50%; background: var(--brass-wash);
    border: 1px solid var(--brass-lit); color: var(--brass);
    display: grid; place-items: center; font-size: 11.5px; font-weight: 600;
  }

  main { max-width: 1220px; margin: 0 auto; padding: 2.5rem 1.5rem 6rem; }

  .crumb { display: flex; gap: .45rem; align-items: center; margin-bottom: .6rem; font-size: 12.5px; color: var(--muted); flex-wrap: wrap; }
  .crumb button { color: var(--muted); }
  .crumb button:hover { color: var(--brass); }
  .crumb .sep { color: var(--faint); }

  h1.title { font-family: var(--serif); font-weight: 400; font-size: 2.1rem; line-height: 1.15; margin: 0 0 .3rem; letter-spacing: -.015em; }
  .sub { color: var(--muted); font-size: 14px; margin: 0 0 2rem; }

  section.block { margin-bottom: 3.25rem; }
  .block-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--rule); padding-bottom: .55rem; margin-bottom: 1.25rem; }
  .block-head h2 { font-family: var(--sans); font-size: 11px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin: 0; }
  .block-head .aside { font-size: 12.5px; color: var(--faint); }

  /* ---------- search ---------- */

  .searchwrap { position: relative; max-width: 620px; }
  .searchwrap input {
    width: 100%; padding: .9rem 1rem .9rem 2.6rem;
    font-family: var(--serif); font-size: 1.15rem;
    background: var(--surface); border: 1px solid var(--rule-mid); border-radius: 3px; color: var(--ink);
  }
  .searchwrap input::placeholder { color: var(--faint); }
  .searchwrap input:focus { border-color: var(--brass); outline: none; box-shadow: 0 0 0 3px var(--brass-wash); }
  .searchwrap svg { position: absolute; left: .95rem; top: 50%; transform: translateY(-50%); color: var(--faint); }

  /* ---------- covers ---------- */

  .cover { position: relative; aspect-ratio: 2/3; border-radius: 2px; overflow: hidden; box-shadow: var(--shadow); background: var(--sunk); }
  .cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .cover.ph { display: flex; flex-direction: column; justify-content: space-between; padding: .7rem .6rem; border: 1px solid var(--rule-mid); }
  .cover.ph .ph-t {
    font-family: var(--serif); font-size: 11.5px; line-height: 1.22; color: var(--ink);
    overflow: hidden; overflow-wrap: break-word; hyphens: auto; max-height: 62%;
  }
  .cover.ph .ph-a { font-size: 9.5px; color: var(--muted); letter-spacing: .04em; text-transform: uppercase; }
  .cover.ph::after { content: ""; position: absolute; left: .6rem; right: .6rem; top: 2.6rem; height: 1px; background: currentColor; opacity: .25; }

  .shelfrow { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 1.4rem 1.1rem; }
  .bookcard { text-align: left; display: block; width: 100%; }
  .bookcard .t { font-family: var(--serif); font-size: 14px; line-height: 1.28; margin-top: .55rem; display: block; }
  .bookcard .a { font-size: 11.5px; color: var(--muted); margin-top: .1rem; display: block; }
  /* One line, always. The whole breadcrumb is worth having under a card -
     "Shelf B3" means nothing once a second bookcase has one - but three
     wrapped lines of it turns a quiet aside into the loudest thing on the
     card and leaves the grid ragged. Truncated here, complete on the book. */
  .bookcard .w {
    font-size: 10.5px; color: var(--faint); margin-top: .25rem; display: block;
    font-family: var(--mono);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .bookcard:hover .t { color: var(--brass); }
  .bookcard:hover .cover { transform: translateY(-2px); }
  .cover { transition: transform .15s ease; }

  /* ---------- cubby grid ---------- */

  .roomgrid { display: flex; flex-direction: column; gap: .35rem; }
  .cubrow { display: grid; grid-template-columns: 1.6rem repeat(8, 1fr); gap: .35rem; align-items: stretch; }
  .rowlabel { display: grid; place-items: center; font-family: var(--mono); font-size: 11px; color: var(--faint); }
  .collabels { display: grid; grid-template-columns: 1.6rem repeat(8, 1fr); gap: .35rem; margin-top: .35rem; }
  .collabels span { text-align: center; font-family: var(--mono); font-size: 11px; color: var(--faint); }
  .cubby {
    background: var(--surface); border: 1px solid var(--rule); border-radius: 2px;
    aspect-ratio: 1/.48; padding: .4rem .5rem; display: flex; flex-direction: column;
    justify-content: space-between; text-align: left; position: relative; min-width: 0;
  }
  .cubby.blank { background: transparent; border-style: dashed; border-color: #EDEAE2; }
  .cubby.blank .cb-a { color: var(--faint); }
  .cubby:hover { border-color: var(--brass-lit); background: var(--brass-wash); }
  .cubby .cb-a { font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
  .cubby .cb-n { font-family: var(--serif); font-size: 1.05rem; line-height: 1; }
  .cubby .cb-n small { font-size: .62em; color: var(--muted); font-family: var(--sans); }
  .cubby .cb-empty { font-size: 15px; color: #DDD8CE; line-height: 1; }
  .dot { position: absolute; top: .45rem; right: .5rem; width: 6px; height: 6px; }
  .dot.cur { background: var(--brass); }
  .dot.rev { background: var(--warn); }
  .dot.upd { background: var(--brass); opacity: .45; }
  .dot.none { border: 1px solid var(--rule-mid); }

  .legend { display: flex; gap: 1.25rem; flex-wrap: wrap; font-size: 11.5px; color: var(--muted); margin-top: 1rem; }
  .legend span { display: flex; align-items: center; gap: .4rem; }
  .legend i { width: 7px; height: 7px; display: block; position: static; flex-shrink: 0; }

  /* ---------- generic place list ---------- */

  .placelist { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 1rem; }
  .placecard {
    background: var(--surface); border: 1px solid var(--rule); border-radius: 2px;
    text-align: left; overflow: hidden; display: flex; flex-direction: column;
  }
  .placecard:hover { border-color: var(--brass-lit); }
  .placecard .thumb { aspect-ratio: 4/3; background: var(--sunk); overflow: hidden; }
  .placecard .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .placecard .thumb.blank { display: grid; place-items: center; color: var(--faint); font-size: 11.5px; }
  .placecard .pc-body { padding: .75rem .85rem .85rem; display: flex; flex-direction: column; gap: .2rem; }
  .placecard .pc-name { font-family: var(--serif); font-size: 1.05rem; line-height: 1.2; }
  .placecard .pc-meta { font-size: 11.5px; color: var(--muted); display: flex; gap: .5rem; align-items: center; }

  /* ---------- review ---------- */

  .reviewphoto { position: relative; border: 1px solid var(--rule); border-radius: 2px; overflow: hidden; background: var(--surface); }
  .reviewphoto img { width: 100%; display: block; }
  .glow { position: absolute; border-radius: 40%; background: radial-gradient(ellipse at center, rgba(201,169,97,.55), rgba(201,169,97,0) 70%); pointer-events: none; }

  .revgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(216px, 1fr)); gap: 1rem; }
  .revcard { background: var(--surface); border: 1px solid var(--rule); border-radius: 2px; padding: .8rem; display: flex; flex-direction: column; gap: .65rem; }
  .revcard.flag { border-color: var(--brass-lit); background: var(--brass-wash); }
  .revpair { display: grid; grid-template-columns: 54px 1fr; gap: .7rem; align-items: start; }
  .revspine { border-radius: 2px; overflow: hidden; border: 1px solid var(--rule); background: var(--sunk); }
  .revspine img { width: 100%; display: block; }
  .revcard .rt { font-family: var(--serif); font-size: 13.5px; line-height: 1.25; }
  .revcard .ra { font-size: 11px; color: var(--muted); }
  .conf { display: flex; align-items: center; gap: .45rem; font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
  .confbar { flex: 1; height: 3px; background: var(--rule); position: relative; }
  .confbar i { position: absolute; inset: 0 auto 0 0; background: var(--brass); }
  .confbar.low i { background: var(--rule-mid); }
  .revacts { display: flex; gap: .35rem; }
  .revacts button { flex: 1; padding: .3rem; font-size: 11px; border: 1px solid var(--rule-mid); border-radius: 2px; color: var(--muted); }
  .revacts button:hover { border-color: var(--brass); color: var(--brass); }

  .keybar { display: flex; gap: 1.1rem; flex-wrap: wrap; font-size: 11.5px; color: var(--muted); padding: .7rem 0; }
  kbd { font-family: var(--mono); font-size: 10.5px; border: 1px solid var(--rule-mid); border-bottom-width: 2px; border-radius: 3px; padding: .1rem .32rem; background: var(--surface); color: var(--ink-soft); }

  .primary {
    background: var(--ink); color: var(--surface); padding: .55rem 1.05rem;
    border-radius: 3px; font-size: 13px; font-weight: 500;
  }
  .primary:hover { background: var(--brass); }
  .ghost { border: 1px solid var(--rule-mid); padding: .55rem 1.05rem; border-radius: 3px; font-size: 13px; color: var(--ink-soft); }
  .ghost:hover { border-color: var(--brass); color: var(--brass); }

  /* ---------- detail ---------- */

  .detail { display: grid; gap: 2.5rem; }
  @media (min-width: 54rem) { .detail { grid-template-columns: 230px 1fr; } }
  dl.facts { margin: 0; display: grid; grid-template-columns: max-content 1fr; gap: .55rem 1.5rem; font-size: 13.5px; }
  dl.facts dt { color: var(--muted); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; font-weight: 600; padding-top: .18rem; }
  dl.facts dd { margin: 0; }

  .timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0; }
  .timeline li { display: grid; grid-template-columns: 5.5rem 1fr; gap: 1rem; padding: .7rem 0; border-bottom: 1px solid var(--rule); font-size: 13.5px; }
  .timeline li:last-child { border-bottom: none; }
  .timeline .when { font-family: var(--mono); font-size: 11px; color: var(--faint); padding-top: .2rem; }
  .timeline .what strong { font-weight: 600; }
  .timeline .note { color: var(--muted); font-size: 12.5px; }

  /* ---------- coverage ---------- */

  .covrow { display: grid; grid-template-columns: 10rem 1fr 4.5rem; gap: 1rem; align-items: center; padding: .7rem 0; border-bottom: 1px solid var(--rule); }
  .covbar { height: 8px; background: var(--sunk); display: flex; overflow: hidden; border-radius: 1px; }
  .covbar i { display: block; height: 100%; }
  .covbar .b-cur { background: var(--brass); }
  .covbar .b-rev { background: var(--warn); }
  .covbar .b-upd { background: var(--brass-lit); }
  .covpct { text-align: right; font-family: var(--mono); font-size: 12.5px; color: var(--muted); }

  .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); border-radius: 2px; overflow: hidden; margin-bottom: 2.5rem; }
  .stat { background: var(--surface); padding: 1rem 1.1rem; }
  .stat .v { font-family: var(--serif); font-size: 1.9rem; line-height: 1; letter-spacing: -.02em; }
  .stat .v small { font-size: .5em; color: var(--muted); font-family: var(--sans); letter-spacing: 0; }
  .stat .k { margin-top: .35rem; }

  .peoplegrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 1.25rem; }
  .person { background: var(--surface); border: 1px solid var(--rule); border-radius: 2px; padding: 1.25rem; text-align: left; }
  .person:hover { border-color: var(--brass-lit); }
  .person .pav { width: 40px; height: 40px; border-radius: 50%; background: var(--brass-wash); border: 1px solid var(--brass-lit); color: var(--brass); display: grid; place-items: center; font-weight: 600; margin-bottom: .8rem; }
  .person .pn { font-family: var(--serif); font-size: 1.25rem; }
  .person .pc { font-size: 12.5px; color: var(--muted); margin-top: .15rem; }

  .pills { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: 2.75rem; }
  .pill {
    display: inline-flex; align-items: baseline; gap: .4rem;
    border: 1px solid var(--rule-mid); border-radius: 999px;
    padding: .3rem .85rem; font-size: 12.5px; color: var(--muted);
  }
  .pill b { font-family: var(--serif); font-size: 1.05rem; color: var(--ink); font-weight: 400; }
  button.pill:hover { border-color: var(--brass); color: var(--brass); }

  .fourup { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem 1.25rem; max-width: 30rem; }
  @media (min-width: 46rem) { .fourup { grid-template-columns: repeat(4, minmax(0, 1fr)); max-width: 46rem; } }

  .filterwrap { margin: 0 0 1.25rem; max-width: 22rem; }
  .filterwrap input {
    width: 100%; padding: .45rem .7rem; font-size: 13.5px; font-family: var(--sans);
    background: var(--surface); border: 1px solid var(--rule-mid); border-radius: 3px; color: var(--ink);
  }
  .filterwrap input:focus { border-color: var(--brass); outline: none; }

  .rowlist { display: flex; flex-direction: column; border-top: 1px solid var(--rule); }
  .row {
    display: flex; align-items: center; gap: .9rem; width: 100%; text-align: left;
    padding: .8rem .25rem; border-bottom: 1px solid var(--rule);
  }
  .row:hover { background: var(--surface); }
  .row:hover .row-name { color: var(--brass); }
  .row-name { flex: 1; font-family: var(--serif); font-size: 1.1rem; }
  .row-meta { font-size: 12.5px; color: var(--muted); }
  .row-thumb { width: 46px; height: 34px; background: var(--sunk); border-radius: 2px; overflow: hidden; flex-shrink: 0; }
  .row-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .chev { color: var(--faint); font-size: 1.2rem; line-height: 1; }

  .title-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

  .photobtn {
    display: block; width: 100%; max-width: 760px; padding: 0; margin: 0 0 1.75rem;
    border: 1px solid var(--rule); border-radius: 2px; overflow: hidden; background: var(--surface);
  }
  .photobtn img { width: 100%; display: block; }
  .photobtn:hover { border-color: var(--brass-lit); }

  .empty { padding: 3rem 0; color: var(--muted); font-size: 14px; max-width: 34rem; }
  .empty .serif { font-size: 1.3rem; color: var(--ink); display: block; margin-bottom: .5rem; }

  .banner {
    border: 1px solid var(--brass-lit); background: var(--brass-wash); border-radius: 2px;
    padding: 1rem 1.15rem; display: flex; gap: 1rem; align-items: flex-start;
    margin-bottom: 2rem; flex-wrap: wrap;
  }
  .banner .bt { font-family: var(--serif); font-size: 1.1rem; line-height: 1.3; }
  .banner .bd { font-size: 13px; color: var(--ink-soft); margin-top: .25rem; max-width: 46rem; }
  .banner .ba { display: flex; gap: .5rem; margin-top: .75rem; }

  .mockup-note {
    max-width: 1220px; margin: 0 auto; padding: 0 1.5rem 3rem;
    font-size: 12px; color: var(--faint); border-top: 1px solid var(--rule); padding-top: 1.25rem;
  }

  @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }

  /* ---------- application additions -------------------------------------
     The block above is the design mockup's stylesheet, kept verbatim so the
     two do not drift. Everything below exists because the real app uses
     links and forms where the mockup used buttons and inline handlers. */

  nav.main a {
    padding: .45rem .7rem; border-radius: 3px; color: var(--muted);
    font-size: 13.5px; white-space: nowrap; text-decoration: none;
  }
  nav.main a:hover { color: var(--ink); background: var(--sunk); }
  nav.main a[aria-current="page"] { color: var(--ink); font-weight: 600; }
  nav.main a[aria-current="page"]::after {
    content: ""; display: block; height: 2px; background: var(--brass); margin-top: .3rem;
  }
  .langtog form { display: contents; }
  .usermenu form { margin: 0; }

  a.row, a.bookcard, a.person, a.pill { text-decoration: none; color: inherit; }
  a.crumb-link { color: var(--muted); text-decoration: none; }
  a.crumb-link:hover { color: var(--brass); }

  .stack { display: flex; flex-direction: column; gap: .35rem; margin-top: 1.5rem; }
  .stack .lbl { margin-top: .8rem; }
  .field {
    width: 100%; padding: .55rem .7rem; font: inherit;
    background: var(--surface); border: 1px solid var(--rule-mid);
    border-radius: 3px; color: var(--ink);
  }
  .field:focus { border-color: var(--brass); outline: none; }
  /* A hint, not something you typed. The browser default is nearly as dark as
     real input, which makes an empty field look filled in. */
  .field::placeholder, .filterwrap input::placeholder { color: var(--faint); opacity: 1; }
  .stack .primary { margin-top: 1.4rem; }

  .ghost {
    padding: .5rem .9rem; border: 1px solid var(--rule-mid); border-radius: 3px;
    font-size: 13px; color: var(--ink-soft); background: var(--surface);
  }
  .ghost:hover { border-color: var(--brass); color: var(--brass); }
  .ghost[disabled] { opacity: .5; cursor: default; }

  .queue { display: flex; flex-direction: column; gap: .75rem; }
  .qitem {
    background: var(--surface); border: 1px solid var(--rule); border-radius: 2px;
    padding: 1rem 1.15rem; display: grid; grid-template-columns: 4.5rem 1fr auto;
    gap: 1rem; align-items: center;
  }
  .qitem.hot { border-color: var(--brass-lit); background: var(--brass-wash); }
  .qitem .qthumb { width: 4.5rem; aspect-ratio: 4/3; overflow: hidden; border-radius: 2px; background: var(--sunk); }
  .qitem .qthumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .qitem .qt { font-family: var(--serif); font-size: 1.05rem; line-height: 1.3; }
  .qitem .qd { font-size: 12.5px; color: var(--muted); margin-top: .2rem; }
  .qitem .qa { display: flex; gap: .4rem; flex-wrap: wrap; justify-content: flex-end; }

  .dropzone {
    border: 1px dashed var(--rule-mid); border-radius: 3px; padding: 3rem 1.5rem;
    text-align: center; background: var(--surface); margin-bottom: 2.5rem;
  }
  .dropzone.over { border-color: var(--brass); background: var(--brass-wash); }
  .dropzone .dz-t { font-family: var(--serif); font-size: 1.15rem; margin-bottom: .9rem; }

  /* Room, furniture, shelf. Side by side where there is room for them, so the
     whole address is readable at once; stacked on a phone, where three selects
     across would put every name back into an ellipsis. */
  .placepick { max-width: 46rem; margin-inline: auto; text-align: left; margin-bottom: 1.5rem; }
  .placepick-row { display: flex; gap: .6rem; align-items: end; }
  .placepick-row > label { flex: 1 1 0; min-width: 0; display: block; }
  .placepick-row > label > .sub { display: block; font-size: 11.5px; margin-bottom: .2rem; }
  .placepick-row .field { width: 100%; }
  .placepick-row .field:disabled { opacity: .55; cursor: not-allowed; }
  @media (max-width: 640px) {
    .placepick-row { flex-direction: column; align-items: stretch; gap: .5rem; }
  }

  .picker { position: relative; }
  .picker-results {
    position: absolute; left: 0; right: 0; top: calc(100% + .25rem); z-index: 40;
    background: var(--surface); border: 1px solid var(--rule-mid); border-radius: 3px;
    box-shadow: var(--shadow); max-height: 22rem; overflow-y: auto;
  }
  .picker-results button {
    display: grid; grid-template-columns: 30px 1fr; gap: .6rem; width: 100%;
    padding: .5rem .6rem; text-align: left; border-bottom: 1px solid var(--rule);
  }
  .picker-results button:hover { background: var(--sunk); }
  .picker-results img { width: 30px; border-radius: 1px; }
  .picker-results .pt { font-family: var(--serif); font-size: 13.5px; }
  .picker-results .pa { font-size: 11px; color: var(--muted); }

  dialog.sheet {
    border: 1px solid var(--rule-mid); border-radius: 3px; padding: 1.5rem;
    max-width: 32rem; width: calc(100% - 2rem); background: var(--surface);
    color: var(--ink); box-shadow: var(--shadow);
  }
  dialog.sheet::backdrop { background: rgba(27,26,23,.35); }

  .tags { display: flex; gap: .4rem; flex-wrap: wrap; }
  .tag {
    font-size: 11px; padding: .15rem .45rem; border: 1px solid var(--rule-mid);
    border-radius: 2px; color: var(--muted);
  }
  .tag.warn { border-color: var(--brass-lit); color: var(--brass); background: var(--brass-wash); }

  .grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1.5rem; }
  .muted { color: var(--muted); }
  /* A value taken from the book's catalogue record rather than from this
     copy's own printing, which is most of them: a spine names no printing.
     Faint and underdotted, because the difference has to survive being
     scanned down a column at speed and there is no room for a word saying so.
     The dotted rule is the same one browsers put under an abbreviation, which
     is close enough to what this is: a number standing in for one nobody has
     established yet. `title` carries the sentence for anyone who stops. */
  .borrowed {
    color: var(--faint);
    text-decoration: underline dotted var(--rule-mid);
    text-underline-offset: 3px;
    cursor: help;
  }
  .right { text-align: right; }
  a.primary { display: inline-block; text-decoration: none; }
  /* A button that happens to be a link still has to look like a button: the
     bordered box needs a box to be, and the underline the browser draws
     through the label is the tell that it was left as a link. */
  a.ghost { display: inline-block; text-decoration: none; white-space: nowrap; flex-shrink: 0; }

  /* Covers are shown whole, not cropped to fit.
     The mockup could use `object-fit: cover` because its assets were already
     2:3. Real covers are not: cropping them to the box quietly shaves the
     first letter off "The Art of UNIX Programming", and a cover with its title
     clipped is worse than a slightly letterboxed one. The box stays a fixed
     2:3 so the shelf grid still lines up. */
  .cover img { object-fit: contain; }

  /* Nothing on the book page is a hero image.
     `.detail` only gets its 230px first column above 54rem; below that it is a
     single column and the cover stretches to the full width of the page - a
     380px cover blown up past 2x, filling the screen and looking it. The cap
     holds at every width, and matches the desktop column so the two agree.
     The source photograph is a supporting detail too: it is there to show
     where on the shelf this copy was, which it does at a readable size and
     without pushing the history below a second screenful. */
  .detail .cover { max-width: 230px; }

  /* A cover the second source supplied is 128px wide and there is no larger
     one to fetch: Google serves that one size, and asking for more zoom
     returns a magnified corner of the cover rather than a bigger cover. So it
     is capped near its own resolution instead of being upscaled to 230px,
     where it would look like a bad scan of a book we do hold properly. On the
     shelf grid it needs no special handling - it is already smaller there. */
  .detail .cover.lowres { max-width: 148px; }

  /* The edit form used to fold behind an "Edit this place" summary; it is
     open now, a block like the others on the page. */
  .editplace { margin: 0 0 2rem; }

  .ownerbar {
    display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
    margin: 0 0 1.75rem;
  }
  .ownerbar select { max-width: 15rem; padding: .35rem .5rem; font-size: 13px; }
  .ownerbar .ownernote { font-size: 12.5px; }

  .cropview { position: relative; overflow: hidden; background: var(--sunk); }
  .cropview > img { position: absolute; max-width: none; display: block; }

  /* The photograph stays put while you work down the grid. A band pointing at
     a spine is no use once the shelf has scrolled off the top. */
  .reviewsticky {
    position: sticky; top: 56px; z-index: 10;
    background: var(--ground); padding: .5rem 0 .75rem; margin-bottom: .5rem;
  }
  /* Capped so the shelf never takes more than about a third of the screen -
     it is a reference while you work down the grid, not the thing you are
     working on. aspect-ratio on the crop keeps the shape as the height bites. */
  .reviewsticky .reviewphoto { max-width: 720px; max-height: 32vh; margin: 0 auto; }

  /* Where you are. The focus ring alone is easy to lose in a grid of
     twenty-five near-identical cards. */
  .revcard[data-here] {
    outline: 2px solid var(--brass); outline-offset: 2px;
    box-shadow: var(--shadow);
  }
  .revcard[data-here]::before {
    content: ""; position: absolute; left: -1px; top: -1px; bottom: -1px;
    width: 3px; background: var(--brass);
  }
  .revcard { position: relative; }
  .revcard .rstate:empty { display: none; }
  .seen-photo {
    max-width: 30rem;
    border: 1px solid var(--rule); border-radius: 2px; overflow: hidden;
    margin-bottom: 2.5rem;
  }

  /* Pointing at one book inside a photograph.
     `.glow` above is the mockup's mark for this, and it works for exactly what
     the mockup did with it: one region, on one screen. Drawn once per book it
     is twenty-seven overlapping ellipses and the photograph looks like it was
     taken through a dirty lens. So: dim everything except a vertical band, and
     only while you are actually asking about a book.
     A band rather than a box because horizontal is the axis the model is
     trustworthy on - the left-to-right ordering is known - and a shelf-height
     spine fills the frame vertically anyway. Claiming vertical precision we do
     not have is what makes an off-by-a-bit highlight read as broken. */
  .shot { position: relative; }
  .shot > img { display: block; width: 100%; }
  /* The band runs the way the spine does. A shelf of upright books is cut
     left to right; a pile lying on its side is cut top to bottom, because
     that is the direction its spines are stacked in. One vertical bar drawn
     over a horizontal pile points at every book in it at once. */
  .spot {
    position: absolute; inset: 0; pointer-events: none;
    --b0: 0%; --b1: 100%; --feather: 2.5%; --sweep: to right;
    background: rgba(27, 26, 23, .52);
    opacity: 0; transition: opacity .18s ease;
    -webkit-mask-image: linear-gradient(var(--sweep),
      #000 0,
      #000 calc(var(--b0) - var(--feather)),
      transparent var(--b0),
      transparent var(--b1),
      #000 calc(var(--b1) + var(--feather)),
      #000 100%);
    mask-image: linear-gradient(var(--sweep),
      #000 0,
      #000 calc(var(--b0) - var(--feather)),
      transparent var(--b0),
      transparent var(--b1),
      #000 calc(var(--b1) + var(--feather)),
      #000 100%);
  }
  .spot[data-axis="y"] { --sweep: to bottom; }
  .spot[data-on] { opacity: 1; }
  @media (prefers-reduced-motion: reduce) { .spot { transition: none; } }

  /* A spine crop is as tall as the shelf and as wide as one book, so left to
     itself it is a 54x700 ribbon that shreds the card grid. Pin it to the
     cover's proportions and fill: what makes the image-to-image comparison
     work is colour, thickness and the band of title in the middle, and all
     three survive the crop. */
  .revspine { aspect-ratio: 1 / 3.9; position: relative; }
  .revspine img { width: 100%; height: 100%; object-fit: cover; display: block; }

  /* A book lying flat gives a wide crop for a tall slot, and `cover` would
     throw away everything but a sliver of its middle. Rotate it instead: the
     element is laid out with the box's dimensions swapped - as wide as the
     box is tall, as tall as the box is wide - so a quarter turn lands it
     exactly in the slot. Clockwise, because that is the way the spine of a
     shelved book reads. */
  .revspine.flat img {
    position: absolute; top: 50%; left: 50%;
    width: 390%;                 /* the box is 1 : 3.9, so this is its height */
    height: 25.641%;             /* ...and this is its width */
    transform: translate(-50%, -50%) rotate(90deg);
    transform-origin: center center;
  }
  .revcard .revpair > div:last-child { align-self: start; }

  /* The detail sheet. Wider than the fix picker because it is a reading of a
     record, not a one-line search. */
  .sheet.wide { max-width: 46rem; }
  .detail-body .dpair {
    display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.25rem;
  }
  .detail-body .dspine img, .detail-body .cover img {
    width: 100%; height: 100%; object-fit: contain; display: block;
  }
  .detail-body .dspine { max-height: 220px; }
  .detail-body .cover { max-width: 150px; }
  .detail-body .dh {
    font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted); margin: 1.25rem 0 .4rem;
  }
  .detail-body dl { margin: 0; }
  .detail-body .drow {
    display: grid; grid-template-columns: 10rem 1fr; gap: .6rem;
    padding: .28rem 0; border-bottom: 1px solid var(--rule);
    font-size: 13px;
  }
  .detail-body dt { color: var(--muted); }
  .detail-body dd { margin: 0; }
  .detail-acts {
    display: flex; gap: .5rem; margin-top: 1.5rem; flex-wrap: wrap;
  }
  .detail-acts .danger { color: var(--warn); }
  @media (max-width: 34rem) {
    .detail-body .drow { grid-template-columns: 1fr; gap: 0; }
  }

  /* Looking closer. The shelf crop is one band of a frame and a spine that
     cannot be read is exactly why a book goes missing from the list. */
  .zoomwrap {
    overflow: hidden; background: var(--sunk); border-radius: 2px;
    height: min(70vh, 560px); display: flex; align-items: center;
    justify-content: center; touch-action: none; cursor: grab;
  }
  .zoomwrap:active { cursor: grabbing; }
  .zoomwrap img {
    max-width: 100%; max-height: 100%; display: block;
    transform-origin: center center; will-change: transform;
    user-select: none; -webkit-user-drag: none;
  }
  .picker-empty { padding: .5rem .6rem; font-size: 13px; color: var(--muted); }
  /* The photograph is a button now, not a link: it opens the closer look. */
  button.photobtn, .photobtn[role="button"] {
    display: block; width: 100%; padding: 0; border: 0; background: none;
    cursor: zoom-in; text-align: left; font: inherit; color: inherit;
  }

  /* The themes were a bar of chips here. They are a droplist now, sharing
     `.revfilters` with the review screen: chips read well at six labels and
     wrap into three lines at twenty, and a house acquires themes faster than
     it acquires rooms. Nothing else used the bar, so it is gone rather than
     left behind for somebody to wonder about. */

  /* Photographing a jacket, under the cover it replaces. */
  .jacket { margin-top: .6rem; display: flex; flex-direction: column; gap: .35rem; }
  .jacketbtn {
    display: inline-block; text-align: center; cursor: pointer;
    padding: .4rem .6rem; border: 1px solid var(--rule-mid); border-radius: 3px;
    font-size: 12.5px; color: var(--muted); background: var(--surface);
  }
  .jacketbtn:hover { color: var(--ink); border-color: var(--ink); }
  .jacket .linky {
    background: none; border: 0; padding: 0; cursor: pointer;
    font-size: 11.5px; color: var(--faint); text-align: center;
  }
  .jacket .linky:hover { color: var(--ink); }
  .detail .cover.lowres + .jacket, .detail .jacket { max-width: 230px; }
  .jacketurl summary {
    cursor: pointer; font-size: 11.5px; color: var(--faint);
    list-style: none; text-align: center; padding: .15rem 0;
  }
  .jacketurl summary:hover { color: var(--ink); }
  .jacketurl .field { font-size: 12px; padding: .3rem .4rem; min-width: 0; }
  .jacketurl .ghost { font-size: 12px; padding: .3rem .5rem; white-space: nowrap; }

  /* Where a photograph was taken, as the chain you would walk down to find
     it. Indented rather than run together with separators: three levels of
     address are easier to take in as steps than as one long line. */
  .placetrail { list-style: none; margin: 0 0 1.25rem; padding: 0; }
  .placetrail li { padding: .12rem 0; }
  .placetrail li + li { position: relative; }
  .placetrail a { font-family: var(--serif); font-size: 1.05rem; color: var(--ink); }
  .placetrail a:hover { color: var(--brass); }
  .placetrail li:not(:last-child) a { color: var(--muted); font-size: .95rem; }

  .relocate summary {
    cursor: pointer; font-size: 12px; color: var(--faint);
    list-style: none; padding: .2rem 0;
  }
  .relocate summary:hover { color: var(--ink); }

  /* Review, book at a time --------------------------------------------- */
  .revfilters {
    display: flex; gap: .8rem; align-items: center; flex-wrap: wrap;
    margin: 0 0 1.5rem;
  }
  .revfield { display: flex; align-items: center; gap: .35rem; font-size: 12.5px; color: var(--muted); }
  .revfield select {
    font-family: var(--sans); font-size: 12.5px; padding: .3rem .4rem;
    border: 1px solid var(--rule-mid); border-radius: 3px;
    background: var(--surface); color: var(--ink);
  }

  .revlist { display: flex; flex-direction: column; gap: .3rem; }
  .revrow {
    display: grid; grid-template-columns: 3.2rem 1fr auto; gap: .8rem;
    align-items: center; padding: .55rem .7rem;
    border: 1px solid var(--rule); border-radius: 3px; background: var(--surface);
  }
  .revrow:hover { border-color: var(--rule-mid); }
  .revconf {
    font-family: var(--mono); font-size: 11.5px; color: var(--muted);
    text-align: right;
  }
  .revconf.low { color: var(--warn); font-weight: 600; }
  .revwhat { min-width: 0; }
  .revwhat .t { font-family: var(--serif); font-size: 14px; display: block; }
  .revwhat .a { font-size: 11.5px; color: var(--muted); display: block; }
  .revwhat .w {
    font-family: var(--mono); font-size: 10px; color: var(--faint);
    display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .revflags { display: flex; gap: .3rem; flex-wrap: wrap; justify-content: flex-end; }

  .revone { max-width: 62rem; }
  .revone-head {
    display: flex; gap: 1rem; align-items: baseline; margin-bottom: 1rem;
  }
  .revone-nav { margin-left: auto; display: flex; gap: .8rem; font-size: 12.5px; }
  /* The picture is bounded by the height of the screen rather than by its own
     proportions. A shelf is a wide sliver, and left to size itself it pushes
     the three answers below the fold - and this whole screen exists to be
     answered without scrolling. */
  .revone-frame { min-width: 0; height: 34vh; margin-bottom: 1rem; }
  /* Both dimensions are ceilings, and neither is a demand. `height: 100%` with
     `max-width: 100%` is a contradiction the moment a crop is wide enough to
     hit the cap: the height is held, the width is clipped, and the box stops
     being the shape the aspect-ratio asked for. The picture inside is placed
     in percentages *of that box* - that is what makes the detection regions
     line up without arithmetic - so a box of the wrong shape does not crop the
     photograph, it stretches it.
     Measured on Stack 3, whose bottom band is 5.8:1 against a 3:4 frame: the
     box came out 4.87 instead of 5.77 and the picture 16% out of true. An
     ordinary shelf never reached the cap, which is why this waited for a
     photograph of books lying flat to show up. */
  .revone-frame .shot {
    height: auto; width: auto; max-height: 100%; max-width: 100%; margin: 0 auto;
  }
  /* The picture is a button now, so it has to carry the height through rather
     than shrink to its content and take the crop's sizing down with it. The
     margin and width cap are for the one on a place page, which stands alone
     on the page; here the frame is a fixed height and they would overflow it. */
  .revone-frame > .photobtn { height: 100%; margin: 0; max-width: none; }

  /* Where a copy is filed, each step its own link. The dots are spaced to read
     as separators between two targets rather than as part of either. */
  .wheretrail .sep { color: var(--muted); margin: 0 .15rem; }

  /* Cover, then everything known about the book. `min-width: 0` on both this
     and the column beside it because the neighbour list holds long titles set
     `nowrap`: without it their min-content width wins the grid negotiation
     and squeezes the facts into a two-word ribbon. */
  .revone-body {
    display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 1.25rem;
    min-width: 0;
  }
  .revone-left { min-width: 0; }
  /* Picture on one side, what is known on the other, so the three answers
     stay on the screen. Stacked below that width, where they cannot. */
  @media (min-width: 64rem) {
    .revone-main {
      display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
      gap: 1.75rem; align-items: start;
    }
    .revone-frame { height: 40vh; }
    .revone-body { margin-top: 0; }
  }
  .revone-flags { display: flex; gap: .35rem; flex-wrap: wrap; margin-top: .8rem; }
  .revone-acts { display: flex; gap: .6rem; margin: 2rem 0 .5rem; flex-wrap: wrap; }
  .revone-acts .danger { color: var(--warn); }
  .facts dd.mono { font-family: var(--mono); font-size: 12px; }

  @media (max-width: 48rem) {
    .revone-body { grid-template-columns: 1fr; }
    .revone-cover { max-width: 150px; }
  }

  .qfilter { display: flex; gap: .4rem; margin: 1.25rem 0 0; }
  .qfilter .chip {
    display: inline-flex; padding: .3rem .7rem; border-radius: 999px;
    border: 1px solid var(--rule); font-size: 12.5px; color: var(--muted);
    background: var(--surface);
  }
  .qfilter .chip:hover { border-color: var(--rule-mid); color: var(--ink); }
  .qfilter .chip.on {
    background: var(--ink); border-color: var(--ink); color: var(--surface);
  }

  /* The reading queue. */
  .qlist { display: flex; flex-direction: column; gap: .3rem; margin-top: 1.5rem; }
  /* One job and whatever it had to say for itself, as a single thing the
     refresh can leave alone. */
  .qentry { display: flex; flex-direction: column; }
  .qjob {
    display: grid; grid-template-columns: 44px 1fr auto auto 1.2rem; gap: .8rem;
    align-items: center; padding: .5rem .7rem;
    border: 1px solid var(--rule); border-radius: 3px; background: var(--surface);
  }
  .qjob.stalled { border-color: var(--warn); }
  .qjob.failed { border-color: var(--rule-mid); opacity: .7; }
  .qjob .qthumb img { width: 44px; border-radius: 2px; display: block; }
  .qstep .t { font-size: 13.5px; display: block; }
  .qstep .a { font-size: 11px; color: var(--muted); display: block; }
  /* Capped rather than filling the column: across a wide screen the four pips
     stretch into a rule with gaps in it, which is not what it is for. */
  .qstep .upstages { max-width: 11rem; margin-top: .35rem; }
  /* A stalled job holds still. The whole point of this page is that a
     photograph which has stopped looks exactly like one waiting its turn, and
     a pip that goes on pulsing over the word "stopped" puts that back. */
  .qjob.stalled .upstages i.now { animation: none; background: var(--warn); }
  .qstate { font-size: 12px; color: var(--muted); }
  .qjob.stalled .qstate { color: var(--warn); }
  .qwhen { font-size: 11px; color: var(--faint); }
  /* Quiet until wanted. A dismiss control that shouts sits on the page far
     longer than the failure it clears. */
  .qact { display: flex; justify-content: flex-end; }
  .qdismiss {
    background: none; border: 0; padding: 0 .2rem; cursor: pointer;
    font-size: 15px; line-height: 1; color: var(--faint);
  }
  .qdismiss:hover, .qdismiss:focus-visible { color: var(--warn); }
  .qerr {
    font-size: 11.5px; color: var(--warn); padding: .2rem .7rem .5rem;
    margin-top: -.2rem;
  }

  /* Supplying an ISBN, under the search box in the fix picker. */
  /* A button that reads as a sentence rather than a control, for the quieter
     of two actions. Was scoped to .jacket; unscoped now that review wants one
     too, with that block still free to say something different. */
  .linky {
    background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
    color: var(--muted); text-decoration: underline;
    text-underline-offset: 2px; text-decoration-thickness: 1px;
  }
  .linky:hover { color: var(--ink); }

  /* Year and publisher sit together: both are about one printing rather than
     about the book, and neither needs a whole row to itself. */
  .detailsrow { display: flex; gap: .6rem; margin-top: .9rem; }
  .detailsrow > div { flex: 1 1 0; min-width: 0; }
  .detailsrow .field { width: 100%; }
  @media (max-width: 520px) { .detailsrow { flex-direction: column; } }

  .isbnbox { margin-top: 1rem; border-top: 1px solid var(--rule); padding-top: .8rem; }
  .isbnbox .lbl { display: block; margin-bottom: .35rem; }
  .isbnrow { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; }
  .isbnrow .field { flex: 1 1 8rem; min-width: 0; font-family: var(--mono); }
  .isbnshot { cursor: pointer; white-space: nowrap; }

  /* What a book is about, on the book's own page. A theme is the household's
     own word for the subject, so each one links to everything else wearing
     it; the cross beside it takes it off this book and off every other copy
     of the same book, which is why it sits with the chip rather than in a
     dialog somewhere. Styled here and not with the Books page's theme filter
     because they are the same word doing different jobs: this is what one
     book is about, that is how to narrow a listing. */
  .themes {
    display: inline-flex; flex-wrap: wrap; align-items: center; gap: .3rem .4rem;
  }
  .themes .chip {
    display: inline-flex; align-items: baseline;
    padding: .2rem .5rem; border: 1px solid var(--rule); border-radius: 999px;
    font-size: 12px; color: var(--muted); background: var(--surface);
    white-space: nowrap; text-decoration: none;
  }
  .themes .chip:hover { border-color: var(--rule-mid); color: var(--ink); }
  /* Sits just left of the next chip, so it reads as belonging to the one
     before it rather than floating between the two. */
  .themes .linky {
    margin-right: .25rem; text-decoration: none; font-size: 13px; line-height: 1;
  }

  /* Theming a whole shelf at once. Same bar as the owner picker above it,
     because they are the same kind of act: one answer, many books. */
  .ownerbar .themenote { font-size: 12px; }

  /* ---------- review: one book at a time ---------------------------------

     The arrow that says which book. The dimming band already narrows the
     picture down, but on a shelf of thirty spines a band is still four or
     five books wide once the feather is counted, and it does not say which
     end of itself is meant. The arrow rides the edge of the picture and
     points across the shelf, so it reads at a glance and never covers the
     spine it is about - which a marker drawn on top of the book would. */
  .revone-frame { position: relative; }
  .revarrow {
    position: absolute; pointer-events: none; --size: 13px;
    width: 0; height: 0; border: var(--size) solid transparent;
    filter: drop-shadow(0 1px 2px rgba(27, 26, 23, .45));
  }
  /* A shelf of upright books: the arrow sits on the top edge and points down
     the spine's own column. */
  .revarrow[data-axis="x"] {
    left: var(--at); top: 0; transform: translateX(-50%);
    border-top-color: var(--ink); border-bottom-width: 0;
  }
  /* A pile lying flat is stacked top to bottom, so the arrow comes in from
     the left instead. One vertical arrow over a horizontal pile would point
     at every book in it at once. */
  .revarrow[data-axis="y"] {
    top: var(--at); left: 0; transform: translateY(-50%);
    border-left-color: var(--ink); border-right-width: 0;
  }

  /* The other readings from the same picture. Deliberately plain text in
     shelf order rather than a grid of spine crops: the crops are what the
     old shelf review showed, and a crop that is a position off is a
     confident picture of the wrong book. The order and the titles are what
     you need to spot one spine read as two. */
  .revneigh { margin-top: 1rem; }
  .revneigh-list {
    list-style: none; margin: .4rem 0 0; padding: 0;
    border-top: 1px solid var(--rule);
  }
  .revneigh-item { border-bottom: 1px solid var(--rule); font-size: 12.5px; }
  .revneigh-item > a,
  .revneigh-item > span {
    display: grid; grid-template-columns: 1rem 1fr auto; gap: .5rem;
    align-items: baseline; padding: .3rem .1rem;
    color: var(--muted); text-decoration: none;
  }
  .revneigh-item > a:hover { color: var(--ink); background: var(--surface); }
  .revneigh-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .revneigh-state { font-size: 11px; opacity: .75; white-space: nowrap; }
  .revneigh-mark {
    width: .5rem; height: .5rem; border-radius: 999px; justify-self: center;
    border: 1px solid var(--rule-mid);
  }
  /* The one being asked about is the only thing here drawn in full ink. */
  .is-here > span { color: var(--ink); font-weight: 600; }
  .is-here .revneigh-mark { background: var(--ink); border-color: var(--ink); }
  .is-settled .revneigh-mark { background: var(--rule-mid); }
  .is-dropped { text-decoration: line-through; opacity: .55; }

  /* Filters live in the header now, because this page is where the work
     happens and narrowing to one shelf should not mean going back to a list
     to do it. */
  .revone-head { flex-wrap: wrap; gap: .6rem 1rem; }
  .revone-filters { display: flex; gap: .75rem; flex-wrap: wrap; align-items: center; }
  .revone-where {
    display: flex; gap: .75rem; align-items: baseline;
    margin-left: auto; flex-wrap: wrap;
  }
  .revone-place { font-size: 12px; color: var(--muted); }
  .revone-count { font-size: 12px; color: var(--muted); font-family: var(--mono); }

  /* Runs of one book -----------------------------------------------------
     An editor's household owns eleven of a title, not one, and they stand in
     a row. The listing folds them into a single card and puts the number on
     the cover, which is where the eye already is: the cover is what says
     which book, so the count belongs on it rather than in the small print
     under the author.

     Brass, because it is the one accent this palette has and this is the one
     thing on a card that is not a fact about the book itself. Bottom right,
     clear of the placeholder cover's title, which sets from the top. */
  .runmark {
    position: absolute; right: .3rem; bottom: .3rem;
    padding: .1rem .34rem; border-radius: 2px;
    font-family: var(--mono); font-size: 10.5px; line-height: 1.4;
    color: var(--brass); background: var(--brass-wash);
    border: 1px solid var(--brass-lit);
    /* The shadow keeps it legible over a dark cover; the wash alone is not
       enough contrast against every jacket in the house. */
    box-shadow: 0 1px 3px rgba(27,26,23,.25);
  }
  /* A placeholder cover writes the author along its bottom edge, which is
     where the count sits. Left alone the count lands on top of the name and
     clips it - and an unresolved run is the common case here, because the
     books a house owns eleven of are galleys and journal issues that no
     catalogue has. So the name gives the count its corner. */
  .cover.ph:has(.runmark) .ph-a { padding-right: 2.1rem; }

  /* "Right, and the other five like it." Brass rather than a second solid
     button: there is one primary answer on this page and it is about the one
     book, and two black buttons side by side read as two different questions.
     The same brass as the count on the cover, so the run answer and the run
     mark are visibly the same idea. */
  .runall {
    background: var(--brass-wash); color: var(--brass);
    border: 1px solid var(--brass-lit); padding: .55rem 1.05rem;
    border-radius: 3px; font-size: 13px; font-weight: 500;
  }
  .runall:hover { background: var(--brass); color: var(--surface); border-color: var(--brass); }

  /* Said in words in the review list, where there is a line of prose to say
     it in and the size of the job is the point: one row, several books. */
  .runcount {
    font-family: var(--mono); font-size: 10.5px; color: var(--brass);
    border: 1px solid var(--brass-lit); background: var(--brass-wash);
    border-radius: 2px; padding: .05rem .3rem; margin-left: .4rem;
    white-space: nowrap;
  }

  /* The copies of one book on one shelf, on the book's own page. Nothing
     tells them apart but where they stand, so the handle is a number. */
  .kin { display: inline-flex; flex-wrap: wrap; gap: .25rem; margin-left: .4rem; }
  .kin .chip {
    display: inline-flex; min-width: 1.4rem; justify-content: center;
    padding: .1rem .4rem; border: 1px solid var(--rule); border-radius: 999px;
    font-family: var(--mono); font-size: 11px; color: var(--muted);
    background: var(--surface); text-decoration: none;
  }
  .kin a.chip:hover { border-color: var(--rule-mid); color: var(--ink); }
  .kin .chip.on { border-color: var(--brass-lit); color: var(--brass); background: var(--brass-wash); }

  /* For a screen reader only. "×6" reads as nothing spoken aloud, so the
     words go beside it and out of sight rather than into a title attribute,
     which is not reliably announced and cannot be reached by touch. */
  .offscreen {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }

  /* Adding photographs ---------------------------------------------------
     One row per photograph, and the same four columns whether the server
     drew it or the uploader did: a row that reshapes itself the moment it
     finishes is the page moving under somebody's hand. */
  .uprow {
    display: grid; grid-template-columns: 4rem 12rem 1fr 8rem;
    gap: 1rem; align-items: center; padding: .7rem 0;
    border-bottom: 1px solid var(--rule);
  }
  .upthumb { aspect-ratio: 4/3; background: var(--sunk); border-radius: 2px; overflow: hidden; }
  .upthumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
  /* The picture the browser already holds, shown before anything has been
     sent. It is the right picture and it costs nothing; it is only slightly
     the wrong shape, because the derived thumbnail it gives way to was cut
     to fill. Dimmed until then so the swap reads as an arrival. */
  .upthumb img.local { opacity: .55; }
  .upwhere {
    display: block; margin-top: .15rem; font-size: 12px; color: var(--brass);
    text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .upwhere:hover { text-decoration: underline; }

  /* Bytes going up, which is the one part of this with a real percentage. */
  .upbar { height: 4px; background: var(--sunk); border-radius: 1px; margin-top: .4rem; overflow: hidden; }
  .upbar i { display: block; height: 100%; width: 0; background: var(--brass-lit); transition: width .2s ease; }

  /* And then the read, which has no percentage but does have four stages -
     prepare, extract, resolve, localize - each of them real work with a real
     duration. Four segments say which one is running; a spinner over the same
     minute says only that something is happening, which is indistinguishable
     from hung. */
  .upstages { display: flex; gap: 3px; margin-top: .4rem; }
  .upstages i {
    flex: 1; height: 4px; border-radius: 1px; background: var(--sunk);
  }
  .upstages i.done { background: var(--brass); }
  .upstages i.now { background: var(--brass-lit); animation: uppulse 1.1s ease-in-out infinite; }
  .upstages i.failed { background: var(--warn); }
  @keyframes uppulse { 50% { opacity: .35; } }
  @media (prefers-reduced-motion: reduce) {
    .upstages i.now { animation: none; }
    .upbar i { transition: none; }
  }
  .upstage-name { font-size: 11px; color: var(--faint); margin-top: .25rem; }

  @media (max-width: 620px) {
    /* The counts column is the first thing worth losing: it is the only one
       that is still legible as a number in the row above it. */
    .uprow { grid-template-columns: 3rem 1fr; gap: .5rem .8rem; }
    .uprow > :nth-child(3) { grid-column: 2; }
    .uprow > :nth-child(4) { grid-column: 2; text-align: left; }
  }

  /* How many of this book are here ---------------------------------------
     A number you can type over, because a run of identical spines is the one
     thing the reading gets wrong often enough to be worth a field. */
  .countbox { display: block; }
  .countrow { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
  .countfield {
    width: 4.5rem; text-align: right; font-family: var(--mono); padding: .35rem .5rem;
  }
  .countbox .kin { margin-left: 0; margin-top: .5rem; }
  .countnote { display: block; margin-top: .4rem; font-size: 12px; }
  .countnote .linky { font-size: 12px; }

  /* What an ISBN would change ---------------------------------------------
     Offered as a list rather than applied, so the shape has to make "what we
     hold" and "what was found" comparable at a glance. Three columns: the
     field, the old answer, the new one. */
  .proposal {
    margin-top: .7rem; padding: .8rem .9rem; border: 1px solid var(--rule);
    border-radius: 3px; background: var(--sunk);
  }
  .proposal .prop {
    display: grid; grid-template-columns: auto 8rem 1fr auto 1fr;
    gap: .5rem; align-items: baseline; padding: .35rem 0; font-size: 12.5px;
  }
  .proposal .prop + .prop { border-top: 1px solid var(--rule); }
  .prop-what { color: var(--muted); }
  /* The answer being replaced, struck through only when there is one: a dash
     with a line through it reads as a deletion rather than as a gap. */
  .prop-was { color: var(--faint); }
  .prop-arrow { color: var(--faint); }
  .prop-now { color: var(--ink); }
  .proposal .ghost { margin-top: .7rem; }

  /* A checkbox with a sentence beside it, aligned on the first line of it
     rather than on the middle of the block. */
  .check { display: flex; gap: .5rem; align-items: flex-start; font-size: 13px; }
  .check input { margin-top: .2rem; }

  @media (max-width: 620px) {
    .proposal .prop { grid-template-columns: auto 1fr; }
    .prop-arrow { display: none; }
  }

  /* Four filters beside a search field do not fit on a laptop. Wrapped as one
     block they go under the search together; left to wrap one at a time the
     last one lands on a line of its own and reads as a different control. */
  .filterset {
    display: flex; gap: .75rem; flex-wrap: wrap; align-items: end;
  }

  /* Three ways to look at the same books -----------------------------------
     Small, quiet, and to the right of the count: choosing how to look is not
     the reason anybody came to the page. */
  .viewpick { display: inline-flex; gap: 0; margin: 0 0 .9rem; }
  .viewpick-b {
    display: inline-flex; align-items: center; justify-content: center;
    width: 2.1rem; height: 1.9rem; color: var(--muted);
    border: 1px solid var(--rule); background: var(--surface);
    text-decoration: none;
  }
  .viewpick-b + .viewpick-b { border-left: 0; }
  .viewpick-b:first-child { border-radius: 3px 0 0 3px; }
  .viewpick-b:last-child { border-radius: 0 3px 3px 0; }
  .viewpick-b:hover { color: var(--ink); }
  .viewpick-b.on {
    background: var(--ink); border-color: var(--ink); color: var(--surface);
  }

  /* The table. Eight columns do not fit on a phone, so it scrolls inside its
     own box rather than pushing the page sideways - a heading that has
     wandered off the left edge is worse than a scrollbar. */
  .tablewrap { overflow-x: auto; margin-bottom: 1.5rem; }
  .booktable { width: 100%; border-collapse: collapse; font-size: 13px; }
  .booktable th, .booktable td {
    text-align: left; padding: .5rem .7rem; border-bottom: 1px solid var(--rule);
    vertical-align: middle;
  }
  .booktable thead th {
    position: sticky; top: 0; background: var(--ground); z-index: 1;
    border-bottom: 1px solid var(--rule-mid); white-space: nowrap;
    font-size: 11.5px; letter-spacing: .04em; text-transform: uppercase;
    color: var(--muted); font-weight: 500;
  }
  .booktable tbody tr:hover { background: var(--sunk); }
  .booktable .num { text-align: right; font-family: var(--mono); }
  .booktable .nowrap { white-space: nowrap; }
  /* A thumbnail the height of a line of text: enough to recognise a jacket
     you have seen, not enough to make the row about the picture. */
  .booktable .cell-cover { width: 2.6rem; padding-right: 0; }
  .booktable .cell-cover .cover { width: 2.2rem; aspect-ratio: 2/3; box-shadow: none; }
  .booktable .cell-cover .ph-t, .booktable .cell-cover .ph-a,
  .booktable .cell-cover::after { display: none; }
  .booktable .cell-cover .cover.ph { border: 1px solid var(--rule); }
  .booktable .cell-cover .runmark { display: none; }
  .cell-title { font-family: var(--serif); font-size: 14px; color: inherit; text-decoration: none; }
  .cell-title:hover { color: var(--brass); }
  .booktable .cell-where { color: var(--muted); font-size: 12px; }

  .sortlink { color: inherit; text-decoration: none; display: inline-flex; gap: .25rem; }
  .sortlink:hover { color: var(--ink); }
  .sortlink.on { color: var(--brass); }
  .sortmark { font-family: var(--mono); }

  /* Covers only, at twice the card grid's size - 96px becomes 192px - because
     the point of this one is to walk along a wall of jackets. The words are
     over the top and arrive on hover or focus; they are in the document
     either way, so this is a picture wall to the eye and a list to a screen
     reader. */
  .coverwall {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(192px, 1fr));
    gap: 1.6rem 1.3rem; margin-bottom: 1.5rem;
  }
  .wallcard { position: relative; display: block; text-decoration: none; color: inherit; }
  .wallcard .cover { transition: transform .15s ease; }
  .wallcard:hover .cover, .wallcard:focus-within .cover { transform: translateY(-3px); }
  .wallover {
    position: absolute; inset: auto 0 0 0; display: flex; flex-direction: column;
    gap: .1rem; padding: .7rem .6rem .6rem;
    background: linear-gradient(to top, rgba(27,26,23,.92), rgba(27,26,23,.72) 60%, transparent);
    color: #fff; border-radius: 0 0 2px 2px;
    opacity: 0; transition: opacity .15s ease; pointer-events: none;
  }
  .wallcard:hover .wallover, .wallcard:focus .wallover,
  .wallcard:focus-within .wallover { opacity: 1; }
  .wall-t { font-family: var(--serif); font-size: 14px; line-height: 1.25; }
  .wall-a { font-size: 11.5px; opacity: .8; }
  @media (prefers-reduced-motion: reduce) {
    .wallcard .cover, .wallover { transition: none; }
  }

  /* The filter row's own controls -----------------------------------------
     A native select beside that search box was two different ideas about
     what a control looks like sitting next to each other: half the height,
     the system's own chevron, the system's own font. These take the search
     box's ground, border, radius and focus ring, and its height.

     `appearance: none` is what makes that possible and also what removes the
     chevron, so one is drawn back on as a background image. It has to be a
     literal colour rather than `currentColor` - a background image cannot see
     the text colour - which is affordable here only because this stylesheet
     is single-theme by decision. */
  .revfilters select {
    appearance: none; -webkit-appearance: none;
    font-family: var(--sans); font-size: 13px; line-height: 1.2;
    /* Height off the shared property rather than out of padding, so the two
       kinds of control cannot drift apart when one of them is restyled. */
    height: var(--ctl-h); padding: 0 2rem 0 .8rem;
    border: 1px solid var(--rule-mid); border-radius: 3px;
    background-color: var(--surface); color: var(--ink);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%237A756B' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right .6rem center;
    /* Capped so one very long shelf name cannot push the row onto two lines.
       The open list still draws itself at its own width. */
    max-width: 11rem; min-width: 0; text-overflow: ellipsis;
  }
  .revfilters select:focus {
    border-color: var(--brass); outline: none;
    box-shadow: 0 0 0 3px var(--brass-wash);
  }
  .revfilters select:hover { border-color: var(--rule-mid); }

  /* The search box gives way in *width* rather than the filters wrapping. It
     is the one control here that reads fine at any width, and four controls
     on two lines costs more than a truncated placeholder does. Its height and
     its serif are untouched: it is the one the others are matching. */
  .revfilters { --ctl-h: 3.25rem; align-items: center; }
  /* In the row, the toggle is another control and stands as tall as the rest.
     On a page with no filters (a shelf, Unplaced) it keeps its own smaller
     size and its margin, because there is nothing there to line up with. */
  .revfilters .viewpick { margin: 0; }
  .revfilters .viewpick-b { height: var(--ctl-h); width: 2.6rem; }
  .revfilters .searchwrap { flex: 1 1 9rem; min-width: 0; }
  .revfilters .searchwrap input { height: var(--ctl-h); padding-block: 0; }
  .revfilters .revfield { min-width: 0; }

/* Books the camera could not read.

   A list, not the card grid the shelf uses. A card is mostly a cover and a
   title, and these have neither - drawn as cards they are a row of grey
   rectangles that look like something failed to load. What is worth showing
   is the position on the shelf, whatever fragment came off the spine, and the
   two answers; the book itself is found by hovering, which lights it in the
   photograph above. */
ul.unnamed { list-style: none; margin: 1rem 0 0; padding: 0; }
ul.unnamed li {
  display: flex; align-items: center; gap: .9rem;
  padding: .55rem .7rem; border-radius: 3px;
  border-bottom: 1px solid var(--rule, rgba(0,0,0,.08));
}
ul.unnamed li:last-child { border-bottom: 0; }
ul.unnamed li:hover, ul.unnamed li:focus-within { background: rgba(0,0,0,.03); }
ul.unnamed li:focus { outline: 2px solid var(--accent, #7a6a58); outline-offset: -2px; }
ul.unnamed .pos { flex: 0 0 2rem; font-size: 12px; color: var(--muted); text-align: right; }
ul.unnamed .said { flex: 1 1 auto; font-size: 13px; min-width: 0; }
ul.unnamed .said .mono { font-size: 12px; }
ul.unnamed .acts { flex: 0 0 auto; display: flex; gap: .4rem; flex-wrap: wrap; }
ul.unnamed .acts .ghost { padding: .25rem .6rem; font-size: 12px; }
@media (max-width: 34rem) {
  ul.unnamed li { flex-wrap: wrap; }
  ul.unnamed .said { flex-basis: 100%; order: 3; }
}

/* The second level of the nav.

   A bar rather than a dropdown, because these are queues you work between:
   finishing Review and hopping to Unidentified should cost one click, not
   open-menu-then-click, and the counts on the siblings are half the reason to
   look. Lighter than the top bar on purpose - it is where you are inside a
   section, not which section you are in. */
nav.subnav {
  display: flex; gap: .15rem; flex-wrap: wrap;
  margin: 0 0 1.6rem; padding-bottom: .5rem;
  border-bottom: 1px solid var(--rule-mid);
}
/* Sat under the bar, so it does not want the full page inset above it - the
   two rows read as one piece of chrome rather than as a row that has drifted
   down into the page. */
main.hassub { padding-top: .9rem; }
nav.subnav a {
  padding: .3rem .6rem; border-radius: 3px; text-decoration: none;
  color: var(--muted); font-size: 13px; white-space: nowrap;
}
nav.subnav a:hover { color: var(--ink); background: var(--sunk); }
nav.subnav a[aria-current="page"] { color: var(--ink); font-weight: 600; background: var(--sunk); }
nav.subnav .num { font-size: 11.5px; opacity: .75; margin-left: .15rem; }

/* A heading inside the phone panel, where both levels are shown at once. */
.navpanel .navgroup {
  display: block; margin: .5rem .6rem .15rem;
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); opacity: .7;
}
.navpanel .navgroup ~ a { padding-left: 1.2rem; }

}

/* Questions about a whole photograph.

   The picture is the question here - "is this the whole shelf" is answered by
   looking at it - so it gets real width rather than a thumbnail beside a line
   of text. */
.photoqs { display: flex; flex-direction: column; gap: 2.2rem; }
.photoq {
  display: grid; grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
  gap: 1.6rem; align-items: start;
}
/* A photograph whose derived sizes have not been made yet still has a question
   worth answering, and an empty column beside it reads as a broken page. */
.photoq:not(:has(.pq-shot img)) { grid-template-columns: 1fr; }
.photoq:not(:has(.pq-shot img)) .pq-shot { display: none; }
.photoq .pq-shot img {
  width: 100%; border-radius: 3px; display: block;
  border: 1px solid var(--rule-mid);
}
.photoq .pq-item { padding: .2rem 0 1rem; }
.photoq .pq-item + .pq-item {
  border-top: 1px solid var(--rule, rgba(0,0,0,.08)); padding-top: 1rem;
}
.photoq .qt { font-weight: 600; font-size: 15px; }
.photoq .qd { color: var(--muted); font-size: 13px; margin-top: .3rem; }
.photoq .qa { display: flex; gap: .5rem; margin-top: .9rem; flex-wrap: wrap; }
/* Scoped so the shared `.primary` keeps its own colours - overriding only the
   background left white text on a white button. */
.photoq .qa button:not(.primary) {
  padding: .4rem .8rem; font-size: 13px; border-radius: 3px;
  border: 1px solid var(--rule-mid); background: var(--surface);
  color: var(--ink-soft); cursor: pointer;
}
.photoq .qa button:not(.primary):hover { border-color: var(--brass); color: var(--brass); }
.photoq .qa button.primary { padding: .4rem .8rem; font-size: 13px; cursor: pointer; }
.qfilter .chip .num { opacity: .7; margin-left: .2rem; font-size: 11.5px; }
/* The tabs sit directly under the Manage row, so they get room above them but
   not the full page inset - three rows of chrome stacked with equal gaps reads
   as three unrelated bars. */
.revtabs { margin: 0 0 1.6rem; }
@media (max-width: 46rem) {
  .photoq { grid-template-columns: 1fr; }
  .photoq .pq-shot img { max-height: 40vh; object-fit: contain; }
}

/* ---------- intake: the Photos screen and the wizard header ------------- */
.segmented { display: inline-flex; gap: 0; border: 1px solid var(--rule); border-radius: 4px; overflow: hidden; }
.segmented a, .segmented button { padding: .25rem .7rem; font-size: 12px; letter-spacing: .04em; color: var(--muted); text-decoration: none; text-transform: none; }
.segmented a.on, .segmented button.on { background: var(--ink); color: var(--surface); }
.segmented + .segmented { margin-left: .8rem; }
.photolist { display: flex; flex-direction: column; }
.photorow { display: grid; grid-template-columns: 1.5rem 4.5rem 1fr auto auto; gap: .6rem 1rem; align-items: center;
  padding: .7rem 0; border-bottom: 1px solid var(--rule); }
.photorow .pick input { margin: 0; }
.photorow .upthumb { width: 4.5rem; height: 3.2rem; background: var(--sunk); border-radius: 3px; overflow: hidden; display: block; }
.photorow .upthumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photorow .statusline { display: flex; gap: .55rem; align-items: baseline; text-decoration: none; color: var(--ink); }
.photorow .statusline .num:first-child { font-family: var(--mono); font-size: 11px; color: var(--faint); width: 1rem; }
.photorow .status { font-family: var(--serif); font-size: 1.05rem; }
.photorow .muted, .wizhead .muted { color: var(--muted); font-size: 12px; }
.photorow .where { display: flex; gap: .6rem; align-items: baseline; font-size: 12px; color: var(--muted); margin-top: .2rem; }
.photorow .where .mono { font-family: var(--mono); font-size: 11.5px; }
.strip { display: inline-flex; gap: 2px; }
.strip .pip { display: inline-block; min-width: 1.4rem; padding: 0 .3rem; text-align: center; font-family: var(--mono); font-size: 11px;
  border-radius: 2px; background: var(--sunk); color: var(--muted); }
.strip .pip.reading { border-bottom: 2px solid var(--faint); }
.strip .pip.text { border-bottom: 2px solid var(--brass-lit); }
.strip .pip.matching { border-bottom: 2px solid var(--brass); }
.strip .pip.matches { border-bottom: 2px solid var(--brass); }
.strip .pip.accepted { background: var(--ink); color: var(--surface); }
.marker { font-size: 12px; min-width: 5rem; text-align: right; }
.marker .busy { color: var(--muted); }
.marker .stuck { color: var(--warn); }
.photorow.uploading .upbar { margin-top: .3rem; }
.bulkbar[hidden] { display: none; }
.bulkbar { display: flex; flex-wrap: wrap; gap: .8rem; align-items: flex-end; padding: .8rem; margin-bottom: 1rem;
  background: var(--sunk); border-radius: 4px; }
.bulkbar label { display: flex; flex-direction: column; gap: .2rem; font-size: 12px; }
.bulkbar .lbl { align-self: center; font-size: 12.5px; color: var(--muted); }
.wizhead { margin-bottom: 1.5rem; }
.stepbar { display: flex; gap: 1.2rem; border-bottom: 1px solid var(--rule); margin-bottom: .6rem; align-items: baseline; }
.stepbar .where { margin-left: auto; font-size: 13px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 50%; }
.stepbar .where a { color: inherit; }
.stepbar a { padding: .5rem 0; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); text-decoration: none; }
.stepbar a.on { color: var(--ink); border-bottom: 2px solid var(--ink); }
.wizhead .statusline { display: flex; gap: .8rem; align-items: baseline; }
.wizhead .statusline .status { font-family: var(--serif); font-size: 1.1rem; }
.lockbar { margin-top: .6rem; padding: .5rem .8rem; background: var(--brass-wash); border: 1px solid var(--brass-lit); border-radius: 4px; font-size: 13px;
  display: flex; gap: 1rem; align-items: center; }
.wiznav { margin-top: 1.5rem; display: flex; justify-content: flex-end; }
@media (max-width: 640px) {
  .photorow { grid-template-columns: 1.5rem 3.5rem 1fr; }
  .photorow .strip, .photorow .marker { grid-column: 3; text-align: left; }
}

/* ---------- intake: the geometry step ------------------------------------ */
.geomtools { display: flex; gap: .4rem; align-items: center; margin-bottom: .6rem; flex-wrap: wrap; }
.geomtools .tool { font-size: 12px; padding: .3rem .7rem; border: 1px solid var(--rule); background: var(--surface); border-radius: 3px; cursor: pointer; }
.geomtools .tool.on { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.geomtools .spacer { flex: 1; }
.geomtools .ghost { font-size: 12px; padding: .3rem .7rem; }
.geomtools .rerun { font-size: 12px; position: relative; }
.geomtools .rerun summary { cursor: pointer; padding: .3rem .5rem; color: var(--muted); }
.geomtools .rerun[open] { background: var(--sunk); border-radius: 3px; }
.geomtools .rerun button { font-size: 12px; margin: .2rem; }
.geomstage { position: relative; background: var(--sunk); border-radius: 4px; overflow: hidden; touch-action: none; user-select: none; }
.geomstage img { display: block; width: 100%; height: auto; }
.geomstage svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.geomstage svg polygon { vector-effect: non-scaling-stroke; }
/* A transparent fill rather than none: an unfilled SVG polygon is only
   hit-testable on its stroke, and a region's is a thin dashed line. Items
   are drawn above regions, so a click on a book still reaches the book. */
/* Regions are blue and books are yellow, so a shelf's outline and the
   books' are told apart at a glance rather than by dash pattern alone. */
.geomstage .region { fill: rgba(0,0,0,.001); stroke: rgba(96,168,255,.9); stroke-width: 1.5; stroke-dasharray: 6 4; cursor: pointer; }
.geomstage .region.selected { stroke: #a8d0ff; stroke-width: 2.4; stroke-dasharray: none; }
.geomstage .region.person { stroke: #3d8ef0; }
.geomstage .item { fill: rgba(255,255,255,.04); stroke: rgba(255,230,120,.85); stroke-width: 1.2; cursor: pointer; }
.geomstage .item.person { stroke: var(--brass-lit); }
.geomstage .item.gone { stroke: #e66; stroke-dasharray: 3 3; }
.geomstage .item.pending { stroke-dasharray: 2 2; }
.geomstage .item.unread { stroke: rgba(255,255,255,.55); stroke-dasharray: 5 3; }
.geomstage .item.accepted { stroke: #8fd18f; }
.geomstage .item.lit { fill: rgba(255,230,120,.18); stroke-width: 2.2; }
.geomstage .item.selected { stroke: #fff; stroke-width: 2.4; }
.geomstage .shade { fill: rgba(0,0,0,.72); pointer-events: none; }
.geomstage .dim { fill: rgba(0,0,0,.35); pointer-events: none; }
.geomstage .handle { fill: #fff; stroke: var(--ink); stroke-width: 1; cursor: grab; vector-effect: non-scaling-stroke; }
.geomstage line.side { stroke: rgba(255,255,255,.001); stroke-width: 12px; vector-effect: non-scaling-stroke; }
.geomstage .side.ns { cursor: ns-resize; }
.geomstage .side.ew { cursor: ew-resize; }
.geomstage polygon.selected { cursor: move; }
.geomstage .draft { fill: rgba(255,255,255,.1); stroke: #fff; stroke-width: 1.5; stroke-dasharray: 4 3; pointer-events: none; }
.geomstage.tool-rect, .geomstage.tool-corners, .geomstage.tool-rescan { cursor: crosshair; }
.geomfoot { display: flex; gap: 1rem; align-items: center; margin-top: .6rem; font-size: 12px; color: var(--muted); }
.geomfoot .slider { display: flex; gap: .6rem; align-items: center; flex: 1; }
.geomfoot input[type=range] { flex: 1; }
.geomfoot .spacer { flex: 1; }
.geomfoot .ghost { font-size: 12px; padding: .2rem .6rem; }
.geomfoot .ghost.on { background: var(--ink); color: var(--surface); }
@media (min-width: 800px) { .geomfoot .slider { max-width: 24rem; } }

/* ---------- intake: the text and match grids ----------------------------- */
.gridbar { display: flex; gap: .8rem; align-items: center; margin: .6rem 0; font-size: 12px; }
.gridbar .spacer { flex: 1; }
.gridrows { display: flex; flex-direction: column; }
.gridrow { display: grid; grid-template-columns: 4rem 1fr auto; gap: .6rem 1rem; padding: .7rem 0; border-bottom: 1px solid var(--rule); align-items: start; }
/* Important: the text page's layouts set display on .gridrow with more
   specificity, and a filtered-out row must stay out. */
.gridrow[hidden] { display: none !important; }
/* All | Accepted | Remaining: the sort's segmented control, as buttons. */
.segmented button { border: 0; background: transparent; font: inherit; cursor: pointer; line-height: inherit; }
.gridrow .itemimg { width: 4rem; height: 7rem; background: var(--sunk); border-radius: 3px; overflow: hidden; cursor: zoom-in; }
.gridrow .itemimg img { width: 100%; height: 100%; object-fit: contain; display: block; }
.gridrow .lines { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-bottom: .4rem; white-space: pre-wrap; }
.gridrow .lines .orient { color: var(--faint); margin-left: .3rem; }
.gridrow .fields { display: grid; grid-template-columns: 2fr 1.4fr 1fr; gap: .5rem; }
.gridrow .fields label { display: flex; flex-direction: column; gap: .15rem; font-size: 11px; color: var(--muted); }
.gridrow .fields input { font: inherit; font-size: 13px; padding: .3rem .4rem; border: 1px solid var(--rule); border-radius: 3px; background: var(--surface); }
/* The text step: big enough to scan. A spine lies flat across the page
   with its text upright, the verbatim line under it and the three fields
   under that; a cover sits at the left with both lines beside it. */
[data-grid="text"] .gridrow .lines { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4; margin: .25rem 0 .4rem; }
[data-grid="text"] .gridrow .lines .sep { color: var(--faint); }
[data-grid="text"] .gridrow .fields label { font-size: 12px; }
[data-grid="text"] .gridrow .fields input { font-size: 18px; padding: .35rem .5rem; }
[data-grid="text"] .gridrow.lay-cover { grid-template-columns: 10rem 1fr auto; }
[data-grid="text"] .gridrow.lay-cover .itemimg { width: 10rem; height: 10rem; }
[data-grid="text"] .gridrow.lay-cover .itemimg img { transform: rotate(var(--rot, 0deg)); }
[data-grid="text"] .gridrow.lay-spine { display: block; }
[data-grid="text"] .gridrow.lay-spine .spineimg { position: relative; width: 100%; aspect-ratio: var(--h) / var(--w); background: var(--sunk); border-radius: 3px; overflow: hidden; cursor: zoom-in; }
[data-grid="text"] .gridrow.lay-spine .spineimg img { position: absolute; top: 50%; left: 50%; width: calc(100% * var(--w) / var(--h)); height: auto; display: block; transform: translate(-50%, -50%) rotate(var(--rot)); }
[data-grid="text"] .gridrow.lay-spine .ord { display: inline-block; margin-top: .3rem; }
[data-grid="text"] .gridrow.lay-spine .flip { position: absolute; right: .4rem; top: .4rem; font-size: 16px; line-height: 1; padding: .2rem .45rem; border-radius: 3px; border: 1px solid var(--rule); background: var(--surface); color: var(--muted); cursor: pointer; opacity: .75; }
[data-grid="text"] .gridrow.lay-spine .flip:hover { opacity: 1; color: var(--ink); }
[data-grid="text"] .gridrow.lay-spine .fieldrow { display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: end; }
[data-grid="text"] .gridrow.lay-spine .act { flex-direction: row; align-items: center; min-width: 0; }
.gridrow .fields input.edited { border-color: var(--brass-lit); background: var(--brass-wash); }
.gridrow .act { display: flex; flex-direction: column; gap: .4rem; align-items: flex-end; min-width: 7rem; }
.gridrow .act .done { color: var(--good); font-size: 12px; }
.gridrow .spin { color: var(--muted); font-size: 12px; }
button.busy .spin { display: inline-block; animation: carnadh-spin 1.2s linear infinite; }
button.busy { opacity: .75; cursor: progress; }
.gridrow.accepted { background: linear-gradient(90deg, transparent, rgba(76,107,82,.05)); }
.gridrow .ord { font-family: var(--mono); font-size: 10.5px; color: var(--faint); }
.match { display: grid; grid-template-columns: 7rem 1fr auto; gap: .5rem 1rem; align-items: center; }
.match .cover { width: 7rem; height: 10rem; background: var(--sunk); border-radius: 2px; overflow: hidden; }
.match .cover img { width: 100%; height: 100%; object-fit: contain; display: block; }
.match .facts { font-size: 16px; }
.match .facts .t { font-family: var(--serif); font-size: 1.7rem; line-height: 1.15; margin-bottom: .25rem; }
.alts .match { grid-template-columns: 3.5rem 1fr auto; }
.alts .match .cover { width: 3.5rem; height: 5rem; }
.alts .match .facts { font-size: 13px; }
.alts .match .facts .t { font-size: 1.05rem; margin-bottom: 0; }
.match .cover.drop { display: grid; place-items: center; border: 1.5px dashed var(--rule); background: transparent; color: var(--faint); font-size: 11.5px; text-align: center; padding: .4rem; }
.match .cover.drop.over { border-color: var(--brass-lit); background: var(--brass-wash); color: var(--ink); }
.match .cover.has.over { outline: 2px dashed var(--brass-lit); outline-offset: -2px; opacity: .6; }
.match .cover.drop .find { font-size: 11px; color: var(--brass); text-decoration: none; background: none; border: 0; padding: 0; cursor: pointer; margin-top: .3rem; }
.match .cover.drop .find:hover { text-decoration: underline; }
/* On a cover already there: the same finders, over its foot, on hover. */
.match .cover.has { position: relative; }
.match .cover.has .findbar { position: absolute; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; gap: .15rem; align-items: center;
  padding: .3rem .2rem; background: rgba(20, 16, 10, .72); opacity: 0; transition: opacity .15s; }
.match .cover.has:hover .findbar, .match .cover.has:focus-within .findbar { opacity: 1; }
.match .cover.has .find { font-size: 11px; color: #fff; text-decoration: none; background: none; border: 0; padding: 0; cursor: pointer; }
.match .cover.has .find:hover { text-decoration: underline; }
.alts .match .cover.has .findbar { display: none; }
.coverpick { border: 1px solid var(--rule); border-radius: 6px; padding: 1rem; max-width: 52rem; background: var(--surface); }
.coverpick::backdrop { background: rgba(0,0,0,.45); }
.coverpick .pickgrid { display: flex; flex-wrap: wrap; gap: .6rem; min-height: 6rem; margin-bottom: .8rem; }
.coverpick .pick { border: 1px solid var(--rule); border-radius: 3px; padding: 0; background: var(--sunk); cursor: pointer; width: 8rem; height: 11rem; overflow: hidden; }
.coverpick .pick img { width: 100%; height: 100%; object-fit: contain; display: block; }
.coverpick .pick:hover { border-color: var(--brass-lit); }
.coverpick .pick:disabled { opacity: .5; }
/* The matches step: the spine is out of the way until asked for. */
[data-grid="matches"] .gridrow { grid-template-columns: 5rem 1fr auto; }
[data-grid="matches"] .spinecol { display: flex; flex-direction: column; gap: .3rem; align-items: flex-start; }
[data-grid="matches"] .spinecol .itemimg { width: 4rem; height: 7rem; background: var(--sunk); border-radius: 3px; overflow: hidden; cursor: zoom-in; }
[data-grid="matches"] .spinecol .itemimg[hidden] { display: none; }
[data-grid="matches"] .spinecol .small { font-size: 11px; padding: .2rem .45rem; }
.match .facts .src-openlibrary { color: #3a5f8a; }
.match .facts .src-google { color: #7a4a8a; }
.match .facts .src-isbn { color: var(--good); }
.match .facts .src-library { color: var(--brass); }
.match .meter { height: 4px; width: 6rem; background: var(--sunk); border-radius: 2px; overflow: hidden; }
.match .meter i { display: block; height: 100%; background: var(--brass-lit); }
.match .score { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.alts { margin-top: .4rem; }
.alts summary { font-size: 12px; color: var(--muted); cursor: pointer; }
.alts .match { opacity: .7; padding: .3rem 0 .3rem 1rem; cursor: pointer; }
.alts .match:hover { opacity: 1; background: var(--sunk); }
.notice { font-size: 12px; margin-top: .4rem; }
.notice.here { color: var(--good); }
.notice.elsewhere { color: var(--warn); }
.notice.stale { color: var(--warn); }
.isbnrow { display: flex; gap: .3rem; margin-top: .4rem; }
.isbnrow input { font: inherit; font-size: 12px; width: 9rem; padding: .2rem .4rem; border: 1px solid var(--rule); border-radius: 3px; }
.lightbox { border: none; padding: .5rem; background: var(--surface); box-shadow: var(--shadow); max-width: 90vw; }
.lightbox img { max-height: 85vh; max-width: 85vw; display: block; }
.lightbox::backdrop { background: rgba(0,0,0,.6); }
@media (max-width: 640px) {
  .gridrow { grid-template-columns: 3rem 1fr; }
  .gridrow .act { grid-column: 2; flex-direction: row; }
  .gridrow .fields { grid-template-columns: 1fr; }
}

/* ---------- intake: disagreements and the sweep -------------------------- */
.disagree { margin-top: .6rem; font-size: 12.5px; border: 1px solid var(--brass-lit); background: var(--brass-wash); border-radius: 4px; padding: .3rem .6rem; }
.disagree summary { cursor: pointer; color: var(--brass); }
.disrow { display: grid; grid-template-columns: 2.5rem 1fr auto; gap: .6rem; padding: .5rem 0; border-top: 1px solid var(--rule); align-items: center; }
.disrow img { width: 2.5rem; height: 4rem; object-fit: contain; background: var(--sunk); }
.disrow .act { display: flex; gap: .4rem; }
.disrow .ghost { font-size: 12px; }
.sweep { margin-top: .8rem; padding: .6rem .8rem; background: var(--sunk); border-radius: 4px; font-size: 13px; display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; }
.sweep[hidden] { display: none; }

/* ---------- intake: the overlay on the place and book pages ------------- */
.polyview { position: relative; overflow: hidden; border-radius: 4px; background: var(--sunk); aspect-ratio: var(--fw) / var(--fh); }
.polyview .frame { position: absolute; inset: 0; }
.polyview .frame img { display: block; position: absolute; inset: 0; width: 100%; height: 100%; }
.polyview .polys { position: absolute; inset: 0; width: 100%; height: 100%; }
/* At the crop: the box is the crop's shape, and the frame slides under it. */
.polyview.cropped { aspect-ratio: calc(var(--fw) * var(--cw)) / calc(var(--fh) * var(--ch)); }
.polyview.cropped .frame { width: calc(100% / var(--cw)); height: calc(100% / var(--ch)); inset: auto;
  left: calc(-1 * var(--cx) / var(--cw) * 100%); top: calc(-1 * var(--cy) / var(--ch) * 100%); }
.polyview .whole { position: absolute; top: .4rem; right: .4rem; z-index: 2; }
.polyview .poly { fill: transparent; stroke: transparent; vector-effect: non-scaling-stroke; transition: fill .12s ease, stroke .12s ease; }
.polyview .poly.lit { fill: rgba(255,230,120,.28); stroke: rgba(255,230,120,.95); stroke-width: 2; }
.polyview .poly.dimmed { fill: rgba(0,0,0,.35); stroke: rgba(255,255,255,.4); stroke-dasharray: 4 3; stroke-width: 1; }
.polyview .poly.dimmed.lit { fill: rgba(0,0,0,.2); stroke: rgba(255,230,120,.95); }
.seefilter { display: flex; gap: 1rem; align-items: baseline; margin: 1rem 0 1.25rem; font-size: 12.5px; }
.polycard { display: flex; flex-direction: column; }
.polycard.dimmed .bookcard { opacity: .55; }
.polycard .note { font-size: 11.5px; color: var(--muted); margin-top: .2rem; }

/* The geometry step while the passes run and nothing is drawn yet. */
.geomview .working {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: .5rem;
  background: rgba(255, 255, 255, .55); font-size: 1.15rem; pointer-events: auto; cursor: progress; z-index: 2;
}
.geomview .working[hidden] { display: none; }
.geomview .working[data-needs-details] { pointer-events: auto; flex-direction: column; }
.geomview .working .busy { display: inline-block; animation: carnadh-spin 1.2s linear infinite; }
@keyframes carnadh-spin { to { transform: rotate(360deg); } }

.ghost.danger { color: var(--warn); border-color: var(--rule); }
.ghost.danger[data-armed] { color: #fff; background: var(--warn); border-color: var(--warn); }
.wiznav .spacer { flex: 1; }

/* The whole picture on screen at once, so both crop edges are in reach:
   as wide as the column or as tall as the window allows, whichever binds. */
.fullview { margin-top: 1.5rem; background: var(--sunk); border-radius: 4px; overflow: hidden; cursor: zoom-in; position: relative; }
/* Sized from the photograph's own dimensions (--ar) before a byte arrives,
   so the crop frame has a box to follow from the first paint. */
.fullview img { display: block; margin: 0 auto; width: min(100%, calc((100vh - 7rem) * var(--ar, 1.5))); height: auto; }
.cropbar { display: flex; align-items: center; gap: .8rem; margin-top: 1.5rem; font-size: 12.5px; }
.cropbar + .fullview { margin-top: .5rem; }
/* The space the picture will take, while prepare is still making it. */
.fullview .preparing {
  display: flex; align-items: center; justify-content: center; gap: .6rem;
  min-height: 14rem; color: var(--muted); font-size: 15px; cursor: default;
}
.fullview .preparing .busy { display: inline-block; animation: carnadh-spin 1.2s linear infinite; }
.fullview[data-picture-pending] { cursor: default; }
.cropper[hidden] { display: none; }
/* The crop: a box on the picture, everything outside it shaded, an edge
   strip to drag on each side. The box never takes the click that zooms. */
/* Pinned to the picture's own box by intake.js (the picture is centred and
   may be narrower than the well). */
.cropper { position: absolute; inset: 0; pointer-events: none; }
.cropbox { position: absolute; box-shadow: 0 0 0 100vmax rgba(0,0,0,.55); outline: 1px solid rgba(255,255,255,.8); }
.cropedge { position: absolute; pointer-events: auto; touch-action: none; }
.cropedge.top, .cropedge.bottom { left: 0; right: 0; height: 18px; cursor: ns-resize; }
.cropedge.left, .cropedge.right { top: 0; bottom: 0; width: 18px; cursor: ew-resize; }
.cropedge.top { top: -9px; } .cropedge.bottom { bottom: -9px; }
.cropedge.left { left: -9px; } .cropedge.right { right: -9px; }
.cropedge::after { content: ""; position: absolute; background: #fff; border: 1px solid var(--ink); border-radius: 3px; }
.cropedge.top::after, .cropedge.bottom::after { left: 50%; top: 4px; width: 44px; height: 10px; transform: translateX(-50%); }
.cropedge.left::after, .cropedge.right::after { top: 50%; left: 4px; width: 10px; height: 44px; transform: translateY(-50%); }

/* The lifecycle strip: per stage, what the machine is doing and how far the
   person has got. One vocabulary everywhere: working, ready, done, attention. */
.life { display: inline-flex; gap: .9rem; align-items: center; margin-left: .8rem; font-size: 12.5px; }
.life .stage { display: inline-flex; align-items: center; gap: .3rem; color: var(--muted); white-space: nowrap; }
.life .stage i { font-style: normal; }
.life .stage small { font-size: 11px; color: var(--faint); }
.life .s-working { color: var(--ink); }
.life .s-working i { display: inline-block; animation: carnadh-spin 1.2s linear infinite; }
.life .s-done { color: var(--good); }
.life .s-failed { color: var(--warn); }
.life .stage .retry { font: inherit; font-size: 11px; padding: .1rem .4rem; margin-left: .2rem; border: 1px solid var(--warn); border-radius: 3px; background: var(--surface); color: var(--warn); cursor: pointer; }
.upstream { display: flex; align-items: center; gap: .5rem; padding: .5rem .8rem; margin-bottom: .6rem; border: 1px solid var(--brass-lit); background: var(--brass-wash); border-radius: 4px; font-size: 13px; }
.upstream .busy { display: inline-block; animation: carnadh-spin 1.2s linear infinite; }
.upstream[hidden] { display: none; }
/* Rows: greyed while the machine works, plain when ready, tinted when done,
   amber-edged when only the person can settle it. */
.gridrow.working { opacity: .6; }
.gridrow.working .why { font-size: 14px; color: var(--muted); margin-top: .4rem; }
.gridrow.working .why .spin { display: inline-block; animation: carnadh-spin 1.2s linear infinite; }
.gridrow.working .why .what { color: var(--ink); }
.gridrow.w-attention { border-left: 3px solid var(--warn); padding-left: .6rem; }
.gridrow.w-done { background: linear-gradient(90deg, transparent, rgba(76,107,82,.05)); }
.geomstage .item.w-working { opacity: .55; stroke-dasharray: 2 2; }
.geomstage .item.w-attention { stroke: var(--warn); }

.gridrow .readtext { font-size: 12.5px; color: var(--muted); margin: .2rem 0 .3rem; }

/* The geometry step, pinned: header, tools and the slider above, the
   picture filling the rest of the screen, no page scroll. The frame is
   sized by geometry.js to fit the well, so the overlay and the image
   agree to the pixel. */
main#app:has(.wizbody.geom) { display: flex; flex-direction: column; overflow: hidden; max-width: none; padding: 1rem 1.5rem .6rem; }
main#app:has(.wizbody.geom) .wizhead { margin-bottom: .6rem; flex: none; }
.wizbody.geom { flex: 1; min-height: 0; display: flex; flex-direction: column; margin-bottom: 0; }
.wizbody.geom .geomtools, .wizbody.geom .geomfoot { flex: none; }
.wizbody.geom .geomfoot { margin: 0 0 .5rem; }
.wizbody.geom .geomtools .gap { width: .6rem; }
.wizbody.geom .geomtools .next { font-size: 12px; padding: .3rem .8rem; }
.geomwell { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; background: var(--sunk); border-radius: 4px; }
.geomwell .geomview { flex: none; position: relative; overflow: auto; border-radius: 4px; }
.geomwell .geompane { position: relative; overflow: hidden; }
.geomwell .geomstage { position: absolute; border-radius: 0; }
.geomfoot .zoom { display: inline-flex; gap: .25rem; }
.geomfoot .zoom .ghost { min-width: 1.9rem; padding: .2rem .45rem; }
.geomfoot .zoom .ghost:disabled { opacity: .45; cursor: default; }
.geomwell .geomstage img { width: 100%; height: 100%; }

  /* ---------- places outline ------------------------------------------
     The whole house on one page: rooms fold open on areas, areas on shelves.
     A <details> per branch, so the folding works without a script; the
     summary is a row like any other, with the marker replaced by a caret
     that turns. */
  .outline details > summary { list-style: none; cursor: pointer; }
  .outline details > summary::-webkit-details-marker { display: none; }
  .outline .caret {
    width: 10px; flex-shrink: 0; color: var(--faint); font-size: 11px; line-height: 1;
    transition: transform .12s ease;
  }
  .outline summary .caret::before { content: "\25B6"; }
  .outline details[open] > summary .caret { transform: rotate(90deg); }
  .outline-kids { padding-left: 1.4rem; }
  .outline-empty { padding: .6rem .25rem .6rem 1.35rem; margin: 0; border-bottom: 1px solid var(--rule); }
  .row a.row-name { text-decoration: none; color: inherit; }
  .row a.row-name:hover { color: var(--brass); }
  /* The pencil at the end of a row: the place's own page. */
  .editlink {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 28px; height: 28px; border-radius: 50%; color: var(--faint); text-decoration: none;
  }
  .editlink:hover { color: var(--brass); background: var(--sunk); }
  .photolink { display: flex; justify-content: flex-end; margin: 0 0 .75rem; font-size: 13px; }
  .photolink a { color: var(--muted); text-decoration: none; }
  .photolink a:hover { color: var(--brass); }

  /* The "+" that opens the add-place sheet. Round so it reads as an action
     on the row rather than as part of its name. */
  .plus {
    width: 28px; height: 28px; flex-shrink: 0; padding: 0; cursor: pointer;
    border: 1px solid var(--rule-mid); border-radius: 50%; background: var(--surface);
    color: var(--ink-soft); font: inherit; font-size: 18px; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .plus:hover { border-color: var(--brass); color: var(--brass); }
  .title-tools { display: flex; align-items: center; gap: .6rem; }
  /* The "+" that adds a book. Beside the one that adds a place it needs a
     word, or two identical circles sit on one row asking to be confused. */
  .plus.book { width: auto; padding: 0 .6rem 0 .45rem; border-radius: 14px; gap: .3rem; }
  .plus.book span { font-size: 12px; }
  /* The add-a-book sheet: the answers as rows, each with its two buttons. */
  .addbook { width: min(92vw, 34rem); }
  .addbook-state { margin: .5rem 0 0; font-size: 12.5px; }
  .addbook-results { margin-top: .75rem; max-height: 22rem; overflow-y: auto; }
  .addbook-row {
    display: grid; grid-template-columns: 30px 1fr auto; gap: .6rem; align-items: center;
    padding: .5rem .2rem; border-bottom: 1px solid var(--rule);
  }
  .addbook-row img { width: 30px; border-radius: 1px; }
  .addbook-row .pt { font-family: var(--serif); font-size: 13.5px; }
  .addbook-row .pa { font-size: 11px; color: var(--muted); }
  .addbook-pick { display: flex; gap: .35rem; flex-shrink: 0; }
  .addbook-pick .sm { padding: .3rem .6rem; font-size: 12px; white-space: nowrap; }
  .addbook-acts { display: flex; gap: .5rem; margin-top: 1.25rem; flex-wrap: wrap; }
  @media (max-width: 34rem) {
    .addbook-row { grid-template-columns: 30px 1fr; }
    .addbook-pick { grid-column: 2; }
  }
  .addplace fieldset { border: 0; padding: 0; margin: 0; display: grid; gap: 1rem; }
  .themerow { display: flex; gap: .5rem; align-items: stretch; }
  .themerow .ghost { white-space: nowrap; flex-shrink: 0; }
  /* `display: grid` above would otherwise beat the browser's own rule for
     `hidden`, and all three levels' fields would show at once. */
  .addplace fieldset[hidden] { display: none; }
