/* style.css — dunkles, kantiges Design nach dem Formsystem von hemmersbach.tools
   (das seinerseits vom Corporate Design auf hemmersbach.com abgeleitet ist):

   - Signalorange ist der einzige Marken-Akzent und bleibt für Interaktion reserviert
     (Buttons, Aktiv-Zustände, Hover, Eyebrow-Label). Grün und Rot behalten daneben ihre
     Bedeutung als Zustandsfarben — sonst wäre "gefunden" von "Achtung" nicht unterscheidbar.
   - Keine Rundungen und keine Schatten: Flächen werden ausschließlich über 1px-Haarlinien
     getrennt. Ausnahme ist das Foto-Modal, das als einziges Element wirklich über der Seite
     schwebt und dafür Tiefe braucht.
   - Überschriften und Tabellenköpfe laufen schmal und versal über die Breitenachse von Archivo.

   Bewegung läuft hier bewusst *immer*: Es gibt in dieser Datei keine
   `@media (prefers-reduced-motion: reduce)`-Blöcke mehr, und es sollen auch keine neuen
   hinzukommen. Windows meldet die Einstellung "Animationen in Windows anzeigen: aus" an den
   Browser, und in vielen Firmen-Images steht sie aus Gewohnheit auf aus — die Anwendung sähe dort
   ohne Grund abgehackt aus. Die Bewegungen sind kurz, klein und ohne Parallaxe oder Blitzen, das
   Weglassen brächte niemandem etwas. Wer weiche Bewegung programmgesteuert braucht (Scrollen),
   nimmt die Helfer aus js/scrollen.js — Browser schalten das native `behavior: 'smooth'` bei
   dieser Systemeinstellung von sich aus ab. */

/* Archivo ersetzt die GT America des Corporate Designs. Bewusst selbst gehostet statt per
   <link> von fonts.googleapis.com: Das Tool läuft lokal und im Firmennetz, wo der Google-CDN
   regelmäßig geblockt ist — dort bliebe sonst Arial ohne Breitenachse übrig und die schmalen
   Versalien fielen ersatzlos weg. */
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 300 700;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url("fonts/archivo-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 300 700;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url("fonts/archivo-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
    U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* Marke */
  --brand: hsl(17 89% 58%);
  --brand-hover: hsl(17 79% 48%);
  --brand-faint: hsl(17 89% 58% / 0.13);
  --brand-line: hsl(17 89% 58% / 0.45);

  /* Flächen — vier Stufen reichen: Seite, Karte, erhöhte Karte (Tabellenkopf), Hover. */
  --color-bg: hsl(240 2% 12%);
  --color-bg-sunken: hsl(240 3% 8%);
  --color-surface: hsl(240 2% 15%);
  --color-surface-raised: hsl(240 2% 19%);
  --color-surface-hover: hsl(0 0% 100% / 0.06);
  --color-black: #0b0b0c;

  --color-dot: hsl(0 0% 100% / 0.045);

  /* Text */
  --color-text: hsl(0 0% 100%);
  --color-text-muted: hsl(0 0% 66%);
  --color-text-faint: hsl(0 0% 45%);
  --color-text-on-brand: #111112;

  --color-border: hsl(0 0% 24%);
  --color-border-strong: hsl(0 0% 34%);

  /* Zustandsfarben, für dunklen Grund aufgehellt. Rot liegt bewusst weit genug von der
     Markenfarbe entfernt, damit eine Warnung nicht wie ein Bedienelement aussieht. */
  --color-danger: hsl(4 82% 66%);
  --color-danger-surface: hsl(4 45% 16%);
  --color-danger-surface-hover: hsl(4 45% 21%);
  --color-success: hsl(145 48% 55%);
  --color-success-faint: hsl(145 48% 55% / 0.14);
  --color-success-line: hsl(145 48% 55% / 0.42);
  /* Blau für "gerade eben direkt geholt". Weder Zustand noch Bedienelement, sondern reine
     Herkunftsangabe — deshalb eine eigene Farbe und nicht Grün (das steht für den Cache-Treffer)
     oder Orange (das gehört der Marke). */
  --color-info: hsl(205 78% 62%);
  --color-info-faint: hsl(205 78% 62% / 0.14);
  --color-info-line: hsl(205 78% 62% / 0.42);
  /* Gedämpftes Rot für das durchgestrichene Foto-Icon. Bewusst nicht --color-danger: Ein Teil
     ohne Bild ist kein Fehler, und in einer Liste mit 300 Zeilen sähe das volle Warnrot aus wie
     eine Mängelliste. Blass genug, um neben dem Grün als "gibt es nicht" zu lesen. */
  --color-photo-none: hsl(4 42% 54%);
  /* Bernstein für Hinweise, die weder Fehler noch Bedienelement sind. Bewusst gelber als die
     Markenfarbe, damit Orange für Interaktion reserviert bleibt. */
  --color-warning: hsl(38 95% 58%);
  --color-warning-faint: hsl(38 95% 58% / 0.13);
  --color-warning-line: hsl(38 95% 58% / 0.45);

  --font: "Archivo", "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
  --font-mono: "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;

  /* Gemeinsames 4-/8-Pixel-Raster für Maße und Abstände. Farben, Flächen und Zustände bleiben
     davon vollständig unberührt. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --control-size: 2.75rem;

  /* Inhaltsbreite — umschaltbar über das Segmented Control rechts in der Kopfzeile.
     Der Wert wird per data-width am <html> gesetzt (siehe Regeln unten). Kopfzeile,
     Inhalt und Fußzeile teilen sich Breite und Innenabstand, damit die linke Kante
     über die ganze Seite auf einer Linie liegt. */
  --content-width: 980px;
  --gutter: 1.5rem;

  /* Senkrechter Rhythmus: *ein* Abstand zwischen allen Blöcken der Inhaltsspalte — Suchfeld,
     Statuszeile, Trefferkopf, Modellauswahl, Steuerung, Kopierbalken, Tabelle.

     Gruppiert wird nicht über unterschiedlich große Abstände, sondern darüber, was sich einen
     Rahmen teilt: Reiterleiste und Filterzeile stehen ohne jeden Abstand in einem Kasten
     (.results-head), alles andere steht mit diesem einen Abstand darunter. Dadurch bleibt die
     Seite ruhig, und wo etwas zusammengehört, sieht man es am Rahmen statt an einer
     Abstandsstufe, die man erst mit dem Lineal von der nächsten unterscheidet. */
  --space-block: var(--space-5);

  /* Was der Browser selbst zeichnet (Scrollbalken, Fokusringe, Textmarkierung, Autofill,
     Kontextmenüs), soll mitdunkeln. */
  color-scheme: dark;
}

:root[data-width="compact"] { --content-width: 760px; }
:root[data-width="standard"] { --content-width: 980px; }
:root[data-width="wide"] { --content-width: 1280px; }
:root[data-width="full"] { --content-width: 100%; --gutter: 2rem; }

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Native Scrollbar der Seite komplett versteckt — sie wird durch die Eigenbau-Leiste
   (#custom-scrollbar, siehe unten + app.js) ersetzt. Grund: Chrome/Windows reserviert bei
   scrollbar-gutter: stable dauerhaft ein paar Pixel Breite, auch wenn die Seite (noch) gar
   nicht scrollbar ist; Firefox macht das mit seiner Overlay-Scrollbar nicht — je nach
   Browser sah die Seite also unterschiedlich breit aus. Die eigene Leiste ist position:
   fixed und nimmt nie Platz vom Layout weg, verhält sich also in jedem Browser gleich und
   springt beim Erscheinen nicht. */
html {
  scrollbar-width: none;
  -ms-overflow-style: none;
  -webkit-text-size-adjust: 100%;
}

html::-webkit-scrollbar {
  display: none;
}

/* overflow-x: clip auf html *und* body kappt den Lichtschein (body::before), der absichtlich
   über den rechten Fensterrand hinausragt. Ohne das Kappen zählt dieser Überstand zur
   scrollbaren Fläche: Schon die leere Startseite bekommt eine waagerechte Scrollleiste — und
   weil deren Höhe von der sichtbaren Fensterhöhe abgeht, das min-height: 100vh unten aber
   unverändert bleibt, obendrein eine senkrechte über wenige Pixel.

   Beide Elemente sind nötig: Steht die Regel nur an einem von beiden, reicht der Browser sie
   ans Viewport durch (overflow-Propagation) und dort wird nur die Scrollleiste versteckt, der
   Bereich bleibt scrollbar. Erst wenn html nicht mehr `visible` ist, entfällt das Durchreichen
   und body kappt selbst. `clip` statt `hidden`, weil `hidden` aus body einen Scrollcontainer
   machen würde — die klebende Kopfzeile würde dann nicht mehr kleben.
   Breite Inhalte scrollen weiterhin in ihrem eigenen Container (.table-wrapper). */
html,
body {
  overflow-x: clip;
}

body {
  font-family: var(--font);
  background-color: var(--color-bg);
  background-image: radial-gradient(circle, var(--color-dot) 1px, transparent 1px);
  background-size: 24px 24px;
  color: var(--color-text);
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  position: relative;
  -webkit-font-smoothing: antialiased;
}

/* Oranger Lichtschein oben rechts — dasselbe Motiv wie im Hero der Landingpage. Bewusst
   absolut und nicht fixed: Der Schein gehört zum Seitenkopf und soll beim Scrollen durch
   eine lange Trefferliste nach oben verschwinden, statt mitzuwandern. Liegt hinter dem
   Inhalt (z-index 0), Kopfzeile und Inhalt darüber. */
body::before {
  content: "";
  position: absolute;
  top: -30vh;
  right: -10vw;
  width: min(60vw, 720px);
  aspect-ratio: 1;
  background: radial-gradient(circle at 50% 50%, hsl(17 89% 58% / 0.16), transparent 68%);
  pointer-events: none;
  z-index: 0;
}

::selection {
  background: var(--brand);
  color: var(--color-text-on-brand);
}

/* Gemeinsame Breitenbegrenzung für Kopfzeile, Inhalt und Fußzeile. */
.wrap {
  width: 100%;
  max-width: var(--content-width);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Überschriften des Corporate Designs: schmal, versal, eng laufend. font-stretch greift auf
   die Breitenachse von Archivo zu; ohne geladene Schrift bleibt es schlicht normal breit. */
.h-cond {
  font-stretch: 80%;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  line-height: 1.06;
  margin: 0;
}

.eyebrow {
  margin: 0 0 0.4rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brand);
}

/* ---------- Kopfzeile ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--color-bg-sunken);
  border-bottom: 1px solid var(--color-border);
}

.site-header .wrap {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: 60px;
}

.brandmark {
  display: inline-flex;
  align-items: baseline;
  gap: 0.05rem;
  margin-right: auto;
  font-stretch: 80%;
  font-weight: 700;
  font-size: 1.1rem;
  letter-spacing: -0.01em;
  text-decoration: none;
  color: var(--color-text);
  border-bottom: 2px solid transparent;
  padding-bottom: 2px;
}

.brandmark .dot { color: var(--brand); }
.brandmark .sub { font-weight: 400; color: var(--color-text-muted); }
.brandmark:hover { border-bottom-color: var(--brand); }

/* ---------- Breitenumschalter (Menü rechts in der Kopfzeile) ---------- */

.width-menu {
  position: relative;
  /* Wie beim Verlaufsmenü: Das Panel hängt sich absolut an diesen Container, der selbst nur so
     groß ist wie sein Knopf. */
  display: inline-flex;
}

/* Form vollständig wie .history-toggle und .faq-btn daneben — die drei sollen als eine Reihe
   gleichartiger Knöpfe gelesen werden, nicht als drei verschiedene Bauteile. */
.width-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.45rem 0.7rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: 700;
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.width-toggle:hover,
.width-toggle[aria-expanded="true"] {
  background: var(--color-surface-hover);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.width-toggle:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Die Textspalte im Knopfsymbol bleibt immer orange: Sie ist hier keine Auswahlmarkierung wie in
   der Liste, sondern zeigt die eingestellte Stufe — und die soll man im Vorbeigehen sehen. */
.width-toggle .width-icon-fill {
  fill: var(--brand);
}

.width-toggle-caret {
  flex: none;
  width: 0;
  height: 0;
  border-left: 0.3rem solid transparent;
  border-right: 0.3rem solid transparent;
  border-top: 0.36rem solid currentColor;
  transition: transform 0.12s ease;
}

.width-toggle[aria-expanded="true"] .width-toggle-caret {
  transform: rotate(180deg);
}

/* Hängt unter dem Knopf an dessen rechter Kante — dieselbe Machart wie das Verlaufsmenü. */
.width-panel {
  position: absolute;
  top: calc(100% + 0.45rem);
  right: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  min-width: 100%;
  width: max-content;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  box-shadow: 0 18px 40px hsl(0 0% 0% / 0.45);
}

.width-btn {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.75rem;
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: 0.82rem;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}

/* Trägt den Namen der Stufe und schiebt den Pixelwert an das rechte Ende, damit die vier Werte
   untereinander eine Spalte bilden. */
.width-btn-name {
  flex: 1 1 auto;
}

.width-btn-hint {
  flex: none;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--color-text-faint);
}

/* Das Piktogramm: der Rahmen ist das Fenster, die Fläche darin die Textspalte — die vier Stufen
   unterscheiden sich allein in deren Breite. Beides erbt die Buttonfarbe, damit Hover und
   Aktiv-Zustand ohne eigene Regeln durchschlagen. */
.width-icon {
  display: block;
  width: 20px;
  height: 14px;
}

/* In der Kopfzeile dieselbe Symbolhöhe wie bei FAQ und Verlauf reservieren, damit alle drei
   Knöpfe trotz des flacheren Breiten-Piktogramms exakt gleich hoch sind. */
.width-toggle .width-icon {
  height: 16px;
}

/* Bewusst blass: der Rahmen ist nur der Bezugspunkt. Kräftiger gezeichnet läse sich das
   Symbol als "Kasten im Kasten" statt als Spalte in einem Fenster. */
.width-icon-frame {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  opacity: 0.3;
}

.width-icon-fill {
  fill: currentColor;
  transition: fill 0.15s ease;
}

.width-btn:hover:not([aria-pressed="true"]) {
  color: var(--color-text);
  background: var(--color-surface-hover);
}

/* Die gewählte Stufe: Text und Piktogramm in Orange. Eine erhöhte Fläche wie früher in der
   Segmentleiste braucht es hier nicht — in einer untereinander stehenden Liste genügt die
   Farbe, und der Kasten säße als einziger Eintrag mit Hintergrund unruhig in der Reihe. */
.width-btn[aria-pressed="true"] {
  color: var(--brand);
}

.width-btn[aria-pressed="true"] .width-icon-fill {
  fill: var(--brand);
}

.width-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

/* Unterhalb von 1000px verschwindet die Umschaltung: Von den vier Stufen bleiben dort
   höchstens zwei unterscheidbare übrig (760px gegenüber "so breit wie das Fenster"), weil
   schon "standard" mit 980px die Fensterbreite erreicht. Der Schwellwert deckt zugleich jedes
   Handy im Querformat ab — die reichen bis etwa 930px (iPhone Pro Max) — sowie Tablets im
   Hochformat. Bewusst nur nach der Breite gefragt und nicht zusätzlich nach der Zeigerart:
   ein schmal gezogenes Desktop-Fenster hat dasselbe Platzproblem, und die Abfrage kann je
   nach Gerät und Emulation anders ausfallen als erwartet.

   Dazu muss auch die gemerkte Stufe fallen: Ohne Schalter käme man aus einem aus einer
   Desktop-Sitzung übernommenen "compact" nicht mehr heraus und läse den Inhalt auf dem Handy
   in einer 760px-Spalte mit Rändern. Ebenso der breitere Innenabstand von "full" — sonst
   sähe dieselbe Seite auf demselben Gerät je nach unsichtbarer Alt-Einstellung anders aus.
   Für Fenster bis 560px setzt die Media Query am Dateiende beide Werte danach noch einmal
   herunter; sie steht später in der Datei und gewinnt bei gleicher Spezifität. */
@media (max-width: 1000px) {
  .width-menu { display: none; }
  :root[data-width] { --content-width: 100%; }
  :root[data-width="full"] { --gutter: 1.5rem; }
}

/* ---------- Installieren (Knopf in der Kopfzeile) ---------- */

/* Maße und Rahmen wie beim Verlaufsknopf daneben — die Kopfzeile soll nicht in drei
   verschiedenen Knopfformen auseinanderfallen. Einziger Unterschied ist die Farbe: Text und
   Rand in Orange, weil dieser Knopf einmalig etwas anbietet, statt eine Funktion der Seite zu
   sein. Eine gefüllte Fläche wäre an dieser Stelle zu laut; sie gehört dem "Suchen"-Knopf. */
.install-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.45rem 0.7rem;
  background: transparent;
  border: 1px solid var(--brand);
  color: var(--brand);
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: 700;
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}

.install-btn:hover {
  background: var(--brand);
  color: var(--color-text-on-brand);
}

.install-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.install-icon {
  display: block;
  flex: none;
}

/* Die Anleitung für iOS: kurzer Text, deshalb ein schmales Panel. */
.install-modal {
  max-width: 30rem;
}

.install-modal-title {
  font-stretch: 82%;
  font-weight: 700;
  font-size: 0.95rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.install-modal-body {
  font-size: 0.9rem;
  line-height: 1.55;
}

.install-modal-intro {
  margin: 0 0 0.9rem;
  color: var(--color-text-muted);
}

.install-steps {
  margin: 0;
  padding-left: 1.4rem;
  display: grid;
  gap: 0.5rem;
}

.install-steps strong {
  color: var(--color-text);
  font-weight: 700;
}

.install-modal-note {
  margin: 1.1rem 0 0;
  padding-top: 0.9rem;
  border-top: 1px solid var(--color-border);
  font-size: 0.82rem;
  color: var(--color-text-faint);
}

/* ---------- FAQ (Knopf in der Kopfzeile) ---------- */

/* Maße wie .history-toggle daneben, neutrale Farbe wie dort: Die FAQ ist eine dauerhafte
   Funktion der Seite, kein einmaliges Angebot wie "Installieren". */
.faq-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.45rem 0.7rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: 700;
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.faq-btn:hover {
  background: var(--color-surface-hover);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.faq-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.faq-icon {
  display: block;
  flex: none;
}

/* Ab hier wird die Kopfzeile eng: Wie beim Verlauf trägt das Symbol dann allein. Der Knopf
   bleibt sichtbar — am Handy ist die Installation der eigentliche Gewinn (Symbol auf dem
   Homescreen statt Lesezeichen). */
@media (max-width: 860px) {
  .install-btn-label { display: none; }

  .install-btn {
    padding: 0.45rem 0.55rem;
  }
}

/* ---------- Verlauf (Menü rechts in der Kopfzeile) ---------- */

.history-menu {
  position: relative;
  /* Ohne die Grundlinie der Kopfzeile zu verlassen: Das Panel hängt sich absolut an diesen
     Container, der selbst nur so groß ist wie sein Knopf. */
  display: inline-flex;
}

/* Rahmen und Höhe wie beim Breitenumschalter daneben, damit beide als ein Paar gelesen werden. */
.history-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.45rem 0.7rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: 700;
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.history-toggle:hover,
.history-toggle[aria-expanded="true"] {
  background: var(--color-surface-hover);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.history-toggle[aria-expanded="true"] .history-icon {
  color: var(--brand);
}

.history-toggle:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.history-icon {
  display: block;
  flex: none;
}

/* Hängt unter dem Knopf an der rechten Kante. Die Breite folgt dem Fenster, damit das Panel auf
   dem Handy nicht seitlich hinausragt — dort ist es dann fast so breit wie der Bildschirm. */
.history-panel {
  position: absolute;
  top: calc(100% + 0.45rem);
  right: 0;
  z-index: 60;
  width: min(380px, calc(100vw - 2 * var(--gutter)));
  display: flex;
  flex-direction: column;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  box-shadow: 0 18px 40px hsl(0 0% 0% / 0.45);
}

.history-head,
.history-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
}

.history-head {
  border-bottom: 1px solid var(--color-border);
}

.history-foot {
  border-top: 1px solid var(--color-border);
}

.history-title {
  margin: 0;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--brand);
}

/* Beide Fuß-Knöpfe teilen sich die Form der Nebenknöpfe der App (.btn-utility), nur schmaler:
   Sie sitzen in einem 380px breiten Panel und sollen die Einträge darüber nicht überstrahlen. */
.history-foot-btn {
  padding: 0.35rem 0.6rem;
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: 0.68rem;
  font-weight: 700;
  font-stretch: 88%;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.history-foot-btn:hover:not(:disabled) {
  background: var(--color-surface-hover);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

/* Abgeschaltete Aufzeichnung ist ein Zustand, den man sehen muss — sonst wundert man sich
   später, warum nichts mehr dazukommt. */
#history-record-btn[aria-pressed="false"] {
  color: var(--color-warning);
  border-color: var(--color-warning-line);
}

/* Erst beim Zeigen rot: Löschen ist unwiderruflich, soll aber im Ruhezustand nicht lauter
   sein als der Schalter daneben. */
.history-clear-btn:hover:not(:disabled) {
  background: var(--color-danger-surface);
  border-color: var(--color-danger);
  color: var(--color-danger);
}

.history-clear-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.history-foot-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

/* Etwa acht Einträge sind sichtbar, der Rest scrollt — ein Panel, das bis zum Fußende der Seite
   reicht, ließe sich am Handy nicht mehr schließen, ohne vorher zurückzuscrollen. */
.history-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: min(52vh, 420px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.history-item {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--color-border);
}

.history-item:last-child {
  border-bottom: none;
}

/* Angeheftete Einträge tragen links eine orange Kante — dieselbe Farbe wie die aktive Nadel
   rechts, damit auf einen Blick klar ist, warum dieser Eintrag oben steht. */
.history-item.is-pinned {
  border-left: 2px solid var(--brand);
}

.history-open {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.5rem 0.4rem 0.5rem 0.75rem;
  background: transparent;
  border: none;
  color: inherit;
  font-family: inherit;
  text-decoration: none;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.15s ease;
}

.history-open:hover {
  background: var(--color-surface-hover);
}

.history-open:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.history-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  min-width: 0;
}

/* Die Nummer ist das, wonach man sucht — Schreibmaschinenschrift wie überall sonst, wo im Tool
   eine Teile- oder Seriennummer steht. */
.history-q {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--color-text);
}

.history-time {
  flex: none;
  font-size: 0.68rem;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
}

.history-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

.history-actions {
  display: flex;
  align-items: center;
  flex: none;
  padding-right: 0.35rem;
}

.history-pin,
.history-remove {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border: none;
  color: var(--color-text-faint);
  font-family: inherit;
  cursor: pointer;
  transition: color 0.15s ease;
}

.history-remove {
  font-size: 1.15rem;
  line-height: 1;
}

.history-pin:hover,
.history-remove:hover {
  color: var(--color-text);
}

.history-remove:hover {
  color: var(--color-danger);
}

.history-pin[aria-pressed="true"] {
  color: var(--brand);
}

.history-pin:focus-visible,
.history-remove:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.history-empty {
  margin: 0;
  padding: 1.25rem 0.75rem;
  text-align: center;
  font-size: 0.78rem;
  color: var(--color-text-faint);
}

/* Ab hier wird die Kopfzeile eng — wie beim Breitenumschalter fällt das Wort weg und das
   Symbol trägt allein. Ausgeblendet wird das Menü dabei nie: Gerade auf dem Handy erspart der
   Verlauf das Abtippen einer zehnstelligen Seriennummer. */
@media (max-width: 860px) {
  .history-toggle-label { display: none; }

  .history-toggle {
    padding: 0.45rem 0.55rem;
  }
}

/* ---------- Inhalt ---------- */

/* z-index 2 statt 1: über dem Lichtschein (body::before, z-index 0) — und über der Fußzeile,
   die als Geschwisterelement ebenfalls auf 1 steht und deshalb bisher gewonnen hat. Im normalen
   Fluss überlappen sich beide nie, weshalb es lange nicht auffiel; ein aufgeklapptes Menü am
   unteren Rand des Inhalts (die Kategorienliste in der Filterleiste) verschwand aber dahinter.
   Foto-Modal, Scrollbalken und Nach-oben-Knopf stehen außerhalb von <main> und bleiben von der
   Änderung unberührt. */
/* Spalte statt Block, damit ein einzelnes Kind den verbleibenden Platz bis zur Fußzeile nehmen
   kann (`flex: 1`) — genutzt vom Leerzustand und von der Ladeanzeige, die beide dort in der Mitte
   stehen sollen statt oben am Suchfeld zu kleben. Alle Kinder von <main> arbeiten ausschließlich
   mit `margin-bottom` (--space-block), es fällt also kein zusammenfallender Rand weg. */
