/* JetBrains Mono, served from this host.
   Self-hosted rather than linked from Google Fonts on purpose: a tool whose
   pitch is that your scans never leave the machine should not make every page
   view a request to a third party, and it has to render the same on a box with
   no route to the internet. Subsets are unicode-ranged, so a latin-only page
   fetches one 31 KB file. */
/* cyrillic-ext */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-greek.woff2') format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* The application chrome follows the design mock exactly: a fixed-height shell
   with a topbar, a 176px rail, a scrolling screen and a status line at the
   foot. Nothing scrolls except the panes that are meant to.

   Values here are transcribed from the mock rather than rounded to a scale.
   The density is the design: this is a screen someone works in for an hour at
   a time, and the difference between 26px rows and 40px rows is whether they
   can see the rest of the queue while deciding on one card. */
:root {
  --ink: #17171a;
  /* Both darkened to clear WCAG AA (4.5:1) against --paper.
     --faint was #9a9a95, which measures 2.6:1 there and was carrying real
     text, not decoration: the landing page's eyebrow, its figure captions, the
     note under the price table, every field hint in the application. --muted
     moved with it, from #6f6f6b, because it had only 4.58:1 of its own — so
     leaving it put would have made the two indistinguishable and flattened the
     hierarchy the pair exists to express. Now 6.0:1 and 4.5:1. */
  --muted: #5d5d58;
  --faint: #70706a;
  --rule: #dedbd5;
  --rule-soft: #ededea;
  --paper: #f4f4f2;
  --surface: #fff;
  --surface-2: #fbfbfa;
  --surface-3: #f7f7f5;
  --green: #2f6b4f;
  --green-dark: #24543e;
  --green-tint: #f4f9f6;
  --green-line: #c9dcd0;
  --green-rail: #eef3ef;
  --warn: #b4451f;
  --warn-tint: #fdf7f2;
  --warn-line: #e8c4b4;
  --track: #ebebe7;
  --edge: #b9b6ae;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --sans: 'Helvetica Neue', Helvetica, Arial, sans-serif;

  --scan-hatch: repeating-linear-gradient(135deg, #efefec 0 5px, #f7f7f5 5px 10px);
  --ref-hatch: repeating-linear-gradient(135deg, #e9efeb 0 5px, #f3f7f4 5px 10px);
}

*, *::before, *::after { box-sizing: border-box; }

/* The `hidden` attribute hides an element via `display: none` in the UA
   stylesheet, which any author rule with a `display` beats — including
   `.tiles-3 { display: grid }` and an inline `style="display: flex"`. That is
   how the import screen came to show its progress tiles and Start button
   before a file had been chosen. */
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.35;
  -webkit-font-smoothing: antialiased;
}
a { color: var(--green); text-decoration: none; }
a:hover { color: var(--green-dark); text-decoration: underline; }
input, button, select { font-family: inherit; }
::selection { background: #cfe3d7; }

/* ==========================================================================
   Shell
   ========================================================================== */
body.app {
  display: flex; flex-direction: column;
  height: 100vh; min-height: 0;
  overflow-x: hidden; overflow-y: auto;
}

.topbar {
  display: flex; align-items: center; gap: 16px;
  min-height: 38px; flex: 0 0 auto; flex-wrap: wrap; row-gap: 4px;
  padding: 4px 12px;
  border-bottom: 1px solid var(--rule); background: var(--surface-2);
}
.brand { display: flex; align-items: center; gap: 8px; color: var(--ink); }
.brand:hover { text-decoration: none; color: var(--ink); }
.brand-mark { width: 20px; height: 20px; flex: none; display: block; }
.brand b { font-weight: 700; letter-spacing: -0.01em; }
.brand .ver {
  font-family: var(--mono); font-size: 10px; color: var(--muted);
  border: 1px solid var(--rule); border-radius: 3px; padding: 1px 4px;
}
/* The commit the running image was built from, inside the version chip so the
   two are read as one fact. Fainter than the version because it is the thing
   you go looking for rather than the thing you notice. */
.brand .ver .sha { margin-left: 5px; padding-left: 5px; border-left: 1px solid var(--rule); opacity: 0.75; }
.spacer { flex: 1; }
.topbar-meta {
  display: flex; align-items: center; gap: 14px; min-width: 0; overflow: hidden;
  font-family: var(--mono); font-size: 11px; color: var(--muted);
}
.topbar-meta > * { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.topbar-meta a { font-family: var(--mono); color: var(--green); }
.host { display: flex; align-items: center; gap: 5px; }
.host-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); flex: none; }

.shell { display: flex; flex: 1; min-height: 0; overflow: hidden; }

/* ---------- rail ---------- */
.rail {
  width: 176px; flex: 0 0 176px;
  border-right: 1px solid var(--rule); background: var(--surface-2);
  display: flex; flex-direction: column; padding: 8px 0;
}
.rail-sec {
  font-family: var(--mono); font-size: 10px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint); padding: 6px 12px;
}
.rail a.nav-item {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; padding: 6px 12px;
  border: none; border-left: 2px solid transparent;
  background: transparent; color: var(--ink);
  font: inherit; text-align: left; cursor: pointer; white-space: nowrap;
  text-decoration: none;
}
.rail a.nav-item:hover { background: var(--surface-3); text-decoration: none; color: var(--ink); }
.rail a.nav-item.on {
  border-left-color: var(--green); background: var(--green-rail); font-weight: 600;
}
.nav-badge { font-family: var(--mono); font-size: 10px; color: var(--faint); }
.nav-badge:empty { display: none; }
.rail-spacer { flex: 1; }

/* ---------- buy me a coffee ----------
   Sits at the foot of the rail on every install, self-hosted included. This is
   AGPL software that costs its users nothing; the ask is the only thing that
   makes that sustainable, so it is present rather than tucked behind an About
   page. Quiet by default, warm on hover — a support link that shouts competes
   with the work. */
.coffee {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 8px; padding: 8px;
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); color: var(--ink);
  text-decoration: none;
}
.coffee:hover {
  border-color: var(--green-line); background: var(--green-tint);
  color: var(--ink); text-decoration: none;
}
.coffee b { display: block; font-size: 12px; font-weight: 600; }
.coffee-sub {
  display: block; font-family: var(--mono); font-size: 10px;
  color: var(--muted); line-height: 1.4; margin-top: 2px;
}
/* Drawn rather than assembled out of borders, which is what this was: a box
   with its top border removed for the cup and a pseudo-element for the handle.
   Two shapes cannot make a handle meet a wall — there was always a gap — and
   with no steam the result read at 14px as the letters "U p". */
