/* One palette, and it is dark. `color-scheme: light dark` used to stand here, which promised a
   light palette that was never written: the browser took the promise at its word and drew anything
   this file had no colour for in its light-scheme default. */
:root {
  color-scheme: dark;
  --bg: #12141a;
  --panel: #1b1f28;
  --ink: #e7e9ee;
  --muted: #98a0b3;
  --accent: #6ea8fe;
  --line: #2a3040;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* And every link takes its colour from it. The one this rule exists for is the Listing title in
   the Deals table — the primary column of the primary page, and the thing the operator clicks —
   which no rule named: it was drawn in the user agent's light-scheme link blue on a near-black
   background at 1.96:1, the least readable text on the page. The accent measures 7.62:1 there,
   well past WCAG AA, and the component rules below that repeat it do no harm. */
a {
  color: var(--accent);
}

/* The header wraps, because it is the one thing on the page that is not a table and overflowed
   anyway: at 390 px the nav's right edge measured 396 px and "Abmelden", which `margin-left: auto`
   pushes to the far end of the row, sat at 518 px — the way out of the session was off the screen
   and the last nav item read "Einstellung…". Both the bar and the nav inside it may break, and at
   the desk neither has to: one line still fits, so nothing there moves. */
.topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.5rem;
  padding: 0.75rem 1.5rem;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}

.brand {
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--ink);
  text-decoration: none;
}

.topbar nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.topbar a {
  color: var(--accent);
  text-decoration: none;
}

.topbar .logout {
  margin-left: auto;
}

main {
  max-width: 68rem;
  margin: 0 auto;
  padding: 1.5rem;
}

/* Except on the one page that *is* a table. 68 rem is a measure for prose — the width a paragraph
   is comfortable to read across — and on the Deals page it left 1040 px of a 1265 px window to
   twelve columns that needed more than that: the table measured 1331 px inside it, so Übergabe was
   cut in half and Alter, Eingestellt and Status stood off the right edge, reachable only by
   dragging the table inside its scroller. A table's measure is its columns, not a paragraph, so
   this page is given the window; 90 rem is still a cap, because the same table spread across a
   2560 px monitor would put the Marge a hand's width from the title it belongs to. */
main:has(table.listings) {
  max-width: 90rem;
}

h1 {
  font-size: 1.4rem;
  margin: 0 0 1rem;
}

button {
  padding: 0.5rem 1rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--accent);
  color: #10131a;
  font-weight: 600;
  cursor: pointer;
}

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 0.6rem;
  padding: 1.5rem;
}

.login {
  max-width: 22rem;
  margin: 3rem auto;
}

.login form {
  display: grid;
  gap: 0.4rem;
}

.login input {
  padding: 0.5rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: #10131a;
  color: var(--ink);
}

.login button {
  margin-top: 0.8rem;
}

.error {
  color: #ff9a9a;
  margin: 0 0 1rem;
}

.empty {
  color: var(--muted);
}

/* What a wide table scrolls inside. Twelve columns of Deals measured 1407 px and took the whole
   document with them, so the operator reached Preis by dragging the page and read the nav clipped
   mid-word — with Marge and Deckung, the two numbers the page exists for, three columns further
   right again. The answer is not a column diet: merging Alter with Eingestellt and Ort with
   Entfernung and dropping Art still leaves some 1000 px, which is 2.6 phones. So the table keeps
   every column it has and this is what moves instead. It has to be a wrapper rather than the
   table: `overflow-x: auto` on a `<table>` only takes effect with `display: block`, which throws
   away the column sizing that makes the table a table. */
.scroller {
  /* Positioned, and not for anything of its own: the label of every manual-price field is a
     `.visually-hidden`, which is `position: absolute`, and an absolutely positioned box whose
     containing block lies outside a scroll container is not clipped by it. Without this the
     Models page still scrolled sideways by 459 px — to a label nobody can see, standing at the
     static position its row would have given it. */
  position: relative;
  overflow-x: auto;
  /* A sideways drag inside the table is about the table, not a gesture to leave the page with. */
  overscroll-behavior-x: contain;
}

table {
  width: 100%;
  border-collapse: collapse;
}

table th,
table td {
  text-align: left;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--line);
}