.container {
  position: relative;
  z-index: 2;
  flex: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: var(--content-width);
  margin: 0 auto;
  padding: 2.25rem var(--gutter) 3rem;
  /* Breitenwechsel als weiche Bewegung statt als harter Sprung. */
  transition: max-width 0.2s ease;
}

.page-head {
  margin-bottom: 1.75rem;
}

h1 {
  font-size: clamp(1.6rem, 1.2rem + 1.6vw, 2.35rem);
  margin: 0;
}

/* Der Titel führt zurück auf die Startansicht des Tools. Als Hover-Signal dient eine
   orange Unterstreichung über text-decoration statt über border-bottom: sie beeinflusst
   kein Layout und bricht bei schmalen Fenstern sauber über beide Zeilen mit. Die
   Wortfarben bleiben dabei unangetastet, sonst würde der Titel beim Hover seine
   Markenfarbe verlieren. */
.page-title-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: 3px;
  text-underline-offset: 0.16em;
  transition: text-decoration-color 0.15s ease;
}

.page-title-link:hover,
.page-title-link:focus-visible {
  text-decoration-color: var(--brand);
  outline: none;
}

/* Zweiter Teil des Titels in Signalorange — dasselbe Muster wie im Hero der Landingpage
   ("Werkzeuge von Technikern *für Techniker*"). */
.page-title-accent {
  font-style: normal;
  color: var(--brand);
}

/* ---------- Buttons ---------- */

/* Gemeinsame Button-Basis — alle Buttons der App teilen sich Größe und Verhalten,
   nur Farbe/Gewicht unterscheiden sich je nach Rolle (primary / secondary / utility). */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.6rem 1.25rem;
  font-size: 0.82rem;
  font-weight: 700;
  font-family: inherit;
  font-stretch: 88%;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border: 1px solid transparent;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.btn:active {
  transform: translateY(1px);
}

.btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.btn-primary {
  background: var(--brand);
  color: var(--color-text-on-brand);
}

.btn-primary:hover {
  background: var(--brand-hover);
  color: #fff;
}

.btn-secondary {
  background: transparent;
  color: var(--color-text);
  border-color: var(--color-border-strong);
}

.btn-secondary:hover {
  border-color: var(--color-text);
  background: hsl(0 0% 100% / 0.08);
}

.btn-utility {
  background: transparent;
  color: var(--color-text-muted);
  border-color: var(--color-border);
  font-size: 0.74rem;
  padding: 0.45rem 0.85rem;
}

.btn-utility:hover {
  border-color: var(--brand);
  color: var(--brand);
}

/* ---------- Suche und Status ---------- */

.search-form {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-block);
}

/* Umschließt Eingabe und alle Aktionen, die optisch zu ihr gehören. Der reguläre Suchen-Knopf
   bleibt außerhalb: Er schickt eine selbst eingegebene Nummer ab; der Clipboard-Knopf im Feld
   befüllt es dagegen und sucht unmittelbar danach. */
.search-input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
}

.search-form input {
  flex: 1;
  min-width: 0;
  min-height: var(--control-size);
  padding: var(--space-2) 5.5rem var(--space-2) var(--space-3);
  font-family: inherit;
  font-size: 1rem;
  color: var(--color-text);
  border: 1px solid var(--color-border-strong);
  background: var(--color-bg-sunken);
}

.search-form input::placeholder {
  color: var(--color-text-faint);
}

.search-form input:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-faint);
}

/* Hilfe und × teilen sich denselben Platz vor den beiden Aktionssegmenten. Dadurch springt nichts,
   wenn das erste Zeichen eingegeben wird und der Hilfe- durch den Löschen-Knopf ersetzt wird. */
.search-help-btn,
.search-clear-btn {
  position: absolute;
  top: 0;
  right: var(--control-size);
  bottom: 0;
  width: var(--control-size);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--color-text-faint);
  line-height: 1;
  cursor: pointer;
}

.search-clear-btn {
  font-size: 1.35rem;
}

.search-help-btn:hover,
.search-clear-btn:hover {
  color: var(--brand);
}

.search-help-btn:focus-visible,
.search-clear-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

/* Kamera und Zwischenablage bilden am rechten Feldrand zwei gleich große Symbolsegmente. Der
   Scanner bleibt nur so lange verborgen, bis JavaScript bestätigt hat, dass dieser Browser
   Kamerazugriff anbietet; die Fensterbreite spielt dabei bewusst keine Rolle. */
.search-scan-btn,
.search-paste-btn {
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--control-size);
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}

.search-scan-btn {
  right: var(--control-size);
  display: none;
}

.search-paste-btn {
  right: 0;
  display: flex;
}

/* Auf breiten Ansichten reichen die vertrauten Symbole im Eingabefeld. Die ausgeschriebenen
   Bezeichnungen werden erst in der eigenen mobilen Aktionszeile eingeblendet. */
.search-action-label {
  display: none;
}

.search-input-wrap.has-scanner .search-scan-btn {
  display: flex;
}

.search-input-wrap.has-scanner input {
  padding-right: 8.25rem;
}

.search-input-wrap.has-scanner .search-help-btn,
.search-input-wrap.has-scanner .search-clear-btn {
  right: 5.5rem;
}

/* Nur mit Zeiger: Ohne die Abfrage bliebe die Einfärbung nach einem Fingertipp stehen, weil
   mobile Browser den Hover-Zustand bis zum nächsten Tipp woandershin festhalten. Genau das war
   nach dem Scannen und nach "Einfügen" zu sehen — der Knopf blieb orange, obwohl der Finger
   längst weg war. */
@media (hover: hover) {
  .search-scan-btn:hover,
  .search-paste-btn:hover {
    color: var(--brand);
    background: var(--brand-faint);
  }
}

/* Ersatz ohne Zeiger: eine Rückmeldung im Moment des Tippens, die von selbst wieder verschwindet. */
@media (hover: none) {
  .search-scan-btn:active,
  .search-paste-btn:active {
    color: var(--brand);
    background: var(--brand-faint);
  }
}

.search-scan-btn:focus-visible,
.search-paste-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.search-scan-btn:disabled,
.search-paste-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* setSearchBusy() sperrt während einer laufenden Suche alle Knöpfe im Formular — die Aktionen
   im Textfeld sollen dann auch so aussehen, statt stumm auf Klicks zu schweigen. */
.search-help-btn:disabled,
.search-clear-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.search-form > .btn {
  min-height: var(--control-size);
}

.status {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  min-height: 1.3rem;
  margin-bottom: var(--space-block);
  font-size: 0.9rem;
  font-weight: 500;
}

.status.loading { color: var(--color-text-muted); }
.status.error { color: var(--color-danger); }
.status.success { color: var(--color-success); }

/* Solange eine Anfrage läuft, wird die Statuszeile zur Ladeanzeige: zentriert und größer,
   auf derselben Mittelachse und an derselben Stelle, an der gleich die Ergebnisse erscheinen.

   Weniger Innenabstand als beim früheren runden Throbber: Die Animation blendet an ihren vier
   Rändern selbst aus und bringt damit ihren Rand schon mit — 2.5rem obendrauf ließen sie in der
   Seite schwimmen.

   `flex: 1` nimmt den Platz bis zur Fußzeile ein (siehe .container) und `justify-content: center`
   stellt die Animation darin in die Mitte. Sonst hinge sie dicht unter dem Suchfeld, mit einer
   halben Bildschirmhöhe Leere darunter — dieselbe Achse, auf der auch der Leerzustand und später
   die Ergebnisse liegen. Wenn eine Anfrage läuft, ist alles darunter ausgeblendet; die Regel
   kommt sich also mit nichts in die Quere. */
.status[aria-busy="true"] {
  flex: 1;
  flex-direction: column;
  justify-content: center;
  gap: 0.4rem;
  padding: 1rem 1rem 3rem;
  font-size: 1.05rem;
  text-align: center;
}

/* Ladeanimation über dem Statustext — wird nur eingefügt, solange tatsächlich eine Anfrage läuft
   (setStatus(..., { busy: true })), nicht bei rein informativen "loading"-Meldungen. Gezeichnet
   wird in js/ladeanimation.js; hier steht nur, wie viel Platz sie bekommt.

   Die Höhe ist fest und die Breite gedeckelt: Das Canvas rechnet in Bildpunkten (die vier Zeilen
   liegen auf ±19 und ±40 um die Mitte), eine mitwachsende Höhe würde sie auseinanderziehen. */
.status-animation {
  flex: none;
  display: block;
  width: 100%;
  max-width: 560px;
  height: 120px;
}

.search-form .btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ---------- Leerzustand (noch keine Suche gelaufen) ---------- */

/* Füllt den sonst leeren Content unterhalb des Suchfelds mit einem Tipp, solange noch nichts
   gesucht wurde. flex: 1 auf .container schiebt das Element über den verbleibenden Platz nach
   unten in die Mitte (siehe justify-content unten) — dieselbe Achse, auf der später die
   Ergebnisse erscheinen. app.js blendet den Block aus, sobald performSearch() startet, und
   zeigt ihn danach nicht wieder. */
.empty-state {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 2.5rem 1rem 4rem;
  text-align: center;
  color: var(--color-text-muted);
}

.empty-state-icon {
  color: var(--color-border-strong);
  margin-bottom: 0.4rem;
}

.empty-state .eyebrow {
  margin-bottom: 0.15rem;
}

.empty-state-tip {
  margin: 0;
  max-width: 34rem;
  font-size: 1.2rem;
  line-height: 1.5;
}

/* Markiert den Text ausdrücklich als Tipp und nicht als Statusmeldung — sonst liest sich ein
   Satz ohne Kontext direkt unter der Lupe schnell wie eine Fehlermeldung oder ein Hinweis zur
   laufenden Suche. Kein Doppelpunkt im Markup, damit ein Screenreader "Tipp" und Satz nicht
   als ein zusammenhängendes Wort vorliest — stattdessen per ::after nur optisch angehängt. */
.empty-state-tip-label {
  color: var(--brand);
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.empty-state-tip-label::after {
  content: ":";
  margin-right: 0.35em;
}

/* ---------- Trefferkopf ---------- */

/* Karte über dem Ergebnispanel mit Modellname und den beiden Nummern.
   Der Modellname steht bewusst als Überschrift — er ist die Kontrolle, ob wirklich das
   erwartete Gerät gefunden wurde (siehe showResultSummary in app.js). Der linke Balken ist
   Zustands- und nicht Markenfarbe: grün bei sauberem Treffer, rot sobald etwas nicht passt.

   Zwei Spalten statt drei Stockwerke: Name links, Nummern rechts. Gestapelt (Name, Trennlinie,
   Angaben) war der Kopf fast so hoch wie der halbe erste Bildschirm Tabelle darunter, obwohl er
   nur vier kurze Werte trägt. Nebeneinander fällt auch die interne Trennlinie weg — die
   Spalten trennen sich schon durch den Zwischenraum zwischen ihnen. */
.result-summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  column-gap: 2rem;
  row-gap: 0.85rem;
  margin-bottom: var(--space-block);
  padding: 1rem 1.1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-success);
}

.result-summary.has-warning {
  border-left-color: var(--color-danger);
}

/* Eine Teilenummer ist kein Modell: Im kompakten Einzelteilkopf steht sie direkt unter der
   orangenen Bezeichnung. Die rechte Spalte behält nur noch den Herkunftshinweis; Suchbegriff und
   die in diesem Fall nicht vorhandene Modellnummer verschwinden. */
.result-summary.is-part-result .result-summary-facts {
  display: none;
}

.result-summary.is-part-result .result-summary-name {
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
}

/* Eyebrow und Modellname als eine Einheit in der linken Spalte — ohne diesen Kasten säßen sie
   als zwei eigene Rasterzellen untereinander und die Nummern rechts stünden neben dem Eyebrow
   statt neben dem Namen. */
.result-summary-main {
  min-width: 0;
}

/* Kopfzeile des Trefferkopfs: nur noch "Gefundenes Modell". Das Herkunfts-Abzeichen sitzt
   rechts oben in .result-summary-side. */
.result-summary-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.result-summary-head .eyebrow {
  margin-bottom: 0;
}

/* Rechte Spalte: Herkunfts-Abzeichen oben, darunter die beiden Nummern. Rechtsbündig, damit das
   Abzeichen an der rechten Kante der Karte sitzt — es ist je nach Quelle und Datenalter
   unterschiedlich breit und stünde linksbündig bei jeder Suche woanders. */
.result-summary-side {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.6rem;
  min-width: 0;
}

/* Woher die Auskunft kam. Text und Icon tragen dieselbe Zustandsfarbe: Blau frisch von HP, Grün
   aus dem Server-Cache, Bernstein bei veraltetem Stand. Ohne Kasten bleibt die Angabe eine
   unaufdringliche Zusatzinformation; das Icon steht als fester Abschluss immer ganz rechts. */
.summary-cache {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.4rem;
  padding: 0;
  color: var(--color-info);
  background: none;
  border: 0;
  font-family: inherit;
  font-size: 0.76rem;
  font-weight: 600;
  line-height: 1.25;
  text-align: left;
  white-space: nowrap;
  cursor: default;
}

.summary-cache.is-cached {
  color: var(--color-success);
}

/* Der veraltete Stand ist der einzige Fall, in dem die Angabe warnen soll: Hier ist die
   Auskunft nicht von jetzt, und der Satz darunter erklärt warum. */
.summary-cache.is-stale {
  color: var(--color-warning);
}

.summary-cache svg {
  flex: none;
  width: 1.1rem;
  height: 1.1rem;
  stroke-width: 1.8;
}

.summary-cache:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

/* Gemeinsamer Anker für Icon und Toast. Der Toast ist absolut zu diesem kleinen Kasten
   positioniert und kann deshalb erscheinen, ohne das Raster des Trefferkopfs zu verändern. */
.summary-source {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  justify-content: flex-end;
}

/* Nur sichtbar, solange die Angabe links tatsächlich aus dem Cache kommt (frisch oder veraltet) —
   siehe showCacheBadge() in js/trefferkopf.js. Der Klick selbst ist in .part-search-icon-btn
   verlinkt: dieselbe Rückfrage-Sprechblase (.is-confirming::after weiter unten), derselbe Ablauf
   wie bei der Lupe für die Teilenummer-Suche in der Tabelle. */
.summary-cache-reload-btn {
  appearance: none;
  display: inline-flex;
  position: relative;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 1.8rem;
  height: 1.8rem;
  padding: 0;
  color: var(--color-info);
  background: none;
  border: 1px solid transparent;
  border-radius: 999px;
  cursor: pointer;
}

.summary-cache-reload-btn:hover {
  background: var(--color-info-faint);
  border-color: var(--color-info-line);
}

.summary-cache-reload-btn:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

/* Der mobile Herkunftstext steht direkt oberhalb des Icons, rechts an ihm ausgerichtet und wächst
   nach links. So bleibt der Hinweis beim Auslöser, ohne den Trefferkopf zu verschieben. */
.summary-source-toast {
  position: absolute;
  right: 0;
  bottom: calc(100% + 0.45rem);
  z-index: 100;
  max-width: calc(100vw - 2rem);
  width: max-content;
  padding: 0.55rem 0.75rem;
  color: var(--color-info);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  box-shadow: 0 0.5rem 1.5rem hsl(0 0% 0% / 0.35);
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  animation: summarySourceToastIn 0.18s ease-out;
}

.summary-source-toast.is-cached {
  color: var(--color-success);
}

.summary-source-toast.is-stale {
  color: var(--color-warning);
}

@keyframes summarySourceToastIn {
  from {
    opacity: 0;
    transform: translateY(0.3rem);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.result-summary-names {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.15rem;
  min-width: 0;
  /* Der Innenabstand der kopierbaren Felder wird per negativem Rand wieder hereingeholt, damit
     Name und Eyebrow auf einer Kante stehen (siehe .summary-copyable). Bei mehrzeiligen Namen
     bräuchte der Kasten sonst rechts Platz, den die Spalte nicht hat. */
  padding-right: 0.35rem;
}

/* Ohne align-items: flex-start wären Name und Kurzform als Flex-Elemente auf die volle
   Spaltenbreite gedehnt und der Hover-Hintergrund liefe bis zum rechten Rand der Karte, statt
   den Text zu umschließen. Bei einem umbrechenden Namen bleibt der Kasten dann ein Block über
   beide Zeilen — das ist gewollt, kopiert wird ja auch der ganze Name. */
.result-summary-names .summary-copyable {
  max-width: 100%;
}

.result-summary-name {
  font-stretch: 86%;
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1.25;
}

/* Kein echter Modellname, sondern der Hinweis, dass HP keinen geliefert hat — nichts zum
   Kopieren, also auch kein Zeiger und keine Hover-Einfärbung (siehe summaryCopyTarget). */
.result-summary-name.is-unknown {
  color: var(--color-danger);
  cursor: default;
}

.result-summary-name.is-unknown:hover {
  background: none;
  color: var(--color-danger);
}

/* HPs Kurzschreibweise desselben Modells — nur als Beleg, deshalb klein und gedämpft. */
.result-summary-short {
  font-size: 0.82rem;
  color: var(--color-text-muted);
}

/* Die beiden Nummern in der rechten Spalte. Die Werte sind dicktengleich gesetzt und stehen
   innerhalb ihrer Angabe linksbündig, damit man sie mit dem Auge vergleichen kann — der Block
   als Ganzes richtet sich über .result-summary-side an der rechten Kante aus. */
.result-summary-facts {
  display: flex;
  gap: 0.5rem 2rem;
  margin: 0;
}

.summary-fact {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}

.summary-fact dt {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-text-faint);
}

.summary-fact dd {
  margin: 0;
}

.summary-value {
  font-size: 0.98rem;
  font-weight: 600;
}

/* Serien- und Modellnummer sind Codes, die abgetippt/verglichen werden — dafür ist eine
   dicktengleiche Schrift deutlich besser lesbar (0/O, 1/l). */
.summary-value-code {
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
}

.summary-copyable {
  cursor: pointer;
  /* Der eingefärbte Hover-Bereich soll den Text umschließen, nicht die ganze Zeilenbreite. */
  display: inline-block;
  padding: 0.1rem 0.35rem;
  margin-left: -0.35rem;
}

.summary-copyable:hover {
  background: var(--brand-faint);
  color: var(--brand);
}

/* Fehlt der Wert in der HP-Antwort, steht dort ein Platzhalter — der ist nicht kopierbar. */
.summary-value.is-empty {
  color: var(--color-text-faint);
  font-weight: 500;
  cursor: default;
}

.summary-value.is-empty:hover {
  background: none;
  color: var(--color-text-faint);
}

/* Läuft über beide Spalten: Die Warnung gilt dem Treffer als Ganzem, nicht dem Namen oder den
   Nummern allein. Die Trennlinie darüber bleibt — anders als die frühere zwischen Name und
   Angaben trennt sie hier zwei verschiedene Aussagen und nicht nur zwei Zeilen desselben. */
.result-summary-warning {
  grid-column: 1 / -1;
  margin: 0;
  padding-top: 0.75rem;
  border-top: 1px solid var(--color-border);
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--color-danger);
}

/* Reine Einordnung statt Warnung (z.B. "gesucht wurde eine Produktnummer"): gleicher Platz,
   aber unaufgeregt — sonst stumpft die rote Warnung ab, auf die es wirklich ankommt. */
.result-summary-warning.is-note {
  color: var(--color-text-faint);
}

/* ---------- Modellauswahl ---------- */

/* Erscheint nur bei Seriennummern, die HP nicht eindeutig zuordnen kann. */
.product-select {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-bottom: var(--space-block);
  padding: 0.9rem 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--brand);
}

.product-select-label {
  font-size: 0.9rem;
  font-weight: 500;
}

/* Bei einer Wildcard-Suche ("roller") liefert HP schnell mehrere hundert Produkte. Die Liste
   wird serverseitig gekappt — damit sie trotzdem nicht die halbe Seite einnimmt, scrollt sie
   ab einer gewissen Höhe in sich. */
.product-select-options {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-height: 22rem;
  overflow-y: auto;
}

/* Die Beschriftung ist "Produktnummer – Modellname" und damit deutlich länger als bei einem
   gewöhnlichen Button. Das nowrap aus .btn würde den Button auf schmalen Geräten über den
   Viewport hinausschieben — hier darf umbrochen werden. overflow-wrap greift für den Fall,
   dass schon ein einzelner Modellname länger als die Zeile ist. */
.product-select-option {
  justify-content: flex-start;
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
  line-height: 1.35;
}

.product-select-note {
  font-size: 0.8rem;
  color: var(--color-text-faint);
}

/* ---------- Ergebnisbereich ---------- */

/* Reiner Gruppierungskasten ohne eigene Optik: app.js blendet über ihn den ganzen Bereich
   ein und aus, statt jedes Teil einzeln zu schalten. Bewusst kein display: contents — als
   gewöhnlicher Block verhält er sich in jedem Browser wie die frühere lose Folge von
   Geschwisterelementen, und die Abstände darin stimmen ohne Sonderfall. */
.results-panel {
  display: block;
}

/* Steuerung des Ergebnisses: Reiterleiste und Filterzeile teilen sich einen Rahmen. Vorher
   waren es zwei getrennte Kacheln mit gleicher Fläche, gleichem Rahmen und gleichem Abstand
   dazwischen — der Abstand behauptete eine Trennung, die es inhaltlich nicht gibt: Der Reiter
   wählt die Liste, der Filter grenzt genau diese Liste ein.

   Was dabei herauskommt, bleibt bewusst außerhalb: Zwischen dieser Steuerung und der Tabelle
   steht weiterhin ein Abstand. Er trennt die beiden Dinge, die wirklich verschieden sind —
   Bedienung oben, Ergebnis darunter.

   Sie klebt beim Scrollen unter der Kopfzeile fest. Bei achthundert Ersatzteilen war der Weg
   zurück zum Filterfeld sonst ein kompletter Bildlauf nach oben. Das "top:" nimmt die gemessene
   Höhe der Kopfzeile (js/klebekopf.js setzt --kopfhoehe): Sie steht schon in dieser Datei, und
   ein zweiter, von Hand gepflegter Wert liefe spätestens dann daneben, wenn der
   Installieren-Knopf die Kopfzeile umbrechen lässt. Der Reservewert greift nur, falls das
   Skript gar nicht läuft.

   Der z-index sorgt dafür, dass später im Dokument stehender Inhalt nicht darüber rutscht. Zur
   Kopfzeile ändert er nichts: .container macht mit z-index: 2 ohnehin einen eigenen
   Stapelkontext auf, in dem alles unter deren 50 bleibt.

   --klebekopf-ausgleich ist der Höhenausgleich fürs Zusammenklappen in der Handy-Ansicht (siehe
   die Media Query am Dateiende); außerhalb davon steht die Variable auf 0.

   Die Fläche entspricht dem Trefferkopf darüber (--color-surface), damit beide Bereiche als
   zusammengehörige Ergebniskarten erscheinen. Filterfeld und Kategorie-Auswahl setzen sich mit
   der zuvor hier verwendeten dunkleren Fläche (--color-bg-sunken) davon ab. */
.results-head {
  position: sticky;
  top: var(--kopfhoehe, 60px);
  z-index: 30;
  margin-bottom: calc(var(--space-block) + var(--klebekopf-ausgleich, 0px));
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

/* Bei genau einem Ersatzteil gibt es keine alternativen Listen zu steuern. Reiter, Filter und
   ihr Sticky-Merker fallen gemeinsam weg; die einzelne Teilezeile beginnt direkt darunter. */
.results-panel.single-part-result .results-head,
.results-panel.single-part-result .klebekopf-marke {
  display: none;
}

/* Zum Messen des Höhenausgleichs setzt js/klebekopf.js den eingeklappten Zustand für einen
   Wimpernschlag und nimmt ihn sofort wieder zurück. Liefe dabei ein Übergang an, käme statt der
   Endhöhe eine Zwischenhöhe heraus — und die Steuerung zappelte sichtbar. */
.results-head.ohne-uebergang,
.results-head.ohne-uebergang * {
  transition: none !important;
}

/* Nullhoher Merker unmittelbar über der Steuerung (siehe index.html): Er bleibt an ihrer Stelle
   im Fluss stehen, während sie selbst oben klebt, und ist damit der einzige verlässliche
   Messpunkt für "wie weit hängt sie schon unter der Kopfzeile?". */
.klebekopf-marke {
  height: 0;
}

/* Der Knopf, der die eingeklappte Steuerung wieder hervorholt. Es gibt ihn nur in der
   Handy-Ansicht; hier steht lediglich, dass er sonst keinen Platz beansprucht. */
.results-head-toggle {
  display: none;
}

/* ---------- Filterleiste ---------- */

/* Untere Zeile der Steuerung: Filterfeld (mit der Trefferzahl darin) und Kategorie-Auswahl.
   Rahmen und Fläche kommen aus .results-head, hier bleibt die innere Aufteilung — und die Haarlinie zur
   Reiterleiste darüber. Die brauchte es zwischenzeitlich nicht, solange die Mulde nur unter den
   Reitern lag und ihre Kante die Trennung übernahm; jetzt liegen beide Zeilen auf derselben
   Fläche, und ohne Linie liefen sie zu einem Block zusammen. Im QR-Tab steht die Zeile auf
   .hidden — die Linie verschwindet dort also mit ihr, ganz ohne Sonderregel. */
.filter-wrapper {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--color-border);
}

