/* ---- ONE PALETTE, AND IT IS THE DARK ONE --------------------------------
   There is no light version of this interface. There were two, switched by
   `prefers-color-scheme`, and the second one cost more than it was worth: it
   is the same list of numbers either way, and **every colour on a row had to
   be legible on a white page AND on a near-black one**, which is what forced
   the three condition labels and every warning into dark hexes that measured
   3.0:1 on the dark half. `cond-parts` red was 2.92:1 - under the 3:1 floor
   for any text at all - and nothing reported it, because it was correct in
   the theme whoever wrote it was looking at.

   One palette means every accent can be chosen for the surface it is actually
   drawn on. Measured on this background: the warning amber went from 3.77 to
   8.64, the parts red from 2.92 to 7.51, and the used yellow is 12.27.

   `color-scheme` is the half a stylesheet cannot do for itself - it is what
   tells the browser which way round to draw the parts of a native control no
   rule here can reach: a select's arrow, a checkbox, a scrollbar, a number
   spinner. Without it those render light on a dark page. */
:root{
  color-scheme:dark;
  --bg:#0d1117; --panel:#161b22; --line:#30363d; --ink:#e6edf3; --dim:#8b949e;
  --accent:#4493f8; --hot:#ff7b72; --good:#3fb950; --chip:#21262d;
  /* AMBER AND YELLOW ARE TWO COLOURS, and they were one. A warning and a
     second-hand listing were both `#b45309`, so `USED` and `TOO CHEAP?` were
     the same pill in the same hue side by side on the same row. 35° and 51°:
     far enough apart to read as amber and yellow rather than as one accent
     drawn twice. */
  --warn:#e8a13a; --yellow:#e6d155;
}
*{box-sizing:border-box}
body{margin:0;font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;
     background:var(--bg);color:var(--ink);height:100vh;display:flex;flex-direction:column}
header{padding:9px 14px;border-bottom:1px solid var(--line);background:var(--panel);
       display:flex;gap:9px;align-items:center;flex-wrap:wrap}
h1{font-size:15px;margin:0;font-weight:650;white-space:nowrap}
.meta{color:var(--dim);font-size:12px;white-space:nowrap}
a.signout{margin-left:auto;color:var(--dim);font-size:12px;text-decoration:none}
a.signout:hover{color:var(--ink);text-decoration:underline}
input,select,button{font:inherit;padding:5px 8px;border:1px solid var(--line);
       border-radius:6px;background:var(--bg);color:var(--ink)}
input[type=search]{min-width:170px}
input[type=number]{width:70px}
label.f{display:flex;flex-direction:column;gap:2px;font-size:11px;color:var(--dim)}
label.f > span{padding-left:2px}
label.cb{display:flex;gap:5px;align-items:center;font-size:13px;white-space:nowrap}
/* **A BRIGHT FILL TAKES A DARK INK.** White on this accent is 3.10:1 - it was
   right when the accent was the light theme's #0969da and became wrong the
   moment that theme went. The page's own background is the ink, at 6.11. */
button.go{background:var(--accent);color:var(--bg);border-color:transparent;font-weight:650;
       cursor:pointer;padding:7px 16px}
button.go:disabled{opacity:.55;cursor:default}
/* THE SET-UP IS A DIALOG, NOT A RIBBON. It was a band across the top that
   pushed the results down the page and was on show permanently for controls
   pressed once a session - which on a phone meant the first listing sat below
   the fold. `panel-hidden` used to be its narrow-screen behaviour; it is now
   the ordinary state at every width and the class is gone.
   Positioned rather than laid out, so nothing behind it moves when it opens. */
#panel{position:fixed;z-index:30;top:50%;left:50%;transform:translate(-50%,-50%);
  width:min(640px,calc(100vw - 20px));max-height:88vh;overflow-y:auto;
  border:1px solid var(--line);border-radius:12px;
  box-shadow:0 18px 50px rgba(0,0,0,.35);
  background:var(--bg);padding:0;display:block}

/* ---- inside the dialog -------------------------------------------------
   Grouped, labelled, and one primary button per pane. It was eleven bare
   boxes in a flat grid with two identical blue buttons at the end, so every
   setting looked equally relevant to whichever one you were about to press
   and nothing said which controls belonged to which job. */
.panel-head{padding:12px 14px;border-bottom:1px solid var(--line);
  background:var(--panel);border-radius:12px 12px 0 0;position:sticky;top:0;z-index:2}
.tabs{display:flex;gap:2px;padding:10px 14px 0;border-bottom:1px solid var(--line);
  background:var(--panel)}
.tab{cursor:pointer;background:none;border:none;border-bottom:2px solid transparent;
  padding:7px 14px;font-size:13px;font-weight:600;color:var(--dim);
  margin-bottom:-1px}
