/* One stylesheet, three layouts, two breakpoints (web spec 3.3).
   HTMX 2.0.4 is vendored beside this file (static/htmx.min.js), fetched
   once from https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js on
   2026-09-28; sha256 e209dda5c8235479f3166defc7750e1dbcd5a5c1808b7792fc2e6733768fb447.
   Record the new version and sha256 here on every upgrade so the bump is
   a deliberate act rather than a diff nobody can read. */

:root {
  --ink: #1b1b1b;
  --ink-soft: #6b6660;
  --paper: #fbfbfa;
  --card: #ffffff;
  --rule: #d8d5cf;
  --hover: #f3f1ec;
  --link: #14507a;
  --warn: #8a5a00;
  --warn-bg: #fff4d6;
  --bad: #9c0006;
  --bad-bg: #ffc7ce;
  --good: #076400;
  --good-bg: #c6efce;
  --skip-bg: #d9d9d9;
  --approved-bg: #ffffcc;
  --del: #8a1c1c;
  --del-bg: #fbd5d5;
  --ins: #14532d;
  --ins-bg: #d3f5dc;
  --column: 1100px;
  /* The smallest target a thumb hits reliably (web spec 3.3). */
  --touch: 44px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
}

/* A reading-width column for pages of prose and field tables. A page that
   is one wide table (`main--wide`, the coins grid) takes the window
   instead: centred in a column, its filters start a third of the way
   across a wide monitor while the table runs off the right edge. */
main { max-width: var(--column); margin: 0 auto; padding: 1rem; }
main.main--wide { max-width: none; padding-inline: 0.5rem; }

/* --- links -------------------------------------------------------------
   Coloured, and underlined only under the pointer: a page of underlines
   reads as a page of noise. The links that are a whole box -- a topbar
   item, a grid row, a pager button -- tint the box instead, so the
   underline is for the ones that are only text. Keyboard focus is an
   outline on everything that can take it, because the hover cue is no
   use to a keyboard. */
a { color: var(--link); text-decoration: none; }
a:not(.topbar a, .row, .pager a):hover { text-decoration: underline; }
:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }

/* --- the bar ----------------------------------------------------------
   Every item -- a link, the user's name, the sign-out button -- is the
   same centred, touch-height box, so they share one line whatever
   element each happens to be. */
.topbar {
  display: flex; flex-wrap: wrap; align-items: center;
  padding: 0 0.5rem; border-bottom: 1px solid var(--rule);
  background: var(--card);
}
.topbar nav, .topbar__user { display: flex; flex-wrap: wrap; align-items: center; }
.topbar__user { margin-left: auto; }
.topbar a, .topbar .linkish, .topbar__user span {
  display: inline-flex; align-items: center;
  min-height: var(--touch); padding: 0 0.6rem;
  color: var(--ink); border-radius: 4px; white-space: nowrap;
}
.topbar a:hover, .topbar .linkish:hover { background: var(--hover); }
.topbar .site { font-weight: 600; }
.topbar__user span { color: var(--ink-soft); }
.topbar nav a[aria-current="page"] { box-shadow: inset 0 -2px 0 var(--link); }
/* A <button> that reads as a link. It drops the button's own box --
   border, padding and the thumb-sized minimums `button` sets below -- so
   whatever styles it as a link decides its size. */
.linkish {
  background: none; border: 0; color: var(--link); cursor: pointer;
  padding: 0; font: inherit; min-height: 0; min-width: 0;
}
.banner {
  margin: 0; padding: 0.5rem 1rem; background: var(--warn-bg);
  color: var(--warn); border-bottom: 1px solid var(--rule);
}

/* --- cards, badges, provenance --------------------------------------- */
.card {
  border: 1px solid var(--rule); border-radius: 6px;
  padding: 0.75rem 1rem; margin: 1rem 0; background: var(--card);
}
.card--narrow { max-width: 24rem; margin-inline: auto; }
/* The dashboard's Pages and DB status: tight, so the page is read at a
   glance under its coin. */
