/* ── Color tokens ─────────────────────────────────────── */
:root {
  --bg:          #f5f5f0;
  --surface:     #ffffff;
  --header-bg:   #1a1a1a;
  --text:        #1a1a1a;
  /* ⚠️ #777 lag bei 4.47:1 auf --surface und 4.09:1 auf --bg — 36 der 49
     Kontrast-Knoten aus DID-184 gingen auf diesen einen Wert zurueck.
     #6e6e6e haelt 4.66:1 gegen den schlechtesten der drei Gruende. */
  --text-muted:  #6e6e6e;
  /* Trennlinien und Raster — Dekoration, ohne Kontrastpflicht. */
  --border:      #ddd;
  /* ⚠️ Der Rand eines Bedienelements ist etwas anderes: er ist das
     einzige, was es begrenzt, und faellt unter 1.4.11 (3:1). #ddd lag bei
     1.36:1. Eigene Marke statt eines schwereren Rahmens ueberall — eine
     Tabellenzeile braucht ihn nicht. */
  --border-ui:   #8f8f8f;
  /* Weiss auf --link ergibt im dunklen Satz 2.45:1. Flaechen mit weisser
     Schrift tragen deshalb --link-solid; --link bleibt die Textfarbe. */
  --link-solid:  #2471a3;
  /* ⚠️ Diese zwei wurden in .onboard seit jeher benutzt, waren aber im
     ganzen Repo NIRGENDS definiert — beide Themes fielen auf dieselben
     hellen Literale zurueck, der Balken blieb im Dunkelmodus hellgruen. */
  --accent-bg:   #eafaf1;
  --accent-fg:   #1b8146;
  --danger:      #c0392b;
  /* Zwei Zustaende des Fortschrittsstreifens, die keinen Markt tragen: eine
     Warnung (`--lauf-teil`: die Fehlerzeile, ein angehaltener Nachlauf) und ein
     Fehlschlag (`--lauf-fehler`: der Nachlauf, die Zeile unter dem Knopf).
     ⚠️ Beide standen als blanke Hex-Werte in den `.sp-*`-Regeln und waren damit
     fuer tests/test_kontrast.py unsichtbar, das die TOKEN nachrechnet; der
     axe-Lauf sah sie auch nicht, weil `#scrapeProgress` auf einer frisch
     geladenen Seite leer ist. Gemessen am 21.10.2026 unterschritten sie im
     hellen Theme den Wert (orange 2.85, gelb 2.87 statt 4.5) (DID-335).
     `--lauf-ok` und `--lauf-aktiv` gibt es seit DID-334 nicht mehr: im Streifen
     traegt die Farbe den Markt und das Zeichen den Zustand. */
  --lauf-fehler: #a95a13;      /* war #e67e22 = 2.85:1 */
  --lauf-teil:   #856d08;      /* war #b7950b = 2.87:1 */
  --row-border:  #f0f0f0;
  --row-hover:   #fafafa;
  --input-bg:    #ffffff;
  --shadow:      rgba(0,0,0,.07);
  --link:        #2471a3;
  --rm1-bg:      #e8f4fd; --rm1-fg: #2471a3;
  --rm2-bg:      #eafaf1; --rm2-fg: #1e8449;
  /* Eine Farbe je Plattform, drei Verwendungen: der Punkt in der Tabelle, der
     Punkt an einer Suchanfrage und der Knopf im Suchformular. Vorher standen
     die vier Werte nur an den .plt-*-Regeln, und der Knopf war fuer jede
     Plattform dasselbe Blau — die Auswahl sagte also nichts ueber die Sache.
     ⚠️ Die Schriftfarbe steht je Plattform daneben und ist nicht durchgehend
     weiss: Weiss auf #e8843a kommt auf rund 2.4:1 und ist im Sonnenlicht auf
     dem Telefon nicht mehr zu lesen. Dunkel auf hell, weiss nur auf dem Rot.
     Sie gelten in beiden Themes, weil der Grund die Plattformfarbe selbst ist. */
  --plt-ricardo: #e8843a; --plt-ricardo-fg: #2b1508;
  --plt-tutti:   #3d9be9; --plt-tutti-fg:   #08243b;
  --plt-anibis:  #3faf6a; --plt-anibis-fg:  #08301b;
  /* ⚠️ #e53238 trug weiss auf 4.33:1. #df2c32 kommt auf 4.62:1 und bleibt
     dasselbe Rot. */
  --plt-ebay:    #df2c32; --plt-ebay-fg:    #ffffff;
  /* Dieselben vier Farben als SCHRIFT (DID-334): der Fortschrittsstreifen
     faerbt den Namen des Marktes. ⚠️ Die Punktfarben tragen als Text nicht —
     gerechnet am 21.10.2026 auf --surface: Ricardo 2.69, Tutti 2.97, Anibis
     2.78, eBay 4.62 (gefordert 4.5). Ton und Saettigung bleiben, nur die
     Helligkeit wandert; ein Punkt (3:1) und die Pillen (eigene --plt-*-fg)
     bleiben bei den Originalen. */
  --plt-ricardo-text: #ab5414;  /* 5.24:1 auf --surface, 4.79:1 auf --bg */
  --plt-tutti-text:   #156fba;  /* 5.23:1, 4.78:1 */
  --plt-anibis-text:  #2c7a4a;  /* 5.26:1, 4.81:1 */
  --plt-ebay-text:    #d42026;  /* 5.21:1, 4.76:1 */
  /* Width of the drawer once it is docked as a permanent column. */
  --menu-w:      340px;
}
html.dark {
  --bg:          #111318;
  --surface:     #1e2029;
  --header-bg:   #0d0f14;
  --text:        #e2e4ea;
  --text-muted:  #919191;      /* war #888 = 4.16:1 auf --row-hover */
  --border:      #2e3140;
  --border-ui:   #707382;      /* 3.13:1, war #2e3140 = 1.26:1 */
  --link-solid:  #2a7aaf;      /* weiss darauf 4.67:1, war 2.45:1 */
  --accent-bg:   #14281c;
  --accent-fg:   #2ecc71;      /* 7.40:1 auf dem eigenen Grund */
  --danger:      #e45d4f;      /* 4.62:1, war #c0392b = 2.98:1 */
  /* Die zwei Zustandsfarben tragen im Dunkeln ihren Ausgangswert. */
  --lauf-fehler: #e67e22;      /* 5.70:1 */
  --lauf-teil:   #b7950b;      /* 5.66:1 */
  --row-border:  #252830;
  --row-hover:   #252830;
  --input-bg:    #252830;
  --shadow:      rgba(0,0,0,.3);
  --link:        #5dade2;
  --rm1-bg:      #1a2a3a; --rm1-fg: #5dade2;
  --rm2-bg:      #162a20; --rm2-fg: #2ecc71;
  /* Ricardo, Tutti und Anibis halten als Schrift schon mit dem Punktwert (5.46
     bis 6.02:1 auf --surface); nur eBay reisst mit 3.51:1 und wird heller. */
  --plt-ricardo-text: #e8843a; /* 6.02:1 */
  --plt-tutti-text:   #3d9be9; /* 5.46:1 */
  --plt-anibis-text:  #3faf6a; /* 5.84:1 */
  --plt-ebay-text:    #e65d61; /* 4.72:1, war #df2c32 = 3.51:1 */
  /* Der Scrollbalken gehoert dem Browser, nicht der Seite - keine unserer
     Regeln erreicht ihn. Er folgt allein color-scheme. Ohne diese Zeile bleibt
     er hellgrau und leuchtet aus dem dunklen Menue heraus.
     Dasselbe gilt fuer die Vorgabe-Darstellung von Eingabefeldern und
     Auswahlkaestchen, die damit ebenfalls in den Dunkelmodus wechseln. */
  color-scheme: dark;
}