.coffee-cup {
  width: 16px; height: 16px; flex: none; margin-top: 1px;
  fill: none; stroke: var(--green); stroke-width: 1.25;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ---------- account ---------- */
.account {
  max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.inline-form { display: inline; }
.linkish {
  border: none; background: none; padding: 0; cursor: pointer;
  font-family: var(--mono); font-size: 11px; color: var(--green);
}
.linkish:hover { color: var(--green-dark); text-decoration: underline; }

/* ---------- topbar tab nav (narrow) ---------- */
.tabs { display: none; gap: 2px; flex-wrap: wrap; }
.tab {
  height: 24px; padding: 0 10px;
  border: 1px solid transparent; border-radius: 3px;
  background: transparent; color: var(--ink);
  font: inherit; font-size: 12.5px; cursor: pointer;
  white-space: nowrap; flex-shrink: 0; text-decoration: none;
  display: inline-flex; align-items: center;
}
.tab:hover { text-decoration: none; color: var(--ink); border-color: var(--rule); }
.tab.on {
  border-color: var(--ink); background: var(--ink); color: #fff; font-weight: 600;
}
.tab.on:hover { color: #fff; }

/* ---------- screen + status ---------- */
.screen { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.screen-scroll { flex: 1; overflow: auto; }
.statusbar {
  display: flex; align-items: center; gap: 16px;
  min-height: 26px; flex: 0 0 auto; flex-wrap: wrap;
  padding: 2px 12px;
  border-top: 1px solid var(--rule); background: var(--surface-2);
  font-family: var(--mono); font-size: 10.5px; color: var(--muted);
}

/* ---------- the 40px bar at the head of every screen ---------- */
.bar {
  display: flex; align-items: center; gap: 10px;
  min-height: 40px; flex: 0 0 auto; flex-wrap: wrap; row-gap: 6px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--rule); background: var(--surface-2);
}
.bar h1 { font-size: 13px; font-weight: 700; white-space: nowrap; flex-shrink: 0; margin: 0; }
.bar-note {
  font-family: var(--mono); font-size: 11px; color: var(--muted);
  white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
/* The listing run carries three buttons where every other screen carries one
   or two, and at 1000px — the width the README animation and the landing hero
   are both captured at — the third wrapped the primary onto a row of its own,
   orphaned from the pair it belongs to. Wrapping is the wrong give here: the
   note is repeated verbatim in the status bar along the bottom of the same
   screen, so truncating it costs nothing, and `.bar-note` already carries the
   ellipsis to do it. Items only shrink after wrapping, so turning the wrap off
   is what lets the shrink happen at all. */
.bar-nowrap { flex-wrap: nowrap; }
.bar-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

/* ==========================================================================
   Controls
   ========================================================================== */
.btn {
  height: 26px; padding: 0 10px;
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); color: var(--ink);
  font: inherit; font-size: 12px; cursor: pointer;
  white-space: nowrap; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none;
}
.btn:hover { border-color: var(--ink); text-decoration: none; color: var(--ink); }
.btn-primary {
  padding: 0 12px;
  border-color: var(--green); background: var(--green); color: #fff; font-weight: 600;
}
.btn-primary:hover { background: var(--green-dark); border-color: var(--green-dark); color: #fff; }
.btn-dark {
  padding: 0 8px; height: 24px;
  border-color: var(--ink); background: var(--ink); color: #fff;
  font-size: 11px; font-weight: 600;
}
.btn-dark:hover { background: #000; color: #fff; }
.btn-tall { height: 32px; font-size: 13px; }
.btn-block { flex: 1; }
.btn:disabled { opacity: .5; cursor: default; }
/* Disabled-because-busy and disabled-because-there-is-nothing-yet want
   different paint, and the blanket rule above only suits the first. A primary
   button is white on colour, so .5 opacity fades the label and the fill toward
   the same page white and the words land at 2.3:1. That is fine on
   `Importing…`, which is a button reporting on itself: it was just pressed,
   and it is read by its position and by what the seller did a second ago.
   It is not fine on a button that *is* the instruction. `List selected` at
   rest is the only thing on the inventory bar saying that picking rows leads
   anywhere, and it has to be readable before anything has been clicked. A
   disabled control is exempt from the contrast floors, but the exemption
   assumes the label is not doing any work; here it is doing all of it. */
.btn-primary.btn-idle:disabled {
  opacity: 1;
  background: var(--surface); border-color: var(--rule); color: var(--faint);
}
.btn-quiet {
  border: none; background: none; color: var(--faint); cursor: pointer;
  font-family: var(--mono); font-size: 10.5px; padding: 0; height: auto;
}
.btn-quiet:hover { color: var(--warn); border: none; }

.chip {
  height: 26px; padding: 0 9px;
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); color: var(--ink);
  font-family: var(--mono); font-size: 11px; cursor: pointer;
  display: inline-flex; align-items: center; text-decoration: none;
}
.chip:hover { border-color: var(--ink); text-decoration: none; color: var(--ink); }
.chip.on { border-color: var(--ink); background: var(--ink); color: #fff; }
.chip-sm { height: 20px; padding: 0 5px; border-radius: 2px; font-size: 9.5px; color: var(--muted); }
.chip-sm.on { color: #fff; }
/* A finish the catalogue has no printing for. Styled in both states because
   this is the one chip that has to read as "off-catalogue" while selected —
   selected is exactly when it is doing damage. */
.chip.unpriced { border-color: var(--warn-line); background: var(--warn-tint); color: var(--warn); }
.chip.unpriced.on { border-color: var(--warn); background: var(--warn); color: #fff; }
.chip-filter {
  height: 24px; padding: 0 9px;
  font-family: var(--sans); font-size: 11.5px; white-space: nowrap;
}

/* ---------- the filter row under the inventory bar ---------- */
/* A row of its own rather than more controls in the bar: the bar already wraps
   at 1280 with a search box and two buttons in it, and five filters in there
   pushed the primary button onto a second line at every width worth caring
   about. Separated, the bar stays one line and the filters get to wrap among
   themselves. */
.filters {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
  flex: 0 0 auto; padding: 6px 12px;
  border-bottom: 1px solid var(--rule); background: var(--surface);
}
/* `[hidden]` loses to a `display` set on the same element, and every control
   in this row has one. The folded filters are hidden with the attribute so
   "More filters" has exactly one thing to toggle, so it has to win here. */
.filters [hidden] { display: none; }

.filter { position: relative; flex: 0 0 auto; }
.filter-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 26px; padding: 0 8px;
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); color: var(--ink);
  font-size: 11.5px; white-space: nowrap; cursor: pointer;
  list-style: none;
}
.filter-btn::-webkit-details-marker { display: none; }
.filter-btn:hover, .filter[open] .filter-btn { border-color: var(--ink); }
.filter-label {
  font-family: var(--mono); font-size: 9.5px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint);
}
/* The whole point of a menu over a row of chips is that it states its own
   state when shut, so the value carries the weight and the unfiltered case
   stays quiet. A picked filter has to be legible from across the row. */
.filter-value {
  color: var(--muted); max-width: 170px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.filter-value.on { color: var(--ink); font-weight: 600; }
.filter-caret { color: var(--faint); font-size: 9px; }

.filter-menu {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 30;
  min-width: 210px; max-width: 340px; max-height: 320px; overflow-y: auto;
  padding: 4px;
  border: 1px solid var(--edge); border-radius: 3px; background: var(--surface);
  box-shadow: 0 6px 18px rgba(0, 0, 0, .10);
}
.filter-opt {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 7px; border-radius: 2px;
  font-size: 12px; color: var(--ink); text-decoration: none;
}
/* Holds its width empty, so the labels line up in one column whether or not
   anything above them is ticked. */
.filter-tick { flex: 0 0 9px; font-size: 9px; color: var(--green); }
.filter-opt:hover { background: var(--surface-3); color: var(--ink); text-decoration: none; }
.filter-opt.is-on { background: var(--green-rail); color: var(--green); font-weight: 600; }
.filter-opt-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filter-note {
  margin-top: 3px; padding: 5px 7px 3px; border-top: 1px solid var(--rule-soft);
  font-family: var(--mono); font-size: 9.5px; color: var(--faint); line-height: 1.4;
}
/* Dashed, because it is the one control in this row that filters nothing —
   it only admits that the row is not all of itself. */
.filter-more {
  height: 26px; padding: 0 8px; cursor: pointer;
  border: 1px dashed var(--rule); border-radius: 3px; background: none;
  font: inherit; font-size: 11.5px; color: var(--muted); white-space: nowrap;
}
.filter-more:hover { border-color: var(--ink); color: var(--ink); }

/* Dimmed against its own label so the count reads as an annotation on the
   value rather than as part of it — "Base Set 12", not a set called that. */
.facet-n { color: var(--faint); margin-left: 4px; }
.chip.on .facet-n { color: rgba(255, 255, 255, .65); }
.facet-clear {
  font-family: var(--mono); font-size: 10px; color: var(--faint);
  margin-left: auto;
}
.facet-clear:hover { color: var(--warn); text-decoration: none; }

/* The paging control under a table.
   Centred rather than left-aligned: it is the end of the rows, not another
   filter, and sitting under the first column it reads as one. Sticky-free and
   inside the scroller on purpose — a pager pinned to the viewport claims the
   table continues past it, which is the one thing it is there to deny. */
.pager {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 3px; padding: 14px 12px 22px;
}
/* Present but dead at the ends of the run. Absent instead, the row reflows by
   one chip width every time you reach the first or last page, and the number
   under the cursor moves out from under it. */
.pager .is-off { opacity: 0.35; cursor: default; }
.pager-gap {
  font-family: var(--mono); font-size: 9.5px; color: var(--faint);
  padding: 0 3px;
}

/* The offer to select past this page. Tinted rather than plain, because it
   appears between the filter row and the table and has to read as a statement
   about the selection rather than as another row of controls. It is only ever
   present when the page is already fully ticked, so it never pushes the table
   down at rest. */
.selbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding: 7px 12px; border-bottom: 1px solid var(--rule);
  background: var(--green-tint); font-size: 12px;
}
.selbar .linkish { font-size: 12px; }

/* A sortable heading. Inherits the heading's own type so the table head does
   not gain a second style — the only thing that marks it as a control is the
   cursor and the arrow on the one in force. */
.sortlink { color: inherit; font: inherit; letter-spacing: inherit; text-decoration: none; }
.sortlink:hover { color: var(--ink); text-decoration: none; }
.sortlink.on { color: var(--ink); }

/* The one item in a nowrap bar that may give, and the one that takes what is
   left over. It is how a seller reaches a card in an inventory too large to
   scroll, so it gets the width; everything else on that bar is a count or a
   button, and a count that shrinks truncates the thing it exists to show. */
.searchform { min-width: 0; flex: 1 1 320px; }
.searchform .search { width: 100%; flex-shrink: 1; }
.search {
  width: 240px; flex-shrink: 0; height: 26px; padding: 0 8px;
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); font-size: 12px;
}
.searchbox { position: relative; display: flex; align-items: center; }
.searchbox .search { padding-left: 27px; }
.searchbox-icon {
  position: absolute; left: 8px; width: 13px; height: 13px; pointer-events: none;
  fill: none; stroke: var(--faint); stroke-width: 1.6; stroke-linecap: round;
}
.searchbox:focus-within .searchbox-icon { stroke: var(--muted); }
/* Room for the Clear link, and only when there is one — an input permanently
   indented for a control that is usually absent looks like a bug. */
.searchbox:has(.searchbox-clear) .search { padding-right: 50px; }
.searchbox-clear {
  position: absolute; right: 4px; height: 20px; padding: 0 6px; border-radius: 2px;
  display: inline-flex; align-items: center;
  font-family: var(--mono); font-size: 10px; color: var(--faint);
}
.searchbox-clear:hover { color: var(--warn); background: var(--surface-3); text-decoration: none; }
/* The browser's own clear button sits exactly where ours does and means
   something else: it empties the field and leaves the screen still filtered
   by whatever was in it. */
.search::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

.pill {
  font-family: var(--mono); font-size: 10.5px;
  padding: 1px 5px; border-radius: 2px;
  border: 1px solid var(--rule); background: var(--surface-2); color: var(--muted);
}
.pill.ok { border-color: var(--green-line); background: var(--green-rail); color: var(--green); }
.pill.warn { border-color: var(--warn-line); background: var(--warn-tint); color: var(--warn); }
.pill.demo { border-color: var(--warn-line); background: var(--warn-tint); color: var(--warn); }

.field-label {
  font-family: var(--mono); font-size: 10px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint);
}
.sec-label {
  font-family: var(--mono); font-size: 10px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint); margin-bottom: 8px;
}

/* ==========================================================================
   Import screen
   ========================================================================== */
.split { display: flex; flex-wrap: wrap; flex: 1; min-height: 0; overflow: auto; }
.pane-import {
  /* A fixed column rather than a percentage. As a percentage it took a share
     of every extra pixel, so the queue — which has eight columns to fit and
     actually needs the room — never gained any. The controls in here do not
     get better with more width. */
  width: 400px; flex: 0 1 400px; min-width: 300px;
  border-right: 1px solid var(--rule);
  display: flex; flex-direction: column; padding: 14px; gap: 12px;
  background: var(--surface); overflow: auto;
}
.pane-queue {
  flex: 1 1 360px; min-width: 330px; min-height: 0;
  display: flex; flex-direction: column; overflow: auto;
}
.pane-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
}
.pane-head b { font-weight: 700; font-size: 14px; white-space: nowrap; }

