/* Willett Street Inventory — single stylesheet. Mobile first; the
   @media (min-width: 800px) block adjusts things for desktop. */
:root { --paper:#f3f4f1; --ink:#1c1e1d; --accent:#2f6b5e; --muted:#6b6f6d; --line:#d9dbd7; --card:#ffffff; --warn:#b8651b;
        /* Quiet Gallery (QG01 onwards). New names, so the Viewing Room, which
           reads the ones above, is not changed by them. */
        --qg-surface:#ffffff; --qg-ink:#203831; --qg-muted:#56665e; --qg-accent:#2c675b; --qg-line:#dbe3dd; --qg-wash:#f0f5f1; }
* { box-sizing:border-box; }
html { -webkit-text-size-adjust:100%; }
body { margin:0; background:var(--paper); color:var(--ink); font:16px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
h1 { font-size:1.5rem; margin:.6rem 0; } h1 small { color:var(--muted); font-weight:normal; font-size:1rem; }
h2 { font-size:1.1rem; margin:1rem 0 .5rem; }
.container { padding:.75rem; max-width:1200px; margin:0 auto; }
.topbar { background:var(--accent); color:#fff; padding:.5rem .75rem; display:flex; flex-wrap:wrap; gap:.5rem 1rem; align-items:center; padding-top:calc(.5rem + env(safe-area-inset-top)); }
.topbar a, .topbar button { color:#fff; }
/* The brand is a link to the dashboard, so it is a tap target like any
   other. It measured 23px at 430 and 26px at 1,440 — found by the same sweep
   that caught Log out during change 2, and not in the change 1b check, which
   only named Log out. Inline-flex with a min-height does not change where
   anything sits: the bar is already taller than 44px at every width. */
.brand { font-weight:700; font-size:1.1rem; letter-spacing:.02em;
         display:inline-flex; align-items:center; min-height:44px; }
.topbar nav { display:flex; gap:.9rem; flex-wrap:wrap; align-items:center; }
.topbar nav a, .topbar nav button { padding:.35rem 0; min-height:44px; display:inline-flex; align-items:center; }
.topbar .search { flex:1 1 100%; display:flex; gap:.4rem; align-items:center; } .topbar .search input { width:100%; flex:1 1 auto; min-width:0; }

/* === The staff bar: Quiet Gallery, QG01 (19 October 2026) ================
   Eric chose Quiet Gallery from two reviewed proposals. This is its first
   phase, the bar and the footer only; page surfaces follow in QG02-QG05.
   White, a hairline under it, ink for the links, green for + Add and for the
   mark under the current section. The values are the approved reference's,
   as the --qg- variables in :root, and were checked for contrast on white:
   ink 12.2:1, muted 5.9:1, white on the green 6.6:1.

   EVERY RULE HERE HANGS OFF `.appbar` OR `.foot`. The Viewing Room's header
   is a `.topbar` and shares this file; it carries no `appbar`, so nothing
   below can reach the one guest-facing surface. The `.topbar` rules above
   are the room's and were left exactly as they were.

   Mobile first. Below 900px the bar is two rows: brand and + Add, then a
   "Browse & tools" menu beside the search box. From 900px it is the desktop
   bar: search on a second row of its own up to 1,279, and one row from
   1,280 (see the 900px block for why those numbers). */
.appbar { position:relative; z-index:50; display:grid;
          grid-template-columns:auto minmax(0,1fr) auto;
          grid-template-areas:"brand brand add" "browse search search";
          gap:.35rem .6rem; align-items:center;
          background:var(--qg-surface); color:var(--qg-ink);
          border-bottom:1px solid var(--qg-line);
          padding:.3rem max(.75rem, env(safe-area-inset-right)) .5rem max(.75rem, env(safe-area-inset-left));
          padding-top:calc(.3rem + env(safe-area-inset-top)); }
.appbar a { color:var(--qg-ink); }
.appbar a:hover { color:var(--qg-accent); text-decoration:none; }
.appbar .brand { grid-area:brand; color:var(--qg-ink); font-weight:600; font-size:1.05rem; letter-spacing:-.015em; white-space:nowrap; }
.appbar .appnav, .appbar .account { display:none; }

/* Focus is drawn, always, and in the accent: a keyboard user has to be able
   to see where they are on a white bar as well as they could on the green. */
.appbar a:focus-visible, .appbar summary:focus-visible, .appbar button:focus-visible,
.appbar input:focus-visible { outline:3px solid var(--qg-accent); outline-offset:2px; border-radius:6px; }

/* + Add. aria-label in the template keeps the accessible name "Add item". */
.appbar .navadd { grid-area:add; background:var(--qg-accent); border-color:var(--qg-accent); color:#fff;
                  font-weight:500; padding:0 1rem; border-radius:7px; white-space:nowrap; }
.appbar .navadd:hover { color:#fff; filter:brightness(1.08); }

/* Search, and Find by photo beside it. 16px text: anything smaller and an
   iPhone zooms the page when the box takes focus. */
.appbar .search { grid-area:search; display:flex; gap:.4rem; align-items:center; min-width:0; }
.appbar .search input { flex:1 1 auto; width:100%; min-width:0; min-height:44px; font-size:16px;
                        border:1px solid var(--qg-line); border-radius:7px; padding:.45rem .7rem; color:var(--qg-ink); }
.appbar .search input::placeholder { color:var(--qg-muted); }
.appbar .camera { flex:0 0 auto; width:44px; height:44px; display:inline-flex; align-items:center; justify-content:center;
                  border:1px solid var(--qg-line); border-radius:7px; color:var(--qg-ink); }
.appbar .camera:hover { background:var(--qg-wash); color:var(--qg-accent); }

/* The menus. <details> is the mechanism; see base.html. */
.appbar .navmenu > summary { list-style:none; cursor:pointer; min-height:44px; display:inline-flex; align-items:center;
                             gap:.35rem; white-space:nowrap; }
.appbar .navmenu > summary::-webkit-details-marker { display:none; }
/* The caret is a swapped GLYPH, not a rotated one: rotating a text triangle
   inside a flex line left it visibly off-axis at 430 (change 1b). */
.appbar .navmenu > summary::after { content:"\25be"; font-size:.75em; line-height:1; color:var(--qg-muted); }
.appbar .navmenu[open] > summary::after { content:"\25b4"; }
.appbar .navmenu > summary:hover { color:var(--qg-accent); }

/* "Browse & tools", the phone's menu. A button-like summary — it has to be
   seen to be a control, which a bare word in a white bar is not — and an
   open panel that hangs full width under the whole bar, over the page,
   positioned against the bar (the details itself is static). Two columns
   of 44px links, nothing scrolls sideways, and nothing is a menu inside a
   menu: Reports and Workbench are labelled groups, every link one tap from
   the open panel. */
.appbar .browse { grid-area:browse; position:static; }
.appbar .browse > summary { border:1px solid var(--qg-line); border-radius:7px; padding:0 .75rem; font-weight:500; }
.appbar .browse[open] > summary { background:var(--qg-wash); border-color:var(--qg-accent); }
.appbar .browse-here { display:none; color:var(--qg-muted); font-weight:400; }
.appbar .browse-panel { position:absolute; left:0; right:0; top:100%; z-index:60;
                        background:var(--qg-surface); border-bottom:1px solid var(--qg-line);
                        box-shadow:0 14px 28px rgba(32,56,49,.14);
                        padding:.25rem max(.75rem, env(safe-area-inset-right)) .75rem max(.75rem, env(safe-area-inset-left)); }
.appbar .browse-group { display:grid; grid-template-columns:1fr 1fr; gap:0 .75rem; padding:.3rem 0; }
.appbar .browse-group + .browse-group, .appbar .browse-out { border-top:1px solid var(--qg-line); }
.appbar .browse-group a { min-height:44px; display:flex; align-items:center; padding:.2rem .5rem; border-radius:6px; line-height:1.25; }
.appbar .browse-group a:hover { background:var(--qg-wash); }
.appbar .browse-group a.on { background:var(--qg-wash); color:var(--qg-accent); font-weight:600; box-shadow:inset 3px 0 var(--qg-accent); }
.appbar .browse-h { grid-column:1 / -1; margin:.45rem .5rem .1rem; font-size:.75rem; font-weight:600;
                    letter-spacing:.1em; text-transform:uppercase; color:var(--qg-muted); }
.appbar .browse-h.on { color:var(--qg-accent); }
.appbar .browse-out { padding-top:.3rem; }
.appbar .browse-out .linklike { min-height:44px; display:inline-flex; align-items:center; gap:.4rem; padding:0 .5rem; color:var(--qg-ink); }
.appbar .who { color:var(--qg-muted); }

@media (max-width: 339px) {
  /* Narrower than any phone, but not than a zoomed one: a 390 phone at 200%
     lays out in 195px. Row two stacks (menu, then search) and the open
     menu is one column, so nothing runs off the edge. Measured with
     tools/measure_nav.py --zoom. */
  .appbar { grid-template-columns:minmax(0,1fr) auto; grid-template-areas:"brand add" "browse browse" "search search"; }
  .appbar .brand { white-space:normal; }
  .appbar .browse > summary { display:flex; }
  .appbar .browse-group { grid-template-columns:1fr; }
}

@media (min-width: 600px) and (max-width: 899px) {
  /* A tablet has the room to say where you are on the closed menu. */
  .appbar .browse-here { display:inline; }
  .appbar .browse-group { grid-template-columns:repeat(3, 1fr); }
}

@media (min-width: 900px) {
  /* THE DESKTOP ROWS ARE A GRID, NOT A WRAPPING FLEX ROW. Flexbox packs a
     line by each item's preferred width and wraps whatever is left, so where
     the account landed depended on the length of the signed-in name and the
     font: Codex measured it alone on a second row at 1,200 and 1,210 on
     staging with "staging-admin" (Staging Review v05, R1), and with a longer
     name it also happened at 900 and up to 1,280. A grid puts each part in a
     named place, so nothing can wrap anywhere unplanned.

       900-1,279   row one: brand, links, + Add, Log out
                   row two: search, on its own
       1,280 up    one row: brand, links, search, + Add, name, Log out

     The signed-in name is drawn only in the one-row layout. With it, row one
     does not fit at 900 even for a 13-character name; without it, row one
     needs about 830px (measured, with the widest fallback font here), well
     inside 900 less a 15px scrollbar. The name stays on Log out's tooltip
     and in the phone menu. In one row the name is capped at 10rem, so the
     widest row is about 1,026px plus search; at 1,280 less a scrollbar that
     leaves search 239px. Measured with tools/measure_nav.py --headed. */
  .appbar { grid-template-columns:auto 1fr auto auto;
            grid-template-areas:"brand links add account" "search search search search";
            gap:.4rem 1.4rem;
            padding:.55rem max(1.5rem, env(safe-area-inset-right)) .55rem max(1.5rem, env(safe-area-inset-left));
            padding-top:calc(.55rem + env(safe-area-inset-top)); }
  .appbar .browse { display:none; }
  .appbar .appnav { grid-area:links; display:flex; align-items:center; gap:1.2rem; }
  .appbar .appnav > a { min-height:44px; display:inline-flex; align-items:center; }
  /* The current section: aria-current for a screen reader, and this, the
     visible half — a green rule under the word, as in the reference. */
  .appbar .appnav > a.on, .appbar .appnav .navmenu > summary.on { color:var(--qg-ink); box-shadow:inset 0 -2px 0 var(--qg-accent); }
  .appbar .appnav .navmenu { position:relative; }
  .appbar .appnav .navpanel { position:absolute; top:100%; left:-.6rem; z-index:60; min-width:16rem; display:flex; flex-direction:column;
                              background:var(--qg-surface); border:1px solid var(--qg-line); border-radius:9px;
                              box-shadow:0 12px 28px rgba(32,56,49,.16); padding:.3rem; margin-top:.2rem; }
  .appbar .appnav .navpanel a { min-height:44px; display:flex; align-items:center; padding:.45rem .7rem; border-radius:6px; white-space:nowrap; }
  .appbar .appnav .navpanel a:hover { background:var(--qg-wash); }
  .appbar .search { width:100%; max-width:36rem; justify-self:start; }
  .appbar .account { grid-area:account; display:flex; align-items:center; gap:.9rem; }
  .appbar .account .who { display:none; max-width:10rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:.92rem; }
  .appbar .navout .linklike { min-height:44px; display:inline-flex; align-items:center; padding:0 .15rem; color:var(--qg-ink); }
  .appbar .navout .linklike:hover { color:var(--qg-accent); }
}

@media (min-width: 1280px) {
  .appbar { grid-template-columns:auto auto minmax(0, 1fr) auto auto;
            grid-template-areas:"brand links search add account"; }
  .appbar .search { max-width:17rem; justify-self:end; }
  .appbar .account .who { display:block; }
}

/* The footer: the same links as before, on the bar's white, each a 44px
   target. The build line stays in plain sight for staff — it is how the
   checking session confirms which build is live — and is not tucked into a
   disclosure. */
.foot { text-align:center; font-size:.88rem; background:var(--qg-surface); border-top:1px solid var(--qg-line); color:var(--qg-muted);
        margin-top:2.5rem; padding:.75rem .75rem calc(1.75rem + env(safe-area-inset-bottom)); }
.foot a { display:inline-flex; align-items:center; min-height:44px; color:var(--qg-accent); }

.linklike { background:none; border:0; padding:0; font:inherit; cursor:pointer; color:var(--accent); }
.linklike.danger { color:#a33; }
.inline { display:inline; }
.messages { list-style:none; padding:0; margin:0 0 .75rem; } .messages li { background:#e3efe9; border-left:4px solid var(--accent); padding:.5rem .75rem; margin-bottom:.4rem; border-radius:4px; }
.error { color:#a33; font-size:.9rem; } .notice { background:#fbf1e6; border-left:4px solid var(--warn); padding:.5rem .75rem; border-radius:4px; }
/* A notice that is not an aside. The departure notice explains a number that
   moved; this one says something is broken and wants to be told apart from it
   at a glance, without becoming a second visual language. */
.notice.bad { background:#fdecea; border-left-color:#a33; }
.muted { color:var(--muted); } .small { font-size:.88rem; } .lead { font-size:1.1rem; color:var(--muted); margin:.2rem 0 .8rem; }
.mono { font-family:ui-monospace, Menlo, Consolas, monospace; font-size:.92em; } .mono.big { font-size:1.2rem; font-weight:700; }
.pre { white-space:pre-line; }
.pill { display:inline-block; background:#e6e8e4; border-radius:999px; padding:.1rem .6rem; font-size:.8rem; color:var(--ink); } .pill.warn { background:#f7e3cf; color:var(--warn); }
.card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:.9rem 1rem; margin-bottom:.9rem; }
.card.narrow { max-width:520px; margin:1rem auto; }
.btn { display:inline-flex; align-items:center; justify-content:center; min-height:44px; padding:.4rem 1rem; border:1px solid var(--accent); border-radius:8px; background:#fff; color:var(--accent); font:inherit; cursor:pointer; }
.btn.primary { background:var(--accent); color:#fff; } .btn.big { width:100%; font-size:1.1rem; }
.btn:hover { text-decoration:none; filter:brightness(1.05); }
.actions { display:flex; flex-wrap:wrap; gap:.5rem; margin:.5rem 0; }
.page-head { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:.5rem; }
/* dashboard */
.stats { display:grid; grid-template-columns:repeat(auto-fit, minmax(140px,1fr)); gap:.6rem; margin:.5rem 0 1rem; }
.stat { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:.8rem; display:flex; flex-direction:column; color:var(--ink); }
.stat b { font-size:1.5rem; color:var(--accent); } .stat span { font-size:.82rem; color:var(--muted); } .stat.warn b { color:var(--warn); }
.grid.cats { display:grid; grid-template-columns:repeat(auto-fill, minmax(150px,1fr)); gap:.6rem; }
.card.cat { display:flex; flex-direction:column; margin:0; color:var(--ink); min-height:96px; }
.card.cat .code { font-family:ui-monospace, monospace; color:var(--accent); font-weight:700; } .card.cat .name { font-size:.9rem; flex:1; } .card.cat .n { font-size:1.4rem; font-weight:700; text-align:right; }
.two-col { display:grid; grid-template-columns:1fr; gap:1rem; }
.list { list-style:none; padding:0; margin:0; } .list li { padding:.4rem 0; border-bottom:1px solid var(--line); }
/* tables */
.table { width:100%; border-collapse:collapse; background:var(--card); } .table td, .table th { padding:.45rem .5rem; border-bottom:1px solid var(--line); text-align:left; vertical-align:top; } .table th { font-size:.82rem; color:var(--muted); text-transform:uppercase; letter-spacing:.03em; }
.table .num { text-align:right; white-space:nowrap; } .table.small { font-size:.88rem; } .table-wrap { overflow-x:auto; } .table img.mini { width:44px; height:44px; object-fit:cover; border-radius:4px; }
/* item cards — the gallery tiles. Two across on a phone; the desktop
   block near the bottom of this file widens them to about four across. */
.card-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(160px,1fr)); gap:.6rem; }
.item-card { background:var(--card); border:1px solid var(--line); border-radius:10px; overflow:hidden; color:var(--ink); display:flex; flex-direction:column; }
/* aspect-ratio:1 reserves the square before the picture arrives, so the
   grid does not jump about while photos load in from the photo store. */
.item-card .thumb, .review-row .thumb { aspect-ratio:1; background:#e6e8e4; display:flex; align-items:center; justify-content:center; } .item-card .thumb img, .review-row .thumb img { width:100%; height:100%; object-fit:cover; }
.item-card .thumb:hover { text-decoration:none; }
.noimg { color:var(--muted); font-family:ui-monospace, monospace; font-size:1.2rem; }
/* A tile with no photograph. Grey and calm on purpose — a missing photo
   is a job waiting, not an error, so nothing here is red or alarming. */
.item-card .thumb.empty { flex-direction:column; gap:.4rem; }
.item-card .addphoto { font-size:.8rem; color:var(--muted); background:var(--card); border:1px dashed var(--line); border-radius:999px; padding:.25rem .7rem; }
.item-card .addphoto:hover { color:var(--accent); border-color:var(--accent); text-decoration:none; }
.item-card .meta { padding:.5rem .6rem; display:flex; flex-direction:column; gap:.15rem; font-size:.9rem; color:var(--ink); } .item-card small { color:var(--muted); }
/* The gallery is allowed to use a wide monitor. Text pages stay at 1200px
   because long lines are tiring to read, but a wall of pictures only gets
   better with room: on a 4K screen the old cap left the gallery sitting in a
   narrow column with two-thirds of the screen empty. The item list adds
   .wide to its container when the gallery view is showing. */
.container.wide { max-width:1900px; }
/* Keep every tile the same shape. The picture must not stretch to fill a
   tall row (grid children stretch by default, which was pulling some images
   out of square), and the title is clamped to two lines so the rows line up
   instead of stepping up and down. */
.card-grid { align-items:start; }
.item-card .thumb { flex:0 0 auto; overflow:hidden; position:relative; }
/* The picture is positioned inside the square rather than sized by it. A tall
   photograph was otherwise pushing its own tile taller than the square: the
   img's height:100% has nothing to resolve against in an auto-height parent,
   so the picture kept its natural height and dragged the tile with it. */
.item-card .thumb img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.item-card .meta strong { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.item-card .idline { display:flex; align-items:center; gap:.4rem; }
.item-card .idline .pill { flex:0 0 auto; font-size:.72rem; padding:.05rem .45rem; }
.item-card .meta strong { font-weight:600; line-height:1.3; }
.item-card .meta:hover { text-decoration:none; } .item-card .meta:hover strong { text-decoration:underline; }
/* Gallery / List switch on the items page, plus the "N without photos"
   chip beside it. Both are ordinary links, so they work with JavaScript
   switched off and the browser's Back button behaves normally. */
.viewbar { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; }
.segmented { display:inline-flex; border:1px solid var(--accent); border-radius:8px; overflow:hidden; }
.segmented .seg { display:inline-flex; align-items:center; min-height:44px; padding:.4rem .95rem; background:var(--card); color:var(--accent); font-size:.95rem; }
.segmented .seg + .seg { border-left:1px solid var(--accent); }
.segmented .seg:hover { text-decoration:none; filter:brightness(.97); }
.segmented .seg.on { background:var(--accent); color:#fff; font-weight:600; }
.chip.gap { display:inline-flex; align-items:center; min-height:44px; padding:.3rem .85rem; border:1px solid var(--line); border-radius:999px; background:var(--card); color:var(--muted); font-size:.9rem; }
.chip.gap:hover { border-color:var(--accent); color:var(--accent); text-decoration:none; }
.chip.gap.on { border-color:var(--accent); color:var(--accent); background:#e3efe9; }
/* filters */
.filters { margin:.5rem 0 1rem; } .filters summary { cursor:pointer; padding:.5rem 0; font-weight:600; }
.filter-form { display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; }
.filter-form > input[type=search] { flex:1 1 100%; } .filter-form select, .filter-form input { min-height:44px; }
.filter-form .range { display:flex; gap:.3rem; } .filter-form .range input { width:9rem; }
.check { display:inline-flex; align-items:center; gap:.3rem; min-height:44px; }
input, select, textarea { font:inherit; padding:.5rem .6rem; border:1px solid #b9bcb8; border-radius:8px; background:#fff; color:var(--ink); max-width:100%; }
input[type=checkbox] { width:1.3rem; height:1.3rem; }
/* forms */
.form label { display:block; font-size:.85rem; color:var(--muted); margin-bottom:.15rem; }
.form .field { margin-bottom:.7rem; } .form .field input, .form .field select, .form .field textarea { width:100%; }
.form-grid { display:grid; grid-template-columns:1fr; gap:.2rem .9rem; }
/* F03: the refused-save panel on the edit form. Long and multi-line text is
   shown whole, wrapped, never cut off; each choice is a 44px tap target. */
.edit-conflict fieldset.conflict-row { border:1px solid var(--line); border-radius:8px; margin:0 0 .7rem; padding:.5rem .75rem; min-width:0; }
.edit-conflict fieldset.conflict-missing { border-color:#a33; }
.edit-conflict legend { font-weight:600; padding:0 .3rem; }
.conflict-values { margin:.2rem 0 .4rem; } .conflict-values dt { font-size:.88rem; color:var(--muted); margin-top:.3rem; } .conflict-values dd { margin:0; }
.conflict-value { white-space:pre-wrap; overflow-wrap:anywhere; max-height:16rem; overflow:auto; background:var(--paper); border-radius:4px; padding:.25rem .45rem; }
.form .conflict-choice { display:flex; align-items:center; gap:.5rem; min-height:44px; margin:0; cursor:pointer; font-size:1rem; color:var(--ink); }
.conflict-choice input { width:1.2rem; height:1.2rem; margin:0; padding:0; flex:none; }
.pager { display:flex; justify-content:center; align-items:center; gap:1rem; margin:1rem 0; }
/* detail */
.gallery { display:flex; gap:.6rem; overflow-x:auto; padding-bottom:.4rem; margin-bottom:.6rem; } .gallery figure { margin:0; flex:0 0 auto; max-width:85vw; } .gallery img { max-height:320px; max-width:100%; border-radius:8px; border:1px solid var(--line); } .gallery figcaption { font-size:.82rem; color:var(--muted); display:flex; gap:.6rem; flex-wrap:wrap; }
.fields { display:grid; grid-template-columns:1fr; gap:.15rem; margin:0; } .fields dt { font-size:.78rem; color:var(--muted); text-transform:uppercase; letter-spacing:.03em; margin-top:.5rem; } .fields dd { margin:0; }
/* review */
.review-row { display:grid; grid-template-columns:80px 1fr; gap:.8rem; } .review-row .thumb { width:80px; height:80px; border-radius:8px; overflow:hidden; } .review-row p { margin:.2rem 0; }
.inline-form { display:flex; flex-wrap:wrap; gap:.4rem; align-items:center; margin-top:.4rem; }
.tree, .tree ul { list-style:none; padding-left:1rem; } .tree > li { padding:.3rem 0; }
@media (min-width: 800px) {
  .container { padding:1.25rem; }
  .two-col { grid-template-columns:3fr 2fr; }
  /* On a laptop the tiles grow to roughly four across. auto-fill works it
     out from the width available, so a wider window simply fits more. */
  .card-grid { grid-template-columns:repeat(auto-fill, minmax(260px,1fr)); gap:.9rem; }
  .form-grid { grid-template-columns:1fr 1fr; } .form-grid .wide { grid-column:1 / -1; }
  .fields { grid-template-columns:160px 1fr; gap:.4rem .8rem; } .fields dt { margin-top:0; padding-top:.1rem; }
  .filter-form > input[type=search] { flex:1 1 320px; }
  h1 { font-size:1.8rem; }
}

/* Dashboard hero: featured photo (left) + stats */
.hero { display:flex; gap:.9rem; align-items:stretch; margin-bottom:.9rem; }
.hero .stats { flex:1; margin-bottom:0; }
.featured { flex:0 0 300px; max-width:300px; background:var(--card); border:1px solid var(--line); border-radius:10px; overflow:hidden; color:var(--ink); display:flex; flex-direction:column; }
.featured img { width:100%; aspect-ratio:1; object-fit:cover; background:#e6e8e4; }
.featured .cap { padding:.5rem .6rem 0; font-size:.9rem; }
.featured small { padding:0 .6rem .5rem; color:var(--muted); }
@media (max-width:800px) { .hero { flex-direction:column; } .featured { flex:none; max-width:none; } .featured img { aspect-ratio:16/10; } }
/* Pager with jump-to-page */
.pager { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; justify-content:center; margin:1rem 0; }
.pager .jump { display:inline-flex; gap:.3rem; align-items:center; }
.pager .jump input { width:5.5rem; padding:.35rem .45rem; border:1px solid var(--line); border-radius:8px; }

/* Recent acquisitions: Artlogic-style list that scrolls inside its own box */
.acq-box { background:var(--card); border:1px solid var(--line); border-radius:10px; max-height:430px; overflow-y:auto; }
.acq-row { display:flex; align-items:center; gap:.9rem; padding:.55rem .9rem; border-bottom:1px solid var(--line); color:var(--ink); }
.acq-row:hover { background:var(--paper); }
.acq-row .thumb { flex:0 0 64px; width:64px; height:64px; background:#e6e8e4; border-radius:6px; overflow:hidden; display:flex; align-items:center; justify-content:center; }
.acq-row .thumb img { width:100%; height:100%; object-fit:cover; }
.acq-row .thumb .noimg { font-family:ui-monospace, monospace; font-size:.75rem; color:var(--muted); }
.acq-row .meta { flex:1; min-width:0; display:flex; flex-direction:column; gap:.1rem; }
.acq-row .meta strong { font-weight:600; line-height:1.3; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.acq-row .meta .mono { color:var(--muted); font-size:.85rem; }
.acq-row .when { text-align:right; white-space:nowrap; display:flex; flex-direction:column; gap:.1rem; }
.acq-row .when .date { font-size:.9rem; }
.acq-row .when small { color:var(--muted); }
.acq-more { display:block; padding:.7rem .9rem; text-align:center; font-size:.9rem; color:var(--accent); }
@media (max-width:600px) { .acq-row .when small { display:none; } .acq-row .meta strong { white-space:normal; font-size:.92rem; } }

/* Curated collections controls */
.addcoll { display:inline-flex; gap:.3rem; align-items:center; }
.addcoll select, .addcoll input[type=text] { padding:.3rem .4rem; border:1px solid var(--line); border-radius:8px; max-width:170px; }
.addall { display:flex; flex-wrap:wrap; gap:.6rem; align-items:center; }
.addall select, .addall input[type=text] { padding:.35rem .45rem; border:1px solid var(--line); border-radius:8px; }
.item-card .cardfoot { padding:0 .6rem .5rem; } .linklike.small { font-size:.8rem; color:var(--muted); }
.btn.danger { border-color:#a33; color:#a33; }

/* =====================================================================
   Gallery First, carried through the rest of the app.
   Everything below is additive: the items page's .card-grid / .item-card
   rules above are untouched, because that page is the reference.
   ===================================================================== */

/* --- The homepage: Quiet Gallery, QG02 (19 October 2026 mockup) ---------
   Search first, then every category beside a restrained featured
   photograph, then the recent acquisitions. White page, ink text, green only
   for the Search button and links. Every rule here hangs off .home or
   body.qg-home, which only the homepage carries, so no other page — and not
   the Viewing Room, which shares this file — is touched.

   Phone first: one column, categories BEFORE the featured photograph. From
   900px (where the bar becomes the desktop bar) the photograph takes a
   column of its own beside the categories. */
body.qg-home { background:var(--qg-surface); }
.home { color:var(--qg-ink); }
.home a { color:var(--qg-accent); }
.home a:focus-visible, .home button:focus-visible, .home input:focus-visible {
  outline:3px solid var(--qg-accent); outline-offset:2px; border-radius:6px; }

.home-intro { margin:.6rem 0 1.1rem; }
.home-kicker { margin:0 0 .4rem; font-size:.75rem; font-weight:500; letter-spacing:.11em; text-transform:uppercase; color:var(--qg-muted); }
.home-intro h1 { margin:0; font-size:1.75rem; font-weight:500; line-height:1.2; letter-spacing:-.035em; color:var(--qg-ink); text-wrap:balance; }
.home-lede { margin:.5rem 0 0; color:var(--qg-muted); }

/* The label is for a screen reader; the placeholder and the button say the
   rest to a sighted reader. Hidden this way, not with display:none, so it
   stays the input's accessible name. */
.home-search-label { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden;
                     clip:rect(0 0 0 0); white-space:nowrap; border:0; }
.home-search { margin:0 0 1.6rem; }
.home-search-row { display:flex; gap:.5rem; }
.home-search input { flex:1 1 auto; min-width:0; min-height:48px; font-size:16px; padding:.6rem .8rem;
                     border:1px solid var(--qg-line); border-radius:7px; color:var(--qg-ink); }
.home-search input::placeholder { color:var(--qg-muted); }
.home-search .btn.primary { flex:0 0 auto; min-height:48px; padding:0 1.2rem; border-radius:7px;
                            background:var(--qg-accent); border-color:var(--qg-accent); color:#fff; font-weight:500; }

.home h2 { margin:0 0 .8rem; font-size:1.3rem; font-weight:500; letter-spacing:-.015em; color:var(--qg-ink); }
.home-grid { display:grid; grid-template-columns:minmax(0,1fr); gap:1.8rem; align-items:start; }

/* One category link: whole photograph on white above a centred group of
   code, actual name and live count. Natural inline flow preserves ordinary
   words; the last count can wrap as one centred unit on a narrow card. */
.home-cat-list { list-style:none; margin:0; padding:0; display:grid; gap:.6rem;
                 grid-template-columns:repeat(auto-fill, minmax(min(100%, 10.5rem), 1fr)); }
.home-cat { display:flex; flex-direction:column; height:100%; min-height:44px;
            border:1px solid var(--qg-line); border-radius:8px; overflow:hidden; background:var(--qg-surface); }
.home a.home-cat { color:var(--qg-ink); }
.home-cat-pic { display:flex; align-items:center; justify-content:center; height:120px; padding:.55rem;
                background:#fff; }
.home-cat-pic img { display:block; width:auto; height:auto; max-width:100%; max-height:100%; object-fit:contain; }
.home-cat-pic:has(.home-cat-fallback) { background:var(--qg-wash); }
.home-cat-fallback { color:var(--qg-muted); font-size:.8rem; text-align:center; }
.home-cat-label { display:flex; justify-content:center; align-items:center; flex:1;
                  padding:.65rem .5rem; min-height:64px; text-align:center; }
.home-cat-label-text { min-width:0; font-size:.85rem; line-height:1.4; overflow-wrap:anywhere; }
.home-cat-code, .home-cat-count { white-space:nowrap; }
.home-cat-label .code { color:var(--qg-muted); font-size:.72rem; }
.home-cat-separator { color:var(--qg-muted); }
.home-cat .name { font-weight:500; line-height:1.25; overflow-wrap:anywhere; }
.home-cat .meta { font-size:.72rem; color:var(--qg-muted); font-variant-numeric:tabular-nums; white-space:nowrap; }
.home-cat:hover { text-decoration:none; border-color:var(--qg-accent); background:var(--qg-wash); }
.home-cat:hover .name { text-decoration:underline; text-underline-offset:3px; }
.home-more { margin:.8rem 0 0; }
.home-more a { display:inline-flex; align-items:center; min-height:44px; text-decoration:underline; text-underline-offset:4px; }

/* The featured object: the WHOLE photograph, at its own proportions, in a
   box no taller than 210px on a phone and 240px from 900px — the approved
   mockup's sizes, as a ceiling and never as a crop. The width and height
   attributes give the browser the proportions before the file arrives;
   max-width and max-height then shrink it evenly (a replaced element keeps
   its ratio under both limits), and object-fit:contain is the backstop.
   White behind it, never grey: on this site grey means "no photograph". */
.home-feature { margin:0; border:1px solid var(--qg-line); border-radius:9px; overflow:hidden; background:var(--qg-surface); }
.home-feature-pic { display:flex; align-items:center; justify-content:center; padding:.9rem .9rem 0; background:#fff; }
.home-feature-pic img { display:block; width:auto; height:auto; max-width:100%; max-height:210px; object-fit:contain; }
.home-feature figcaption { padding:.85rem 1rem 1rem; display:flex; flex-direction:column; gap:.25rem; }
.home-feature figcaption .home-kicker { margin:0 0 .1rem; }
/* A block that wraps anywhere: a long catalogue title with no spaces (a
   lot heading, a URL) breaks inside the card rather than being cut off by
   its rounded corner. min-height keeps the 44px tap target. */
.home a.home-feature-title { display:block; min-height:44px; padding:.55rem 0; color:var(--qg-ink); font-weight:500;
                             line-height:1.3; overflow-wrap:anywhere; text-decoration:underline; text-underline-offset:4px; text-decoration-color:var(--qg-line); }
.home a.home-feature-title:hover { text-decoration-color:currentColor; }
.home-feature .by { color:var(--qg-ink); font-size:.92rem; overflow-wrap:anywhere; }
.home-cat-none { color:var(--qg-muted); }
.home-feature .ref { color:var(--qg-muted); font-size:.8rem; }
.home-feature small { color:var(--qg-muted); font-size:.78rem; }

/* Recent acquisitions: the same list and query as before, set under a
   hairline like the mockup's sections. */
.home-acq { margin-top:2.2rem; padding-top:1.4rem; border-top:1px solid var(--qg-line); }
.home .acq-box { border-color:var(--qg-line); border-radius:9px; }
.home .acq-row { color:var(--qg-ink); border-bottom-color:var(--qg-line); }
.home .acq-row:hover { background:var(--qg-wash); }
.home .acq-row .meta .mono, .home .acq-row .when small { color:var(--qg-muted); }
.home .acq-more { min-height:44px; display:flex; align-items:center; justify-content:center; }
.acq-empty { margin:0; padding:1rem .9rem; color:var(--qg-muted); }

@media (min-width: 900px) {
  .home-intro h1 { font-size:2rem; }
  .home-grid { grid-template-columns:minmax(0,1fr) minmax(17rem, 22rem); gap:2rem; }
  .home-grid.no-feature { grid-template-columns:minmax(0,1fr); }
  .home-cats { grid-column:1; grid-row:1; }
  .home-feature { grid-column:2; grid-row:1; }
  .home-feature-pic img { max-height:240px; }
}

.figures { list-style:none; margin:0 0 .9rem; padding:0; border-top:1px solid var(--line); }
.figures li { border-bottom:1px solid var(--line); }
.figures a { display:flex; align-items:baseline; justify-content:space-between; gap:1rem; padding:.55rem .1rem; color:var(--ink); }
.figures a:hover { text-decoration:none; }
.figures a:hover .lbl { color:var(--accent); text-decoration:underline; }
.figures .lbl { color:var(--muted); font-size:.9rem; line-height:1.25; }
.figures .lbl small { display:block; font-size:.76rem; }
.figures b { font-weight:600; font-size:1.05rem; font-variant-numeric:tabular-nums; white-space:nowrap; }
.figures b.warn { color:var(--warn); }
.dash-cta { display:flex; flex-wrap:wrap; gap:.5rem; margin:0; }
/* On the Figures page the list has no photograph beside it, but it keeps
   the width it had in that column: across the whole 1200px a label sits
   too far from its number to be read as one line. */
section.dash-figures { max-width:28rem; margin-bottom:1.3rem; }

/* The "Recently added" list now sits in the hero column beside the picture. */
.dash-figures h2 { margin-top:1.2rem; }
.list.recent li { font-size:.92rem; }

/* --- Item detail: the photograph leads ----------------------------------
   .stage is a scroll-snap row of full-width slides and .strip below it is a
   row of plain #anchor links, so moving between photographs needs no
   JavaScript and works as a swipe on a phone. */
.shots { margin:.3rem 0 1.2rem; }
.stage { display:flex; overflow-x:auto; scroll-snap-type:x mandatory; scroll-behavior:smooth; scrollbar-width:none; }
.stage::-webkit-scrollbar { display:none; }
.shot { flex:0 0 100%; min-width:0; margin:0; scroll-snap-align:start; display:flex; flex-direction:column; }
/* WITH JAVASCRIPT OFF a thumbnail is a plain #photo-N link, and following it
   brings the slide to the top of the window. This keeps 2rem clear above it
   instead of leaving the photograph flush against the top edge. With
   JavaScript on, static/item_photos.js slides the stage itself and the
   window does not move at all, so this never comes into play.

   It is scroll-padding on the window and not scroll-margin on the slide
   because Chromium ignores a slide's scroll-margin here: the slide is inside
   the stage, which is a scroll container of its own, and when the window is
   scrolled to bring the stage into view only the window's own padding
   counts. Measured, not assumed: scroll-margin-top on .shot (and on .stage)
   left the photograph at 0px; this puts it at 32px. Only on an item page
   with a strip, so no other page's in-page links change. */
html:has(.shots .strip) { scroll-padding-top:2rem; }
/* The base frame. The item page overrides it just below. (The dashboard's
   featured picture used it until QG02; it has its own white frame now,
   .home-feature-pic.) */
.shotframe { display:flex; align-items:center; justify-content:center; max-width:100%; background:#e6e8e4; border:1px solid var(--line); border-radius:12px; overflow:hidden; }
.shotframe img { display:block; width:auto; height:auto; max-width:100%; max-height:64vh; object-fit:contain; }

/* NO SURROUND ON THE ITEM PAGE (display round 1, change 3, 1 October 2026).
   Eric: "I want that grey surround removed." This SUPERSEDES two earlier
   rules, both kept in the history: the frame cut to the picture's shape
   (grey and a border hugging it), and then the frame filling the column with
   the picture centred "the way a small print is centred on a mount" — which
   is exactly the grey beside, above and below that he did not want.

   Now there is no frame to see. No fill, no border, no shadow, no rounded
   corner: the photograph sits on the page. It is shown whole and never
   enlarged — width and height stay auto, only max-width and max-height
   bind — so a 400px original is 400px, centred in the column, and the
   space it takes is its own. The link is the picture alone, not the empty
   page beside it.

   Grey keeps its one meaning, "no photograph yet": .shotframe.empty, below,
   is untouched. */
.shots .shotframe:not(.empty) { width:100%; background:none; border:0; border-radius:0; overflow:visible; }
.shots .shotframe:not(.empty) > a { display:block; flex:0 1 auto; max-width:100%; }
.shots .shotframe:not(.empty) img { margin:0 auto; }
/* SEVERAL PHOTOGRAPHS OF DIFFERENT SHAPES. The slides must not change height
   as you move between them, or the caption and the thumbnail strip jump up
   and down. When this was written the width and height of the stage's
   picture were not stored, so the height could not be reserved from the
   server. (They are now, from 2 October 2026 - see the rule after this one -
   but a photograph not yet measured still relies on this.) Instead the row is
   stretched: a flex row is as tall as its tallest slide, so every slide is as
   tall as the tallest photograph at the current width, and a shorter picture
   is centred in that height on page colour, which reads as page, not as a
   box. The caption sits at the foot of every slide, in the same place.

   The cap is what it was for a multi-photograph stage (a frame of 56vh, at
   least 300px and at most 600px), now applied to the picture itself. */
.shots.multi .stage { align-items:stretch; }
.shots.multi .shotframe:not(.empty) { flex:1 1 auto; }
.shots.multi .shotframe img { max-height:clamp(300px, 56vh, 600px); }
/* A PICTURE WHOSE SIZE IS STORED HAS ITS SPACE BEFORE IT ARRIVES (2 October
   2026). The width and height attributes alone reserve nothing here, because
   width:auto above overrides them, and an image that has not loaded has no
   size of its own to be auto about - so the page drew it at nothing and
   everything below jumped down when it came. This states the width the
   browser would have worked out once the file was in: the file's own width,
   or the column, or the height cap times the picture's shape, whichever is
   smallest. The height then follows from the width and height attributes.
   Same box before and after; the picture is still never enlarged and never
   cropped. --w and --h come from the template, only where the size is
   known; a photograph not yet measured has neither and is drawn exactly as
   it was before. The two caps must stay equal to the max-heights above.

   "The column" is 100cqw, the frame's width, and not 100%: the link round
   the picture shrinks to fit it, so a percentage would be a share of a box
   that is waiting for the picture to say how wide it is - nothing, before
   it loads, which is the fault this is here to fix. The frame is already
   width:100%, so making it a size container changes nothing about it. A
   browser too old for cqw drops the whole declaration and draws the picture
   the way it did before this rule. */
.shots .shotframe:not(.empty):has(img[width][height]) { container-type:inline-size; }
.shots .shotframe:not(.empty) img[width][height] { width:min(calc(var(--w) * 1px), 100cqw, calc(64vh * var(--w) / var(--h))); }
.shots.multi .shotframe:not(.empty) img[width][height] { width:min(calc(var(--w) * 1px), 100cqw, calc(clamp(300px, 56vh, 600px) * var(--w) / var(--h))); }
.shot figcaption { align-self:stretch; }
.shotframe:hover { text-decoration:none; border-color:var(--accent); }
/* An item with no photograph gets the whole stage to itself, dashed and
   waiting. The gap is meant to be seen. */
.shotframe.empty { width:100%; height:46vh; min-height:260px; max-height:520px; flex-direction:column; gap:.7rem; border-style:dashed; text-align:center; padding:1rem; }
.noimg.big { font-size:2.6rem; letter-spacing:.08em; }
.shot figcaption { display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:.7rem; min-height:2.1rem; padding:.45rem .2rem 0; font-size:.85rem; color:var(--muted); }
.shot figcaption .cap { max-width:60ch; }
.strip { display:flex; gap:.45rem; overflow-x:auto; padding:.55rem 0 .25rem; }
.striplink { position:relative; flex:0 0 auto; display:flex; align-items:center; justify-content:center; width:74px; height:74px; border:1px solid var(--line); border-radius:8px; overflow:hidden; background:#e6e8e4; }
.striplink img { width:100%; height:100%; object-fit:cover; }
.striplink:hover { border-color:var(--accent); text-decoration:none; }
.striplink .dot { position:absolute; left:5px; top:5px; width:8px; height:8px; border-radius:50%; background:var(--accent); box-shadow:0 0 0 2px #fff; }
.striplink.add { background:var(--card); border-style:dashed; color:var(--muted); font-size:1.6rem; line-height:1; }
.striplink.add:hover { color:var(--accent); }

/* --- The photographs screen (photo_add): the item's photographs listed, with
   "make primary" and "remove" against each (moved off the item page in
   change 3). The whole picture, uncropped, on the card's white. */
.photo-manage { margin-top:1rem; }
.photo-manage h2 { margin-top:0; }
.pm-grid { list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(auto-fill, minmax(200px,1fr)); gap:1rem; }
.pm-item { display:flex; flex-direction:column; gap:.4rem; padding:.5rem; border:1px solid var(--line); border-radius:10px; }
.pm-item.is-primary { border-color:var(--accent); box-shadow:inset 0 0 0 1px var(--accent); }
.pm-pic { display:flex; align-items:center; justify-content:center; height:180px; }
.pm-pic img { display:block; max-width:100%; max-height:180px; width:auto; height:auto; object-fit:contain; }
.pm-meta { display:flex; flex-direction:column; gap:.4rem; }
.pm-actions { display:flex; flex-wrap:wrap; gap:.4rem; align-items:center; }
.pm-actions form { margin:0; }
.pm-actions .btn { padding:.3rem .8rem; min-height:44px; }

/* --- Item detail: the heading line and the Actions menu (change 3) -------
   ID and pills on the left, Edit and Actions on the right, one line on a
   desktop. The menu is a <details>: it works with JavaScript off. Its panel
   drops over the page below it, right-aligned under the button, and is never
   wider than the screen. */
.browse-return { margin:.2rem 0 .4rem; }
.browse-return a { display:inline-flex; align-items:center; min-height:44px; min-width:44px; }
.item-card[id], .table.items tr[id], .vr-grid > [id] { scroll-margin-top:1rem; }
.item-head { align-items:center; margin-bottom:.2rem; }
.item-head .ids { min-width:0; }
.head-actions { display:flex; gap:.5rem; align-items:center; margin-left:auto; }
.actmenu { position:relative; }
.actmenu > summary { list-style:none; cursor:pointer; display:inline-flex; align-items:center; gap:.35rem; }
.actmenu > summary::-webkit-details-marker { display:none; }
.actmenu > summary::after { content:"\25be"; font-size:.75em; line-height:1; }
.actmenu[open] > summary::after { content:"\25b4"; }
.actpanel { position:absolute; right:0; top:calc(100% + .3rem); z-index:20; width:min(22rem, calc(100vw - 1.5rem)); background:var(--card); border:1px solid var(--line); border-radius:10px; box-shadow:0 8px 24px rgba(0,0,0,.14); padding:.3rem 0; display:flex; flex-direction:column; }
.actpanel > a { display:flex; align-items:center; min-height:44px; padding:0 1rem; color:var(--ink); }
.actpanel > a:hover, .actpanel > a:focus-visible { background:var(--paper); color:var(--accent); text-decoration:none; }
.actpanel .addcoll { display:flex; flex-wrap:wrap; gap:.35rem; padding:.5rem 1rem; margin:.2rem 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line); }
.actpanel .addcoll select, .actpanel .addcoll input[type=text] { flex:1 1 9rem; max-width:100%; min-height:44px; }
.actpanel .addcoll .btn { min-height:44px; }

/* A small text link in the heading of three boxes — Valuations, Location,
   Documents — so the common actions stay one click away. 44px tall for a
   thumb; the negative margin keeps the heading row its old height. */
.boxhead { display:flex; justify-content:space-between; align-items:center; gap:.5rem; margin-bottom:.5rem; }
.boxhead h2 { margin:0; }
.boxlink { display:inline-flex; align-items:center; min-height:44px; margin:-.6rem 0; font-size:.88rem; }

/* The purchase notes, folded the same way as "Where this record came from". */
.purchase-notes { margin-top:.8rem; }
.purchase-notes p { margin:.6rem 0 0; font-size:.9rem; line-height:1.5; max-width:68ch; }

/* The facts. Cards sit at their natural height instead of being stretched
   to match the tallest column, and prose is held to a comfortable measure
   so a long provenance note does not run the full width of the card. */
.detail-top { display:grid; grid-template-columns:1fr; gap:1rem; align-items:start; }
/* Grid items must be allowed to be narrower than their content, or the
   sideways-scrolling photo stage widens the whole page instead of scrolling
   inside itself — it did, by 173px, at phone width. */
.detail-top > *, .detail-main > * { min-width:0; }
.shots, .stage, .strip { min-width:0; max-width:100%; }
.detail-side .card, .detail-prose { margin-bottom:.9rem; }
.detail-side .card h2, .detail-prose h2 { margin-top:0; }
.detail-side .fields dd, .detail-prose .fields dd { line-height:1.5; }
/* The 68-character measure is on the TEXT, not the box (change 4, second
   round). As a max-width it also shortened the box, so the hairline above a
   prose field (border-top, in the 800px block) stopped at 68ch while the one
   above a short field ran the whole width - visible as soon as the column
   was wider than the measure. Padding takes up whatever is beyond 68ch, so
   the box and its line run full width and the words still wrap at 68ch. The
   percentage is of the field's own grid column; where that column is
   narrower than 68ch the padding is 0, which is every width under 1,200. */
.detail-prose .fields dd.pre { padding-right:max(0px, 100% - 68ch); }
.detail-side .fields dd.total b { font-size:1.15rem; }

/* --- Review queue: a wall of the same tiles ------------------------------ */
/* Two across on a phone, like the gallery — one giant tile per screen made
   the queue twenty thousand pixels long. */
.review-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(160px,1fr)); gap:.6rem; align-items:stretch; margin-top:.6rem; }
.review-tile { display:flex; flex-direction:column; background:var(--card); border:1px solid var(--line); border-radius:10px; overflow:hidden; }
/* The tile inside is the gallery's own .item-card, include and all; only its
   frame is switched off so this box supplies the border instead. */
.review-tile .item-card { border:0; border-radius:0; background:transparent; }
/* Every item on this page is in review by definition, so the pill would be
   on all of them and would tell you nothing. */
.review-tile .item-card .idline .pill.warn { display:none; }
.review-why { padding:0 .6rem .45rem; font-size:.82rem; color:var(--muted); }
.review-why p { margin:.15rem 0; }
.review-why .who { color:var(--ink); }
.review-why .desc summary { cursor:pointer; color:var(--accent); }
.review-why .desc p { margin-top:.3rem; }
.review-tile .inline-form { display:flex; flex-wrap:wrap; align-items:center; gap:.35rem; margin:auto 0 0; padding:.5rem .6rem .6rem; border-top:1px solid var(--line); background:var(--paper); }
.review-tile .inline-form select { flex:1 1 100%; min-height:38px; padding:.35rem .45rem; font-size:.85rem; }
.review-tile .inline-form .check { flex:1 1 auto; min-height:34px; font-size:.8rem; }
.review-tile .inline-form .btn { flex:0 0 auto; margin-left:auto; min-height:38px; padding:.3rem .9rem; }

@media (min-width: 800px) {
  .review-grid { grid-template-columns:repeat(auto-fill, minmax(250px,1fr)); gap:.9rem; }
  /* Photograph about three fifths, the hard facts beside it. */
  .detail-top { grid-template-columns:minmax(0,1.55fr) minmax(0,1fr); gap:1.2rem; }
  /* Hairlines between the fact rows: with twenty-odd rows in the Details
     card the eye needs something to run along. */
  .detail-side .fields > dt, .detail-side .fields > dd,
  .detail-prose .fields > dt, .detail-prose .fields > dd { border-top:1px solid var(--line); padding-top:.45rem; }
  .detail-side .fields > dt:first-of-type, .detail-side .fields > dd:first-of-type,
  .detail-prose .fields > dt:first-of-type, .detail-prose .fields > dd:first-of-type { border-top:0; padding-top:0; }
}
/* THE BOXES ON THE RIGHT, 800 TO 1,199 WIDE (2 October 2026). The right
   column is about 291px there, and a 160px label column left the values
   about 120px, so "Invented Saleroom, Nowhere" or a sale name broke over two
   or three lines. In this range each label sits above its value, as it does
   on a phone. The hairline stays, above each label, so it runs between one
   fact and the next and never between a label and its own value. The
   Details box is wide enough and keeps its two columns; under 800 and from
   1,200 nothing here applies. */
@media (min-width: 800px) and (max-width: 1199px) {
  .detail-side .fields { grid-template-columns:1fr; gap:.15rem; }
  .detail-side .fields > dt { margin-top:.35rem; padding-top:.45rem; }
  .detail-side .fields > dt:first-of-type { margin-top:0; padding-top:0; }
  .detail-side .fields > dd, .detail-side .fields > dd:first-of-type { border-top:0; padding-top:0; }
}

/* --- Item detail on a wide screen (display round 1, change 4) -----------
   Before this the page sat in the 1,200px column at every width, which on
   Eric's monitor (3,822 CSS px across) used under a third of the window,
   and at 1,440 left the Details box too narrow for its 68-character
   measure (a provenance ran at about 63).

   NOTHING HERE APPLIES BELOW 1,200px, and below 1,200 the container was
   already the whole window, so a phone, a tablet and a laptop under 1,200
   are exactly as they were. The <picture> round a large photograph (see
   item_detail.html) is display:contents at every width: it adds no box, so
   it changes nothing where its <source> does not apply.

   From 1,200: the column grows to 1,800px. The boxes on the right are held
   at a readable width - 37% of the window, so 444px at 1,200 (where the old
   rule gave 447, no jump), and 480px from 1,300 on - and every other pixel
   goes to the photograph and the Details under it: 901px at 1,440 and
   1,261px from 1,840 on.

   The photograph's height limits go up. One photograph: 64vh -> the screen
   less 20rem (320px) for the bar, the heading and the title above it, or
   64vh if that is more - so a laptop 900px tall is exactly as before (a
   72vh draft put the foot of a tall picture 34px below the fold at
   1,440 x 900, and change 3 wants the title and the photograph on one
   screen there), and a tall monitor gets most of its height. Several:
   clamp(300px, 56vh, 600px) -> clamp(300px, 64vh, 1,400px); the 600px cap is
   what kept the picture small on a tall monitor, and 64vh still leaves the
   picture's foot on the first screen at 1,440 x 900. Still never enlarged: the width is
   the file's own width or, where the original is offered (--ow), the
   original's own width; and the several-photograph row still takes the
   height of its tallest picture, so pictures of different shapes share one
   height.

   THESE CAPS ARE ALSO IN item_detail.html, in the `sizes` of the <source>,
   which has to state the width the picture is drawn at for the browser to
   choose the right file. Change one, change the other: the column
   (min(100vw, 1800px) - 3.7rem - min(37vw, 480px): the container's padding
   of 2 x 1.25rem plus the 1.2rem gap) and both height caps. */
.shots picture { display:contents; }
@media (min-width: 1200px) {
  .container.item-wide { max-width:1800px; }
  .detail-top { grid-template-columns:minmax(0,1fr) min(37vw, 480px); }
  .shots .shotframe img { max-height:max(64vh, 100vh - 20rem); }
  .shots.multi .shotframe img { max-height:clamp(300px, 64vh, 1400px); }
  .shots .shotframe:not(.empty) img[width][height] { width:min(calc(var(--ow, var(--w)) * 1px), 100cqw, calc(max(64vh, 100vh - 20rem) * var(--w) / var(--h))); }
  .shots.multi .shotframe:not(.empty) img[width][height] { width:min(calc(var(--ow, var(--w)) * 1px), 100cqw, calc(clamp(300px, 64vh, 1400px) * var(--w) / var(--h))); }
}

/* The Add-to-collection row on the item page had a fixed-width select and a
   fixed-width text box side by side, which pushed the whole document about
   23px wider than a phone screen (the page scrolled sideways). Let it wrap. */
@media (max-width: 799px) {
  .addcoll { flex-wrap:wrap; }
  .addcoll select, .addcoll input[type=text] { flex:1 1 9rem; max-width:100%; }
}

/* ---------------------------------------------------------------------------
   Jobs — running the imports from inside the app instead of the DigitalOcean
   console. Two grids of cards; the "check first" ones are visually quieter so
   that "dry run" and "import" cannot be confused at a glance.
   --------------------------------------------------------------------------- */
.job-grid { display:grid; gap:14px; margin:12px 0 28px;
            grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); }
.job { border:1px solid var(--line,#d8dcd6); border-radius:10px; padding:14px 16px;
       background:#fff; display:flex; flex-direction:column; gap:8px; }
.job h3 { margin:0; font-size:1rem; }
.job p { margin:0; font-size:.88rem; color:#555; flex:1; }
.job button { align-self:flex-start; }
.job.safe { background:#f7f8f6; border-style:dashed; }
/* A job whose code is not in this build. Greyed rather than hidden: hiding it
   would leave no trace of the thing that is missing, and the whole point of
   the change is that a gap should be visible rather than discovered by
   pressing a button and reading a traceback. */
.job.missing { opacity:.55; border-style:dotted; background:#faf9f7; }
.job.missing h3 { text-decoration:line-through; text-decoration-thickness:1px; }
.job.missing button[disabled] { cursor:not-allowed; }
.job-running { border:1px solid #c9a227; background:#fdf8e6; border-radius:10px;
               padding:12px 16px; margin:12px 0; }
.job-running p { margin:6px 0 0; font-size:.9rem; }
.joblog { background:#1d2733; color:#e8eef5; padding:14px 16px; border-radius:8px;
          max-height:60vh; overflow:auto; font-size:.82rem; line-height:1.45;
          white-space:pre-wrap; word-break:break-word; }
.pill.live { background:#2f6b5e; color:#fff; }
@media (max-width:520px) { .job-grid { grid-template-columns:1fr; } }

/* ---------------------------------------------------------------------------
   Backups. The missing-off-site-copy warning is meant to be the loudest thing
   on the page: a second copy that quietly stopped existing looks exactly like
   one that never did.
   --------------------------------------------------------------------------- */
.warn-panel { border:2px solid #b4552b; background:#fdf1ea; border-radius:10px;
              padding:16px 20px; margin:16px 0 24px; }
.warn-panel h2 { margin:0 0 8px; color:#8f3d1c; font-size:1.15rem; }
.warn-panel p, .warn-panel li { margin:6px 0; }
.warn-panel code { background:#fff; padding:1px 5px; border-radius:4px;
                   font-size:.86em; }
.ok-panel { border:1px solid #2f6b5e; background:#eef5f2; border-radius:10px;
            padding:14px 18px; margin:16px 0 24px; }
.ok-panel h2 { margin:0 0 6px; color:#2f6b5e; font-size:1.05rem; }
.ok-panel ul { margin:6px 0 0 18px; }
.bad { color:#a8321a; }

/* ---------------------------------------------------------------------------
   Finding things on the Items page.

   The filters were all correct and all invisible: one folded-up "Filters"
   line that nobody opens, so the search and the house/dealer list may as well
   not have existed. Three parts now:

     .findbar   the search box, house and category, always open, above the fold
     .filters   the rest, folded, labelled "More filters", opened by the
                template whenever something inside it is switched on
     .activebar what is currently filtering the page, in words, each one a
                link that takes that single filter back off

   No new colours: the chips are the accent green on the same pale wash
   (#e3efe9) already used by the "without photos" chip in the view bar.
   --------------------------------------------------------------------------- */
.findbar { margin:.5rem 0 .2rem; }
.findbar-row { display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; }
/* Phone: the search box owns its own line, the two lists share the next. */
.findbar-row .find-q { flex:1 1 100%; min-width:0; min-height:44px; }
.findbar-row select { flex:1 1 45%; min-width:0; min-height:44px; }
/* The house/dealer box is a box you TYPE into now, not a list of 313 names
   you scroll. It sits beside the two remaining lists, so it needs the same
   width and the same 44px thumb-target they have. */
.findbar-row .find-house { flex:1 1 45%; min-width:0; min-height:44px; }
/* On a phone the button lands on a line of its own; give it the whole line
   so it is an obvious thumb target and plainly belongs to the boxes above.
   (Changing the category submits by itself, but changing the house does not,
   so there has to be something unmistakable to press.) */
.findbar-row .btn { flex:1 1 100%; }
/* The fold underneath is a control, not a heading — it should look clickable. */
.filters { margin:.15rem 0 .2rem; }
.filters > summary { display:inline-flex; align-items:center; gap:.45rem; min-height:44px; padding:.3rem 0; color:var(--accent); font-weight:600; cursor:pointer; }
.filters > summary:hover { text-decoration:underline; }
/* "on" badge in the same green as the chips below, not the orange of a
   review flag — a filter being set is a state, not a problem. */
.filters > summary .pill { background:#e3efe9; color:var(--accent); font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.05em; }
.filters[open] > summary { margin-bottom:.2rem; }
.filters .filter-form { padding-bottom:.3rem; }

.activebar { display:flex; flex-wrap:wrap; align-items:center; gap:.4rem; margin:.1rem 0 .9rem; }
.activebar .lbl { color:var(--muted); font-size:.88rem; margin-right:.1rem; }
.chip.filter-chip, .chip.clearall { display:inline-flex; align-items:center; gap:.4rem; min-height:36px; padding:.2rem .75rem; border-radius:999px; font-size:.88rem; }
.chip.filter-chip { border:1px solid var(--accent); background:#e3efe9; color:var(--accent); }
.chip.filter-chip b { font-weight:700; opacity:.65; }
.chip.filter-chip:hover { background:var(--accent); color:#fff; text-decoration:none; }
.chip.filter-chip:hover b { opacity:1; }
.chip.clearall { border:1px solid var(--line); background:var(--card); color:var(--muted); }
.chip.clearall:hover { border-color:var(--accent); color:var(--accent); text-decoration:none; }
@media (min-width: 800px) {
  /* Desktop: search, house, category and the button all sit on one line. */
  .findbar-row .find-q { flex:1 1 320px; }
  .findbar-row select { flex:0 1 15rem; }
  .findbar-row .find-house { flex:0 1 15rem; }
  .findbar-row .btn { flex:0 0 auto; }
}

/* The import record on an item page: present, findable, and quiet. It is
   folded shut because "artlogic_image_hash: b7a81110..." is not a fact about
   an object; it is a fact about a file. */
.import-record { margin-top:1.1rem; border-top:1px solid var(--line); padding-top:.7rem; }
.import-record > summary { cursor:pointer; font-size:.85rem; color:var(--muted); }
.import-record > summary:hover { color:var(--accent); }
.import-record .fields { margin-top:.7rem; }
.import-record .fields dt, .import-record .fields dd { color:var(--muted); font-size:.85rem; }
.import-record .fields dd { overflow-wrap:anywhere; }

/* The build marker in the footer: present when you go looking, invisible
   when you are not. It sits under the footer links on its own line and is
   only rendered for staff. */
.build-marker {
  color: #8a8a8a;
  font-size: 0.85em;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  cursor: help;
}

/* A source that has been checked, on the photo spot-check landing screen.
   Green rather than the amber of .pill.warn — the two sit side by side in
   that list and must not be mistaken for each other at a glance. */
.pill.ok { background:#e3efe9; color:#2f6b5e; }

/* ---------------------------------------------------------------------
   The Jobs page search box.

   Added 25 September 2026, after Eric pointed out the obvious: there are
   more than fifty buttons, and being told to press one by name means
   reading every label until it turns up. Scanning a list is cheap for a
   machine and expensive for a person - which is the wrong way round when
   the machine is the one asking for the button to be pressed.

   ?q= prefills it, so a link can land on one button rather than naming it.
   --------------------------------------------------------------------- */
.job-find { display:flex; align-items:center; gap:.6rem; flex-wrap:wrap;
            margin:1rem 0 1.4rem; }
.job-find label { flex:0 0 auto; }
.job-find input { flex:1 1 22rem; max-width:34rem; padding:.55rem .7rem;
                  font:inherit; border:1px solid var(--line,#ccc);
                  border-radius:6px; background:var(--bg,#fff);
                  color:inherit; }
.job-find input:focus { outline:2px solid var(--accent,#456); outline-offset:1px; }
#jobq-count { flex:0 0 auto; }

/* HIDING HAS TO ACTUALLY HIDE, and this one line is why the search box did
   not work at all on 25 September.

   The script sets `form.hidden = true`, which relies on the BROWSER'S OWN
   rule `[hidden] { display:none }`. That rule lives in the user-agent
   stylesheet, and every author rule beats every user-agent rule - so
   `.job { display:flex }`, forty lines above, quietly put all 148 hidden
   buttons back on the screen.

   Nothing looked wrong from the inside. The box was there, the script ran,
   the attribute was set, the counter read "3 of 151 shown" - and all 151
   were visible. Three tests asserted the machinery was present and correct
   and every one of them passed.

   "Present is not right" for the third time in a week, and the third variant
   of it: the thing was present, in the right place, AND doing its job - and
   the result was still wrong, because something else undid it. A test that
   reads the HTML can never see this; only a browser can. So the assertion
   below is the next best thing - it pins the rule that makes hiding work -
   and the real check is a screenshot. */
[hidden] { display:none !important; }

/* A button arrived at by a #job-<key> link. Marked rather than isolated,
   so its neighbours are still visible and the page still makes sense. */
form.job.job-found { outline:3px solid var(--accent,#456); outline-offset:3px; }

@media print { .job-find { display:none; } }

/* --- Shaped tiles: the Items gallery, the Review queue, a collection -----
   Eric's decision, 2 October 2026, looking at a very wide picture whose
   square tile showed less than half of it: tiles take the shape of the
   photograph - "the most natural feel because you see the actual photo of
   the entire object." This REPLACES fill-the-square on these three pages.
   The Viewing Room keeps its filled squares (viewing_room/base.html); the
   spot-check and wrong-pictures pages keep the whole picture on white; a
   location's items, the Collections index and Find by photo are untouched
   and still use .card-grid above.

   NO JAVASCRIPT. Each tile carries its picture's width over height as --a,
   written by the template from the size stored on the photograph. A tile's
   flex-grow is that number and its flex-basis is that number times the row
   height, so every tile in a row starts at the row height and, when the row
   is stretched to the full line, each grows in proportion to its own width:
   they stay level. The ::after spacer takes all the spare room in the LAST
   row, so that row is not stretched.

   THE ROW HEIGHTS ARE ALSO IN collection/tile_shapes.py, which writes each
   picture's `sizes` from them. Change one, change the other.

   EXTREME SHAPES. --fa, the tile's shape, is the picture's shape held
   between --amin (so the caption under a tall narrow picture stays
   readable) and 3 (so a panorama does not take a whole row). A picture
   outside those limits is shown whole, centred on WHITE in the tile. White,
   never grey: grey means "no photograph yet" and nothing else. For every
   other picture the tile is exactly its shape and nothing shows beside it.

   No stored size, or no photograph: --a is 1, the square, as before. */
.container.full { max-width:none; }
@media (min-width: 1900px) { .container.full { padding-left:2rem; padding-right:2rem; } }

.shape-grid { display:flex; flex-wrap:wrap; align-items:stretch; gap:.6rem;
              /* Phone: two SQUARE pictures share a row - 185px at 430
                 wide, Eric's choice of 2 October 2026 over a smaller
                 height that fitted two 3:2 landscapes. */
              --h:calc((100vw - 60px) / 2); --g:1.6; --amin:.66; --bx:2px; }
.shape-grid.review { --amin:.8; }         /* room for the category form */
.shape-grid > * { --fa:clamp(var(--amin), var(--a, 1), 3);
                  flex-grow:var(--fa); flex-shrink:1;
                  /* --bx is the tile's own border, so the picture inside,
                     not the box around it, is what comes out level. */
                  flex-basis:calc(var(--fa) * var(--h) + var(--bx));
                  /* THE CAP ON STRETCHING. Every tile stops growing at the
                     same multiple --g of its starting size, so a row that
                     would have to stretch further than that stays level
                     and leaves its spare room at the right instead. Without
                     it, a row that cannot fit the next 3:1 picture was
                     stretched to 1.7 times its height. It is also what
                     lets `sizes` state each tile's true largest width. */
                  max-width:calc(var(--fa) * var(--h) * var(--g) + var(--bx));
                  min-width:0; }
.shape-grid::after { content:""; flex:1000000 1 0px; }
@media (min-width: 800px) {
  .shape-grid { gap:.9rem; --h:230px; --g:1.3; --amin:.6; }
  .shape-grid.review { --amin:.8; }
}
.shape-grid .item-card .thumb { aspect-ratio:var(--fa); }
.shape-grid .item-card .thumb.shape { background:#fff; }
.shape-grid .item-card .thumb.shape img { object-fit:contain; }
/* The caption: ID, title, then the line with the price. Title two lines at
   most (the rule above), the small line too, so no caption runs on; the
   tiles in a row are stretched to the tallest, so they end level. */
.shape-grid .item-card .meta small { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.shape-grid .item-card .meta { min-height:44px; }
@media (max-width: 799px) {
  .shape-grid .item-card .meta { padding:.4rem .45rem; font-size:.82rem; }
}

/* Quiet Gallery QG03: only inventory/curated browsing opts into these rules.
   Photo geometry, rendition sizes, the shared shell and guest wall stay as
   they are. Long catalogue labels wrap in full below equal-height pictures. */
.browse-page { color:var(--qg-ink); }
.browse-page input:not([type=hidden]):not([type=checkbox]):not([type=radio]), .browse-page select { min-height:44px; }
.browse-page .page-head { margin:.4rem 0 1rem; gap:.8rem 1.2rem; }
.inventory-browse .browse-titlebar { display:flex; justify-content:space-between; align-items:center; gap:.75rem; width:100%; min-width:0; }
.browse-page h1 { font-size:clamp(1.5rem, 1.1rem + 1vw, 2rem); line-height:1.25; overflow-wrap:anywhere; }
.browse-page h1 small { display:inline-block; font-size:.9rem; }
.browse-page .findbar { margin:.6rem 0 1rem; padding:1rem; background:var(--qg-surface); border:1px solid var(--qg-line); border-radius:12px; }
.browse-page .findbar-row { display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:.65rem; align-items:end; }
.browse-page .browse-field { display:flex; flex-direction:column; gap:.35rem; min-width:0; font-size:.875rem; color:var(--qg-muted); }
.browse-page .browse-field > span { font-weight:600; }
.browse-page .browse-field input, .browse-page .browse-field select { flex:0 0 auto; width:100%; min-width:0; min-height:44px; margin:0; color:var(--qg-ink); }
.browse-page .browse-query { grid-column:1 / 10; }
.browse-page .findbar-row > .btn { grid-column:10 / -1; padding:.4rem .6rem; }
.browse-page .browse-house { grid-column:1 / 7; }
.browse-page .browse-category { grid-column:7 / -1; }
.browse-page .browse-search-help { font-size:.8125rem; color:var(--qg-muted); margin:.6rem 0 .2rem; }
.browse-page .filters { margin:.2rem 0; }
.browse-page .filters > summary { min-height:44px; }
.browse-page .filters .filter-form { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); align-items:end; gap:.8rem; padding:.5rem 0 1rem; }
.browse-page .filters .check { min-height:44px; display:flex; align-items:center; gap:.4rem; }
.browse-page .filters .btn { min-width:0; }
.browse-page .browse-options { display:grid; grid-template-columns:auto minmax(0,1fr); gap:.5rem .75rem; align-items:end; }
.browse-page .browse-options .filters[open] { grid-column:1 / -1; }
.browse-page .browse-filter-footer { display:flex; flex-wrap:wrap; gap:.75rem 1.5rem; align-items:end; justify-content:end; }
.browse-page .browse-options:has(.filters[open]) .browse-filter-footer { grid-column:1 / -1; padding-top:.75rem; border-top:1px solid var(--qg-line); }
.browse-page .browse-sort { display:flex; flex:1 1 100%; gap:.5rem; align-items:end; }
.browse-page .browse-sort .browse-field { flex:1; }
.browse-page .browse-sort .btn { white-space:nowrap; }
.browse-page .browse-options .browse-sort .btn { padding:.4rem .65rem; }
.browse-page .browse-options .browse-sort .browse-field { min-width:0; }
.browse-page .browse-table-help { color:var(--qg-muted); font-size:.875rem; }
.browse-page .browse-reset { border-color:var(--qg-line); font-size:.875rem; }
.browse-page .activebar { gap:.5rem; margin:1rem 0; }
.browse-page .activebar .filter-chip, .browse-page .activebar .clearall,
.browse-page .viewbar .gap { min-height:44px; height:auto; white-space:normal; overflow-wrap:anywhere; }
.browse-page .activebar .filter-chip { max-width:100%; }
.browse-page .browse-result-count { margin:1.1rem 0 .75rem; color:var(--qg-muted); font-size:.9rem; }
.inventory-browse .item-card { border-color:var(--qg-line); border-radius:8px; }
.inventory-browse .item-card .meta { padding:.75rem; gap:.45rem; font-size:.9375rem; }
.inventory-browse .item-card .meta strong,
.inventory-browse .item-card .meta small,
.browse-page .glx-title, .browse-page.collections-browse .meta strong { display:block; -webkit-line-clamp:unset; overflow:visible; overflow-wrap:anywhere; }
.inventory-browse .item-card .meta strong, .browse-page .glx-title { font-size:1rem; font-weight:600; line-height:1.4; }
.browse-page .browse-maker { color:var(--qg-muted); font-size:.875rem; line-height:1.45; overflow-wrap:anywhere; }
.inventory-browse .item-card .idline { flex-wrap:wrap; font-size:.8125rem; color:var(--qg-muted); }
.inventory-browse .item-card small { font-size:.8125rem; line-height:1.45; color:var(--qg-muted); }
.inventory-browse .item-card .addphoto, .browse-page .glx-shot .addphoto { display:inline-flex; align-items:center; justify-content:center; min-height:44px; padding:.35rem .65rem; max-width:100%; text-align:center; }
.browse-page .noimg { min-height:44px; min-width:44px; display:inline-flex; align-items:center; justify-content:center; }
.browse-page a:focus-visible, .browse-page button:focus-visible, .browse-page input:focus-visible,
.browse-page select:focus-visible, .browse-page summary:focus-visible, .browse-page .table-wrap:focus-visible { outline:2px solid var(--qg-accent); outline-offset:3px; }
.browse-page .thumb:focus-visible, .browse-page .glx-shot:focus-visible { outline-offset:-3px; }
.browse-page .glx-cap { min-height:44px; padding:.75rem .15rem; gap:.4rem; }
.browse-page .glx-cap .glx-sub { color:var(--qg-muted); font-size:.8125rem; }
.browse-page .glx-remove button { min-height:44px; }
.browse-page .glx-bar { padding:.6rem 0; gap:.65rem; }
.browse-page .table-wrap { background:#fff; border:1px solid var(--qg-line); border-radius:10px; }
.browse-page .table.items { min-width:1050px; width:100%; }
.browse-page .table.items th { color:var(--qg-muted); background:var(--qg-wash); font-size:.8125rem; }
.browse-page .table.items td { padding:.8rem .65rem; vertical-align:top; }
.browse-page .table.items td:nth-child(3) { min-width:230px; max-width:440px; overflow-wrap:anywhere; font-weight:600; }
.browse-page .table.items td:nth-child(4) { min-width:180px; max-width:300px; overflow-wrap:anywhere; }
.browse-page .table.items td a { min-height:44px; display:inline-flex; align-items:center; }
.browse-page .browse-period { display:block; margin-top:.4rem; color:var(--qg-muted); }
.browse-page .pager { padding:1rem 0 .5rem; gap:.6rem; }
.browse-page .pager .jump input { min-height:44px; }
@media (min-width:800px) {
  .inventory-browse .browse-titlebar { width:auto; justify-content:flex-start; gap:1rem; }
  .browse-page .findbar-row { grid-template-columns:minmax(200px,2fr) auto minmax(150px,1fr) minmax(150px,1fr); }
  .browse-page .browse-query, .browse-page .findbar-row > .btn, .browse-page .browse-house, .browse-page .browse-category { grid-column:auto; }
  .browse-page .filters .filter-form { grid-template-columns:repeat(4,minmax(0,1fr)); }
  .browse-page .browse-sort { flex:0 1 auto; min-width:350px; }
}

/* --- Zoom into a photograph (change 5a, static/photo_viewer.js) ---------
   Everything here is drawn only when the script has run: the magnifier and
   the viewer are made by it, and .pv-host / .pv-zoomable / .pv-open are
   classes it adds. With scripting off none of these rules matches anything
   and the page is exactly as it was. The ground is near-black; the controls
   are the page's own colours, quiet until pointed at. */
.pv-open, .pv-open body { overflow:hidden; }
.pv-host { position:relative; }
.pv-wrap { display:block; }
.pv-zoomable { cursor:zoom-in; }
.pv-btn { display:inline-flex; align-items:center; justify-content:center;
          width:44px; height:44px; padding:0; border:0; border-radius:50%;
          background:rgba(28,30,29,.55); color:#fff; cursor:pointer; }
.pv-btn svg { width:22px; height:22px; fill:none; stroke:currentColor;
              stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
.pv-btn:hover, .pv-btn:focus-visible { background:var(--accent); }
.pv-btn:focus-visible { outline:2px solid #fff; outline-offset:2px; }
.pv-btn[disabled] { opacity:.3; cursor:default; background:rgba(28,30,29,.55); }
.pv-btn[hidden] { display:none; }
/* The magnifier sits in the stage's top right corner. Where the stage is
   wider than the photograph (most windows) that corner is empty ground;
   where the photograph fills the stage (a wide picture on a phone) the
   visible disc is 30px and the rest of the 44px tap area is transparent -
   and tapping there does what tapping the photograph does anyway. */
.pv-magnifier { position:absolute; top:0; right:0; z-index:2;
                background:radial-gradient(circle, rgba(28,30,29,.5) 14px, transparent 15px); }
.pv-magnifier svg { width:17px; height:17px; }
.pv-magnifier:hover, .pv-magnifier:focus-visible {
                background:radial-gradient(circle, var(--accent) 14px, transparent 15px); }
.pv { position:fixed; inset:0; z-index:1000; background:#0b0c0c; color:#fff;
      touch-action:none; overscroll-behavior:contain; user-select:none;
      -webkit-user-select:none; }
.pv[hidden] { display:none; }
.pv:focus { outline:none; }
.pv-canvas { position:absolute; inset:0; overflow:hidden; cursor:default; }
.pv-zoomed .pv-canvas { cursor:grab; }
.pv-zoomed .pv-canvas:active { cursor:grabbing; }
.pv-img { position:absolute; left:0; top:0; max-width:none; max-height:none;
          transform-origin:0 0; will-change:transform; cursor:zoom-in;
          -webkit-user-drag:none; }
.pv-zoomed .pv-img { cursor:inherit; }
.pv .pv-close { position:absolute; top:calc(.6rem + env(safe-area-inset-top)); right:.6rem; }
.pv .pv-zoom { position:absolute; top:calc(.6rem + env(safe-area-inset-top)); right:3.6rem; }
.pv .pv-prev, .pv .pv-next { position:absolute; top:50%; margin-top:-22px; }
.pv .pv-prev { left:.6rem; } .pv .pv-next { right:.6rem; }
.pv-count { position:absolute; left:50%; transform:translateX(-50%);
            bottom:calc(.8rem + env(safe-area-inset-bottom)); font-size:.85rem;
            color:rgba(255,255,255,.75); background:rgba(28,30,29,.55);
            padding:.15rem .6rem; border-radius:1rem; pointer-events:none; }
.pv-count[hidden] { display:none; }
.pv-loading { position:absolute; left:.9rem; top:calc(.9rem + env(safe-area-inset-top));
              width:16px; height:16px; border-radius:50%;
              border:2px solid rgba(255,255,255,.25); border-top-color:rgba(255,255,255,.8);
              animation:pv-spin 1s linear infinite; pointer-events:none; }
.pv-loading[hidden] { display:none; }
@keyframes pv-spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .pv-loading { animation:none; } }