.tab:hover{color:var(--ink)}
.tab.active{color:var(--accent);border-bottom-color:var(--accent)}
.pane{padding:4px 14px 14px}
.pane[hidden]{display:none}
/* What the pane is FOR, in one sentence, before any control. */
.lede{color:var(--dim);font-size:12.5px;line-height:1.5;margin:12px 0 4px}
.pane fieldset{border:none;border-top:1px solid var(--line);margin:14px 0 0;
  padding:10px 0 0}
.pane legend{font-size:10.5px;font-weight:700;text-transform:uppercase;
  letter-spacing:.07em;color:var(--dim);padding:0}
.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:8px}
.row .f{flex:1 1 130px;min-width:0}
.hint{color:var(--dim);font-size:11.5px;line-height:1.5;margin:8px 0 0}
/* Categories as checkboxes, because more than one at a time is the ordinary
   case and a single-select made it impossible to say so. */
.boxes{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));
  gap:4px 10px;margin-top:8px}
.boxes label{display:flex;align-items:center;gap:7px;font-size:12.5px;
  padding:4px 6px;border-radius:6px;cursor:pointer;color:var(--ink)}
.boxes label:hover{background:var(--panel)}
/* **THE `.runs` DECLARATIONS USED TO SIT HERE WITH NO SELECTORS.** `.runs` is
   the fourth of the five defects `uicheck` was built for - four rules naming a
   class that exists nowhere - and the way it got resolved was by deleting the
   two SELECTORS and leaving their declarations behind. That silenced the check
   and left something worse: an orphan is read at the top level as the start of
   a selector, so the pair of them consumed `.boxes input` as well, and the
   checkbox in every category row has had no rule since. Check 6 exists because
   fixing check 3's complaint is how this arrived. */
.boxes input{margin:0;flex:0 0 auto}
.pane .cb{display:flex;align-items:center;gap:8px;margin-top:9px;font-size:12.5px}
.actions{position:sticky;bottom:0;background:var(--bg);padding:14px 0 2px;
  margin-top:16px;border-top:1px solid var(--line)}
.actions .go{width:100%}
#status{font-size:12px;color:var(--dim)}
#status.err{color:var(--hot)}
main{flex:1;display:flex;min-height:0}
/* The COLUMN is the wrapper now, because the filter strip has to sit above
   the list and scroll with neither. `#list` carried the width until the strip
   existed; leaving it there made the list 46% of a wrapper with no width. */
/* THE COLUMN, and every declaration here is load-bearing. It went in beside
   `#pager` and landed inside the 700px block, so at any wider width the
   wrapper was `display:block`, `#list{flex:1}` meant nothing and the list had
   zero height - an empty screen at 1280 and correct on a phone. A general
   rule written inside a media query, which is the trap worth naming. */
.listwrap{width:46%;min-width:330px;border-right:1px solid var(--line);
  display:flex;flex-direction:column;min-height:0;overflow:hidden}
#list{flex:1;min-height:0;overflow-y:auto}
/* ---- THE PAGER, AND IT IS THE ONLY ONE -----------------------------------
   **Every one of these five declarations was written INSIDE the 700px block**,
   so above that width the foot of the list was `display:block`, transparent,
   with no border and no padding - it only looked deliberate because the two
   buttons carry the ordinary button border. Measured: `justify-content:normal`
   at 1440 against `space-between` at 500. That is the `.listwrap{display:flex}`
   fault this project keeps a check for, in a second place, and it matters more
   than it did: there used to be a second pair of arrows up in the header, and
   now there is not.

   `flex:0 0 auto` beside `#list`'s `flex:1` is what keeps it pinned under a
   list that scrolls rather than riding up with the content. */
.pagerfoot{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;
  gap:8px;padding:9px 12px;border-top:1px solid var(--line);background:var(--panel)}
.pagerfoot[hidden]{display:none}
.pagerfoot button{cursor:pointer;padding:6px 12px;font-size:12.5px}
.pagerfoot button:disabled{opacity:.4;cursor:default}
.row{display:flex;gap:10px;padding:9px 12px;border-bottom:1px solid var(--line);cursor:pointer}
.row:hover{background:var(--panel)}
/* **WHAT THE SYNC JUST BROUGHT IN**, and it is a tint rather than a badge: the
   rows are already at the top of a newest-first list, so their position is
   most of the message and this only has to say which of them are the new ones.
   Slight on purpose - it is on whole rows, and a whole row of colour is the
   thing the palette section of the sibling project spends a page forbidding.
   Written BEFORE `.sel` so selecting one still reads as selected: the two are
   the same specificity and file order is what decides it. */
/* **AMBER WHILE IT WAITS TO BE READ, green once it has been.** A listing the
   sync has just found cannot be priced until something reads its title, so
   the two are worth telling apart: amber is here-but-unknown, green is
   here-and-understood. Both come from the row's own `fresh` field, so they
   survive a reload - the class used to be written from a Set the page held in
   memory, which is why the green vanished on every refresh. */
