/* ==========================================================================
   Vidya Price Guide - stylesheet
   Light palette on :root, dark overrides in one media block at the bottom.
   ========================================================================== */

:root {
  --bg:        #faf9f7;
  --surface:   #ffffff;
  --border:    #e5e2dc;
  --border-sw: #d8d4cc;
  --ink:       #1a1917;
  --ink-2:     #55514a;
  --ink-3:     #8a857c;
  --accent:    #4a3fbd;
  --accent-sw: #ede9ff;

  /* Text that sits on a filled accent background. Not always white:
     the accent flips light in dark mode, where white on it falls to
     about 2:1 contrast and stops being readable. */
  --on-accent: #ffffff;
  --up:        #1c7c4a;
  --down:      #b03a2e;

  /* Chart series: loose / CIB / new / graded. */
  --s1: #c8323e;
  --s2: #1c7c4a;
  --s3: #4a3fbd;
  --s4: #c9960c;

  --radius:    10px;
  --maxw:      1200px;

  /* The height of a single-line control: the search page's box, the filter
     buttons and Clear Filters beside it, and the fields in the add and edit
     dialogs. One figure, so none of them can drift from the others. */
  --control-h: 38px;

  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
          "Liberation Mono", monospace;
}

* { box-sizing: border-box; }

/* The browser's own `[hidden] { display: none }` is a single attribute
   selector, so any class rule setting `display` silently beats it - which is
   how the chart panel (`display: flex`) stayed on screen while carrying the
   attribute. Script toggles visibility with `hidden`, so it has to win. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;

  /* Always reserve room for the vertical scrollbar. Without it the page width
     jumps by the scrollbar's ~15px whenever a result set or a condition tab
     changes the page from scrolling to not scrolling. */
  scrollbar-gutter: stable;
}

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

  /* Full-height column so the footer is pushed to the bottom of the window on
     short pages instead of floating mid-screen. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.site-head, .site-foot { flex: none; }

/* width:100% is load-bearing. `.wrap` sets `margin: 0 auto`, and an auto margin
   on the cross axis of a flex column cancels the default stretch - main would
   size to its widest child instead, so the page width would shift between
   condition tabs as the listing titles changed. */
main { flex: 1 0 auto; width: 100%; }

a { color: inherit; text-decoration: none; }

.wrap {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 24px;
}

/* --------------------------------------------------------------- header -- */

.site-head {
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

/* Logo left, account links right. */
.brand-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 20px 0;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

/* The wordmark: 28px wherever the header has room for it. It cannot wrap the
   way the old three-word name could, so below about 430px it shrinks with the
   window instead, staying on one line beside "My Account" and "Sign out" -
   down to 17px on a 320px phone, which is all that pair leaves it.

   The line height never drops below the 26px the row has always been. The
   account links arrive late, and on a phone they would otherwise be the
   tallest thing in the row, pushing the page down as they appeared. */
.brand-name {
  font-size: clamp(17px, 10vw - 15px, 26px);
  font-weight: 640;
  line-height: max(26px, 1em);
  letter-spacing: -0.02em;
}

/* The dot in ".io", enlarged and in the accent. line-height: 0 stops the
   bigger glyph from stretching the line box: a period sits on the baseline,
   so it needs none of the extra room its larger font would claim. */
.brand-dot {
  color: var(--accent);
  font-size: 1.5em;
  line-height: 0;
}

/* ----------------------------------------------------------- home page --- */

/* Centred block: the header logo and the footer stay left-aligned. */
.home {
  padding: 24px 0;          /* even, so the flex centring below reads as centred */
  max-width: 560px;
  margin: 0 auto;
  text-align: center;
}

/* The hero is centred in a band 42vh tall rather than in the whole window,
   which keeps the page from running long. min-height: 0 releases the body's
   100vh so the footer follows straight after the band instead of being held
   at the bottom of the screen. */
[data-page="home"] { min-height: 0; }

[data-page="home"] main {
  flex: 0 0 auto;
  min-height: 42vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.home .search { margin: 0 auto; }

/* The magnifier is pinned to the left edge, so the typed text stays with it. */
.home .search input { text-align: left; }

.home h1 {
  margin: 0 0 6px;
  font-size: 29px;
  font-weight: 640;
  letter-spacing: -0.025em;
  line-height: 1.2;
}

.home p {
  margin: 0 0 28px;
  color: var(--ink-2);
  font-size: 14.5px;
}

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

.search {
  position: relative;
  max-width: 420px;
}

/* 36px on the right keeps typed text out from under the clear button. */
.search input {
  width: 100%;
  padding: 11px 36px 11px 38px;
  font: inherit;
  font-size: 13.5px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border-sw);
  border-radius: var(--radius);
  outline: none;
  transition: border-color 0.13s ease, box-shadow 0.13s ease;
}

.search input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

.search input::placeholder {
  color: var(--ink-3);
}

/* The magnifier. A direct child only: the clear button carries an svg of its
   own. */
.search > svg {
  position: absolute;
  left: 13px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-3);
  pointer-events: none;
}

/* The clear button. Chrome, Edge and Safari draw one inside a search box and
   Firefox draws none, so the browsers' own is switched off and app.js adds
   this one to every search box instead - the same button in every browser.
   28px square, which leaves 5px around it in the 38px box.

   Shown only while the box holds text. That is read off :placeholder-shown
   rather than set by script, because the pages also fill the box from
   script - the search page from ?q= - and that fires no event to follow. */
.search input::-webkit-search-cancel-button { display: none; }

.search-clear {
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--ink-3);
  cursor: pointer;
  transition: color 0.13s ease;
}

.search-clear:hover { color: var(--ink); }

.search input:placeholder-shown ~ .search-clear { display: none; }

/* ----------------------------------------------------------- page head --- */

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  padding: 40px 0 22px;
}

.page-head h1 {
  margin: 0;
  font-size: 24px;
  font-weight: 640;
  letter-spacing: -0.02em;
}

.page-head .sub {
  margin: 4px 0 0;
  font-size: 12.5px;
  color: var(--ink-3);
}

/* As a flex item the search form would shrink to fit its own content, so it
   is given a width - the same on the search page and the collection page, so
   the box does not change size between the two. It shrinks only on a screen
   too narrow to hold it at all. Only this row's box: the homepage keeps the
   420px from .search above.

   The width is the collection page's first card at its widest, so there the
   two share a right edge as well as a left one on a full-width page. Here it
   is fixed; the collection page has a rule of its own that follows the card
   down as the window narrows (see .list-search below). The card's widest is
   the page's --maxw less
   the wrap's two 24px gutters, split three ways around the grid's two 12px
   gaps (.price-grid's `gap`) - (1200 - 48 - 24) / 3 = 376px. Written as that
   sum so a change to --maxw carries through; a change to the gap or the
   gutters has to be made here too. */
.search-row .search { flex: 0 1 calc((var(--maxw) - 72px) / 3); }

/* The search box and the filters beside it. Wraps rather than squeezing the
   box: below roughly 560px the filters drop to their own line, where they
   keep their full width instead of being crushed into the gap left over. */
.search-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  width: 100%;

  /* Every control in the row - the search box, both filter buttons and Clear
     Filters - is --control-h tall (on :root), so they cannot drift apart and
     leave a pixel between boxes sitting side by side. */
}

/* The search box in a row, pinned to the row's height rather than left to its
   padding plus its line box, which came out at 44.25px beside buttons pinned
   at 44. The homepage box sits outside .search-row and keeps its own padding.
   An input centres its text vertically by itself, so the vertical padding
   goes. */
.search-row .search input {
  height: var(--control-h);
  padding-top: 0;
  padding-bottom: 0;
}

/* Same structure as .search - a relative box with the icon absolutely placed
   inside it - so the chevron themes with `color` instead of being baked into
   a background image that cannot follow the palette into dark mode. */
.filter {
  position: relative;
  display: inline-flex;
}

.filter svg {
  position: absolute;
  right: 11px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-3);
  pointer-events: none;
}

/* The trigger every filter uses. Matched to .search input beside it - same
   font, border, radius and focus ring - so the row reads as one set of
   controls. The right padding is wider than the left to clear the chevron
   above, which .filter places for it: its 11px inset, its 10px, and 5px of
   air between it and the label - 7px once a tick count is on the end of it,
   which brings 2px of margin with it (.filter-count), so a ticked button
   keeps the room it had before the unticked ones were closed up.

   With nothing ticked, every filter is as wide as Condition, the longest
   name. In Segoe UI that name and the padding come to 99.5px where the
   borders are drawn a whole pixel wide, and a little less on a scaled screen
   that draws them thinner, so the floor is 100px: just over it at either
   scale, which is what makes the three come out exactly equal. Item Type is
   within a tenth of a pixel of Condition, and Platform, the shortest name, is
   brought up to it, its spare room falling between the name and the
   chevron - so the filters read as a set of equal controls until a count
   widens one. In another system face the names come out a pixel or so either
   side of this, and a longer one sets its own button's width. Ticked, every
   button is wider than the floor and sizes to its label.

   The height is pinned rather than left to the line box. A <button> and a
   <select> do not lay their text out identically even given the same font and
   padding, and one pixel between two boxes side by side in a centred flex row
   is visible. .add-btn pins its own size for the same reason. box-sizing is
   border-box globally, so --control-h is the outside. */
.filter-button {
  display: inline-flex;
  align-items: center;
  min-width: 100px;
  height: var(--control-h);
  padding: 0 26px 0 13px;
  font: inherit;
  font-size: 13.5px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border-sw);
  border-radius: var(--radius);
  outline: none;
  cursor: pointer;
  white-space: nowrap;      /* "Platform (2)" never breaks across two lines */
  transition: border-color 0.13s ease, box-shadow 0.13s ease;
}

/* The tick count, " (2)" on the end of the label, and 2px of margin after
   it (see .filter-button). It takes room only while something is ticked -
   app.js sets [hidden] on it otherwise - so a filter with nothing ticked is
   only as wide as its name and the floor make it. The exception is a button
   at the end of a full line, which taking or giving back the room with its
   panel open would move onto another line (see checkboxFilter). There the
   room waits for the panel to shut, and a count keeping its room with nothing
   left ticked loses its `on` instead: visibility keeps the space and drops
   the text, from a screen reader too.

   tabular-nums so every count is the same width: without it a 1 is narrower
   than an 8 in most UI faces, and the button would change width as the count
   changed. */