/* ── Base ─────────────────────────────────────────────── */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
       background: var(--bg); color: var(--text); transition: background .2s, color .2s; }

/* ── Header ───────────────────────────────────────────── */
header { background: var(--header-bg); color: white; padding: 1.2rem 2rem;
         display: flex; align-items: center; gap: 1rem; flex-wrap: nowrap; }
header .title { flex: 1; min-width: 0; }
header h1 { font-size: 1.3rem; font-weight: 600; }
/* Das Zeichen im Kopf: an der Schrifthöhe ausgerichtet, damit es auf der
   Grundlinie sitzt und nicht darüber schwebt. `vertical-align` statt Flex,
   weil es im Textfluss der Überschrift steht. */
.marke { height: 1.15em; width: auto; vertical-align: -.16em; margin-right: .38rem; }
header span { font-size: 0.85rem; color: #aaa; }
.container { max-width: 1400px; margin: 2rem auto; padding: 0 1.5rem; }

/* ── Page layout ──────────────────────────────────────── */
.main { min-width: 0; }

/* ── Burger ───────────────────────────────────────────── */
.btn-burger { background: transparent; border: 1px solid rgba(255,255,255,.3);
  color: white; border-radius: 6px; padding: .35rem .65rem; cursor: pointer;
  font-size: 1.25rem; line-height: 1; flex-shrink: 0; }
.btn-burger:hover { background: rgba(255,255,255,.1); }
html[data-hand="left"] .btn-burger { order: -1; }
html[data-hand="right"] .btn-burger { order: 1; }

/* ── Drawer ───────────────────────────────────────────── */
.drawer { position: fixed; top: 0; bottom: 0; width: min(340px, 86vw);
  background: var(--surface); color: var(--text); z-index: 60;
  overflow-y: auto; padding: 1.1rem 1rem 1rem;
  box-shadow: 0 0 24px var(--shadow);
  display: flex; flex-direction: column; gap: 1rem;
  transition: transform .25s ease; }
html[data-hand="right"] .drawer { right: 0; transform: translateX(100%); }
html[data-hand="left"]  .drawer { left: 0;  transform: translateX(-100%); }
/* Must out-specify the html[data-hand="…"] .drawer rules above. */
html[data-hand] .drawer.open { transform: none; }
.drawer.dragging { transition: none; }
/* Carries the auto margin for the whole bottom block: toggles first, footer
   right below it. On the footer alone it would strand the toggles up top. */
.drawer-row { display: flex; gap: .5rem; margin-top: auto; }
.drawer-row .btn-toggle { flex: 1; }
/* Die Seite, auf der man steht, wird genannt statt verlinkt: ein Link auf
   sich selbst sieht aus wie ein Weg und ist keiner. */
.menu-here { color: var(--text); font-weight: 600; }
.drawer-foot { padding-top: .9rem; border-top: 1px solid var(--border);
  font-size: .78rem; color: var(--text-muted); line-height: 1.6; }
.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 59;
  opacity: 0; pointer-events: none; transition: opacity .25s; }
.scrim.show { opacity: 1; pointer-events: auto; }

/* ── Theme / handedness toggles ───────────────────────── */
.btn-toggle { background: var(--input-bg); border: 1px solid var(--border-ui);
  color: var(--text); border-radius: 6px; padding: .5rem .7rem; cursor: pointer;
  font-size: .85rem; font-family: inherit; line-height: 1.2; }