/* Die Kategorie-Auswahl steckt in diesem Kasten, damit sie in der Handy-Ansicht wegklappen kann
   (siehe die Media Query am Dateiende). Überall sonst soll der Kasten nicht existieren:
   display: contents nimmt ihn aus dem Layout und lässt sein Kind direkt in der Flexbox der
   Filterzeile stehen. */
.filter-meta {
  display: contents;
}

/* Der sichtbare Kasten des Filterfelds. Rahmen und Fläche sitzen hier und nicht am <input>,
   weil im Feld drei Dinge nebeneinanderstehen: die Eingabe, die Trefferzahl und der ×-Knopf.
   Als Flexzeile teilen sie sich den Kasten sauber auf — mit einem gerahmten <input> müssten
   Zahl und Knopf darüber schweben (position: absolute) und das Feld bräuchte für beide eine
   von Hand geratene Polsterung rechts, die bei "117 / 117 angezeigt" nicht mehr reicht.

   Standardmäßig steht der Kasten auf --color-surface. Der Ergebnisfilter überschreibt das direkt
   darunter mit der dunkleren Fläche, die zuvor den ganzen Steuerungsblock gefüllt hat. */
.filter-input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  min-height: var(--control-size);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
}

/* Auf der jetzt gleichfarbigen Ergebniskarte bleibt „Suchergebnisse filtern“ mit dem bisherigen
   dunklen Steuerungsgrund klar als Eingabefeld erkennbar. Andere Verwendungen dieses Bausteins,
   etwa im Modell-Querverweis, behalten ihre dort passende Standardfläche. */
.filter-wrapper > .filter-input-wrap {
  background: var(--color-bg-sunken);
}

/* Der Fokusring gehört jetzt an den Kasten: Am <input> säße er mitten im Feld und ließe Zahl
   und ×-Knopf außerhalb stehen. */
.filter-input-wrap:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-faint);
}

.filter-wrapper input,
.models-filter input {
  /* Die Untergrenze steht der schrumpfenden Trefferzahl gegenüber (siehe .result-count): Ohne
     sie gäbe die Eingabe als flexibles Element zuerst nach und wäre bei langem Zählertext nur
     noch ein paar Pixel breit. */
  flex: 1;
  min-width: 4.5rem;
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
  font-family: inherit;
  color: var(--color-text);
  border: none;
  background: transparent;
}

.filter-wrapper input::placeholder,
.models-filter input::placeholder {
  color: var(--color-text-faint);
}

.filter-wrapper input:focus,
.models-filter input:focus {
  outline: none;
}

/* ×-Knopf im Filterfeld: löscht den Filter mit einem Klick, statt dass man ihn markieren
   und selbst löschen muss. Er sitzt im selben Slot wie der Hilfe-Knopf und ersetzt ihn, sobald
   etwas im Feld steht — deshalb dieselbe Breite und nichts springt beim ersten Zeichen. Hinter
   Scannen und Einfügen stünde er dagegen ohne Trennlinie am Rand des letzten Segments. */
.filter-clear-btn {
  flex: none;
  align-self: stretch;
  width: 2.1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--color-text-faint);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.filter-clear-btn:hover {
  color: var(--brand);
}

.filter-clear-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

/* Hilfe (?): dieselbe Optik wie der ×-Knopf, mit dem er sich den Platz teilt (siehe dort). */
.filter-help-btn {
  flex: none;
  align-self: stretch;
  width: 2.1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--color-text-faint);
  cursor: pointer;
}

.filter-help-btn:hover {
  color: var(--brand);
}

.filter-help-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

/* Scannen und Einfügen: eigene, etwas schmalere Fassung der Knöpfe aus dem Suchfeld oben
   (.search-scan-btn/.search-paste-btn) — hier stehen sie in einer flexen Zeile statt absolut
   positioniert, deshalb reicht eine einfache Trennlinie ohne eigene Breitenrechnung. */
.filter-scan-btn,
.filter-paste-btn {
  flex: none;
  align-self: stretch;
  /* Genau die Höhe des Feldes: Die beiden abgetrennten Segmente sind damit quadratisch. */
  width: var(--control-size);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-left: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}

/* Dieselbe Trennung wie bei den Knöpfen im Suchfeld oben (siehe .search-scan-btn:hover): mit
   Zeiger Hover, mit Finger ein kurzes :active — ein klebender Hover-Zustand sähe hier aus wie
   ein aktivierter Knopf. */
@media (hover: hover) {
  .filter-scan-btn:hover,
  .filter-paste-btn:hover {
    color: var(--brand);
    background: var(--brand-faint);
  }
}

@media (hover: none) {
  .filter-scan-btn:active,
  .filter-paste-btn:active {
    color: var(--brand);
    background: var(--brand-faint);
  }
}

.filter-scan-btn:focus-visible,
.filter-paste-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

/* Kategorie-Filter (Teileart) — ein selbstgebautes Listbox-Menü, kein <select>. Grund: Die
   aufgeklappte Liste eines <select> zeichnet das Betriebssystem, nicht der Browser; sie ließ
   sich als einziges Element der Seite nicht gestalten und sprang beim Aufklappen sichtbar aus
   dem Formsystem heraus. Der Nachbau kostet die Bedienlogik in app.js (Tastatur,
   Anfangsbuchstaben, Fingerbedienung), gibt dafür aber dieselbe Optik wie Filterfeld und
   Verlaufsmenü daneben.

   Die feste, responsive Breite verhindert, dass eine lange Beschriftung ("Transferband-Reinigung")
   das Filterfeld daneben zusammenschiebt. Auf breiten Ansichten reserviert die Auswahl ungefähr
   ein Viertel der Filterzeile; das Filterfeld nimmt den übrigen Platz. Das aufgeklappte Menü darf
   breiter werden als der Knopf und ist deshalb nicht daran gebunden. */
.category-select {
  position: relative;
  flex: 0 0 clamp(12rem, 25%, 20rem);
  max-width: 20rem;
  min-width: 0;
  display: flex;
}

/* Knopf und Einträge sind Bedienelemente, kein Text: Ein langer Fingerdruck soll sie nicht
   markieren, und der graublaue Kasten, den mobile Browser beim Antippen kurz darüberlegen, passt
   weder zur Farbe noch zur Kantigkeit des Rests. Die Rückmeldung beim Tippen kommt stattdessen
   aus :focus (siehe .category-select-option:focus weiter unten). */
.category-select-btn,
.category-select-option {
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}

/* Übernimmt Maße und Farben des früheren <select>, damit der geschlossene Zustand aussieht wie
   das Filterfeld links daneben. touch-action unterdrückt die Tippverzögerung, mit der mobile
   Browser sonst auf einen möglichen Doppeltipp warten. */
.category-select-btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* Nimmt die ganze Breite, gibt aber nach, wenn der ×-Knopf daneben auftaucht. */
  flex: 1 1 auto;
  width: 100%;
  min-width: 0;
  min-height: var(--control-size);
  padding: var(--space-2) var(--space-3);
  font-family: inherit;
  font-size: 0.85rem;
  text-align: left;
  color: var(--color-text);
  background: var(--color-bg-sunken);
  border: 1px solid var(--color-border);
  cursor: pointer;
  touch-action: manipulation;
}

/* Eine lange Beschriftung wird gekürzt statt den Knopf zu dehnen — die volle steht im Menü. */
.category-select-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Dreieck aus Rahmenkanten statt Zeichen oder SVG: Es erbt über currentColor die Farbe des
   Knopfes und wird damit im aktiven Zustand von allein orange. */
.category-select-caret {
  flex: none;
  width: 0;
  height: 0;
  border-left: 0.3rem solid transparent;
  border-right: 0.3rem solid transparent;
  border-top: 0.36rem solid currentColor;
  transition: transform 0.12s ease;
}

.category-select-btn[aria-expanded="true"] .category-select-caret {
  transform: rotate(180deg);
}

.category-select-btn:focus-visible,
.category-select-btn[aria-expanded="true"] {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-faint);
}

/* Eine gesetzte Auswahl blendet Zeilen aus, ohne dass man das dem Knopf ansähe — der Farbakzent
   macht sichtbar, dass hier noch etwas aktiv ist. Die Klasse setzt app.js beim Aufbau der Liste. */
.category-select-btn.is-active {
  color: var(--brand);
  border-color: var(--brand);
  background: var(--brand-faint);
}

.category-select-btn:disabled {
  color: var(--color-text-faint);
  cursor: default;
}

/* Der ×-Knopf sitzt bündig am Öffner und bildet mit ihm ein Bauteil: gemeinsame Kante (deshalb
   kein eigener linker Rahmen), gleiche Farben wie der aktive Öffner. Er ist nur zu sehen, solange
   eine Kategorie gewählt ist — die Zeile wird also im Normalfall nicht enger. */
.category-select-clear {
  flex: none;
  width: var(--control-size);
  min-height: var(--control-size);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-size: 1.1rem;
  line-height: 1;
  color: var(--brand);
  background: var(--brand-faint);
  border: 1px solid var(--brand);
  border-left: none;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}

.category-select-clear:hover {
  background: var(--brand);
  color: #fff;
}

.category-select-clear:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.category-select-btn:disabled .category-select-caret {
  opacity: 0.5;
}

/* Das aufgeklappte Menü. Hängt am Knopf, ist aber mindestens so breit wie er und höchstens so
   breit wie das Fenster — die längsten Kategorienamen brauchen mehr Platz als die 16rem des
   Knopfes. Schatten und Rahmen wie beim Verlaufsmenü in der Kopfzeile, damit beide Menüs der
   Seite als dasselbe Bauteil erkennbar sind. Die Höhe deckelt app.js zusätzlich auf den
   tatsächlich freien Platz unter (bzw. über) dem Knopf. */
.category-select-panel {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  min-width: 100%;
  width: max-content;
  max-width: min(24rem, calc(100vw - 2 * var(--gutter)));
  max-height: 18rem;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  box-shadow: 0 18px 40px hsl(0 0% 0% / 0.45);
}

/* Zu wenig Platz nach unten: Das Menü klappt nach oben auf, statt am Fensterrand abgeschnitten
   zu werden. Gesetzt von app.js beim Öffnen. */
.category-select-panel.is-above {
  top: auto;
  bottom: calc(100% + 0.35rem);
}

/* Nur im Handy-Blatt (siehe Media Query weiter unten) sichtbar. */
.category-select-head {
  display: none;
}

/* Sucheingabe innerhalb des Menüs. Steht unabhängig von der Bildschirmbreite immer da — auch am
   Desktop lohnt sie sich bei langen Listen (z.B. im Component BOM). */
.category-select-search {
  flex: none;
  padding: 0.4rem;
  border-bottom: 1px solid var(--color-border);
}

.category-select-search-input {
  width: 100%;
  padding: 0.4rem 0.6rem;
  font-family: inherit;
  font-size: 0.85rem;
  color: var(--color-text);
  border: 1px solid var(--color-border);
  background: var(--color-bg-sunken);
}

.category-select-search-input::placeholder {
  color: var(--color-text-faint);
}

.category-select-search-input:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-faint);
}

.category-select-list {
  flex: 1 1 auto;
  min-height: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
  /* Ist die Liste am Ende angekommen, soll das Wischen nicht an die Seite dahinter durchgereicht
     werden — sonst scrollt beim Suchen in einer langen Kategorienliste die Tabelle darunter weg. */
  overscroll-behavior: contain;
}

/* Beschriftung links, Anzahl rechts. Die Beschriftung darf umbrechen: Im Menü ist die volle
   Lesbarkeit wichtiger als eine feste Zeilenzahl. */
.category-select-option {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.45rem 0.6rem;
  border-left: 2px solid transparent;
  font-size: 0.85rem;
  line-height: 1.35;
  color: var(--color-text-muted);
  cursor: pointer;
  touch-action: manipulation;
}

/* Der Tastaturfokus wandert wirklich von Eintrag zu Eintrag (roving tabindex, siehe app.js) —
   deshalb reicht :focus als Markierung, ein zusätzlicher Fokusring wäre doppelt gemoppelt. */
.category-select-option:focus {
  outline: none;
  background: var(--color-surface-hover);
  border-left-color: var(--brand-line);
  color: var(--color-text);
}

@media (hover: hover) {
  .category-select-option:hover {
    background: var(--color-surface-hover);
    color: var(--color-text);
  }
}

/* Die aktuelle Auswahl bleibt auch dann erkennbar, wenn der Fokus schon woanders steht. */
.category-select-option[aria-selected="true"] {
  color: var(--brand);
  border-left-color: var(--brand);
  font-weight: 600;
}

.category-select-count {
  flex: none;
  font-size: 0.76rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-faint);
}

.category-select-option[aria-selected="true"] .category-select-count,
.category-select-option:focus .category-select-count {
  color: inherit;
}

/* Von der Sucheingabe ausgeblendete Einträge — see js/kategoriefilter.js. */
.category-select-option.is-search-hidden {
  display: none;
}

/* Am Desktop nie sichtbar: Das Menü hängt dort am Knopf, ein Abdunkeln der ganzen Seite wäre
   für eine Filterauswahl viel zu großes Geschütz. Erst im Handy-Blatt wird daraus display:
   block (siehe Media Query). app.js schaltet unabhängig davon nur .hidden um. */
.category-select-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 59;
  background: hsl(0 0% 0% / 0.55);
  /* Ohne das verschöbe ein Wischen über der Abdeckung die Seite dahinter, während das Blatt
     stehen bleibt — es sähe aus, als hätte man danebengegriffen. */
  touch-action: none;
}

/* Steht im Filterfeld selbst (siehe .filter-input-wrap) und nicht mehr neben dem
   Kategorie-Knopf: Die Zahl beziffert das Ergebnis der Eingabe links von ihr.

   Das Raster stapelt zwei Zeilen in einer Spalte: darüber den Platzhalter aus data-sizer (von
   js/status.js gesetzt, siehe setResultCount), darunter den sichtbaren Text. Die Spalte ist damit
   immer so breit wie die längstmögliche Beschriftung — sonst wüchse beim Filtern ("3 / 117" statt
   "117 / 117") das Eingabefeld daneben nach und der Text darin verrutschte unter dem Cursor. */
.result-count {
  /* Darf schrumpfen (0 1 auto statt none): Der längste Fall ist die laufende Fotoprüfung
     ("300 Foto(s) — 300/300 geprüft"), und auf einem 360px-Display bliebe für die Eingabe
     daneben sonst nichts übrig. Lieber die Zahl beschneiden als das Feld unbenutzbar machen. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  display: grid;
  padding-right: 0.6rem;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-faint);
  white-space: nowrap;
  /* Ohne das ließe sich die Zahl beim Ziehen über das Feld mitmarkieren und landete beim
     Kopieren des Filterbegriffs mit in der Zwischenablage. */
  user-select: none;
}

.result-count::before {
  content: attr(data-sizer);
  height: 0;
  overflow: hidden;
  visibility: hidden;
}

/* ---------- Kopierbalken im Tab "Component BOM" ---------- */

/* Featurebyte und Build-ID am Stück, direkt über der Tabelle (siehe renderDeviceCopyBar in
   app.js). Steht bewusst außerhalb der Tabelle: Der Filter darunter arbeitet auf Tabellenzeilen
   und würde die beiden Werte sonst wegfiltern, sobald man etwas eintippt. */
.device-copy-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: var(--space-block);
}

/* Ein Kopierknopf pro Wert: Beschriftung, Wert, Klemmbrett-Icon. Die Breite richtet sich nach dem
   Inhalt, ist aber gedeckelt — der Featurebyte ist über 80 Zeichen lang und würde den Balken sonst
   allein füllen. */
.device-copy-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  max-width: 100%;
  padding: 0.4rem 0.6rem;
  font-family: inherit;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.device-copy-chip:hover {
  border-color: var(--brand);
}

.device-copy-chip:hover .device-copy-chip-icon {
  color: var(--brand);
}

.device-copy-chip:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.device-copy-chip-label {
  flex: none;
  font-size: 0.72rem;
  font-stretch: 90%;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-faint);
}

/* Der vollständige Wert steht im gemeinsamen Tooltip — hier wird er einzeilig abgeschnitten, damit
   beide Knöpfe nebeneinander passen. Kopiert wird immer der ganze Wert. */
.device-copy-chip-value {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: 0.82rem;
}

.device-copy-chip-icon {
  flex: none;
  display: inline-flex;
  color: var(--color-text-muted);
  transition: color 0.15s ease;
}

/* ---------- Ergebnistabelle ---------- */

/* overflow-x: auto statt hidden: In der Zone zwischen Desktop und Kartendarstellung (siehe
   Media Queries am Dateiende) kann die Tabelle breiter werden als ihr Container — dann soll
   sie in sich scrollen und nicht abgeschnitten werden. Auf Desktopbreiten tritt der Fall nicht
   ein, dort ändert die Regel nichts. */
.table-wrapper {
  border: 1px solid var(--color-border);
  overflow-x: auto;
  scrollbar-width: thin;
}

.results-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--color-surface);
  font-size: 0.9rem;
}

.results-table th,
.results-table td {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-border);
}

/* Tabellenkopf in der Typografie des Corporate Designs: schmal, versal, gesperrt. */
.results-table th {
  background: var(--color-surface-raised);
  font-stretch: 84%;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.results-table tbody tr:last-child td {
  border-bottom: none;
}

/* Zellen oben ausrichten statt mittig — und die Symbolknöpfe trotzdem auf der ersten Textzeile
   halten.
   ---------------------------------------------------------------------------------------------
   Seit die Beschreibung in zwei Spalten steht, bricht der Text regelmäßig auf zwei oder drei
   Zeilen um. Mittig ausgerichtet schwimmt dann jede Zelle auf einer anderen Höhe: Die Teilenummer
   steht in der Mitte einer dreizeiligen Beschreibung, und beim Überfliegen einer langen Liste
   findet man nicht mehr, was zusammengehört. Oben ausgerichtet fängt jede Zeile der Tabelle
   wieder an derselben Kante an.

   Der Haken daran sind die Symbolknöpfe (Lupe, Kopieren, FE, Foto, Shop): Sie sind 2rem hoch, eine
   Textzeile nur rund 1,4rem. Oben ausgerichtet säße der Text spürbar über der Mitte der Knöpfe
   daneben. Die Textzellen bekommen deshalb oben einen Ausgleich von der halben Differenz — damit
   liegt die ERSTE Textzeile wieder auf der Höhe der Knöpfe, alle weiteren laufen darunter weiter.

   Eine einzeilige Tabelle sieht damit aus wie vorher (Text und Knöpfe auf einer Höhe), und die
   Zeilenhöhe wächst nicht: Die Knöpfe bleiben mit 2rem + Zell-Padding weiterhin das höchste
   Element der Zeile.

   --zellen-erstzeile trägt die Rechnung an einer Stelle: 2rem Knopfhöhe gegen die Zeilenhöhe des
   Tabellentexts (0.9rem × 1.55 aus der Grundeinstellung). Wer an der Knopfgröße oder am
   Schriftgrad der Tabelle dreht, ändert den Wert hier mit. */
.results-table {
  --zellen-erstzeile: calc((2rem - 0.9rem * 1.55) / 2);
}

.results-table tbody td {
  vertical-align: top;
}

/* Die Teilenummer bricht nie um: Sie ist der Schlüssel, nach dem man in der Liste sucht, und über
   zwei Zeilen verteilt liest man sie falsch ab und kopiert sie beim Markieren mit einem Umbruch
   darin. Die Spalte wird dadurch von selbst so breit wie die längste Nummer — mehr Breite braucht
   sie nie: Die automatische Tabellenbreite gibt einer nicht umbrechenden Spalte genau ihren
   Inhalt, den Rest teilen sich die beiden Beschreibungsspalten daneben. Reicht die Fensterbreite
   nicht, scrollt der .table-wrapper waagerecht (so wie bisher auch schon bei vielen Spalten). */
.results-table tbody td.part-cell {
  white-space: nowrap;
}

.results-table tbody td.part-cell,
.results-table tbody td.desc-cell,
.results-table tbody td.parttype-cell,
.results-table tbody td.device-label,
.results-table tbody td.device-desc,
.results-table tbody td.device-value {
  padding-top: calc(var(--space-2) + var(--zellen-erstzeile));
}

/* Die erkannte Teileart ist eine Einordnung, keine HP-Angabe — sie soll die Beschreibung daneben
   nicht überstimmen, aber beim Überfliegen einer langen Liste sofort ins Auge fallen. Deshalb
   kleiner und gedämpft, aber nicht umbrechend: eine Kategorie über zwei Zeilen zerreißt die
   Zeilenhöhe der ganzen Tabelle. */
.results-table tbody td.parttype-cell {
  font-size: 0.8rem;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.results-table tbody tr:nth-child(even) {
  background: hsl(0 0% 100% / 0.022);
}

/* Nur Zellen mit kopierbarem Text hervorheben — die Kopier-, Such-, FE- und Foto-Spalte haben
   ihre eigenen Buttons und werden nicht per Zellklick kopiert.
   Nur für Geräte mit echtem Zeiger: Auf dem Touchscreen bleibt ein per Fingertipp ausgelöster
   :hover-Zustand bis zum nächsten Tipp stehen — die zuletzt angetippte Zelle sähe dann dauerhaft
   wie ausgewählt aus. Dort übernimmt stattdessen :active (siehe Media Queries am Dateiende). */
@media (hover: hover) {
  .results-table tbody tr:not(.row-obsolete):not(.device-row-empty) td:not(.actions-cell):not(.copy-cell):not(.part-search-cell):not(.fe-cell):not(.photo-cell):not(.shop-cell):hover {
    background-color: var(--brand-faint);
    box-shadow: inset 0 0 0 1px var(--brand-line);
    color: #fff;
    cursor: pointer;
  }
}

/* Erste Spalte: schmaler Button, der Part Number + Beschreibung zusammen kopiert (Component BOM,
   wo sie noch für sich alleine steht — bei Ersatzteilen siehe .actions-cell). */
.copy-col {
  width: 1%;
}

.copy-cell {
  width: 1%;
  text-align: center;
  padding-left: 0.35rem;
  padding-right: 0.15rem;
}

/* Erste Spalte bei Ersatzteilen: Such- und Kopier-Knopf stehen zusammen in einer Zelle, statt
   sich über zwei Spalten mit je eigenem Zell-Padding zu verlieren — so rücken sie wirklich nah
   zusammen statt nur ihr Padding zu verkleinern. */
.actions-col {
  width: 1%;
}

.actions-cell {
  width: 1%;
  text-align: center;
  white-space: nowrap;
  padding-left: 0.25rem;
  padding-right: 0.25rem;
}

.actions-cell > .part-search-icon-btn {
  margin-right: 0.1rem;
}

.copy-combo-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  background: none;
  border: 1px solid transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.copy-combo-btn:hover {
  background: var(--brand-faint);
  border-color: var(--brand-line);
  color: var(--brand);
}

.copy-combo-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -1px;
}

/* Nur für Screenreader: die erste Spalte hat visuell absichtlich keine Überschrift. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Der Hintergrund bleibt unverändert — Signal genug ist der rote Text im Badge (siehe
   .replacement-badge). Nur der Zeiger zeigt weiterhin an, dass sich die Zeile anklicken lässt. */
.row-obsolete td {
  cursor: pointer;
}

.replacement-badge {
  margin-top: 0.25rem;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--color-danger);
}

/* Sprungziel nach dem Klick auf ein Ersatzteil: kurzer oranger Blitz statt des früheren
   Gelbs — Gelb hätte auf dunklem Grund als eigene Zustandsfarbe missverstanden werden können. */
@keyframes flashHighlight {
  0%, 100% { background-color: transparent; }
  50% { background-color: hsl(17 89% 58% / 0.32); }
}

.flash-highlight td {
  animation: flashHighlight 0.8s ease-in-out 2;
}

/* ---------- Gemeinsamer Tooltip ---------- */

/* Heller Kasten auf dunklem Grund: die Umkehrung von vorher, aus demselben Grund —
   der Tooltip soll sich klar von der Fläche darunter abheben. */
.ui-tooltip {
  position: fixed;
  z-index: 1300;
  width: max-content;
  max-width: min(30rem, calc(100vw - 1rem));
  background: hsl(0 0% 96%);
  color: var(--color-text-on-brand);
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.35rem 0.65rem;
  pointer-events: none;
  white-space: pre-line;
  overflow-wrap: anywhere;
  text-align: center;
  line-height: 1.35;
  transform: translate(-50%, -100%);
  transition: background-color 0.2s;
}