.filter-count {
  margin-right: 2px;
  visibility: hidden;
  font-variant-numeric: tabular-nums;
}

.filter-count.on { visibility: visible; }

/* The same ring the search box and the select use - keyboard focus only. The
   open and ticked states below take the border alone, so the ring stays what
   it is everywhere else on the site: the mark of having tabbed somewhere. */
.filter-button:focus-visible {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

/* The accent edge while the panel is open, and for as long as anything is
   ticked in it, open or shut - so beside the count, the row marks which
   filters are narrowing the table at a glance. Unticking the last box, or
   Clear Filters, takes it away. */
.filter-button[aria-expanded="true"],
.filter-button.ticked { border-color: var(--accent); }

/* The chevron turns over while the panel is down, so the button says which way
   it is pointing even when its label has not changed. The translateY has to be
   repeated: transform replaces, it does not compose. */
.filter svg { transition: transform 0.13s ease; }

.filter-button[aria-expanded="true"] ~ svg {
  transform: translateY(-50%) rotate(180deg);
}

/* Anchored to .filter rather than placed in the row: the row is a flex line,
   and an in-flow panel would shove whatever sits beside it sideways every time
   it opened.

   Pinned to the button's right edge, not its left. This is the last control in
   .search-row, so its right edge is inside the content box by construction and
   a panel growing leftwards from it can never push the page into horizontal
   scroll - which a left-anchored one would, as soon as a platform name made it
   wider than its button. Swap to `left: 0` if the control stops being last.

   That is where it opens. While it is open, app.js holds it there by its left
   edge, since its button widens and narrows as the tick count comes and goes
   (see checkboxFilter).

   --border rather than --border-sw: this is a panel, not a control. */
.filter-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 100%;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 4px 12px rgba(26, 22, 18, 0.1);

  /* The results table's <thead> is sticky at z-index: 1 - until now the only
     z-index in this file - and the table starts immediately below this row, so
     without this the panel opens behind the column headings. It works because
     neither element sits in a stacking context of its own, which makes that an
     invariant: a transform, filter, opacity or will-change landing on
     .page-head or .search-row later would quietly create one and break this. */
  z-index: 2;
}

/* Eight platforms in one column makes a panel nearly as tall as the table it
   filters, and the alphabet splits the families that would make it scannable -
   3DS, DS, GameCube, GBA, Switch, Switch 2, Wii, Wii U. The platform filter
   deals its boxes into .filter-col columns instead, consoles down one and
   handhelds down the other, and this class is how the panel says so.

   A class rather than [data-platform-filter]: the bare .filter-panel has to
   stay a block, or the Item Type rows would lay themselves out in a line. The
   width this adds all grows leftwards, because the panel is pinned to its
   button's right edge - which is what `right: 0` above is there for, and why a
   second column cannot push the page sideways. [hidden] is !important, so a
   panel in columns still shuts. */
.filter-panel.columns {
  display: flex;
  align-items: flex-start;   /* each column ends where its own last box does */
  gap: 2px;
}

/* Grows into whatever width the panel has over its columns, and never shrinks
   below its own - so a single column still fills its panel and its hover bands
   still run the width of it, the way they did before there were columns at
   all. The boxes inside are nowrap, so shrinking could only spill them. */
.filter-col {
  display: flex;
  flex-direction: column;
  flex: 1 0 auto;
}

/* All of it on the last column, though, when there is more than one. An
   open panel widens to keep up with its button taking the tick count (see
   checkboxFilter), and shared between the columns those pixels would walk
   the second one sideways under the pointer. On the last, every box stays
   where it was. */
.filter-col:not(:last-child) { flex-grow: 0; }

.filter-check {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 9px;
  border-radius: 7px;
  font-size: 13.5px;
  line-height: 1.3;
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;      /* "Switch 2" is one name, not two lines */
}

/* The account menu's hover, so the site's dropdowns answer the mouse the same
   way. The underline lands on the label only - a checkbox draws no decoration. */
.filter-check:hover {
  background: var(--bg);
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The site's first checkbox. Left as the native control and tinted, rather
   than hidden behind a hand-built box: accent-color keeps the platform's own
   focus ring, its touch target and its forced-colors rendering, none of which
   a <span> pretending to be a checkbox gets back for free.

   color-scheme because the unchecked box is drawn by the OS, not by this
   stylesheet: without it the box stays a white square on a dark panel once the
   dark palette is in play. Scoped to the control - on :root it would restyle
   every native widget on the site. */
.filter-check input {
  flex: none;
  width: 14px;
  height: 14px;
  margin: 0;
  accent-color: var(--accent);
  color-scheme: light dark;
  cursor: pointer;
}

/* Clear Filters. .linkish makes it a button that reads as a link - it is an
   action rather than a destination, and a third bordered box in the row would
   read as a third filter. Sized to the filter buttons so it sits on their
   centre line, and full height so the hit area is the height of the row rather
   than of the text. Scoped through .search-row because .linkish comes later in
   this file and would otherwise take the padding and the font straight back.

   Underlined on hover, as the site's other text actions are. */
.search-row .clear-filters {
  height: var(--control-h);
  padding: 0 4px;
  font-size: 13.5px;
  white-space: nowrap;
}

.search-row .clear-filters:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The 40/22 padding above is tuned for a heading, which wants more air above it
   than between it and its table. The search page's head holds only the search
   box, so that asymmetry just reads as the box sitting low. An even split
   centres it between the header rule and the table; a search box on its own
   needs less air around it than a heading does, so the band is tighter than
   the 62px the base rule would give it. */
[data-page="search"] .page-head { padding: 22px 0; }

/* And the same 22px below the table, so the page is framed evenly: the band
   between the header rule and the search box matches the one between the table
   and the footer rule. The card's own 40px is tuned for the listings page,
   where the head is a heading. */
[data-page="search"] .table-card { margin-bottom: 22px; }

/* ---------------------------------------------------------------- table -- */

.table-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow-x: auto;
  margin-bottom: 40px;
}

/* The header row rides down the page with the reader instead of scrolling off
   the top of it. `thead th` below is already sticky, but sticky measures
   against the nearest scrolling ancestor, and `overflow-x: auto` above makes
   this card one: the row stuck to the card's own top edge, which is where it
   was sitting anyway. `clip` clips the same corners - which is all the
   rounding wants of it - without establishing a scrollport, so the row sticks
   to the window instead.

   What `clip` will not do is scroll, so it can only have the card once the
   table already fits inside it. 780px of table, the card's 2px of border, the
   wrap's 48px of gutter and the ~15px `scrollbar-gutter` holds back come to
   845px of window; 860 leaves a little slack. The collection's table is 880px
   wide, so it clears at 960, and the watchlist's 720px at 800. Narrower than that the card keeps its horizontal
   scroll and the header behaves as it does today - as it also does in a
   browser that has no `clip` (Safari before 16), which drops the line and is
   left with the `auto` above. */
@media (min-width: 860px) {
  [data-page="search"] .table-card { overflow-x: clip; }
}

@media (min-width: 960px) {
  [data-page="collection"] .table-card { overflow-x: clip; }
}

@media (min-width: 800px) {
  [data-page="watchlist"] .table-card { overflow-x: clip; }
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 11px 16px;
  text-align: left;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-3);
  white-space: nowrap;
}

/* Price columns are right-aligned, but headers keep the label typography. */
thead th.num {
  font-family: var(--sans);
  font-size: 10.5px;
  font-variant-numeric: normal;
}

thead th.sortable { cursor: pointer; user-select: none; }
thead th.sortable:hover { color: var(--ink); }

/* The sort arrow. Drawn here as a triangle rather than left to the ▲ in the
   markup, which font-size: 0 hides. The glyph put arrows at different
   heights: turned over for a descending sort it pivoted on the middle of its
   line box, which is not the middle of the glyph, so it dropped by a pixel
   or so; and the price columns took theirs out of the text flow, which
   seated it by its line box rather than by the text's baseline. A drawn
   triangle has no such slack - pointing up or down, it fills the same 8x7
   box.

   That box stands on the label's baseline - an inline-block whose overflow
   is hidden takes its baseline from its bottom edge - and 7px is the height
   of the capitals at 10.5px, so the arrow sits level with the letters of
   every heading, sorted either way.

   Transparent until its column is sorted, but not absent: it keeps its
   width, so a heading does not shift when the sort lands on it. */
thead th .arrow {
  display: inline-block;
  width: 8px;
  height: 7px;
  margin-left: 5px;
  overflow: hidden;
  vertical-align: baseline;
  font-size: 0;
  background: currentColor;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
  opacity: 0;
}

thead th[data-dir] .arrow { opacity: 1; }
thead th[data-dir="desc"] .arrow { clip-path: polygon(0 0, 100% 0, 50% 100%); }
thead th[data-dir] { color: var(--ink); }

/* In a right-aligned heading that kept width would sit between the label and
   the cell edge, and every price heading would hang ~13px left of the
   numbers under it. A negative right margin the size of the arrow and its
   gap takes it back out of the line's width without taking it out of the
   line: the label ends exactly where the values do, the triangle sits in the
   gutter that was already there - 16px, or the last column's wider one - and
   it stays on the same baseline as every other heading's arrow. */
thead th.num .arrow { margin-right: -13px; }

tbody td {
  padding: 11px 16px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--bg); }

/* Results table (search + collection). Fixed columns for the same reason as
   the sold-listings table further down: left to size themselves, Title
   stretches to whichever game has the longest name in the current result set,
   so the price columns move on every keystroke. Title carries no width and
   absorbs the remainder - at ~592px it clears the longest title in the
   catalogue (511px including padding) with room to spare. */
.results-table {
  table-layout: fixed;
  min-width: 780px;      /* below this the card scrolls rather than crushing */

  /* Right gutter of the last column. Both the padding and the sort arrow's
     inset are derived from it, so they cannot drift apart. */
  --gutter-r: 30px;
}