.btn-toggle:hover { border-color: #2471a3; }

/* ── Add form ─────────────────────────────────────────── */
.form-box { background: var(--surface); border-radius: 10px; padding: 1.5rem;
            box-shadow: 0 1px 4px var(--shadow); margin-bottom: 2rem; }
.form-box h2 { font-size: 1rem; margin-bottom: 1rem; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: .75rem; }
.form-grid input, .form-grid select, .form-grid textarea {
  width: 100%; padding: .5rem .75rem; border: 1px solid var(--border-ui); border-radius: 6px;
  font-size: .87rem; font-family: inherit;
  background: var(--input-bg); color: var(--text); }
.form-grid textarea { resize: vertical; min-height: 60px; }
.btn { background: var(--text); color: var(--surface); border: none; padding: .6rem 1.4rem;
       border-radius: 6px; cursor: pointer; font-size: .87rem; font-weight: 500; }
.btn:hover { opacity: .85; }
.btn-red { background: #c0392b; color: white; }
.btn-red:hover { background: #e74c3c; opacity: 1; }
.form-actions { margin-top: 1rem; display: flex; gap: .5rem; }

/* ── Table ────────────────────────────────────────────── */
.active-title { font-size: 1.05rem; font-weight: 600; margin: 0 0 .6rem; }
/* ⚠️ Nicht --text-muted: auf dem Seitengrund sind das 4.09:1, und seit die
   Bilanz hier steht statt im Menue traegt diese Stelle eine Aussage und nicht
   nur eine Verzierung. Gemessen am 13.10.2026. */
.active-title .at-count { color: var(--text); font-weight: 400; }
.active-title .rq-counts { font-size: .9rem; }
.table-wrap { background: var(--surface); border-radius: 10px; overflow: hidden;
              box-shadow: 0 1px 4px var(--shadow); }
table { width: 100%; border-collapse: collapse; font-size: .87rem; }
thead { background: var(--header-bg); color: white; }
th { padding: .75rem 1rem; text-align: left; font-weight: 500; }
td { padding: .7rem 1rem; border-bottom: 1px solid var(--row-border); vertical-align: top; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--row-hover); }
.badge { display: inline-block; padding: .2rem .6rem; border-radius: 20px;
         font-size: .75rem; font-weight: 600; }
.rm1 { background: var(--rm1-bg); color: var(--rm1-fg); }
.rm2 { background: var(--rm2-bg); color: var(--rm2-fg); }
.price { font-weight: 700; }
.expired { color: var(--danger); font-weight: 600; }
.soon { color: #e67e22; font-weight: 600; }
a { color: var(--link); text-decoration: none; }
/* ⚠️ Ein Link mitten im Fliesstext darf nicht allein an der Farbe erkennbar
   sein (1.4.1). Im dunklen Theme reichte der Abstand zur Textfarbe nicht —
   axe meldete `link-in-text-block` auf /datenschutz. Ausserhalb von
   Fliesstext (Menue, Fuss, Knoepfe) steht der Link fuer sich und braucht
   die Unterstreichung nicht. */
p a, li a, .set-hint a, .legal-text a, .faq-steps a { text-decoration: underline; }

/* ⚠️ Es gab bisher keine einzige :focus-Regel — es galt also der Ring des
   Browsers, und auf den farbigen Knoepfen war er ungeprueft. --text traegt
   gegen jeden unserer Gruende (16.7:1 hell, 11.6:1 dunkel), und der
   Abstand hebt ihn von der Flaeche ab, auf der er sitzt.
   :focus-visible statt :focus: ein Mausklick soll keinen Ring hinterlassen,
   die Tastatur sehr wohl. */
:focus-visible { outline: 2px solid var(--text); outline-offset: 2px;
  border-radius: 3px; }
a:hover { text-decoration: underline; }
.empty { text-align: center; padding: 3rem; color: var(--text-muted); }
/* ── Buttons ──────────────────────────────────────────── */
.btn-scrape { background: #2471a3; color: white; border: none; padding: .55rem 1.2rem;
  border-radius: 6px; cursor: pointer; font-size: .87rem; font-weight: 500; }
.btn-scrape:hover { background: #1a5276; }
.btn-scrape:disabled { background: #555; cursor: default; }
.btn-hide { background: #666; color: white; border: none; padding: .3rem .8rem;
  border-radius: 6px; cursor: pointer; font-size: .8rem; margin-left: .3rem; }
.btn-hide:hover { background: #444; }

/* ── Search requests panel ────────────────────────────── */
.rq-add { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; }
.rq-add input { flex: 1; min-width: 180px; padding: .5rem .75rem;
  border: 1px solid var(--border-ui); border-radius: 6px; font-size: .87rem;
  background: var(--input-bg); color: var(--text); font-family: inherit; }
/* The wrapper only exists so app.js has somewhere to write; display:contents
   keeps it out of .rq-add's flex layout, so the pills sit exactly where the
   hardcoded ones did. */
.platform-opts { display: contents; }
.platform-opt { display: none; }
/* Abgewaehlt derselbe Farbton, nur schwach: ein blasser Grund und ein Rand aus
   derselben Farbe. So sagt der Knopf schon vor dem Antippen, um welche
   Plattform es geht, und der Unterschied gewaehlt/abgewaehlt bleibt der
   Kontrast, nicht die Farbe.
   ⚠️ color-mix bleibt bewusst auf Grund und Rand beschraenkt. Faellt es aus,
   verschwinden genau diese beiden Angaben — Schriftfarbe und Form bleiben, der
   Knopf ist weiterhin lesbar und der Plattform zuzuordnen. */
.platform-opt + label { display: inline-block; padding: .35rem .8rem;
  border: 1px solid var(--border); border-radius: 20px; cursor: pointer;
  font-size: .82rem; color: var(--plt, var(--text-muted)); user-select: none;
  background: color-mix(in srgb, var(--plt, transparent) 12%, transparent);
  border-color: color-mix(in srgb, var(--plt, var(--border)) 42%, transparent); }
.platform-opt:checked + label { background: var(--plt, #2471a3);
  color: var(--plt-fg, #fff); border-color: var(--plt, #2471a3); }
.rq-list { margin-top: .9rem; display: flex; flex-direction: column; gap: .4rem; }
/* Wraps rather than overflows: docked as a permanent column the menu is only
   ~316px wide, and a long query plus four platform pills does not fit on a line. */
.rq-item { display: flex; align-items: center; gap: .5rem; padding: .4rem .6rem;
  background: var(--rq-grund, var(--row-hover)); border-radius: 6px; flex-wrap: wrap; }
.rq-query { flex: 1 1 auto; min-width: 0; font-size: .88rem; font-weight: 500;
  overflow-wrap: anywhere; }
/* The row stays a div; this button is its keyboard-reachable selector. It looks
   like the plain text it wraps. */
.rq-select { font: inherit; color: inherit; background: none; border: 0; padding: 0;
  cursor: pointer; text-align: left; }
/* Ausgeschrieben kosteten die Namen in der angedockten Spalte (~316px) eine
   eigene Zeile je Suchanfrage. Ein Punkt traegt dieselbe Unterscheidung — wie
   in der Tabelle, nur dass er hier der einzige Traeger der Angabe ist; darum
   steht der Name in title und aria-label. */
/* ⚠️ Die Punkte ueberlappen (DID-240, Variante A). Gemessen am 13.10.2026:
   38.4 px fuer drei Plattformen vorher, 23.3 px nachher, bei vier 52.8 -> 30.1.
   In der angedockten Spalte (~316 px) steht das in direkter Konkurrenz zum
   Suchbegriff, der sonst umbricht. */
.rq-platforms { display: inline-flex; align-items: center; }
/* ⚠️ Der Ring ist kein Zierrat, sondern der Trenner. Punkt gegen Punkt liegt
   bei 1.03:1 bis 1.61:1 (gemessen) - ohne ihn verschmelzen zwei Nachbarn zu
   einem Fleck. Er traegt die Zeilenfarbe, nicht eine eigene, damit er wie ein
   Spalt wirkt und nicht wie ein zweiter Rand. */
.rq-platform { width: .62rem; height: .62rem; border-radius: 50%; flex-shrink: 0;
  background: var(--plt, var(--text-muted)); margin-left: -.2rem;
  box-shadow: 0 0 0 1.5px var(--rq-grund, var(--row-hover)); }
.rq-platform:first-child { margin-left: 0; }
/* The platform is switched off for this deployment. The request keeps it - a
   config change must not consume user data - so the dot says so instead.
   Durchgestrichen geht an einem Punkt nicht; hohl statt gefuellt sagt dasselbe.
   ⚠️ Gefuellt mit der Zeilenfarbe statt transparent: ein durchsichtiger Punkt
   liesse den darunterliegenden Nachbarn durchscheinen, und aus zwei Punkten
   wuerde einer. */
.rq-platform.off { background: var(--rq-grund, var(--row-hover));
  box-shadow: 0 0 0 1.5px var(--rq-grund, var(--row-hover)),
              inset 0 0 0 1.5px var(--plt, var(--text-muted)); }
.btn-del-rq { background: none; border: none; cursor: pointer; color: var(--text-muted);
  font-size: 1rem; padding: 0 .2rem; line-height: 1; }
.btn-del-rq:hover { color: var(--danger); }
/* neu | gefiltert | alle. Eine Gruppe, damit die Zahlen bei umbrechender Zeile
   zusammenbleiben, und mit Tabellenziffern, damit die Spalte ueber mehrere
   Suchauftraege untereinander steht. */
.rq-counts { display: inline-flex; align-items: center; gap: .25rem;
  flex: 0 0 auto; font-size: .78rem; color: var(--text);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
/* ⚠️ --text-muted liegt hier bei 4.29:1 (hell) und 4.16:1 (dunkel) auf
   --row-hover, dem Grund der Menuezeile. Gemessen am 13.10.2026; gefordert sind
   4,5:1. Die Zahlen tragen deshalb --text (16.67:1 bzw. 11.59:1), und der
   Unterschied zwischen „gefiltert“ und „alle“ wird mit dem Gewicht gemacht,
   nicht mit der Helligkeit. */
.rq-shown { font-weight: 600; }
.rq-sep { opacity: .5; }
.rq-all { font-weight: 400; }
/* ⚠️ Weiss auf #27ae60 ergab 2.87:1 und fiel damit durch 1.4.3 — gemessen am
   13.10.2026. In der Ist-Aufnahme von DID-184 fehlte die Pille, weil sie nur
   nach einem Scrape existiert und der Lauf ueber die ausgelieferten Dateien sie
   nie sieht. #1b7a43 kommt auf 5.37:1 und bleibt dasselbe Gruen. */
.rq-new { display: inline-block; background: #1b7a43; color: white;
  border-radius: 20px; font-size: .7rem; padding: .1rem .45rem;
  font-weight: 600; vertical-align: middle; margin-left: .3rem; }
/* In der Titelzeile traegt die Gruppe den Abstand (gap), die Marke also nicht. */
.rq-counts .rq-new { margin-left: 0; }
/* Im dunklen Theme ist weiss auf demselben Gruen zu hart und der Grund dunkel:
   helles Gruen auf dunklem Feld, 9.01:1. */
html.dark .rq-new { background: #16321f; color: #6ee7a0; }
.rq-item { cursor: pointer; }
/* ⚠️ --rq-grund statt background: der Trennring der gestapelten Punkte muss
   dieselbe Farbe tragen wie die Zeile, auf der er liegt, und die aktive
   Zeile hat eine andere. Eine Marke statt dreier Regeln. */
.rq-item.active { --rq-grund: #1a2a3a; outline: 1px solid #2471a3; }
html:not(.dark) .rq-item.active { --rq-grund: #e8f4fd; }
.rq-filters { display: flex; gap: .75rem; flex-wrap: wrap; align-items: center;
  padding: .5rem .6rem .55rem; background: var(--row-hover);
  border: 1px solid #2471a3; border-top: none; border-radius: 0 0 6px 6px;
  margin-top: -4px; }
/* One control per line, each free to shrink: the drawer is narrow when docked,
   and a fixed-width input would push the row past its edge.
   min-width: 0 is what makes that true — the same rule .rq-drawer-only needed.
   The label is the flex item, and its automatic minimum size is its min-content
   width: caption plus the *intrinsic* width of a text input, twenty characters
   wide whatever min-width the input itself carries. Measured ~246px against a
   ~214px box, so the „Ausschliessen" field was drawn across the filter box's
   right border. „Max. Preis" escaped it only because max-width caps a number
   input at 110px. */
.rq-filters label { font-size: .8rem; color: var(--text-muted); display: flex;
  align-items: center; gap: .4rem; flex: 1 1 100%; min-width: 0; }
/* :not([type=range]) — the slider is an input inside .rq-filters too, and this
   rule is text-field chrome. The padding was the „white bar" at the right end of
   the radius track: the browser draws a range's track across the padding box but
   moves its thumb only within the content box, so at ∞ the thumb sat ~9px short
   of the track's end with the unfilled remainder showing beside it, while the
   left-hand padding stayed invisible under the filled part. Measured on the
   docked menu: track 107–262, thumb 226–250. That, not the row's width, is what
   made the thumb look like it stopped at ~90 %; the copy in .filter-bar was
   never affected because this rule does not reach it. */
.rq-filters input:not([type=range]) { padding: .3rem .55rem;
  border: 1px solid var(--border); border-radius: 5px;
  font-size: .82rem; background: var(--input-bg); color: var(--text); font-family: inherit;
  flex: 1 1 auto; min-width: 0; }
.rq-filters input[type=number] { max-width: 110px; }
/* Radius and categories also live in the mobile filter bar; exactly one copy is
   ever visible, so only the visible one can be hit by the paint gesture.
   min-width: 0 for the same reason as on the range input below — without it the
   automatic minimum size of this flex item is its min-content width, caption and
   readout included, so the row grew wider than the docked menu and the ∞ landed
   in the margin beside the box. The thumb stopping short of the track's end was
   *not* this; that was the padding above. */
.rq-drawer-only { flex-basis: 100%; min-width: 0; }
.rq-cats { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: .3rem; align-items: center;
  border-top: 1px dashed var(--border); padding-top: .45rem; }
.rq-cats-label { font-size: .8rem; color: var(--text-muted); margin-right: .15rem; }
.cat-chip { padding: .18rem .55rem; border-radius: 20px; font-size: .75rem; cursor: pointer;
  background: #e8f4fd; color: #2471a3; border: 1px solid transparent; font-family: inherit;
  /* The chip owns the gesture: without this the browser scrolls or drags the
     drawer instead of letting a drag paint across several chips. */
  touch-action: none; user-select: none; -webkit-user-select: none; }
.rq-cats.painting .cat-chip { cursor: crosshair; }
/* Sits above the drawer (z-index 60) so the stroke stays visible over it. */
.paint-trail { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 61;
  pointer-events: none; display: none; opacity: 1; transition: opacity .3s ease; }
.paint-trail.on { display: block; }
.paint-trail.fading { opacity: 0; }
.paint-trail polyline { fill: none; stroke: #e74c3c; stroke-width: 13; opacity: .8;
  stroke-linecap: round; stroke-linejoin: round; }
html.dark .cat-chip { background: #1a2a3a; color: #5dade2; }
.cat-chip.off { background: transparent; color: var(--text-muted);
  border-color: var(--border); text-decoration: line-through; }
.cat-n { opacity: .6; margin-left: .25rem; }
/* Plain muted text, not a pill: as a pill it pushed the title into another line. */
.cat-tag { font-size: .72rem; color: var(--text-muted); margin-left: .45rem;
  white-space: nowrap; }
/* The platform used to be a written-out pill per row. A dot carries the same
   distinction without competing with the title for width. */
.plt-dot { display: inline-block; width: .55rem; height: .55rem; border-radius: 50%;
  margin-right: .4rem; vertical-align: baseline; flex-shrink: 0;
  background: var(--plt, var(--text-muted)); }
/* Die Klasse faerbt nichts mehr selbst, sie reicht nur die Farbe der Plattform
   weiter. So traegt sie derselbe Name am Punkt wie am Knopf, wo dieselbe Farbe
   einmal kraeftig und einmal schwach gebraucht wird. Ein unbekannter Name laesst
   --plt ungesetzt und faellt auf die gedaempfte Schriftfarbe zurueck, statt
   unsichtbar zu werden. */
.plt-ricardo { --plt: var(--plt-ricardo); --plt-fg: var(--plt-ricardo-fg); --plt-text: var(--plt-ricardo-text); }
.plt-tutti   { --plt: var(--plt-tutti);   --plt-fg: var(--plt-tutti-fg);   --plt-text: var(--plt-tutti-text); }
.plt-anibis  { --plt: var(--plt-anibis);  --plt-fg: var(--plt-anibis-fg);  --plt-text: var(--plt-anibis-text); }
.plt-ebay    { --plt: var(--plt-ebay);    --plt-fg: var(--plt-ebay-fg);    --plt-text: var(--plt-ebay-text); }

/* ── Thumbnails / image strip ─────────────────────────────
   One markup for both breakpoints. Here in the desktop table it is clipped to
   the single 56px thumbnail the column has room for; the mobile card below turns
   the same element into a full-width, scroll-snapping strip with dots. */
.th-img { width: 68px; }
td.td-img { padding-right: 0; }
.img-strip-wrap { position: relative; width: 56px; }
.img-strip { display: flex; overflow: hidden; border: 1px solid var(--border);
  border-radius: 6px; background: var(--row-hover); }
.strip-img { flex: 0 0 100%; height: 56px; padding: 0; border: none; display: block;
  background: none; cursor: pointer; }
.strip-img + .strip-img { display: none; }
.strip-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A dead image URL would otherwise leave a broken-image glyph in every row. */
.strip-img.broken img { display: none; }
.strip-dots { display: none; }
.thumb-n { position: absolute; right: 2px; bottom: 2px; background: rgba(0,0,0,.65);
  color: white; font-size: .62rem; line-height: 1; padding: .12rem .28rem; border-radius: 8px;
  pointer-events: none; }
.thumb-none { color: var(--text-muted); font-size: .8rem; }

/* ── Distance column ──────────────────────────────────────
   Without a home location in the settings there is nothing to measure from, so
   the whole column — header, cells and its mobile sort button — disappears. */
.th-dist, .td-dist { white-space: nowrap; }
.td-dist { color: var(--text-muted); }
/* Placeholder for an empty cell: the desktop table wants the dash to keep the
   column readable, the mobile card is better off without a stray "–". */
.cell-none { opacity: .5; }
body:not(.has-home) .th-dist,
body:not(.has-home) .td-dist,
body:not(.has-home) .sb-dist { display: none; }

/* ── Lightbox ─────────────────────────────────────────── */
/* Near-solid: at .92 the header's white text stayed faintly visible through
   the backdrop, which reads as a rendering glitch behind the photo. */
.lightbox { position: fixed; inset: 0; z-index: 80; background: rgba(0,0,0,.97);
  display: none; flex-direction: column; }
.lightbox.on { display: flex; }
.lb-bar { display: flex; align-items: center; gap: .6rem; padding: .7rem .9rem; color: #eee; }
.lb-title { flex: 1; font-size: .9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-note { font-size: .75rem; color: #f5b041; white-space: nowrap; }
.lb-open { color: #5dade2; font-size: .82rem; white-space: nowrap; }
/* 2.5.8 — gemessen 24.4 x 20.8, also an der Hoehe vorbei. */
.lb-close { background: none; border: none; color: #eee; font-size: 1.3rem;
  cursor: pointer; line-height: 1; min-width: 24px; min-height: 24px;
  display: inline-flex; align-items: center; justify-content: center; }
/* touch-action: none hands every finger on the stage to the gesture code — the
   drag follows the image, and two fingers pinch instead of zooming the page.
   overflow keeps a zoomed photo from painting over the bar and the dots. */
.lb-stage { flex: 1; display: flex; align-items: center; justify-content: center;
  position: relative; min-height: 0; padding: 0 .5rem; overflow: hidden;
  touch-action: none; }
.lb-stage img { max-width: 100%; max-height: 100%; object-fit: contain;
  will-change: transform; -webkit-user-select: none; user-select: none;
  -webkit-user-drag: none; }
.lb-stage img.zoomed { cursor: grab; }
.lb-nav { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(0,0,0,.45);
  color: white; border: none; font-size: 2rem; line-height: 1; padding: .4rem .7rem;
  border-radius: 8px; cursor: pointer; }
.lb-prev { left: .6rem; }
.lb-next { right: .6rem; }
.lb-foot { display: flex; flex-direction: column; align-items: center; gap: .4rem;
  padding: .7rem; color: #ccc; font-size: .8rem; }
.lb-dots { display: flex; gap: .35rem; }
.lb-dot { width: 7px; height: 7px; border-radius: 50%; background: #666; }
.lb-dot.on { background: #eee; }

/* ── Sortable column headers ──────────────────────────── */
.th-sort { cursor: pointer; user-select: none; -webkit-user-select: none; }
.th-sort:hover { color: var(--text); }
/* The button is the control (keyboard, Enter/Space); the th only carries aria-sort.
   It must look like the header text it replaces. */
.th-sort button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
  cursor: pointer; text-align: inherit; text-transform: inherit; letter-spacing: inherit; }
.sort-ind { font-size: .8rem; margin-left: .3rem; opacity: .55; }
.sort-ind.on { opacity: 1; }

/* Replaces the clickable thead on mobile, where thead is hidden. */
.sort-bar { display: none; align-items: center; gap: .4rem; margin-bottom: .5rem; }
.sort-bar-label { font-size: .8rem; color: var(--text-muted); }
.sort-bar button { background: var(--surface); color: var(--text); border: 1px solid var(--border-ui);
  border-radius: 20px; padding: .25rem .7rem; font-size: .8rem; font-family: inherit; cursor: pointer; }
.sort-bar.hidden { display: none; }

/* ── Radius slider ────────────────────────────────────────
   Detents are non-linear (1 km steps to 20, then 5, then 25, then ∞), so the
   readout beside it is not decoration — it is the only place the chosen value
   is legible. Tabular figures keep it from twitching while dragging. */
.f-radius { display: flex; align-items: center; gap: .5rem; width: 100%;
  font-size: .8rem; color: var(--text-muted); }
.f-radius-cap { flex-shrink: 0; }
/* min-width: 0 matters — a range input's intrinsic minimum is wide enough to
   push the readout out of the docked menu. The wrapping .rq-drawer-only needs
   the same rule; this one alone does not save it. */
.f-radius input[type=range] { flex: 1 1 60px; min-width: 0; accent-color: #2471a3;
  height: 1.5rem; }
.f-radius-val { flex-shrink: 0; min-width: 3.2rem; text-align: right; color: var(--text);
  font-variant-numeric: tabular-nums; font-weight: 600; }

/* ── Mobile filter bar ────────────────────────────────────
   Hidden wherever the drawer is permanently docked; below that it carries the
   controls that get touched constantly and sticks under the header. */
/* ── Die Filterleiste ──────────────────────────────────────
   Eine Leiste fuer beide Geraete (DID-271). Zugeklappt eine Zeile Knoepfe,
   der gewaehlte Abschnitt darunter. ⚠️ Kein max-height und kein overflow mehr:
   die Kategorien scrollten frueher in einem eigenen Kasten, und ein Kasten,
   der im Fluss der Seite scrollt, findet mit dem Finger niemand. Zugeklappt
   braucht es ihn nicht. */
.filter-bar { display: none; }
.filter-bar.on { display: block; margin: 0 0 .7rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: .5rem .6rem; }
.fb-row { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.fb-luecke { flex: 1 1 auto; }
.fb-tab { font: inherit; font-size: .8rem; font-weight: 600; cursor: pointer;
  display: inline-flex; align-items: center; gap: .3rem; min-height: 36px;
  padding: .25rem .7rem; border-radius: 999px;
  border: 1px solid var(--border-ui); background: transparent; color: var(--text); }
.fb-tab.on { border-color: var(--link); background: var(--rm1-bg); color: var(--link); }
.fb-tab:hover { border-color: var(--link); }
.fb-tab:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }
.fb-caret { font-size: .7rem; opacity: 1; }
.fb-body { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--border); }
.fb-body .rq-cats, .fb-body .ex-words { border-top: none; padding-top: 0; margin: 0;
  display: flex; flex-wrap: wrap; align-items: center; gap: .3rem; }
.f-text { flex: 1 1 14rem; min-width: 8rem; font: inherit; font-size: .8rem;
  padding: .3rem .5rem; border: 1px solid var(--border-ui); border-radius: 4px;
  background: var(--bg); color: var(--text); }
.f-text-ex { flex: 1 1 10rem; }
.f-span { display: inline-flex; align-items: center; gap: .3rem;
  font-size: .78rem; color: var(--text-muted); }
.f-cap { color: var(--text-muted); }
.f-num { width: 5rem; font: inherit; font-size: .8rem; padding: .3rem .4rem;
  border: 1px solid var(--border-ui); border-radius: 4px;
  background: var(--bg); color: var(--text); }
.f-einheit { color: var(--text-muted); font-size: .78rem; }
/* Preis wie Umkreis: geschoben, nicht getippt — aber EIN Regler mit zwei
   Griffen, einer von unten, einer von oben.

   ⚠️ Darunter liegen zwei echte <input type="range"> uebereinander. Die Spur
   nimmt keine Zeiger an, nur die Griffe — sonst faenge der obere jeden Klick
   ab und der untere waere mit der Maus unerreichbar. Mit der Tastatur bleiben
   beide erreichbar, und genau dafuer sind es zwei Felder und kein Nachbau aus
   divs. */
.f-preis-paar { display: flex; align-items: center; gap: .5rem; flex: 1 1 18rem; }
.f-preis-spur { position: relative; flex: 1 1 auto; min-width: 7rem; height: 22px; }
.f-preis-spur::before { content: ""; position: absolute; left: 0; right: 0; top: 50%;
  transform: translateY(-50%); height: 4px; border-radius: 2px; background: var(--border); }
.f-preis-fuellung { position: absolute; top: 50%; transform: translateY(-50%);
  height: 4px; border-radius: 2px; background: var(--link-solid); }
.f-preis-griff { position: absolute; left: 0; top: 0; width: 100%; height: 22px;
  margin: 0; background: none; -webkit-appearance: none; appearance: none;
  pointer-events: none; }
.f-preis-griff::-webkit-slider-thumb { -webkit-appearance: none; pointer-events: auto;
  width: 18px; height: 18px; border-radius: 50%; border: 2px solid var(--surface);
  background: var(--link-solid); cursor: pointer; }
.f-preis-griff::-moz-range-thumb { pointer-events: auto;
  width: 18px; height: 18px; border-radius: 50%; border: 2px solid var(--surface);
  background: var(--link-solid); cursor: pointer; }
.f-preis-griff::-webkit-slider-runnable-track { background: none; }
.f-preis-griff::-moz-range-track { background: none; }
.f-preis-griff:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--text); outline-offset: 2px; }
.f-preis-griff:focus-visible::-moz-range-thumb { outline: 2px solid var(--text); outline-offset: 2px; }
.f-preis-val { min-width: 3rem; font-variant-numeric: tabular-nums;
  color: var(--text); font-weight: 600; flex: none; font-size: .78rem; }
.f-preis-bis { text-align: right; }
.f-text:focus-visible, .f-num:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }

@media (max-width: 1023px) {
  /* Am Rand der Schmalansicht darf sie die Breite nehmen, die die Seite hat. */
  .filter-bar.on { position: sticky; top: 0; z-index: 40;
    margin: 0 -1.5rem .7rem; border-radius: 0; border-left: none; border-right: none;
    padding: .5rem 1.5rem; box-shadow: 0 2px 6px var(--shadow); }
  .rq-drawer-only { display: none; }
}
@media (max-width: 768px) {
  .filter-bar.on { margin: 0 -.75rem .7rem; padding: .5rem .75rem; }
  /* Mit dem Finger getroffen, nicht mit dem Zeiger. */
  .fb-tab { min-height: 40px; font-size: .82rem; padding: .3rem .8rem; }
}

/* ── Permanent menu (wide viewports) ──────────────────────
   The drawer stops being an overlay and becomes a sticky column beside the
   content. body is the grid: header spans it, the content and the menu share
   row two, and html[data-hand] decides which side the menu docks to. */
@media (min-width: 1024px) {
  body { display: grid; align-items: start;
         grid-template-columns: minmax(0, 1fr) var(--menu-w); }
  html[data-hand="left"] body { grid-template-columns: var(--menu-w) minmax(0, 1fr); }
  /* Every row is named explicitly. Left-handed puts the menu in column 1, which
     auto-placement would read as "behind the content" and drop into a third row. */
  header { grid-column: 1 / -1; grid-row: 1; }
  /* As a grid item, `margin-inline: auto` suppresses stretching and the box
     shrink-wraps its content. With the table that still looked plausible —
     tables have an intrinsic width — but the map has none and collapsed to
     387px. width:100% restores the stretch; max-width still caps it. */
  /* .set-wrap ist dasselbe für die Einstellungsseite. Ausdrücklich benannt,
     nicht der Auto-Platzierung überlassen — aus demselben Grund wie oben. */
  .container, .set-wrap { grid-column: 1; grid-row: 2; width: 100%; }
  html[data-hand="left"] .container,
  html[data-hand="left"] .set-wrap { grid-column: 2; }
  /* Must out-specify html[data-hand="…"] .drawer, which anchors and hides it. */
  html[data-hand] .drawer {
    grid-column: 2; grid-row: 2; align-self: start; position: sticky; top: 1.5rem;
    left: auto; right: auto; bottom: auto; transform: none; transition: none;
    width: auto; margin: 2rem 1.5rem 2rem 0; max-height: calc(100vh - 3rem);
    border-radius: 10px; box-shadow: 0 1px 4px var(--shadow); }
  html[data-hand="left"] .drawer { grid-column: 1; margin: 2rem 0 2rem 1.5rem; }
  /* Always visible, so neither the burger nor the scrim has anything to do. */
  .btn-burger, .scrim { display: none; }
}

/* ── Mobile responsive ────────────────────────────────── */
@media (max-width: 768px) {
  .container { padding: 0 .75rem; margin: 1rem auto; }
  header { padding: .9rem 1rem; gap: .6rem; }
  header h1 { font-size: 1.1rem; }
  header span { font-size: .78rem; }
  th, td { padding: .5rem .6rem; font-size: .8rem; }

  /* Rows become cards so nothing has to scroll sideways. The two hidden
     columns keep their inline display:none — inline beats a stylesheet rule. */
  .sort-bar { display: flex; }
  .table-wrap { overflow-x: visible; }
  thead { display: none; }
  table, tbody { display: block; }
  tr { display: grid; grid-template-columns: 1fr auto auto; column-gap: .6rem;
       grid-template-areas: "img img img" "title title title" "price dist end";
       padding: .6rem .7rem; border-bottom: 1px solid var(--row-border);
       /* pan-y leaves vertical scrolling to the browser and hands only the
          horizontal gesture to JS. */
       position: relative; touch-action: pan-y; overflow: hidden; }
  tr:last-child { border-bottom: none; }
  /* The cells slide, not the row: that way the label below stays put while
     the card moves off it. They need an opaque background to cover it. */
  td { display: block; border: none; padding: .15rem 0;
       background: var(--surface); position: relative; z-index: 1; }
  /* Only while a swipe is running: the cells cover their own boxes but not the
     row padding and column gap, so a permanent backdrop would frame every card
     in red. */
  tr.swiping::before, tr.settling::before {
    content: "Ausblenden"; position: absolute; inset: 0; z-index: 0;
    display: flex; align-items: center; justify-content: center;
    background: #8c2f22; color: #fff; font-size: .85rem; font-weight: 600; }
  tr.swiping td { transition: none; }
  tr.settling td { transition: transform .2s ease; }
  /* Photos are what one actually shops by, so on the card they get the full
     width and bleed into the card padding. tr already clips with overflow. */
  .td-img { grid-area: img; padding: 0; margin: -.6rem -.7rem .4rem; }
  .td-img:has(.thumb-none) { margin: 0; padding: .15rem 0; }
  .img-strip-wrap { width: auto; }
  /* pan-x is the whole point: without it the tr rule above would hand every
     horizontal drag to the row-swipe handler instead of scrolling the strip.
     pan-y belongs beside it — touch-action names every direction the browser may
     still have, so pan-x alone also forbade scrolling the page from a finger that
     landed on a photo, and the photo is most of the card. */
  .img-strip { overflow-x: auto; scroll-snap-type: x mandatory; touch-action: pan-x pan-y;
    border: none; border-radius: 0; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
  .img-strip::-webkit-scrollbar { display: none; }
  .strip-img { height: 170px; scroll-snap-align: center; }
  .strip-img + .strip-img { display: block; }
  .strip-dots { display: flex; justify-content: center; gap: .3rem; padding-top: .35rem; }
  .strip-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--border);
    transition: background .15s; }
  .strip-dot.on { background: var(--text-muted); }
  /* Below the strip sit the dots, so the count moves out of their way. */
  .img-strip-wrap .thumb-n { top: .4rem; right: .5rem; bottom: auto; }
  .td-title { grid-area: title; font-weight: 500; }
  .td-price { grid-area: price; }
  .td-dist { grid-area: dist; }
  .cell-none { display: none; }
  .td-end { grid-area: end; text-align: right; }
  /* ⚠️ Die Zelle war hier ganz ausgeblendet, weil der Wisch das Ausblenden
     uebernimmt - und damit war auch der Weg zum Inserat weg. Der Knopf
     "Ausblenden" bleibt verborgen, der zum Marktplatz nicht. */
  /* ⚠️ Kein Knopf mehr: ein Tipp auf den Titel oeffnet das Inserat, und
     das ist die Geste, die ein Mensch ohnehin versucht. Der Langdruck
     markiert weiterhin — die beiden stoeren sich nicht, weil der Tipp
     abbricht, sobald eine Markierung steht. Ausblenden macht der Wisch. */
  .td-actions { display: none; }
  .td-title { cursor: pointer; }
  /* Wie der Ausblenden-Knopf gebaut, aber gedaempft: in der Zeile darunter,
     mittig, fett - und nicht die lauteste Farbe der Karte. */
  .lb-nav { font-size: 1.6rem; padding: .3rem .5rem; }
  .lb-title { font-size: .82rem; }
}

/* Touch devices turn the pages by swiping; the arrows would only sit on top of
   the photo and steal taps. */
@media (hover: none) and (pointer: coarse) {
  .lb-nav { display: none !important; }
}


/* The styling used to sit inline on the element, where no stylesheet rule could
   override it — which is why a scrape problem still rendered in muted grey. */
#scrapeStatus { font-size: .78rem; color: var(--text-muted); margin-top: .4rem; }
#scrapeStatus.scrape-problem { color: var(--lauf-fehler); }
/* Ein Eintrag je Plattform, solange ein Lauf geht. Bricht um, statt ueberzulaufen:
   das angedockte Menue ist nur ~316px breit.
   ⚠️ Die Farbe traegt den MARKT, das Zeichen (✓ ✕ – ⚠, „läuft…") den ZUSTAND
   (DID-334). Vorher sagten Farbe und Haken beide „geliefert", und die
   Zugehörigkeit zu einem Markt sagte gar nichts: das sichtbare „·" stand
   INNERHALB einer Plattform, zwischen den Plattformen nur ein Wortabstand —
   das schwaechere Trennzeichen an der staerkeren Grenze. */
#scrapeProgress { font-size: .75rem; color: var(--text-muted); margin-top: .25rem;
  display: flex; flex-wrap: wrap; gap: .1rem .75rem; }
/* Fehlschlag und Warnung ohne Marktfarbe: die Zeile unter den Plattformen und
   der Nachlauf. Die Plattformeintraege ueberschreiben die Farbe weiter unten. */
#scrapeProgress .sp-error { color: var(--lauf-fehler); font-weight: 700; }
/* Geliefert, aber nicht vollstaendig. Weder gruen noch rot, weil es weder das
   eine noch das andere ist. */
#scrapeProgress .sp-partial { color: var(--lauf-teil); }
/* Der Markt: seine Farbe als Schrift (eigene Token, siehe --plt-*-text). Steht
   nach .sp-error und .sp-partial, damit ein gescheiterter Markt seine Farbe
   behaelt; der Fehlschlag faellt dort durch Fettdruck und ✕ auf. */
#scrapeProgress .sp-plt { color: var(--plt-text, var(--text-muted)); }
/* Der Nachlauf ist kein Markt und liest sich in derselben Zeile wie ein vierter.
   Eigene Zeile, so wie .sp-why. */
#scrapeProgress .sp-nachlauf { flex-basis: 100%; }
/* Eine Zeile ohne Zahl braucht ein anderes Zeichen dafuer, dass sie lebt: der
   wachsende Zaehler war vorher genau dieses Zeichen. */
#scrapeProgress .sp-pulse { animation: sp-breathe 1.4s ease-in-out infinite; }
@keyframes sp-breathe { 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { #scrapeProgress .sp-pulse { animation: none; } }
#scrapeProgress .sp-disabled { text-decoration: line-through; }
/* Der Grund eines Fehlschlags, sichtbar statt nur im `title` (DID-316). Eigene
   Zeile, weil der Streifen ein flex-Container ist und der Satz sonst neben der
   letzten Plattform stuende; `flex-basis: 100%` erzwingt den Umbruch. Die Farbe
   kommt von .sp-partial daneben — hier entsteht bewusst kein zweiter
   Farbentscheid, und tests/test_kontrast.py haelt die Token, nicht diese Regel.
   Keine display-Regel: die Zeile wird gar nicht erst gebaut, wenn nichts
   gescheitert ist, statt versteckt zu werden. */
#scrapeProgress .sp-why { flex-basis: 100%; line-height: 1.35; }
/* Ein „i" im Kreis, nicht das Unicode-ⓘ: das rendert in mehreren Schriften als
   Emoji und bringt dann eigene Farbe und Groesse mit. currentColor haelt es an
   der Farbe der Zeile, in der es steht. */
.info-i { display: inline-flex; align-items: center; justify-content: center;
  width: .95em; height: .95em; margin-left: .25rem; border-radius: 50%;
  border: 1px solid currentColor; font-size: .9em; font-style: italic;
  line-height: 1; opacity: .75; cursor: help; }
/* Nothing at all to show is the empty state of the platform picker, not a run. */
.platform-none { font-size: .8rem; color: var(--text-muted); }

/* First-run nudge for the home location. Dismissible, because a user who does
   not want distances should not be asked twice. */
/* Ohne diese Regel ist das hidden-Attribut wirkungslos: es wirkt nur über die
   Browser-Vorgabe display:none, und die display:flex-Regel unten schlägt sie.
   Der Hinweis liess sich deshalb weder schliessen noch bei gesetztem Standort
   ausblenden, obwohl das JavaScript beides korrekt anfragte. */
.onboard[hidden] { display: none; }
.onboard { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap;
  background: var(--accent-bg); color: var(--accent-fg);
  border-radius: 8px; padding: .7rem .9rem; margin-bottom: .8rem; font-size: .85rem; }
.onboard span { flex: 1; min-width: 12rem; }
.onboard .btn { padding: .35rem .9rem; font-size: .8rem; }
/* ⚠️ 2.5.8 verlangt 24x24 CSS-Pixel. Gemessen bei 390 px: 19.4 x 21.6 —
   und der Knopf steht neben anderen, die Abstandsausnahme greift nicht. */
.onboard-x { background: none; border: none; cursor: pointer; color: inherit;
  font-size: .95rem; line-height: 1; min-width: 24px; min-height: 24px;
  display: inline-flex; align-items: center; justify-content: center; }

/* Der Einrichtungs-Balken traegt Schalter und einen Verweis. Die Schalter
   duerfen nicht wie der Text mitwachsen, sonst reisst die Zeile auf; der
   Verweis bricht in eine eigene Zeile, wenn es eng wird. */
.onboard .onboard-toggles { flex: 0 0 auto; }
.onboard .onboard-toggles .btn-toggle { flex: 0 0 auto; font-size: .8rem; padding: .35rem .7rem; }
/* ⚠️ Kein `opacity` mehr: .85 drueckte denselben Ton auf 3.38:1. Eine
   Daempfung, die den Kontrast frisst, ist keine Daempfung. */
.onboard-note { flex: 1 1 100%; font-size: .78rem; }
.onboard-note a { color: inherit; }

/* Vorschlaege fuer die Ausschliessen-Liste (DID-259). Dieselben Chips wie bei
   den Kategorien, damit die Malgeste sich gleich anfuehlt — nur der Behaelter
   ist ein anderer, und daran unterscheidet catContext() die beiden. */
.ex-bar { display: none; }
.ex-bar.on { display: block; margin: .2rem 0 .5rem; }
.ex-words { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem; }
.ex-words.painting { cursor: crosshair; }

/* ── Filter-Zeile und Markier-Leiste (DID-271) ──────────────
   Die Filter-Zeile ist das Gegenstueck zur Ausschlussliste darueber: was
   bleiben soll statt was weg soll. Beide Zeilen stehen ueber der Tabelle, weil
   der <thead> auf schmalen Schirmen ausgeblendet ist. */
.filter-row { display: none; }
.filter-row.on { display: block; margin: .2rem 0 .5rem; }
.fr-row { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .5rem; }
.f-text { flex: 1 1 14rem; min-width: 8rem; font: inherit; font-size: .8rem;
  padding: .2rem .45rem; border: 1px solid var(--border-ui); border-radius: 4px;
  background: var(--bg); color: var(--text); }
/* Das Feld der Ausschlussliste steht neben den Chips und darf die Zeile nicht
   sprengen, wenn viele Chips da sind. */
.f-text-ex { flex: 1 1 10rem; }
.f-span { display: inline-flex; align-items: center; gap: .2rem;
  font-size: .78rem; color: var(--text-muted); }
.f-num { width: 4.5rem; font: inherit; font-size: .8rem; padding: .2rem .35rem;
  border: 1px solid var(--border-ui); border-radius: 4px;
  background: var(--bg); color: var(--text); }
.f-bis, .f-einheit { color: var(--text-muted); font-size: .78rem; }
.f-text:focus-visible, .f-num:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }

/* Erscheint ueber der Markierung. `position: absolute` mit Seitenkoordinaten,
   damit sie beim Scrollen am Text bleibt; tabfilter.js rechnet sie aus. */
#selbar { position: absolute; z-index: 70; display: none; gap: .25rem; padding: .25rem;
  background: var(--surface); border: 1px solid var(--border-ui); border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .25); }
#selbar.show { display: flex; }
#selbar button { font: inherit; font-size: .78rem; padding: .18rem .5rem; cursor: pointer;
  border: 1px solid var(--border-ui); border-radius: 4px;
  background: var(--bg); color: var(--text); white-space: nowrap; }
#selbar button:hover { border-color: var(--link); }
#selbar button:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }
/* Auf dem Telefon wird mit dem Finger getroffen, nicht mit dem Zeiger: WCAG
   2.2 nennt 24 px als Mindestmass, und die native Auswahlleiste des Browsers
   liegt haeufig direkt daneben. Deshalb hier groesser, nicht nur breiter. */
@media (pointer: coarse), (max-width: 768px) {
  #selbar { padding: .35rem; gap: .35rem; }
  #selbar button { font-size: .9rem; padding: .5rem .8rem; min-height: 44px; }
}

/* Hinweis unter dem Scrape-Knopf (DID-255). Genau einer ist sichtbar, und
   die Grenze ist dieselbe wie bei der Karte (MAP_NARROW_PX, 768). */
.scrape-hint { margin: .35rem 0 0; font-size: .74rem; line-height: 1.35;
               color: var(--text-muted); }
.scrape-hint-mobil { display: none; }
@media (max-width: 768px) {
  .scrape-hint-mobil { display: block; }
  .scrape-hint-desktop { display: none; }
}
/* Die Marktpunkte sind seit DID-255 Bedienelemente: sie brauchen einen
   Zeiger und einen sichtbaren Fokus, sonst ist die Tastaturbedienung zwar
   da und trotzdem unbenutzbar. */
.rq-platform { cursor: pointer; }
.rq-platform.off { cursor: default; }
.rq-item { transition: transform .18s ease; }
.rq-item.swiping { transition: none; }
/* Der Weg zum Inserat, seit der Titel kein Link mehr ist (DID-271): ein
   Langdruck auf einen Link markiert keinen Text, sondern oeffnet das
   Kontextmenue. Gross genug fuer den Finger - 44 px, WCAG 2.2 verlangt 24.
   Die Titelzelle wird im Gegenzug ausdruecklich markierbar, auch dort, wo
   ein Browser Tabellentext von sich aus nicht hergibt. */
/* Dieselbe Zeile, zwei gleichwertige Aktionen — also auch dieselbe Form.
   Vorher war der eine ein Kasten mit Rahmen und der andere ein Knopf, was
   aussah, als taete einer von beiden etwas anderes. */
.btn-open { display: inline-flex; align-items: center; justify-content: center;
  background: #666; color: white; border: none; padding: .3rem .8rem;
  border-radius: 6px; cursor: pointer; font-size: .8rem; margin-left: .3rem;
  text-decoration: none; min-height: 32px; }
.btn-open:hover { background: #444; }
.btn-open:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }
.td-title { user-select: text; -webkit-user-select: text; }

/* ── Text nur fuer Hilfsmittel ──────────────────────────────────────────────
   Ein Zeichen allein traegt keine Aussage (WCAG 1.4.1, CLAUDE.md), und ein
   `title` erreicht weder Tastatur noch Screenreader. Diese Klasse haengt den
   Satz daneben: fuer Hilfsmittel da, fuer das Auge weg.

   ⚠️ `clip-path`, **nicht** `display: none` und nicht `visibility: hidden` —
   beide nehmen den Knoten auch dem Screenreader weg, womit genau die Aussage
   fehlte, um die es hier geht.

   ⚠️ Und bewusst **ohne** eigene `display`-Regel: ein `hidden` im Markup haengt
   allein an der Browser-Vorgabe `display: none`, und jede eigene display-Regel
   schlaegt sie (tests/test_static_assets.py, test_hidden_elements_are_really_hidden).
   So laesst sich derselbe Knoten ein- und ausschalten, statt dauerhaft
   mitgelesen zu werden.

   Die Technik stand im Projekt bis hierher genau **einmal**, in `comic.py`
   (dort an der Pausetaste) — nachgemessen, nicht angenommen. ⚠️ Der Name der
   Klasse steht hier bewusst nicht: tests/test_static_assets.py verbietet jede
   Nennung einer Comic-Klasse in app.css, weil beide CSS auf der FAQ
   zusammentreffen. Dort bleibt die Regel auch: die
   Landing Page laedt app.css nicht, sie traegt ihr CSS inline. */
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden;
           clip-path: inset(50%); white-space: nowrap; }

/* Die Sanduhr vor „Karte", solange fuer den offenen Suchauftrag noch
   Reisezeiten nachkommen. Keine display-Regel, damit das `hidden` im Markup
   wirkt — siehe oben. */
.karte-sanduhr { margin-right: .25rem; }

/* Tastaturkürzel (DID-321): die Zeile, auf der j/k stehen, und die Hilfe. */
#tbody tr.kz-aktiv { outline: 2px solid var(--text); outline-offset: -2px; }
.kz-menue-knopf { background: none; border: 0; padding: 0; font: inherit;
  color: var(--link); cursor: pointer; text-align: left; }
.kz-menue-knopf:hover { text-decoration: underline; }
.kz-schleier { position: fixed; inset: 0; z-index: 90; background: rgba(0,0,0,.55);
  display: flex; align-items: center; justify-content: center; padding: 1rem; }
.kz-schleier[hidden] { display: none; }
.kz-dialog { background: var(--surface); color: var(--text); border-radius: 8px;
  max-width: 28rem; width: 100%; max-height: 90vh; overflow: auto; padding: 1rem 1.2rem; }
.kz-dialog h2 { margin: 0 0 .7rem; font-size: 1.1rem; }
.kz-liste { display: grid; grid-template-columns: max-content 1fr; gap: .35rem .9rem; margin: 0 0 .8rem; }
.kz-liste dt, .kz-liste dd { margin: 0; }
.kz-liste kbd { border: 1px solid var(--border-ui); border-radius: 4px; padding: 0 .35rem;
  font-family: inherit; font-size: .85rem; }
.kz-stand { font-size: .82rem; color: var(--text-muted); }
.kz-zu { display: block; margin-top: .8rem; background: var(--input-bg); color: var(--text);
  border: 1px solid var(--border-ui); border-radius: 6px; padding: .5rem .9rem; cursor: pointer; font: inherit; }