.ui-tooltip::after {
  content: '';
  position: absolute;
  /* Einen Pixel mit der Fläche überlappen: Ohne die Überdeckung kann der Browser zwischen
     Kasten und Border-Dreieck eine feine horizontale Naht zeichnen. */
  top: calc(100% - 1px);
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: hsl(0 0% 96%);
}

/* Steht das Zielelement so weit oben, dass der Tooltip darüber nicht mehr ins Bild passt, setzt
   tooltips.js ihn darunter — dann muss der Zeiger nach oben statt nach unten weisen. */
.ui-tooltip.is-below::after {
  top: auto;
  bottom: calc(100% - 1px);
  border-top-color: transparent;
  border-bottom-color: hsl(0 0% 96%);
}

.ui-tooltip.is-below.copied::after {
  border-top-color: transparent;
  border-bottom-color: var(--color-success);
}

.ui-tooltip.copied {
  background: var(--color-success);
  color: var(--color-text-on-brand);
  animation: tooltipPulse 0.4s ease;
}

.ui-tooltip.copied::after {
  border-top-color: var(--color-success);
}

@keyframes tooltipPulse {
  0% { box-shadow: 0 0 0 0 hsl(145 48% 55% / 0.6); }
  100% { box-shadow: 0 0 0 8px hsl(145 48% 55% / 0); }
}

/* ---------- Foto-Spalte ---------- */

.photo-cell,
.shop-cell {
  text-align: center;
}

/* Das Kamerasymbol ist immer grün — egal ob das Foto im JSON geflaggt war oder erst über die
   Gegenprobe gefunden wurde (siehe probeUnflaggedPhotos). Grün heißt hier schlicht: hier gibt
   es ein Bild. Eine Unterscheidung nach Herkunft des Treffers hilft beim Suchen nicht. */
.photo-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid transparent;
  width: 2rem;
  height: 2rem;
  padding: 0;
  cursor: pointer;
  color: var(--color-success);
}

.photo-icon-btn:hover {
  background: var(--brand-faint);
  border-color: var(--brand-line);
  color: var(--color-success);
}

.photo-icon-btn:focus-visible {
  outline: 2px solid var(--color-success);
  outline-offset: -1px;
}

/* Kein Foto: dasselbe Symbol, durchgestrichen und in gedämpftem Rot. Es steht an derselben Stelle
   wie das grüne, weil eine leere Zelle zwei Dinge bedeuten konnte — "kein Bild" und "Prüfung läuft
   noch". Maße und Innenabstand sind identisch zum Knopf, damit die Spalte auf einer Linie bleibt.
   Ein <span> ohne Zeigerhand: Hier gibt es nichts zu klicken. */
.photo-icon-none {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid transparent;
  padding: 0;
  color: var(--color-photo-none);
}

/* Die Shop-Spalte hinter dem Foto: der Verweis auf das Angebot des Ersatzteilhändlers, an jeder
   Zeile mit Teilenummer (siehe shopLinkHtml in js/tabelle.js und js/shop.js).

   Blau und nicht grün, weil es ausdrücklich NICHT dasselbe ist wie ein Foto in der Anwendung: Grün
   heißt in der Nachbarspalte "hier ist ein Bild, ein Klick zeigt es". Dieser Link führt dagegen
   aus der Anwendung heraus zu einem fremden Anbieter, und das soll man vorher sehen. Aus demselben
   Grund wird dort auch kein fremdes Bild eingebunden — die Rechte daran liegen nicht bei uns.

   Maße und Innenabstand wie beim grünen Knopf, damit beide Spalten auf einer Linie bleiben. */
.shop-link-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid transparent;
  padding: 0;
  color: var(--color-info);
}

.shop-link-btn:hover {
  background: var(--color-info-faint);
  border-color: var(--color-info-line);
  color: var(--color-info);
}

.shop-link-btn:focus-visible {
  outline: 2px solid var(--color-info);
  outline-offset: -1px;
}

/* ---------- Foto-Modal ---------- */

/* ---------- Gemeinsame Grundlage aller Modale ---------- */

/* Es gibt drei Overlays (Foto, gleichwertige Teile, Cache-Ansicht), und sie standen vorher
   dreimal vollständig ausgeschrieben im Stylesheet — bis auf Deckkraft und z-index Zeile für
   Zeile identisch, inklusive Kopfzeile und Schließen-Knopf. Beim Nachziehen einer Änderung
   erwischte man zuverlässig nur zwei davon.
   Hier steht jetzt, was alle drei gemeinsam haben; darunter bleibt je Modal nur das, was es
   wirklich unterscheidet (Maße, Deckkraft, Ebene). Ein viertes Modal sieht damit ohne eigenes
   CSS von Anfang an richtig aus. */

.modal-overlay {
  position: fixed;
  inset: 0;
  background: hsl(240 3% 5% / 0.72);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  z-index: 1150;
  /* Eigener Scrollcontainer statt "body { overflow: hidden }" (Details in js/fotomodal.js beim
     Öffnen des Modals) — "contain" verhindert trotzdem, dass Mausrad-Scrollen an die Seite
     dahinter durchgereicht wird. */
  overflow: auto;
  overscroll-behavior: contain;
}

/* Der Kasten selbst: einziges Element mit Schatten, denn es schwebt tatsächlich über der Seite
   und braucht die Tiefe. Maße setzt jedes Modal für sich. */
.modal-panel {
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  display: flex;
  flex-direction: column;
  max-width: 100%;
  max-height: 100%;
  box-shadow: 0 24px 60px hsl(240 3% 3% / 0.7);
}

.modal-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

/* Schiebt sich immer an das rechte Ende der Kopfzeile, egal was links davon steht. */
.modal-close {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--control-size);
  height: var(--control-size);
  padding: 0;
  color: var(--color-text-muted);
  background: none;
  border: 1px solid transparent;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}

.modal-close:hover {
  color: var(--brand);
  border-color: var(--brand-line);
}

/* Stand vorher nur an einem der drei Knöpfe — mit der gemeinsamen Basis ist die Tastaturbedienung
   überall gleich sichtbar. */
.modal-close:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

/* Der scrollbare Inhalt unter der Kopfzeile. Das Foto-Modal nimmt das bewusst NICHT: dort ist der
   Body die Zoom-Bühne mit overflow: hidden. */
.modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
}

/* ---------- Kamera-Scanner ---------- */

.scanner-modal {
  width: min(92vw, 44rem);
}

.scanner-modal-title {
  font-weight: 700;
}

.scanner-body {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.scanner-video-wrap {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: #050506;
  container-type: size;
}

.scanner-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Ein quadratischer Rahmen bildet QR-Codes unverzerrt ab. Seine Größe richtet sich nach der
   kürzeren Seite des Kamerabilds, damit er auch im mobilen Hoch- und Querformat vollständig
   sichtbar bleibt. Die vier Ecken bleiben vor einem unruhigen Kamerabild besonders deutlich.
   Im Barcode-Modus (data-modus am Modal) wird daraus ein liegendes Rechteck: Ein langer
   Strichcode passt sonst nur quer und dann so klein ins Quadrat, dass die Striche verschwimmen.
   Der Rahmen bleibt eine Zielhilfe — beschnitten wird das Kamerabild nicht, siehe js/scanner.js. */
.scanner-guide {
  position: absolute;
  --scanner-guide-size: min(66vmin, 26rem);
  --scanner-guide-hoehe: var(--scanner-guide-size);
  top: 50%;
  left: 50%;
  width: var(--scanner-guide-size);
  height: var(--scanner-guide-hoehe);
  transform: translate(-50%, -50%);
  pointer-events: none;
  transition: width 0.2s ease, height 0.2s ease;
}

@supports (width: 1cqw) {
  .scanner-guide {
    --scanner-guide-size: min(76cqw, 76cqh, 26rem);
  }
}

.scanner-modal-overlay[data-modus="barcode"] .scanner-guide {
  --scanner-guide-size: min(88vmin, 34rem);
  --scanner-guide-hoehe: min(30vmin, 11rem);
}

@supports (width: 1cqw) {
  .scanner-modal-overlay[data-modus="barcode"] .scanner-guide {
    --scanner-guide-size: min(92cqw, 34rem);
    --scanner-guide-hoehe: min(46cqh, 11rem);
  }
}

/* Die Codeart liegt im Bild, nicht darunter: Am Handy füllt das Video den Rest des Fensters,
   eine Zeile darunter drängte Status und Hinweis aus dem Sichtfeld. */
.scanner-modes {
  position: absolute;
  left: 50%;
  bottom: 0.9rem;
  transform: translateX(-50%);
  display: flex;
  gap: 2px;
  padding: 2px;
  border-radius: 999px;
  background: rgba(5, 5, 6, 0.72);
  backdrop-filter: blur(4px);
}

.scanner-mode {
  padding: 0.4rem 1.05rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: #f2f2f3;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 700;
  cursor: pointer;
}

.scanner-mode[aria-checked="true"] {
  background: var(--brand);
  color: #101013;
}

.scanner-guide span {
  position: absolute;
  width: 2.5rem;
  height: 2.5rem;
  border-color: var(--brand);
  border-style: solid;
}

.scanner-guide span:nth-child(1) { top: 0; left: 0; border-width: 3px 0 0 3px; }
.scanner-guide span:nth-child(2) { top: 0; right: 0; border-width: 3px 3px 0 0; }
.scanner-guide span:nth-child(3) { right: 0; bottom: 0; border-width: 0 3px 3px 0; }
.scanner-guide span:nth-child(4) { bottom: 0; left: 0; border-width: 0 0 3px 3px; }

.scanner-guide::after {
  content: '';
  position: absolute;
  left: 0.75rem;
  right: 0.75rem;
  top: 0.75rem;
  height: 1px;
  background: var(--brand);
  box-shadow: 0 0 8px var(--brand);
  animation: scanner-line 2.2s ease-in-out infinite alternate;
}

@keyframes scanner-line {
  from { transform: translateY(0); }
  to { transform: translateY(calc(var(--scanner-guide-hoehe) - 1.5rem)); }
}

.scanner-status {
  min-height: 1.3rem;
  margin: 0;
  padding: 0.8rem 1rem 0.25rem;
  color: var(--brand);
  font-weight: 700;
  text-align: center;
}

.scanner-status.error {
  color: var(--color-danger);
}

.scanner-note {
  margin: 0;
  padding: 0.2rem 1rem 1rem;
  color: var(--color-text-faint);
  font-size: 0.78rem;
  line-height: 1.4;
  text-align: center;
}

@media (max-width: 560px) {
  .scanner-modal-overlay {
    align-items: stretch;
    padding: 0;
  }

  .scanner-modal {
    width: 100%;
    height: 100%;
    border: 0;
  }

  .scanner-body {
    flex: 1 1 auto;
  }

  .scanner-video-wrap {
    flex: 1 1 auto;
    min-height: 0;
    aspect-ratio: auto;
  }
}

/* ---------- Foto-Modal ---------- */

/* .photo-modal-overlay braucht auf dieser Breite keine eigenen Regeln — Grund, Deckkraft und
   Ebene kommen vollständig aus .modal-overlay. Die Klasse bleibt trotzdem am Element: Weiter
   unten stellt eine Media Query das Modal auf Handybreiten auf Vollbild um. */

/* Trägt Pfeil – Modal – Pfeil in einer Reihe. Feste Höhe (statt auto), damit das
   max-height: 100% des Modals weiterhin einen definierten Bezug hat. */
.photo-modal-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  width: 100%;
  height: 100%;
  min-height: 0;
}

/* Rahmen, Hintergrund, Schatten und die Flex-Spalte kommen aus .modal-panel — hier stehen nur
   die Maße. Bewusst ohne "resize": Der Anfasser säße in der rechten unteren Ecke und damit über
   dem Bild, genau dort, wo man zum Verschieben zieht. */
.photo-modal {
  /* Wächst mit dem Viewport, bleibt auf sehr großen Displays aber angenehm überschaubar.
     Die Untergrenze entspricht ungefähr der bisherigen Desktopgröße; max-width/max-height
     aus .modal-panel lassen das Modal auf kleineren Fenstern weiterhin passend schrumpfen
     (Bezug ist das Overlay inkl. seines Innenabstands — dadurch bleibt rundherum immer ein
     sichtbarer Rand, ohne mit vw/vh am reservierten Scrollbar-Gutter vorbeizurechnen). */
  width: clamp(650px, 78vw, 1800px);
  height: clamp(600px, 86vh, 1400px);
  /* Untergrenze für das Schrumpfen neben den Blätter-Pfeilen (siehe .photo-modal-nav). */
  min-width: 320px;
  min-height: 260px;
  overflow: hidden;
}

/* Der linke Innenabstand ist eine Spur größer als in .modal-head: Die Überschrift steht hier
   allein, ohne Untertitel daneben. */
.photo-modal-header {
  padding-left: 1.1rem;
}

/* Überschrift und Galeriezähler als eine Einheit links im Kopf. baseline statt center, damit die
   Ziffern auf derselben Linie sitzen wie die Versalien der Überschrift. */
.photo-modal-titles {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  min-width: 0;
}

.photo-modal-title {
  font-stretch: 84%;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* Zeile unter der Kopfzeile: Teilenummer, dahinter die Beschreibung. Zwei getrennte Knöpfe statt
   eines Textes, damit man beides unabhängig voneinander kopieren kann; der Abstand dazwischen
   kommt aus gap und nicht aus einem Leerzeichen, das sonst mitkopiert würde.
   Der linke Innenabstand ist um die Polsterung der Knöpfe kleiner als der der Kopfzeile —
   dadurch beginnt die Nummer genau unter der Überschrift und nicht daneben versetzt.
   Auf dem Handy stehen Beschreibung und Nummer stattdessen untereinander (Media Query
   <= 560px), weil die drei Elemente dort nicht nebeneinander passen. */
.photo-modal-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.1rem 0.4rem;
  padding: 0.4rem 0.7rem;
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}

/* Kopiert Nummer und Beschreibung zusammen; sieht und verhält sich wie sein Gegenstück in der
   ersten Tabellenspalte (.copy-combo-btn, dessen Stil er mitbenutzt). Er trägt nur ein Symbol und
   damit keine Schriftlinie, an der ihn die Grundlinien-Ausrichtung der Zeile festmachen könnte —
   oben ausgerichtet sitzt er auf Höhe der ersten Zeile, auch wenn die Beschreibung daneben
   mehrzeilig umbricht. */
.photo-modal-copy {
  align-self: flex-start;
}

/* Teilenummer des gezeigten Fotos, per Klick kopierbar (siehe photoModalPartBtn in app.js).
   Dicktengleiche Schrift wie überall, wo Codes zum Abtippen oder Vergleichen stehen. */
.photo-modal-part {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.02em;
  color: var(--color-text);
  background: none;
  border: 1px solid transparent;
  padding: 0.25rem 0.4rem;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.photo-modal-part:hover {
  background: var(--brand-faint);
  border-color: var(--brand-line);
  color: var(--brand);
}

.photo-modal-part:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -1px;
}

/* Beschreibung neben der Nummer. Bricht bei langen Texten innerhalb ihres eigenen Kastens um,
   statt die Zeile zu sprengen — min-width: 0 hebt dafür die Mindestbreite auf, die ein
   Flex-Element sonst von seinem längsten Wort erbt. */
.photo-modal-desc {
  min-width: 0;
  font: inherit;
  font-size: 0.85rem;
  text-align: left;
  color: var(--color-text-muted);
  background: none;
  border: 1px solid transparent;
  padding: 0.25rem 0.4rem;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.photo-modal-desc:hover {
  background: var(--brand-faint);
  border-color: var(--brand-line);
  color: var(--brand);
}

.photo-modal-desc:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -1px;
}

/* Schiebt sich selbst nach rechts, statt die Kopfzeile per space-between auseinanderzuziehen —
   sonst stünde die Teilenummer je nach Fensterbreite irgendwo in der Mitte statt neben dem Titel. */
.photo-modal-header-right {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
}

/* Steht in .photo-modal-titles direkt hinter der Überschrift. Der Trennpunkt gehört zum Zähler
   selbst und nicht als eigenes Element ins HTML — so verschwindet er mit ihm, wenn das Modal
   ohne Bilderliste geöffnet wurde (.hidden). */
.photo-modal-counter {
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--color-text-muted);
}

.photo-modal-counter::before {
  content: '·';
  margin-right: 0.45rem;
  color: var(--color-border);
}

/* Zoom-Bedienung im Kopf: minus / Prozentwert (= Reset) / plus. */
.photo-modal-zoom {
  display: flex;
  align-items: center;
  gap: 0.15rem;
}

.photo-zoom-btn,
.photo-zoom-level {
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  color: var(--color-text);
  font-family: inherit;
  cursor: pointer;
  line-height: 1;
}

.photo-zoom-btn {
  width: 2rem;
  height: 2rem;
  font-size: 1.05rem;
}

.photo-zoom-level {
  height: 2rem;
  padding: 0 0.5rem;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
  min-width: 3.4rem;
}

.photo-zoom-btn:hover,
.photo-zoom-level:hover {
  border-color: var(--brand);
  color: var(--brand);
}

.photo-zoom-btn:disabled {
  opacity: 0.35;
  cursor: default;
  border-color: var(--color-border);
  color: var(--color-text);
}

.photo-modal-body {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  overflow: hidden;
  min-height: 0;
  /* Ausschnitt für den Zoom: Ein Klick vergrößert, im vergrößerten Zustand wird gezogen.
     touch-action: none, damit Wischen auf dem Touchscreen das Bild schiebt statt der Seite. */
  cursor: zoom-in;
  touch-action: none;
}

/* Meldung an der Stelle des Bildes, wenn es sich nicht laden lässt (siehe fotomodal.js). Das Bild
   selbst wird dabei ausgeblendet — ein gescheitertes <img> zeigt sonst das Bruchsymbol des
   Browsers, das wie ein Fehler der Anwendung aussieht. Es bleibt im Dokument, damit der Zoom
   weiterhin an demselben Element hängt. */
.photo-modal-body.is-missing {
  cursor: default;
}

.photo-modal-body.is-missing img {
  display: none;
}

.photo-modal-error {
  margin: 0;
  padding: 0 1rem;
  text-align: center;
  color: var(--color-text-muted);
}

.photo-modal-body.is-zoomed {
  cursor: grab;
}

.photo-modal-body.is-panning {
  cursor: grabbing;
}

/* Blätter-Pfeile sitzen als eigene Spalten neben dem Modal und überdecken das Foto nicht.
   Das Modal darf dafür schrumpfen (flex-shrink) bis zu seiner min-width. */
.photo-modal-nav {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid var(--color-border-strong);
  background: var(--color-surface);
  color: var(--color-text);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.photo-modal-nav:hover {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--color-text-on-brand);
}

.photo-modal-nav:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Zu schmal für seitliche Spalten: Pfeile rücken zurück über die Bildränder. */
@media (max-width: 560px) {
  .photo-modal-stage {
    gap: 0;
    position: relative;
  }

  .photo-modal-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 1;
    width: var(--control-size);
    height: var(--control-size);
  }

  .photo-modal-nav-prev {
    left: 0.5rem;
  }

  .photo-modal-nav-next {
    right: 0.5rem;
  }
}

/* Weiße Unterlage: HP liefert die Teilefotos freigestellt oder auf Weiß. Ohne eigene Fläche
   würden dunkle Motive und transparente Ränder im dunklen Modal verschwinden. */
.photo-modal img {
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  /* Zoom/Pan laufen über transform — das Layout bleibt davon unberührt, deshalb springt
     das Modal beim Zoomen nicht. Origin ist die Bildmitte (siehe Berechnung in app.js). */
  transform-origin: center center;
  will-change: transform;
  user-select: none;
  -webkit-user-drag: none;
}

/* Sanfter Übergang beim Zoomen per Klick/Knopf/Mausrad — beim Ziehen stört er
   (das Bild würde der Maus hinterherhinken) und wird darum abgeschaltet. */
.photo-modal img.is-animated {
  transition: transform 0.15s ease-out;
}

/* Vollständig aus .modal-close — bis auf margin-left: Hier sitzt der Knopf als letztes Element in
   .photo-modal-header-right, das sich seinerseits schon nach rechts schiebt. */
.photo-modal-close {
  margin-left: 0;
}

/* ---------- Galerie ---------- */

/* Galerie-Tab: normaler Seiteninhalt an der Stelle der Tabelle, kein Overlay mehr.
   Trefferzahl und Filter kommen aus derselben Toolbar wie bei der Tabelle (.filter-wrapper). */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 1.1rem;
  align-content: start;
}

.gallery-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  border: 1px solid var(--color-border);
  padding: 0.65rem;
  background: var(--color-surface);
  font: inherit;
  color: inherit;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
}

/* Das Anheben um 2px ist eine reine Zeigergeste — per Fingertipp ausgelöst bliebe die zuletzt
   berührte Kachel dauerhaft aus der Reihe stehen. */
@media (hover: hover) {
  .gallery-item:hover {
    border-color: var(--brand);
    background: var(--color-surface-raised);
    transform: translateY(-2px);
  }

  .gallery-item:hover .gallery-part-number {
    color: var(--color-text);
  }

  .gallery-item:hover .gallery-desc {
    color: var(--color-text-muted);
  }
}

.gallery-item:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Feste Höhe für ein gleichmäßiges Raster — der freie Platz um ein flaches oder schmales
   Foto herum zeigt dabei die Kachelfarbe, nicht die weiße Bildunterlage. */
.gallery-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 140px;
}

/* Gleiche Begründung wie im Modal: eigene weiße Fläche, damit jedes Foto sichtbar bleibt.
   max-* statt fester Maße, damit die Box exakt auf das skalierte Foto schrumpft und das
   Weiß nicht darüber hinaussteht. */
.gallery-item img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  background: #fff;
  pointer-events: none;
}

.gallery-item .gallery-part-number {
  margin-top: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  color: var(--color-text-muted);
  word-break: break-all;
}

/* Beschreibung unter der Nummer. Auf zwei Zeilen gekürzt, weil die Kacheln sonst je nach
   Textlänge unterschiedlich hoch würden und das Raster ausfranst — der vollständige Text
   steht im gemeinsamen Tooltip (siehe renderGallery) und im Foto-Modal. */
.gallery-item .gallery-desc {
  margin-top: 0.25rem;
  font-size: 0.72rem;
  line-height: 1.35;
  color: var(--color-text-faint);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Die drei Leerzustände der Galerie: Prüfung läuft, nichts gefunden, nichts passt zum Filter.
   Aufbau bewusst wie beim Startbildschirm (.empty-state weiter oben) — Icon, große Zeile, leiser
   Zusatz. Vorher stand hier nur ein grauer Satz oben links im leeren Raster; der war als Antwort
   auf die Suche kaum zu erkennen. Die Mindesthöhe hält den Block ungefähr dort, wo sonst die
   erste Kachelreihe steht, damit die Galerie beim Umschalten nicht in sich zusammenfällt. */
.gallery-empty {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-height: 15rem;
  padding: 2rem 1rem;
  text-align: center;
  color: var(--color-text-muted);
}

/* Dieselbe zurückhaltende Farbe wie das Lupen-Icon des Startbildschirms: deutlich sichtbar,
   aber leiser als der Text darunter — es erklärt den Zustand, es meldet keinen Fehler. */
.gallery-empty-icon {
  color: var(--color-border-strong);
  margin-bottom: 0.5rem;
}

.gallery-empty-title {
  margin: 0;
  font-size: 1.15rem;
  line-height: 1.4;
  color: var(--color-text);
}

.gallery-empty-hint {
  margin: 0;
  max-width: 26rem;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--color-text-faint);
}

/* Erscheint nur, wenn die Fotoprüfung an der Verbindung gescheitert ist — also in genau dem Fall,
   in dem die Oberfläche früher fälschlich "keine Fotos vorhanden" behauptet hat. Der Knopf ist
   die einzige Stelle in der Galerie, an der etwas nachgeladen wird, deshalb darf er wie ein Knopf
   aussehen und nicht wie ein Link. */