/* --- What makes twelve columns fit the window they are read in ---------------------------------

   Three measured things hold a table open and only one of them is the text in it. The gaps between
   the columns are the second: twelve of them at 24 px is 288 px of the 1331 the Deals table wanted,
   and 16 px still separates one column from the next — the money is aligned on its right edge, so
   what the gap has to do here is keep a heading off the column beside it. That is 96 px back, and
   it leaves 1235.

   The headings are the third, and the surprising one: three columns are wider than anything ever
   printed in them because of the word standing at the top. "Entfernung" is a longer word than any
   distance it stands over — 107 px against the 71 its values need — "Deckung" 87 against 66,
   "Status" 67 against 47. A notch smaller they measure 93, 76 and 59: the word still legible, and
   35 px the table no longer spends on saying it.

   Together with the window the page was given above: 1331 px inside 1040 becomes 1200 px inside
   1217, which is every column on the screen at 1280 px with 17 px in hand. */
table.listings th,
table.listings td {
  padding: 0.5rem;
}

table.listings th {
  font-size: 0.85rem;
}

/* And the seller's own words are the one thing in the row with no bound at all — four of the 2709
   adverts in the corpus rule themselves off with a row of sixty "=" — so the title may break
   inside a word where there is no other way to fit it. It is the last resort and nothing else: a
   column only falls back to breaking when the table has run out of room, so at the desk the titles
   wrap between words exactly as they did. Without it the fit above would be a property of the page
   that was measured rather than of the table. */
table.listings td.title {
  overflow-wrap: anywhere;
}

/* --- The columns that are read down the page rather than across it ------------------------------

   Five of the Deals table's twelve columns are numbers to be compared with the numbers above and
   below them, and the rule over this one was the whole stylesheet's only word on alignment. The
   Asking Price alone renders at five widths across the corpus — 280 Listings at `€ 5,00`, 1154 at
   `€ 65,00`, 1074 at `€ 400,00`, 177 at `€ 1.500,00`, one at `€ 15.999,00` — so left-aligned a
   four-figure amount pushes its decimal point a character and a half out of the column the
   three-figure amounts sit in. The `tabular-nums` below buys nothing against that: what it
   guarantees is that digits are equally *wide*, not that they start in the same place. Aligned on
   the right they share their last character's position, which is where the cents are, and the
   decimal points line up whatever the amount.

   The headings move with their cells — a right-aligned column under a left-aligned heading is a
   heading standing over the column to its left — which is why each of them carries its own
   column's class in `deals.html` and `models.html`. */
table.listings th.price,
table.listings td.price,
table.listings th.coverage,
table.listings td.coverage,
table.listings th.distance,
table.listings td.distance,
table.models th.price,
table.models td.price,
table.models th.sample,
table.models td.sample {
  text-align: right;
}

/* And whatever stands beside an amount leaves the amount's line. A right-aligned cell aligns the
   end of its *last* line, so the "VB" four of the 38 Deals carry and the "Ohne Preis: …" one of
   them carries would become the thing that lines up, with the amount pushed left by however wide
   the pill happens to be — a column of money no more than the left alignment was. The markup keeps
   the reading order, the number and then what qualifies it, and the pill drops onto a line of its
   own underneath instead, still against the right edge so that the two read as one statement.
   `width: fit-content` rather than a plain block, because a pill as wide as the column is a
   banner. */
table.listings td.price .flag {
  display: block;
  width: fit-content;
  margin-top: 0.15rem;
  margin-left: auto;
}

/* Every one of those cells also carries the name of its own column, which is drawn nowhere near
   here: at this width the heading stands over the column and saying it twice would be a second
   heading beside every number. It is rendered at both widths and hidden at one of them rather than
   rendered at one, because which of the two says it — the heading over the column, or the name in
   the cell — is a question about the width of the screen, and the width of the screen is this
   file's business and not the template's. */
table.listings .label {
  display: none;
}