.card--compact { padding: 0.5rem 0.75rem; margin: 0.6rem 0; }
.card--compact h2 { font-size: 1.05rem; margin: 0 0 0.25rem; }
.card--compact p { margin: 0; }
.status-line {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The dashboard's page links, one a line with its count beside it; a
   line never wraps, and a long one is cut rather than broken. The line is
   a flex row, so the cutting is on its items (an ellipsis on the row
   itself never applies): each may shrink below its text (`min-width: 0`),
   the count first. */
.pages { list-style: none; margin: 0; padding: 0; }
.pages li {
  display: flex; align-items: center; gap: 0.5rem; min-height: 2rem;
}
.pages li > * {
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pages li > a { flex-shrink: 0; max-width: 100%; }
.pages__count { color: var(--ink-soft); }

/* The dashboard's one coin, its chosen images in one row, each drawn at
   its thumbnail's size (`?h=192`) and never enlarged; a narrower screen
   shrinks them whole. The row never wraps: with more than one image each
   takes at most half the row, less half the gap, so two sides fit a phone
   side by side; a lone image keeps the whole width. Top-aligned, so a
   shrunk image keeps its shape rather than stretching to its neighbour. */
.featured { margin: 1rem 0; max-width: 100%; }
.featured a {
  display: flex; flex-wrap: nowrap; align-items: flex-start; gap: 0.25rem;
  width: fit-content; max-width: 100%;
}
.featured img {
  display: block; flex: 0 1 auto; min-width: 0;
  width: auto; height: auto; max-width: 100%;
}
.featured img:not(:only-child) { max-width: calc(50% - 0.125rem); }
.featured figcaption { color: var(--ink-soft); font-size: 0.8em; margin-top: 0.15rem; }
.badge {
  display: inline-block; padding: 0 0.4em; border-radius: 4px;
  background: var(--rule); font-size: 0.85em;
}
.badge--warn { background: var(--warn-bg); color: var(--warn); }
.badge--bad { background: var(--bad-bg); color: var(--bad); }
.badge--good { background: var(--good-bg); color: var(--good); }
.prov { color: var(--ink-soft); font-size: 0.9em; margin: 0.25rem 0 0; }
.error { color: var(--bad); font-weight: 600; }
.blank { color: var(--ink-soft); }

/* --- forms ------------------------------------------------------------ */
label { display: block; margin-top: 0.6rem; font-weight: 600; }
input, select, textarea { font: inherit; }
/* A text control fills its line; a tick or a radio keeps its own size: at
   100% it took a flex row whole and pushed its label out of sight (Review's
   new-value radios). `:where` adds no specificity, so every narrower rule
   still wins as it did. */
input:where(:not([type="checkbox"]):not([type="radio"])), select, textarea {
  width: 100%; padding: 0.45rem;
  border: 1px solid var(--rule); border-radius: 4px; background: var(--card);
}
button {
  font: inherit; padding: 0.5rem 0.9rem; border-radius: 4px;
  border: 1px solid var(--rule); background: var(--card); cursor: pointer;
  /* Thumb-sized: the review queue's accept/reject are hit on a sofa
     (web spec 3.3). */
  min-height: var(--touch); min-width: var(--touch);
}
.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; }

/* --- the grid: ONE TABLE AT EVERY WIDTH --------------------------------
   ONE ROW IS ONE ELEMENT (`a.row`), laid out as a table row; there is no
   second copy of the markup, because a fragment route has to be able to
   return exactly one element with a given id.

   PHONE (<640px) is the default, and the whole row should fit a 360px
   screen without scrolling sideways. So only the narrow column set is
   shown (`.cell--wide` is hidden, below), and the width goes first to
   what must be whole -- the thumbnail, the header labels, and the
   `cell--fit` columns (the id and the value: `NarrowColumn.fit`). Every
   other VALUE takes what is left and is cut with an ellipsis; the full
   text is one tap away on the coin's own page. */
.layout > aside .card { margin-top: 0; }
.grid-status { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: baseline; }
.sort-line { margin: 0.25rem 0 0.5rem; color: var(--ink-soft); font-size: 0.9em; }

/* While a request is in flight HTMX puts `htmx-request` on the element
   its `hx-indicator` names: `#coin-grid` for the grid (`coins.html`),
   `#review-queue` for the review queue (`review/queue.html`), and the
   tapped button for a block's Accept / Reject / Undo
   (`review/_proposal.html`). What is waiting dims. `visibility`, not
   `display`, for "Loading…", so the line's space is already there and
   nothing jumps when it appears. The short delay keeps a fast response
   from flickering. */
.loading { visibility: hidden; color: var(--ink-soft); font-size: 0.9em; }
.grid-panel.htmx-request .loading { visibility: visible; }
#coin-rows, #review-queue, .block-actions button {
  transition: opacity 0.15s 0.1s;
}
.grid-panel.htmx-request #coin-rows, #review-queue.htmx-request,
.block-actions .htmx-request { opacity: 0.45; }

/* The box a too-wide table scrolls in, so the PAGE never scrolls
   sideways. */
.rows-scroll {
  overflow-x: auto; border: 1px solid var(--rule); border-radius: 6px;
  background: var(--card);
}
.rows {
  --thumb: 36px;
  display: table; width: 100%;
  /* `separate` with no spacing, not `collapse`: a collapsed border
     belongs to the table rather than the cell, so it stays behind when a
     sticky header cell moves. */
  border-collapse: separate; border-spacing: 0;
  font-size: 0.8rem;
}
.rows-head { display: table-header-group; }
/* The row is a link to the coin, and reads as a row: ink, no underline.
   The hover and focus tint is what says it can be opened. */
.row { display: table-row; color: inherit; }
.row:hover .cell, .row:focus-visible .cell { background: var(--hover); }
/* An outline on a table-row is not drawn by every browser; inset on the
   row it is, and the tint above covers the ones that still skip it. */
.row:focus-visible { outline-offset: -2px; }
/* `.rows-head`'s cells need `table-cell` as much as a row's do: inline
   `<span>`s inside a `table-header-group` collapse into ONE anonymous
   cell, putting the whole header in the grid's first column. */