.gallery-retry-btn {
  margin-top: 0.9rem;
  padding: 0.45rem 1rem;
  font: inherit;
  font-size: 0.85rem;
  color: var(--color-text);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  border-radius: 999px;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

@media (hover: hover) {
  .gallery-retry-btn:hover {
    border-color: var(--brand);
    color: var(--brand);
  }
}

.gallery-retry-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Fortschrittszeile über dem Raster, solange die Fotoprüfung läuft. Sie ist die Antwort auf die
   Frage "ist diese Liste schon vollständig?" und steht deshalb dort, wo man mit dem Lesen anfängt,
   nicht unter hundertsiebzig Kacheln.

   Blau statt Orange: Die Markenfarbe bleibt der Interaktion vorbehalten, und ein laufender Abruf
   ist keine. Es ist dieselbe Farbe, mit der der Trefferkopf "gerade direkt von HP geholt" meldet —
   hier passiert genau das, nur für die Bilder. */
.gallery-progress {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.9rem;
  margin-bottom: 1rem;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.85rem;
  color: var(--color-text-faint);
}

/* Nimmt den Rest der Zeile ein, fällt in der schmalen Ansicht aber unter den Text statt ihn auf
   zwei Zeichen zusammenzuquetschen. */
.gallery-progress-track {
  flex: 1 1 8rem;
  height: 2px;
  background: var(--color-border);
  overflow: hidden;
}

.gallery-progress-bar {
  display: block;
  width: 0;
  height: 100%;
  background: var(--color-info);
  /* Die Blöcke kommen in Sprüngen von bis zu 36 Teilen zurück; ohne den Übergang ruckte der Balken
     in großen Schritten. Dauer und Kurve wie bei den übrigen Zustandswechseln der Oberfläche. */
  transition: width 0.25s ease;
}

/* Teilerfolg-Zeile unter dem Raster: Kacheln sind da, aber nicht alle Teile konnten geprüft
   werden. Bewusst unscheinbar — sie ergänzt das Ergebnis, statt es infrage zu stellen. */
.gallery-incomplete {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  margin-top: 1.25rem;
  padding: 0.85rem 1rem;
  border-top: 1px solid var(--color-border);
  font-size: 0.85rem;
  text-align: center;
  color: var(--color-text-faint);
}

.gallery-incomplete .gallery-retry-btn {
  margin-top: 0;
}

/* ---------- QR-Codes ---------- */

/* Wie die Galerie normaler Seiteninhalt an der Stelle der Tabelle, kein Overlay. */
.qr-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.1rem;
  align-content: start;
}

.qr-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  padding: 1.4rem 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

.qr-section-title {
  margin: 0;
  font-size: 0.78rem;
  font-stretch: 90%;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* Weißer Grund unter dem Code: dunkle Module auf dunklem Untergrund liest kein Scanner. */
.qr-image {
  display: block;
  width: 220px;
  height: 220px;
  background: #fff;
  border: 4px solid #fff;
  image-rendering: pixelated;
}

/* Der Wert im Klartext — zugleich Kopier-Schaltfläche, wie die Zeilen im Geräteinfo-Tab.
   Der Featurebyte ist über 80 Zeichen lang, deshalb Umbruch mitten im „Wort". */
.qr-value {
  max-width: 100%;
  margin: 0;
  padding: 0.4rem 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--color-text);
  background: var(--color-bg-sunken);
  border: 1px solid var(--color-border);
  cursor: pointer;
  overflow-wrap: anywhere;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.qr-value:hover {
  border-color: var(--brand);
}

.qr-value:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* ---------- Tabs ---------- */

/* Obere Zeile der Steuerung. Rahmen und Außenabstand kommen aus .results-head — die
   Reiter sitzen mit der Filterzeile in einem Kasten, statt als eigenes Segmented Control
   darüber zu schweben. Damit ist auf den ersten Blick klar, dass beide dasselbe Ergebnis
   steuern.

   Die versenkte Fläche kommt aus .results-head und gilt für den ganzen Kasten (siehe dort),
   damit der aktive Reiter darin *eine Ebene höher* stehen kann statt nur eine Spur heller zu
   sein. Vorher trugen aktiver und inaktiver Reiter dieselbe Fläche wie der Kasten, und der
   Zustand hing allein an einem 2px-Strich — in einem Kasten, der ringsum schon 1px-Linien hat,
   geht so ein Strich unter. Hier steht deshalb keine eigene Fläche mehr; eine zweite Angabe
   derselben Farbe liefe der in .results-head irgendwann davon. */
.tabs {
  display: flex;
  width: 100%;
  gap: var(--space-1);
  padding: var(--space-1);
  box-sizing: border-box;
}

.tab-btn {
  min-height: var(--control-size);
  padding: var(--space-2) var(--space-4);
  font-family: inherit;
  font-size: 0.78rem;
  font-stretch: 90%;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: none;
  background: transparent;
  cursor: pointer;
  color: var(--color-text-muted);
  transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}

/* Jeder Reiter trägt beide Beschriftungen im Markup; sichtbar ist immer genau eine. Getauscht
   wird ab 720px (siehe Media Queries am Dateiende). */
.tab-label-short {
  display: none;
}

/* Trefferzahl am Reiter — die Anzahl der Teile bei Advanced und General, die geprüfte Anzahl
   der Fotos bei der Galerie. Der Abstand steht hier und nicht als Leerzeichen im Text
   (Begründung siehe app.js). Leer, solange nichts gezählt wurde; :empty verhindert, dass dann
   trotzdem ein Abstand hinter der Beschriftung entsteht.

   Gedämpft und etwas kleiner: Die Zahl ist eine Zusatzangabe zur Beschriftung, keine zweite
   Beschriftung. Am aktiven Reiter darf sie mit auf die Vordergrundfarbe wechseln, sonst
   wirkte ausgerechnet der offene Reiter halb ausgegraut. */
.tab-count:not(:empty) {
  margin-left: 0.35em;
  font-size: 0.92em;
  font-weight: 600;
  color: var(--color-text-faint);
}

.tab-btn.active .tab-count,
.tab-btn:hover:not(:disabled) .tab-count {
  color: var(--color-text-muted);
}

/* Bei aktivem Textfilter steht an allen durchsuchbaren Reitern "Treffer / Gesamt". Nur positive
   Treffer werden orange; Beschriftung und Fläche bleiben unverändert, damit die Farbe eindeutig
   als Suchauskunft und nicht als zweiter aktiver Reiter gelesen wird. */
.tab-btn.hat-filterauswertung .tab-count {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.tab-btn.hat-filtertreffer .tab-count,
.tab-btn.hat-filtertreffer:hover:not(:disabled) .tab-count {
  color: var(--brand);
  font-weight: 700;
}

/* Nulltreffer bleiben bewusst grau und der Reiter bleibt anklickbar. Die höhere Spezifität
   schlägt auch die normale Farbaufhellung des aktiven bzw. gerade überfahrenen Reiters. */
.tab-btn.hat-filterauswertung:not(.hat-filtertreffer) .tab-count,
.tab-btn.hat-filterauswertung:not(.hat-filtertreffer):hover:not(:disabled) .tab-count {
  color: var(--color-text-faint);
  font-weight: 600;
}

/* Aktiver Reiter: der orange Fuß und die volle Textfarbe, sonst nichts. Er trug zwischenzeitlich
   zusätzlich eine erhöhte Fläche — die stammt aus der Zeit, als der Kasten drumherum hell war
   und der Strich allein zwischen lauter 1px-Linien unterging. Seit die ganze Steuerung eine
   Mulde ist (siehe .results-head), steht der Fuß auf ruhigem Grund und trägt für sich. */
.tab-btn.active {
  color: var(--color-text);
  box-shadow: inset 0 -2px 0 var(--brand);
}

/* Hover trägt einen durchscheinenden Film — jetzt die einzige Fläche in der Leiste. Zu
   verwechseln ist er trotzdem nicht: Der aktive Reiter hängt am orangen Fuß, nicht an einer
   Füllung, und der Film wandert sichtbar mit dem Zeiger mit. Nicht sichtbar am Handy, wo es
   keinen Zeiger gibt — dort ist der Fuß ohnehin das einzige Signal. */
.tab-btn:hover:not(.active):not(:disabled) {
  color: var(--color-text);
  background: var(--color-surface-hover);
}

/* Zu einer Produkt- oder Teilenummer liefert HP nur eine der beiden Listen. Der zugehörige Tab
   bleibt sichtbar (die Trennung Advanced/General soll erklärt bleiben), führt aber ins Leere und
   wird deshalb ausgegraut statt eine leere Tabelle anzubieten. */
.tab-btn:disabled {
  cursor: not-allowed;
  color: var(--color-text-faint);
  opacity: 0.55;
}

.tab-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -1px;
}

/* Die Galerie zeigt dieselben Daten in einer anderen Darstellungsform als die beiden
   Tabellen-Tabs — deshalb bleibt sie Teil des Segmented Controls, wird davon aber durch
   einen schmalen Trennstrich abgesetzt. */
.tab-btn.tab-separated {
  position: relative;
  margin-left: 0.55rem;
}

.tab-btn.tab-separated::before {
  content: '';
  position: absolute;
  left: -0.4rem;
  top: 0.3rem;
  bottom: 0.3rem;
  width: 1px;
  background: var(--color-border-strong);
}

/* ---------- Rohdaten ---------- */

.raw-json {
  background: var(--color-black);
  border: 1px solid var(--color-border);
  color: hsl(0 0% 78%);
  padding: 1rem;
  overflow-x: auto;
  max-height: 400px;
  overflow-y: auto;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  margin-bottom: var(--space-block);
}

/* ---------- Fußzeile ---------- */

.site-footer {
  position: relative;
  z-index: 1;
  background: var(--color-black);
  border-top: 1px solid var(--color-border);
  /* Etwas heller als --color-text-faint: auf dem fast schwarzen Fußzeilengrund käme der
     Haftungshinweis sonst unter das Kontrastminimum für Fließtext. */
  color: hsl(0 0% 58%);
  font-size: 0.78rem;
  font-weight: 300;
  line-height: 1.55;
  padding-block: 2rem 2.25rem;
}

.site-footer p {
  margin: 0;
  max-width: 78ch;
}

.site-footer p + p {
  margin-top: 0.6rem;
}

.site-footer strong {
  font-weight: 700;
  color: hsl(0 0% 75%);
}

.site-footer .dot {
  color: var(--brand);
}

.site-footer a {
  color: hsl(0 0% 70%);
}

.site-footer a:hover {
  color: var(--brand);
}

/* ---------- Eigene Scrollbar ---------- */

/* Ersetzt die native (versteckte, siehe html oben) Scrollbar der Seite. position: fixed
   heißt: nimmt nie Platz vom Layout weg, egal ob sichtbar oder nicht — dadurch springt
   nichts, wenn sie erscheint. Liegt über der Kopfzeile (z-index 50), sonst würde die
   Leiste beim Scrollen unter der klebenden Kopfzeile verschwinden; unter dem Foto-Modal
   (1150), weil das Modal seine eigene (native) Scrollbar hat und unsere hier nichts
   verloren hätte. Sichtbarkeit/Position/Höhe steuert updateCustomScrollbar() in app.js. */
.custom-scrollbar {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 10px;
  z-index: 200;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
}

.custom-scrollbar.is-active {
  opacity: 1;
  pointer-events: auto;
}

.custom-scrollbar-thumb {
  position: absolute;
  right: 2px;
  left: 2px;
  /* Höhe und vertikale Position werden per style.height / style.transform aus app.js
     gesetzt (abhängig von scrollHeight/scrollY) — hier nur die Optik. */
  background: var(--color-border-strong);
  cursor: pointer;
  /* Verhindert, dass ein Ziehen auf Touch-Geräten zusätzlich als Seiten-Scroll gewertet wird
     (der Thumb übersetzt die Bewegung ja schon selbst per Pointer-Events in scrollY). */
  touch-action: none;
}

.custom-scrollbar-thumb:hover,
.custom-scrollbar-thumb.is-dragging {
  background: var(--brand);
}

/* ---------- Nach-oben-Knopf ---------- */

/* Quadratisch und kantig wie alles andere; liegt über dem Inhalt, aber unter Kopfzeile
   (z-index 50 reicht dafür nicht — der Knopf steht ja unten) und Foto-Modal (1150),
   damit er das Overlay nicht durchstößt. */
.to-top {
  position: fixed;
  right: var(--gutter);
  bottom: var(--gutter);
  z-index: 90;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--color-border-strong);
  background: var(--color-surface-raised);
  color: var(--color-text-muted);
  cursor: pointer;
  /* Ausgangszustand: unsichtbar und nicht fokussierbar. visibility (statt nur opacity)
     nimmt den Knopf zusätzlich aus der Tab-Reihenfolge, solange er oben steht. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.5rem);
  transition: opacity 0.18s ease, transform 0.18s ease,
              background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease,
              /* erst am Ende der Ausblendung umschalten, sonst verschwindet er hart */
              visibility 0s linear 0.18s;
}

.to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
  /* Beim Einblenden soll die Sichtbarkeit sofort greifen. */
  transition-delay: 0s;
}

.to-top:hover {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--color-text-on-brand);
}

.to-top:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.to-top svg {
  display: block;
}

/* ---------- Dev-Panel (DEV-ONLY: vor Go-Live entfernen, siehe index.html) ---------- */

/* Bewusst unten links, damit es sich nicht mit dem Nach-oben-Knopf (unten rechts) beißt. */
.dev-panel {
  position: fixed;
  left: var(--gutter);
  bottom: var(--gutter);
  z-index: 90;
}

.dev-panel-toggle {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--color-border-strong);
  background: var(--color-surface-raised);
  color: var(--color-text-muted);
  font-size: 1.1rem;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.dev-panel-toggle:hover {
  background: var(--color-warning);
  border-color: var(--color-warning);
  color: var(--color-text-on-brand);
}

.dev-panel-toggle:focus-visible {
  outline: 2px solid var(--color-warning);
  outline-offset: 2px;
}

.dev-panel-body {
  position: absolute;
  left: 0;
  bottom: calc(100% + 0.5rem);
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  width: max-content;
  padding: 0.75rem;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-warning-line);
}

/* Die Rohdaten-Buttons sind gesperrt, solange keine Suche gelaufen ist — das muss man sehen. */
.dev-panel-body .btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.dev-panel-title {
  margin: 0 0 0.15rem;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-warning);
}

/* ---------- Passwortgeschützte Cache-Übersicht (DEV-ONLY) ---------- */

/* Liegt über dem Foto-Modal, weil sie aus dem Entwicklerpanel heraus geöffnet wird und dabei
   alles andere überdecken soll. Etwas dunklerer Grund als die übrigen Overlays: Dahinter steht
   in aller Regel eine vollständige Trefferliste, die sonst durchscheint und ablenkt. */
.cache-view-overlay {
  z-index: 1200;
  padding: 2.25rem;
  background: hsl(240 3% 5% / 0.78);
}

.cache-view-modal {
  width: 100%;
  height: 100%;
  max-height: none;
  min-height: 0;
}

/* Etwas mehr Abstand als .modal-head: In dieser Kopfzeile stehen Titel und Untertitel neben-
   einander und brauchen Luft dazwischen. */
.cache-view-header {
  gap: 1rem;
}

.cache-view-header > div {
  display: flex;
  align-items: baseline;
  gap: 0.65rem;
}

.cache-view-title {
  font-stretch: 84%;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.cache-view-subtitle,
.cache-view-updated,
.cache-view-hint {
  color: var(--color-text-muted);
  font-size: 0.76rem;
}

/* .cache-view-close kommt vollständig aus .modal-close. */

.cache-view-body {
  padding: 1rem;
}

.cache-view-login {
  width: min(440px, 100%);
  margin: clamp(1rem, 8vh, 5rem) auto;
  padding: 1rem;
  border: 1px solid var(--color-border);
  background: var(--color-bg-sunken);
}

.cache-view-login label {
  display: block;
  margin-bottom: 0.45rem;
  color: var(--color-text-muted);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.cache-view-login-row {
  display: flex;
  gap: 0.5rem;
}

.cache-view-login input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.6rem 0.75rem;
  font: inherit;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
}

.cache-view-login input:focus {
  border-color: var(--brand);
  outline: 1px solid var(--brand);
}

.cache-view-hint {
  margin: 0.55rem 0 0;
}

.cache-view-status {
  min-height: 1.2rem;
  color: var(--color-text-muted);
  font-size: 0.8rem;
}

.cache-view-status.is-loading {
  color: var(--color-warning);
}

.cache-view-status.is-error {
  color: var(--color-danger);
}

.cache-view-content {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.cache-view-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.cache-view-actions {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}

/* Der Reset steht bei den Cache-Aktionen, bleibt durch das Warnrot aber klar vom rein lesenden
   Aktualisieren zu unterscheiden. */
.cache-reset-btn {
  border-color: var(--color-danger);
  color: var(--color-danger);
}

.cache-reset-btn:hover:not(:disabled) {
  background: var(--color-danger-surface);
  border-color: var(--color-danger);
  color: var(--color-danger);
}

.cache-reset-btn:focus-visible {
  outline-color: var(--color-danger);
}

/* auto-fit statt einer festen Spaltenzahl: Die Zahl der Kacheln hat sich mit dem Cache schon
   einmal geändert (die Quote "Ohne HP" kam dazu), und bei fest eingetragenen sechs Spalten stand
   die siebte allein in einer eigenen Zeile neben fünf leeren Feldern. So verteilen sie sich von
   selbst, egal wie viele es sind. */
.cache-view-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 1px;
  background: var(--color-border);
  border: 1px solid var(--color-border);
}

.cache-view-stat {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
  padding: 0.8rem;
  background: var(--color-bg-sunken);
}

.cache-view-stat-value {
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: 1.05rem;
  font-weight: 600;
  text-overflow: ellipsis;
}

.cache-view-stat-label {
  color: var(--color-text-muted);
  font-size: 0.68rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.cache-view-charts {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr);
  gap: 0.75rem;
}

.cache-view-chart-card {
  min-width: 0;
  border: 1px solid var(--color-border);
  background: var(--color-bg-sunken);
}

.cache-view-chart-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.65rem 0.8rem;
  border-bottom: 1px solid var(--color-border);
}

.cache-view-chart-head h2 {
  margin: 0;
  color: var(--color-text-muted);
  font-stretch: 84%;
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.cache-view-chart-head span {
  flex: 0 0 auto;
  color: var(--color-text-faint);
  font-size: 0.67rem;
}

.cache-view-hours-chart {
  padding: 0.75rem 0.75rem 0.5rem;
}

.cache-view-hours-total {
  margin-bottom: 0.45rem;
  color: var(--color-text-faint);
  font-size: 0.7rem;
}

.cache-view-hours-total strong {
  color: var(--color-text);
  font-family: var(--font-mono);
}

.cache-view-hours-scroll {
  overflow-x: auto;
  padding-bottom: 0.15rem;
}

.cache-view-hours-bars {
  display: grid;
  grid-template-columns: repeat(24, minmax(22px, 1fr));
  gap: 0.25rem;
  min-width: 650px;
  height: 170px;
}

.cache-view-hour {
  display: grid;
  grid-template-rows: 1rem minmax(0, 1fr) 1rem;
  min-width: 0;
  text-align: center;
}

.cache-view-hour-count,
.cache-view-hour-label {
  overflow: visible;
  color: var(--color-text-faint);
  font-family: var(--font-mono);
  font-size: 0.58rem;
  line-height: 1;
  white-space: nowrap;
}

.cache-view-hour-count {
  color: var(--color-text-muted);
}

.cache-view-hour-track {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  min-height: 0;
  border-bottom: 1px solid var(--color-border-strong);
  background: repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent calc(25% - 1px),
    var(--color-border) 25%
  );
}

.cache-view-hour-bar {
  width: min(100%, 18px);
  height: var(--cache-bar-height);
  background: var(--brand);
  transition: height 0.2s ease-out, background-color 0.15s ease;
}

.cache-view-hour:hover .cache-view-hour-bar {
  background: var(--color-warning);
}

.cache-view-types-chart {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 1rem;
  min-height: 218px;
  padding: 1rem;
}

.cache-view-donut {
  --donut-printer: var(--brand);
  --donut-computer: var(--color-success);
  --donut-unknown: var(--color-border-strong);
  position: relative;
  display: grid;
  place-items: center;
  width: 132px;
  height: 132px;
  border-radius: 50%;
  background: conic-gradient(
    var(--donut-printer) 0 var(--printer-end),
    var(--donut-computer) var(--printer-end) var(--computer-end),
    var(--donut-unknown) var(--computer-end) 100%
  );
}

.cache-view-donut::after {
  content: '';
  position: absolute;
  inset: 25px;
  border-radius: 50%;
  background: var(--color-bg-sunken);
}