table.listings td.price {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

table.listings td.distance {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

table.crawls,
table.models {
  margin-top: 1rem;
}

table.models td.price,
table.models td.sample {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

table.models tr.unpriced td {
  color: var(--muted);
}

/* The used median is a control column, not a second price: it stands apart, and it keeps its
   colour on a row whose part is no longer sold new — that is the row it is there for. */
table.models th.control,
table.models td.control {
  border-left: 1px solid var(--line);
}

table.models tr.unpriced td.used {
  color: var(--ink);
}

table.crawls th,
table.crawls td {
  vertical-align: top;
  white-space: nowrap;
}

table.crawls ul.errors {
  margin: 0;
  padding-left: 1rem;
  white-space: normal;
  color: #ff9a9a;
}

.flag {
  display: inline-block;
  padding: 0.05rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: 0.8rem;
  font-size: 0.75rem;
  color: var(--muted);
  white-space: nowrap;
}

.flag.shipping {
  border-color: var(--accent);
  color: var(--accent);
}

.flag.gone {
  border-color: #6b4a4a;
  color: #ff9a9a;
}

p.filters {
  margin: 0 0 1rem;
}

p.filters a {
  color: var(--accent);
}

ol.price-history {
  margin: 0 0 1rem;
  padding-left: 1.2rem;
  font-variant-numeric: tabular-nums;
}

/* The seller's own words, in the three boxes that are as wide as the page rather than as wide as
   what they hold. A table too wide for the window scrolls inside a `.scroller`; a word too wide
   for its line has nothing to scroll inside and widens the document instead. At 390 px the
   description's box is 341 px, which is 38 characters, and twelve of the 2709 Listings in the
   corpus are already past that: four sellers rule their description off with a row of sixty `=`,
   which measured a 582 px document against a 375 px screen, one lists two serial numbers with no
   space between them, one runs five words together — and a 44-character e-mail address is enough
   on its own, because Chromium offers a soft wrap after "/" and "-" but none after "." or "@".
   That last part is also why the 135-character Willhaben URL a seller pastes is not the case worth
   checking: it breaks at its own slashes and measures 375 px with no help from here.

   `anywhere` and not `break-word`, which is the same declaration everywhere except where it
   matters: a grid column's auto minimum is the longest word its cells hold, and only `anywhere`
   lowers that. With `break-word` the title and the description come back to 375 px and the facts
   list stays at 449.

   And named here rather than inherited from `main`, which would be one selector instead of three
   and would cost the Deals page the columns this ticket promised not to touch: `overflow-wrap`
   reaches a table cell too, and a cell's longest word is exactly what holds a column open against
   a table that has no room. Measured on `main` at the desk, the Deals table falls from 1414 px to
   1040 — Art 130 to 41, Ort 125 to 44, Eingestellt 109 to 37 — and the Crawls page's Fehler column
   from 320 to 83. So the breaking stops where the seller's prose stops. */
.listing h1,
.listing .description,
dl.facts dd {
  overflow-wrap: anywhere;
}

.listing .price {
  font-size: 1.2rem;
  font-weight: 600;
}

.listing .description {
  white-space: pre-wrap;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 0.6rem;
  padding: 1rem;
}

dl.facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.25rem 1rem;
  margin: 0 0 1rem;
}

dl.facts dt {
  color: var(--muted);
}

dl.facts dd {
  margin: 0;
}

.actions {
  display: flex;
  gap: 1rem;
  align-items: center;
}

.actions a {
  color: var(--accent);
}

table.components {
  margin-bottom: 1rem;
}

table.components td.raw {
  color: var(--muted);
}

table.components tr.inferred td:first-child {
  border-left: 3px solid #6b5f3a;
  padding-left: 0.5rem;
}

.flag.inferred {
  border-color: #6b5f3a;
  color: #e3c877;
}

span.alternative {
  display: block;
  font-size: 0.75rem;
  color: var(--muted);
}

/* Scoring: the three columns the page exists for, and the two things that qualify them —
   a row that cleared the Deal threshold, and a Parts Value that is missing a part's price. */
table.listings td.parts-value,
table.listings td.margin,
table.listings td.coverage,
table.components td.conservative,
table.components td.optimistic,
dl.facts.score dd.price,
dl.facts.score dd.coverage {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* And of those three the Marge is the one the whole list is *ordered* by, so it is the one the eye
   should land on: it read in the weight of the postcode beside it. The cells only — a `th` is bold
   at 700 already, and 600 there would make this heading lighter than its neighbours rather than
   heavier — and this column only, because emphasising all three of them emphasises none. */
table.listings td.margin {
  font-weight: 600;
}

/* A Margin below zero is a loss, and the leading minus sign said so very quietly: one character at
   the head of a number otherwise printed exactly like a profit, and at the head is where a left
   alignment put it. 2492 of the corpus's 2686 active Listings carry one and they all sort behind
   the positive ones, so most of `?deals=0` is this colour and what the operator walking it is
   looking for is the row that is not. It is the colour the page already marks a gone Listing and a
   failed Crawl in — 9.07:1 against `--bg`, so no new palette entry and nothing new to learn — and
   the Listing's own page takes the same mark rather than a second spelling of it. */
table.listings td.margin.negative,
dl.facts.score dd.margin.negative {
  color: #ff9a9a;
}

.flag.deal {
  border-color: #3f6b4a;
  color: #8ede9f;
}

/* Never quietly worth zero: a Component the chain could not price says so wherever it is shown. */
.flag.unvalued {
  border-color: #6b5f3a;
  color: #e3c877;
  white-space: normal;
}

table.listings tr.incomplete td.parts-value {
  border-left: 3px solid #6b5f3a;
}

/* What the machine is made of, under the title it is sold under. Two Deals are usually told apart
   by the card in one of them, so the row that carries the money carries the names too — small and
   muted, because the ranking is read first and the parts once two rows are close. Five names is
   the most a Listing can keep (one CPU, one GPU, one kit, two drives), which is some 90 characters
   in the corpus: shorter than the headline above it, and therefore no taller a row than the title
   already makes. */
table.listings p.parts {
  margin: 0.25rem 0 0;
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--muted);
}

/* A part's name is one thing however many spaces are in it: "DDR4-3200 16GB (2x8GB)" broken across
   two lines reads as two parts. The line breaks between the names instead, on the separator. */
table.listings p.parts .part {
  white-space: nowrap;
}

/* And the part nobody could price wears the colour it is marked in everywhere else, so the name
   and the "Ohne Preis" beside it are one statement. */
table.listings p.parts .part.unvalued {
  color: #e3c877;
}

p.warning.unvalued {
  color: #e3c877;
  margin: 0 0 1rem;
}

dl.facts.score {
  margin-bottom: 1rem;
}

form.manual-price {
  display: flex;
  gap: 0.4rem;
  align-items: center;
}

form.manual-price input[type="text"] {
  width: 7rem;
  padding: 0.25rem 0.4rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 0.3rem;
  color: var(--ink);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Settings: one column of labelled fields per group, and the schedule the service runs on. */
form.settings fieldset {
  margin: 0 0 1.5rem;
  padding: 1rem 1.25rem 1.25rem;
  border: 1px solid var(--line);
  border-radius: 0.6rem;
  background: var(--panel);
}

form.settings legend {
  padding: 0 0.4rem;
  color: var(--muted);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

form.settings .field {
  display: grid;
  grid-template-columns: 18rem 10rem;
  gap: 0.5rem 1rem;
  align-items: center;
  margin-bottom: 0.5rem;
}

form.settings label {
  color: var(--ink);
}

form.settings input {
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 0.3rem;
  background: #10131a;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

form.settings .note {
  grid-column: 1 / -1;
  margin: 0;
  color: #e3c877;
  font-size: 0.8rem;
}

p.saved {
  color: #8ede9f;
  margin: 0 0 1rem;
}

ul.error {
  margin: 0 0 1rem;
  padding-left: 1.2rem;
  color: #ff9a9a;
}

table.schedule {
  margin-top: 0.5rem;
}

table.schedule td {
  white-space: nowrap;
}

/* The filters that narrow the list. They sit on one line above the table, because the operator
   reaches for them between rows rather than at the start of a session. */
form.narrow {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: flex-end;
  margin: 0 0 1rem;
}

form.narrow label {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  color: var(--muted);
}

form.narrow select,
form.narrow input[type="number"] {
  padding: 0.25rem 0.4rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 0.3rem;
  color: var(--ink);
}

form.narrow input[type="number"] {
  width: 5.5rem;
}

form.narrow a {
  color: var(--accent);
}

table.listings td.triage {
  white-space: nowrap;
}

/* Triage: the operator's own verdict, one click per button and no page behind it. */
.triage {
  margin-bottom: 1rem;
}

form.triage-status {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

form.triage-status button {
  background: var(--panel);
  color: var(--ink);
  font-weight: 400;
}

form.triage-status button.current {
  background: var(--accent);
  color: #10131a;
  font-weight: 600;
}

p.triage-state {
  margin: 0.6rem 0 0;
}

p.triage-state .muted {
  color: var(--muted);
}

form.triage-note {
  display: grid;
  gap: 0.5rem;
  justify-items: start;
  max-width: 36rem;
}

form.triage-note textarea {
  width: 100%;
  padding: 0.5rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  color: var(--ink);
  font: inherit;
  resize: vertical;
}

/* A save the operator cannot see is a save they press again. The panel that comes back is
   otherwise almost the one they were looking at — the moment they last looked is printed to the
   minute — so this line carries the second, and it is swapped in as a new element every time,
   which is what makes the fade behind it play from the start even on two saves that read the
   same. */
p.triage-saved {
  margin: 0;
  padding: 0.2rem 0.5rem;
  border-radius: 0.3rem;
  color: #8ede9f;
  font-variant-numeric: tabular-nums;
  animation: triage-saved 1.6s ease-out;
}

@keyframes triage-saved {
  from {
    background: #24402c;
  }

  to {
    background: transparent;
  }
}

/* Whoever asked for less motion keeps the line and its moment and loses only the fade. */
@media (prefers-reduced-motion: reduce) {
  p.triage-saved {
    animation: none;
  }
}

/* Paging: both long tables are cut into pages, because a month of crawling is more rows than a
   browser wants in one response. The navigation sits above and below the table and reads as one
   line — where the operator is, of how many, and out of how much — because a page number without
   the total behind it is a number nobody can act on. */
nav.paging {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: baseline;
  margin: 0.75rem 0;
}

nav.paging a {
  color: var(--accent);
  white-space: nowrap;
}

nav.paging .page-of {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* --- The phone the pages are read on ----------------------------------------------------------

   One breakpoint, and everything a narrow screen needs is inside it. 40 rem is 640 px at the
   browser's default font size: a phone held upright is below it and the desk above it, so nothing
   here touches the desktop layout — same columns, same widths, same one-line filter row.

   The largest thing in it is the Deals table, which stops being a table at all: twelve columns do
   not fit 390 px in any order, so one row becomes one card. The other wide tables keep the
   `.scroller` they are wrapped in at every width, because a table too wide for the window is too
   wide at 1280 px as well; the rest of this is a gutter, the fields the operator types into and
   the parts in a row — the filter row already wraps, and so does everything on the Listing page. */
@media (max-width: 40rem) {
  /* A 16 px side gutter instead of 24, which is the most a phone can spare and still look laid
     out rather than pressed against the glass. */
  main {
    padding: 1.5rem 1rem;
  }

  .topbar {
    gap: 0.75rem 1rem;
    padding: 0.75rem 1rem;
  }

  /* --- One Listing, one card ------------------------------------------------------------------

     The scroller above put the sideways drag inside the table instead of under the whole document,
     which made the page well-behaved without making it useful: what fits in the 343 px a phone has
     to give is Titel, Art and the first digit of Preis, and the Marge cell measured [526, 627] —
     167 px past the right edge of the screen, with Deckung 268 px past it. The page the operator
     opens in the morning to answer "which of these is worth the drive tonight" still hid the
     number that answers it.

     No order of the columns fixes that. Titel, Preis, Marge and Deckung alone measure 475 px, so
     even a table holding nothing but the four things this ticket promises would be a third wider
     than the screen. Below the breakpoint the row therefore stops being a row: the table, its
     body, every row and every cell become blocks, and one Listing becomes one card with its
     title, its parts and its four amounts stacked down it — every column, nothing hidden, nothing
     dragged.

     The markup is untouched by all of it. It is the same table the desk reads as twelve columns,
     which is what keeps #42's Components in the row, the page's one query one query, and every
     assertion about a cell true at both widths. What it costs is the table's own semantics —
     blocks are not rows, so the heading row no longer names anything — and that is exactly what
     the name inside each money cell is for. */
  table.listings,
  table.listings tbody,
  table.listings tbody tr,
  table.listings tbody td {
    display: block;
  }

  /* The headings named twelve columns that are no longer there. */
  table.listings thead {
    display: none;
  }

  table.listings tbody tr {
    margin-bottom: 0.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--line);
    border-radius: 0.6rem;
    background: var(--panel);
  }

  /* The rule under each cell drew a line across the card between every pair of them; the card's
     own border is what separates one Listing from the next now. */
  table.listings tbody td {
    padding: 0.1rem 0;
    border-bottom: 0;
  }

  /* And the name of the column the heading row used to carry, at the left edge of the cell it
     belongs to, with the amount staying where the right alignment above puts it — so the four
     amounts still line up under each other down the card, and down the page, which is the whole
     of what #83 was about. Floated rather than reordered: the name comes after the amount in the
     markup, because the amount is what the cell is about and a pill after it drops onto its own
     line. The weight and the colour are its own — the Marge is set in 600 and a loss in #ff9a9a,
     and neither is a statement about the word "Marge". */
  table.listings .label {
    display: inline;
    float: left;
    color: var(--muted);
    font-weight: 400;
  }

  /* What is left needs no naming — "213 km", "vor 2 Tagen", "1030 Wien", "Versand" and
     "Systeminserat" each say what they are — so none of it takes a line of its own. Where the
     advert is, how it gets here, how old it is and what the operator made of it read as one muted
     line under the money, which is what keeps the card short enough that the next Listing is on
     the screen with it. The kind of advert stays above them, beside the parts it is about: it
     answers "a whole machine or one part", which is a question about the thing being sold rather
     than about the drive to go and get it. */
  table.listings tbody td.kind {
    color: var(--muted);
    font-size: 0.85rem;
  }

  table.listings tbody td.place,
  table.listings tbody td.distance,
  table.listings tbody td.handover,
  table.listings tbody td.age,
  table.listings tbody td.published,
  table.listings tbody td.triage {
    display: inline;
    color: var(--muted);
    font-size: 0.85rem;
  }

  /* The separator holds on to the item in front of it, or a wrap opens the next line with a dot
     standing over nothing. A non-breaking space does that between two words; between the last
     "Versand" pill and the dot after it Chromium breaks regardless, because the pill is an atomic
     box and the boundary of one is a break opportunity of its own — so the Übergabe cell is one
     run for the length of a line instead. Both pills and the dot after them measure 174 px of the
     card's 317, so they stay side by side here; where they do not fit beside what stands in front
     of them the whole group moves down together. */
  table.listings tbody td.handover {
    white-space: nowrap;
  }

  table.listings tbody td.place::after,
  table.listings tbody td.distance::after,
  table.listings tbody td.handover::after,
  table.listings tbody td.age::after,
  table.listings tbody td.published::after {
    content: "\00a0·";
  }

  /* The parts must not be what makes a card wide. A name held on one line is up to 25 characters
     nothing can give way on ("GeForce RTX 4070 Ti Super"), where the same name allowed to wrap is
     at most "i9-14900KF" or "DDR4-3200". Two names of a kit read as two parts, which is what the
     rule at the desk is for; a name wider than the screen reads as nothing at all. */
  table.listings p.parts .part {
    white-space: normal;
  }

  /* The schedule is three short columns, and `nowrap` made it 405 px wide against the 343 px a
     phone has to give. It has a `.scroller` like every other table, but three columns are worth
     wrapping rather than leaving to be dragged: a timestamp broken after its date is still a
     timestamp, and the one thing here that cannot break — "Preisaktualisierung" — is what the
     scroller behind it is left holding. */
  table.schedule th,
  table.schedule td {
    white-space: normal;
  }

  /* The form the operator actually types into, and the one place where this was more than an
     inconvenience: `18rem 10rem` is 448 px of fixed grid, so the label measured 288 px and the
     input's right edge sat 119 px past the edge of the screen. A field whose label cannot be seen
     while it is being filled in is a field nobody can fill in. One column puts them above each
     other, and the note under them already spanned the whole field. */
  form.settings .field {
    grid-template-columns: 1fr;
  }

  /* And a control the operator works one-handed is 44 px tall, which is a thumb. These measured 25
     to 28 — a desk's mouse pointer is one pixel wide and a thumb is not. */
  form.settings input,
  form.narrow select,
  form.narrow input[type="number"],
  form.manual-price input[type="text"] {
    min-height: 2.75rem;
  }
}