/* The collection table: Condition, then its three money columns. The search
   table has its own second column and overrides every one of them below. */
.results-table th:nth-child(2) { width: 246px; }   /* Condition      */
.results-table th:nth-child(3) { width:  92px; }   /* Total cost     */
.results-table th:nth-child(4) { width:  92px; }   /* Current value  */
.results-table th:nth-child(5) { width: 106px; }   /* Difference     */

/* The search table, whose second column is Item Type. It is sized to its own
   longest value - "Controller", with a few px over for system fonts that set
   wider - plus the 16px gutters and the sort arrow, and no more: packed
   against the prices, so everything left over falls to the title, which is the
   column with something to say and which carries the platform after it, as
   the list pages' titles do. Item Type still reads apart from the price trio
   without a gap between them, because it is a left-aligned word and those
   three are right-aligned figures.

   A price column is 108px: 76px of figure between the 16px gutters, which
   holds a five-figure price to the cent - "$11,355.50" - in a Mac's
   monospace as well as Windows'. At 92px anything from $1,000 up ran past
   the right edge of the prices above it. New $ is 14px wider than its
   neighbours purely to absorb its own 14px of extra right gutter, so all
   three prices end up an even 108px apart.

   No min-width of its own: the shared 780px leaves the title a 324px floor,
   room for a long title and its platform, and is the width the sticky
   header's `clip` breakpoint above is worked out from. */
[data-page="search"] .results-table th:nth-child(2) { width: 118px; }   /* Item Type */
[data-page="search"] .results-table th:nth-child(3) { width: 108px; }   /* Loose $   */
[data-page="search"] .results-table th:nth-child(4) { width: 108px; }   /* CIB $     */
[data-page="search"] .results-table th:nth-child(5) { width: 122px; }   /* New $     */

/* New $ is right-aligned against the card edge, so widening the column cannot
   pull it clear - only the padding can. Applied to the header too, which is
   right-aligned with it. */
.results-table th:last-child,
.results-table td:last-child { padding-right: var(--gutter-r); }

/* The arrow follows the label rather than the cell's edge, so the wider
   gutter needs nothing of its own for it: it sits the same 5px after the
   label here as in every other price column. */

.c-title { font-weight: 520; }
.c-title a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

.c-meta { color: var(--ink-3); white-space: nowrap; }

.num {
  text-align: right;
  font-family: var(--mono);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.num.empty { color: var(--ink-3); }

.empty-state {
  padding: 44px 16px;
  text-align: center;
  color: var(--ink-3);
  font-size: 13px;
}

/* Shown when data/index.json cannot be fetched - almost always because the
   page was opened from the filesystem rather than served over HTTP. */
.load-error {
  max-width: 620px;
  margin: 64px auto;
  color: var(--ink-2);
  font-size: 14px;
}

.load-error h1 {
  margin: 0 0 14px;
  font-size: 21px;
  font-weight: 640;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.load-error p { margin: 0 0 12px; }

.load-error pre {
  margin: 0 0 14px;
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--ink);
  overflow-x: auto;
}

/* Trailing note when a result set is larger than the render cap. */
.more-row {
  padding: 13px 16px;
  text-align: center;
  color: var(--ink-3);
  font-size: 12px;
  background: var(--bg);
}

/* ----------------------------------------------------------- game page --- */

/* The 19px here and the matching padding-top on .game-head below split the gap
   between the header rule and the title evenly, so the link sits centred in the
   band rather than drifting down toward the title. */
.back {
  display: inline-block;
  margin: 19px 0 0;
  font-size: 12.5px;
  color: var(--ink-3);
}

.back:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

/* Title row: the text on the left, the add button hard against the right edge.
   The two pieces of text are their own flex group so this outer row has just
   two items to place - the button centres against the text as a block, and the
   baseline tuning below stays where the text is. The padding is trimmed from
   the old 12/26 top and bottom by the 4px the 40px button overhangs the 31px
   title line, so the title and everything under it stay where they were. */
.game-head {
  padding: 8px 0 21px;
  display: flex;
  align-items: center;
  gap: 16px;
}

/* The title and platform read as one unit, so they share a line. Baseline
   alignment keeps them sitting on the same rule despite the size difference;
   the platform drops underneath only when a long title runs out of room, and
   the row gap is what separates them in that wrapped case. */
.game-head-text {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 10px;
  min-width: 0;          /* a long title wraps rather than shoving the button */
}

.game-head h1 {
  margin: 0;
  font-size: 26px;
  font-weight: 640;
  letter-spacing: -0.022em;
  line-height: 1.2;
}

/* `top` is an optical nudge, not layout: a shared baseline is geometrically
   correct but reads slightly high next to the much larger title, so the
   platform is pushed down a couple of pixels to sit level with it. Retune it if
   the gap between the two font sizes changes. */
.game-head .sub {
  position: relative;
  top: 0px;
  font-size: 20px;
  letter-spacing: -0.022em;
  line-height: 1.2;
  color: var(--ink-3);
}

/* The watchlist star and the add button, together at the end of the title
   row. `margin-left: auto` is what right-aligns them: the text takes the
   width it needs and this eats the rest. Closer to each other than to the
   title, so they read as a pair. */
.game-actions {
  flex: none;
  display: flex;
  gap: 8px;
  margin-left: auto;
}

/* Square icon button: the add button. */
.add-btn {
  flex: none;

  /* `top` is an optical nudge, not layout: centring puts the button on the
     middle of the title's line box, but the title's ink sits low in that box
     (there is more room above the capitals than below the baseline), so dead
     centre reads a touch low against the text. Two pixels up settles it, and
     paint moves while the row height does not. */
  position: relative;
  top: 4px;

  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--border-sw);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
  cursor: pointer;
  transition: border-color 0.13s ease, color 0.13s ease, background 0.13s ease;
}

.add-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.add-btn:active {
  background: var(--accent-sw);
  box-shadow: none;
}

/* The same focus ring the condition tabs and the search box use. */
.add-btn:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

/* The watchlist star: the icon on its own, with no box round it. It keeps
   the add button's height and nudge, so the two glyphs sit on one line and
   the star is still a button-sized target, but it is narrower than the
   add button's 40px, so the bare icon sits nearer the box beside it. The
   icon is drawn at 20px rather than the + button's 16px, because with no
   frame round it the star has to fill the space on its own. */
.watch-btn {
  flex: none;
  position: relative;
  top: 4px;
  width: 32px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--ink-2);
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  transition: color 0.13s ease;
}

.watch-btn:hover { color: var(--accent); }

/* No border to light up, so the ring alone - the trash icon's. */
.watch-btn:focus-visible {
  outline: none;
  color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

/* ----------------------------------------------------------- games table -- */



/* Each game row opens its own sold-listings page. */
tr.pick { cursor: pointer; }
tr.pick:hover .c-title a { color: var(--accent); }

/* ------------------------------------------------- box art + game details -- */

/* Cover on the left, details beside it; they stack on narrow screens.
   `stretch` lets the chart panel match the box art frame's height exactly. */
.game-top {
  display: flex;
  align-items: stretch;
  gap: 32px;
  flex-wrap: wrap;
  margin-bottom: 30px;
}

/* The frame is as tall as the art in it, and the chart stretched beside it
   matches, so the row ends where the picture (or the details, when those run
   longer) ends. A fixed 368px frame kept the chart one height on every
   listing, but left a band of empty frame under anything shorter than a
   618x1000 cover - DS and Wii cases, the placeholder, hardware photos. */
.boxart {
  margin: 0;
  flex: none;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  width: 228px;
}

/* The direct child only: the preview strip below puts its own <img> inside
   this figure, and a descendant selector would size those like a cover. */
.boxart > img {
  display: block;
  width: 228px;
  height: auto;          /* real cover art keeps its own proportions */
  max-height: 368px;     /* anything taller than a cover scales down to fit */
  /* A standard cover's shape until the file arrives, then the picture's own.
     Most art is exactly that shape, so most pages do not move when it loads. */
  aspect-ratio: auto 618 / 1000;
  background: var(--surface);
  border: 1px solid var(--border);
}

/* Hardware is photographed square, so a console or controller came out 228
   wide in a column cut for a cover, dwarfed by the art on every other page.
   These get a squarer frame instead, bounded to 294px on both axes.

   294px is the ceiling on the width, not a target: the chart is pinned to
   half the row whatever the image does, so anything wider pushes it onto its
   own line, where it collapses to its min-height and goes squat. */
.boxart.wide {
  width: 294px;
  max-width: 100%;
}

.boxart.wide > img {
  /* A fixed box, rather than one fitted to whatever picture is in it.
     Hardware photos are all roughly square but not identically so - item
     71046 carries a 1008x805 and a 914x914 - and letting each one size the
     box moved everything beneath it the moment you switched between them.

     `contain` is what makes a width/height pair safe: it scales the picture
     into the box and never stretches it, which is the squash this rule used
     to avoid by setting neither. Whatever is left over is the frame's own
     surface, which is white in light mode - the same ground these product
     shots are photographed on. */
  width: 294px;
  max-width: 100%;
  height: 294px;
  object-fit: contain;
}

/* Only while renderGame has made the frame a control: it drops the role when
   the cover 404s and the placeholder stands in, and the cursor goes with it.
   zoom-in rather than pointer - what happens is an enlargement, not a
   navigation, and the two cursors are the browser's own way of saying so. */
.boxart > img[role="button"] { cursor: zoom-in; }

/* The same focus ring the previews below it use, on the border the image
   already has. */
.boxart > img[role="button"]:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

/* Previews under the image, for an item with more than one. The figure turns
   into a column, which swaps what the two alignment properties mean: the
   vertical axis becomes `justify-content` and the horizontal `align-items`.
   So both are restated here: `flex-start` keeps the top alignment the frame
   already had, and `center` has to be spelled out again or the base rule's
   `align-items: flex-start` would push the column to the left edge.

   The image gives up the strip's share of a full cover's height, so a gallery
   is 368px tall however many pictures it holds. */
.boxart.has-thumbs {
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
}

/* 48px of preview - 46 plus its 1px border either side - and the 8px above.

   A fixed height, not a max-height, for the same reason .boxart.wide > img
   has one: sized to its picture, the image moved the strip beneath it on
   every click between two shapes - item 13801 carries a 1091x923 and a
   1808x1097, 54px apart at this width. `contain` fits each one into the box
   without stretching it, which a fixed width over a clamped height did not. */
.boxart.has-thumbs > img[data-boxart] {
  height: 312px;                                                     /* 368 - 56 */
  object-fit: contain;
}
.boxart.wide.has-thumbs > img[data-boxart] { height: 238px; }       /* 294 - 56 */

.art-thumbs {
  display: flex;
  flex: none;
  gap: 8px;
  margin-top: 8px;

  /* Against the left edge rather than centred under the image: a short row of
     previews centred beneath a full-width picture reads as adrift, and the
     edge is what the details and chart beside it line up on too. Overrides
     the frame's `align-items: center`, which holds for the image above. */
  align-self: flex-start;
}

.art-thumb {
  padding: 0;
  line-height: 0;                /* no descender gap under the picture */
  cursor: pointer;
  border: 1px solid var(--border);
  border-radius: 3px;
  background: var(--surface);
}

/* contain: a tall cover and a square hardware photo sit in the same box
   without either being stretched to fill it. */
.art-thumb img {
  display: block;
  width: 46px;
  height: 46px;
  object-fit: contain;
}

/* The one showing above, and the one being tabbed to, read the same. */
.art-thumb[aria-pressed="true"],
.art-thumb:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

/* --------------------------------------------------------- sold listings -- */

.h-sold { font-size: 18px; }

.nowrap { white-space: nowrap; }

/* Fixed columns, so switching condition tabs changes only the rows. Left to
   size themselves, the columns would track whichever titles and prices the
   current tab happened to contain and shift on every switch. Listing Title is
   the one column without a width, so it absorbs the remainder. */
#sold-panel table {
  table-layout: fixed;
  min-width: 894px;      /* below this the card scrolls rather than crushing */
}