.cache-view-donut > span {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.cache-view-donut strong {
  font-family: var(--font-mono);
  font-size: 1.1rem;
}

.cache-view-donut small {
  color: var(--color-text-faint);
  font-size: 0.62rem;
  text-transform: uppercase;
}

.cache-view-chart-legend {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.cache-view-chart-legend > div {
  display: grid;
  grid-template-columns: 0.55rem minmax(0, 1fr) auto;
  gap: 0.4rem;
  align-items: center;
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.72rem;
}

.cache-view-chart-legend > div:last-child {
  border-bottom: 0;
}

.cache-view-chart-legend i {
  width: 0.5rem;
  height: 0.5rem;
  background: var(--color-border-strong);
}

.cache-view-chart-legend i.is-printer {
  background: var(--brand);
}

.cache-view-chart-legend i.is-computer {
  background: var(--color-success);
}

.cache-view-chart-legend span {
  min-width: 0;
  overflow: hidden;
  color: var(--color-text-muted);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cache-view-chart-legend strong {
  font-family: var(--font-mono);
}

.cache-view-chart-legend small {
  grid-column: 3;
  color: var(--color-text-faint);
  font-size: 0.62rem;
}

/* ---------- Woher kamen die Antworten (Quellen des Caches) ---------- */

/* Die Karte spannt über beide Spalten des Diagrammrasters: Vier Anteile nebeneinander lesen sich
   als waagerechter Balken deutlich besser als in einem dritten kleinen Ring, und der Platz in der
   zweiten Zeile ist ohnehin frei. */
.cache-view-chart-card.is-wide {
  grid-column: 1 / -1;
}

.cache-view-sources {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.75rem 0.8rem;
}

.cache-view-source-head {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  color: var(--color-text-muted);
  font-size: 0.74rem;
}

.cache-view-source-head strong {
  color: var(--brand);
  font-family: var(--font-mono);
  font-size: 1.15rem;
}

.cache-view-source-bar {
  display: flex;
  overflow: hidden;
  height: 0.75rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

/* Die Breite kommt als --anteil aus dem JavaScript. flex-basis statt width, damit sich die
   Abschnitte den Balken ohne Rundungslücke am Ende teilen. */
.cache-view-source-part {
  flex: 0 0 var(--anteil, 0%);
  min-width: 2px;
  background: var(--color-border-strong);
}

.cache-view-source-part.is-fresh,
.cache-view-source-legend i.is-fresh {
  background: var(--color-success);
}

/* Veraltet ausgelieferte Antworten sind kein Fehler, aber auch kein Normalzustand — sie stehen
   für eine Störung bei HP und bekommen deshalb die Warnfarbe. */
.cache-view-source-part.is-stale,
.cache-view-source-legend i.is-stale {
  background: var(--color-warning);
}

.cache-view-source-part.is-refresh,
.cache-view-source-legend i.is-refresh {
  background: var(--brand);
}

.cache-view-source-part.is-hp,
.cache-view-source-legend i.is-hp {
  background: var(--color-border-strong);
}

.cache-view-source-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
}

.cache-view-source-legend span {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  color: var(--color-text-muted);
  font-size: 0.72rem;
}

.cache-view-source-legend i {
  width: 0.5rem;
  height: 0.5rem;
}

.cache-view-source-legend strong {
  color: var(--color-text);
  font-family: var(--font-mono);
}

.cache-view-source-legend small {
  color: var(--color-text-faint);
  font-size: 0.62rem;
}

/* ---------- Filter und Seitenteilung des Archivs ---------- */

.cache-view-archive-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 0.8rem;
  border-bottom: 1px solid var(--color-border);
}

.cache-view-archive-search {
  flex: 1 1 16rem;
  min-width: 0;
  padding: 0.35rem 0.5rem;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  font-family: inherit;
  font-size: 0.76rem;
}

.cache-view-archive-search:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -1px;
}

.cache-view-archive-info {
  color: var(--color-text-faint);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  white-space: nowrap;
}

.cache-view-pager {
  display: flex;
  gap: 0.35rem;
  margin-left: auto;
}

.cache-view-section,
.cache-view-lookup {
  border: 1px solid var(--color-border);
  background: var(--color-bg-sunken);
}

.cache-view-section > h2,
.cache-view-lookup > h3 {
  margin: 0;
  padding: 0.65rem 0.8rem;
  border-bottom: 1px solid var(--color-border);
  font-stretch: 84%;
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.cache-view-section > .cache-view-hint {
  margin: 0;
  padding: 0.55rem 0.8rem;
  border-bottom: 1px solid var(--color-border);
}

.cache-view-table-wrap {
  overflow-x: auto;
}

.cache-view-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.75rem;
}

.cache-view-table th,
.cache-view-table td {
  padding: 0.55rem 0.7rem;
  border-right: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
  text-align: left;
  vertical-align: top;
  white-space: nowrap;
}

.cache-view-table th:last-child,
.cache-view-table td:last-child {
  border-right: 0;
}

.cache-view-table tbody tr:last-child td {
  border-bottom: 0;
}

.cache-view-table th {
  color: var(--color-text-faint);
  background: var(--color-surface);
  font-size: 0.65rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.cache-view-table td:first-child,
.cache-view-table td:nth-child(2) {
  font-family: var(--font-mono);
}

.cache-view-table tbody tr:hover td {
  background: var(--color-surface-hover);
}

.cache-view-empty {
  margin: 0;
  padding: 0.9rem;
  color: var(--color-text-faint);
  font-size: 0.8rem;
}

.cache-view-lookups {
  display: grid;
  gap: 0.75rem;
}

.cache-view-lookup + .cache-view-lookup {
  margin-top: 0.75rem;
}

.cache-view-photo-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.75rem;
}

.cache-view-photo-facts span {
  padding: 0.4rem 0.55rem;
  color: var(--color-text-muted);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  font-size: 0.74rem;
}

.cache-view-photo-facts strong {
  color: var(--color-text);
  font-family: var(--font-mono);
}

@media (max-width: 760px) {
  .cache-view-summary {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .cache-view-charts {
    grid-template-columns: minmax(0, 1fr);
  }

  .cache-view-types-chart {
    grid-template-columns: auto minmax(0, 1fr);
  }

  .cache-view-login-row {
    flex-direction: column;
  }
}

/* ---------- Rechtstexte (Impressum, Datenschutz) ---------- */

/* Fließtext-Seiten teilen sich Kopf-/Fußzeile und .container mit dem Tool, brauchen aber
   eigene Regeln für Absätze, Zwischenüberschriften und Links — dafür gibt es sonst nirgends
   generische Vorgaben, weil das Tool selbst ausschließlich mit eigenen Komponenten arbeitet. */
.legal {
  max-width: 68ch;
}

.legal section + section {
  margin-top: 2.25rem;
  padding-top: 2.25rem;
  border-top: 1px solid var(--color-border);
}

.legal h2 {
  margin: 0 0 0.9rem;
  font-stretch: 82%;
  font-weight: 700;
  font-size: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text);
}

.legal p {
  margin: 0 0 0.9rem;
  color: var(--color-text-muted);
  line-height: 1.65;
}

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

/* Hervorhebungen im Fließtext heben sich sonst kaum ab, weil der Absatz ohnehin
   gedämpft ist — deshalb zusätzlich zur Fettung die hellere Textfarbe. */
.legal strong {
  font-weight: 600;
  color: var(--color-text);
}

.legal a {
  color: var(--brand);
  text-decoration-color: hsl(17 89% 58% / 0.4);
  text-underline-offset: 0.15em;
}

.legal a:hover {
  text-decoration-color: currentColor;
}

.legal .legal-placeholder {
  color: var(--color-warning);
}

/* ---------- Hilfsklassen ---------- */

/* Utility-Klasse: muss jede display-Regel schlagen, unabhängig davon, wo im Stylesheet
   die Regel des jeweiligen Elements steht (sonst gewinnt z.B. `.tabs { display: inline-flex }`). */
.hidden {
  display: none !important;
}

/* Tabellenspalten, die im aktuellen Datensatz durchgehend leer sind (HP liefert dort nur
   Platzhalter wie "-N/A-"). Eigene Klasse statt `.hidden`, damit Zeilen-Filter und
   Spalten-Sichtbarkeit sich nicht gegenseitig überschreiben.
   !important wie bei `.hidden`: In der Kartendarstellung (Media Queries am Dateiende) werden
   die Zellen über ihr data-col angesprochen, und solche Selektoren sind spezifischer als
   dieser hier — ohne !important käme eine leere Spalte auf dem Handy wieder zum Vorschein. */
.results-table th.col-hidden,
.results-table td.col-hidden {
  display: none !important;
}

/* ============================================================================
   Schmale Viewports (Tablet und Handy)

   Die Regeln stehen bewusst am Dateiende: Sie überschreiben die Desktop-Regeln oben und
   sollen das bei gleicher Spezifität allein über die Reihenfolge tun, ohne !important.

   Drei Stufen:
     <= 900px  Tablet — dieselbe Tabelle, nur dichter gesetzt, Reiterleiste darf umbrechen.
     <= 720px  Kartenansicht — die Tabelle wird zeilenweise zu Karten aufgelöst (siehe unten),
               die Reiter tragen ihre Kurzbeschriftung.
     <= 560px  Handy — einspaltige Formulare, schmalere Ränder, Vollbild-Foto.

   Dazu zwei Abfragen nach der Eingabeart (pointer/hover) statt nach der Breite: ein Touchscreen
   kann auch groß sein, und ein schmales Fenster hat trotzdem eine Maus.
   ============================================================================ */

/* ---------- <= 900px: Tabelle dichter, Reiter dürfen umbrechen ---------- */

@media (max-width: 900px) {
  .results-table {
    font-size: 0.84rem;
  }

  .results-table th,
  .results-table td {
    padding: var(--space-2);
  }

  .results-table th {
    font-size: 0.66rem;
    letter-spacing: 0.06em;
  }

  /* Fünf Reiter passen ausgeschrieben nicht mehr in eine Zeile. Umbrechen statt seitlich
     scrollen: ein waagerecht scrollender Reiter, den man nicht sieht, wird auch nicht benutzt. */
  .tabs {
    flex-wrap: wrap;
  }

  .tab-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 1 1 auto;
    text-align: center;
    padding: var(--space-2) var(--space-3);
  }

  /* Der Trennstrich vor den Ansichts-Reitern (QR/Galerie) setzt eine durchgehende Zeile voraus.
     Sobald umbrochen wird, landet er als loser Strich am Anfang der zweiten Zeile. */
  .tab-btn.tab-separated {
    margin-left: 0;
  }

  .tab-btn.tab-separated::before {
    display: none;
  }

  /* DEV-ONLY: Die Entwicklerwerkzeuge werden am Schreibtisch benutzt, nicht unterwegs. Auf
     Tablet und Handy belegt der Knopf nur eine der wenigen freien Ecken — weg damit (display:
     none nimmt ihn zugleich aus der Tab-Reihenfolge). */
  .dev-panel {
    display: none;
  }
}

/* ---------- <= 720px: aus jeder Tabellenzeile wird eine Karte ---------- */

/* Nebeneinander wären die Spalten auf einem Handy unlesbar schmal, und als seitlich scrollende
   Tabelle verliert man die Teilenummer aus dem Blick, sobald man nach rechts schiebt.
   Stattdessen wird jede Zeile zu einem Block:

     (TEILEART)                                    [Suche] [FE] [Foto] [kopieren]
     Teilenummer (dicktengleich) Beschreibung

   Die Teileart steht als kurzer Chip vor den Aktionsknöpfen statt als beschriftetes Kästchen:
   Sie ist meist ein einzelnes Wort und würde als Kästchen unnötig Platz von den Knöpfen
   abziehen. Die übrigen Beschriftungen wandern aus dem Tabellenkopf in die Zellen:
   app.js schreibt sie in data-label, ausgegeben werden sie hier per ::before. Bewusst nicht als
   Textknoten — sonst würden Filter und Kopieren die Beschriftung mit einsammeln. */
@media (max-width: 720px) {
  /* Kurzbeschriftung der Reiter (siehe index.html) */
  .tab-label-full {
    display: none;
  }

  .tab-label-short {
    display: inline;
  }

  .tab-btn {
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    padding: var(--space-2);
  }

  /* Kein waagerechtes Scrollen mehr nötig, sobald die Zeilen Blöcke sind. */
  .table-wrapper {
    overflow-x: visible;
  }

  /* Nebeneinander bliebe von jedem Wert nur ein Wortanfang übrig — untereinander bekommt jeder
     die volle Breite. */
  .device-copy-chip {
    width: 100%;
  }

  .results-table,
  .results-table tbody,
  .results-table tr,
  .results-table td {
    display: block;
  }

  /* Die Spaltenüberschriften stehen jetzt in den Zellen selbst. */
  .results-table thead {
    display: none;
  }

  .results-table tbody tr {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    /* Kleinerer Abstand quer (zwischen Chip und Knöpfen, zwischen den Knöpfen untereinander) als
       hoch (zwischen den Zeilen der Karte) — die vier Aktionsknöpfe sind schon für sich genommen
       breit genug, ein großzügiger Abstand dazwischen kostete nur Platz, den der Teileart-Chip
       gut gebrauchen kann. */
    gap: var(--space-2) 0.3rem;
    padding: var(--space-3);
    border-bottom: 1px solid var(--color-border);
  }

  .results-table tbody tr:last-child {
    border-bottom: none;
  }

  /* Umbruchpunkt zwischen der Kopfzeile (Teileart-Chip + Aktionsknöpfe) und dem Block aus
     Teilenummer und Beschreibung, unabhängig davon, welche Zellen in dieser Zeile gefüllt sind.
     Ein leeres Flex-Element über die volle Breite erzwingt die neue Zeile — Verlass auf
     flex-basis:100% allein reicht nicht, weil eine leere (und damit ausgeblendete) Nachbarzelle
     den Umbruch sonst überspringen würde. */
  .results-table tbody tr:not([data-device-row])::before {
    content: '';
    order: 6;
    flex: 1 0 100%;
    height: 0;
  }

  .results-table tbody tr:not([data-device-row])::after {
    content: '';
    order: 9;
    flex: 1 0 100%;
    height: 0;
  }

  .results-table tbody td {
    padding: 0;
    border-bottom: none;
  }

  /* Der Ausgleich für die erste Textzeile (siehe oben) gilt nur für die echte Tabelle. In der
     Karte steht nichts mehr in Spalten nebeneinander, an dem sich eine Textzeile ausrichten
     müsste — dort wäre er nur ein schiefer Abstand. Eigene Regel statt einer Media Query um den
     Block oben, weil die Kartenregel `padding: 0` weniger spezifisch ist und ihn sonst nicht
     zurücknimmt. */
  .results-table tbody td.part-cell,
  .results-table tbody td.desc-cell,
  .results-table tbody td.parttype-cell,
  .results-table tbody td.device-label,
  .results-table tbody td.device-desc,
  .results-table tbody td.device-value {
    padding-top: 0;
  }

  /* Spalten, die in dieser Zeile nichts enthalten, fallen ersatzlos weg — ein Kästchen mit
     Beschriftung, aber ohne Wert wäre schlechter als gar keines. (Spalten, die im *gesamten*
     Datensatz leer sind, hat app.js schon per .col-hidden entfernt.) */
  .results-table tbody td:empty {
    display: none;
  }

  /* Kopfzeile der Karte: die erkannte Teileart links als kurzer Chip, die Aktionsknöpfe rechts
     bündig. Die Teileart ist meist ein einzelnes Wort — dafür kein beschriftetes Kästchen, sondern
     ein kompakter, klar als Einordnung erkennbarer Chip, der den Buttons rechts genug Platz
     lässt, statt mit ihnen im Design zu verschwimmen.
     max-width ist absichtlich knapp bemessen: Die Zeile bricht sonst um, sobald Chip und Knöpfe
     zusammen nicht mehr in die Breite passen (die vier Fingerziele allein sind schon über 10rem
     breit). Statt umzubrechen, wird eine lange Teileart lieber abgeschnitten — die Knöpfe bleiben
     dafür immer in der Kopfzeile stehen, den vollen Wert gibt es weiterhin über den Tooltip. */
  .results-table tbody td.meta-cell.parttype-cell {
    order: 1;
    flex: 0 1 auto;
    min-width: 0;
    max-width: 42%;
    padding: 0.18rem 0.55rem;
    background: var(--color-bg-sunken);
    border: 1px solid var(--color-border);
    border-radius: 999px;
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--color-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Kein "TEILEART"-Label vor dem Wert — als Chip ist die Einordnung schon als solche erkennbar. */
  .results-table tbody td.meta-cell.parttype-cell::before {
    content: none;
  }

  /* Die Aktionszelle (Such- und Kopier-Knopf) ist der erste Aktionsknopf der Kopfzeile:
     margin-left:auto schiebt sie samt der restlichen Knöpfe an den rechten Rand, unabhängig
     davon, ob der Teileart-Chip da ist. */
  .results-table tbody td.actions-cell {
    order: 2;
    flex: 0 0 auto;
    width: auto;
    margin-left: auto;
    padding-right: 0;
  }

  .results-table tbody td.fe-cell {
    order: 3;
    flex: 0 0 auto;
    width: auto;
  }

  .results-table tbody td.photo-cell {
    order: 4;
    flex: 0 0 auto;
  }

  /* Direkt hinter dem Foto, wie am Desktop auch. Dieselbe Ordnungszahl wie die Kopier-Spalte
     darunter ist unbedenklich: Die gibt es nur in Device-Zeilen, und die haben weder Foto- noch
     Shop-Zelle. */
  .results-table tbody td.shop-cell {
    order: 5;
    flex: 0 0 auto;
  }

  /* Component BOM (Device-Zeilen) hat noch eine eigene Kopier-Spalte für sich alleine. */
  .results-table tbody td.copy-cell {
    order: 5;
    flex: 0 0 auto;
    width: auto;
    padding-right: 0;
  }

  /* Teilenummer und Beschreibung gehören zusammen und stehen deshalb in derselben Zeile, als
     wäre es ein einziger Satz — bleiben aber zwei eigene Zellen und damit einzeln antippbar zum
     Kopieren. Die Teilenummer behält nur so viel Breite wie ihr Inhalt braucht (flex: 0 1 auto),
     die Beschreibung nimmt den Rest der Zeile und bricht bei Bedarf in sich selbst um. */
  .results-table tbody td.part-cell {
    order: 7;
    flex: 0 1 auto;
    min-width: 0;
    font-family: var(--font-mono);
    font-size: 0.95rem;
    font-weight: 600;
    /* In der Karte darf die Nummer doch umbrechen (overflow-wrap: anywhere) — das nowrap der
       Tabelle wird hier zurückgenommen. Dort gibt es eine Spalte, die breiter werden kann, und
       notfalls den waagerechten Scroll; hier ist die Kartenbreite die Bildschirmbreite, und eine
       lange Nummer schöbe die ganze Seite seitlich hinaus. */
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .results-table tbody td.desc-cell {
    order: 8;
    flex: 1 1 auto;
    min-width: 6rem;
    font-size: 0.86rem;
    line-height: 1.4;
    color: var(--color-text-muted);
  }

  /* Die Enhanced-Beschreibung steht hinter der Basic-Beschreibung, wie am Desktop auch. Eigene
     Ordnungszahl, weil beide dieselbe Klasse .desc-cell tragen. */
  .results-table tbody td.desc-enhanced {
    order: 9;
  }

  /* Grundwerte für .meta-cell — greifen praktisch nur noch als Ergänzung zur spezifischeren
     .meta-cell.parttype-cell-Regel oben (Zeilenumbruch, falls die Teileart doch einmal nicht
     abgeschnitten wird). Stammt noch aus der Zeit, in der hier auch die Kategorie als eigenes,
     beschriftetes Kästchen stand — die gibt es nicht mehr, HP lieferte darin ohnehin fast immer
     nur "Not Available"/"N-A". */
  .results-table tbody td.meta-cell {
    order: 10;
    flex: 0 1 auto;
    min-width: 0;
    padding: 0.22rem 0.45rem;
    background: var(--color-bg-sunken);
    border: 1px solid var(--color-border);
    font-size: 0.78rem;
    line-height: 1.3;
    overflow-wrap: anywhere;
  }

  .results-table tbody td.meta-cell::before {
    content: attr(data-label);
    display: block;
    font-size: 0.56rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-text-faint);
  }

  /* Die Teileart ist nicht bei jedem Teil erkannt. In der Tabelle bleibt die Zelle dann einfach
     leer; als Karte stünde dort ein leeres Kästchen mit Beschriftung und ohne Inhalt — das sieht
     nach einem Fehler aus statt nach einer fehlenden Angabe. */
  .results-table tbody td.meta-cell:empty {
    display: none;
  }

  /* Gerätespezifische Informationen: Kennung als Überschrift, darunter der Wert, darunter
     die Beschreibung. Die ganze Zeile ist ohnehin als Ganzes zum Kopieren klickbar. */
  .results-table tbody tr[data-device-row] {
    gap: 0.1rem 0.45rem;
  }

  /* Zeilen ohne Wert (siehe renderDeviceTable, z.B. "Unique Product Serial Number" bei einer
     Modellnummernsuche) sind hier nichts als eine Kennung ohne Inhalt — in der Karte fällt das
     stärker auf als in der Tabelle am Desktop, wo sie stehen bleiben. */
  .results-table tbody tr.device-row-empty {
    display: none;
  }

  .results-table tbody td.device-label {
    order: 1;
    flex: 1 1 100%;
    font-family: var(--font-mono);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-text-faint);
  }

  .results-table tbody td.device-value {
    order: 2;
    flex: 1 1 100%;
    font-family: var(--font-mono);
    font-size: 0.92rem;
    overflow-wrap: anywhere;
  }

  .results-table tbody td.device-desc {
    order: 3;
    flex: 1 1 100%;
    font-size: 0.78rem;
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
  }

  /* In der Kartenansicht sind die Zellen ansonsten getrennt: Beim Sprung zum Ersatzteil blinkt
     deshalb die ganze Zeile, nicht die einzelnen Zellen (deren Kästchen würden sonst einzeln
     aufleuchten). */
  .results-table tbody tr.flash-highlight {
    animation: flashHighlight 0.8s ease-in-out 2;
  }

  .results-table tbody tr.flash-highlight td {
    animation: none;
  }

  /* Größere Fingerziele für die Aktionsknöpfe in der Kartenkopfzeile. Das durchgestrichene
     Foto-Icon ist kein Ziel, bekommt denselben Abstand aber trotzdem — sonst säße es in der
     Kartenkopfzeile enger als das grüne und die Zeilen liefen auseinander. */
  .copy-combo-btn,
  .part-search-icon-btn,
  .fe-icon-btn,
  .photo-icon-btn,
  .photo-icon-none,
  .shop-link-btn {
    width: var(--control-size);
    height: var(--control-size);
    padding: 0;
  }

  /* Im Trefferkopf teilen sich nur die kurze Fundart und das Herkunfts-Abzeichen die oberste
     Zeile. Modellname und Nummern bekommen darunter jeweils die volle Breite. So bleibt die
     Quelle auch am Handy oben rechts sichtbar, ohne lange Modellnamen einzuengen. */
  .result-summary {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 0.75rem;
    row-gap: 0.4rem;
  }

  /* Die beiden Wrapper werden für das mobile Raster durchlässig: Dadurch kann das Abzeichen
     neben der Fundart sitzen, obwohl es im Markup zusammen mit den Nummern gruppiert bleibt. */
  .result-summary-main,
  .result-summary-side {
    display: contents;
  }

  .result-summary-head {
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
    margin-bottom: 0;
  }

  .result-summary-names {
    grid-column: 1 / -1;
    grid-row: 2;
  }

  .result-summary-side .summary-source {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    align-self: start;
  }

  .result-summary-side .summary-cache {
    /* Mittig und nicht am oberen Rand: Die Fläche ist hier auf 1.8rem vergrößert (Tippziel),
       das Icon säße mit flex-start eine halbe Zeile über dem gleich hohen Reload-Knopf daneben —
       genau die Verschiebung, die im Trefferkopf auffiel. */
    align-items: center;
    min-width: 1.8rem;
    min-height: 1.8rem;
    padding-left: 0.5rem;
    cursor: pointer;
  }

  /* Mobil bleibt die Herkunft dauerhaft beim Icon; der Text erscheint per Tipp im Toast und kann
     deshalb weder die Fundart links einengen noch den restlichen Trefferkopf verschieben. */
  .summary-cache-text {
    display: none;
  }

  .result-summary-facts {
    grid-column: 1 / -1;
    grid-row: 3;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75rem 1rem;
    margin-top: 0.5rem;
  }

  /* Enger gesetzt, damit die Beschriftung in einer halben Spaltenbreite auf eine Zeile passt —
     sonst rutscht der Wert darunter gegenüber seinem Nachbarn nach unten und die beiden
     Angaben stehen nicht mehr auf einer Linie. */
  .summary-fact dt {
    font-size: 0.6rem;
    letter-spacing: 0.05em;
  }

  .filter-wrapper {
    flex-wrap: wrap;
    gap: 0.5rem 0.75rem;
  }

  /* Das Filterfeld (samt Trefferzahl darin) bekommt eine eigene Zeile; die Kategorie-Auswahl
     steht darunter. */
  .filter-input-wrap {
    flex: 1 1 100%;
  }

  /* Die Kategorie-Liste rückt in die Zeile unter das Filterfeld und nutzt dort die ganze Breite.
     Die Desktop-Aufteilung in 25 % / Rest gilt hier bewusst nicht, weil beide Bedienelemente
     bereits untereinander stehen. */
  .category-select {
    flex: 1 1 100%;
    max-width: none;
  }
}

/* ---------- <= 560px: Handy ---------- */

