/*
 * Gemeinsame Bausteine - EINE Quelle für Desktop und Mobil.
 *
 * Wird von ALLEN vier Layouts geladen (auth und main, je Desktop und Mobil).
 * Der Name war zuerst auth-components.css; die Klassen werden aber auch
 * ausserhalb der Anmeldeseiten gebraucht - etwa .auth-status in der
 * Passkey-Verwaltung, die im Haupt-Layout laeuft.
 *
 * Hintergrund: bis zu diesem Umbau existierte jede Anmeldeseite zweimal,
 * einmal unter templates/pages/ und einmal unter templates/pages/mobile/. Die
 * beiden Fassungen unterschieden sich fachlich nicht, nur in Pixelwerten und
 * Klassennamen - aber jede Korrektur musste an beiden Stellen ankommen, und
 * genau daran ist sie in dieser Runde mehrfach nur zur Hälfte angekommen.
 *
 * Mobile first: die Grundregeln gelten für schmale Fenster, die Media Query
 * ergänzt, was auf breiten dazukommt. Das ist der übliche Weg und kommt ohne
 * User-Agent-Erkennung aus - ein Desktop-Browser mit schmalem Fenster bekommt
 * dieselbe Darstellung wie ein Telefon, was bei UA-Erkennung nicht der Fall war.
 *
 * Die Farben kommen ausschließlich aus den CSS-Variablen der beiden Layouts.
 * Deshalb funktioniert dieselbe Datei in beiden, ohne sie anzugleichen.
 */

.auth-card__icon {
    width: 52px;
    height: 52px;
    border-radius: 50%;
    background: rgba(245, 158, 11, 0.15);
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 auto 0.75rem;
}

.auth-card__head {
    text-align: center;
    margin-bottom: 1.25rem;
}

.auth-card__title {
    font-size: 1.125rem;
    font-weight: 600;
    margin-bottom: 0.375rem;
}

.auth-card__text {
    color: var(--text-muted);
    font-size: 0.8125rem;
    line-height: 1.4;
}

.auth-card__hint {
    text-align: center;
    font-size: 0.75rem;
    color: var(--text-muted);
    margin-top: 0.875rem;
}

.auth-card__footer {
    text-align: center;
    margin-top: 1.25rem;
    padding-top: 1rem;
    border-top: 1px solid var(--border-color);
}

/* Ein Knopf, der wie ein Textlink aussieht. Bewusst ein <button> in einem
   POST-Formular und kein <a>: die Abbrechen-Aktion verwirft einen
   halbauthentifizierten Zustand und braucht deshalb CSRF-Schutz. Als GET-Link
   konnte eine fremde Seite sie per <img src="..."> auslösen. */
.auth-linkbutton {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    font-size: 0.8125rem;
    cursor: pointer;
    color: var(--text-muted);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
}

.auth-linkbutton:hover {
    text-decoration: underline;
}

/* Vollbreite Knöpfe untereinander - auf schmalen Fenstern die einzige
   sinnvolle Anordnung, auf breiten immer noch gut lesbar. */