/* 95px is as tight as Date Sold goes. The binding content is the heading, not
   the dates: "Date Sold" plus its sort arrow runs 91.9px from the padding
   edge, which leaves the arrow 3px off the border - the same gutter the price
   headings pin theirs to, so the two conventions agree by arithmetic rather
   than by coincidence. The widest date numericDate() can emit, 12/28/2026,
   ends 12.8px short of that. Every px taken here is a px Listing Title gets,
   which is the only column that can use it. */
#sold-panel th:nth-child(1) { width:  95px; }   /* Date Sold   */
#sold-panel th:nth-child(6) { width: 232px; }   /* eBay Seller */

/* The three prices are one figure read three ways, so they share a width and
   sit at an even pitch instead of the 112/100/112 they used to - three widths
   set the numbers drifting apart across the row for no reason the reader can
   see. 98px is the tight fit: both the longest heading, "Sold Price", and a
   four-figure sale ($4,432.22) clear the 16px gutter by about 4px, and a
   five-figure one lands flush against it. The 36px this frees goes to Listing
   Title, the only column without a width. */
#sold-panel th:nth-child(3),                    /* Sold Price  */
#sold-panel th:nth-child(4),                    /* Shipping    */
#sold-panel th:nth-child(5) { width: 98px; }    /* Total       */

/* The report column. Just wide enough for the 26px button and the right-hand
   gutter. The 12px of left padding plus the seller cell's 16px keeps it clear
   of the feedback figures. On a wide screen the 44px comes out of Listing
   Title, as any new column's width would. The table's min-width above grew
   by the same 44px (from 850px), so a narrow screen scrolls 44px sooner
   rather than squeezing the title further. */
#sold-panel th.c-report { width: 44px; }

/* Right padding of 6px, not the 16px gutter. The glyph sits 5.5px inside
   its button, so this lines the icon's right edge up near the gutter the
   price figures end at. */
#sold-panel th.c-report,
#sold-panel td.c-report {
  padding-left: 12px;
  padding-right: 6px;
  text-align: right;
}

/* Muted until pointed at, red on hover - .icon-btn.danger. Once this page
   has sent a report for the row, the icon stays red, so it is clear which
   listings have already been flagged. */
.report-btn.reported { color: var(--down); }

.c-listing { color: var(--ink-2); }

.c-seller {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-3);
}

/* Handle and feedback sit on one line, but only the handle gives way. The flex
   is on a span inside the cell rather than on the cell itself, which would
   stop being a table cell and take the fixed column widths with it. */
.c-seller .seller-line {
  display: flex;
  align-items: baseline;
}

.c-seller .s-name {
  min-width: 0;              /* or a flex item refuses to shrink below its text */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-2);       /* the handle leads; the feedback sits behind it */
}

/* pre keeps the space this span begins with - the one the handle was split
   on - so the cell still reads, and copies, as "colts_closet (622) 99.8%". */
.c-seller .s-fb {
  flex: none;                /* "(622) 99.8%" is never what gets trimmed */
  white-space: pre;
}

/* Total is the number people actually compare, so it carries the weight. */
td.total { color: var(--ink); font-weight: 600; }

td.free {
  color: var(--up);
  font-family: var(--sans);
  font-size: 12px;
}


.price-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
  margin-bottom: 24px;
}

/* The "Sold listings" heading follows the condition tabs rather than the site
   header, so it does not need the page-head's full top padding. Together with
   the grid margin above, this leaves ~30px - the same gap the box-art row uses. */
[data-page="listings"] .page-head { padding-top: 6px; }

.price-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px;
}

/* On the game page the cards are the condition tabs for the table below. */
button.price-card {
  display: block;
  width: 100%;
  font: inherit;
  color: inherit;   /* buttons default to the system ButtonText, black in Firefox */
  text-align: left;
  cursor: pointer;
  transition: border-color 0.13s ease, background 0.13s ease;
}

/* The border hover .add-btn and .ebay-btn use. Only the border: the accent text
   they also take is what marks the selected tab here. */
button.price-card:hover { border-color: var(--accent); }

button.price-card[aria-selected="true"] {
  border-color: var(--accent);
  background: var(--accent-sw);
}

button.price-card[aria-selected="true"] .label,
button.price-card[aria-selected="true"] .value { color: var(--accent); }

button.price-card:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

.price-card .count {
  margin-top: 5px;
  font-size: 11.5px;
  color: var(--ink-3);
}