@media (max-width: 560px) {
  /* Beide Selektoren nötig: :root[data-width="full"] weiter oben ist spezifischer als :root
     allein, und die dort gemerkte Einstellung kann aus einer früheren Desktop-Sitzung stammen
     (der Breitenumschalter selbst ist auf dieser Breite ausgeblendet). */
  :root,
  :root[data-width="full"] {
    --gutter: 1rem;
  }

  .site-header .wrap {
    min-height: 52px;
    gap: 0.75rem;
  }

  .container {
    padding-top: 1.5rem;
    padding-bottom: 2.25rem;
  }

  .page-head {
    margin-bottom: 1.25rem;
  }

  /* Das Suchfeld bekommt die ganze erste Zeile. display: contents lässt Scanner und
     Zwischenablage in der zweiten Zeile zu Geschwistern des normalen Suchen-Knopfs werden,
     während Markup und Desktop-Darstellung unverändert zusammenbleiben. */
  .search-form {
    position: relative;
    flex-wrap: wrap;
  }

  .search-input-wrap {
    display: contents;
  }

  .search-form input,
  .search-input-wrap.has-scanner input {
    flex: 1 0 100%;
    width: 100%;
    padding-right: calc(var(--control-size) + var(--space-2));
  }

  /* Hilfe und × bleiben in der ersten Zeile am rechten Rand des nun vollen Suchfelds. */
  .search-help-btn,
  .search-clear-btn,
  .search-input-wrap.has-scanner .search-help-btn,
  .search-input-wrap.has-scanner .search-clear-btn {
    top: 0;
    right: 0;
    bottom: auto;
    height: var(--control-size);
  }

  /* Die verfügbaren Aktionen teilen sich die zweite Zeile automatisch: zu dritt je ein Drittel,
     ohne Kamera zu zweit je die Hälfte. Derselbe gedeckte Grund wie das Suchfeld (--color-bg-sunken)
     hält sie optisch als eine zusammenhängende Gruppe zusammen statt als lose Einzelknöpfe. */
  .search-scan-btn,
  .search-paste-btn {
    position: static;
    flex: 1 1 0;
    width: auto;
    min-width: 0;
    min-height: var(--control-size);
    gap: 0.4rem;
    padding: 0.5rem;
    border: 1px solid var(--color-border-strong);
    background: var(--color-bg-sunken);
    font-family: inherit;
    font-size: 0.72rem;
    font-weight: 700;
    line-height: 1.15;
    text-transform: uppercase;
    letter-spacing: 0.03em;
  }

  .search-action-label {
    display: inline;
    white-space: normal;
  }

  .search-form > .btn {
    flex: 1 1 0;
    min-width: 0;
    padding-inline: 0.5rem;
    font-size: 0.72rem;
    letter-spacing: 0.03em;
  }

  /* 16px ist die Grenze, unterhalb derer iOS Safari beim Antippen eines Eingabefelds in die
     Seite hineinzoomt und danach seitlich verschoben stehen bleibt. */
  .search-form input,
  .filter-wrapper input,
  .models-filter input {
    font-size: 16px;
  }

  /* ---------- Geklebte Steuerung: auf das Filterfeld zusammenklappen ---------- */

  /* Der volle Kopf ist hier rund 100px hoch (Reiterzeile plus zweizeilige Filterleiste).
     Zusammen mit der 52px hohen Kopfzeile klebte sonst ein Viertel eines 640px hohen Displays
     dauerhaft fest. Sobald er klebt, schrumpft er deshalb auf Reiter- und Filterzeile zusammen;
     der Pfeil daneben holt die Kategorie-Auswahl zurück. Die Trefferzahl steht im Filterfeld und
     bleibt damit auch eingeklappt sichtbar.

     Die Reiterzeile klappt bewusst NICHT mit weg (anders als früher): Ohne sie wüsste man beim
     Kleben nicht mehr, in welcher Liste (Advanced/General/BOM/Galerie) man gerade steht, und
     käme dort auch nicht mehr weg. Nur die Kategorie-Auswahl — die seltener gebrauchte, größere
     Fläche — spart beim Zusammenklappen Platz.

     Warum erst hier und nicht schon ab 720px: Zwischen 561px und 720px hängt die Kategorie-Liste
     noch als absolut positioniertes Menü am Knopf und würde vom overflow: hidden weiter unten
     abgeschnitten. Ab dieser Breite ist sie ein Blatt am unteren Fensterrand (position: fixed)
     und damit unbeschnitten — fixed entkommt der Beschneidung eines Vorfahren.

     Die Kategorie-Auswahl klappt über die verschachtelte grid-template-rows im .filter-wrapper
     weiter unten weg (1fr/0fr statt max-height, aus demselben Grund wie früher hier: die echte
     Höhe auf den Pixel, ohne eine geratene Obergrenze). */
  .results-head {
    --klappdauer: 0.24s;
    display: grid;
    grid-template-rows: auto auto;
    transition: margin-bottom var(--klappdauer) ease;
  }

  .tabs {
    min-height: 0;
    overflow: hidden;
  }

  /* Die Filterzeile wird zum Raster: Zeile 1 Eingabefeld und Aufklappknopf, Zeile 2 der Kasten mit
     der Kategorie-Auswahl. Das Eingabefeld überspannt dabei beide Spalten; der Knopf liegt nur im
     klebenden Zustand über seiner rechten Kante. So bleibt das Feld im normalen mobilen Zustand
     wirklich voll breit. Der Zeilenabstand klappt mit weg — sonst bliebe von der verschwundenen
     Zeile eine Lücke in genau seiner Höhe zurück. */
  .filter-wrapper {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto 1fr;
    /* Hebt das align-items: center der Flex-Fassung auf. Ohne das behielte der Kasten in Zeile 2
       seine Inhaltshöhe, statt sich auf die 0fr hohe Zeile zu ducken — er würde also gar nicht
       eingeklappt, sondern stünde nur über den Rand hinaus. */
    align-items: stretch;
    column-gap: 0.5rem;
    row-gap: 0.5rem;
    transition:
      grid-template-rows var(--klappdauer) ease,
      row-gap var(--klappdauer) ease;
  }

  .results-head.is-eingeklappt .filter-wrapper {
    grid-template-rows: auto 0fr;
    row-gap: 0;
  }

  .filter-input-wrap {
    grid-column: 1 / -1;
    grid-row: 1;
  }

  /* Der Pfeil liegt bei voller Feldbreite über dem rechten Rand. Erst wenn er sichtbar wird,
     reserviert das Feld dafür Innenraum, damit Trefferzahl und Löschknopf nicht darunter liegen. */
  .results-head.is-einklappbar .filter-input-wrap {
    padding-right: 3rem;
  }

  /* Auf Handybreite steht das Feld allein in seiner Zeile, muss sich die Breite aber mit bis zu
     vier Knöpfen und der Trefferzahl teilen. Schmalere Knöpfe und eine niedrigere Untergrenze für
     die Eingabe halten das auch auf kleinen Bildschirmen (ab ca. 340px) noch aus, ohne dass
     etwas umbricht. */
  .filter-wrapper input {
    min-width: 3rem;
  }

  .filter-help-btn,
  .filter-scan-btn,
  .filter-paste-btn,
  .filter-clear-btn {
    width: 1.9rem;
  }

  /* Dieselbe Enge im Modell-Querverweis: Feld, vier Knöpfe und der abgesetzte Suchen-Knopf
     teilen sich eine Zeile. */
  .models-filter input {
    min-width: 3rem;
  }

  .models-filter-btn {
    padding-inline: 0.6rem;
    font-size: 0.72rem;
    letter-spacing: 0.03em;
  }

  .filter-meta {
    grid-column: 1 / -1;
    grid-row: 2;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.75rem;
    min-height: 0;
    overflow: hidden;
    transition: visibility var(--klappdauer);
  }

  .results-head.is-eingeklappt .filter-meta {
    visibility: hidden;
  }

  /* Der Knopf gehört zum geklebten Zustand: Steht die Liste noch an ihrem Anfang, gibt es
     nichts hervorzuholen. Die zweite Grid-Spalte bleibt für ihn vorhanden; das Filterfeld
     überspannt sie aber, damit seine sichtbare Fläche nicht schmaler wird. */
  .results-head-toggle {
    grid-column: 2;
    grid-row: 1;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--control-size);
    z-index: 1;
    padding: 0;
    color: var(--color-text-muted);
    /* Gleiche dunkle Bedienelement-Fläche wie das Filterfeld daneben. */
    background: var(--color-bg-sunken);
    border: 1px solid var(--color-border);
    cursor: pointer;
    visibility: hidden;
    opacity: 0;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
    -webkit-user-select: none;
    user-select: none;
    transition:
      opacity var(--klappdauer) ease,
      visibility var(--klappdauer),
      color 0.15s ease,
      background-color 0.15s ease,
      border-color 0.15s ease;
  }

  .results-head.is-einklappbar .results-head-toggle {
    visibility: visible;
    opacity: 1;
  }

  /* Dasselbe Dreieck aus Rahmenkanten wie an der Kategorie-Auswahl daneben, damit beide als
     dieselbe Art Bedienelement gelesen werden. Eingeklappt zeigt es nach unten ("da ist mehr"),
     aufgeklappt dreht es sich um. */
  .results-head-toggle-caret {
    width: 0;
    height: 0;
    border-left: 0.3rem solid transparent;
    border-right: 0.3rem solid transparent;
    border-top: 0.36rem solid currentColor;
    transition: transform 0.12s ease;
  }

  .results-head:not(.is-eingeklappt) .results-head-toggle-caret {
    transform: rotate(180deg);
  }

  /* Mit der eingeklappten Kategorie-Auswahl verschwände sonst auch der Hinweis darauf, dass die
     Liste überhaupt eingegrenzt ist. Der Knopf übernimmt ihn in derselben Auszeichnung, die der
     Öffner der Auswahl im aktiven Zustand trägt (.category-select-btn.is-active) — ein eigenes
     Signal wäre ein zweites für dieselbe Sache. Die Klasse setzt js/klebekopf.js. */
  .results-head-toggle.hat-kategoriefilter {
    color: var(--brand);
    background: var(--brand-faint);
    border-color: var(--brand);
  }

  .results-head-toggle:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
  }

  /* Auf Handybreite hängt die Kategorienliste nicht mehr am Knopf, sondern steht als Blatt am
     unteren Fensterrand. Zwei Gründe: Ein am Knopf hängendes Menü landet auf einem hohen, schmalen
     Bildschirm regelmäßig halb außerhalb, und der untere Rand ist die einzige Stelle, die man mit
     dem Daumen sicher erreicht — oben am Gerät zielt man an einer langen Liste vorbei.
     Der zweite Selektor hebt die Aufklapprichtung von oben wieder auf: Sie wird von app.js für
     die Desktop-Ansicht gesetzt und ergibt hier keinen Sinn mehr. */
  .category-select-panel,
  .category-select-panel.is-above {
    position: fixed;
    top: auto;
    right: 0;
    bottom: 0;
    left: 0;
    width: auto;
    min-width: 0;
    max-width: none;
    /* Höchstens zwei Drittel des Fensters: Der Rest der Trefferliste bleibt sichtbar, damit man
       sieht, worauf sich die Auswahl auswirkt. Das gilt nur, solange die Bildschirmtastatur zu
       ist — sobald das Suchfeld sie holt, überschreibt updateCategorySheetForKeyboard() in
       js/kategoriefilter.js sowohl bottom als auch max-height inline: bottom:0 bezieht sich auf
       das Layout-Viewport und bliebe sonst unbeirrt hinter der Tastatur stehen. */
    max-height: min(68vh, 30rem);
    border: none;
    border-top: 1px solid var(--color-border-strong);
    box-shadow: 0 -12px 40px hsl(0 0% 0% / 0.5);
  }

  .category-select-backdrop {
    display: block;
  }

  .category-select-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.6rem 0.9rem;
    border-bottom: 1px solid var(--color-border);
  }

  /* Gleiche Auszeichnung wie die Überschrift des Verlaufsmenüs. */
  .category-select-head-title {
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--brand);
  }

  .category-select-close {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    /* Negative Ränder: Die Fläche wächst auf Fingergröße, ohne die Kopfzeile auseinanderzuziehen. */
    width: 2.75rem;
    height: 2.75rem;
    margin: -0.6rem -0.6rem -0.6rem 0;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--color-text-muted);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
  }

  .category-select-close:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
  }

  /* Im Blatt ist Platz — größere Schrift und mehr Luft, damit man die Einträge im Vorbeiwischen
     lesen und ohne Zielen treffen kann. */
  .category-select-option {
    padding: 0.7rem 0.9rem;
    font-size: 0.95rem;
  }

  .category-select-count {
    font-size: 0.82rem;
  }

  /* ---------- Ersatzteilkarte: alle Aktionen hinter einem Knopf ---------- */

  /* Bis 720px stehen Lupe, Kopieren, FE und Foto nebeneinander in der Kopfzeile der Karte (siehe
     die Abfrage weiter oben). Ab Handybreite passt das nicht mehr: Die vier Fingerziele sind
     zusammen über 10rem breit, der Teileart-Chip daneben musste deshalb bei 42 % abgeschnitten
     werden, und beim Scrollen mit dem Daumen trifft man leicht den Nachbarknopf. Hier wird
     daraus:

       Teilenummer Beschreibung                    │
       [TEILEART]                                  │ ⋮

     Rechts eine über die volle Kartenhöhe reichende Spur mit einem einzigen Knopf; er öffnet das
     Zeilenmenü (js/zeilenmenue.js). Der Platz dafür ist am Zeilen-Padding reserviert, damit weder
     eine lange Beschreibung noch ein langer Chip hineinlaufen kann — genau darum braucht der Chip
     hier auch keine max-width mehr.

     Gescopet auf #results-body: Die Kartenregeln oben gelten über .results-table auch für die
     Modell-Liste des Querverweises, die diesen Umbau nicht bekommt (dort steht nur eine Lupe).
     Die ID schlägt zugleich die Spezifität der 720px-Regeln, ohne die Reihenfolge im Stylesheet
     ausnutzen zu müssen. */
  /* Kein Flex-Container mehr, sondern ein Block mit laufendem Text: Nur so stehen Teilenummer und
     Beschreibung wirklich als ein Satz nebeneinander und die Beschreibung läuft bei einer langen
     Nummer unter ihr weiter. Als Flex-Elemente wäre die Beschreibung immer dann komplett in die
     nächste Zeile gerutscht, wenn sie nicht am Stück in den Rest der Zeile passte — bei einer
     Beschreibung von HP also praktisch immer.
     Die beiden Umbrüche der 720px-Fassung entfallen damit; die Reihenfolge im Dokument stimmt
     ohnehin (Nummer, Beschreibung, Teileart), und die Aktionszelle steht absolut. */
  #results-body tr:not([data-device-row]) {
    display: block;
    position: relative;
    padding-right: calc(var(--control-size) + var(--space-3));
  }

  #results-body tr:not([data-device-row])::before,
  #results-body tr:not([data-device-row])::after {
    content: none;
  }

  /* Zwei Zellen, ein Satz: Das Leerzeichen zwischen ihnen ist der Zeilenumbruch im Markup, den
     der Browser im Textfluss zu genau einem Leerzeichen zusammenzieht. Getrennt bleiben sie
     trotzdem — jede ist für sich antippbar und kopiert ihren eigenen Wert. */
  #results-body td.part-cell,
  #results-body td.desc-cell {
    display: inline;
  }

  /* Der Umbruch samt dem kleinen Abstand darüber: ein Blockkasten im laufenden Text. Jede der
     beiden Beschreibungen schließt damit ihre Zeile ab — in der Karte stehen also untereinander:
     Teilenummer + Basic-Beschreibung, darunter die Enhanced-Beschreibung, darunter die Chips.

     Eine LEERE Enhanced-Beschreibung bleibt ausnahmsweise stehen: Sie ist die letzte Angabe vor
     den Chips, und ohne sie nähme td:empty den Umbruch mit weg — die Chips rutschten hinter die
     Teilenummer. */
  #results-body td.desc-enhanced:empty {
    display: inline;
  }

  /* Die Basic-Beschreibung dagegen verschwindet leer samt ihrem Umbruch, sonst stünde in der
     Karte eine leere Zeile zwischen Teilenummer und Enhanced-Beschreibung. Ist die Spalte im
     ganzen Datensatz leer (General-Liste), ist sie ohnehin schon per .col-hidden weg. */
  #results-body td.desc-basic:empty {
    display: none;
  }

  #results-body td.desc-cell::after {
    content: '';
    display: block;
    height: var(--space-1);
  }

  /* Teileart-Chip in der zweiten Zeile. Die max-width der 720px-Fassung kann weg: Sie hielt den
     Chip von den vier Knöpfen daneben frei, und die stehen jetzt im Menü. */
  #results-body td.meta-cell {
    display: inline-block;
    vertical-align: middle;
    max-width: 100%;
    margin-right: 0.3rem;
  }

  /* Leer kommt bei ihr weiterhin nichts ins Bild. Die ID hier schlägt die :empty-Regel aus dem
     alten 720px-Block (dieselbe Spezifität wie die Zeile darüber), die sonst noch das leere
     Chip-Kästchen mit Rahmen und Hintergrund, aber ohne Text zeigen würde. */
  #results-body td.meta-cell:empty {
    display: none;
  }

  /* Die Aktionszelle wird zur Spur am rechten Rand. Absolut positioniert heißt hier: an der
     Innenkante des Zeilen-Paddings, also über die volle Höhe der Karte — ein Ziel, das man mit
     dem Daumen nicht verfehlen kann. Die feine Linie links macht sichtbar, dass diese Spur dem
     Knopf gehört und kein Text dort hineinreicht. */
  #results-body td.actions-cell {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    width: auto;
    margin: 0;
    padding: 0;
    border-left: 1px solid var(--color-border);
  }

  /* Die Knöpfe selbst stehen jetzt im Menü. Sie bleiben trotzdem im Dokument: Das Menü baut seine
     Einträge aus ihnen auf, und die Foto-Zelle bleibt das Ziel der nachlaufenden Fotoprüfung
     (siehe setzePhotoZelle in js/tabelle.js). */
  #results-body td.actions-cell > .part-search-icon-btn,
  #results-body td.actions-cell > .copy-combo-btn,
  #results-body td.fe-cell,
  #results-body td.photo-cell,
  #results-body td.shop-cell {
    display: none;
  }

  #results-body td.actions-cell > .row-menu-btn {
    display: flex;
    width: var(--control-size);
    height: 100%;
  }

  /* Solange das Menü offen steht, ist markiert, zu welcher Zeile es gehört — es steht neben ihr,
     nicht über ihr, und ohne diese Markierung wäre bei zwei ähnlichen Zeilen untereinander nicht
     sicher zu sagen, welche gemeint ist. */
  #results-body tr.row-menu-active {
    background: var(--brand-faint);
  }

  .status[aria-busy="true"] {
    padding: 1.75rem 0.5rem;
  }

  .empty-state {
    padding: 1.5rem 0.5rem 2.5rem;
  }

  .empty-state-tip {
    font-size: 1.02rem;
  }

  .result-summary,
  .product-select {
    padding: 0.85rem 0.9rem;
  }

  .result-summary-name {
    font-size: 1.05rem;
  }

  .product-select-options {
    max-height: 16rem;
  }

  /* Sperrsatz und Innenabstand kosten bei den langen Modellnamen jeweils eine ganze Zeile
     zusätzlich — auf dem Handy beides etwas enger. */
  .product-select-option {
    padding: 0.55rem 0.7rem;
    letter-spacing: 0.03em;
  }

  /* Kleinere Kacheln: bei 170px Mindestbreite bliebe auf 360px nur eine einzige Spalte übrig. */
  .gallery-grid {
    grid-template-columns: repeat(auto-fill, minmax(125px, 1fr));
    gap: 0.7rem;
  }

  .gallery-thumb {
    height: 105px;
  }

  .qr-grid {
    grid-template-columns: 1fr;
    gap: 0.85rem;
  }

  .qr-section {
    padding: 1.1rem 0.85rem;
  }

  /* Feste 220px passen samt Innenabstand nicht mehr in ein 320px breites Fenster. */
  .qr-image {
    width: min(220px, 62vw);
    height: auto;
    aspect-ratio: 1;
  }

  /* Das Foto bekommt das ganze Fenster: Auf dieser Breite ist ein schwebendes Fenster mit
     Rand ringsum verschenkter Platz — und genau der fehlt beim Betrachten des Teils. */
  .photo-modal-overlay {
    padding: 0.5rem;
  }

  .photo-modal {
    /* Höhe des Zoom-Streifens unten. Steht als Variable, weil sie an zwei Stellen gebraucht wird:
       einmal als freigehaltener Platz (padding-bottom), einmal als Höhe der Leiste selbst.
       Gerechnet aus der Knopfhöhe plus je 0.75rem Luft darüber und darunter — knapper sah es aus,
       als klebten die Knöpfe an ihrer Trennlinie. Mit dem Finger sind die Knöpfe größer
       (--control-size, siehe @media (pointer: coarse) weiter unten); über --zoomknopf wächst die
       Leiste dann mit, statt die Luft aufzubrauchen. */
    --zoomknopf: 2rem;
    --zoomleiste: calc(var(--zoomknopf) + 1.5rem);

    width: 100%;
    height: 100%;
    min-width: 0;
    /* Bezugspunkt für die Zoom-Leiste unten (position: absolute). */
    position: relative;
    padding-bottom: var(--zoomleiste);
  }

  /* Die Zoom-Bedienung wandert auf Handybreite an den unteren Rand. Oben nahm sie der Überschrift
     den Platz; unten liegt sie in dem Streifen, den ein hochkant gehaltenes Gerät unter einem eher
     quadratischen Foto ohnehin frei lässt — und der Daumen erreicht sie, ohne dass die Hand über
     dem Bild liegt.
     Absolut positioniert und nicht umsortiert: Die Gruppe steckt in der Kopfzeile und ist damit
     kein Geschwister des Bildbereichs — "order" käme gar nicht an sie heran. Den Platz hält das
     Modal mit padding-bottom frei, deshalb verdeckt die Leiste nichts; die Trennlinie oben
     markiert sie wie die Kopfzeile als eigenen Abschnitt. */
  .photo-modal-zoom {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: var(--zoomleiste);
    justify-content: center;
    gap: 0.4rem;
    border-top: 1px solid var(--color-border);
    background: var(--color-surface);
  }

  .photo-modal-header {
    padding: 0.5rem 0.5rem 0.5rem 0.75rem;
    /* Überschrift samt Zähler und der Schließen-Knopf passen nebeneinander (die Zoom-Knöpfe stehen
       hier unten); wird es enger — sehr schmales Gerät, große Schrift —, rutscht der
       Schließen-Knopf in eine zweite Zeile, statt die Überschrift aus dem Fenster zu drängen. */
    flex-wrap: wrap;
  }

  /* Etwas kleiner und enger gesetzt: So bleibt neben dem Schließen-Knopf Platz für die
     Überschrift — sie musste hier früher ganz weichen, als die Teilenummer noch daneben stand. */
  .photo-modal-title {
    font-size: 0.7rem;
    letter-spacing: 0.06em;
  }

  /* Der Zähler bleibt wie am Desktop in derselben Zeile wie die Überschrift, durch den Punkt von
     ihr getrennt: Seit die Zoom-Knöpfe unten stehen, teilt sich der Kopf den Platz nur noch mit
     dem Schließen-Knopf — da ist für beides Raum. (Bei sehr großer Schrift greift weiterhin das
     flex-wrap der Kopfzeile.) Nur die Schriftgröße zieht mit der kleineren Überschrift mit. */
  .photo-modal-counter {
    font-size: 0.7rem;
  }

  /* Zwei Zeilen in einem Raster: links das Kopiersymbol über beide, rechts daneben die Nummer und
     darunter die Beschreibung — beide beginnen an genau derselben Kante.
     Nebeneinander geht hier nicht auf: Die Beschreibung passt bei ihrer natürlichen Breite nicht
     neben die Nummer, rutschte als Ganzes in eine zweite Flex-Zeile und begann dann ganz links
     unter dem Kopiersymbol — genau der Versatz, der auffiel. Fließtext ("Nummer Leerzeichen
     Beschreibung", beim Umbruch zurück an die Spaltenkante) ist keine Alternative: Ein <button>
     ist ein atomarer Inline-Kasten, sein Text bricht nie über die eigene Kastengrenze hinaus.
     Der Zeilenabstand kommt aus der Polsterung der Knöpfe (row-gap 0) — fehlt eine der beiden
     Angaben, bleibt ihre Zeile dadurch wirklich ohne Höhe. */
  .photo-modal-meta {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    justify-items: start;
    align-items: start;
    gap: 0 0.5rem;
    padding: 0.6rem 0.5rem;
  }

  /* Über beide Zeilen und mittig darin — das Symbol kopiert beide Angaben zusammen, oben
     ausgerichtet sähe es nur der Nummer zugeordnet aus. Überschreibt das flex-start, das der
     Knopf in der einzeiligen Desktop-Fassung braucht. */
  .photo-modal-copy {
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: center;
  }

  /* Ohne Rahmen und ohne seitliche Polsterung: Die beiden Kästen ließen die Angaben wie
     Eingabefelder aussehen, dabei ist es Text, den man antippen kann. Getrennte Knöpfe bleiben
     sie trotzdem — jeder kopiert weiterhin für sich, und das Antippen färbt den Text.
     Die Polsterung nach oben und unten bleibt: Sie vergrößert die Trefferfläche, ohne sichtbar
     zu sein, und hält zugleich die beiden Zeilen auseinander. */
  .photo-modal-part,
  .photo-modal-desc {
    padding: 0.3rem 0;
    border-width: 0;
  }

  /* Höher spezifiziert, weil die Regel für Fingerbedienung (@media (pointer: coarse)) weiter
     unten im Stylesheet steht und sonst gewönne: Ihre 2.75rem hohen Kästen rissen die beiden
     Zeilen auseinander, sobald sie keinen Rahmen mehr haben, an dem man sie als Ziel erkennt. */
  .photo-modal-meta .photo-modal-part,
  .photo-modal-meta .photo-modal-desc {
    min-height: 0;
  }

  /* Eine sehr lange Nummer darf in der Spalte umbrechen, statt sie zu sprengen — dann aber
     linksbündig und nicht in der Knopf-Voreinstellung zentriert. */
  .photo-modal-part {
    grid-column: 2;
    grid-row: 1;
    text-align: left;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .photo-modal-desc {
    grid-column: 2;
    grid-row: 2;
    font-size: 0.8rem;
  }

  .photo-modal-body {
    padding: 0.5rem;
  }

  .site-footer {
    padding-block: 1.75rem 2rem;
  }
}

/* ---------- <= 380px: sehr schmale Geräte ---------- */

@media (max-width: 380px) {
  /* Ab hier reicht die Breite für ein Kästchen neben dem Rest oft nicht mehr; es darf dann eine
     eigene Zeile einnehmen, statt auf zwei Zeilen umzubrechen. */
  .results-table tbody td.meta-cell {
    flex: 1 1 100%;
  }

  /* Auch hier bleiben Serien- und Modellnummer nebeneinander statt übereinander: Untereinander
     kostet der Trefferkopf eine ganze Zeile mehr, und die beiden Werte sind kurz genug, dass
     sie sich eine 360px-Zeile teilen können — enger gesetzt und mit kleinerem Wert. */
  .result-summary-facts {
    gap: 0.5rem 0.75rem;
  }

  .summary-fact .summary-value {
    font-size: 0.9rem;
  }

  .to-top,
  .dev-panel-toggle {
    width: var(--control-size);
    height: var(--control-size);
  }
}

/* ---------- Bedienung per Finger statt Zeiger ---------- */