.auth-actions {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

@media (min-width: 640px) {
    .auth-card__icon {
        width: 56px;
        height: 56px;
        margin-bottom: 1rem;
    }

    .auth-card__title {
        font-size: 1.25rem;
        margin-bottom: 0.5rem;
    }

    .auth-card__text {
        font-size: 0.875rem;
        line-height: 1.5;
    }
}

/* Variante des Kopf-Icons fuer Seiten, die die Primaerfarbe statt der
   Warnfarbe nutzen (Zweiter-Faktor-Eingabe). */
.auth-card__icon--primary {
    background: rgba(79, 70, 229, 0.15);
}

/* Meldung mit Symbol davor. Die Klassen alert-* und mobile-alert-* liefern
   Farbe und Rahmen, hier steht nur die Anordnung. */
.auth-meldung {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

/* Code-Eingabe: mittig, gespreizt, dicktengleiche Ziffern. Ohne
   font-variant-numeric stehen die Stellen je nach Schrift unterschiedlich
   breit und das Feld "wackelt" beim Tippen. */
.auth-code {
    text-align: center;
    font-size: 1.25rem;
    letter-spacing: 0.25rem;
    font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
    font-variant-numeric: tabular-nums;
}

.auth-submit {
    width: 100%;
    justify-content: center;
    margin-top: 1rem;
}

/* Statusmeldung der Passkey-Vorgaenge.
   Ersetzt die Inline-Styles, die die Mobil-Fassung von showStatus() einzeln
   per style-Attribut setzte - jeder davon ein Grund mehr, warum die CSP
   'unsafe-inline' tragen muss. */
.auth-status:not([hidden]) {
    /* Flex fuer Symbol und Text nebeneinander. Als Klasse, nicht als
       Inline-Style: showStatus ruft zwei Zeilen spaeter
       style.removeProperty('display') auf, um ein altes display:none aus dem
       Markup wegzuraeumen - ein hier gesetztes display:flex waere davon
       gleich mit betroffen.
       :not([hidden]), damit die versteckte Meldung versteckt bleibt. */
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.auth-status {
    padding: 0.7rem 0.9rem;
    border-radius: 6px;
    font-size: 0.85rem;
    margin-top: 0.75rem;
}

.auth-status[hidden] {
    display: none;
}

/* Auszeichnung ueber Flaeche und Balken, nicht ueber die Schriftfarbe -
   gleiche Umstellung wie bei den .alert-Klassen in app.css. Mit eingefaerbter
   Schrift lagen diese drei bei 2,03 bis 4,50:1. */
.auth-status--ok {
    background: rgba(16, 185, 129, 0.15);
    border: 1px solid rgba(16, 185, 129, 0.3);
    border-left: 4px solid var(--meldung-success);
}

/* Dritter Typ, beim Zusammenlegen fast verlorengegangen: login.php meldet den
   bestätigten Passkey mit 'info', nicht mit 'success'. Ohne eigene Klasse wäre
   diese Meldung grün als Erfolg erschienen, obwohl die Anmeldung an dieser
   Stelle noch läuft. */
.auth-status--info {
    background: rgba(59, 130, 246, 0.12);
    border: 1px solid rgba(59, 130, 246, 0.3);
    border-left: 4px solid var(--meldung-info);
}

.auth-status--error {
    background: rgba(239, 68, 68, 0.15);
    border: 1px solid rgba(239, 68, 68, 0.3);
    border-left: 4px solid var(--meldung-danger);
}

/* Fortschrittsanzeige des Proof-of-Work-Captchas (Passwort-Reset). Reine
   Sichtanzeige, fuer Screenreader versteckt; die Meldung steht in .auth-status. */
.pow-progress {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-top: 0.5rem;
}

.pow-progress[hidden] {
    display: none;
}

.pow-progress__track {
    flex: 1 1 auto;
    height: 6px;
    border-radius: 3px;
    background: rgba(128, 128, 128, 0.25);
    overflow: hidden;
}

.pow-progress__bar {
    height: 100%;
    width: 0;
    background: var(--meldung-info, #3b82f6);
    transition: width 0.3s ease;
}

/* Die Laufzeit in Sekunden wird nicht mehr gezeigt (Wunsch des Betreibers):
   der Balken sagt genug. Das Skript schreibt den Wert weiter in das Element. */
.pow-progress__detail {
    display: none;
}

/* Solange die Pruefung laeuft, sieht der Knopf auch am Desktop gesperrt aus. */
.auth-submit:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.pow-haken {
    font-weight: 700;
}

@media (prefers-reduced-motion: reduce) {
    .pow-progress__bar {
        transition: none;
    }
}

/* Das Mobil-Formular zentriert seine Statusmeldung. Als eigene Klasse statt
   als Inline-Style, damit showStatus sie stehen lassen kann. */
.auth-status--zentriert {
    text-align: center;
    /* Seit .auth-status ein Flex-Container ist, richtet text-align allein nichts
       mehr aus - der Inhalt stand links in einer sonst zentrierten Karte. */
    justify-content: center;
}

/* Ladeanzeige. Stand bis zum Umbau nur in app.css — das Mobil-Layout lädt die
   nicht, dort war der Spinner also unsichtbar. Hier gilt er für alle vier
   Layouts.

   ENTFERNT (Runde 58): die größere Variante `.spinner` stand hier, wurde aber
   im ganzen Baum nie als Klasse gesetzt — alle vier Verwendungsstellen nutzen
   `.spinner-inline`. Der einzige Treffer für „spinner" außerhalb dieser Datei
   war eine JS-Variable gleichen Namens, der eine Zeile später
   `className = 'spinner-inline'` zugewiesen wird. Sie hätte als <span> ohnehin
   nicht funktioniert: ohne `display` ist ein <span> ein Inline-Element und
   ignoriert width/height — Fläche null. Wer wieder eine große Variante
   braucht, kopiert `.spinner-inline` und setzt die Maße. */
.spinner-inline {
    width: 1rem;
    height: 1rem;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    display: inline-block;
    vertical-align: middle;
    animation: spin 0.8s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Wer weniger Bewegung eingestellt hat, bekommt eine ruhende Anzeige statt
   gar keiner - der Knopf soll trotzdem als beschäftigt erkennbar bleiben. */
@media (prefers-reduced-motion: reduce) {
    .spinner-inline {
        animation-duration: 3s;
    }
}

/* ---------------------------------------------------------------------------
   Bausteine der zusammengeführten Seiten
   --------------------------------------------------------------------------- */

/* Kurzmeldung nach einer Aktion.

   Sie schwebt nirgends mehr ueber dem Inhalt. Oben rechts lag sie auf den
   Seitenknoepfen ("Zurueck", "+ Neuer Benutzer"), unten rechts auf den
   Aktionsknoepfen der Tabellenzeilen - auf einer vollen Seite gibt es keine
   Ecke, in der ein Kasten nichts verdeckt. Stattdessen:

   - ab 1280 px sitzt eine Erfolgsmeldung in der Kopfzeile, im freien Raum
     zwischen Seitentitel und Sitzungsanzeige. Dort steht sonst nichts, und der Inhalt verschiebt
     sich weder beim Erscheinen noch beim Ausblenden.
   - darunter laeuft sie im Textfluss ueber dem Seiteninhalt mit; die
     Kopfzeile hat dort nicht genug freien Raum. */
.flash-meldung {
    margin-bottom: 1rem;
}

/* Rechtsbuendig vor der Sitzungsanzeige, nicht mittig: so bleibt bei 1280 px
   genug Breite fuer zwei Zeilen. Nur im Desktop-Layout - die Mobil-Kopfzeile
   ist anders aufgebaut.

   Nur ERFOLGSMELDUNGEN. Fehlermeldungen bleiben bei jeder Breite im Textfluss:
   sie sind bis 267 Zeichen lang (2fa_self_required) und wurden in der
   Kopfzeile abgeschnitten, ohne dass der Rest erreichbar war - und einen
   Fehler muss man ganz lesen koennen. Die Erfolgstexte sind kurz. */
@media (min-width: 1280px) {
    body.layout-desktop .alert-success.flash-meldung {
        position: fixed;
        top: 0.5rem;
        right: 16.5rem;
        z-index: 150;
        box-sizing: border-box;
        width: max-content;
        max-width: min(40rem, calc(100vw - var(--sidebar-width) - 32rem));
        max-height: calc(var(--header-height) - 1rem);
        margin: 0;
        padding: 0.25rem 0.875rem;
        font-size: 0.8125rem;
        line-height: 1.3;
        overflow: hidden;
        background-color: var(--bg-card);
        color: var(--text-primary);
        pointer-events: none;
    }
}

/* Punkt am Avatar in der Benutzerliste: offene Passwort-Reset-Anfrage. Kostet
   keine Breite - die Spalten dieser Tabelle sind gleich breit und schmal. */
.avatar-mit-hinweis {
    position: relative;
}

.avatar-mit-hinweis::after {
    content: "";
    position: absolute;
    top: -2px;
    right: -2px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--meldung-warning);
    border: 2px solid var(--bg-card);
}

/* Breite Inhalte scrollen in ihrem eigenen Kasten, nie die ganze Seite. */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* Tabelle, die auf schmalen Schirmen zu einer Kartenliste wird.
   Jede Zelle braucht dafür ein data-label mit ihrer Spaltenüberschrift; die
   erscheint dann links neben dem Wert. Das Kriterium ist die Breite, nicht die
   User-Agent-Weiche - ein Tablet im Querformat bekommt das Mobil-Layout, hat
   aber genug Platz für eine echte Tabelle. */
@media (max-width: 640px) {
    .table--stapelbar thead {
        /* Nicht display:none: das nähme die Überschriften auch aus der
           Sprachausgabe. So bleiben sie vorhanden, nur außerhalb des Bildes. */
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }

    .table--stapelbar tr {
        display: flex;
        flex-wrap: wrap;
        column-gap: 1rem;
        padding: 0.75rem 1rem;
        border-bottom: 1px solid var(--border-color);
    }

    .table--stapelbar td {
        display: flex;
        flex: 1 1 100%;
        justify-content: space-between;
        align-items: center;
        gap: 1rem;
        padding: 0.25rem 0;
        border: none;
        text-align: left;
    }

    /* Zwei kurze, zusammengehoerige Felder nebeneinander statt je einer
       eigenen Zeile (z. B. Farbe+Services, 2FA+Passkeys, Systemname+Typ) -
       die DOM-Reihenfolge bleibt dabei IMMER identisch zur Desktop-
       <thead>-Spaltenfolge, nur die Klasse hier entscheidet ueber die
       Breite. Zwei direkt aufeinanderfolgende .td-halb landen durch
       flex-wrap automatisch in derselben Zeile - nie durch Umsortieren im
       Markup, das wuerde am Schreibtisch die falsche Ueberschrift ueber den
       Wert setzen. Nur fuer kurze Werte gedacht (Badges, kurze Codes,
       Zaehler) - freier Text/Beschreibungen bleiben volle Breite, damit sie
       vernuenftig umbrechen. */
    .table--stapelbar td.td-halb {
        flex: 1 1 calc(50% - 0.5rem);
    }

    /* Bei voller Breite ist "anywhere" (siehe .table--stapelbar td code oben)
       richtig - bei halber Breite zerlegt genau diese Regel lange,
       zusammenhaengende Werte (Fremdpruefung: Service-Systemnamen wie
       "mariadb.service" wurden auf EIN Zeichen pro Zeile zusammengedrueckt,
       deshalb steht Systemname/Typ NICHT mehr als Paar in admin/services.php).
       break-word statt anywhere bricht nur noch an vernuenftigen Stellen
       (Satzzeichen/Wortgrenzen) - fuer den verbliebenen Fall (Farbe-Hexcode
       in categories.php) bleibt dadurch hoechstens EIN Umbruch mitten im
       7-stelligen Code, statt eines ganzen Buchstabenturms. */
    .table--stapelbar td.td-halb code,
    .table--stapelbar td.td-halb .font-mono {
        overflow-wrap: break-word;
    }

    .table--stapelbar td::before {
        content: attr(data-label);
        font-size: 0.8125rem;
        color: var(--text-muted);
        flex-shrink: 0;
    }

    /* Schaltflächen in der gestapelten Ansicht sind Touch-Ziele. .btn-sm ist
       32px hoch - die abgelöste Mobil-Fassung benutzte .mobile-btn mit 44px,
       und darunter trifft man am Telefon nicht mehr zuverlässig. */
    .table--stapelbar .btn {
        min-height: 44px;
        padding-left: 1rem;
        padding-right: 1rem;
    }

    /* Die erste Zelle trägt Name und Rolle und braucht die Beschriftung nicht. */
    .table--stapelbar td:first-child {
        display: block;
        padding-bottom: 0.5rem;
    }

    .table--stapelbar td:first-child::before {
        content: none;
    }

    /* Eine Aktionen-Zelle mit mehreren Knöpfen (.dienst-aktionen: Start/Stop/
       Restart/Logs in services.php, admin/services.php, dashboard.php) ist
       kein Label:Wert-Paar wie die übrigen Zellen - das bisherige
       space-between schob den Knopf-Block als Ganzes an den rechten Rand und
       zentrierte "Aktionen" mittig daneben, über die volle Höhe von zwei
       Knopf-Reihen hinweg (Nutzermeldung "sehen deplatziert aus",
       Screenshot von /services bei 390px). Gleiche Behandlung wie beim
       ersten Kind oben: Beschriftung als eigene Zeile DARÜBER, die Knöpfe
       bekommen darunter die volle Zeilenbreite und können sich gleichmäßig
       anordnen.
       :has() statt einer eigenen Klasse je Vorlage: alle drei Seiten
       benutzen exakt dieselbe .dienst-aktionen-Klasse, eine Regel deckt sie
       ab - eine Klasse pro Vorlage haette bei der naechsten neuen Seite mit
       Aktions-Knöpfen wieder vergessen werden können. */
    .table--stapelbar td:has(.dienst-aktionen) {
        display: block;
        padding-top: 0.5rem;
    }

    .table--stapelbar td:has(.dienst-aktionen)::before {
        display: block;
        margin-bottom: 0.5rem;
    }
}

.ist-eigene-sitzung {
    background: rgba(79, 70, 229, 0.1);
}

.user-avatar--klein {
    width: 32px;
    height: 32px;
    font-size: 0.75rem;
}

.card--hinweis {
    background: rgba(255, 193, 7, 0.1);
    border-color: var(--warning-text, var(--warning));
}

.text-right {
    text-align: right;
}

/* Dashboard: Dienstliste breit, Aktivitätenliste schmal. Unter 900px
   untereinander - zwei Spalten sind auf einem Telefon zwei zu schmale Spalten.
   Stand vorher als Inline-Style in der Vorlage und konnte deshalb gar nicht
   umbrechen. */
.dashboard-raster {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1.5rem;
    align-items: start;
}

@media (min-width: 901px) {
    .dashboard-raster {
        grid-template-columns: 2fr 1fr;
    }
}

.dienst-aktionen {
    display: flex;
    gap: 0.375rem;
    justify-content: flex-end;
}

/* In der gestapelten Ansicht steht die Beschriftung links, die Knöpfe rechts -
   und sie brauchen dort mehr Platz als in einer Tabellenzelle. */
@media (max-width: 640px) {
    .dienst-aktionen {
        flex-wrap: wrap;
        justify-content: flex-start;
    }
}

/* Anklickbare Kennzahl-Karte. Stand als <style>-Block mitten in der
   Dashboard-Vorlage; die Klassen sind aber nicht seitenspezifisch. */
.stat-card-link,
.stat-card-link:hover,
.stat-card-link:focus,
.stat-card-link:active,
.stat-card-link:visited {
    text-decoration: none !important;
    color: inherit;
    outline: none;
}

.stat-card-link {
    transition: border-color 0.15s, background 0.15s;
    cursor: pointer;
}

.stat-card-link:hover {
    border-color: var(--primary);
    background: rgba(79, 70, 229, 0.05);
}

.stat-card-link:active {
    background: rgba(79, 70, 229, 0.1);
}

/* Grid-Kinder bekommen von sich aus min-width:auto und schrumpfen deshalb
   nicht unter ihren Inhalt - eine breite Tabelle darin schiebt dann die ganze
   Seite auseinander, obwohl sie einen eigenen Scroll-Kasten hat. Gemessen auf
   /dashboard: 53px Überstand bei 800px, 92px bei 950px. */
.dashboard-raster > * {
    min-width: 0;
}

/* Passkey-Verwaltung. Ersetzt .content-grid und .page-header-content, die in
   der Vorlage standen, aber in keinem Stylesheet definiert waren. */
.passkey-bereich {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

/* .card bringt seinen eigenen margin-bottom mit. In einem Flex-Container
   addiert der sich zum gap - gemessen 48px statt der 24px, die die Seite
   vorher hatte. Der Abstand gehoert hier dem Container. */
.passkey-bereich > .card {
    margin-bottom: 0;
}

.page-header-inhalt {
    min-width: 0;
}

/* Ein Passkey-Eintrag: Symbol, Name mit Zeitangaben, Löschknopf. Auf schmalen
   Schirmen bricht der Knopf in eine eigene Zeile und wird vollbreit - sonst
   drängt er den Namen zusammen, und Namen sind hier frei wählbar und damit
   beliebig lang. */
@media (max-width: 480px) {
    .passkey-item {
        flex-wrap: wrap;
    }

    .passkey-actions {
        width: 100%;
    }

    .passkey-actions .btn {
        width: 100%;
        min-height: 44px;
    }
}

/* Auf dem Telefon sind das Touch-Ziele. Die abgeloeste Mobil-Fassung gab dem
   Registrier-Knopf 52px und dem Zurueck-Link 44px; mit den Desktop-Klassen
   waren es 35px und 37px.

   .page-header .btn gilt bewusst fuer ALLE Seiten mit Kopfbereich, nicht nur
   fuer die Passkey-Seite - gemessen 18 Vorlagen, dort ueberall vorher 35px.
   Ein Knopf, den man am Telefon nicht trifft, ist auf jeder davon derselbe
   Mangel. */
@media (max-width: 640px) {
    .passkey-bereich .btn,
    .page-header .btn {
        min-height: 44px;
    }

    .passkey-bereich .btn-primary {
        width: 100%;
        min-height: 52px;
    }
}


/* ---------------------------------------------------------------------------
   Symbole in Meldungen - eine Regel fuer ALLE Familien
   ---------------------------------------------------------------------------

   Das Panel hat vier davon, historisch gewachsen:
     .alert              app.css, die Meldungen im Seiteninhalt
     .mobile-alert       mobile.css, das Gegenstueck im Mobil-Layout
     .auth-status        components.css, die Statuszeile der Passkey-Wege
     .global-toast       app.css, die Kurzmeldung
     .maintenance-banner das Wartungsband im Haupt-Layout
   Alle sind Flex-Container. In einem Flex-Container gibt ein SVG seine Breite
   her, sobald der Text mehr braucht - gemessen bis auf 4,1px herunter, bei
   einer Sollgroesse von 20px.

   Das Symbol traegt die Unterscheidung der Meldungsarten, weil die vier
   Balkenfarben gegeneinander nur 1,02 bis 1,49:1 erreichen. Ein Symbol, das auf
   die halbe Groesse schrumpft, traegt sie nicht mehr.

   Eine Regel statt fuenf, weil die Erfahrung dieser Runde ist: fuenf Stellen
   sind fuenf Gelegenheiten, eine zu vergessen. */
.alert > svg,
.mobile-alert > svg,
.auth-status > svg,
.global-toast svg,
.maintenance-banner svg {
    flex-shrink: 0;
}

/* Die Ziel-Spalte des Protokolls kuerzt lange Werte mit Auslassungspunkten -
   in der gestapelten Ansicht darf sie das nicht, dort ist Platz. */
.zelle-gekuerzt {
    max-width: 200px;
    overflow: hidden;
    text-overflow: ellipsis;
}

@media (max-width: 640px) {
    .zelle-gekuerzt {
        max-width: none;
        overflow: visible;
        text-overflow: clip;
        /* overflow-wrap steht hier nicht mehr: die Regel fuer
           .table--stapelbar td weiter unten deckt denselben Fall ab, und zwar
           samt der Kinder code und .font-mono, die den geerbten Wert sonst
           ueberschreiben. Doppelt gesetzt waere nur die Frage, welche Zeile
           gilt. */
    }
}

/* Zwei Spalten nebeneinander, unter 900px untereinander. Stand in profile.php
   als Inline-Style und schlug damit jede Media-Query - auf 390px standen die
   beiden Spalten weiter nebeneinander und die Seite war 493px breit. */
.two-col-layout {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1.5rem;
    align-items: start;
}

@media (min-width: 901px) {
    .two-col-layout {
        grid-template-columns: 1.5fr 1fr;
    }
}

.two-col-layout > * {
    min-width: 0;
}

/* Zwei Formularfelder nebeneinander, unter 640px untereinander. Das Muster
   stand neunmal als Inline-Style im Baum (settings, service-edit, user-edit,
   category-edit) und war damit gegen jede Media-Query immun: ein Eingabefeld
   bringt rund 177px Mindestbreite mit, zwei davon plus Abstand passen auf
   390px nicht nebeneinander. */
.feld-paar {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
}

@media (min-width: 641px) {
    .feld-paar {
        grid-template-columns: 1fr 1fr;
    }

    .feld-paar--breit-schmal {
        grid-template-columns: 2fr 1fr;
    }
}

.feld-paar > * {
    min-width: 0;
}

/* Tabelle mit gleichmaessig geteilten Spalten.
   table-layout:fixed allein war nicht das Problem der alten Fassung - die
   festen px-Breiten an den Ueberschriften waren es, die sich auf 390px nicht
   unterbieten liessen. Ohne fixed dehnt sich die Tabelle dagegen nach ihrem
   Inhalt aus: gemessen 1079px in einem 954px breiten Kasten, wodurch die
   Aktionen-Spalte samt Loeschen-Knopf aus dem Bild rutschte.
   Beides zusammen - fixed, aber ohne feste Breiten - teilt den vorhandenen
   Platz auf und laesst die Spalten mitschrumpfen. */
@media (min-width: 641px) {
    .table--gleichmaessig {
        table-layout: fixed;
        width: 100%;
    }

    .table--gleichmaessig td {
        overflow-wrap: anywhere;
    }
}

/* Ein <code> darf die Seite nie auseinanderschieben.

   Aufgefallen an der IP der letzten Anmeldung: als IPv4 harmlos, als IPv6 sind
   es bis zu 45 Zeichen ohne Trennstelle, und die Seite lief bei 390px seitlich
   ueber - gemessen 405px im Desktop-Layout, 409px im Mobil-Layout. Das
   overflow-x: hidden am body faengt das nicht, weil html nicht mitgesetzt ist.
   Die Spalte ist varchar(45), und getClientIp validiert mit FILTER_VALIDATE_IP
   ohne Beschraenkung auf IPv4 - der Fall ist echt erreichbar, nicht theoretisch.

   Steht hier und nicht in app.css, weil auch die auth-Layouts es brauchen:
   layouts/mobile/auth.php laedt app.css nicht. Betroffen waren drei Stellen
   (admin/user-edit.php, profile.php und die Kartenansicht); eine Regel ist
   besser als drei Inline-Stile, weil die naechste IP-Ausgabe sie sonst wieder
   vergisst.

   break-word und NICHT anywhere: anywhere senkt zusaetzlich die
   kleinstmoegliche Breite des Elements auf EIN Zeichen. In den Tabellen mit
   automatischer Spaltenbreite fallen die Spalten dadurch zusammen - gemessen
   brach '192.168.1.10' zwischen 700 und 1400px zweizeilig um, 'nginx'
   dreizeilig.

   Was break-word dafuer NICHT leistet, damit sich niemand darauf verlaesst:
   in einem Kasten mit overflow-x: auto bricht es gar nicht, weil dort kein
   Platzdruck entsteht - die Tabelle wird stattdessen breiter. Auf
   /admin/sessions und /admin/audit-log steht die IPv6 deshalb genauso weit
   rechts wie ganz ohne Regel (gemessen 481 bzw. 485px). Das ist hinnehmbar:
   der table-scroll-Rahmen haelt den Ueberlauf von der SEITE fern, und
   Querscrollen im Rahmen ist dort vorgesehen. Wirksam ist die Regel an den
   drei Stellen ausserhalb von Tabellen - und genau die liefen ueber.

   Ebenfalls nicht behoben: die Kuerzung mit Auslassungspunkten in
   .zelle-gekuerzt bleibt wirkungslos, mit anywhere wie mit break-word. */
code {
    overflow-wrap: break-word;
}

/* Passkey-Namen sind frei waehlbar, duerfen 100 Zeichen lang sein und kein
   einziges Leerzeichen enthalten. Ohne Umbruch schob so ein Name die Seite bei
   390px auf 1141px auseinander.

   Stand bis Runde 44 als <style>-Block in profile-passkeys.php. Dieselben Namen
   zeigt aber auch admin/user-edit.php, und dort fehlte die Regel - eine Kopie
   waere die vierzehnte Zwillingsstelle dieser Runde geworden.

   anywhere und nicht break-word: hier ist genau das Zerlegen an beliebiger
   Stelle gewollt, weil ein 100 Zeichen langer Name ohne Leerzeichen sonst
   nirgends umbricht. Die Einschraenkung, die bei code gegen anywhere sprach
   (kollabierende Tabellenspalten), greift hier nicht: der Name steht in einer
   Spalte, die ohnehin den Rest bekommt. */
.passkey-name {
    font-weight: 500;
    margin-bottom: 0.25rem;
    overflow-wrap: anywhere;
}

/* Der Hinweis, dass eine Liste leer ist ("Keine aktiven Sessions.",
   "Noch keine Passkeys hinterlegt.").

   Stand bis Runde 44 als <style>-Block in profile-passkeys.php, wurde aber auch
   von admin/sessions.php benutzt - dort ohne jede Wirkung: gemessen textAlign
   start statt center und padding 0 statt 2rem. Aufgefallen erst, als aus
   demselben Block eine andere Regel herausgenommen wurde. */
.empty-state {
    text-align: center;
    padding: 2rem;
}

/* Der Text in den Dialogen von app.js.
   
   Er nennt haeufig einen frei gewaehlten Namen ("Passkey 'xy' wirklich
   entziehen?"), und der darf 100 Zeichen ohne Leerzeichen haben. white-space:
   pre-line erhaelt zwar Zeilenumbrueche, bricht aber kein langes Wort: gemessen
   941px Inhalt in einem 276px breiten Dialog, ohne Rollbalken - der Name war
   also mittendrin abgeschnitten, und man sah nicht mehr, was man da loescht.
   
   Hier ist anywhere richtig und nicht break-word: der Dialog hat keine
   Tabellenspalten, die kollabieren koennten, und der Name MUSS vollstaendig
   lesbar sein, bevor jemand auf Bestaetigen drueckt. */
#globalConfirmMessage,
#globalAlertMessage {
    overflow-wrap: anywhere;
}

/* Die Schriftart selbst steht in app.css und mobile.css - hier nur der Umbruch,
   damit er in BEIDEN Layouts gilt, ohne die beiden Schriftdefinitionen
   anzufassen.

   Traegt Dienstnamen in dashboard.php und services.php und das TOTP-Geheimnis
   in admin/user-edit.php - in audit-log.php gibt es die Klasse NICHT, das stand
   hier laenger falsch. Dienstnamen sind frei konfiguriert und enthalten
   typischerweise kein Leerzeichen. break-word und nicht anywhere: einige dieser
   Stellen liegen in Karten mit Rasterspalten, die sonst kollabieren koennten -
   dieselbe Ueberlegung wie bei code.

   Am TOTP-Feld in admin/user-edit.php ist die Klasse inzwischen wirkungslos:
   auf ein einzeiliges <input> wirkt overflow-wrap nicht, und die Schriftart
   setzt dort ein eigener Inline-Stil. Sie steht dort nur noch aus Gewohnheit.

   Wirkt damit nur dort, wo Platzdruck herrscht: gemessen in der
   Aktivitaetskarte des Dashboards (843 auf 422px). In den Tabellenzellen von
   services bricht nichts, weil der table-scroll-Rahmen den Druck wegnimmt -
   dort haelt der Rahmen den Ueberlauf von der Seite fern, und Querscrollen ist
   vorgesehen.

   Unterhalb 640px nennt die .table--stapelbar-Regel weiter unten diese Klasse
   ausdruecklich mit. Das ist noetig, nicht redundant: diese Deklaration hier
   wuerde den vom td geerbten Wert sonst schlagen. */
.font-mono {
    overflow-wrap: break-word;
}

/* In der gestapelten Ansicht duerfen lange Werte umbrechen - und muessen es.
   
   Unterhalb 640px ist jede Zelle ein eigener Block ohne Spaltenkonkurrenz; die
   Ueberlegung, die oberhalb GEGEN anywhere spricht (kollabierende Spalten),
   greift hier also nicht. Umgekehrt richtet das Fehlen echten Schaden an:
   gemessen stand ein 100 Zeichen langer Passkey-Name als 919px breite Zelle in
   einem 316px-Rahmen, und der Knopf zum Entziehen war nur per Querscrollen
   erreichbar.
   
   Deshalb hier und nicht als Klasse am Element: der Unterschied haengt an der
   Darstellungsform, nicht am Inhalt. */
@media (max-width: 640px) {
    /* Die Kinder muessen einzeln genannt werden.
       
       overflow-wrap vererbt sich zwar, aber eine Deklaration AM KIND schlaegt
       den geerbten Wert - und code sowie .font-mono setzen weiter oben
       break-word. In einem Flex-Item senkt break-word die kleinstmoegliche
       Breite nicht, also blieb die Zelle breit: gemessen 708 bis 823px in einem
       364px-Rahmen, mit Knoepfen ausserhalb. An vier von acht stapelbaren
       Tabellen wirkte die Regel deshalb gar nicht, obwohl der Selektor passt.
       Am Elternelement gesetzt, vom Kind ueberschrieben - beim Lesen nicht
       sichtbar. */
    .table--stapelbar td,
    .table--stapelbar td code,
    .table--stapelbar td .font-mono {
        overflow-wrap: anywhere;
    }
}

/* Der Untertitel der Kopfzeile traegt an vier Bearbeiten-Seiten einen frei
   gewaehlten Namen (Benutzer, Kategorie, Dienst, Gruppe). Ohne Umbruch schob
   ein Name ohne Leerzeichen die SEITE auseinander - gemessen 825 bis 912px bei
   390px Bildbreite. Der einzige verbliebene echte Seitenueberlauf des Baums. */
.page-subtitle,
.page-title {
    overflow-wrap: anywhere;
}

/* Dieselbe Begruendung wie bei #globalConfirmMessage, andere Stellen: Dialoge
   und Modaltitel, die einen frei gewaehlten Namen nennen.

   #statusToggleMessage ist der seiteneigene Dialog in admin/services (gemessen
   943px Text in 308px Breite - der Dienstname war nicht lesbar, obwohl man
   gleich seinen Status aendern soll). #logsModalTitle traegt in services.php
   den Dienstnamen in der Ueberschrift (1141px, Seitenueberlauf schon bei 900px
   Bildbreite). .form-check und .group-tag tragen Gruppennamen in den
   Berechtigungsrastern der beiden Bearbeiten-Seiten (900 bzw. 952px).

   Der Zwilling faellt hier besonders leicht durch: die zentralen Dialoge aus
   app.js waren gehaertet, die seiteneigenen nicht. */
#statusToggleMessage,
#logsModalTitle,
.form-check,
.group-tag {
    overflow-wrap: anywhere;
}

/* Textlink als Touch-Ziel (Passwort-Reset, Runde 84): mindestens 44 px hohe,
   volle Klickflaeche, mittig - .auth-linkbutton allein ist nur 16 px hoch. */
.auth-linkbutton--touch {
    min-height: 44px;
    min-width: 44px;
    padding: 0 0.75rem;
    justify-content: center;
}

/* Fusszeile ohne Trennlinie (Link unter einem Button, z. B. Fehlerseite). */
.auth-card__footer--ohne-linie {
    border-top: none;
    margin-top: 0.5rem;
    padding-top: 0;
}

/* Ein <a class="btn"> als Knopf: Linkunterstreichung weg, Text mittig. */
.auth-submit--link {
    text-decoration: none;
    text-align: center;
    min-height: 44px;
    align-items: center;
}

.auth-retry {
    width: 100%;
    justify-content: center;
    margin-top: 0.5rem;
}

.auth-retry[hidden] {
    display: none;
}

/* === Passwort-Checkliste (Runde 88) =========================================
   Markup: templates/partials/passwort-liste.php, Verhalten:
   assets/js/passwort-liste.js. EINE Fassung fuer Reset-Seite, Profil und
   Benutzer-Anlage; ersetzt .password-req* aus app.css (das Auth-Layout laedt
   app.css nicht).

   Der Zustand haengt nie nur an der Farbe: erfuellt = Haken, offen = Kreis,
   dazu fuer die Sprachausgabe ein Wort je Punkt (.pw-nur-vorlesen).

   Ohne JavaScript fehlt .pw-liste--aktiv: dann stehen neutrale Punkte da,
   kein "offen", das nie umspringen wuerde.

   Farben nur aus den Variablen der Layouts. Haken in --meldung-success
   (gegen --bg-card dunkel 5,77:1, hell 5,48:1 - Werte aus layouts/auth.php),
   Text in --text-primary bzw. --text-muted. */
.pw-liste {
    margin-top: 0.5rem;
    padding: 0.625rem 0.75rem;
    border: 1px solid var(--border-color);
    border-radius: 0.375rem;
    font-size: 0.8125rem;
    line-height: 1.4;
    text-align: left;
}

.pw-liste__titel {
    margin: 0 0 0.25rem;
    font-weight: 600;
    color: var(--text-primary);
}

.pw-liste__punkte {
    list-style: none;
    margin: 0;
    padding: 0;
}

.pw-liste__punkte--arten {
    padding-left: 1.5rem;
}

.pw-punkt {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 0.375rem;
    padding: 0.125rem 0;
    color: var(--text-muted);
}

.pw-punkt__marke {
    flex: 0 0 1.125rem;
    text-align: center;
    font-weight: 700;
}

.pw-punkt__text {
    flex: 1 1 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

.pw-punkt__zaehler {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.pw-punkt__marke::before {
    content: "\2022";
}

.pw-liste--aktiv .pw-punkt__marke::before {
    content: "\25CB";
}

.pw-liste--aktiv .pw-punkt--erfuellt {
    color: var(--text-primary);
}

.pw-liste--aktiv .pw-punkt--erfuellt .pw-punkt__marke {
    color: var(--meldung-success);
}

.pw-liste--aktiv .pw-punkt--erfuellt .pw-punkt__marke::before {
    content: "\2713";
}

/* Vierte Art, wenn drei schon reichen: weder Mangel noch Haken. */
.pw-liste--aktiv .pw-punkt--frei .pw-punkt__marke::before {
    content: "\2013";
}

.pw-liste__zeichen {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    white-space: nowrap;
}

.pw-liste__hinweis {
    margin: 0.375rem 0 0;
    color: var(--text-muted);
    font-size: 0.75rem;
}

/* Knopf "Passwort anzeigen". Steht im Markup mit `hidden` und wird erst vom
   Skript freigegeben - ohne JavaScript gaebe es sonst einen toten Knopf.
   Die eigene [hidden]-Regel ist noetig, weil display:inline-flex das
   hidden-Attribut sonst ueberstimmt. 44px Mindesthoehe als Touch-Ziel. */
.pw-zeigen {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    min-height: 44px;
    margin: 0.5rem 0 0;
    padding: 0 0.75rem;
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--text-primary);
    background: transparent;
    border: 1px solid var(--border-color);
    border-radius: 0.375rem;
    cursor: pointer;
    width: auto;
}

.pw-zeigen[hidden] {
    display: none;
}

.pw-zeigen:hover {
    border-color: var(--text-secondary);
}

.pw-zeigen:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* Gedrueckt = Passwort ist sichtbar: gefuellte Flaeche UND das Auge verliert
   seinen Strich. Im Ruhezustand ist das Auge durchgestrichen (verdeckt). */
.pw-zeigen[aria-pressed="true"] {
    background: var(--bg-hover);
    border-color: var(--text-secondary);
}

.pw-zeigen[aria-pressed="true"] .pw-zeigen__strich {
    display: none;
}

.pw-abgleich {
    display: flex;
    align-items: baseline;
    gap: 0.375rem;
    margin: 0.375rem 0 0;
    font-size: 0.8125rem;
    color: var(--text-primary);
}

.pw-abgleich[hidden] {
    display: none;
}

.pw-abgleich .pw-punkt__marke {
    color: var(--meldung-danger);
}

.pw-abgleich .pw-punkt__marke::before {
    content: "\2717";
}

.pw-abgleich--gleich .pw-punkt__marke {
    color: var(--meldung-success);
}

.pw-abgleich--gleich .pw-punkt__marke::before {
    content: "\2713";
}

/* Nur fuer die Sprachausgabe. */
.pw-nur-vorlesen {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