.price-card .label {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.price-card .value {
  margin-top: 6px;
  font-family: var(--mono);
  font-size: 23px;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.price-card .delta { margin-top: 3px; font-size: 11.5px; }
.delta.up   { color: var(--up); }
.delta.down { color: var(--down); }
.delta.flat { color: var(--ink-3); }

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px 22px;
  margin-bottom: 32px;
}

.panel h2 {
  margin: 0 0 16px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 14px;
}

/* Legend entries double as series toggles. */
.legend-btn {
  display: inline-flex;
  align-items: center;
  padding: 4px 9px;
  font: inherit;
  font-size: 11.5px;
  color: var(--ink-2);
  background: none;
  border: 1px solid transparent;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.13s ease, color 0.13s ease, opacity 0.13s ease;
}

.legend-btn:hover { background: var(--bg); color: var(--ink); }

.legend-btn:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

.legend-btn.off { opacity: 0.45; text-decoration: line-through; }

.legend-btn i {
  display: inline-block;
  width: 11px;
  height: 3px;
  margin-right: 7px;
  border-radius: 2px;
}

/* Details fill whatever the cover leaves on their line, so a long note or
   developer list wraps as little as it can, and the column is the same width
   on every listing rather than following its longest value. The 210px basis
   decides when they drop below the cover on a phone. */
.game-top .spec { flex: 1 1 210px; min-width: 132px; }

.game-top .panel {
  flex: 1 1 400px;
  min-width: 0;          /* lets the SVG shrink instead of forcing overflow */
  margin-bottom: 0;
  display: flex;
  flex-direction: column;
}

/* flex-basis 0 stops the SVG's own aspect ratio from setting the panel height;
   it takes whatever vertical space the cover leaves, and the script draws to
   that box. Without this the panel grows taller than the cover on wide screens. */
.game-top .panel svg {
  flex: 1 1 0;
  min-height: 120px;
}

/* Once the cover, details and chart share one line, the chart takes the exact
   span of the New and Graded cards below and squares up with them. Those are
   two of the four cards plus the 12px gap between them, which is half the row
   less 6px whatever the window width. Pinning the details column's basis keeps
   a long series - some run to two lines - from claiming enough of the line to
   push the chart down onto its own row. */
@media (min-width: 920px) {
  .game-top .spec { flex: 1 1 0; }

  .game-top .panel {
    flex: 0 0 calc(50% - 6px);
    margin-left: auto;      /* stay on the right edge, against the grid below */
  }
}

/* Label above its value, so each detail reads as one block. Side by side the
   two columns were held apart by the widest value on the listing, which left
   most rows with a stretch of empty space between label and answer. */
.spec {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding-top: 2px;          /* optically level with the top of the cover */
  font-size: 18px;
}

/* The same label treatment as the condition cards below.

   The gap above each label is 10px to 15px, whichever makes the column end
   level with the cover. The column is stretched to the row's height and every
   label but the first grows into the spare room, its text held to the bottom,
   up to 5px taller than itself; room left after that sits under the last row.
   10px is the floor that still fits a two-line Note beside a cover. */
.spec dt {
  flex: 1 0 auto;
  max-height: calc(1.2em + 5px);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  margin-top: 10px;
  line-height: 1.2;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.spec dt:first-child { margin-top: 0; flex-grow: 0; }

/* All six details, set by app.js. Such a column is nearly the cover's height
   already, so the gaps may grow as far as it takes to end level with it. With
   fewer rows the same stretch spreads two or three of them down the whole
   cover, which is why the 15px ceiling above holds everywhere else. */
.spec.complete dt { max-height: none; }

.spec dd { margin: 3px 0 0; line-height: 1.35; }
.spec:empty { display: none; }

/* A sentence rather than a fact, so it is set small: at the 18px of the rows
   above, a note wraps to four lines and pushes the row past the cover. */
.spec dd.note { font-size: 13px; line-height: 1.4; }

/* Beside the Sold listings heading. The snippet this came from carried its
   colours and its hover states inline; both move into the stylesheet here so
   the button follows the palette into dark mode. The hover is .add-btn's
   rather than the snippet's lift-and-bloom, so the two controls on this page
   answer the cursor the same way. */
.ebay-btn {
  /* `top` is an optical nudge, not layout: the row lines up bottom edges, and
     the button is taller than the heading's line box, so bottom-aligned its
     middle sits 5px above the middle of the heading's capitals. Paint moves
     while the row height does not, so the heading and the table stay put and
     only the gap under the button narrows. Where the button wraps under the
     heading, below about 340px, the nudge goes with it; 5px is too little
     there to earn a breakpoint. Retune it if the heading's size changes. */
  position: relative;
  top: 5px;

  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 11px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  box-shadow: 0 1px 2px rgba(26, 22, 18, 0.04);
  transition: border-color 0.13s ease, color 0.13s ease, background 0.13s ease;
}

.ebay-btn img { display: block; height: 20px; width: auto; }

/* Only the "Search on" text takes the accent; the logo beside it is a raster
   image and keeps eBay's own colours, where .add-btn's glyph is an SVG on
   currentColor and turns with the border. */
.ebay-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.ebay-btn:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

/* Metacritic score, in Metacritic's own colour bands. Fixed colours rather
   than theme tokens: the white numeral has to stay legible in both themes. */
.mc {
  display: inline-block;
  min-width: 27px;
  padding: 1px 6px;
  border-radius: 4px;
  font-family: var(--mono);
  font-size: 18px;
  font-weight: 600;
  text-align: center;
  color: #fff;
}

.mc.good  { background: #1a7f4b; }
.mc.mixed { background: #a8730a; }
.mc.bad   { background: #a83a2c; }

/* --------------------------------------------------------------- footer -- */

/* The 20px below the last line is the header's own 20px above the logo
   (`.brand-row`), so the page is bounded by the same gap top and bottom. */
.site-foot {
  border-top: 1px solid var(--border);
  padding: 22px 0 20px;
  font-size: 11.5px;
  color: var(--ink-3);
}

/* The left-hand lines, stacked. A flex gap rather than a margin on each line,
   because the counts start out `hidden` and a margin would then leave its gap
   behind above the contact line - a gap between display:none children never
   renders at all. */
.foot-meta {
  display: flex;
  flex-direction: column;
  gap: 5px;

  /* Set in from the card's left edge, which `.wrap` already puts it flush
     with. Half the 16px cell padding above: flush reads as slightly adrift of
     the table, and the full 16px pulls the footer all the way out to the first
     column's text, which makes this small print look like another row of it.
     Half clears the edge without joining the column - an optical figure, not a
     structural one, so it is the one number here to adjust by eye. */
  padding-left: 4px;
}

/* The coverage counts. The bolded figures pick up --ink-2 so they read as the
   line's subject without leaving the footer's quiet register; the line
   inherits the footer's 11.5px. */
.foot-stats b { font-weight: 600; color: var(--ink-2); }

/* The break between the tracker and the contact lines. No width of its own: it
   fills .foot-meta, and .foot-meta is only ever as wide as its longest line -
   the counts - so the rule ends where the tracker text ends and follows it if
   that text changes. Same hairline as the footer's own top border.

   The margin is the difference between the 5px that sets one line off from the
   next and the wider gap a break between two groups wants. It is on both
   edges: .foot-meta's gap adds its 5px either side, so the rule sits 9px clear
   of each group rather than closer to the one below it. */
.foot-rule {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 4px 0;
}

/* The counts start out `hidden` and stay that way if the fetch fails, and a
   rule with nothing above it separates nothing - so it goes when they go. */
.foot-stats[hidden] + .foot-rule { display: none; }

/* The contact line, under the counts. It states no size or family of its own:
   inheriting the footer's 11.5px --sans is what keeps it from drifting away
   from the line it sits under. Only the address is a link, and it takes the
   same --ink-2 / --accent treatment as the page links opposite. */

.foot-contact a { color: var(--ink-2); }

.foot-contact a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

.foot-contact a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* Discord and Twitter, under the contact line. Nothing here sets a size or a
   family either - the row is the same 11.5px --sans as the lines above it, and
   the names take the --ink-2 the address does, being the same kind of thing.

   Hover and focus are hung on `a[href]` rather than `a`: an <a> without href
   is not a link, and lighting it up on hover would promise a click that does
   not happen. */
.foot-social a { color: var(--ink-2); }

.foot-social a[href]:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

.foot-social a[href]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* The counts and the contact line stack on the left, the page links sit
   opposite them. `margin-left: auto` on the links rather than
   `justify-content: space-between` here, so they stay right once the row wraps
   and they are alone on their line. */
.site-foot .wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 24px;

  /* The links are a size up from the rest of the footer, so the two sides are
     hung from their first baseline rather than their top edges - aligning the
     boxes would leave the larger text sitting visibly low. */
  align-items: baseline;
}

/* Sized and coloured to match the header's account links: this is the same
   kind of navigation, at the other end of the page, and at the footer's own
   11.5px it read as small print instead. */
.foot-links {
  margin-left: auto;
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 13px;
}

.foot-links a { color: var(--ink-2); white-space: nowrap; }

.foot-links a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

.foot-links a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* Punctuation between the links rather than content: hidden from screen
   readers, which already read them as two items. A step below the links, but
   still ink - at border weight it all but disappears on the footer's ground.
   Scoped to the footer rather than to one row, so the social names and the
   page links are divided by the same mark. */
.site-foot .sep { color: var(--ink-3); }



/* --------------------------------------------------------- account nav -- */

.account-nav {
  display: flex;
  align-items: center;
  gap: 18px;
  font-size: 13px;
}

.account-link {
  color: var(--ink-2);
  white-space: nowrap;
}

.account-link:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

/* My Account, and the panel it opens: My Collection and My Watchlist. The
   panel is the filters' panel - same surface, border, shadow and rows - hung
   from the button's right edge, which is inside the page by construction, so
   it can never push the header into horizontal scroll. */
.account-menu { position: relative; }

.account-menu-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* The filters' chevron, at their ink, turned over while the panel is down. */
.account-menu-btn svg {
  color: var(--ink-3);
  transition: transform 0.13s ease;
}

.account-menu-btn[aria-expanded="true"] svg { transform: rotate(180deg); }

/* In the accent while open, as an open filter's border is: the panel under
   it belongs to this button. */
.account-menu-btn[aria-expanded="true"] { color: var(--accent); }

.account-menu-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  display: flex;
  flex-direction: column;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 4px 12px rgba(26, 22, 18, 0.1);

  /* Above the results tables' sticky headings, as .filter-panel is. */
  z-index: 2;
}

.account-menu-item {
  padding: 6px 9px;
  border-radius: 7px;
  font-size: 13.5px;
  line-height: 1.3;
  color: var(--ink);
  white-space: nowrap;
}

/* The accent and the underline every other link on the site takes on hover,
   over the filter rows' hover band. */
.account-menu-item:hover {
  background: var(--bg);
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.account-menu-item[aria-current="page"] { color: var(--accent); }

.account-menu-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Sign in, as a filled button rather than a quiet link: signing up is the
   one thing every page wants from a visitor who has not, so it carries the
   weight in the header throughout rather than on the homepage alone. The
   account page is the exception, and it is handled where the link is built:
   app.js leaves it out there, because the answer to "sign in" is the form
   already on screen.

   Colours come from the same --accent / --on-accent pair .btn-primary uses,
   so this matches the account page's own Sign in button and flips with it in
   dark mode, where the accent lightens and the text on it goes near-black.

   The hover rule above has to be undone rather than left to cascade: accent
   text on an accent fill is purple on purple. Dimming the whole button is
   what .btn-primary does, so it is what this does. */
.account-cta {
  padding: 5px 13px;
  color: var(--on-accent);
  background: var(--accent);
  border-radius: var(--radius);
  font-weight: 550;
  transition: opacity 0.13s ease;
}

/* Centre the letters rather than the line box. A line box carries the font's
   ascent and descent, and Segoe UI's ascent is far taller than its capitals, so
   equal padding left the text ~1px low on Windows. Trimming to cap height and
   baseline makes the padding measure from the letters themselves, whatever the
   font; 10.5px keeps the button the same ~30px tall. Browsers without text-box
   keep the plain padding above. */
@supports (text-box: trim-both cap alphabetic) {
  .account-cta {
    text-box: trim-both cap alphabetic;
    padding-block: 10.5px;
  }
}

.account-cta:hover {
  color: var(--on-accent);
  text-decoration: none;
  opacity: 0.88;
}

/* A button that reads as a link. Used for sign-out and the row actions,
   where the thing is an action rather than a destination - `button` is the
   correct element, and this stops it looking like a form control. */
.linkish {
  padding: 0;
  font: inherit;
  color: var(--ink-2);
  background: none;
  border: 0;
  cursor: pointer;
}

.linkish:hover { color: var(--accent); }

/* Edit is a link in everything but element, so it takes the hover underline the
   real links wear - the footer's and the header's account links both mark
   themselves this way, and a row action that only changed colour read as the
   one link on the page that had been left out. Scoped to the row rather than
   applied to every .linkish: the sign-out and account buttons already carry
   .account-link, which underlines them, and the delete icon has no text to
   rule. The sign-in page's "Create an account" / "Sign in" switch is the same
   case, so it joins the rule. */
.row-actions .linkish:hover,
.auth-switch .linkish:hover { text-decoration: underline; text-underline-offset: 2px; }
.linkish.danger:hover { color: var(--down); }
.linkish:disabled { opacity: 0.5; cursor: default; }

.linkish:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* Present for screen readers, absent visually: the Remove column has no
   heading worth showing, but a blank <th> is a hole in the table for anyone
   navigating it by voice. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ------------------------------------------------------------- buttons -- */

.btn-primary {
  padding: 10px 18px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 550;
  color: var(--on-accent);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  cursor: pointer;
  transition: opacity 0.13s ease;
}

.btn-primary:hover { opacity: 0.88; }
.btn-primary:disabled { opacity: 0.55; cursor: default; }

.btn-quiet {
  padding: 10px 16px;
  font: inherit;
  font-size: 13.5px;
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--border-sw);
  border-radius: var(--radius);
  cursor: pointer;
}

.btn-quiet:hover { border-color: var(--ink-3); color: var(--ink); }

.btn-primary:focus-visible,
.btn-quiet:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

/* -------------------------------------------------------------- fields -- */

.field {
  display: block;
  margin-bottom: 14px;
}

.field > span {
  display: block;
  margin-bottom: 5px;
  font-size: 12px;
  font-weight: 550;
  color: var(--ink-2);
}

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 10px 12px;
  font: inherit;
  font-size: 13.5px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border-sw);
  border-radius: var(--radius);
  outline: none;
  transition: border-color 0.13s ease, box-shadow 0.13s ease;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

/* Two fields on one line on a roomy screen, stacked when there is no room. */
.field-row {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}

.field-row .field {
  flex: 1 1 140px;
  min-width: 0;
}

/* Dialog field sizing. "CIB (Complete)" is the longest option at 89px of
   text, plus 12px of padding either side. The remainder is for the native
   dropdown arrow, and that is the part worth measuring rather than
   calculating: at 132px Chrome clipped the closing bracket, because its
   arrow claims more than the 18px that arithmetic left it. 142px renders the
   full string with the arrow clear of it. Total Cost takes the rest of the
   row, and both fields start from the same left edge as their labels. */
/* Equal halves of the row, not two fixed 142px boxes. 142px is the floor -
   what "CIB (Complete)" needs - and letting them grow from it means the pair
   always ends exactly at the content edge, which is where the buttons below
   align to. Pinned at 142 they fell 3px short of it, and the Add button
   overhung the field above by that much. */
.add-dialog .field-row .field-condition,
.add-dialog .field-row .field-paid { flex: 1 1 142px; }

/* The fields carry their own 14px bottom margin for the stacked case; inside
   a row it just adds to the gap before the buttons, which came out at 34px
   and read as the form having finished early. Zeroed here, so the spacing
   below the row is the one number in .dialog-actions. */
.add-dialog .field-row .field { margin-bottom: 0; }

/* The dialog's fields are the search page's box height, pinned rather than
   left to padding plus the line box - which came to 42.25px and read as
   oversized in a dialog this small. An input and a select both centre their
   text vertically by themselves, so the vertical padding goes. The buttons
   below keep their own padded height. */
.add-dialog .field input,
.add-dialog .field select {
  height: var(--control-h);
  padding-top: 0;
  padding-bottom: 0;
}

/* No stepper arrows on the price. Nobody nudges 34.99 up a dollar at a time,
   they type the figure - and the buttons cost width, sit under the cursor
   where a click is meant to place the caret, and turn an idle scroll over a
   focused field into a silent edit. */
.add-dialog .field-paid input {
  -moz-appearance: textfield;
  appearance: textfield;
}

.add-dialog .field-paid input::-webkit-outer-spin-button,
.add-dialog .field-paid input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* The note, under the Condition / Total Cost row and as wide as the pair of
   them - it is outside the row, so it takes the full content width, which is
   where the pair ends too. 12px above, the same as the gap between the two
   fields, so the three read as one block; nothing below, so the space to the
   buttons is still the one number in .dialog-actions. */
.add-dialog .field-item-note { margin: 12px 0 0; }

/* The character count shares the label's line, pushed to the far end, and is
   quieter than the label it sits beside. */
.field-item-note > span {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}

.field-item-note .note-count {
  font-style: normal;
  font-weight: 450;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.field-note {
  margin: -8px 0 14px;
  font-size: 12px;
  color: var(--ink-3);
}

.form-error {
  margin: 0 0 14px;
  padding: 9px 12px;
  font-size: 12.5px;
  color: var(--down);
  background: var(--surface);
  border: 1px solid var(--down);
  border-radius: var(--radius);
}

/* --------------------------------------------------------- account page -- */

/* As on the home page: release the body's 100vh so the footer follows straight
   after the card instead of being held at the bottom of the screen. */
[data-page="account"] { min-height: 0; }

/* Even margins, so the card sits centred between the header and footer rules. */
.auth-card {
  max-width: 380px;
  margin: 48px auto;
  padding: 28px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.auth-card h1 {
  margin: 0;
  font-size: 20px;
  font-weight: 640;
  letter-spacing: -0.02em;
}

.auth-card .sub {
  margin: 6px 0 22px;
  font-size: 13px;
  color: var(--ink-3);
}

.auth-card .btn-primary { width: 100%; }

.auth-switch {
  margin: 18px 0 0;
  font-size: 12.5px;
  color: var(--ink-3);
  text-align: center;
}

/* The code step's two lines, "Send a new code" and "Use a different email",
   read as one group under the button rather than two a full gap apart. */
.auth-switch + .auth-switch { margin-top: 6px; }

/* The code from the sign-up email: large, spaced and centred, so six digits
   read as one code and a mistyped one stands out. letter-spacing also lands
   after the last digit, which would pull the digits left of centre; an
   indent of the same width puts them back. */
.field-code input {
  font-size: 20px;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------- collection + watchlist pages -- */

/* No visible heading on this page, so the cards come straight after the
   header rule - 22px off it, the band the search page leaves above its search
   row, so the first thing under the header sits at the same height on both.
   On main rather than on the cards: on a failed load the cards never appear,
   and the error line below is what comes first instead. It beats .wrap's
   `padding: 0 24px` on specificity, at every width. */
[data-page="collection"] main,
[data-page="watchlist"] main { padding-top: 22px; }

/* The failed-load line. The size and ink of the count line that used to
   carry it; 14px down to the table, the gap the search row leaves above the
   table when that is showing instead. */
.list-error {
  margin: 0 0 14px;
  font-size: 12.5px;
  color: var(--ink-3);
}

/* Seven columns, not the results table's five, so its nth-child widths would
   squeeze the title into a four-line stack. Title is again the one column
   left without a width, absorbing whatever the fixed ones do not take. */
[data-page="collection"] .results-table { min-width: 880px; }

[data-page="collection"] .results-table th:nth-child(2) { width: 132px; }  /* Condition   */
[data-page="collection"] .results-table th:nth-child(3) { width: 108px; }  /* Added       */
[data-page="collection"] .results-table th:nth-child(4) { width: 116px; }  /* Total cost  */
/* Current value, the longest heading, needs 87px of its own at 10.5px in
   capitals; at 110px the cell left it 78, so the heading ran 9px past the
   numbers under it and under its own arrow. 124 gives it room with some to
   spare for a wider system font, and the title column gives up the 14. */
[data-page="collection"] .results-table th:nth-child(5) { width: 124px; }  /* Current value */
[data-page="collection"] .results-table th:nth-child(6) { width: 110px; }  /* Difference  */
/* Wide enough for "Edit" and "Remove" side by side once the cell's own 16px
   left and 30px right padding are taken off - at 92px they wrapped onto two
   lines and made every row taller. */
[data-page="collection"] .results-table th:nth-child(7) { width: 105px; }  /* actions     */

/* The watchlist's six: Added at the width of the collection's Acquired, the
   three guide prices at the search table's 108px - without the 14px New takes
   there for being last - and the trash icon alone, 26px between the cell's
   16px left padding and its 30px right gutter. */
[data-page="watchlist"] .results-table { min-width: 720px; }

[data-page="watchlist"] .results-table th:nth-child(2) { width: 108px; }  /* Added   */
[data-page="watchlist"] .results-table th:nth-child(3) { width: 108px; }  /* Loose   */
[data-page="watchlist"] .results-table th:nth-child(4) { width: 108px; }  /* CIB     */
[data-page="watchlist"] .results-table th:nth-child(5) { width: 108px; }  /* New     */
[data-page="watchlist"] .results-table th:nth-child(6) { width:  72px; }  /* remove  */

/* The list pages' search row: the search page's box and both its filters,
   and everything they look like already comes from `.search-row` and the
   rules under it - same width, same height, same panels, same focus ring. All
   that is left is where the row sits. It is block-level, so it lines up with
   the left edge of the cards and the table on its own; the grid's own 24px is
   the air above it, and this is the gap below - deliberately the tighter of
   the two, because the row belongs to the rows it filters rather than to the
   summary it hangs under. */
.list-search { margin-bottom: 14px; }

/* The search box's width - the first card's at its widest - is set with
   .search-row's, since the search page shares it.

   On the collection page it goes on following that card as the window
   narrows, rather than holding at 376px while the card slims beside it. A
   third of the row, less the grid's two 12px gaps (.price-grid's `gap`), is
   the first card's width exactly, since the row and the grid are the same
   width; the cap keeps it from ever coming out wider than the shared rule
   makes it. From 640px, where the cards sit three tracks across. Narrower,
   they stack full width and the shared rule already matches them, up to its
   376px. */
@media (min-width: 640px) {
  [data-page="collection"] .list-search .search {
    flex: 0 0 calc((100% - 24px) / 3);
    max-width: calc((var(--maxw) - 72px) / 3);
  }
}

/* And the Item Type filter starts where the second card does. The box above
   ends where the first card ends, but the row's gap is 10px and the grid's is
   12px, so the filter sat 2px short of the middle card's left edge; these
   2px make up the difference. Only where the filter shares the
   box's line - from 640px it always does. Narrower, it can wrap to the start
   of a line of its own, and there any margin would push it out of line with
   the box instead. */
@media (min-width: 640px) {
  .list-search [data-type-filter] { margin-left: 2px; }
}

/* The count line, last in the row: "Displaying all 5 items in your
   collection", or "Displaying 2 of 5 items ..." once something narrows them.
   The size and muted
   ink it had under the heading,
   a step quieter than the controls, because it reports on them rather than
   being one. The row centres it on their line, and its gap spaces it off
   whatever comes before it - the Platform button, or Clear Filters while
   that is showing. No inset of its own: on a narrow screen it wraps to the
   start of a line, and there it lines up with the box and the buttons above
   it. */
.list-count {
  margin: 0;
  font-size: 12.5px;
  color: var(--ink-3);
}

/* An empty strut at the controls' 13.5px, so the line's baseline is theirs.
   Centred in the row, smaller type sits on a higher baseline - by about half
   a pixel here - and beside Clear Filters that read as the count riding a
   hair high. The strut is the line's tallest box above and below the
   baseline, so the line box takes its shape, and centring that puts the
   baseline exactly where Clear Filters' and the buttons' is, in any font. */
.list-count::before {
  content: "";
  font-size: 13.5px;
}

/* The figures in it - how many are showing, of how many, and what they are
   worth - in the footer counts' weight and ink, so they stand out from the
   words around them without leaving the line's quiet register. */
.list-count b { font-weight: 600; color: var(--ink-2); }

/* Two cards, on the grid's three tracks: Collection Value keeps the first, and
   the value history chart spans the other two. Three tracks rather than a 1fr 2fr
   split, because a third of the row is the width the search box below is
   sized to (see .search-row .search) and the second track's edge is where its
   filters start. From 640px; narrower, one card above the other, because a
   chart in half a phone's width has no room to be read. */
[data-totals] { grid-template-columns: 1fr; }

@media (min-width: 640px) {
  [data-totals] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  [data-totals] > .price-card:last-child { grid-column: span 2; }
}

/* The card heading here as written - "Collection Value" - rather than in the
   capitals the game page's cards use.
   The letter-spacing goes with the capitals, since tracking is for caps, and
   the size comes up from 10.5px, which reads as a caption once the words are
   in lower case. The game page's cards keep the capitals. */
[data-totals] .price-card .label {
  font-size: 12.5px;
  letter-spacing: 0;
  text-transform: none;
}

/* A fixed height rather than one taken from the card. The grid row is as tall
   as its tallest card, so a chart sized from its card would be growing the
   row it is sized from. drawChart's own floor is 110px. 145px is the 120px it
   had under a heading, plus the heading's line and the gap below it, so the
   row - and the Collection Value card beside it - keep their height. */
[data-totals] .history-chart {
  display: block;
  width: 100%;
  height: 145px;
}

/* The point under the pointer on a chart drawn with opts.hover: a halo and a
   larger dot in the line's colour, with its figure above it. The layer never
   takes the pointer itself, or the chart would read the pointer as leaving
   every time it crossed onto the label. It fades rather than blinks, and
   hides with opacity rather than display so the label can still be measured
   while it is out of sight. */
.chart-hover {
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease;
}

.chart-hover.on { opacity: 1; }

.chart-halo { opacity: 0.2; }

/* Ringed in the card's own colour, so the dot lifts off the line it sits on. */
.chart-dot {
  stroke: var(--surface);
  stroke-width: 2;
}

/* The figure: the page's ink as the ground and its surface as the text, so
   the label inverts with the theme and stays the strongest thing on the chart
   in either. */
.chart-tip rect { fill: var(--ink); }

.chart-tip text {
  fill: var(--surface);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The collection's value is the figure the page exists to show, so it is
   set larger than the cards' 23px: 30px.

   Sized to its card as well as capped, because at 640px the card is a third
   of the row - about 190px - and 30px would push a value like $12,345.00
   out of the side of it. 17cqw is 17% of the card's own inner width: the
   mono face advances 0.53em a character, so that fits eleven of them -
   $100,000.00 - and the figure steps down only where the card is too narrow
   for it. A browser without container units drops the whole declaration and
   keeps the 23px. */
[data-totals] .collection-value { container-type: inline-size; }

[data-totals] .collection-value .value { font-size: min(30px, 17cqw); }

/* Two figures in the card - the value, and under it the count - in the
   height the chart beside them gives the row. The cards' own 1.55 line is
   46.5px around a 30px figure, and two of those would grow the row; at 1.2
   both fit, and each label sits closer to its own figure than to the one
   above it, so the card reads as two pairs. */
[data-totals] .collection-value .value { line-height: 1.2; }

[data-totals] .collection-value .value + .label { margin-top: 14px; }

/* The "nothing here yet" row on the collection page.

   Deliberately not `.empty`: gameTable already puts that class on price cells
   with no figure, so styling `td.empty` here reached every "--" in the search
   and homepage tables, padding their rows taller and centring a column that
   is meant to be right-aligned. */
.results-table td.empty-row {
  padding: 22px 16px;
  color: var(--ink-3);
  text-align: center;
}

/* "Find a game" in the empty collection's prompt, styled as the footer's
   links are: --ink-2 at rest, a shade darker than the --ink-3 sentence
   around it, and the accent with an underline on hover. Ringed on keyboard
   focus, as they are too. */
.empty-row a { color: var(--ink-2); }

.empty-row a:hover {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.empty-row a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* A collection entry's note, as a second line under the game's title: the
   same size as the table's secondary text and in its muted ink, and back to
   normal weight so the title above stays the thing the eye lands on.

   Wraps rather than truncating. A full 50 characters come to ~260px, which
   fits the title column on a wide screen but not on a narrow one, and an
   ellipsis there would hide the very words someone wrote to tell this copy
   apart. Titles in this column already wrap, so a note that does too is in
   keeping with the row. */
.results-table .item-note {
  margin-top: 3px;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.35;
  color: var(--ink-3);
  overflow-wrap: anywhere;   /* a 50-character unbroken string still wraps */
}

/* The platform after a game's title, on the list pages and in search results:
   the note's size, weight and ink, so the title stays the thing the eye lands
   on. nowrap keeps "Switch 2"
   and "Wii U" whole when a long title pushes the platform onto a new line. */
.results-table .item-platform {
  margin-left: 2px;
  font-size: 12px;
  font-weight: 400;
  color: var(--ink-3);
  white-space: nowrap;
}

.results-table td.num.up   { color: var(--up); }
.results-table td.num.down { color: var(--down); }

.muted { color: var(--ink-3); }

/* Edit and the delete icon sit together at the right end of a row. Not
   `.num`, which would put them in the tabular font meant for figures. */
.row-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  justify-content: flex-end;
  font-size: 12.5px;
}

/* An icon-only button. The 26px box is deliberate: the glyph is 15px, but a
   pointer target smaller than about 24px is genuinely hard to hit, and on a
   touch screen it is worse. The padding is the target, not decoration.

   The negative vertical margin is what keeps that target from costing row
   height. 26px is taller than the 19px line of text beside it, so laid out
   plainly it made every row 6px taller - the opposite of what replacing the
   word with an icon was for. Pulling 4px off each side leaves an 18px
   footprint in the layout while the button itself, and the area that
   responds to a click, stay 26px. It overflows into the cell's own 11px of
   padding, which is empty space either way. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  margin: -4px 0;
  padding: 0;
  color: var(--ink-3);
  background: none;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
  transition: color 0.13s ease, background 0.13s ease;
}

.icon-btn:hover { color: var(--ink); background: var(--bg); }

/* Red only on hover. A row of permanently red icons reads as a list of
   errors; muted until pointed at reads as an action available on each row. */
.icon-btn.danger:hover { color: var(--down); }

.icon-btn:disabled { opacity: 0.4; cursor: default; }

.icon-btn:focus-visible {
  outline: none;
  color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-sw);
}

/* Names the entry being edited, since several rows can look identical. The
   negative top margin pulls it up under the heading - the two belong
   together as one title block, so the gap there is smaller than the gap to
   the fields below. */
.dialog-sub {
  margin: -16px 0 10px;
  font-size: 12.5px;
  color: var(--ink-3);
}

/* ---------------------------------------------------------- add dialog -- */

/* Sized to its contents rather than to a round number: two 142px fields and
   the 12px between them come to 296px, plus 24px of padding either side.
   The 4px of slack keeps sub-pixel rounding from wrapping the second field
   onto its own line.

   Widening this again reopens the problem it fixes - the fields stay 142px,
   so the extra goes into dead space on the right, and the buttons below,
   which align to the content edge, stop lining up with the field above. */
.add-dialog {
  width: min(348px, calc(100vw - 32px));

  /* 16px at the top against 24px elsewhere, and the two read as equal. The
     heading's line box is taller than its letters - at 26px there is 8px of
     leading above the capitals - so an even 24px measured 33px to the top of
     the text while the buttons sat a true 24px off the bottom. The padding is
     uneven so that the space is not. Retune if the heading's size or
     line-height changes. */
  padding: 16px 24px 24px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.add-dialog::backdrop,
.shot-dialog::backdrop {
  background: rgb(0 0 0 / 0.45);
}

/* Sized to match the game title on the listing page (.game-head h1), letter
   spacing and line height included - the dialog reads as a title over its
   own small page rather than as a label on a box. */
.add-dialog h2 {
  margin: 0 0 18px;
  font-size: 20px;
  font-weight: 640;
  letter-spacing: -0.022em;
  line-height: 1.2;
}

.dialog-actions {
  display: flex;
  align-items: center;
  gap: 10px;

  /* Wider than the gaps above it, deliberately. The buttons act on the form
     rather than being part of it, and at 6px they crowded the field they sit
     under. */
  margin-top: 16px;
  font-size: 12.5px;
}

/* Pushes the confirm/cancel pair right while Remove stays left, so the
   destructive action is nowhere near the one people click by reflex. */
.dialog-actions .spacer { flex: 1; }

/* The two buttons span exactly the Total Cost field above them - the row is
   one half of the two-up field row, and the buttons split it evenly, so
   Cancel starts where that field starts and the confirm button ends where it
   ends. Left to size themselves they came to 145.5px against the field's
   143.3px, and Cancel hung 2px past its left edge.

   This holds while both labels are short. It was add-only when the edit
   dialog said "Save changes", which needed 120px and would have wrapped at
   half this width; that button now says "Save". A longer label here is the
   one thing that would break it. */
.add-dialog .dialog-actions {
  width: calc((100% - 12px) / 2);
  margin-left: auto;
}

/* The spacer exists to push the pair away from a left-aligned action. There
   is none here, and inside a fixed-width row it would eat the space the
   buttons need. */
.add-dialog .dialog-actions .spacer { display: none; }

/* The browser's own dropdown arrow sits hard against the right edge, and
   padding-right does not move it in Chrome - it only squeezes the text until
   "CIB (Complete)" loses its bracket. So the native arrow is switched off and
   a chevron drawn in its place, which can be positioned. 14px in from the
   edge, matching the 12px the text sits in from the left.

   The colour is baked into the SVG because a data URI cannot read
   currentColor, which is why the dark palette repeats this rule with the
   darker ink at the bottom of the file. */
.add-dialog .field-condition select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238a857c' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;

  /* Clears the chevron. 143px of field less this and the 12px on the left
     leaves 97px for text, and the longest option needs 89px. */
  padding-right: 32px;
}

/* Equal halves. The horizontal padding comes down because at ~67px the
   original 16-18px each side left too little room for the label. */
.add-dialog .dialog-actions button {
  flex: 1 1 0;
  min-width: 0;
  padding-left: 8px;
  padding-right: 8px;
  white-space: nowrap;
}

/* The add button, once this game is in the collection. */
.add-btn.owned {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-sw);
}

/* The star, once the item is on the watchlist: filled in, in the accent. */
.watch-btn[aria-pressed="true"] { color: var(--accent); }
.watch-btn[aria-pressed="true"] path { fill: currentColor; }

/* Held off while a click is on its way to the server. */
.watch-btn:disabled { cursor: progress; }

/* ------------------------------------------------------ report dialog -- */

/* The add dialog's frame, with one box in place of its fields. Four lines,
   fixed: no resize handle, so the dialog keeps its shape, and a longer note
   scrolls inside the box. */
.report-dialog textarea {
  display: block;
  height: 96px;
  line-height: 1.45;
  resize: none;
}

/* No field row above it to stand off from, so the add dialog's 12px goes. */
.report-dialog .field-report-note { margin: 0; }

/* The error sits between the box and the buttons, so it needs air on both
   sides. */
.report-dialog .form-error { margin: 14px 0 0; }

/* ------------------------------------------------- listing screenshot -- */

/* The trigger in the Listing Title cell. .linkish already makes a button read
   as text; these are what a table cell additionally needs - a button centres
   its own content and will not wrap, and a listing title does both.

   The colour is inherited so the cell's own --ink-2 still governs: until it is
   hovered, a title that opens a screenshot looks exactly like one that does
   not. Most rows have none, and a column half in link blue would read as two
   kinds of listing. */
.c-listing .shot-link {
  display: block;
  width: 100%;
  text-align: left;
  white-space: normal;
  color: inherit;
}

/* The same hover the results table gives a clickable title. */
.c-listing .shot-link:hover {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Sized to the image rather than to a round number: the captures are eBay
   search cards 866px wide, so 866 plus 16px of padding either side is the
   width at which they render 1:1 and stay sharp. Narrower windows scale the
   image down instead. The padding lives on .shot-body, not here, so that a
   click on this element means the backdrop and only the backdrop. */
.shot-dialog {
  width: min(898px, calc(100vw - 32px));
  padding: 0;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);

  /* Both kinds dismiss on a click anywhere, so both say so - and zoom-out is
     the answer to the zoom-in on the box art frame that opens one of them. */
  cursor: zoom-out;
}

/* With nothing inside to hand focus to, the dialog holds it itself, so the
   first arrow key paging through a set of box art gives it the browser's
   keyboard ring - drawn round the whole viewer, where it says nothing that a
   modal dialog does not already make plain. */
.shot-dialog:focus-visible { outline: none; }

.shot-body { padding: 16px; }

/* These are screenshots of a light web page whatever theme the site is in, so
   they keep their own white ground in dark mode rather than being filtered -
   an inverted screenshot of a page is unreadable. That literal #fff is the
   only hard-coded colour here, and is what stops a capture with transparent
   edges bleeding into a dark dialog; the border separates it from a light one.
   Landscape at roughly 866x300, so height follows width and no max-height is
   needed at any realistic window size. */
.shot-img {
  display: block;
  width: 100%;
  height: auto;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 6px;
}

/* The same dialog holding the other shape. Box art is portrait - covers run
   618x1000 - or square, and at the width above it would stand taller than any
   window: 866px of a cover is 1400px tall. So the picture is bounded by the
   window's height instead of the dialog's width, and the dialog shrinks to
   whatever that leaves rather than staying cut for a screenshot.

   The 70px reserved off the height is measured, not guessed: 34px is the
   margin the UA keeps around a modal dialog (its own max-height is
   calc(100% - 34px)), and 34px is everything in this dialog that is not the
   picture - the body's 16px of padding top and bottom, and the border. Give
   the image more than that and the dialog grows past its own maximum and
   scrolls, which puts a scrollbar beside art that was meant to be looked at.
   The spare 2px absorbs sub-pixel rounding. */
.shot-dialog.art {
  /* fit-content, not auto. A modal dialog is positioned with both edges set,
     and `auto` against that pair fills the window rather than shrinking to
     the picture - which is what the UA's own `width: fit-content` was doing
     before this rule replaced it. */
  width: fit-content;
  max-width: calc(100vw - 32px);
}

.shot-dialog.art .shot-img {
  width: auto;
  max-width: calc(100vw - 64px);
  max-height: calc(100vh - 70px);

  /* Not the screenshots' hard-coded #fff: this is the site's own art, and it
     sits on the same surface it does in the frame on the page behind. */
  background: var(--surface);
}

/* .empty-state is tuned for a full-width table cell; in here it only has to
   stand in for the image. */
.shot-dialog .empty-state { padding: 28px 16px; }

/* ------------------------------------------------------------ blog / FAQ -- */

/* The same tightened band the collection page's heading gets, for the same
   reason: a heading with its own line of sub under it fills the space a lone
   search box needs padding to hold, and the base rule's 40px left both pages
   opening low. The 22px below is the base rule's own, left alone. */
[data-page="blog"] .page-head,
[data-page="faq"] .page-head { padding-top: 10px; }

/* Both pages are prose rather than tables, so they get a measure. The 1200px
   wrap is right for six columns of prices and far too wide to read a
   paragraph across. */
.prose {
  max-width: 680px;
  padding-bottom: 48px;
}

.post { padding: 26px 0; border-top: 1px solid var(--border); }

/* .page-head has already spaced the first entry from the heading; a rule and
   another 26px on top of that would read as a gap rather than a division. */
.post:first-child { border-top: 0; padding-top: 0; }

.post h2 {
  margin: 0;
  font-size: 17px;
  font-weight: 640;
  letter-spacing: -0.015em;
}

.post-meta {
  margin: 3px 0 12px;
  font-size: 12px;
  color: var(--ink-3);
}

.post p {
  margin: 0 0 12px;
  color: var(--ink-2);
  font-size: 14px;
}

.post p:last-child { margin-bottom: 0; }

.post b { font-weight: 600; color: var(--ink); }

/* One question. <details> carries the open state itself, so the accordion
   needs no script - these pages load app.js only for the header and footer. */
.faq-item { border-bottom: 1px solid var(--border); }
.faq-item:first-child { border-top: 1px solid var(--border); }

.faq-item summary {
  position: relative;
  padding: 15px 30px 15px 0;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;

  /* The chevron below replaces the default disclosure triangle; a marker and
     a chevron together read as two controls. `list-style` covers Firefox,
     the pseudo-element beneath it covers WebKit. */
  list-style: none;
}

.faq-item summary::-webkit-details-marker { display: none; }

.faq-item summary:hover { color: var(--accent); }

.faq-item summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* Two borders rather than an SVG per question: it is decoration, and turning
   it over is the whole of the open state. */
.faq-item summary::after {
  content: "";
  position: absolute;
  right: 6px;
  top: 21px;
  width: 6px;
  height: 6px;
  border-right: 1.6px solid var(--ink-3);
  border-bottom: 1.6px solid var(--ink-3);
  transform: rotate(45deg);
  transition: transform 0.13s ease;
}

.faq-item[open] summary::after { transform: rotate(-135deg); }

.faq-answer { padding: 0 30px 18px 0; }

.faq-answer p {
  margin: 0 0 10px;
  color: var(--ink-2);
}

.faq-answer p:last-child { margin-bottom: 0; }

.faq-answer ul {
  margin: 0 0 10px;
  padding-left: 20px;
  color: var(--ink-2);
}

.faq-answer li { margin-bottom: 4px; }

.faq-answer b { font-weight: 600; color: var(--ink); }

/* ----------------------------------------------------------------- dark -- */

@media (prefers-color-scheme: dark) {
  :root {
    --bg:        #15161a;
    --surface:   #1c1e23;
    --border:    #2b2e35;
    --border-sw: #3a3e47;
    --ink:       #eceef2;
    --ink-2:     #a8adb8;
    --ink-3:     #767c88;
    --accent:    #9d92ff;
    --accent-sw: #2a2660;
    --on-accent: #15161a;
    --up:        #4ec27f;
    --down:      #f0776a;
    --s1: #f26d77;
    --s2: #4ec27f;
    --s3: #9d92ff;
    --s4: #e8c04f;
  }

  /* The chevron's colour lives inside a data URI, so it cannot follow a
     token - the rule is repeated here with the dark palette's --ink-3. */
  .add-dialog .field-condition select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23767c88' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
}

/* ---------------------------------------------------------------- small -- */

@media (max-width: 640px) {
  .wrap { padding: 0 16px; }
  .home { padding: 60px 0; }   /* even, like the desktop rule, so it centres */
  .home h1 { font-size: 24px; }
  .page-head { padding: 28px 0 18px; }

  /* Scaled to fit a phone, an 866px capture lands around 300px wide and the
     price on it is about four pixels tall - a picture of a sold listing that
     cannot be read is not worth opening. Give it its full width and let it pan
     instead, the same trade the sold listings table already makes on this
     screen, and for the same reason. */
  .shot-frame { overflow-x: auto; }

  .shot-img {
    width: 866px;
    max-width: none;
  }
}