.row.awaiting{background:rgba(210,153,34,.12)}
.row.just-in{background:rgba(63,185,80,.10)}
.row.sel{background:var(--chip);box-shadow:inset 3px 0 0 var(--accent)}
.row img{width:62px;height:62px;object-fit:cover;border-radius:5px;background:var(--chip);flex:none}
.rt{flex:1;min-width:0}
/* ONE LINE, ELLIPSISED. It was clamped to two, which is the right answer for
   a title on its own and the wrong one here: a row whose title takes two
   lines and the next whose title takes one put their columns at different
   heights, and a table whose rows are different heights is a list again. The
   full title is one hover or one click away in the detail pane. */
.rt .t{font-weight:600;line-height:1.3;white-space:nowrap;overflow:hidden;
       text-overflow:ellipsis}
/* ---- THE COLUMNS --------------------------------------------------------
   A FIXED template, identical on every row, is the whole mechanism: `.rt`
   begins at the same x down the list because the star and the thumbnail are
   both `flex:none`, so a grid inside it lines up without a <table>, a
   colgroup or a header row. Five measured columns and then the rest, and the
   widths are what the content needs at 12px rather than a share of anything
   - a percentage would put the price column somewhere different in a docked
   window than in a maximised one, and the point of the exercise is that a
   number is always in the same place. **THE FIVE WIDTHS WERE MEASURED, not
   chosen**: 84px is `+£390.98 784%` and `£198.30/567.70`, which are the
   widest a deal chip and a price pair got over a live page, and 46px is
   `today`. A track a few pixels under its own content clips it silently, so
   every child of a cell also ellipsises - a number cut off mid-digit reads as
   a smaller number, where `£1234.56/199…` reads as a number that did not
   fit.

   **HOW MANY COLUMNS THERE ARE IS A QUESTION ABOUT ROOM, and the answer is a
   different TEMPLATE rather than a fall back to a wrapping line** - three
   columns still put every price under every other price, which is the whole
   ask; a wrapping line does not. The list pane is 46% of the window, so a
   window breakpoint IS a pane breakpoint: six tracks and their gaps are
   320px, plus the star, the thumbnail, the gaps and the padding, which needs
   a pane of about 550 and therefore a window of about 1200. Below that the
   last column starves - measured, at 1100px it was 11px wide and every one of
   the fifty rows overflowed sideways. */
.rt .s{color:var(--dim);font-size:12px;margin-top:3px;
  display:grid;gap:2px 6px;align-items:start;
  grid-template-columns:84px 58px 46px 34px 84px minmax(96px,1fr)}
/* Both boundaries are the arithmetic above rather than a round number that
   looked about right: six tracks and their gaps plus the 96px floor on the
   last need 432px of `.rt`, which is a 46% pane in a 1244px window, and three
   tracks holding an 84px chip need 270px, which is one in 891. Measured at
   1210 and at 830, where the earlier pair of numbers clipped a deal chip by
   seven pixels at both. */