@media (pointer: coarse) {
  /* Auf Touch-Geräten wachsen nur die Trefferflächen; Farben, Rahmen und Symbole bleiben wie in
     der normalen Ansicht. */
  .copy-combo-btn,
  .part-search-icon-btn,
  .fe-icon-btn,
  .photo-icon-btn,
  .photo-icon-none,
  .shop-link-btn,
  .photo-zoom-btn {
    width: var(--control-size);
    height: var(--control-size);
    padding: 0;
  }

  .photo-zoom-level,
  .photo-modal-part,
  .photo-modal-desc {
    min-height: var(--control-size);
  }

  /* Die Zoom-Leiste am unteren Rand des Foto-Modals (Media Query <= 560px) rechnet ihre Höhe aus
     der Knopfhöhe. Mit dem Finger sind die Knöpfe größer — ohne diese Zeile bliebe die Leiste auf
     dem Maß für Zeigergeräte stehen und die Knöpfe klebten an ihren Rändern. */
  .photo-modal {
    --zoomknopf: var(--control-size);
  }

  /* Ein Eintrag der Kategorienliste ist mit Zeiger 30px hoch — genug zum Anklicken, zu wenig
     zum Antippen (Faustregel 44px). Betrifft vor allem das Tablet: Dort steht die Liste noch am
     Knopf und nicht als Blatt am unteren Rand (siehe 560px-Abfrage weiter oben). Nur die Höhe,
     nicht der Innenabstand — sonst überschriebe diese Regel die großzügigeren Handy-Maße, die
     im Stylesheet vor ihr stehen. */
  .category-select-option {
    align-items: center;
    min-height: 2.75rem;
  }

  /* Der Öffner wächst mit — und das Filterfeld daneben gleich mit ihm: Beide stehen in einer
     Zeile und sähen bei unterschiedlicher Höhe wie ein Fehler aus. */
  .category-select-btn,
  .filter-input-wrap {
    min-height: var(--control-size);
  }

  /* Die Eigenbau-Scrollleiste bleibt als Positionsanzeige sichtbar, wird aber schmaler und
     lässt Berührungen durch: Als bedienbares Element würde sie am rechten Rand einen 10px
     breiten Streifen belegen, in dem jeder Fingertipp auf den Inhalt dahinter verloren ginge.
     Gezogen wird sie auf einem Touchscreen ohnehin nicht — dort wischt man den Inhalt selbst. */
  .custom-scrollbar {
    width: 4px;
  }

  .custom-scrollbar.is-active {
    pointer-events: none;
  }

  .custom-scrollbar-thumb {
    right: 1px;
    left: 1px;
    background: var(--color-border-strong);
  }

  /* DEV-ONLY: Zweiter Riegel zur Breitenabfrage oben — ein Tablet im Querformat ist mit 1024px
     breiter als die 900px-Schwelle, hat aber trotzdem keinen Zeiger. Entwicklerwerkzeuge
     bedient man an Tastatur und Maus. */
  .dev-panel {
    display: none;
  }
}

@media (hover: none) {
  /* Ersatz für die Zellhervorhebung, die es ohne Zeiger nicht gibt (siehe die zugehörige
     @media (hover: hover)-Abfrage weiter oben): eine kurze Rückmeldung im Moment des Tippens,
     die nicht stehen bleibt. */
  .results-table tbody tr:not(.row-obsolete):not(.device-row-empty) td:not(.actions-cell):not(.copy-cell):not(.part-search-cell):not(.fe-cell):not(.photo-cell):not(.shop-cell):active {
    background-color: var(--brand-faint);
    box-shadow: inset 0 0 0 1px var(--brand-line);
  }

  .gallery-item:active {
    border-color: var(--brand);
    background: var(--color-surface-raised);
  }

  .summary-copyable:active {
    background: var(--brand-faint);
    color: var(--brand);
  }

  /* Ohne Zeiger gibt es kein Schweben, das Nummer und Beschreibung im Foto-Modal als
     kopierbar ausweist — deshalb stehen die Rahmen hier von vornherein da, statt erst
     beim Hover zu erscheinen. */
  .photo-modal-part,
  .photo-modal-desc {
    border-color: var(--color-border);
  }

  .photo-modal-part:active,
  .photo-modal-desc:active,
  .photo-modal-copy:active {
    background: var(--brand-faint);
    border-color: var(--brand-line);
    color: var(--brand);
  }
}

/* --- Funktional gleichwertige Teile (FE) --------------------------------------
   Der Knopf sitzt in derselben Spaltenfamilie wie das Foto-Icon und übernimmt dessen Maße —
   zwei unterschiedlich große Icons nebeneinander ließen die Zeilen unruhig wirken. Die Farbe
   ist die Markenfarbe statt des Foto-Grüns: Es ist kein Medium, sondern ein Verweis. */
.fe-cell {
  width: 1%;
  white-space: nowrap;
}

.fe-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  background: none;
  border: 1px solid transparent;
  padding: 0;
  cursor: pointer;
  color: var(--brand);
}

.fe-icon-btn:hover {
  background: var(--brand-faint);
  border-color: var(--brand-line);
}

.fe-icon-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -1px;
}

/* --- Direkte Teilenummer-Suche ------------------------------------------------
   Die Lupe steht in einer eigenen, unbeschrifteten Spalte direkt vor Part Number. So bleibt ihr
   Hover-Zustand von der kopierbaren Teilenummer-Zelle getrennt. */
.part-search-col,
.part-search-cell {
  width: 1%;
  white-space: nowrap;
  text-align: center;
  padding-left: 0.15rem;
  padding-right: 0.35rem;
}

.part-search-icon-btn {
  display: inline-flex;
  flex: none;
  position: relative;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  background: none;
  border: 1px solid transparent;
  padding: 0;
  cursor: pointer;
  color: var(--brand);
}

.part-search-icon-btn:hover {
  background: var(--brand-faint);
  border-color: var(--brand-line);
}

.part-search-icon-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -1px;
}

/* Der erste Klick oder Tipp ist immer nur eine Rückfrage, auch am Desktop — eine Lupe sitzt
   dicht neben kopierbarem Text, und ein Doppelklick zum Markieren sollte nicht gleich eine Suche
   auslösen. Die Frage liegt als kleine Blase mittig über dem Knopf; ihr Inhalt gehört nicht zur
   Teilenummer und wird daher auch nicht kopiert. Der Text selbst nennt bewusst keine Teilenummer
   oder Modellbezeichnung mehr — die steht bereits auf dem Knopf, den man gerade angetippt hat.
   Derselbe Aufbau (Klasse .is-confirming, Text in data-confirm-label) trägt auch die Rückfrage
   des Reload-Knopfs im Trefferkopf — deshalb hier gemeinsam mit ihm.
   --bubble-left/--bubble-top kommen aus rueckeBlaseInsBild() in js/werkzeuge.js: Als Nachfahre
   von .table-wrapper (overflow-x: auto) schnitt der frühere `position: absolute` den Rand der
   Blase am Tabellenrahmen ab, sobald sie über dessen Kante reichte — ein `position: fixed` wie
   beim gemeinsamen Tooltip (.ui-tooltip) entgeht dem, braucht dafür aber Fensterkoordinaten statt
   Prozentwerten relativ zum Knopf.
   pointer-events bewusst nicht auf none: Die Blase kann über einer fremden Zeile hängen (durch
   den Randausgleich seitlich verschoben), und ohne eigene Bedienung darunter wäre eine
   Mausbewegung über die Blase in Wahrheit eine über der verdeckten Zelle — deren Tooltip und
   Kopierfunktion würden durchschlagen, obwohl optisch die Blase im Weg steht. */
.part-search-icon-btn.is-confirming::after,
.summary-cache-reload-btn.is-confirming::after {
  content: attr(data-confirm-label);
  position: fixed;
  z-index: 1300;
  left: var(--bubble-left, 0px);
  top: var(--bubble-top, 0px);
  width: max-content;
  max-width: min(16rem, calc(100vw - 2rem));
  padding: 0.45rem 0.6rem;
  transform: translate(-50%, -100%);
  color: var(--color-text);
  background: var(--color-surface-raised);
  border: 1px solid var(--brand-line);
  box-shadow: 0 0.35rem 1rem hsl(0 0% 0% / 0.35);
  font: 600 0.72rem/1.3 var(--font);
  text-align: center;
  white-space: normal;
  user-select: none;
  pointer-events: auto;
}

/* Der Pfeil sitzt an derselben Fensterposition wie die Blasenunterkante (siehe transform der
   Blase oben) und zeigt so unabhängig von deren seitlichem Randausgleich immer auf den Knopf.
   ::before statt eines zweiten ::after, weil das Pseudoelement mit dem Text bereits ::after
   belegt. Ein Pixel Überlappung nach oben, aus demselben Grund wie bei .ui-tooltip::after: ohne
   sie zeichnet der Browser zwischen Blase und Pfeil eine feine Naht. */
.part-search-icon-btn.is-confirming::before,
.summary-cache-reload-btn.is-confirming::before {
  content: '';
  position: fixed;
  z-index: 1300;
  left: var(--bubble-left, 0px);
  top: calc(var(--bubble-top, 0px) - 1px);
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: var(--color-surface-raised);
  pointer-events: none;
}

/* --- Zeilenmenü der Kartenansicht --------------------------------------------
   Der Öffner steht in der Aktionszelle jeder Ersatzteilzeile, ist aber nur ab Handybreite zu
   sehen (siehe die 560px-Abfrage weiter oben) — darüber reicht die Breite für die Knopfreihe in
   der Karte, und in der Tabelle am Desktop hätte er ohnehin keine Spalte. */
.row-menu-btn {
  display: none;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
  color: var(--color-text-muted);
  cursor: pointer;
  /* Unterdrückt die graue Fläche, die mobile Browser beim Tippen selbst zeichnen, und die
     Verzögerung, mit der sie auf einen möglichen Doppeltipp warten. */
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

.row-menu-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.row-menu-btn[aria-expanded="true"] {
  background: var(--brand-faint);
  color: var(--brand);
}

/* Der grüne Punkt sagt, dass zu diesem Teil ein Bild vorliegt — die Auskunft, die in der Tabelle
   das Foto-Icon gibt und die in der Karte sonst im geschlossenen Menü verschwände. Gesetzt wird
   data-foto an der Zeile (siehe setzePhotoZelle in js/tabelle.js), und zwar erst, wenn es
   feststeht: Solange die Prüfung läuft, ist kein Punkt da. */
[data-foto="ja"] .row-menu-btn {
  position: relative;
}

/* Dicht über den drei Punkten und auf derselben Mittelachse: Der Knopf reicht über die volle
   Kartenhöhe, ein Punkt an seiner oberen Kante säße irgendwo in der Ecke und gehörte optisch zu
   nichts. */
[data-foto="ja"] .row-menu-btn::after {
  content: '';
  position: absolute;
  top: calc(50% - 1.35rem);
  left: 50%;
  width: 0.4rem;
  height: 0.4rem;
  transform: translateX(-50%);
  border-radius: 50%;
  background: var(--color-success);
}


/* Der Grund fängt den Tipp daneben ab, dunkelt aber bewusst nichts ab: Das Menü steht neben seiner
   Zeile, und die soll man dabei weiter lesen können — anders als beim Kategorie-Blatt, das die
   Ansicht wirklich ablöst.

   Hier steht deshalb auch kein `touch-action: none` wie dort: Ein Wisch über diesem Grund soll die
   Liste dahinter ganz normal scrollen, und das Menü schließt sich dabei von selbst (siehe den
   scroll-Zuhörer in js/zeilenmenue.js). Für ein Blatt, das die Ansicht ablöst, wäre das falsch —
   für ein Menü an einer Zeile ist es der kürzere Weg als die Zurück-Geste. */
.row-menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 59;
}

/* Fixed und von js/zeilenmenue.js in Fensterkoordinaten gesetzt: Absolut positioniert hinge das
   Menü am Tabellenwrapper und würde an dessen Kante abgeschnitten, sobald es über seine Zeile
   hinausreicht — was es bei vier Einträgen immer tut. */
.row-menu {
  position: fixed;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 11rem;
  max-width: min(15rem, calc(100vw - 5rem));
  padding: 0.3rem;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  box-shadow: 0 18px 40px hsl(0 0% 0% / 0.45);
}

/* Text links, Icon rechts: Die Icons enden damit alle auf derselben Kante direkt neben dem
   Öffner — dort, wo der Daumen ohnehin steht. Er muss zum Auswählen nur hoch oder runter, nicht
   quer über das Menü. */
.row-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: var(--control-size);
  padding: 0 0.65rem;
  background: none;
  border: 0;
  color: var(--color-text);
  font: 600 0.88rem/1.3 var(--font);
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  animation: rowMenuItemIn 0.16s ease-out both;
  /* Der Verweis zum Ersatzteilhändler ist als einziger Eintrag ein <a> (siehe eintragHtml in
     js/zeilenmenue.js) und brächte sonst Unterstreichung und Linkfarbe des Browsers mit. */
  text-decoration: none;
}

/* Das Icon dieses einen Eintrags trägt seine eigene Farbe, wie in der Foto-Spalte auch: Der Text
   sagt, wohin es geht, das Blau sagt, dass es hinausführt. */
.row-menu-item[data-aktion="shop"] .row-menu-item-icon {
  color: var(--color-info);
}

/* Von oben nach unten aufgeklappt — dieselbe Richtung, in der man die Einträge liest, und ein
   kurzer Hinweis darauf, dass das Menü aus dem Knopf daneben hervorgeht. */
.row-menu-item:nth-child(2) { animation-delay: 0.04s; }
.row-menu-item:nth-child(3) { animation-delay: 0.08s; }
.row-menu-item:nth-child(4) { animation-delay: 0.12s; }
.row-menu-item:nth-child(5) { animation-delay: 0.16s; }

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

.row-menu-item:active {
  background: var(--brand-faint);
  color: var(--brand);
}

/* Die Foto-Auskunft steht auch dann im Menü, wenn es kein Bild gibt oder die Prüfung noch läuft:
   Ein fehlender Eintrag ließe offen, welcher der beiden Fälle vorliegt. */
.row-menu-item[disabled] {
  color: var(--color-text-faint);
  cursor: default;
}

.row-menu-item-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-menu-item-icon {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
}

/* Dieselben Farben wie in der Tabelle: Das Foto-Icon ist grün, der FE-Pfeil trägt die
   Markenfarbe. Wer die Knopfreihe am Tablet kennt, findet sie im Menü wieder. */
.row-menu-item[data-aktion="foto"] .row-menu-item-icon {
  color: var(--color-success);
}

.row-menu-item[data-aktion="fe"] .row-menu-item-icon {
  color: var(--brand);
}

.row-menu-item[disabled] .row-menu-item-icon {
  color: var(--color-text-faint);
}

@keyframes rowMenuItemIn {
  from {
    opacity: 0;
    transform: translateX(0.5rem);
  }

  to {
    opacity: 1;
    transform: translateX(0);
  }
}

/* ---------- FAQ-Modal ---------- */

/* Breiter als das Modal der gleichwertigen Teile: Hier stehen Screenshots, die sonst zu klein
   zum Lesen würden. Höhe nach Inhalt bis zu einer Obergrenze (statt fest): Zugeklappt ist die
   Fragenliste kürzer als der Bildschirm, eine feste Höhe hinterließe darunter leere Fläche.
   Wächst sie beim Aufklappen über die Grenze, übernimmt der Body das Scrollen. */
/* Overlay und Kasten kommen aus .modal-overlay / .modal-panel. */
.faq-modal {
  width: clamp(320px, 90vw, 760px);
  max-height: min(85vh, 46rem);
}

.faq-modal-title {
  font-stretch: 82%;
  font-weight: 700;
  font-size: 0.95rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.faq-modal-body {
  padding: 0.9rem 1.1rem 1.2rem;
  display: flex;
  flex-direction: column;
  font-size: 0.9rem;
  line-height: 1.55;
}

/* Kein gap mehr zwischen den Fragen: Zugeklappt sollen sie als eine durchgehende Liste gelesen
   werden, die Trennlinie reicht dafür. */
.faq-item {
  border-top: 1px solid var(--color-border);
}

.faq-item:first-child {
  border-top: none;
}

.faq-question-head {
  margin: 0;
  font-size: inherit;
  font-weight: inherit;
}

/* Die ganze Zeile ist der Schalter — nicht nur der Text, damit auch der Daumen am Handy trifft. */
.faq-question {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  width: 100%;
  padding: 0.8rem 0;
  background: none;
  border: 0;
  font: inherit;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
  transition: color 0.15s ease;
}

.faq-question:hover {
  color: var(--brand);
}

.faq-question:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.faq-chevron {
  flex: none;
  color: var(--color-text-muted);
  transition: transform 0.25s ease, color 0.15s ease;
}

.faq-question:hover .faq-chevron {
  color: var(--brand);
}

.faq-item.is-open .faq-chevron {
  transform: rotate(180deg);
  color: var(--brand);
}

/* Animiert wird grid-template-rows von 0fr auf 1fr — wie beim klebenden Kopf (siehe
   klebekopf.js): So braucht es keine geratene Obergrenze wie bei max-height. */
.faq-answer-wrap {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.25s ease;
}

.faq-item.is-open .faq-answer-wrap {
  grid-template-rows: 1fr;
}

/* overflow: hidden klemmt den Inhalt ab, visibility nimmt ihn zusätzlich aus der Tabulatorreihe
   und aus dem Screenreader — sonst führte Tab in eine zugeklappte Antwort. Verzögert, damit der
   Text erst nach dem Zufahren verschwindet. */
.faq-answer {
  min-height: 0;
  padding-bottom: 1.1rem;
  overflow: hidden;
  visibility: hidden;
  transition: visibility 0s linear 0.25s;
}

.faq-item.is-open .faq-answer {
  visibility: visible;
  transition: visibility 0s;
}

.faq-answer p {
  margin: 0 0 0.6rem;
  color: var(--color-text-muted);
}

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

.faq-answer ul {
  margin: 0 0 0.6rem;
  padding-left: 1.3rem;
  color: var(--color-text-muted);
  display: grid;
  gap: 0.35rem;
}

.faq-answer strong {
  color: var(--color-text);
  font-weight: 700;
}

.faq-answer code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  color: var(--color-text);
}

.faq-answer a {
  color: var(--brand);
}

/* Screenshots aus der Anwendung selbst — leichter Rahmen, damit sie sich vom dunklen
   Modal-Hintergrund abheben, statt mit ihm zu verschwimmen. */
.faq-answer img {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 0.7rem;
  border: 1px solid var(--color-border);
}

/* Kleiner als das Foto-Modal: Hier stehen ein paar Teilenummern, kein Bild. Ein Modal in
   Bildgröße wirkte bei drei Zeilen Inhalt wie ein Fehler. */
/* Overlay und Kasten kommen aus .modal-overlay / .modal-panel. */
.equivalent-modal {
  width: clamp(320px, 46vw, 640px);
  overflow: hidden;
}

/* Titel und Teilenummer stehen untereinander, deshalb oben ausgerichtet statt mittig. */
.equivalent-modal-header {
  align-items: flex-start;
  padding: 0.85rem 0.6rem 0.85rem 1rem;
}

.equivalent-modal-title {
  display: block;
  font-weight: 600;
}

/* Die Nummer, von der aus gefragt wurde — ohne sie stünde im Modal eine Liste ohne Bezug. */
.equivalent-modal-part {
  display: block;
  margin-top: 0.15rem;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

/* .equivalent-modal-close kommt vollständig aus .modal-close. Damit ist die Rückmeldung beim
   Überfahren dieselbe wie bei den anderen beiden Modalen (vorher: nur etwas hellerer Text). */

.equivalent-modal-body {
  padding: 0.75rem 1rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.equivalent-empty {
  color: var(--color-text-muted);
  padding: 0.75rem 0;
}

.equivalent-item {
  border: 1px solid var(--color-border);
  background: var(--color-bg-sunken);
  padding: 0.6rem 0.7rem;
}

/* Dieselbe Warnfarbe wie eine abgekündigte Tabellenzeile: Der häufigste FE-Treffer ist der
   Vorgänger, der nicht mehr lieferbar ist — das muss auf den ersten Blick erkennbar sein. */
.equivalent-item.is-obsolete {
  border-color: var(--color-warning-line);
  background: var(--color-warning-faint);
}

.equivalent-item-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.equivalent-part-btn {
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-mono);
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--color-text);
  cursor: pointer;
  text-align: left;
}

.equivalent-part-btn:hover {
  color: var(--brand);
}

.equivalent-search-btn {
  background: none;
  border: 1px solid var(--color-border-strong);
  color: var(--color-text-muted);
  padding: 0.2rem 0.6rem;
  font-size: 0.8rem;
  cursor: pointer;
  flex: 0 0 auto;
}

.equivalent-search-btn:hover {
  border-color: var(--brand-line);
  background: var(--brand-faint);
  color: var(--brand);
}

.equivalent-desc {
  margin: 0.35rem 0 0;
  font-size: 0.86rem;
  line-height: 1.4;
  color: var(--color-text-muted);
}

.equivalent-marks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: 0.45rem;
}

.equivalent-mark {
  border: 1px solid var(--color-border);
  padding: 0.1rem 0.35rem;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--color-text-faint);
}

/* --- Passende Modelle (Querverweis) --------------------------------------------
   Die Zeilen sind als Ganzes klickbar wie beim Component BOM — deshalb hier derselbe
   Zeiger und dieselbe Hervorhebung beim Überfahren. */
.models-inline {
  margin-top: var(--space-block);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
}

/* Langgezogene Klappleiste über die volle Ergebnisbreite. Titel, bereits vorab geladene Zahl
   und Pfeil bleiben auch bei sehr großen Treffermengen in einer ruhigen Zeile. */
.models-inline-toggle {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.9rem 1rem;
  border: 0;
  background: var(--color-surface-raised);
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.models-inline-toggle:hover {
  background: var(--brand-faint);
  color: var(--brand);
}

.models-inline-toggle:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.models-inline-title {
  min-width: 0;
  font-weight: 700;
}

.models-inline-count {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-left: auto;
  color: var(--color-text-muted);
  font-size: 0.82rem;
  white-space: nowrap;
}

/* Drehendes Rad, solange die Gesamtzahl noch nicht feststeht — ohne das sah "Wird ermittelt…"
   wie stehengebliebener Text aus, nicht wie eine laufende Ermittlung. */
.spinner-icon {
  flex: none;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.models-inline-toggle:hover .models-inline-count {
  color: currentColor;
}

.models-inline-caret {
  flex: none;
  width: 0.55rem;
  height: 0.55rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translateY(-0.15rem);
  transition: transform 0.15s ease;
}

.models-inline-toggle[aria-expanded="true"] .models-inline-caret {
  transform: rotate(225deg) translate(-0.1rem, -0.1rem);
}

.models-inline-content {
  border-top: 1px solid var(--color-border);
}

/* Eigenes Suchfeld für den Querverweis. Es verwendet dieselben Feldbausteine wie der Filter der
   Teileliste, bleibt aber Teil des aufgeklappten Bereichs und klebt nicht mit dessen Steuerung. */
.models-filter {
  display: flex;
  align-items: center;
  /* Derselbe Abstand, den das Suchformular oben zwischen Feld und Suchen-Knopf hat. */
  gap: var(--space-2);
  padding: 0.65rem 0.75rem;
  background: var(--color-bg-sunken);
  border-bottom: 1px solid var(--color-border);
}

/* Das Feld nimmt den Rest der Zeile — kein width: 100% mehr, sonst drückte es den Suchen-Knopf
   daneben aus der Zeile. */
.models-filter .filter-input-wrap {
  flex: 1;
  min-width: 0;
}

/* Abgesetzter Suchen-Knopf, wie im Formular oben (.search-form > .btn): er gibt vor dem
   Eingabefeld keine Breite ab und übernimmt dessen Höhe.

   align-self: stretch statt einer eigenen Höhe — das Feld daneben wächst je nach Ansicht über
   --control-size hinaus (auf Handybreite steht die Eingabe auf 16px, damit iOS nicht
   hineinzoomt). Eine feste min-height stimmte dann genau dort nicht mehr. */
.models-filter-btn {
  flex: none;
  align-self: stretch;
  min-height: var(--control-size);
}

.models-inline-table-wrapper {
  border: 0;
}

/* Lade-, Fehler- und Leermeldung stehen als Zeile in der Tabelle selbst, damit sie dort
   auftauchen, wo die Liste erwartet wird. Kein Zeiger: hier ist nichts anzuklicken außer
   dem Knopf darin. */
.models-message td {
  width: 100%;
  color: var(--color-text-muted);
  padding: 1.25rem 0.5rem;
  text-align: center;
}

.models-retry-btn {
  margin-left: 0.6rem;
  background: none;
  border: 1px solid var(--color-border-strong);
  color: var(--color-text);
  padding: 0.2rem 0.6rem;
  font-size: 0.8rem;
  cursor: pointer;
}

.models-retry-btn:hover {
  border-color: var(--brand-line);
  background: var(--brand-faint);
  color: var(--brand);
}

.models-more {
  display: flex;
  justify-content: center;
  padding: 0.9rem 0.5rem 1.1rem;
  border-top: 1px solid var(--color-border);
}

@media (max-width: 560px) {
  .equivalent-modal-overlay {
    padding: 0;
    align-items: flex-end;
  }

  /* Als Blatt am unteren Rand — dieselbe Geste wie bei den übrigen Overlays in dieser Breite. */
  .equivalent-modal {
    width: 100%;
    max-height: 85vh;
  }

  /* Vollbild statt Blatt: Anders als bei den gleichwertigen Teilen (ein paar Zeilen) steht hier
     eine ganze Seite Text — ein Rand mit durchscheinendem Grund wäre nur ungenutzter Platz.
     Dieselbe Behandlung wie das Foto-Modal weiter oben. */
  .faq-modal-overlay {
    padding: 0;
  }

  .faq-modal {
    width: 100%;
    height: 100%;
    /* Die Obergrenze von oben muss hier weg, sonst bliebe das Vollbild bei 46rem stehen. */
    max-height: 100%;
    border: 0;
  }
}