.drop {
  min-height: 190px;
  border: 1px dashed var(--edge); border-radius: 4px;
  background: repeating-linear-gradient(135deg, #f3f3f0 0 6px, #f8f8f6 6px 12px);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; cursor: pointer;
  transition: border-color .12s, background .12s;
}
.drop:hover, .drop.over { border-color: var(--green); background: var(--green-tint); }
/* Three cards, fanned. It used to be one plain rectangle, which read as a
   white box rather than as anything you would sleeve: nothing about it said
   "card", and at 40x52 it was squarer than the 1:1.4 a real one is.

   The two cards behind are pseudo-elements. The front one has to be a real
   child, because a pseudo-element always paints over its parent's background
   and there is no way to bring the parent in front of its own pseudos without
   a negative z-index that would also drop it behind the drop zone.

   Shared with the file chip, which showed the same plain rectangle the drop
   zone was given this treatment to get rid of — one mark for "card scans",
   before and after a file is chosen. */
.cardfan {
  position: relative; width: 42px; height: 58px; flex: 0 0 auto;
}
.cardfan::before, .cardfan::after, .cardfan > i {
  content: ""; position: absolute; inset: 0;
  border: 1px solid var(--edge); border-radius: 3px; background: var(--surface);
}
.cardfan::before { transform: rotate(-9deg) translate(-4px, 2px); }
.cardfan::after { transform: rotate(9deg) translate(4px, 2px); }
/* An art window and two lines of text: enough of a card face to be read as
   one at 42px, without pretending to be any card in particular. */
.cardfan > i {
  /* ::after paints after child content, so without this the right-hand card
     of the fan covers the face of the one meant to be in front. */
  display: block; z-index: 1;
  background:
    linear-gradient(var(--rule), var(--rule)) 5px 5px / 30px 23px no-repeat,
    linear-gradient(var(--rule-soft), var(--rule-soft)) 5px 33px / 30px 3px no-repeat,
    linear-gradient(var(--rule-soft), var(--rule-soft)) 5px 39px / 19px 3px no-repeat,
    var(--surface);
}
/* Half size, for the file chip. The two text lines go: at 24px they are a
   pair of grey smudges rather than lines of text, and the art window alone
   still reads as a card face. Everything else scales from the same
   proportions, so the two marks are recognisably the same object. */
.cardfan-sm { width: 24px; height: 33px; }
.cardfan-sm::before { transform: rotate(-9deg) translate(-2px, 1px); }
.cardfan-sm::after { transform: rotate(9deg) translate(2px, 1px); }
.cardfan-sm > i {
  border-radius: 2px;
  background:
    linear-gradient(var(--rule), var(--rule)) 3px 3px / 16px 13px no-repeat,
    linear-gradient(var(--rule-soft), var(--rule-soft)) 3px 19px / 16px 2px no-repeat,
    var(--surface);
}
.drop-title { font-size: 13px; font-weight: 600; }
.drop-sub {
  font-family: var(--mono); font-size: 11px; color: var(--muted);
  text-align: center; line-height: 1.5;
}
.drop .btn-choose {
  height: 28px; padding: 0 12px;
  border: 1px solid var(--ink); border-radius: 3px; background: var(--surface);
  font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap;
}
.drop .btn-choose:hover { background: var(--ink); color: #fff; }

.filechip {
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface-2); padding: 10px;
}
/* The fan overhangs its own box by a few pixels either side, so the row pads
   for it rather than letting the left-hand card sit under the chip's edge. */
.filechip-row { display: flex; align-items: center; gap: 12px; padding-left: 3px; }
.filechip-body { flex: 1; min-width: 0; }
.filechip-name {
  font-weight: 600; font-size: 12.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.filechip-meta {
  font-family: var(--mono); font-size: 10.5px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.track {
  margin-top: 10px; height: 4px; border-radius: 2px;
  background: var(--track); overflow: hidden;
}
.track i { display: block; height: 4px; background: var(--green); width: 0; transition: width .2s; }
/* Indeterminate: work that is genuinely happening but has nothing to count
   yet — the archive is being unpacked and nobody knows how many scans are in
   it. Parking the bar at 0% instead says no progress has been made, which
   directly after an upload bar reached 100% reads as a stall. The width comes
   from here rather than from the inline style the script sets, so the sweep
   has something to move. */
.track.busy > i { width: 34%; animation: track-sweep 1.15s ease-in-out infinite; }
@keyframes track-sweep {
  from { transform: translateX(-105%); }
  to { transform: translateX(300%); }
}
@media (prefers-reduced-motion: reduce) {
  /* Still distinct from a stalled 0%, without anything moving. */
  .track.busy > i { width: 100%; animation: none; opacity: .4; }
}
.track-labels {
  /* `align-items: baseline` and a gap, because the left label is a stage name
     most of the time and a wrapped two-line error the rest of it — without
     them the percentage sat inside the first line of the message. */
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 10px; margin-top: 5px;
  font-family: var(--mono); font-size: 10.5px; color: var(--muted);
}
.track-labels > :first-child { min-width: 0; }
.track-labels > :last-child { flex: 0 0 auto; }

.tiles-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.tile-sm {
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); padding: 8px;
}
.tile-sm .k {
  font-family: var(--mono); font-size: 9.5px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint);
}
.tile-sm .v { font-size: 20px; font-weight: 700; letter-spacing: -0.02em; }

.settings { border-top: 1px solid var(--rule); padding-top: 10px; }
.settings-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.settings-row .field-label { margin-bottom: 4px; text-transform: uppercase; }
.chip-set { display: flex; gap: 3px; }
.matcher {
  font-family: var(--mono); font-size: 10.5px; color: var(--muted);
  margin-top: 10px; line-height: 1.6;
}
.matcher .strong { color: var(--green); }
.setting-note { color: var(--muted); font-size: 11.5px; margin-top: 10px; line-height: 1.5; }
.setting-list { margin: 6px 0 0; padding-left: 16px; }
.setting-list li { margin-top: 3px; }

/* The prose under the match settings answers two questions that only need
   answering once — why finish is yours to set, and what auto-accept actually
   checks. Fourteen lines of it sat between the chips and "Recent imports",
   so the answer to "did my last import work" was below the fold on a laptop.
   Kept reachable rather than resident. */
.why { margin-top: 10px; }
.why > summary {
  font-family: var(--mono); font-size: 10px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint);
  display: flex; align-items: center; gap: 6px;
  cursor: pointer; user-select: none;
  /* Both are needed: the second is the only one Safari listens to. */
  list-style: none;
}
.why > summary::-webkit-details-marker { display: none; }
.why > summary::before { content: "+"; font-size: 12px; line-height: 1; }
.why[open] > summary::before { content: "\2212"; }
.why > summary:hover { color: var(--muted); }
.why .setting-note:first-of-type { margin-top: 6px; }