@media (max-width:1280px){.rt .s{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media (max-width:900px){.rt .s{grid-template-columns:repeat(2,minmax(0,1fr))}}
/* A cell stacks its own two lines; nothing in it may wrap into the next
   column, which is what `min-width:0` and the nowrap below are for. */
.rt .s .cell{min-width:0;display:flex;flex-direction:column;gap:1px;
  line-height:1.35;white-space:nowrap;overflow:hidden}
/* **`align-self` AND `max-width` TOGETHER, and neither is optional.** A cell
   is a flex column, so its children stretch to the track by default - which at
   34px is invisible and at 145px, where three columns share the row, drew the
   seen chip as a 145px green bar and the deal chip as a grey slab: a pill is a
   pill because it is the size of its own text. `start` gives it that size, and
   the `max-width` is what keeps the ellipsis working, since a child sized by
   its content has nothing to ellipsise against. */
.rt .s .cell > *{min-width:0;max-width:100%;align-self:start;
  overflow:hidden;text-overflow:ellipsis}
.cell-sub{font-size:10.5px;overflow:hidden;text-overflow:ellipsis}
/* EVERYTHING ELSE, and it is the only cell allowed to wrap - it holds a
   variable number of marks and is the reason the other five can be fixed. */
.rt .s .cell-rest{min-width:0;display:flex;flex-wrap:wrap;gap:3px 5px;
  align-items:center;line-height:1.5}
.rt .s .cell-rest .flag{margin-left:0}
.pr{font-weight:700;white-space:nowrap;color:var(--ink)}
/* The second price on one listing, so it is set INSIDE the first one rather
   than beside it: smaller, quieter, and unmistakably the other half of a
   pair. Marked rather than shouted - it is a second price on the same
   listing, not a second listing. */
.binprice{font-weight:600;font-size:10.5px;color:var(--accent)}
.tl{color:var(--hot);font-variant-numeric:tabular-nums;white-space:nowrap}
.badge{background:var(--accent);color:var(--bg);font-size:10px;font-weight:700;
       padding:1px 5px;border-radius:8px}
#detail{flex:1;overflow-y:auto;padding:20px 24px}
#detail h2{font-size:17px;margin:0 0 10px;line-height:1.35}
#detail img.hero{max-width:100%;max-height:330px;border-radius:8px;background:var(--chip)}
.gal{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}
.gal img{width:58px;height:58px;object-fit:cover;border-radius:4px;cursor:pointer;
         border:1px solid var(--line)}
table.kv{border-collapse:collapse;margin:14px 0;width:100%;max-width:460px}
.kv td{padding:5px 10px 5px 0;border-bottom:1px solid var(--line);vertical-align:top}
.kv td:first-child{color:var(--dim);width:118px;white-space:nowrap}
.big{font-size:22px;font-weight:700}
.desc{color:var(--dim);margin:12px 0;max-width:640px}
a.btn{display:inline-block;background:var(--accent);color:var(--bg);text-decoration:none;
      padding:8px 15px;border-radius:6px;font-weight:600;margin-top:6px}
/* Sold prices is a second door onto eBay, not a second primary action: one
   filled button per screen, the rest outlined. */
a.btn.ghost{background:transparent;color:var(--ink);border:1px solid var(--line);
      margin-left:6px}
.empty{color:var(--dim);padding:40px 24px;text-align:center}
.chip{background:var(--chip);padding:1px 7px;border-radius:9px;font-size:11px}
/* Filled, not outlined: this is a fact about the DATA rather than about the
   listing, and it has to be readable at a glance down a column of fifty. */
.seen{border-radius:9px;padding:0 6px;font-size:10.5px;font-weight:700;
  white-space:nowrap;color:#fff}
.seen.fresh{background:#15803d}
.seen.aging{background:#b45309}
.seen.stale{background:#b91c1c}
/* Facts about a listing, not judgements about it: outlined, never filled, so
   they read as labels beside the one number that is the judgement. */
.flag{border:1px solid var(--line);color:var(--dim);padding:0 6px;
      border-radius:9px;font-size:10px;text-transform:uppercase;
      letter-spacing:.04em;margin-left:3px}
.flag.warn{border-color:var(--warn);color:var(--warn)}
/* THE THREE CONDITION BUCKETS, and they are the TOKENS now. They were three
   fixed hexes, and the reason given was that `--good`/`--hot` lighten in the
   dark theme to stay readable there - which was true and is now moot, since
   there is one theme and these tokens are already tuned for it. Measured, the
   hexes were the worse half of that trade on this background: green 3.77, red
   2.92, against 7.45 and 7.51 for the tokens.
   Yellow is its own token because nothing else in the app is yellow - see the
   note in `:root` about the amber it used to share. */
.flag.cond-new{border-color:var(--good);color:var(--good)}
.flag.cond-used{border-color:var(--yellow);color:var(--yellow)}
.flag.cond-parts{border-color:var(--hot);color:var(--hot)}
.flag .ic{width:9px;height:9px;margin-right:2px;vertical-align:0}
/* A NUMBER IS NOT A LABEL. `.flag` uppercases its contents, which is right for
   a word and wrong for `42d` - it came out as `42D`, which reads as a part
   number rather than as forty-two days. */
.flag.age{text-transform:none;font-size:10.5px}
/* PROVENANCE, and ONLY THE EXCEPTION IS DRAWN. `swept` had the quietest
   hairline on the row and was still a badge printed a thousand times down the
   page saying nothing about any of them, since almost every row is one. What
   is left is the row nothing is watching, which is the one whose missing
   marks mean unknown rather than no - dashed, because that is what it is
   about. */
.flag.oneoff{border-style:dashed;border-color:var(--line);opacity:.65}

/* ---- ICONS: a mark where a word was repeated fifty times ----------------
   `listed 9d ago`, `private`, `246d unsold` - three labels down every row of
   a list whose whole job is the numbers beside them. Each is a MASK filled
   from `currentColor`, so the glyph is defined once here instead of an <svg>
   copied into fifty rows, and a flag that turns amber takes its icon with it.

   A mask and not a background-image for exactly that reason, and a data URI
   and not a file because `render_static()` inlines this stylesheet into a
   page opened from disk, which has no server to fetch an image from.

   No `{` or `}` inside a URI: `uicheck.mjs` finds a rule by splitting on the
   brace, and one inside a value would hand it a selector made of path data. */
.ic{display:inline-block;width:11px;height:11px;flex:none;
  background:currentColor;vertical-align:-1px;margin-right:3px;
  -webkit-mask:var(--i) center/contain no-repeat;
  mask:var(--i) center/contain no-repeat}
.ic-listed{--i:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6'%3E%3Crect x='2' y='3.4' width='12' height='10.6' rx='1.5'/%3E%3Cpath d='M2 6.9h12M5.4 1.9v2.6M10.6 1.9v2.6'/%3E%3C/svg%3E")}
.ic-priv{--i:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='black'%3E%3Ccircle cx='8' cy='4.9' r='2.9'/%3E%3Cpath d='M2.4 14.4c0-3.1 2.5-5.2 5.6-5.2s5.6 2.1 5.6 5.2z'/%3E%3C/svg%3E")}
.ic-biz{--i:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6'%3E%3Crect x='1.8' y='5' width='12.4' height='9.2' rx='1.4'/%3E%3Cpath d='M6 5V3.1h4V5'/%3E%3C/svg%3E")}
.ic-info{--i:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6'%3E%3Ccircle cx='8' cy='8' r='6.4'/%3E%3Cpath d='M8 7.2v4.2'/%3E%3Ccircle cx='8' cy='4.7' r='0.95' fill='black' stroke='none'/%3E%3C/svg%3E")}
/* NO PRICE, and it is a CROSSED CIRCLE rather than a crossed price tag. The
   tag was the obvious drawing and it is illegible at the size this is drawn:
   9px inside a flag, where an outlined tag with a dot and a diagonal through
   it resolves to a single amber stroke that reads as a stray mark. Two big
   shapes survive that; five small ones do not. **Draw an icon at the size it
   will be used, not at the size it is authored.**
   The reason stays in the tooltip, which is where it has to be - `not read
   yet` and `no catalogue match` are different problems and no mark can tell
   them apart. */
.ic-unpriced{--i:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8'%3E%3Ccircle cx='8' cy='8' r='6.3'/%3E%3Cpath d='M3.6 12.4L12.4 3.6'/%3E%3C/svg%3E")}
/* AN EYE WITH A LINE THROUGH IT. Two shapes at the size it is drawn, for the
   reason `.ic-unpriced` states one block up. */
.ic-hide{--i:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5'%3E%3Cpath d='M1.3 8S3.9 3.4 8 3.4 14.7 8 14.7 8 12.1 12.6 8 12.6 1.3 8 1.3 8z'/%3E%3Ccircle cx='8' cy='8' r='2'/%3E%3Cpath d='M2.4 13.6L13.6 2.4' stroke-width='1.7'/%3E%3C/svg%3E")}
.ic-unsold{--i:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.7'%3E%3Cpath d='M13.6 8.2a5.6 5.6 0 1 1-1.7-4'/%3E%3Cpath d='M13.9 1.3v3.4h-3.4'/%3E%3C/svg%3E")}
/* THE SELLER IS A GLYPH AND NOT A PILL. It is on every row, which is the
   argument that took `swept` off it altogether - but both answers here carry
   information, since the Buyer Protection fee is charged on one and not the
   other, so neither can be the silent default. Private takes the accent
   because it is the mispricing anomaly and the one that costs you the fee;
   business stays the colour of the line it sits in. */
.sic{width:12px;height:12px;margin-right:0}
.sic.ic-priv{color:var(--accent)}
.sic.ic-unpriced{color:var(--warn)}

/* ---- the star -----------------------------------------------------------
   An outline until it means something, then filled and coloured. It is one of
   the very few things on a row allowed a hue: what it encodes is a decision
   somebody made, which is exactly the kind of information colour is for.
   Sized for a thumb rather than for the text beside it - it is pressed far
   more often than anything else on the row. */
/* **FIRST IN THE ROW, before the thumbnail.** It is the one control on a row
   that is pressed rather than read, so it gets the position the eye starts
   at and a target big enough to hit without aiming. `align-self: flex-start`
   keeps it level with the title rather than floating in the middle of a
   62px picture. */
.fav{background:none;border:none;cursor:pointer;padding:0 2px;
  font-size:23px;line-height:1;color:var(--dim);
  min-width:34px;min-height:34px;flex:none;align-self:flex-start;
  margin-top:-2px}
.fav:hover{color:var(--accent)}
.fav.on{color:#d9a406}
/* The toggle wears the same star and the same gold as the rows it filters to,
   so what it does needs no label. Sized to the bar rather than to the row. */
#favtoggle{font-size:17px;line-height:1;padding:2px 9px;min-width:34px}
/* ---- the hide button ----------------------------------------------------
   Beside the star and sized to the same thumb, because they are the two things
   a person SAYS about a row rather than reads off it. Quieter than the star at
   rest: a star is an invitation and this is a dismissal, and a row full of
   dismissals is not what anybody came to look at. It takes the warning amber
   when it is on, which is only ever visible with `Hidden too` ticked.
   No border and no background: `background` is the mask's paint here, so a
   button that looked like a button would need a wrapper. */
.hide{background:currentColor;border:none;cursor:pointer;padding:0;
  width:15px;height:15px;margin:8px 2px 0 0;flex:none;align-self:flex-start;
  color:var(--line)}
.hide:hover{color:var(--ink)}
.hide.on{color:var(--warn)}
.hide:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* **STRUCK THROUGH RATHER THAN GONE.** The list is not redrawn when the button
   is pressed - that would take the row out from under the pointer mid-click -
   so the row says what has happened to it and leaves on the next read. It is
   also what a hidden row looks like when `Hidden too` puts it back, which is
   the state somebody is in when they want to undo one. */
.row.is-hidden{opacity:.42}
.row.is-hidden .t{text-decoration:line-through}
/* RESET. Sized like the star beside it - both are one glyph doing one thing to
   the list - and quiet until there is something to undo, because at rest it is
   reporting *nothing is filtered*, which is not news. Marked in the accent
   `#catbtn` already uses for "this control is holding an answer": the two are
   saying the same kind of thing about the same bar. */
#freset{font-size:15px;line-height:1;padding:3px 9px;min-width:34px;
  color:var(--dim);cursor:pointer}
#freset:hover{color:var(--ink)}
#freset.on{border-color:var(--accent);color:var(--accent)}
/* A process, not a fault: the same quiet grey as the rest of the meta line,
   with a pulse so it reads as something happening rather than a label. */
.rebuilding{color:var(--dim);animation:pulse 1.1s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:.35}50%{opacity:1}}
@media (prefers-reduced-motion: reduce){.rebuilding{animation:none;opacity:.7}}
#favtoggle.on{border-color:#d9a406;color:#d9a406}
.fav:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
/* A thin sample is not a warning - scarcity is the thing being hunted - so it
   is marked, not reddened. The prose beside it carries the caveat. */
.chip.scarce{background:transparent;border:1px solid var(--line);
      color:var(--dim);font-weight:600;vertical-align:middle}

/* ---- small screens ------------------------------------------------------
   One column, and the two panes become two SCREENS rather than two columns:
   a 46% list beside a detail pane is 180px of each on a phone, which is
   neither. `body.detail-open` is the whole mechanism - the list and the
   detail are the page's own two halves, shown one at a time, with nothing
   moved and nothing duplicated.

   700px because that is where two panes stop fitting side by side, not
   because a phone is 390: a narrow window on a laptop has the identical
   problem and gets the identical answer. */
@media (max-width: 700px){
  body{height:auto;min-height:100vh}

  /* ---- the header is ONE line, and everything else in it is conditional.
     The first version of this block kept the whole desktop header - title,
     count, search box, two selects, two number boxes, a status paragraph that
     wrapped to three lines, and a Sign out link on a line of its own - and
     put the nine-control panel underneath it. That is a full phone screen of
     chrome before a single listing, on a page whose entire job is listings.

     Nothing here is hidden because a phone cannot use it. Each is hidden at
     the one moment it has nothing to say: you cannot filter results that do
     not exist yet, and a status line reading "ready" is not worth 40px of a
     844px screen. */
  header{position:sticky;top:0;z-index:5;gap:8px;padding:7px 10px;
         align-items:center}
  h1{font-size:14px}
  #panel-toggle{margin-left:auto;padding:7px 13px;min-height:36px}
  a.signout{margin-left:0;font-size:11px}

  /* The filter bar filters what is already on screen. Before a scan there is
     nothing on screen, so it is five controls that can do nothing - and once
     there is, it belongs with the other filters, behind the same button. */
  header input[type=search],header select,header input[type=number]{display:none}
  /* EXCEPT the source, which is how you GET results - hiding it until there
     are some is a door locked from the inside. */
  header #show:not([hidden]){display:block}
  body.has-results header input[type=search],
  body.has-results header select,
  body.has-results header input[type=number]{display:block}
  body.has-results input[type=search]{flex:1 1 100%;min-width:0}
  body.has-results header select,
  body.has-results header input[type=number]{flex:1 1 30%;min-width:0}

  /* One line, ellipsised, with the whole sentence in the tooltip. It wrapped
     to three before: 40px spent on per-category counts. */
  /* `order` rather than a reshuffle of the markup: the status is the only
     thing here that wants a whole line, so it takes the last one and the four
     short controls share the first. Sign out had a 30px line to itself. */
  .meta#status{order:1;flex:1 1 100%;font-size:11px;white-space:nowrap;
               overflow:hidden;text-overflow:ellipsis}
  body:not(.has-results) #status{display:none}

  /* The panel is the screen before a scan and out of the way after one, which
     is the right way round: before a scan, setting it up IS the task. */
  #panel{padding:10px;gap:8px}
  #panel label.f{flex:1 1 44%;min-width:0}
  #panel label.f input,#panel label.f select{width:100%}
  #panel label.cb{flex:1 1 100%}
  /* A primary action on a phone is a full-width target, and 44px is the
     smallest one a thumb hits reliably. */
  button.go{flex:1 1 100%;padding:12px 16px;min-height:44px}

  main{display:block;min-height:0}
  .listwrap{width:100%;min-width:0;border-right:none;overflow:visible}
  #list{width:100%;border-right:none;overflow:visible}
  #detail{display:none;padding:14px 12px}
  body.detail-open #list,
  body.detail-open .listwrap{display:none}
  body.detail-open #detail{display:block}
  .row{padding:12px 10px}
  .row img{width:72px;height:72px}
  #detail img.hero{max-height:44vh}
  table.kv{max-width:none}
  .kv td:first-child{width:96px}
  /* Full-width and tall enough to hit without aiming. */
  a.btn{display:block;text-align:center;padding:13px 16px}
  a.btn.ghost{margin-left:0}
}

/* The way back out of the detail screen. Drawn at every width and hidden
   above the breakpoint rather than rendered conditionally: one render path
   for both screens, and nothing to keep in step. */
/* Drawn at every width now, because the panel is always a dialog. */
#panel-toggle{cursor:pointer;margin-left:auto}
#panel[hidden]{display:none}
#panel-back{position:fixed;inset:0;z-index:29;background:rgba(0,0,0,.45)}

/* ---- the category picker ------------------------------------------------
   Same geometry as the scan dialog, deliberately: two dialogs in one app that
   are positioned, sized and shadowed differently read as two applications.
   Narrower, because it holds one column of names rather than four panes. */
#cats-modal,#filters-modal,#info-modal{position:fixed;z-index:30;top:50%;left:50%;transform:translate(-50%,-50%);
  width:min(420px,calc(100vw - 20px));max-height:80vh;
  display:flex;flex-direction:column;
  border:1px solid var(--line);border-radius:12px;
  box-shadow:0 18px 50px rgba(0,0,0,.35);background:var(--bg)}
#cats-modal[hidden],#filters-modal[hidden],#info-modal[hidden]{display:none}
#cats-back,#filters-back,#info-back{position:fixed;inset:0;z-index:29;
  background:rgba(0,0,0,.45)}
#cats-back[hidden],#filters-back[hidden],#info-back[hidden]{display:none}
#cats-modal .panel-head,#filters-modal .panel-head,#info-modal .panel-head{
  display:flex;align-items:center;justify-content:space-between}
#cats-close,#filters-close,#info-close{cursor:pointer;font-size:20px;line-height:1;
  padding:0 8px;background:none;border:none;color:var(--dim)}
#cats-close:hover,#filters-close:hover,#info-close:hover{color:var(--ink)}
/* **`.cats-list` AND `.cats-foot` HAD NO SELECTORS AT ALL**, from the commit
   that introduced this dialog. The list's declarations were gone outright and
   the foot's were left orphaned at the top level - where CSS reads them as the
   start of a selector and keeps consuming until it finds a block, so they ate
   the two comments below and `#catbtn.on` with them. That rule has therefore
   never applied: the category button has never shown that it was holding an
   answer, which is the one thing this design says a button must do. Second
   time in this file, and neither check could see it - `uicheck` skips a chunk
   with no brace in it, which is exactly what an orphan looks like. There is a
   sixth check for it now.

   The list owns the height and scrolls inside the card - a `vh` of its own
   would add to the card's ceiling instead of fitting inside it, and push the
   buttons out of the bottom. */
.catrow{display:flex;align-items:center;gap:9px;
  padding:8px 16px;cursor:pointer;font-size:13px}
.catrow:hover{background:var(--panel)}
.catrow input{margin:0;flex:0 0 auto}
.catrow span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cats-list{flex:1;min-height:0;overflow-y:auto;padding:4px 0}
/* Show is the one thing you came here to press, so it is the one filled
   button; Clear and Select all are the ordinary outline. */
.cats-foot{display:flex;align-items:center;gap:8px;justify-content:flex-end;
  padding:10px 14px;
  border-top:1px solid var(--line);background:var(--panel);
  border-radius:0 0 12px 12px}
.cats-foot .go{min-width:88px}
/* The button carries the answer, so it has to read as holding one when it
   does - and as ordinary furniture when it does not. */
#catbtn.on,#filterbtn.on{border-color:var(--accent);color:var(--accent)}
#panel-back[hidden]{display:none}
.panel-head{flex:1 1 100%;display:flex;align-items:center;gap:8px;
  flex-wrap:wrap;margin:-2px 0 4px}
.panel-head strong{margin-right:auto}
/* The saved-setups row. Ghost buttons: they act on the dialog rather than
   running anything, so they must not compete with Run scan for the eye. */
.setup-bar{display:flex;gap:5px;align-items:center;flex-wrap:wrap}
.setup-bar select{min-width:140px;max-width:200px}
.setup-bar button{cursor:pointer;padding:4px 9px;font-size:12px;
  background:transparent;border:1px solid var(--line);color:var(--ink);
  border-radius:6px}
.setup-bar button:hover{border-color:var(--accent);color:var(--accent)}
.setup-bar button.armed{border-color:var(--warn);color:var(--warn)}
#setup-name{min-width:140px}
#setup-name[hidden]{display:none}
@media (max-width:520px){
  /* Three rows of header for a title, a picker and a close button was most of
     the dialog before any control. Title and close share a line; the saved-
     setups row takes the next one whole. */
  .panel-head strong{flex:1 1 auto;margin-right:auto}
  #panel-close{order:2}
  .setup-bar{flex:1 1 100%;order:3}
  .setup-bar select{flex:1 1 100%;max-width:none}
  .setup-bar button{flex:1 1 30%}
  /* Two categories across, not one. At 390px the tick and six characters fit
     twice over, and one per row turned six checkboxes into six rows. */
  .boxes{grid-template-columns:repeat(2,1fr);gap:0 6px}
  .pane{padding:2px 10px 8px}
  .lede{display:none}          /* the tab already says which job this is */
  .row .f{flex:1 1 44%}
}
#panel-close{cursor:pointer;font-size:20px;line-height:1;padding:0 8px;
  background:none;border:none;color:var(--dim)}
#panel-close:hover{color:var(--ink)}
/* ---- THE FOUR THINGS THAT SPEND, IN ONE BLOCK ---------------------------
   Refresh, Collect, Check prices and Scan setup, at the end of the bar. The
   filters to the left of them cost nothing and can be changed all day; these
   four are the only controls on the screen that reach eBay, so they are
   grouped and they are last. A hairline round the group rather than a colour
   on each button: what they have in common is a consequence, not an emphasis,
   and four coloured buttons in a row would out-shout the list. */
.spend{display:inline-flex;align-items:center;gap:4px;
  border:1px solid var(--line);border-radius:9px;padding:3px}
.spend button{border-color:transparent;background:none}
.spend button:hover{border-color:var(--line)}
/* ONE MARK FOR THE TWO STANDING FACTS. Icon-only, so its name is in the markup
   rather than on the screen; sized to the row it sits in, and given the
   quota's own amber when the day is nearly spent, because that is the one
   thing behind it that is ever urgent. */
#infobtn{cursor:pointer;width:30px;height:30px;padding:0;flex:none;
  color:var(--dim);border-radius:9px}
#infobtn:hover{color:var(--ink);border-color:var(--accent)}
#infobtn.warn{color:var(--warn);border-color:var(--warn)}
.info-body{padding:4px 16px 14px}
/* The quota is a block in the dialog rather than a chip in a bar, so its pill
   has to stop being the width of the card. */
.info-body .quota{display:inline-block;margin:0 0 4px}
.info-body .meta{white-space:normal;line-height:1.55}
.info-body .lede{margin:14px 0 4px}
/* Sign out is not what anybody opened this for, so it takes the far end and
   the quietest treatment on the screen. It was a link in the header, which put
   the way OUT of the tool beside the controls that run it. */
.cats-foot .signout{margin-right:auto;margin-left:0}
/* The quota, kept in front of you. Amber when the day is nearly spent. */
.quota{font-variant-numeric:tabular-nums;border:1px solid var(--line);
  border-radius:9px;padding:1px 7px}
.quota.warn{border-color:var(--warn);color:var(--warn)}
.dealsonly{display:inline-flex;align-items:center;gap:5px;font-size:12px;
  white-space:nowrap;cursor:pointer}
/* **THE LINE ABOVE THIS ONE WAS FOUR ORPHANED DECLARATIONS**, left behind by
   a deleted rule. At the top level CSS reads them as the start of a selector
   and keeps consuming until it finds a block - so it ate the comment and
   `#show[hidden]` with it, and the rule below had never once applied. A
   stylesheet with no build step reports that as nothing at all. */
#show[hidden]{display:none}
/* Live work, and the way to stop it. Amber rather than red: something is
   running, which is not a fault - but it is spending, so it is not quiet
   either. */
.running{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--warn);
  color:var(--warn);border-radius:9px;padding:1px 4px 1px 8px}
.running[hidden]{display:none}
.running button{cursor:pointer;background:none;border:1px solid var(--warn);
  color:var(--warn);border-radius:6px;font-size:10px;padding:1px 6px;
  text-transform:uppercase;letter-spacing:.05em}
.running button:hover{background:var(--warn);color:var(--bg)}
.back{display:none;margin:0 0 10px;padding:8px 12px;cursor:pointer}
@media (max-width: 700px){
  .back{display:inline-block}
}
.mk{font-size:11px;font-weight:700;padding:1px 6px;border-radius:9px;
    background:var(--chip);color:var(--dim);white-space:nowrap}
.mk.up{color:var(--good)}
.mk.down{color:var(--hot)}
.mk.none{opacity:.65;font-weight:400}
#price-block{margin:14px 0;padding:12px 14px;border:1px solid var(--line);
  border-radius:8px;background:var(--panel);max-width:640px}
#price-block .target{font-size:20px;font-weight:700}
#price-block .target.good{color:var(--good)}
#price-block .target.bad{color:var(--hot)}
#price-block .deal{margin:10px 0 4px;max-width:340px}
#price-block .deal td{padding:4px 8px 4px 0}
#price-block .good{color:var(--good)}
#price-block .bad{color:var(--hot)}
#price-block .advice{color:var(--dim);margin:6px 0 10px}
#price-block ol{margin:0;padding-left:20px}
#price-block li{margin:3px 0}
#price-block li a{color:var(--accent);text-decoration:none}
#price-block .p{font-weight:700;white-space:nowrap}
button.check{cursor:pointer;font-weight:600}
button.check:disabled{opacity:.55;cursor:default}
button.check.mini{padding:0 6px;font-size:11px;line-height:18px;
  border-radius:9px;color:var(--dim)}
button.check.mini:hover{color:var(--ink);border-color:var(--accent)}
