
* { box-sizing: border-box; }
html { color-scheme: dark; }
body {
  margin: 0;
  min-height: 100vh;
  font-family: 'Jost', system-ui, -apple-system, 'Segoe UI', sans-serif;
  /* Etappe 4, Nutzer-Feedback: dunkler Farbverlauf statt komplett flachem Schwarz. Bewusst
     sehr zurueckhaltend (niedrige Deckkraft, gleiche Akzentfarbe wie im Rest des Werkzeugs,
     keine zweite Farbe), damit daraus Tiefe statt eine auffaellige Farbflaeche wird.
     background-attachment: fixed, damit der Verlauf beim Scrollen einer langen Seite (z.B.
     Nutzer-Detailseite) nicht sichtbar mitwandert. */
  background:
    radial-gradient(1100px 720px at 14% -12%, rgba(130, 180, 240, 0.07), transparent 60%),
    linear-gradient(165deg, #060911 0%, #010205 45%, #000103 100%);
  background-attachment: fixed;
  color: #D8D4CE;
  line-height: 1.6;
}
a { color: #82B4F0; transition: color 0.15s ease; }

/* Sichtbarer Fokusring fuer Tastatur-Navigation (Review-Fund, ui-ux-pro-max-Skill: reines
   outline:none + Rand-Farbwechsel auf Formularfeldern reicht fuer Links/Buttons/Nav-Punkte
   nicht aus). Nur :focus-visible (nicht :focus), damit ein Mausklick keinen Ring bekommt,
   Tab-Navigation aber schon. */
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible,
select:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid #82B4F0;
  outline-offset: 2px;
  border-radius: 4px;
}

/* Grundgeruest: Seitenleiste links (fix, Desktop) + Inhaltsbereich rechts (Etappe 4, vorher
   eine horizontale Kopfzeile mit Nav-Punkten nebeneinander, siehe Git-Historie). */
.shell { display: flex; align-items: stretch; min-height: 100vh; }

.sidebar {
  width: 264px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  background: #080D18;
  border-right: 1px solid #131A2C;
  padding: 1.5rem 1rem 1rem;
}
.brand { display: flex; align-items: center; gap: 0.6rem; color: #FFFFFF; text-decoration: none; }
/* Nutzer-Feedback: "Medien-Tool" brach neben dem Logo haeufig um (264px Seitenleiste minus
   Innenabstand ist schmaler als Logo + Label nebeneinander). Gestapelt statt nebeneinander,
   nur in der Seitenleiste (die mobile Kopfzeile bleibt bei der horizontalen Basisregel, dort
   ist die schmale, einzeilige Form die richtige). */
.sidebar > .brand {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0 0.6rem 1.3rem;
  border-bottom: 1px solid #1A2338;
  margin-bottom: 1.3rem;
}
.brand-logo { height: 22px; width: auto; display: block; }
.brand-label { font-weight: 600; letter-spacing: 3px; font-size: 0.9rem; }

.side-nav { flex: 1; display: flex; flex-direction: column; gap: 1.4rem; }
.nav-section-label {
  padding: 0 0.6rem;
  margin-bottom: 0.4rem;
  color: #8A8480;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 1.2px;
  text-transform: uppercase;
}
.nav-section-items { display: flex; flex-direction: column; gap: 0.15rem; }
.side-nav a {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.55rem 0.65rem;
  border-radius: 8px;
  color: #D8D4CE;
  text-decoration: none;
  border-left: 2px solid transparent;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.side-nav a svg { flex-shrink: 0; }
.side-nav a:hover { background: #111729; color: #FFFFFF; }
.side-nav a.active { background: rgba(130, 180, 240, 0.12); color: #82B4F0; border-left-color: #82B4F0; }

/* Wer angemeldet ist plus Abmelden-Link, jetzt am Fuss der Seitenleiste statt rechts in einer
   Kopfzeile (Etappe 4). */
.sidebar-footer {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 1rem;
  padding: 1rem 0.6rem 0;
  border-top: 1px solid #1A2338;
}
/* avatar-initial (Fallback ohne Google-Profilbild) und avatar-img (Nutzer-Feedback: echtes
   Google-Profilbild statt erzeugtem Avatar, siehe renderLayout unten) teilen sich Groesse/
   Form, damit ein fehlendes Profilbild keinen Layout-Sprung verursacht. */
.avatar-initial, .avatar-img {
  width: 30px;
  height: 30px;
  border-radius: 999px;
  flex-shrink: 0;
}
.avatar-initial {
  background: rgba(130, 180, 240, 0.12);
  color: #82B4F0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 0.85rem;
}
.avatar-img { display: block; object-fit: cover; }
.sidebar-user { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.sidebar-user-email { color: #D8D4CE; font-size: 0.82rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Die frueheren .sidebar-user a-Regeln sind mit dem Auftrag "Navigation und Bedienung"
   entfallen (Review-Fund): der Abmelden-Link war der einzige Anker in diesem Bereich, seit er
   ein Button ist, haetten sie auf nichts mehr gewirkt. */
/* Abmelden ist seit dem Auftrag "Navigation und Bedienung" ein echter Button in einem
   POST-Formular (vorher ein GET-Link, der die Session geloescht hat, siehe routes/auth.js).
   Optisch soll er aber weiterhin wie der leise Text daneben wirken und nicht wie ein
   Akzent-Button, deshalb hier die komplette Button-Grundoptik zuruecknehmen. Die Regel steht
   bewusst nach .sidebar-user a oben und traegt zwei Selektoren, damit sie die globale
   button-Basisregel (411) sicher schlaegt. */
.sidebar-user .logout-btn {
  background: transparent;
  border: none;
  padding: 0;
  color: #8A8480;
  font-size: 0.78rem;
  font-weight: 400;
  font-family: inherit;
  cursor: pointer;
}
.sidebar-user .logout-btn:hover { color: #82B4F0; opacity: 1; }
.sidebar-user .logout-btn:active { transform: none; }

.content-area { flex: 1; min-width: 0; }
/* Schmale, mobile Kopfzeile mit Hamburger-Button (siehe renderLayout unten), auf Desktop
   ausgeblendet (siehe @media-Regel weiter unten), dort traegt die immer sichtbare
   Seitenleiste die Marke bereits. */
.mobile-topbar { display: none; }

/* SEITENBREITE, zwei Stufen ueber ein Token (Layout-Durchgang).
   Vorher stand hier eine harte Kappe von 1240px fuer JEDE Seite. Auf einem breiten Monitor blieb
   dadurch rechts viel Flaeche leer, waehrend die zwei wirklich breiten Tabellen (Medien mit 9
   Spalten, Dateiliste der Klangwelt-Detailseite mit 8) in ihrem .table-wrap horizontal scrollen
   mussten. Man scrollte also innerhalb eines Kastens, neben dem Platz frei war.

   MASSGEBLICH IST DIE MINDESTBREITE, NICHT DIE SPALTENZAHL. Fast jede Tabellenzelle umbricht.
   Harte, nicht umbrechbare Breite tragen nur drei Dinge: .file-preview (fest 220px, Zeile ~657),
   td.actions (white-space: nowrap, Zeile ~654) und .badge (white-space: nowrap, Zeile ~816).
   Eine Tabelle sprengt die Standardstufe erst, wenn davon genug zusammenkommt. Die
   Klangwelten-LISTE hat genauso viele Spalten wie die Dateiliste der Detailseite, bleibt mit
   ihrem 40px-Vorschaubild aber darunter und laeuft deshalb bewusst auf der Standardstufe.
   Ein erster Entwurf ging nach Spaltenzahl und hat dabei vier Seiten faelschlich auf die breite
   Stufe gesetzt.

   Warum NICHT einfach global aufweiten: input, textarea und select stehen im Formular auf
   width:100% (siehe weiter unten). Eine breitere Kappe fuer alle Seiten haette jedes Textfeld
   auf jeder Bearbeiten-Seite ueber die volle Breite gezogen, was schlechter waere als das
   Problem, das sie loest. Deshalb zwei Stufen statt einer, und zwar ueber EIN Token mit einer
   dokumentierten Regel (siehe DESIGN.md, Abschnitt Layout), nicht ueber Sonderregeln je Seite.

   .breit ist bewusst nicht auf einen festen groesseren Wert gesetzt, sondern auf min(100%, ...):
   die Seite nutzt damit tatsaechlich die vorhandene Fensterbreite und hoert erst bei 2000px auf
   zu wachsen, wo auch eine Datentabelle nicht mehr besser lesbar wird. */
:root { --inhalt-max: 1240px; }
main { max-width: var(--inhalt-max); margin: 0 auto; padding: 2.4rem 2rem 4rem; }
main.breit { --inhalt-max: min(100%, 2000px); }
/* Lesbarkeitsschutz fuer die breite Stufe: Tabellen duerfen die volle Breite nehmen, zusammen-
   haengender Text nicht. Ohne ihn zoege ein Erklaerabsatz auf der Medien-Seite ueber 2000px und
   waere Zeile fuer Zeile kaum noch zu verfolgen.
   Der Schutz haengt an der ROLLE, nicht am Tag. Ein frueherer Entwurf schrieb nur "main.breit p"
   und liess damit ausgerechnet den laengsten Erklaertext der Medien-Seite durch, weil der dort
   als <span class="field-hint"> gesetzt ist, waehrend derselbe Satz auf der Klangwelt-Detailseite
   als <p class="field-hint"> steht. Ob ein Text lesbar bleibt, haette dann davon abgehangen,
   welches Tag der Autor gegriffen hat. */
main.breit p,
main.breit li,
main.breit .field-hint { max-width: 95ch; }
/* Zweiter Teil desselben Schutzes, und der wichtigere: Formularfelder. input, textarea und select
   stehen auf width:100% (siehe weiter unten). Auf der breiten Stufe liefe ein Eingabefeld sonst
   ueber die volle Fensterbreite.
   EHRLICHERWEISE: heute greift diese Regel auf keiner der beiden breiten Seiten, sie ist reine
   Vorsorge. Sie stammt aber aus einem echten Fund. Die Gutschein-Liste rendert ihr Anlege-
   Formular direkt unter der Tabelle auf derselben Seite; solange jene Seite versuchsweise auf
   der breiten Stufe lief, war das Feld "Laufzeit (Tage)", in das man eine zweistellige Zahl
   tippt, knapp 2000px breit. Die Seite laeuft inzwischen wieder auf der Standardstufe, die
   Konstellation Tabelle-plus-Formular kann aber jederzeit wiederkommen.
   :not(.filter-bar) nimmt die Filterzeilen aus, deren Felder ohnehin auf width:auto stehen und
   deren Breite bewusst am Inhalt haengt. */
main.breit form:not(.filter-bar) input[type=text],
main.breit form:not(.filter-bar) input[type=number],
main.breit form:not(.filter-bar) input[type=file],
main.breit form:not(.filter-bar) textarea,
main.breit form:not(.filter-bar) select { max-width: 46rem; }

h1, h2, h3 { color: #FFFFFF; font-weight: 500; }
h1 { font-size: 1.7rem; }
h2 { font-size: 1.3rem; margin-top: 2rem; }
h3 { font-size: 1.05rem; }

/* Kopfzeile einer Listenseite (Titel links, primaere Aktion rechts als Button). Nutzer-
   Feedback: dasselbe Muster jetzt auch fuer Detailseiten-Aktionsleisten (z.B. Bearbeiten/
   Audio hochladen/Loeschen auf der Klangwelten-Detailseite) statt Aktionen unten in einer
   langen ersten Card zu verstecken. .row darin braucht das eigene margin-top (fuer gestapelte
   Formularfelder gedacht) nicht, .list-header zentriert bereits vertikal. */
/* margin-bottom (Nutzer-Feedback "Button-Abstaende oben"): .card traegt selbst KEINEN oberen
   Abstand, den bekommt nur eine Card, die auf eine andere folgt (.card + .card weiter unten).
   Die erste Card einer Seite klebte dadurch unmittelbar unter der Kopfzeile, auf der
   Klangwelt-Detailseite sassen die beiden Knoepfe praktisch auf der Kartenkante.
   Auf Listenseiten folgt statt einer Card eine Tabelle mit margin-top: 1rem. Beide Abstaende
   fallen hier zusammen (benachbarte Geschwister-Margins kollabieren zum groesseren Wert), es
   entsteht also kein doppelter Abstand, sondern durchgehend derselbe. */
.list-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
/* h1 UND h2: eine .list-header traegt je nach Stelle die eine oder die andere (Seitentitel
   gegen Abschnittstitel innerhalb einer Card). Ohne h2 hier behielte der Abschnittstitel
   sein margin-top: 2rem und schoebe die Knoepfe daneben um genau diesen Betrag nach unten,
   weil align-items: center an der gemeinsamen Zeilenmitte ausrichtet. */
.list-header h1,
.list-header h2 { margin: 0; }
.list-header .row { margin-top: 0; }
/* Eine .list-header INNERHALB einer Card (z.B. "Verknuepfte Dateien" mit ihren Aktionen) braucht
   den Abstand nicht, dort folgt direkt der Erklaertext oder die Tabelle mit eigenem margin. */
.card > .list-header { margin-bottom: 0; }

fieldset {
  margin-top: 1.5rem;
  padding: 1rem 1.2rem 1.2rem;
  border: 1px solid #131A2C;
  border-radius: 10px;
}
fieldset > label:first-of-type { margin-top: 0.2rem; }
legend {
  padding: 0 0.5rem;
  color: #8A8480;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
}

label { display: block; margin-top: 1rem; font-weight: 600; color: #D8D4CE; }
input[type=text], input[type=number], input[type=file], textarea, select {
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.5rem 0.6rem;
  background: #0C111F;
  border: 1px solid #131A2C;
  border-radius: 6px;
  color: #D8D4CE;
  font-family: inherit;
  font-size: 1rem;
  transition: border-color 0.15s ease;
}
/* Review-Fund Etappe 4: outline:none stand hier frueher zusaetzlich, dieselbe Spezifitaet
   wie die neue :focus-visible-Regel oben (je ein Typ- plus ein Pseudoklassen-Selektor),
   gewann den Kaskaden-Gleichstand per Reihenfolge und machte den neuen Fokusring auf Formular-
   feldern wirkungslos (nur der Rand-Farbwechsel blieb sichtbar). Ohne outline:none hier
   gilt fuer Formularfelder wieder die :focus-visible-Regel oben, border-color bleibt
   zusaetzlich (unterschiedliche Eigenschaften, keine Kollision). */
input:focus, textarea:focus, select:focus { border-color: #82B4F0; }
input.field-error, textarea.field-error, select.field-error { border-color: #e5697a; }
.field-hint { display: block; margin-top: 0.3rem; color: #8A8480; font-size: 0.8rem; font-weight: 400; }
.row { margin-top: 1.2rem; display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; }

button, .btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: #82B4F0;
  color: #010205;
  border: none;
  border-radius: 8px;
  padding: 0.6rem 1.3rem;
  font-family: inherit;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: opacity 0.15s ease, transform 0.1s ease;
}
/* .btn macht einen <a>-Link optisch zum Button (fuer primaere Aktionen wie "Neue Klangwelt
   anlegen", die reine Text-Links waeren). text-decoration nur hier noetig, ein echtes
   <button>-Element braucht das nicht. */
.btn { text-decoration: none; }
button:hover, .btn:hover { opacity: 0.9; }
button:active, .btn:active { transform: scale(0.98); }
/* Textuelle (nicht Button-) Kennzeichnung fuer destruktive EINSTIEGS-Links (z.B.
   "Endgueltig loeschen" auf einer Detailseite, die selbst nur zur Bestaetigungsseite
   fuehrt, dort sitzt der eigentliche .btn-danger-Button). Gleiche Rot-Farbe wie
   .btn-danger/.msg-error, aber als schlichter Link statt vollem Button, damit die
   tatsaechlich destruktive Aktion staerker wirkt als der blosse Einstiegspunkt dorthin. */
.link-danger { color: #e5697a; }
.link-danger:hover { opacity: 0.8; }
/* Design-Durchgang M1.3: einheitliche Gefahrenfarbe fuer alle destruktiven Buttons
   (Endgueltig loeschen, Entfernen), ueber alle Seiten mit Loeschfunktion hinweg. Nur
   background noetig, color/border-radius/padding/hover/active erbt der Button bereits von
   der obigen button-Basisregel (hoehere Selektor-Spezifitaet der Klasse gewinnt nur beim
   background). Text (CI.onAccent, von der Basisregel geerbt) hat auf diesem Rot ca. 6.6:1
   Kontrast, waehrend weisser Text hier nur auf ca. 3.2:1 kaeme (unter der 4.5:1-Schwelle),
   deshalb bewusst NICHT auf helle Schrift umgestellt. */
.btn-danger { background: #e5697a; }
/* Nutzer-Feedback: sekundaere Aktionen neben einer primaeren (z.B. "Audio hochladen" neben
   "Bearbeiten" auf einer Klangwelten-Detailseite) waren bisher blosse Text-Links, das wirkte
   neben einem vollen .btn optisch inkonsistent/weniger bedienbar. .btn-secondary macht daraus
   einen echten, aber zurueckhaltenderen Button (Rahmen statt Fuellung), ohne mit der
   primaeren Aktion um Aufmerksamkeit zu konkurrieren. */
.btn-secondary { background: transparent; color: #D8D4CE; border: 1px solid #131A2C; }
.btn-secondary:hover { background: #111729; opacity: 1; }
/* Fuer destruktive EINSTIEGS-Links, die wie ein Button aussehen sollen (statt reiner
   .link-danger-Text), aber bewusst NICHT so voll/laut wie .btn-danger: .btn-danger bleibt der
   tatsaechlichen, finalen Aktion auf der Bestaetigungsseite vorbehalten (siehe Kommentar bei
   .link-danger oben, dieselbe Begruendung gilt hier unveraendert), .btn-outline-danger ist der
   Mittelweg fuer den Einstiegspunkt: erkennbar als Button, aber deutlich leiser. */
.btn-outline-danger { background: transparent; color: #e5697a; border: 1px solid rgba(229, 105, 122, 0.35); }
.btn-outline-danger:hover { background: rgba(229, 105, 122, 0.12); opacity: 1; }
button:disabled { opacity: 0.5; cursor: default; transform: none; }

/* Drag-and-Drop-Zone: das native input[type=file] bleibt der eigentliche, fokussierbare
   Kontrollpunkt (Tastatur/Screenreader-Fallback), siehe upload.js. Ein <label>, das Zone und
   Input umschliesst, macht per natives HTML auch Klicks auf den Hinweistext zum Datei-Dialog-
   Oeffner, ganz ohne eigenes JS dafuer. */
.dropzone {
  margin-top: 0.4rem;
  padding: 1rem;
  border: 2px dashed #131A2C;
  border-radius: 8px;
  background: #0C111F;
  text-align: center;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.dropzone.dragover { border-color: #82B4F0; background: rgba(130, 180, 240, 0.12); }
/* width:100% (nicht auto) beibehalten (Review-Fund, live im Browser nachgewiesen): das
   native, nicht schrumpfende file-Input zwingt sonst sein umschliessendes <fieldset> auf
   schmalen Viewports breiter als den Container (Browser-Eigenheit: fieldset dimensioniert
   sich am min-content seiner Kinder), das fuehrt zu echtem horizontalem Seiten-Scrollen und
   hebelt die @media-Regeln weiter unten aus. */
.dropzone input[type=file] { width: 100%; margin-top: 0; }
.dropzone-hint { margin: 0.5rem 0 0; color: #8A8480; font-size: 0.85rem; }
.dropzone-count { display: block; margin-top: 0.4rem; color: #82B4F0; font-size: 0.85rem; }

/* Upload-Fortschritt, siehe upload.js. [hidden] mit hoeherer Spezifitaet als die Basisregel,
   sonst wuerde "display: flex" das hidden-Attribut wirkungslos machen. */
.upload-progress { margin-top: 0.8rem; display: flex; align-items: center; gap: 0.6rem; flex: 1 1 200px; }
.upload-progress[hidden] { display: none; }
.upload-progress progress { flex: 1; height: 10px; accent-color: #82B4F0; }
.upload-progress-text { color: #8A8480; font-size: 0.85rem; min-width: 3ch; }
/* Verarbeitungsphase (Auftrag "Rueckmeldung waehrend der Upload-Verarbeitung"): nach dem
   letzten gesendeten Byte laeuft serverseitig die ffmpeg-Konvertierung, die den Node-Prozess
   blockiert (siehe enterProcessing() in UPLOAD_JS unten). Der Balken wird dafuer per
   removeAttribute('value') unbestimmt, der Browser animiert ihn dann von selbst; die Klasse
   hier ist nur fuer den Text zustaendig, der in dieser Phase ein ganzer Satz statt einer
   Prozentzahl ist und deshalb umbrechen darf statt auf 3ch Mindestbreite zu sitzen.
   flex-basis 100% schiebt ihn bei schmalem Platz unter den Balken statt ihn zu quetschen. */
.upload-progress.is-processing { flex-wrap: wrap; }
.upload-progress.is-processing .upload-progress-text { flex: 1 1 100%; color: #D8D4CE; }

/* Etappe 4: randlose Tabellen mit duenner Zeilentrennlinie statt vollem Zellenraster, ruhiger
   auf grossen Tabellen (Nutzer-/Klangwelten-Liste). font-variant-numeric global auf allen
   Zellen, damit Zahlenspalten (Reihenfolge, Anzahl, Datum) ohne eigene Markup-Aenderung pro
   Route-Datei tabellarisch ausgerichtet sind. */
table { border-collapse: collapse; width: 100%; margin-top: 1rem; font-size: 0.93rem; }
th, td { padding: 0.65rem 0.7rem; text-align: left; font-variant-numeric: tabular-nums; }
thead th {
  color: #8A8480;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  border-bottom: 1px solid #131A2C;
}
tbody tr { border-bottom: 1px solid #1A2338; transition: background-color 0.15s ease; }
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: #111729; }
/* Cursor-Hinweis nur fuer tatsaechlich klickbare Zeilen (die einen .row-link enthalten, siehe
   UPLOAD_JS-Zeilen-Klick-Komfort unten), damit die globale Hover-Faerbung oben nicht
   faelschlich Klickbarkeit auf Tabellen suggeriert, die keine hat (z.B. Team-Zugriff). */
tbody tr:has(.row-link) { cursor: pointer; }
.table-wrap { overflow-x: auto; margin-top: 1rem; }
.table-wrap table { margin-top: 0; }
/* Sprungziel-Zeilen (Auftrag "Navigation und Bedienung"): Zeilen mit einer stabilen id, die
   ueber ein #fragment direkt angesprungen werden (Rueckweg aus einer Einzelbearbeitung,
   Hoch/Runter-Verschieben). scroll-margin-top haelt die angesprungene Zeile frei unter der auf
   schmalen Viewports klebenden .mobile-topbar (position: sticky, siehe weiter unten), sonst
   verschwindet genau die Zeile darunter, zu der gesprungen wurde. :target hebt sie zusaetzlich
   kurz hervor, damit nach dem Sprung sofort erkennbar ist, welche Zeile gemeint war; die
   Hervorhebung ist rein optisch und verschwindet beim naechsten Klick auf eine andere Zeile.
   Wegen der hoeheren Spezifitaet gewinnt sie gegen die tbody tr:hover-Regel oben nicht
   automatisch, deshalb steht sie hier bewusst NACH ihr. */
tr.row-anchor { scroll-margin-top: 4.5rem; }
tr.row-anchor:target { background: rgba(130, 180, 240, 0.12); }
/* Einzel-Vorschau je Datei (Auftrag "Musik-Einzelpreview"): natives <audio controls> in einer
   Tabellenzelle. Die vom Browser gelieferten Steuerelemente sind von Haus aus recht breit und
   hoch, hier auf Zeilenmass gebracht. width statt max-width, damit alle Zeilen gleich breit
   sind und die Spalte nicht je nach Browser springt. Die Tabelle steht ohnehin in einer
   .table-wrap und scrollt auf schmalen Viewports horizontal. */
.file-preview { width: 220px; max-width: 100%; height: 34px; vertical-align: middle; }
/* Auf der Datei-Bearbeiten-Seite steht der Player nicht in einer Tabellenzelle, sondern allein
   in einer Card, dort darf er die volle Breite nehmen. */
.file-preview-wide { width: 100%; height: 40px; margin-top: 0.4rem; }

/* Onboarding-Antworten-Tabelle (Auftrag "Vier Korrekturen", Punkt 1): bewusst KEIN
   .table-wrap. Die Antwort-Spalte kann eine kommagetrennte Liste aufgeloester Klarnamen
   enthalten (Mehrfachauswahl-Antworten), das ist Fliesstext, kein tabellarischer Zahlenwert
   wie bei den uebrigen .table-wrap-Tabellen (siehe deren eigene Begruendung weiter unten:
   dort ist horizontales Scrollen statt Umbruch die richtige Wahl). Hier ist Umbruch
   angemessener: overflow-wrap bricht auch ein einzelnes langes, leerzeichenloses Wort (z.B.
   ein nicht aufloesbarer roher option_key als Fallback), nicht nur an Wortgrenzen. */
.answers-wrap { margin-top: 1rem; }
.answers-wrap table { margin-top: 0; }
.answers-wrap td { white-space: normal; overflow-wrap: anywhere; }

/* Team-Zugriff: zwei kleine Formulare (Rolle setzen, Entfernen) in derselben Tabellenzelle,
   nebeneinander statt jeweils in eigener Zeile. */
td.actions { white-space: nowrap; }
td.actions form { display: inline-block; margin: 0 0.4rem 0.3rem 0; }
td.actions select { width: auto; display: inline-block; margin: 0 0.4rem 0 0; }

/* Hoch/Runter-Buttons fuer manuell sortierbare Listen, siehe renderMoveButtons() unten.
   Eigene, kleinere Groesse noetig (die globale button-Basisregel ist fuer normale
   Aktions-Buttons ausgelegt, hier viel zu gross fuer ein einzelnes Pfeil-Zeichen in einer
   Tabellenzelle). */
.move-buttons { display: inline-flex; gap: 0.2rem; margin-left: 0.5rem; }
.move-form { display: inline-block; }
.move-buttons button { padding: 0.1rem 0.4rem; font-size: 0.75rem; line-height: 1.4; }

/* Filter-/Sortierleiste ueber einer Listen-Tabelle (Feedback-Inbox, Nutzer-Suche). Die
   globalen label/select-Regeln oben sind fuer gestapelte Formularfelder (block, volle Breite)
   ausgelegt, hier bewusst ueberschrieben fuer eine kompakte, nebeneinander liegende Leiste.
   Etappe 4: umbenannt von .feedback-filters zu .filter-bar (der Name stammte aus der
   Feedback-Inbox, wird inzwischen auch von der Nutzer-Suche verwendet, siehe users.js). */
.filter-bar { display: flex; align-items: flex-end; gap: 1rem; flex-wrap: wrap; margin-top: 1rem; margin-bottom: 1.5rem; }
.filter-bar label { margin-top: 0; display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.85rem; font-weight: 600; }
.filter-bar select { width: auto; min-width: 170px; }
.filter-bar input[type=text] { width: auto; min-width: 220px; margin-top: 0; }
/* Ankreuzfeld in einer Filterzeile. Anders als die uebrigen Filter-Labels steht die Beschriftung
   NEBEN dem Kaestchen, nicht darueber: ohne diese Regel erbt es flex-direction: column von
   .filter-bar label und das Kaestchen stuende ueber seinem eigenen Text. Genau daran lag die
   schiefe Filterzeile der Medien-Seite, die dort zuvor als handgebaute .row statt als
   .filter-bar gesetzt war. */
.filter-bar label.check { flex-direction: row; align-items: center; gap: 0.5rem; }
.filter-bar label.check input[type=checkbox] { width: auto; margin: 0; }
/* Langer, mehrzeiliger Klartext, wie ihn Nutzer oder Redaktion eingegeben haben: die volle
   Feedback-Nachricht und der volle Rechtstext. pre-wrap erhaelt Zeilenumbrueche UND umbricht
   lange Zeilen, im Gegensatz zu pre. Eine Regel fuer beide Faelle statt zweier gleichlautender,
   die auseinanderlaufen koennen. */
.langtext { white-space: pre-wrap; }
/* Feedback-Variante: dieselbe Textbehandlung plus eine Trennlinie zu den Stammdaten darueber,
   weil die Nachricht dort nicht in einer eigenen Card steht. */
.feedback-message { margin-top: 1.3rem; padding-top: 1.2rem; border-top: 1px solid #1A2338; }

.msg-error { color: #e5697a; }
.msg-ok { color: #82B4F0; }
.msg-skip { color: #8A8480; }
/* Auftrag "Helligkeits-Warnung fuer Player-Hintergrundbild": eigene, von .msg-error klar
   unterscheidbare Farbe fuer eine Rueckfrage statt eines harten Fehlers (kein neuer CI-Token
   noetig, CI.premium ist bereits das vorhandene Amber/Gold im Stylesheet). */
.msg-warning { color: #E0B75E; }

.card {
  background: #0C111F;
  border: 1px solid #131A2C;
  border-radius: 12px;
  padding: 1.5rem;
}
/* Abstand zwischen mehreren aufeinanderfolgenden Cards (z.B. Klangwelten-Detailseite). */
.card + .card { margin-top: 1.5rem; }
/* Review-Fund (Karten-Hoehen-Angleichung, Folgerunde): dieselbe Regel deckt NICHT den Fall
   ab, dass eine einzelne Card direkt auf ein .card-grid folgt (z.B. Gefahrenbereich nach der
   Check-ins/Feedback/.../Premium-Reihe auf der Nutzer-Detailseite) -- der unmittelbare
   Vorgaenger traegt dort die Klasse .card-grid, nicht .card, der Selektor oben greift nicht,
   .card-grid selbst hat kein eigenes margin-bottom. Ergebnis: 0px Abstand, gemessen per
   getBoundingClientRect() in einer echten Browser-Instanz (nicht nur per Handrechnung).
   Dieselbe 1.5rem-Konstante wie .card-grid selbst (gap/margin-top) und .card + .card oben,
   keine neu erfundene Zahl. */
.card-grid + .card { margin-top: 1.5rem; }
/* h2 { margin-top: 2rem } kollabiert NICHT mit dem Card-Rand (Margin-Collapsing greift nur
   ohne Padding dazwischen), macht eine h2 als erstes Kind einer Card sonst spuerbar leerer
   oben. */
.card > h2:first-child { margin-top: 0; }

/* Fakten-Raster (Redesign-Runde 2): ersetzt das vorherige <p><strong>Label:</strong> Wert</p>-
   Aufzaehlungsmuster auf Gutschein-/Nutzer-/Feedback-Detailseiten. Kleines, gedaempftes
   Grossbuchstaben-Label ueber dem eigentlichen Wert, dieselbe Rollenverteilung wie bei
   Tabellenkoepfen (thead th) im Rest des Werkzeugs: das Label ordnet ein, der Wert traegt den
   Inhalt. */
.facts { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 1.3rem 1.6rem; margin-top: 0.3rem; }
.fact { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; }
.fact-label { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #8A8480; }
.fact-value { color: #D8D4CE; font-size: 0.95rem; }

/* Uebersetzungen als aufklappbarer Bereich direkt im Bearbeiten-Formular, siehe
   renderTranslationsSection() unten. Eigene Umrandung wie .card/fieldset, damit der Bereich
   trotz <details> optisch als zusammengehoeriger Block erkennbar bleibt, native Auf/Zu-
   Dreieck-Markierung des Browsers bleibt bewusst erhalten. */
.translations-details {
  margin-top: 1.5rem;
  border: 1px solid #131A2C;
  border-radius: 10px;
  padding: 1rem 1.2rem;
}
.translations-details summary { cursor: pointer; font-weight: 600; color: #FFFFFF; }
.translations-details summary .badge { margin-left: 0.5rem; }
.translations-details[open] summary { margin-bottom: 1rem; }

/* Ablese-Leiste (Redesign-Runde 2): ersetzt das vorherige Fuenferraster gleich grosser
   Kennzahl-Kacheln (Startseite, Nutzer-Detailseite). Eine durchgehende Leiste mit duennen
   Trennlinien statt einzelner umrandeter Boxen, dem Instrumenten-Charakter des Signalraums
   naeher als ein generisches Karten-Raster. Icon plus Label bilden die Kopfzeile jeder Zelle,
   der grosse Zahlenwert darunter traegt das eigentliche Gewicht. Nutzer-Feedback: das Label
   war vorher in CI.muted gesetzt und wirkte zu leise fuer eine Kennzahl, die auf einen Blick
   lesbar sein soll, jetzt in der reinen Fliesstextfarbe. Nur das kleine Icon bleibt
   Nachthimmel-Blau (Ein-Akzent-Regel: ein einzelner, bewusster Farbtupfer je Zelle, nicht die
   ganze Kopfzeile).
*/
.readout { display: flex; flex-wrap: wrap; border: 1px solid #131A2C; border-radius: 12px; background: #0C111F; overflow: hidden; margin-top: 1rem; }
.readout-cell { flex: 1 1 150px; padding: 1.1rem 1.4rem; border-right: 1px solid #131A2C; }
.readout-cell:last-child { border-right: none; }
.readout-head { display: flex; align-items: center; gap: 0.45rem; }
.readout-icon { display: flex; flex-shrink: 0; color: #82B4F0; }
.readout-label { color: #D8D4CE; font-size: 0.85rem; font-weight: 500; }
.readout-value { display: block; margin-top: 0.5rem; font-family: ui-monospace, 'Cascadia Mono', 'SFMono-Regular', Consolas, monospace; font-size: 1.9rem; font-weight: 600; color: #FFFFFF; line-height: 1; font-variant-numeric: tabular-nums; }
/* Review-Fund (Auftrag "Naechte/Check-in-Daten...", Startseiten-Kennzahlen): eine Zelle ohne
   Daten (z. B. Ø Erholungswert vor dem ersten Checkin) zeigt statt der grossen Zahl ein
   renderBadge()-Muster ("Keine Daten"). Ohne diese Regel erbt das Badge nur seine eigene,
   fuer eine Fliesstext-Pille gedachte 0.75rem-Schrift (siehe .badge unten) und wirkt neben den
   1.9rem-Zahlen der Nachbarzellen winzig. Bewusst nicht auf volle 1.9rem gesetzt (eine Pille in
   dieser Groesse saehe gestreckt/deplatziert aus), sondern auf ein Zwischenmass, das sich an
   .fact-value (0.95rem, siehe oben) orientiert.
*/
.readout-value .badge { font-size: 1rem; padding: 0.25rem 0.7rem; }
@media (max-width: 640px) {
  .readout { flex-direction: column; }
  .readout-cell { border-right: none; border-bottom: 1px solid #131A2C; }
  .readout-cell:last-child { border-bottom: none; }
}

/* Konsole (Redesign-Runde 2): ersetzt die vorherigen, gleich gewichteten Schnellzugriff-
   Kacheln. Eine Aktion ist immer die tatsaechlich haeufigere/primaere (volle Akzentflaeche),
   die andere bleibt zurueckhaltender (Haarlinien-Rand), statt zwei optisch identischer
   Zwillingskarten ohne echte Rangfolge. */
.console { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; }
.console-primary, .console-secondary {
  flex: 1 1 240px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 1rem;
  border-radius: 12px;
  padding: 1.4rem 1.6rem;
  text-decoration: none;
}
.console-primary { flex-grow: 1.4; background: #82B4F0; transition: opacity 0.15s ease; }
.console-primary:hover { opacity: 0.92; }
.console-secondary { border: 1px solid #131A2C; transition: border-color 0.15s ease, background-color 0.15s ease; }
.console-secondary:hover { border-color: #82B4F0; background: #111729; }
.console-label { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; font-size: 1.02rem; }
.console-primary .console-label { color: #010205; }
.console-secondary .console-label { color: #FFFFFF; }
.console-desc { font-size: 0.85rem; }
.console-primary .console-desc { color: #010205; opacity: 0.75; }
.console-secondary .console-desc { color: #8A8480; }

#mix-preview { margin-top: 1rem; }
.mix-layer { margin-top: 0.6rem; }
.mix-layer label { font-weight: 400; color: #D8D4CE; }
.mix-layer input[type=range] { width: 220px; max-width: 60%; accent-color: #82B4F0; vertical-align: middle; margin: 0 0.6rem; }
.mix-pct { color: #8A8480; font-variant-numeric: tabular-nums; }

/* Badge-Familie. Farbe ist nie alleiniger Traeger (siehe renderBadge() unten), ok/muted/
   premium/error/new haben je ein eigenes Icon mit im Markup, nur neutral (reine Kategorie
   ohne Werturteil, z.B. Sorte/Ebene) kommt ohne Icon aus. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
  border: 1px solid transparent;
}
.badge svg { flex-shrink: 0; }
.badge-ok { color: #6FCF97; background: rgba(111, 207, 151, 0.12); border-color: rgba(111, 207, 151, 0.35); }
/* Review-Fund Etappe 4: die alte Alpha (0.12) war gegen CI.card gerechnet (ca. 4.65:1). Seit
   .card/tbody-tr:hover auf die helleren CI.surface/CI.surfaceHover umgestellt sind, faellt
   dieselbe Flaeche auf ca. 4.46:1 bzw. 4.18:1, unter die 4.5:1-Schwelle. Alpha auf 0.05
   gesenkt (niedrigere Deckkraft = dunklere, naeher an der jeweiligen Untergrundfarbe
   bleibende Flaeche, mehr Abstand zur mittelgrauen Textfarbe): das ergibt gegen CI.surface
   ca. 4.86:1 und gegen das ungeguenstigste reale Vorkommen, CI.surfaceHover, ca. 4.57:1,
   gegen die WCAG-Luminanzformel nachgerechnet. Vor jeder kuenftigen Anpassung an CI.muted,
   CI.surface/CI.surfaceHover oder dem rgba-Alpha-Wert hier den Kontrast neu pruefen. */
.badge-muted { color: #8A8480; background: rgba(138, 132, 128, 0.05); border-color: #131A2C; }
.badge-premium { color: #E0B75E; background: rgba(224, 183, 94, 0.12); border-color: rgba(224, 183, 94, 0.35); }
/* Der Audio-Upload-Ergebnisstatus hat vier echte, unterschiedliche Zustaende (ok/linked/skip/error,
   linked = vorhandene Datei nur verknuepft, nutzt badge-new).
   error auf muted zu legen haette Fehler optisch von skip ununterscheidbar gemacht. */
.badge-error { color: #e5697a; background: rgba(229, 105, 122, 0.12); border-color: rgba(229, 105, 122, 0.35); }
/* warn (Auftrag "Datei-Zustandspruefung"): dieselben Bernstein-Tokens wie .badge-premium und
   .msg-warning, aber eine eigene Variante, weil "premium" eine Einstufung ist und "warn" ein
   Befund. Siehe Begruendung bei BADGE_ICONS weiter unten. */
.badge-warn { color: #E0B75E; background: rgba(224, 183, 94, 0.12); border-color: rgba(224, 183, 94, 0.35); }
/* Feedback-Inbox: "new" ist ein eigener, echter Zustand (unbearbeitet, braucht Aufmerksamkeit),
   nicht dasselbe wie "read" (badge-muted) oder "resolved" (badge-ok). Nutzt CI.accent, dieselbe
   Farbe wie Buttons/Links/Fokus-Zustand im ganzen Werkzeug, hier bewusst als "aktiv, noch
   offen"-Signal. */
.badge-new { color: #82B4F0; background: rgba(130, 180, 240, 0.12); border-color: rgba(130, 180, 240, 0.35); }
/* Nur fuer wertfreie Kategorien ohne Zustand (z.B. Sorte, Ebene). Nicht fuer aktiv/inaktiv
   oder aehnliche echte Status verwenden, dafuer immer badge-ok/badge-muted, sonst wirkt ein
   echter Status wie eine bewusste Nicht-Aussage. */
.badge-neutral { color: #D8D4CE; background: #0C111F; border-color: #131A2C; }

/* Visuell verstecktes, aber fuer Screenreader vorhandenes Textelement (siehe REQUIRED_MARK
   unten). Standard-Technik, kein display:none (das wuerde auch Screenreader ausschliessen). */
.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;
}

/* Thumbnail/Platzhalter-Muster (siehe renderThumbnail() unten). Gleiche Groesse und Form fuer
   echtes Bild und Platzhalter, damit ein Wechsel zwischen beiden Zustaenden keinen
   Layout-Sprung verursacht. */
.thumb {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 8px;
  overflow: hidden;
  background: #0C111F;
  border: 1px solid #131A2C;
  flex-shrink: 0;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb-placeholder { color: #8A8480; }
/* Groessere Variante fuer Bearbeiten-/Detailansichten (renderThumbnail({ size: 'lg' })),
   zusaetzlich zur 40px-Listen-Groesse oben. */
.thumb-lg { width: 160px; height: 160px; border-radius: 12px; margin-bottom: 1rem; }

/* Profil-Kopfbereich (Nutzer-Detailseiten-Layout-Ueberarbeitung, Auftrag Punkt 7): Foto links,
   Name/Anrede/Status-Badges rechts daneben in derselben Zeile statt darunter gestapelt, damit
   der Freiraum neben einem einzelnen Foto nicht mehr ungenutzt bleibt. margin-bottom auf
   .thumb-lg wird hier zurueckgesetzt (dort fuer vertikales Stapeln gedacht, hier sitzt das Foto
   in einer horizontalen Flex-Zeile mit vertikaler Zentrierung). flex-wrap: schmale Bildschirme
   fallen automatisch auf zwei Zeilen (Foto oben, Info darunter) statt das Foto zu quetschen. */
.profile-header { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
.profile-header .thumb-lg { margin-bottom: 0; }
.profile-header-info { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
.profile-header-name { margin: 0; font-size: 1.5rem; }
/* Aktionen rechts im Kopfbereich (Klangwelt-Detailseite). margin-left: auto schiebt sie an die
   rechte Kante, ohne dem Info-Block eine feste Breite zu geben. Bricht die Zeile um, weil der
   Platz nicht reicht (.profile-header hat flex-wrap), landet der Block unter dem Namen; den
   Abstand dorthin liefert dann das row-gap von .profile-header (1.5rem).
   BEWUSST OHNE @media: ein erster Entwurf schaltete das bei 860px um und lag damit genau
   verkehrt herum. Unter 860px verschwindet die 264px-Seitenleiste, der Inhaltsbereich wird also
   BREITER, nicht schmaler. Ob die Zeile umbricht, haengt an der Breite des Inhaltsbereichs und
   an der Laenge des Namens, nicht an der Fensterbreite. Genau dafuer ist flex-wrap da. */
.profile-header-actions { margin-left: auto; display: flex; gap: 0.8rem; flex-wrap: wrap; }
.profile-header-meta { margin: 0; color: #8A8480; }
.profile-header-badges { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.2rem; }

/* Karten-Raster (Auftrag Punkt 7): auf breiten Bildschirmen mehrspaltig statt jede Card immer
   ueber die volle Breite gestapelt, faellt unterhalb von minmax() automatisch auf eine einzige
   Spalte zurueck (kein eigenes @media noetig, auto-fit erledigt das). Direkter-Kind-Selektor
   setzt den bestehenden ".card + .card"-Vertikalabstand innerhalb des Rasters zurueck (das
   grid-gap uebernimmt den Abstand hier, sonst addieren sich beide). */
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 1.5rem; margin-top: 1.5rem; }
.card-grid > .card { margin-top: 0; }

/* Eingebettetes <video>-Element (Klangwelt-Formular-Vorschau und Detailseite, siehe
   soundscapes.js). Zwei Groessen mit eigener Begruendung statt einer erzwungenen einzigen:
   die Formular-Vorschau sitzt kompakt neben anderen Feldern in einem Fieldset, die
   Detailseite hat eine eigene, geraeumigere Card. */
.video-preview { display: block; max-width: 100%; border-radius: 8px; }
.video-preview-form { max-height: 240px; }
.video-preview-detail { max-height: 320px; }

/* Leerzustand-Zeile fuer Listen-Tabellen (siehe emptyStateRow() unten). */
.empty-state td { text-align: center; color: #8A8480; padding: 2.6rem 1rem; }
.empty-state-icon { display: block; margin: 0 auto 0.7rem; color: #1A2338; }
.empty-state a { color: #82B4F0; }

/* Gedimmte Platzhalterzeile je leerer Ebene in der Klangwelt-Datei-Tabelle (siehe
   soundscapes.js). Bewusst NICHT .empty-state (das ist fuer eine einzelne, zentrierte
   Vollbreiten-Zelle gedacht, hier bleibt die Ebene-Spalte eine normale, linksbuendige Zelle
   wie in jeder anderen Zeile der Tabelle). */
.empty-ebene-row td { color: #8A8480; }

/* required-mark und feldspezifische Fehleranzeige (siehe
   REQUIRED_MARK/REQUIRED_HINT/fieldErrorClass()/fieldErrorText() unten). .field-error selbst
   existiert bereits (roter Rand, siehe oben bei input/textarea/select), hier nur die
   ergaenzende Fehlertext-Klasse dazu, in derselben Optik wie .field-hint. */
.required-mark { color: #e5697a; margin-left: 0.15rem; }
.field-error-text { display: block; margin-top: 0.3rem; color: #e5697a; font-size: 0.8rem; font-weight: 400; }

/* Schmalerer Viewport (Tablet/Handy quer): Seitenleiste wird zum Off-Canvas-Menue, per
   Hamburger-Button in der mobilen Kopfzeile ein-/ausklappbar (JS siehe UPLOAD_JS unten).
   .sidebar-backdrop faengt Klicks ausserhalb der geoeffneten Seitenleiste ab (schliesst sie),
   existiert im Markup immer, ist aber nur hier unten ueberhaupt sichtbar. */
@media (max-width: 860px) {
  .mobile-topbar {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    padding: 0.8rem 1rem;
    background: #080D18;
    border-bottom: 1px solid #131A2C;
    position: sticky;
    top: 0;
    z-index: 20;
  }
  .mobile-topbar .brand-logo { height: 18px; }
  .hamburger {
    background: transparent;
    border: none;
    color: #D8D4CE;
    padding: 0.4rem;
    border-radius: 6px;
    cursor: pointer;
    display: flex;
  }
  .hamburger:hover { background: #111729; }
  /* Review-Fund Etappe 4: nur transform verschiebt die Seitenleiste zwar visuell aus dem
     Viewport, laesst sie aber fokussierbar und im Accessibility-Tree, ein Tab-Durchlauf beim
     Laden landet dadurch zuerst auf den (unsichtbaren) Nav-Links, bevor der Hamburger-Button
     ueberhaupt erreicht wird. visibility: hidden nimmt die geschlossene Seitenleiste aus
     Tab-Reihenfolge UND Accessibility-Tree, die verzoegerte transition (0s linear 0.2s) sorgt
     dafuer, dass sie beim OEFFNEN sofort sichtbar wird (keine Verzoegerung auf dem Weg rein),
     aber erst NACH Abschluss der Schiebe-Animation wieder unsichtbar wird (keine Verzoegerung
     waere sichtbar abgeschnitten, mitten in der Animation). */
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    z-index: 30;
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform 0.2s ease, visibility 0s linear 0.2s;
  }
  .sidebar.open {
    transform: translateX(0);
    visibility: visible;
    transition: transform 0.2s ease;
    box-shadow: 8px 0 28px rgba(0, 0, 0, 0.45);
  }
  .sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 25;
  }
  .sidebar-backdrop.open { display: block; }
}

/* Schmaler Viewport: weniger Rand, etwas kleinere Ueberschriften, damit auf einem Handy nicht
   unnoetig viel Platz verschenkt wird. Die Tabelle selbst bleibt unveraendert (voller Umfang,
   lesbar), .table-wrap sorgt fuer horizontales Scrollen statt Umbruch/Abschneiden. */
@media (max-width: 640px) {
  main { padding: 1.3rem 1rem 3rem; }
  h1 { font-size: 1.4rem; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
  }
}