.rows .cell {
  display: table-cell; padding: 0.3rem 0.2rem; vertical-align: middle;
  border-bottom: 1px solid var(--rule); text-align: left;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rows-head .cell { background: var(--card); font-weight: 600; }
/* A sortable header: ink like the label it is, the whole cell its
   target. A flex row so that when the column is too narrow for both, the
   LABEL is what gives way and the sort mark stays whole. */
.sort { display: flex; align-items: baseline; color: inherit; }
.label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.sort-mark { flex: none; margin-left: 0.2em; color: var(--link); }
/* The thumbnail: ONE slot, `--thumb` high and twice as wide, holding the
   coin's one or two chosen images side by side (`coin_thumb`). Each image
   takes an equal share of the slot and is drawn WHOLE inside its share
   (`contain`), against the share's left edge: an image of both sides
   fills the slot, two sides halve it, and a lone side sits where the
   obverse would, with the reverse's half left empty. Nothing is ever
   cropped, and nothing here asks what shape an image is. */
.thumb {
  display: flex; gap: 2px;
  width: calc(var(--thumb) * 2); height: var(--thumb);
}
.thumb img {
  flex: 1 1 0; min-width: 0; height: 100%;
  object-fit: contain; object-position: left center;
}
/* `max-width: 0` is what lets a cell's text be cut: a table cell is
   otherwise as wide as its longest line, and with `nowrap` that is the
   whole value. Capped at zero it asks for no width of its own, so these
   columns split whatever the table has left after the ones that must be
   whole -- header labels included, which is what keeps a long sort mark
   or a seven-figure value from pushing the last column off a phone.
   `min-width` is the floor under that: a column with nothing left to
   share still shows a few characters rather than vanishing. The columns
   that must not be cut ask for their content's width instead (`1%` is
   "as little as the content allows"). */
.rows .cell { max-width: 0; min-width: 2.25rem; }
.rows .cell--thumb, .rows .cell--fit { width: 1%; max-width: none; min-width: 0; }
/* Below the desktop breakpoint a header shows its short name
   (`routes/coins.py`'s `NARROW_COLUMNS`); the full one comes back where
   there is room for it. */
.label-full { display: none; }

/* The coin page's images, wrapping, each at the size its thumbnail was
   rendered for (`?h=240`: at most 240 high and 480 wide, the `2x` source
   keeping it sharp on a dense screen). Never enlarged; two separate sides
   sit side by side where they fit and stack on a phone, and on a screen
   narrower than one image both its sides shrink together, which is what
   `auto` under a `max-width` does and a fixed height would not.
   `min-width: 0` lets a flex item shrink below its image's own width. */
.images { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-start; }
.image { margin: 0; max-width: 100%; min-width: 0; }
.image-link { display: block; }
.image-link img { display: block; width: auto; height: auto; max-width: 100%; }
.image figcaption { color: var(--ink-soft); font-size: 0.8em; margin-top: 0.15rem; }

.filters { display: block; }
.field-table { width: 100%; border-collapse: collapse; }
.field-table th, .field-table td {
  text-align: left; vertical-align: top; padding: 0.4rem 0.3rem;
  border-bottom: 1px solid var(--rule);
}
/* A catalogue reference, a lot URL or a provenance link can be far wider
   than a phone screen with no natural break point -- without this the
   cell forces the whole page into horizontal scroll rather than
   wrapping. `anywhere` (not `break-word`) also counts toward
   `min-content` sizing, so a table column does not blow out to fit one
   long unbroken value first and wrap never. */
.field-table td, .prov { overflow-wrap: anywhere; }
.pager { display: flex; gap: 0.5rem; align-items: center; margin: 1rem 0; }
.pager a {
  display: inline-flex; align-items: center; min-height: var(--touch);
  padding: 0 0.9rem; border: 1px solid var(--rule); border-radius: 4px;
  background: var(--card); color: var(--ink);
}
.pager a:hover { background: var(--hover); }

/* --- the review queue -------------------------------------------------
   One <details> per proposal, read at volume and on a phone: every line
   is one line. A field table cell is cut with an ellipsis, and a value
   opens to its whole text on a tap (`details.long`). */
.proposal, .intake-row, .relabel-row {
  border: 1px solid var(--rule); border-radius: 6px;
  margin: 0.75rem 0; background: var(--card);
}
/* The header's badges say whether the block is already decided, so they
   wrap to a second line rather than ever being cut. The provenance comes
   last and is the one item that shrinks: it takes what is left of the
   line, cut with an ellipsis, and drops to its own line only when less
   than its minimum is left. */
.proposal > summary, .intake-row > summary, .relabel-row > summary {
  padding: 0.6rem 0.75rem; cursor: pointer;
  display: flex; flex-wrap: wrap; column-gap: 0.5rem;
  align-items: baseline; white-space: nowrap;
}
.proposal > summary > *, .intake-row > summary > *,
.relabel-row > summary > * { flex: none; }
.proposal > summary > .prov {
  flex: 1 1 0; min-width: 6rem; margin: 0;
  overflow: hidden; text-overflow: ellipsis;
}
/* The marker is decoration: the empty alternative text after `/` keeps a
   screen reader from announcing it; `<details>` already says whether the
   block is open. The whole header opens and closes the block, but the
   marker is where a thumb aims, so it is a thumb-sized square (`--touch`)
   at the row's left edge, its glyph large, with clear space before the
   Coin ID link beside it: a small marker hard against the link made a
   close a visit to the coin. The
   negative margins let the square reach into the header's padding, so
   the header grows no taller than the square. */
.proposal > summary::before, .intake-row > summary::before,
.relabel-row > summary::before {
  content: "\25B8" / ""; color: var(--ink-soft);
  flex: none; width: var(--touch); line-height: var(--touch);
  margin: -0.6rem 0.25rem -0.6rem -0.75rem;
  text-align: center; font-size: 1.5rem;
}
.proposal[open] > summary::before,
.intake-row[open] > summary::before,
.relabel-row[open] > summary::before { content: "\25BE" / ""; }
.proposal[open] > summary,
.intake-row[open] > summary,
.relabel-row[open] > summary { border-bottom: 1px solid var(--rule); }
.round-heading { font-size: 1rem; margin: 1.25rem 0 0.25rem;
                 color: var(--ink-soft); text-transform: capitalize; }
.field-table tr.changed > th { font-weight: 700; }
/* `fixed` so the columns take the widths below and never their content's:
   a long value then cuts, where `auto` would push the page sideways. */
.diff-table { table-layout: fixed; }
.diff-table .col-field { width: 8.5rem; }
.diff-table th, .diff-table td {
  padding: 0.25rem 0.4rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The name (`.label`) gives way before its confidence badge does. */
.fname { display: flex; align-items: baseline; gap: 0.3rem; }
.fname > .badge { flex: none; font-weight: 400; }
.diff-table del { background: var(--del-bg); color: var(--del); }
.diff-table ins { background: var(--ins-bg); color: var(--ins); text-decoration: none; }
/* The old value whole, then the new one whole: a gap between them. */
.diff-table del + ins { margin-left: 0.25em; }
.diff-table .ws { opacity: 0.7; }
/* Read aloud, never drawn: the words a screen reader needs where the
   colour and the strike say it to the eye. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* Closed, it reads as the plain text it holds: no marker, no pointer. */
.long > summary {
  display: block; list-style: none; cursor: auto;
  overflow: hidden; text-overflow: ellipsis;
}
.long > summary::-webkit-details-marker { display: none; }
.long[open] > summary { white-space: normal; overflow-wrap: anywhere; }
.block-actions { padding: 0 0.75rem 0.75rem; }
/* Confirm match: the block's first question, set off from the fields, and
   its "Wrong coin" kept apart from "Confirm match" and marked as the
   answer that turns the lot away for good. */
.match-question {
  margin: 0.5rem 0.75rem 0.75rem; padding: 0.25rem 0 0.25rem 0.75rem;
  border-left: 3px solid var(--warn);
}
.match-question h3 { margin: 0 0 0.25rem; font-size: 1rem; }
.block-actions button.danger {
  margin-left: 1.5rem; color: var(--bad); border-color: var(--bad);
}
.listing-said, .evidence, .all-fields { margin: 0 0.75rem 0.5rem; }
.listing-said > summary, .evidence > summary, .all-fields > summary {
  cursor: pointer; color: var(--ink-soft);
}
.all-fields { margin-top: 0.5rem; }
/* The proposed listing's hotlinked photos: small, wrapping, one tap to the
   full image. A refused hotlink (review.js adds .broken) is words. */
.listing-photos {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
  margin: 0.5rem 0.75rem;
}
.listing-photo img {
  display: block; width: 120px; height: 120px; object-fit: contain;
  border: 1px solid var(--rule);
}
.listing-photo.broken img { display: none; }
.listing-photo.broken::after { content: "photo blocked"; font-size: 0.9em; }
.evidence ul { margin: 0.25rem 0; padding-left: 1.1rem; font-size: 0.9em; }
.evidence li { overflow-wrap: anywhere; }
.superseded-note, .refused-note {
  margin: 0.25rem 0.75rem; color: var(--warn);
}
.evidence-link { white-space: nowrap; }
/* The house the coin came from, linked to its lot page: one line, cut. */
.seller-line {
  margin: 0.4rem 0.75rem; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* A closed block's placeholder until its body arrives. The Retry line
   shows, in place of the no-script link, only once review.js has marked
   the fetch failed. */
.proposal-body.lazy { padding: 0.25rem 0.75rem 0.75rem; }
.lazy-error { display: none; color: var(--bad); }
.proposal-body.failed .lazy-error { display: inline; }
.proposal-body.failed .lazy-note > a { display: none; }
.banner--apply { display: flex; gap: 0.75rem; align-items: center;
                 flex-wrap: wrap; }
/* `display: flex` above would otherwise outrank the `hidden` attribute and
   draw an empty strip when nothing awaits apply. */
.banner--apply[hidden] { display: none; }
.queue-states { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.queue-states > .expand-toggle { margin-left: auto; }
/* The bulk-accept tick: the label is the thumb-sized target around a
   box that stays the browser's own size. Negative margins keep the
   header's line height. A partial accept's tick, one per changed field,
   is the same target; `flex: none` keeps the field name from squeezing it. */
.bulk-tick, .field-tick {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--touch); min-height: var(--touch);
  margin: -0.6rem -0.35rem -0.6rem -0.5rem; cursor: pointer;
  align-self: center;
}
.field-tick { flex: none; }
.bulk-tick input, .field-tick input, .bulk-all-line input {
  width: 1.2rem; height: 1.2rem; margin: 0;
}
.bulk-all-line {
  display: inline-flex; align-items: center; gap: 0.5rem;
  min-height: var(--touch); cursor: pointer;
}
.bulk-all-line[hidden], .bulk-bar[hidden], .bulk-bar [hidden] { display: none; }
/* Pinned to the bottom of the screen while the queue scrolls under it,
   on a phone and a desktop alike: the count and the button are where
   the thumb is. */
.bulk-bar {
  position: sticky; bottom: 0; z-index: 2;
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem;
  margin: 0.75rem -0.5rem 0; padding: 0.5rem;
  background: var(--card); border-top: 1px solid var(--rule);
  box-shadow: 0 -2px 6px rgb(0 0 0 / 0.06);
}
.bulk-bar form { margin: 0; }
.bulk-accept { font-weight: 600; }
.bulk-notice { display: flex; flex-wrap: wrap; align-items: center;
               gap: 0.5rem; }
.bulk-notice:empty { display: none; }
.bulk-bar.htmx-request { opacity: 0.6; }
/* A field the latest accept left out: struck, so the decided block reads
   as what apply will write. */
.field-table tr.not-accepted .val { text-decoration: line-through; opacity: 0.7; }
/* The decide form is markup for the no-script post; its buttons lay out
   as the actions row's own. */
.decide-form { display: contents; }
/* The "Why?" box takes a line of its own at the end of the actions row,
   so opening it never moves the buttons under the thumb. */
.why { order: 1; flex-basis: 100%; min-width: 0; }
.why > summary { cursor: pointer; color: var(--ink-soft); padding: 0.6rem 0; }
.why textarea { display: block; margin-top: 0.25rem; resize: vertical; }

/* --- the intake queue -------------------------------------------------
   One <details> per queued row, its header ONE line at every width: the
   status and the price keep their size, the title and the lot line share
   what is left, each cut with an ellipsis. */
.intake-row > summary { flex-wrap: nowrap; }
.intake-row > summary > .intake-title,
.intake-row > summary > .intake-meta {
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.intake-row > summary > .intake-title { flex: 0 1 auto; }
.intake-row > summary > .intake-meta { flex: 1 1 6rem; margin: 0; }
.intake-row .price { margin-left: auto; font-variant-numeric: tabular-nums; }
/* The colours the Intake sheet's Status cell carries
   (the workbook's `intake/status_colour.py`, retired 2026-10-11). */
.badge--unreviewed { background: var(--bad-bg); }
.badge--approved { background: var(--approved-bg); }
.badge--labeled { background: var(--good-bg); }
.badge--skip { background: var(--skip-bg); }
.intake-body { padding: 0.25rem 0.75rem 0.75rem; }
/* The lot's photos: a row of thumbnails, scrolling sideways if many. */
.intake-photos { display: flex; gap: 0.5rem; overflow-x: auto;
                 margin: 0.25rem 0 0.5rem; }
.intake-photos img { display: block; height: 96px; width: auto;
                     max-width: 192px; object-fit: contain;
                     border: 1px solid var(--rule); }
/* The status buttons wear their word's colour, so the row's next status
   is picked by the colour the header will show. The pressed one is the
   row's own: outlined, since disabled alone reads as unavailable. */
.status-form { margin: 0.25rem 0 0.5rem; }
/* PHONE: the four status buttons share ONE line at 360px, so they are
   narrower than the app's other buttons -- less padding, a smaller word,
   no 44px minimum width -- and still at least 36px tall: below the app's
   44px touch size by design (Nathan, 2026-10-13: smaller, one line).
   THE WIDTH BUDGET at 360px: the four words at 0.85rem with their padding,
   borders and gaps need about 270px; the row has about 302px (360 less
   main's 1rem a side, the row's border and the body's 0.75rem a side).
   A word that still does not fit is cut, never wrapped. The tablet rule
   gives them back the app's own size and wrapping. */
.status-form.actions { flex-wrap: nowrap; gap: 0.3rem; }
.status-button { color: var(--ink); border: 1px solid var(--rule);
                 flex: 1 1 auto; min-width: 0; min-height: 2.25rem;
                 padding: 0.3rem 0.35rem; font-size: 0.85rem;
                 white-space: nowrap; overflow: hidden;
                 text-overflow: ellipsis; }
.status-button[aria-pressed="true"] { outline: 2px solid var(--ink);
                                      outline-offset: 1px; opacity: 1; }
/* One line at 375px: the label, the select, "picked" or Keep, and the
   no-script Save; the select alone shrinks. */
.series-form { display: flex; flex-wrap: nowrap; gap: 0.5rem;
               align-items: baseline; margin: 0.25rem 0 0.5rem; }
.series-form > * { flex: none; }
.series-form select { flex: 1 1 0; min-width: 0; min-height: 2.5rem; }
.prefix-cell { border-top: 1px solid var(--rule); margin: 0.5rem 0;
               padding-top: 0.25rem; }
.prefix-cell h3 { font-size: 0.9rem; margin: 0.25rem 0; }
.prefix-cell p { margin: 0.25rem 0; }
.prefix-cell .prefix { font-size: 1.1rem; font-variant-numeric: tabular-nums; }
.prefix-cell[data-state^="fix"] .prefix-note,
.prefix-cell[data-state="restate"] .prefix-note { color: var(--bad); }
.prefix-form { display: flex; flex-wrap: wrap; gap: 0.5rem;
               align-items: baseline; }
.prefix-form input[name="prefix"] { text-transform: uppercase; width: 6em; }
.intake-note { margin: 0.25rem 0; color: var(--warn); }
.matches h3 { font-size: 0.9rem; margin: 0.5rem 0 0.25rem; color: var(--ink-soft); }
.matches ul { margin: 0 0 0.5rem; padding-left: 1.1rem; }
/* Outside a table cell nothing else keeps a closed value to one line. */
.intake-note:not([open]) > summary,
#apply-report .long:not([open]) > summary,
.matches .long:not([open]) > summary { white-space: nowrap; }

/* --- the coin page's fields and the edit form -------------------------
   A field is one line: its name a fixed width, its value cut with an
   ellipsis. A tap opens the value whole and, where the web writes it, the
   input and Save; the badges keep their size beside it. */
section[data-group] .field-table { table-layout: fixed; }
section[data-group] .field-table th[scope="row"] {
  width: 8.5rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.val-line { display: flex; align-items: baseline; gap: 0.4rem; min-width: 0; }
.val-line > details { flex: 1 1 auto; min-width: 0; }
.val-line > .badge, .val-line > .saved { flex: none; }
/* Closed only: an open value shows whole (`.long[open] > summary`). */
section[data-group] .long:not([open]) > summary { white-space: nowrap; }
/* A whole thumb-sized target, a blank value's included; the value inside
   is what gets cut. */
section[data-group] .long > summary { display: flex; align-items: center;
                                      min-height: var(--touch); }
/* THE COIN PAGE IS AS DENSE AS REVIEW AND INTAKE (Nathan, 2026-10-13):
   tighter cards and headings, and a row's padding is the diff table's.
   In a field row the value's padding is on its <summary>, not the cell,
   so the summary spans the row's whole height and width: the tap target
   is the row, at least one line plus that padding high even when the
   value is blank -- no longer a 44px one, which is what kept the page
   sparse. The edit form keeps its spacing. */
.coin-card { padding: 0.5rem 0.75rem; margin: 0.5rem 0; }
.coin-card h2 { font-size: 1.05rem; margin: 0 0 0.25rem; }
.coin-card .field-table th, .coin-card .field-table td { padding: 0.25rem 0.4rem; }
section.coin-card[data-group] .field-table td { padding-block: 0; }
section.coin-card[data-group] .long > summary {
  padding-block: 0.25rem; min-height: calc(1.5em + 0.5rem);
}
/* The history is closed until tapped; its summary is one line that says
   how much it holds and keeps the Review link, which has a thumb-sized
   target of its own so a tap on it does not toggle the panel instead.
   The marker is drawn here, so Safari's own is hidden. */
.history > summary {
  display: flex; flex-wrap: wrap; column-gap: 0.5rem; align-items: center;
  cursor: pointer; list-style: none;
}
.history > summary::-webkit-details-marker { display: none; }
.history > summary::before { content: "\25B8" / ""; color: var(--ink-soft); }
.history[open] > summary::before { content: "\25BE" / ""; }
.history > summary > h2, .history > summary > .prov { margin: 0; }
.history > summary > a {
  display: inline-flex; align-items: center; min-height: var(--touch);
  padding-inline: 0.5rem;
}
.history[open] > summary { margin-bottom: 0.25rem; }
.long > summary > .sv { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.long[open] > summary > .sv { overflow-wrap: anywhere; }
.field-edit > summary { cursor: pointer; }
.val-line > input, .val-line > textarea, .val-line > select { flex: 1 1 auto; min-width: 0; }
.edit-table th label { margin: 0; }
/* A focused input is never under the sticky Save bar. */
.edit-table input, .edit-table textarea, .edit-table select { scroll-margin-bottom: 5rem; }
/* A row's inline editor: the input and its Save one line (`.inline-line`,
   which never wraps -- the input gives way), the NEW-value panel a block
   under it. */
.inline-edit { display: block; margin-top: 0.25rem; }
.inline-line { display: flex; flex-wrap: nowrap; gap: 0.4rem;
               align-items: flex-start; min-width: 0; }
.inline-edit input, .inline-edit textarea { flex: 1 1 0; min-width: 0; }
.inline-edit button { flex: none; }
/* A tick in either line (the "approximate" box) keeps its own size. */
.val-line > input[type="checkbox"], .inline-edit input[type="checkbox"] { flex: none; }
.saved { color: var(--good); font-size: 0.85em; }
.val-line > .saved { min-width: 0; overflow: hidden; text-overflow: ellipsis;
                     white-space: nowrap; flex: 0 1 auto; }
/* The edit form's default button (`coin_edit.html`): first in the form, so
   Enter presses Save and never a panel's `use`; off-screen, not hidden. */
.default-submit { position: absolute; left: -10000px; width: 1px;
                  height: 1px; min-width: 0; min-height: 0; overflow: hidden; }
/* The NEW-value panel (PR 3b, `_known.html`): a full-width block under the
   input. Nothing wraps: the badge, the value and each suggestion are cut,
   and a suggestion's buttons sit whole on the line under it -- at 360px a
   name and two buttons on one line left the name no room. */
.known:empty { display: none; }
.known { min-width: 0; margin-top: 0.25rem; }
.known-line { display: flex; gap: 0.4rem; align-items: center; min-width: 0;
              min-height: var(--touch); margin: 0; }
.known-line > .sv, .known-near > .sv, .vocab-pick > .sv {
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.known-line > .sv { flex: 1 1 0; }
.known-line > .badge { flex: 0 1 auto; max-width: 60%; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.known-line > input { flex: none; }
.known-near { display: block; min-width: 0; margin-top: 0.25rem; }
.known-near > .sv { display: block; }
.known-buttons { display: flex; flex-wrap: nowrap; gap: 0.4rem; }
.known-buttons > button { flex: none; padding-inline: 0.6rem; }
/* Review's radios for an off-list proposed value: one per line, each a
   tap; the value named is cut, never the words around it. */
.vocab-row > td { padding-top: 0; }
.vocab-pick { display: flex; align-items: center; min-width: 0;
              min-height: var(--touch); margin: 0; cursor: pointer; }
.vocab-pick > input { flex: none; margin-right: 0.4rem; }
/* The words around the name keep their spaces (`pre`): a flex row drops
   the space at a span's edge. */
.vocab-pick > span:not(.sv) { flex: none; white-space: pre; }
.vocab-pick > .sv { flex: 0 1 auto; }
.notice { margin: 0.5rem 0; padding: 0.4rem 0.6rem; border-radius: 4px;
          background: var(--warn-bg); color: var(--warn); }
.edit-table td > details { min-width: 0; }
.edit-table textarea { resize: vertical; }
.edit-table tr.asked > td, .edit-table tr.asked > th { background: var(--warn-bg); }
/* Save stays under the thumb while the form scrolls. */
.edit-actions {
  position: sticky; bottom: 0; z-index: 2; align-items: center;
  margin: 0.75rem -1rem 0; padding: 0.5rem 1rem;
  background: var(--card); border-top: 1px solid var(--rule);
}

/* --- TABLET (640-1024px): the same narrow table with room to breathe -- */
@media (min-width: 640px) {
  main, main.main--wide { padding: 1.25rem; }
  .edit-actions { margin-inline: -1.25rem; padding-inline: 1.25rem; }
  .filters { border: 1px solid var(--rule); border-radius: 6px; }
  .filters > summary { padding: 0.5rem 0.75rem; cursor: pointer; }
  .rows { --thumb: 56px; font-size: 0.9rem; }
  .rows .cell { padding: 0.35rem 0.4rem; }
  .diff-table .col-field { width: 11rem; }
  /* The intake status buttons back at the app's own button size. */
  .status-form.actions { flex-wrap: wrap; gap: 0.5rem; }
  .status-button { flex: none; min-width: var(--touch);
                   min-height: var(--touch); padding: 0.5rem 0.9rem;
                   font-size: inherit; }
}

/* --- DESKTOP (>=1024px): every column, sticky header, pinned filters -- */
@media (min-width: 1024px) {
  /* `minmax(0, 1fr)`, not `1fr`: a grid track's minimum is its content's
     width, so a bare `1fr` grows to fit the whole table and pushes it
     off the page instead of letting `.rows-scroll` scroll it. */
  .layout {
    display: grid; grid-template-columns: 16rem minmax(0, 1fr);
    gap: 1.25rem; align-items: start;
  }
  /* The filter panel stays put while the rows scroll; with "More
     filters" open it can be taller than the window, so it scrolls
     itself rather than hiding its last fields below the fold. */
  .layout > aside {
    position: sticky; top: 0.5rem;
    max-height: calc(100vh - 1rem); overflow-y: auto;
  }
  /* The SAME elements, re-laid-out: columns as wide as their content up
     to a cap, the table scrolling sideways inside its box. The box is
     also the header's scrolling ancestor -- `overflow` on one axis makes
     it a scroll container on both -- so it is given the window's height
     and the header sticks to ITS top; left to grow with the table it
     would carry the header off the screen with the first row. */
  .rows-scroll { max-height: calc(100vh - 1rem); overflow: auto; }
  .rows { --thumb: 96px; font-size: inherit; }
  .rows .cell { padding: 0.35rem 0.5rem; }
  /* Here a value has room: its column is as wide as its content, up to
     a cap that keeps one long description from owning the screen. */
  .rows .cell { max-width: 22rem; min-width: 0; }
  .rows .cell--thumb, .rows .cell--fit { width: auto; }
  .rows-head .cell { position: sticky; top: 0; z-index: 1; }
  .label-full { display: inline; }
  .label-short { display: none; }
  /* A pointer can hover, so a header not yet in the sort says it sorts. */
  .sort--idle::after { content: " ↕"; color: var(--rule); }
  .sort--idle:hover::after { color: var(--link); }
}

/* WHICH cells a narrow screen shows. Web spec 3.3 names seven things for
   the phone -- id, country, denomination, date, grade, value, thumbnail
   -- so the cells outside that set carry `.cell--wide` and are HIDDEN
   below the desktop breakpoint.
   Hiding a cell is not the thing the one-element rule forbids: the row is
   still sent once, and `#intake-row-{id}` still matches exactly one
   element. Sending a second copy of the row is what was forbidden.
   Every part with a per-row fragment uses THIS class; nobody invents a
   second one. It follows the blocks above because it must beat
   `.rows .cell`'s `display` at equal specificity. */
.rows .cell--wide { display: none; }
@media (min-width: 1024px) {
  .rows .cell--wide { display: table-cell; }
}

/* /exports: one line a file; the name gives way before the time does. */
.exports { list-style: none; margin: 0; padding: 0; }
.exports li { display: flex; gap: 0.75rem; align-items: baseline;
              min-height: var(--touch); }
.exports__name { flex: 1 1 auto; min-width: 0; overflow: hidden;
                 text-overflow: ellipsis; white-space: nowrap; }
.exports__meta { flex: none; white-space: nowrap; color: var(--ink-soft); }

/* --- relabels ----------------------------------------------------------
   One line a sticker: the thumbnail, then the old id and the new one,
   neither of which wraps; a long id is cut. */
.relabel-row > summary { flex-wrap: nowrap; align-items: center; }
.relabel-row .thumb { width: 64px; height: 32px; }
.relabel-ids { min-width: 0; overflow: hidden; text-overflow: ellipsis;
               flex: 1 1 auto; font-variant-numeric: tabular-nums; }
.relabel-old { color: var(--ink-soft); }
.relabel-group > h2 { margin-top: 1.25rem; }
/* The country inside a Series: a smaller heading under it. */
.relabel-country { font-size: 0.95rem; margin: 0.75rem 0 0.25rem; }

/* --- lists (PR 3b-L) ---------------------------------------------------
   /lists: one line a list, the name cut before a count is. /lists/{d}:
   one line a value -- the value, its spellings (the first to give way),
   the coins holding it -- opening to the acts. Nothing wraps. */
.lists-table { width: 100%; table-layout: fixed; border-collapse: collapse; }
.lists-table th, .lists-table td {
  padding: 0 0.4rem; height: var(--touch); text-align: right;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  border-bottom: 1px solid var(--rule); font-variant-numeric: tabular-nums;
}
.lists-table th:first-child, .lists-table td:first-child { text-align: left; width: 52%; }
.lists-table abbr { text-decoration: none; }
.lists-table thead th { color: var(--ink-soft); font-weight: 600; font-size: 0.85em; }
/* The title's name is cut; its count never is. */
.list-title { display: flex; gap: 0.3rem; white-space: nowrap; min-width: 0; }
.list-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.list-count { flex: none; }
.list-find { display: flex; gap: 0.4rem; margin: 0.5rem 0; }
.list-find input { flex: 1 1 0; min-width: 0; }
.list-find button { flex: none; }
.list-row { border: 1px solid var(--rule); border-radius: 6px;
            margin: 0.4rem 0; background: var(--card); }
.list-row > summary {
  display: flex; flex-wrap: nowrap; gap: 0.5rem; align-items: center;
  min-height: var(--touch); padding: 0 0.75rem; cursor: pointer;
  white-space: nowrap; list-style: none;
}
.list-row > summary::-webkit-details-marker { display: none; }
.list-row[open] > summary { border-bottom: 1px solid var(--rule); }
.list-row > summary > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.list-value { flex: 0 1 auto; max-width: 60%; }
.list-spellings { flex: 1 1 0; color: var(--ink-soft); }
.list-coins { flex: none; font-variant-numeric: tabular-nums; }
.list-line { margin: 0.25rem 0; color: var(--ink-soft); white-space: nowrap;
             overflow: hidden; text-overflow: ellipsis; }
.list-act { margin-top: 0.5rem; }
.list-merge { display: flex; gap: 0.4rem; }
.list-merge input { flex: 1 1 0; min-width: 0; }
.list-merge button { flex: none; }
.list-tick { display: flex; align-items: center; gap: 0.4rem; margin: 0;
             min-height: var(--touch); font-weight: normal; white-space: pre;
             min-width: 0; }
.list-tick > input { flex: none; }
/* The words around the value keep their spaces and no gap: the quotes
   sit hard against it, and the value is what gets cut. */
.list-tick-words { display: flex; min-width: 0; }
.list-tick-words > .sv { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.list-act button.danger { color: var(--bad); border-color: var(--bad); }
.list-subhead { font-size: 1rem; margin: 1.25rem 0 0.25rem; color: var(--ink-soft); }
.list-vetoes { list-style: none; margin: 0; padding: 0; }
.list-vetoes li { min-height: var(--touch); display: flex; align-items: center; min-width: 0; }
.list-vetoes li > details { min-width: 0; flex: 1 1 auto; }
/* A closed spelling, country or veto line is one line, cut; a tap opens
   it whole (`.long[open] > summary`). */
.list-row .long:not([open]) > summary,
.list-vetoes .long:not([open]) > summary { white-space: nowrap; }
.notice--good { background: var(--good-bg); color: var(--good); }