/* ---------- the match queue ---------- */
/* Not sticky, though it wants to be. `#queue` carries `overflow: auto` and so
   claims the sticky scrollport, but it is never the box that actually scrolls
   — `.pane-queue` grows to its content and `.split` scrolls the pair of panes.
   A sticky heading therefore pins to a scrollport that never moves and slides
   away with the rows, which is worse than not trying: it looks broken rather
   than absent. Making it work means constraining the pane's height, and that
   is a change to the whole screen's scrolling model. */
.qgroup-head {
  display: flex; align-items: baseline; gap: 8px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--rule);
  background: var(--surface-2);
  cursor: pointer; user-select: none;
  /* Both are needed: the second is the only one Safari listens to. */
  list-style: none;
}
.qgroup-head::-webkit-details-marker { display: none; }
/* A shut section's heading is a link, because opening it loads that batch's
   cards. Styled as the open one's `<summary>` so the two read as one list:
   what tells them apart is the marker below, not the colour of the text. */
a.qgroup-head { color: inherit; text-decoration: none; }
/* The same `+` / `−` the settings panel discloses itself with, rather than a
   triangle. Not decoration: this stylesheet already proves those two glyphs
   render in the shipped webfonts, and a marker that lands as a tofu box is a
   control nobody can read. */
.qgroup-head::before {
  content: "+"; font-family: var(--mono); font-size: 12px;
  line-height: 1; color: var(--faint);
}
.qgroup[open] > .qgroup-head::before { content: "\2212"; }
.qgroup-head:hover { background: var(--surface-3); }
.qgroup-head:hover .qgroup-file { color: var(--muted); }
.qgroup + .qgroup .qgroup-head { border-top: 1px solid var(--rule); }
.qgroup-file {
  /* Truncated by the column, not by a slice — same rule as the recent-imports
     table. The full name stays on the title attribute. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: 600; font-size: 12px;
}
.qgroup-meta {
  flex: 0 0 auto; margin-left: auto;
  font-family: var(--mono); font-size: 10px; color: var(--faint);
}
.qrow {
  /* `flex-wrap: wrap` means items wrap when their *base* sizes overflow —
     shrinking only happens afterwards, within a line. So a generous
     `flex-basis` on the name column, not a generous `min-width`, is what used
     to push the actions onto a second row. Sized to fit a 1280 laptop with the
     arrow hidden. */
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px; row-gap: 8px;
  padding: 8px 12px; border-bottom: 1px solid var(--rule-soft);
  background: var(--surface);
}
.qrow.review { background: var(--warn-tint); }
.thumb {
  width: 46px; height: 64px; flex: 0 0 46px;
  border: 1px solid var(--rule); border-radius: 2px;
  background: var(--scan-hatch); position: relative; overflow: hidden;
}
.thumb img { width: 100%; height: 100%; object-fit: contain; display: block; background: var(--surface); }
.thumb-tag {
  position: absolute; bottom: 2px; left: 2px;
  font-family: var(--mono); font-size: 8px; color: var(--faint);
}
.thumb.ref { background: var(--ref-hatch); }
/* The catalogue's guess sits next to the seller's photograph at the same size.
   Unlabelled, a wrong match reads as "my scan turned into a Pokemon card"
   rather than "the catalogue thinks this is a Pokemon card" — which is how the
   cross-game matching bug was first reported. */
.thumb.ref .thumb-tag { color: var(--green); opacity: .75; }
.qname.faint { color: var(--faint); font-weight: 500; }
.thumb.none { border-style: dashed; background: var(--surface-2); }
.qarrow { width: 16px; flex: 0 0 16px; color: var(--edge); text-align: center; }
.qfile {
  width: 76px; flex: 0 1 76px; min-width: 0;
  font-family: var(--mono); font-size: 10.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Basis at the minimum, not above it. Flex wraps on base sizes, so a basis
   wider than the column can actually be narrowed to buys nothing at full
   width — grow takes the slack either way — and costs a breakpoint's worth
   of room at the bottom of the range. */
.qmain { flex: 1 1 104px; min-width: 104px; }
.qname {
  font-size: 13px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qmeta {
  font-family: var(--mono); font-size: 10.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qalt {
  display: flex; align-items: baseline; gap: 8px; min-width: 0;
  font-family: var(--mono); font-size: 10.5px; color: var(--faint);
}
.qalt button {
  border: none; background: none; padding: 0; cursor: pointer;
  font: inherit; color: var(--faint); text-align: left;
}
.qalt button:hover { color: var(--ink); }
/* The runner-up is the long half and truncates; "wrong card?" is the short
   half and must never be the thing that disappears — it is the only exit from
   a match that is wrong about all five candidates. */
.qalt-swap, .qalt-note { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qalt-fix {
  flex: 0 0 auto; text-decoration: underline; text-underline-offset: 2px;
  text-decoration-style: dotted;
}

/* ---------- correcting a bad match ---------- */
.qfix {
  flex: 1 1 100%; margin-top: 8px; padding: 10px 12px;
  border: 1px solid var(--rule); border-radius: 3px; background: var(--surface-2);
}
.qfix-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.qfix-title { font-size: 12px; font-weight: 600; }
.qfix-head .btn-quiet { margin-left: auto; }
.qfix-label {
  font-family: var(--mono); font-size: 10px; color: var(--faint);
  text-transform: uppercase; letter-spacing: .04em; margin: 10px 0 5px;
}
.qfix-label:first-of-type { margin-top: 0; }
.qfix-search { display: flex; gap: 6px; }
.qfix-search input { flex: 1 1 auto; min-width: 0; }
.qfix-search select { flex: 0 0 150px; }
.qfix-list {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); gap: 4px;
  max-height: 260px; overflow: auto;
}
.qfix-card {
  display: flex; align-items: center; gap: 8px; text-align: left;
  padding: 4px 6px; border: 1px solid transparent; border-radius: 3px;
  background: none; cursor: pointer; font: inherit; color: var(--ink);
}
.qfix-card:hover { border-color: var(--edge); background: var(--surface); }
.qfix-card .thumb { width: 30px; height: 42px; flex: 0 0 30px; }
.qfix-body { min-width: 0; flex: 1 1 auto; }
.qfix-name {
  display: block; font-size: 12px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qfix-var { font-weight: 500; color: var(--muted); }
.qfix-sub {
  display: block; font-family: var(--mono); font-size: 10px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qfix-note { flex: 0 0 auto; font-family: var(--mono); font-size: 10.5px; color: var(--faint); }
.qfix-none {
  grid-column: 1 / -1; padding: 10px 2px;
  font-family: var(--mono); font-size: 10.5px; color: var(--faint);
}
.qconf { width: 68px; flex: 0 0 68px; }
.qconf .track { margin-top: 0; }
.qconf .track i.low { background: var(--warn); }
.qconf .lbl {
  font-family: var(--mono); font-size: 10.5px; color: var(--muted); margin-top: 3px;
}
/* A row corrected by hand, where the match bar used to be. The encoder had a
   score; a person does not. Sized and coloured to sit as quietly as the
   percentage it replaces — this marks a decision, it does not raise an alarm. */
.qchose { display: flex; align-items: center; height: 100%; color: var(--muted); }
.qchose svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
/* Five chips, not four. DMG was sliced off the end to save room, which left
   the one condition the seller is most likely to be looking at a scan of
   choosable in the import defaults and on the card page but not here — so a
   damaged card could only be marked damaged after it was already inventory.

   The fifth chip is paid for out of the other four rather than out of the
   row: at 1280 the row has about 133px to give this column before the
   actions wrap onto a line of their own, and five chips at the default
   padding want 138. Tighter padding and a 2px gap bring them to 124. */
.qcond { width: 128px; flex: 0 0 128px; display: flex; gap: 2px; }
.qcond .chip-sm { padding: 0 4px; }
.qactions {
  flex: 0 1 auto; display: flex; gap: 4px; align-items: center; justify-content: flex-end;
}
.queue-empty {
  padding: 44px; text-align: center;
  font-family: var(--mono); font-size: 11px; color: var(--faint);
}
/* ==========================================================================
   Hover preview
   --------------------------------------------------------------------------
   One shared element, `position: fixed`, moved and re-pointed by JS.

   Not a CSS `:hover` popup inside the thumbnail, which is the obvious way to
   build this and cannot work here: every thumbnail lives inside a pane with
   `overflow: auto`, and an absolutely-positioned child of a scrolling
   container is clipped at its edge. The preview would be cut in half for every
   row near the boundary — which is most of them.
   ========================================================================== */
.zoom {
  position: fixed; z-index: 50; pointer-events: none;
  opacity: 0; visibility: hidden;
  transition: opacity .09s ease-out;
  background: var(--surface);
  border: 1px solid var(--rule); border-radius: 4px;
  box-shadow: 0 8px 28px rgba(23, 23, 26, 0.22);
  padding: 4px;
}
.zoom.on { opacity: 1; visibility: visible; }
.zoom img {
  display: block; width: 320px; height: auto; border-radius: 2px;
  background: var(--surface-3);
}
.zoom .zoom-cap {
  font-family: var(--mono); font-size: 10px; color: var(--muted);
  padding: 4px 2px 1px; max-width: 320px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The cursor affordance: these are the only images worth looking at closely. */
.thumb img, .thumb-xs img { cursor: zoom-in; }

@media (hover: none) {
  /* No hover on touch, so the preview would either never fire or stick after a
     tap. Left off entirely rather than half-working. */
  .zoom { display: none; }
}

/* ==========================================================================
   Dense tables
   ========================================================================== */
.dtable {
  width: 100%; border-collapse: collapse;
  font-family: var(--mono); font-size: 11.5px;
}
.dtable th {
  text-align: left; font-weight: 500; color: var(--muted);
  padding: 5px 10px; border-bottom: 1px solid var(--rule);
  position: sticky; top: 0; background: var(--surface-3); z-index: 1;
  white-space: nowrap;
}
.dtable td { padding: 5px 10px; white-space: nowrap; }
.dtable tbody tr { border-bottom: 1px solid var(--rule-soft); }
.dtable tbody tr.sel { background: var(--green-tint); }
.dtable .r { text-align: right; }
.dtable .name { font-family: var(--sans); font-size: 12.5px; }
.dtable .muted { color: var(--muted); }
.dtable .faint { color: var(--faint); }
.dtable .pos { color: var(--green); }
.dtable .strong { font-weight: 700; }
.dtable .cbcol { width: 30px; }
.dtable .refcol { padding: 4px 8px; }
.dtable .neg { color: var(--warn); }
/* One cell absorbs the slack and truncates; the rest size to their content, so
   the columns line up down the table. `max-width: 0` is what makes it work —
   an auto table gives a cell its content's width unless told otherwise, and
   `overflow: hidden` on something already wide enough never clips. */
.dtable .ell { max-width: 0; width: 100%; overflow: hidden; text-overflow: ellipsis; }

/* Same table in a sidebar: tighter, flush to the column edges, and with the
   sticky header switched off — .pane-import scrolls, so a header that stuck
   would float over the settings above it. */
.dtable.compact { font-size: 10.5px; }
.dtable.compact th, .dtable.compact td { padding: 4px 6px; }
.dtable.compact th { position: static; background: none; }
.dtable.compact th:first-child, .dtable.compact td:first-child { padding-left: 0; }
.dtable.compact th:last-child, .dtable.compact td:last-child { padding-right: 0; }
.thumb-xs {
  /* `display` is not optional here. This is a <span>, so without it the box is
     inline, width/height/overflow are all ignored, and the reference image
     renders at its natural 672x936. It only ever worked by accident, as a flex
     item of a wrapper that has since gone. */
  display: block;
  width: 24px; height: 33px; flex: none;
  border: 1px solid var(--rule); border-radius: 2px;
  background: repeating-linear-gradient(135deg, #efefec 0 4px, #f7f7f5 4px 8px);
  overflow: hidden;
}
.thumb-xs img { width: 100%; height: 100%; object-fit: contain; display: block; background: var(--surface); }
.thumb-xs.ref { background: repeating-linear-gradient(135deg, #e9efeb 0 4px, #f3f7f4 4px 8px); }
.thumb-xs.none { border-style: dashed; background: var(--surface-2); }

/* ==========================================================================
   Card detail drawer
   ========================================================================== */
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: 380px; max-width: 100%;
  z-index: 40; background: var(--surface);
  border-left: 1px solid var(--rule);
  box-shadow: -8px 0 24px rgba(23, 23, 26, 0.10);
  display: flex; flex-direction: column;
}
.drawer[hidden] { display: none; }
.drawer-body { flex: 1; overflow: auto; padding: 14px; }

.panel-head-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;
  padding-bottom: 10px; border-bottom: 1px solid var(--rule-soft); margin-bottom: 12px;
}
.panel-name { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.panel-sub { font-family: var(--mono); font-size: 10.5px; color: var(--muted); margin-top: 2px; }
.panel-head-row .btn-quiet { font-size: 16px; line-height: 1; }

.panel-imgs { display: flex; gap: 8px; margin-bottom: 14px; }
.panel-thumb { width: 104px; height: 145px; flex: 0 0 104px; }

.panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; }
.panel-grid .field { margin-bottom: 0; }
.drawer .chip-set { flex-wrap: wrap; gap: 3px; }

.panel-facts {
  border: 1px solid var(--rule-soft); border-radius: 3px;
  background: var(--surface-2); padding: 8px 10px; margin-bottom: 12px;
  font-family: var(--mono); font-size: 11px;
}
/* `space-between` alone leaves no gap the moment the value is wide enough to
   fill the row, and a two-printing line does exactly that: "Printing" and
   "1 1st Edition Holofoil, 1 Normal" ran together into "Printing1 1st Edition"
   with no space at all. Found in a landing-page screenshot, which is a
   humbling place to find it. The gap is the fix; `flex: none` stops the label
   itself being squeezed instead. */
.panel-facts div { display: flex; justify-content: space-between; gap: 12px; padding: 2px 0; }
.panel-facts span { color: var(--muted); flex: none; }
.panel-facts b { font-weight: 500; text-align: right; }

.panel-sold {
  border: 1px solid var(--rule-soft); border-radius: 3px;
  padding: 10px; margin-bottom: 12px;
}
.panel-sold.on { border-color: var(--green-line); background: var(--green-tint); }
.panel-note {
  font-family: var(--mono); font-size: 10px; color: var(--faint);
  line-height: 1.5; margin-top: 8px;
}
.panel-actions { display: flex; gap: 8px; margin-top: 4px; }
/* Destructive and separated from Save, because the two get clicked by the
   same hand in the same second. */
.btn-danger { border-color: var(--warn-line); color: var(--warn); }
.btn-danger:hover { border-color: var(--warn); background: var(--warn-tint); color: var(--warn); }

/* The name is the way into the detail; the rest of the row still selects. */
.rowname {
  border: none; background: none; padding: 0; cursor: pointer;
  font: inherit; color: var(--ink); text-align: left;
}
.rowname:hover { color: var(--green); text-decoration: underline; }
tr.is-sold { color: var(--faint); }
tr.is-sold .rowname { color: var(--faint); }
.dtable .foil-yes { color: var(--green); }
.batch { border-bottom: 1px solid var(--rule); }
.batch:last-child { border-bottom: none; }
.batch-head {
  display: flex; align-items: baseline; gap: 10px;
  padding: 8px 12px; background: var(--surface-2);
  border-bottom: 1px solid var(--rule-soft);
}
.batch-name { font-size: 12.5px; font-weight: 600; }
.batch-when, .batch-count {
  font-family: var(--mono); font-size: 10.5px; color: var(--muted);
}
/* ---------- printing picker ----------
   A list rather than chips: the options are long ("1st Edition Holofoil") and
   the number beside each is the point — a seller choosing between $10,000 and
   $855 is choosing on price, not on name. */
.printings { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; }
.printing {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  padding: 5px 8px; border: 1px solid var(--rule-soft); border-radius: 3px;
  background: var(--surface); cursor: pointer; font: inherit; color: var(--ink);
  text-align: left; width: 100%;
}
.printing:hover { border-color: var(--rule); }
.printing.on { border-color: var(--green-line); background: var(--green-tint); }
.printing-name { font-size: 12px; }
.printing-price { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.printing.on .printing-price { color: var(--green); }
.field-hint.warn, .panel-note.warn { color: var(--warn); }
.pill.warn { border-color: var(--warn-line); background: var(--warn-tint); color: var(--warn); }

/* ---------- price trend ---------- */
.trend {
  margin-top: 10px; padding: 10px;
  border: 1px solid var(--rule-soft); border-radius: 3px; background: var(--surface);
}
.trend-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.trend-title {
  font-family: var(--mono); font-size: 10px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint);
}
.trend-sub { font-family: var(--mono); font-size: 10px; color: var(--muted); }
.spark { display: block; width: 100%; overflow: visible; margin: 2px 0; }
.trend-figures {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px; margin-top: 4px;
}
.trend-now { font-size: 17px; font-weight: 700; letter-spacing: -0.02em; }
.trend-deltas { display: flex; gap: 8px; }
.delta {
  font-family: var(--mono); font-size: 10.5px; color: var(--faint);
  white-space: nowrap;
}
.delta.up { color: var(--green); }
.delta.down { color: var(--warn); }

.card-hero { width: 100%; height: auto; aspect-ratio: 5 / 7; overflow: visible; }
.card-hero img { width: 100%; height: 100%; object-fit: contain; }
.card-hero .thumb-tag {
  bottom: -14px; left: 0;
  font-family: var(--mono); font-size: 9.5px; color: var(--faint);
}
.qfoil { width: 58px; flex: 0 0 58px; display: flex; align-items: center; gap: 3px; }
.qambig { padding: 0 4px; cursor: help; }
.qprice { color: var(--ink); }
.qprice.warn { color: var(--warn); }
.qfoil .chip-sm { white-space: nowrap; }
/* A compact × rather than the word: the row has eight columns before it, and
   "remove" was the difference between the actions fitting and wrapping onto a
   line of their own. */
.qdrop {
  border: none; background: none; color: var(--faint); cursor: pointer;
  font-size: 15px; line-height: 1; padding: 0 2px;
}
.qdrop:hover { color: var(--warn); }
/* Foil is a toggle rather than a pair of chips: it has exactly two states and
   the row is already dense. `on` is whatever the import asked for, so the
   exception worth seeing is a row that has been changed away from it — which
   `on` used to get wrong for any batch imported as foil. */
.foil-toggle { width: 100%; }
.qfoil .chip-sm { width: 100%; justify-content: center; }

@media (max-width: 860px) {
  .drawer { width: 100%; border-left: none; border-top: 1px solid var(--rule); top: auto; height: 70vh; }
}

/* ==========================================================================
   Listings screen
   ========================================================================== */
.run-side {
  width: 250px; flex: 0 0 250px;
  border-right: 1px solid var(--rule); background: var(--surface);
  padding: 12px; overflow: auto;
}
.run-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.opt {
  display: flex; gap: 8px; align-items: flex-start;
  padding: 7px 8px; margin-bottom: 4px;
  border: 1px solid var(--rule-soft); border-radius: 3px;
  background: var(--surface); cursor: pointer;
  width: 100%; text-align: left; color: var(--ink); font: inherit;
}
.opt:hover { border-color: var(--rule); text-decoration: none; color: var(--ink); }
.opt.on { border-color: var(--green-line); background: var(--green-tint); }
.opt .t { font-size: 12.5px; font-weight: 500; }
.opt .s { font-family: var(--mono); font-size: 10px; color: var(--muted); }
.opt-body { flex: 1; min-width: 0; }
.run-summary {
  margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--rule-soft);
  font-family: var(--mono); font-size: 11px; color: var(--muted); line-height: 1.6;
}

/* The floor, which is a control rather than a caption. One row, because the
   two summary figures below it are one line each and a stacked label would
   read as a different kind of thing from them.

   The input is the flexible item and everything else is `flex: 0 0 auto`:
   this lives in a 250px sidebar, and a number field allowed to keep its
   default intrinsic width pushes the button out of the column entirely. */
.floor-set { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.floor-set label { flex: 0 0 auto; }
.floor-set input {
  flex: 1 1 auto; min-width: 0;
  height: 22px; padding: 0 5px;
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); color: var(--ink);
  font-family: var(--mono); font-size: 11px;
}
.floor-set input:focus { outline: none; border-color: var(--green); }
.floor-set .btn { flex: 0 0 auto; height: 22px; padding: 0 8px; font-size: 11px; }
/* Whether the floor on screen is the saved one. Wraps and is allowed to run
   to two lines — the alternative in this width is truncating the sentence
   that says a run is priced under a number the account did not set. */
.floor-note { margin-bottom: 6px; color: var(--faint); }
/* Full width and on its own line, like the TCGplayer download below it. This
   was a `linkish` span sitting inline after the sentence naming the saved
   floor, and the two ran together — "your floor is $0.35 Save $2.00 as my
   floor" reads as one confused sentence rather than a statement and an
   action. Nobody found it. */
.floor-save {
  width: 100%; height: 24px; margin-bottom: 10px;
  font-family: var(--mono); font-size: 11px;
}

/* The TCGplayer download, which is a round trip rather than a download: their
   own pricing export goes up so that the SKU ids in it can come back down. The
   file input is full width because a 250px sidebar truncates a filename
   otherwise, and a seller who cannot see which file they picked has no way to
   tell the pricing export from the order export. */
.match { margin-bottom: 4px; }
.match input[type="file"] {
  width: 100%; margin-bottom: 4px;
  font-family: var(--mono); font-size: 10px; color: var(--muted);
}
.match input[type="file"]::file-selector-button {
  height: 22px; padding: 0 8px; margin-right: 8px;
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); color: var(--ink);
  font: inherit; font-size: 11px; cursor: pointer;
}
.match input[type="file"]::file-selector-button:hover { border-color: var(--ink); }
.match .btn { width: 100%; }
.match .s {
  font-family: var(--mono); font-size: 10px; color: var(--faint);
  line-height: 1.6; margin-top: 6px;
}

/* The sets to filter that export to. Set names run long — "Strixhaven:
   Mystical Archive" is 28 characters in a 250px sidebar — so the pills wrap
   and are allowed to break rather than being truncated: a half-shown set name
   is one the seller cannot find in TCGplayer's filter, which is the only
   thing this list is for. */
.match-sets { border-top: 1px solid var(--rule); padding-top: 6px; }
.match-line { margin-top: 6px; }
.match-line b { display: block; color: var(--ink); font-weight: 400; }
.match-line .pill {
  display: inline-block; margin: 3px 3px 0 0;
  max-width: 100%; white-space: normal; overflow-wrap: anywhere;
}
.match-warn { margin-top: 6px; color: var(--warn); }

/* The upload's own progress. `.match-progress` deliberately sets no `display`
   — `[hidden]` is an attribute selector of the same specificity as a class, so
   a `display` here would beat it and the tiles would show before a file was
   picked, which is the bug the import screen already shipped once. */
.match-bar {
  height: 3px; border-radius: 2px; overflow: hidden;
  background: var(--rule); margin-top: 8px;
}
.match-bar i { display: block; height: 100%; width: 0; background: var(--green); }
.match-stat {
  font-family: var(--mono); font-size: 10px; color: var(--muted);
  line-height: 1.6; margin-top: 4px; overflow-wrap: anywhere;
}
.match-stat.bad { color: var(--warn); }

.run-summary b { color: var(--ink); font-weight: 400; }
.joblog {
  flex: 0 0 132px; border-top: 1px solid var(--rule);
  background: var(--surface-2); padding: 8px 12px; overflow: auto;
}
.joblog .line {
  font-family: var(--mono); font-size: 11px; color: var(--muted); line-height: 1.6;
}
.joblog .t { color: var(--faint); }

/* ==========================================================================
   Analytics screen
   ========================================================================== */
.stats-pad { flex: 1; overflow: auto; padding: 16px; }

/* The value threshold: which cards are worth counting towards a position.
   In the bar rather than the panel it affects, because it governs four
   figures across three panels and parking it beside any one of them would
   read as belonging to that one. */
.valgate { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.valgate label { font-family: var(--mono); font-size: 11px; color: var(--muted); white-space: nowrap; }
.valgate input {
  width: 80px; height: 22px; padding: 0 5px;
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); color: var(--ink);
  font-family: var(--mono); font-size: 11px;
}
.valgate input:focus { outline: none; border-color: var(--green); }
.valgate .btn { height: 22px; padding: 0 8px; font-size: 11px; }
/* Above the tiles rather than below them, because it is what the tiles mean.
   Read after the numbers it qualifies, it is a correction; read before, it is
   a heading. */
.valgate-note {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  margin-bottom: 10px;
  font-family: var(--mono); font-size: 11px; color: var(--muted);
}
.valgate-note .btn.linkish { font-size: 11px; }
/* What a figure left out, said where the figure is. Dimmer than `.s` and set
   apart from it, so a tile reads as a number, its own caption, and then the
   qualification — three lines of equal weight would make every tile look
   like a warning. */
.tile .s.left-out { color: var(--faint); margin-top: 6px; line-height: 1.35; }
.tile-grid {
  display: grid; grid-template-columns: repeat(4, minmax(150px, 1fr)); gap: 10px;
}
.tile {
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); padding: 12px;
}
.tile .k {
  font-family: var(--mono); font-size: 10px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint);
}
.tile .v { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; margin-top: 6px; }
.tile .s { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 2px; }
.chart-grid { display: grid; grid-template-columns: 1.1fr 1fr; gap: 10px; margin-top: 10px; }
.panel {
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); padding: 12px;
}
.panel-title {
  font-weight: 700; margin-bottom: 10px;
  display: flex; align-items: center; gap: 8px; justify-content: space-between;
}
.brow { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.brow .lbl { width: 104px; font-size: 12px; }
.brow .bartrack {
  flex: 1; height: 10px; background: #f0f0ec; border-radius: 2px; overflow: hidden;
}
.brow .bartrack i { display: block; height: 10px; background: var(--green); }
.brow .val {
  width: 60px; text-align: right;
  font-family: var(--mono); font-size: 11px; color: var(--muted);
}
.kv { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 11.5px; }
.kv tr { border-bottom: 1px solid var(--rule-soft); }
.kv td { padding: 6px 0; }
.kv td:first-child { color: var(--muted); }
.kv td:last-child { text-align: right; }

/* ==========================================================================
   Plugins screen
   ========================================================================== */
.plug-pad { flex: 1; overflow: auto; padding: 16px; display: flex; flex-direction: column; gap: 10px; }
.panel .note { color: var(--muted); font-size: 12px; margin-top: 10px; line-height: 1.5; }
.panel .dtable { margin-top: 2px; }
.panel .tile-grid + .dtable { margin-top: 12px; }

/* The plugins tables carry a cell that has no natural width limit — thirteen
   game names, or an exporter's whole description — and `.dtable td` is
   `nowrap`, so before these the table simply grew past its panel. At 1440,
   which is the width the screenshots are taken at, the last column of two of
   them sat off the side of the viewport: the Export buttons ended at x=1442
   and the ingest command at x=1653. Nothing clipped and nothing wrapped,
   because a table with no constrained cell has no slack to give. `.ell` is
   the fix (see its note above); the sub-line needs its own ellipsis because
   `text-overflow` applies to a box's own text, not to a child block's. */
.dtable .sub {
  font-family: var(--mono); font-size: 10px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A figure that is a problem rather than a quantity. Cards with no vectors
   cannot be matched at all and every scan of them silently misses; a price
   sync that has never run is losing history permanently rather than serving
   something stale. Both read as ordinary numbers in the column's muted grey. */
.dtable .bad { color: var(--warn); }

.tile .v.ok { font-size: 20px; color: var(--green); }
.tile .v.bad { font-size: 20px; color: var(--warn); }
.tile .v.small { font-size: 13px; font-weight: 500; margin-top: 10px; }
code {
  font-family: var(--mono); font-size: 11px; background: var(--surface-3);
  border: 1px solid var(--rule-soft); border-radius: 3px; padding: 1px 5px;
}
.empty {
  padding: 26px; text-align: center;
  font-family: var(--mono); font-size: 11px; color: var(--faint);
}
/* The no-selection state on /listings, which is a screen in its own right
   rather than a one-line apology: it is where a seller who clicked `Listings`
   in the nav lands, and the run they probably wanted has to be reachable from
   it. Generous padding because it sits alone in the table's scroll box. */
.empty-panel { padding: 56px 26px; }
.empty-panel p { margin: 0 0 10px; }
.empty-lead { font-size: 13px; color: var(--ink); }
.empty-actions {
  display: flex; gap: 8px; justify-content: center;
  margin-top: 18px; flex-wrap: wrap;
}

/* ==========================================================================
   Responsive — the mock's two breakpoints. Under 860 the rail becomes tabs in
   the topbar and the panes stack; under 620 everything is one column.
   ========================================================================== */
/* Between a 13" laptop and a desktop the queue row is the tightest thing on
   screen, and when it overflowed the actions wrapped onto a line of their own.
   The arrow is decoration and the filename is repeated on the card page, so
   they are what goes first — the scan, the match and the controls stay. */
@media (max-width: 1400px) {
  .qarrow { display: none; }
  /* The commit button is the point of this bar; a running total is not worth
     pushing it onto a line of its own. */
  .wide-only { display: none; }
}
/* 1280 and not 1200, which is where the filename used to go. Between the
   two the row was a few pixels short and the actions dropped onto a line of
   their own — visible before the condition chips grew by one, and wider
   after it. The filename is the cheapest thing on the row to lose. */
@media (max-width: 1280px) {
  .qfile { display: none; }
}

@media (max-width: 860px) {
  body.app { overflow-y: auto; height: auto; min-height: 100vh; }
  .rail { display: none; }
  .tabs { display: flex; }
  .shell { flex-direction: column; overflow: visible; }
  .split { flex-direction: column; flex: 1 1 auto; min-height: 0; overflow: visible; }
  .pane-import {
    width: 100%; flex: 0 0 auto;
    border-right: none; border-bottom: 1px solid var(--rule);
    padding: 12px; overflow: visible;
  }
  .pane-queue { width: 100%; flex: 1 1 auto; min-height: 320px; }
  .run-side {
    width: 100%; flex: 0 0 auto;
    border-right: none; border-bottom: 1px solid var(--rule); padding: 12px;
  }
  .run-body { width: 100%; flex: 1 1 auto; min-height: 300px; }
  .tile-grid { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
  .chart-grid { grid-template-columns: 1fr; }
  .dtable th { position: static; }
}
@media (max-width: 620px) {
  .tile-grid { grid-template-columns: 1fr; }
  .search { flex: 1 1 160px; min-width: 140px; width: auto; }
  .topbar-meta .wide { display: none; }
}
/* ==========================================================================
   Sign in / register
   ========================================================================== */
body.auth {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  padding: 24px; background: var(--paper);
}
.auth-card {
  width: 100%; max-width: 380px;
  border: 1px solid var(--rule); border-radius: 4px;
  background: var(--surface); padding: 24px;
}
.auth-card .brand { margin-bottom: 18px; }
.auth-card h1 { font-size: 19px; letter-spacing: -0.015em; margin-bottom: 6px; }
.auth-lede { color: var(--muted); font-size: 12.5px; line-height: 1.5; margin-bottom: 18px; }
.auth-error {
  border: 1px solid var(--warn-line); background: var(--warn-tint); color: var(--warn);
  border-radius: 3px; padding: 8px 10px; font-size: 12px; margin-bottom: 14px;
}
.field { display: block; margin-bottom: 14px; }
.field .field-label { display: block; margin-bottom: 4px; }
.input {
  width: 100%; height: 32px; padding: 0 9px;
  border: 1px solid var(--rule); border-radius: 3px;
  background: var(--surface); font-size: 13px; color: var(--ink);
}
.input:focus { outline: none; border-color: var(--green); }
.field-hint {
  display: block; margin-top: 4px;
  font-family: var(--mono); font-size: 10px; color: var(--faint);
}
.auth-alt { margin-top: 14px; font-size: 12.5px; color: var(--muted); }
.auth-foot {
  margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--rule-soft);
  display: flex; flex-direction: column; gap: 4px;
  font-family: var(--mono); font-size: 10px; color: var(--faint); line-height: 1.5;
}

/* ==========================================================================
   Landing page — the marketing front door, which keeps its own airier scale.
   ========================================================================== */
.btn-lg { height: 38px; padding: 0 20px; font-size: 14px; font-weight: 600; }
.dim { color: var(--faint); }
body.site { background: var(--paper); }
.site-nav {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 32px; border-bottom: 1px solid var(--rule); background: var(--surface);
  position: sticky; top: 0; z-index: 5; flex-wrap: wrap; gap: 12px;
}
.site-nav nav { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.site-nav nav a { color: var(--muted); text-decoration: none; font-size: 13.5px; }
.site-nav nav a:hover { color: var(--ink); }
.site-nav nav a.btn { color: #fff; }

/* Wider than the 780px of a text-only hero, because it now carries the demo
   and the demo is the argument. Top padding is smaller for the same reason:
   the old hero floated in whitespace left for an image that was never added. */
.hero { max-width: 1064px; margin: 0 auto; padding: 52px 32px 24px; text-align: center; }
.eyebrow {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.12em;
  color: var(--faint); margin-bottom: 18px;
}
.hero h1 {
  font-size: 44px; line-height: 1.08; letter-spacing: -0.025em;
  font-weight: 700; margin-bottom: 16px;
}
.lede { font-size: 17px; color: var(--muted); line-height: 1.55; max-width: 600px; margin: 0 auto; }
.hero-actions { display: flex; gap: 10px; justify-content: center; margin: 28px 0 14px; flex-wrap: wrap; }
.hero-actions a { text-decoration: none; }
.hero-note { font-size: 12.5px; color: var(--faint); }

/* Screenshots, framed rather than floated: a bare image on a page this plain
   reads as a stray file, and the frame says "this is the product".

   These replaced a single animated WebP in the hero. That file was 3.0 MB of a
   3.2 MB page and was the largest thing painted inside the fold, so the whole
   landing page's load was one image of an app doing something the reader had
   not yet been told the point of. It also carried its own copy of the nav bar,
   version number included, which meant it announced a stale build in 44px type
   for two days after a release. Stills cost about a fortieth as much, go out
   of date one screen at a time, and can be put where the sentence that makes
   the claim is — instead of all of them at once, up front, in a twenty-second
   loop nobody has agreed to watch. */
.shot { margin: 32px auto 0; max-width: 1000px; }
.shot img {
  display: block; width: 100%; height: auto; border: 1px solid var(--rule);
  border-radius: 8px; background: var(--surface);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 12px 32px rgba(0, 0, 0, 0.07);
}
.shot figcaption {
  margin-top: 12px; font-size: 12px; color: var(--faint);
  font-family: var(--mono); line-height: 1.5;
}
.shot-hero { margin-top: 34px; }

/* A phone renders a 1440px-wide screen at 358, which is a grey smudge rather
   than an argument. Rather than crop — the animation's answer, which needed a
   second pre-cropped asset to stay sharp — let it scroll: the image keeps its
   native proportions inside a box the reader can drag sideways, so the part
   that persuades is legible at a size a phone can actually show. */
@media (max-width: 640px) {
  .shot { overflow-x: auto; }
  .shot img { width: 720px; max-width: none; }
  .shot figcaption { position: sticky; left: 0; max-width: 100vw; }
}

.band { max-width: 980px; margin: 0 auto; padding: 54px 32px; }

/* A full-bleed tint whose contents still sit on an untinted band's left edge.
   That used to be `.band-tint > * { max-width: 980px; margin-inline: auto }`,
   which centres whatever it is applied to — so the children carrying a
   narrower measure of their own (.band-lede at 660px, .band-note at 700px)
   centred themselves *inside* the band while the heading beside them did not.
   One section, three different left edges: h2 at 230px, note at 370, lede at
   390. Doing the centring once on a wrapper means the measures run from the
   wrapper's edge, the way they do in every untinted band. */
.band-tint {
  max-width: none; background: var(--surface);
  border-block: 1px solid var(--rule); padding-inline: 0;
}
.band-inner { max-width: 980px; margin-inline: auto; padding-inline: 32px; }
.band-h { font-size: 24px; letter-spacing: -0.015em; margin-bottom: 12px; }
.band-lede { color: var(--muted); font-size: 15px; max-width: 660px; line-height: 1.55; }
.band-note { color: var(--muted); font-size: 13.5px; max-width: 700px; line-height: 1.6; margin-top: 16px; }

.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 28px; }
.step h3 { font-size: 15px; margin-bottom: 5px; }
.step p { color: var(--muted); font-size: 13.5px; line-height: 1.55; }
.step-n { font-family: var(--mono); font-size: 11px; color: var(--green); margin-bottom: 7px; }

.proof {
  margin-top: 24px; border: 1px solid var(--rule); border-radius: 8px;
  overflow: hidden; background: var(--surface-2);
}
.proof-row {
  display: grid; grid-template-columns: 1fr 96px 120px; gap: 14px;
  padding: 12px 15px; border-bottom: 1px solid var(--rule-soft); font-size: 13.5px;
  align-items: center;
}
/* The bar itself is `.track`, shared with the review queue, so the geometry
   and the fill colour cannot drift from the product. The label beneath it is
   restated because the queue's copy is scoped to `.qconf`; it is four
   declarations, and hoisting them would touch a screen this change has no
   business touching. */
.proof .conf .track { margin-top: 0; }
.proof .conf .lbl {
  display: block; margin-top: 4px;
  font-family: var(--mono); font-size: 10.5px; color: var(--muted);
}
.proof-head .pill { margin-left: 6px; }
/* The card beside its own name, the way the inventory table shows it. Reuses
   `.thumb-xs` so the frame is the product's, not a second opinion about how a
   card thumbnail should look. */
.proof-card { display: flex; align-items: center; gap: 10px; }
.proof-card .thumb-xs { flex: 0 0 auto; }
.proof-row:last-child { border-bottom: none; }
.proof-head { background: var(--surface-3); font-size: 10.5px; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--faint); font-weight: 650; }
.proof-row.alt { background: #fdf7f2; color: var(--warn); }
/* The costly row is tinted, but its bar stays green: both of these *are*
   confident matches, and that is the whole point — confidence does not tell
   the printings apart. A red bar here would argue the opposite. */
.proof-row.alt .lbl { color: var(--warn); }

@media (max-width: 620px) {
  /* Three columns will not fit a phone. The name gets the full width and the
     bar sits beside the price underneath it. */
  .proof-row { grid-template-columns: 1fr 110px; gap: 8px 14px; }
  .proof-row > span:first-child { grid-column: 1 / -1; }
  .proof-head > span:not(:first-child) { display: none; }
}
.proof-row .num { font-family: var(--mono); text-align: right; }

.code-block {
  margin-top: 22px; background: var(--ink); color: #e9e9e4;
  border-radius: 8px; padding: 18px 20px; overflow-x: auto;
  font-family: var(--mono); font-size: 12.5px; line-height: 1.7;
}
.code-block code { background: none; border: none; color: inherit; padding: 0; }

/* Says which of the numbers above are measured and which are illustrative.
   The house rule is that numbers are real or visibly marked, and a table
   styled like application output is exactly where that matters. */
.proof-note { margin-top: 10px; font-size: 12px; color: var(--faint); font-family: var(--mono); }

.two-up { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 26px 30px; margin-top: 30px; }
/* Four items, so a wider minimum: at the band's 980px the 260px minimum fits
   three across and leaves the fourth stranded on a row of its own. */
.four-up { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
.two-up h3 { font-size: 15px; margin-bottom: 6px; }
.two-up p { color: var(--muted); font-size: 13.5px; line-height: 1.55; }

/* The games this install can actually ingest, named. Read off the source
   plugins rather than written into the page, so the list cannot promise a game
   the install has no catalogue for. */
.chips { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; padding: 0; }
.chips li {
  font-family: var(--mono); font-size: 12px; padding: 5px 10px;
  border: 1px solid var(--rule); border-radius: 999px;
  background: var(--surface); color: var(--ink);
}

.site-foot {
  border-top: 1px solid var(--rule); padding: 26px 32px 40px;
  display: flex; flex-direction: column; gap: 12px;
  font-size: 12.5px; color: var(--muted);
  max-width: 980px; margin: 40px auto 0;
}
.site-foot a { color: var(--muted); }
.foot-links { display: flex; flex-wrap: wrap; gap: 8px 18px; }
.foot-fine { color: var(--faint); font-size: 12px; line-height: 1.6; }

/* ---------- the prose pages: privacy, terms ---------- */
.prose { max-width: 720px; margin: 0 auto; padding: 44px 32px 8px; }
.prose h1 { font-size: 30px; letter-spacing: -0.02em; margin-bottom: 14px; }
.prose h2 { font-size: 17px; margin: 30px 0 8px; }
.prose p, .prose li { color: var(--muted); font-size: 14.5px; line-height: 1.65; }
.prose p { margin-bottom: 12px; }
.prose-lede { font-size: 15.5px; }
.prose ul { margin: 0 0 12px; padding-left: 20px; }
.prose li { margin-bottom: 6px; }
.prose b { color: var(--ink); font-weight: 600; }
.prose a { color: var(--green); }

@media (max-width: 720px) {
  .hero h1 { font-size: 32px; }
  /* The header is sticky, so anything that wraps inside it is a cost paid on
     every screen of scrolling — and at 390px the four section anchors wrapped
     it to three rows and 129px, a seventh of the viewport, permanently. The
     brand and the one button that matters fit on a single row. The links are
     not lost: the footer carries the same list, which is where a phone reader
     arrives anyway. */
  .site-nav { padding: 10px 16px; }
  .site-nav nav a:not(.btn) { display: none; }
  .band-inner, .band, .hero { padding-inline: 20px; }
  .site-foot { padding-inline: 20px; }
}

/* ---------- re-pointing an inventory row ---------- */
.panel-wrong { margin: -4px 0 12px; }
.panel-wrong-open { font-size: 11.5px; text-decoration: underline; text-underline-offset: 2px; text-decoration-style: dotted; }
.panel-wrong .qfix { margin-top: 8px; }
.panel-wrong .qfix-list { grid-template-columns: 1fr; max-height: 220px; }

/* A game that is ingested but has never been price-synced. Stated in the
   status bar because the alternative is finding out from a card whose price
   never moves. */
.statusbar .stale {
  margin-left: 6px; padding: 0 5px; border-radius: 2px;
  border: 1px solid var(--warn-line); background: var(--warn-tint); color: var(--warn);
}
