<?php
/**
 * @package     com_htprotect
 * @copyright   (C) 2026 Pascal Lohmann / website-bereinigung.de
 * @license     GNU General Public License version 2 or later
 *
 * Oberflaeche: eigenstaendiges dunkles Panel im Joomla-Backend.
 * Reines Inline-HTML/CSS/JS ohne Framework-Abhaengigkeiten,
 * dadurch identisch von Joomla 2.5 bis 6.
 */

defined('_JEXEC') or die;

class HtpUi
{
    /**
     * Komplette Seite ausgeben.
     *
     * @param   string  $sec       Aktiver Bereich
     * @param   array   $cfg       Konfiguration
     * @param   array   $messages  Liste von ['type' => ok|err|warn|info, 'html' => string]
     * @param   array   $data      Bereichsdaten (status, guard, preview, ...)
     *
     * @return void
     */
    public static function page($sec, $cfg, $messages, $data)
    {
        // X-Content-Type-Options auf der eigenen Admin-Antwort: nimmt Browsern (und
        // manchen Browser-Virenscannern) das MIME-Sniffing, das die HTProtect-Scan-
        // Seite faelschlich als ausfuehrbares Script/Schadskript einstufen kann. Rein
        // praeventiv, ohne Funktions- oder Sicherheitsaenderung. Nur senden, solange
        // Joomla noch keine Header rausgeschrieben hat (sonst stiller No-Op).
        if (!headers_sent()) {
            header('X-Content-Type-Options: nosniff');
        }

        // Joomla-Subhead fuellen (sonst leerer Balken / in J3 ueber dem Logo).
        // Desktop-Titel "<Bereich> - <Nutzen>"; das JS tauscht ihn auf Mobil
        // gegen den reinen Nutzen (htpx-tbf/htpx-tbm). Muss vor dem Template-
        // Rendern gesetzt werden - hier am Anfang von page() (noch in run()).
        $titles = self::sectionTitles($sec);
        htp_toolbar_title($titles['desktop'], 'shield');

        self::headStyles();
        self::css();

        // Mobil: Version + URL als schmale Zeile UEBER der Karte (Desktop: in der
        // Kopfzeile via header()). Eigenes .htpx-Element, damit die Theme-Variablen
        // gelten; auf Desktop ausgeblendet. Vor #htpx-root, damit es darueber steht.
        echo '<div class="' . self::rootClass() . ' htpx-abovecard">' . self::metaHtml($sec) . '</div>';

        echo '<div id="htpx-root" class="' . self::rootClass() . '">';
        self::themeBootScript();
        self::header($sec);
        self::updateBanner($data);

        echo '<div class="htpx-body">';
        self::nav($sec, $data);

        echo '<main class="htpx-main">';

        foreach ($messages as $message) {
            echo '<div class="htpx-msg htpx-msg-' . htp_esc($message['type']) . '">' . $message['html'] . '</div>';
        }

        switch ($sec) {
            case 'guard':
                self::secGuard($cfg, $data);
                break;
            case 'shield':
                self::secShield($cfg, $data);
                break;
            case 'scan':
                self::secScan($cfg, $data);
                break;
            case 'backups':
                self::secBackups($cfg, $data);
                break;
            case 'help':
                self::secHelp($cfg, $data);
                break;
            case 'connect':
                self::secConnect($cfg, $data);
                break;
            default:
                self::secOverview($cfg, $data);
                break;
        }

        echo '</main></div>';
        self::footer();
        echo '</div>';

        self::js($titles);
    }

    /**
     * Schmales Update-Banner ganz oben (alle Sektionen), wenn eine neuere HTProtect-
     * Version verfuegbar ist. Info-Stil (blau, KEIN Alarm - es ist etwas Gutes);
     * Ein-Klick auf Joomlas Update-Ansicht. Zeigt nichts, wenn kein Update ansteht.
     *
     * @param   array  $data  Anzeigedaten (erwartet $data['htp_update'] = Zielversion)
     *
     * @return void
     */
    private static function updateBanner($data)
    {
        $ver = isset($data['htp_update']) ? (string) $data['htp_update'] : '';

        if ($ver === '') {
            return;
        }

        echo '<div class="htpx-updbar">'
            . '<span class="htpx-updbar-ico" aria-hidden="true">&#8679;</span>'
            . '<span>' . htp_t('COM_HTPROTECT_UPDATE_AVAILABLE', htp_esc($ver)) . '</span>'
            . '<a class="htpx-updbar-btn" href="index.php?option=com_installer&amp;view=update">'
            . htp_t('COM_HTPROTECT_UPDATE_NOW') . '</a>'
            . '</div>';
    }

    /**
     * Toolbar-/Subhead-Titel je Bereich. 'desktop' = "<Bereich> - <Nutzen>"
     * (ohne Marken-Praefix), 'mobile' = nur der Nutzen (erster Buchstabe gross,
     * ausser .htaccess). Hilfe hat keinen Nutzenzusatz -> mobil = desktop.
     *
     * @param   string  $sec  Aktiver Bereich
     *
     * @return array ['desktop' => string, 'mobile' => string] (mit HTML-Entities)
     */
    private static function sectionTitles($sec)
    {
        $map = array(
            'overview' => array('COM_HTPROTECT_TBTITLE_OVERVIEW', 'COM_HTPROTECT_TBTITLE_OVERVIEW_M'),
            'guard'    => array('COM_HTPROTECT_TBTITLE_GUARD',    'COM_HTPROTECT_TBTITLE_GUARD_M'),
            'shield'   => array('COM_HTPROTECT_TBTITLE_SHIELD',   'COM_HTPROTECT_TBTITLE_SHIELD_M'),
            'scan'     => array('COM_HTPROTECT_TBTITLE_SCAN',     'COM_HTPROTECT_TBTITLE_SCAN_M'),
            // Backups-Titel nur >= 5.4 zu "Backups & Updates"; darunter unveraendert.
            'backups'  => self::backupsTitleKeys(),
            'help'     => array('COM_HTPROTECT_TBTITLE_HELP',     'COM_HTPROTECT_TBTITLE_HELP'),
        );
        $keys = isset($map[$sec]) ? $map[$sec] : $map['overview'];

        // Entities aufloesen -> echtes "Ü" usw. Joomla escaped den Toolbar-Titel
        // fuers <title>-Tag; uebergaeben wir Entities, kaeme dort "&Uuml;" raus.
        return array(
            'desktop' => html_entity_decode(htp_t($keys[0]), ENT_QUOTES, 'UTF-8'),
            'mobile'  => html_entity_decode(htp_t($keys[1]), ENT_QUOTES, 'UTF-8'),
        );
    }

    /**
     * Erscheint das Auto-Update-Feature auf dieser Joomla-Version? Dann heisst der Bereich
     * "Backups & Updates", sonst "Sicherungen". Deckt beide Auto-Update-Aeste ab: den
     * 5.4+-Pfad UND den Joomla-3-Legacy-Pfad (execEligible) - so ist die UI auf J3
     * identisch zu J5/6. Nur Joomla 4.0-<5.4 bleibt bei "Sicherungen" (Feature ausgespart).
     *
     * @return boolean
     */
    private static function backupsUpdates()
    {
        return class_exists('HtpAutoUpdate') && HtpAutoUpdate::execEligible();
    }

    /**
     * Joomla 4.0-<5.4: Auto-Update ist hier NICHT verfuegbar, aber der Bereich heisst trotzdem
     * "Backups & Updates" und zeigt statt der Auto-Update-Oberflaeche einen Hinweis, auf
     * Joomla 5 zu upgraden (damit das Feature nutzbar wird).
     *
     * @return boolean
     */
    private static function upgradeHint()
    {
        return class_exists('HtpAutoUpdate') && HtpAutoUpdate::upgradeRecommended();
    }

    /**
     * Heisst der Bereich "Backups & Updates"? Ab Joomla 3: entweder Auto-Update aktiv (5.4+/J3)
     * ODER die Upgrade-Hinweis-Range (4.0-<5.4). Nur darunter (J2.5) bleibt es "Sicherungen".
     *
     * @return boolean
     */
    private static function backupsSectionNamed()
    {
        return self::backupsUpdates() || self::upgradeHint();
    }

    /** Nav-Label des Backups-Bereichs ("Backups & Updates" ab Joomla 3, sonst "Sicherungen"). */
    private static function backupsNavLabel()
    {
        return self::backupsSectionNamed()
            ? htp_t('COM_HTPROTECT_NAV_BACKUPS_UPD') : htp_t('COM_HTPROTECT_NAV_BACKUPS');
    }

    /** Toolbar-Titel-Schluessel des Backups-Bereichs ("Backups & Updates"-Variante oder Original). */
    private static function backupsTitleKeys()
    {
        return self::backupsSectionNamed()
            ? array('COM_HTPROTECT_TBTITLE_BACKUPS_UPD', 'COM_HTPROTECT_TBTITLE_BACKUPS_UPD_M')
            : array('COM_HTPROTECT_TBTITLE_BACKUPS', 'COM_HTPROTECT_TBTITLE_BACKUPS_M');
    }

    /**
     * CSS-Klassen des Wurzel-Containers je nach Joomla-Generation:
     *  - htpx-legacy : Joomla 2.5/3 (Bootstrap-2-Aera) - gleicht die dort per
     *                  Element+Attribut erzwungenen Feldhoehen aus.
     *  - htpx-light  : Joomla 2.5-4 laufen grundsaetzlich im Light-Mode (helles
     *                  Backend). J5/6 folgen dem in Joomla gewaehlten Schema (JS).
     *
     * @return string
     */
    private static function rootClass()
    {
        $major = preg_match('/^(\d+)/', htp_jversion(), $m) ? (int) $m[1] : 0;
        $class = 'htpx';

        if ($major >= 2 && $major <= 3) {
            $class .= ' htpx-legacy';
        }

        if ($major >= 2 && $major <= 4) {
            $class .= ' htpx-light';
        }

        return $class;
    }

    /**
     * Zwei kleine Regeln in den DOKUMENT-KOPF (vor dem ersten Paint), damit
     * beim Seitenwechsel nichts springt:
     *  1) Leere Toolbar-Leiste sofort ausblenden. Die Komponente registriert KEINE
     *     Toolbar-Buttons, der Titel sitzt im Seitenkopf -> der Balken ist immer
     *     leer. In Atum (J4-6) ist das #subhead-container; in J2.5/3 (Isis) der
     *     leere ".btn-subhead"-Toggle ("Werkzeugleiste"). Jede Regel greift nur auf
     *     ihrer Generation (das jeweils andere Element existiert dort nicht). Per JS
     *     erst spaeter auszublenden lieszt sie kurz aufblitzen (vertikaler Sprung).
     *  2) Am Desktop die vertikale Scrollleiste IMMER reservieren
     *     (overflow-y:scroll). Sonst verschiebt sich der Inhalt minimal nach
     *     links/rechts, je nachdem ob eine Seite scrollbar ist oder nicht.
     *
     * Bewusst in den <head> (addStyleDeclaration) statt inline im Body: nur so
     * gilt es schon beim ersten Paint. Stark gekapselt -> bricht nie.
     *
     * @return void
     */
    private static function headStyles()
    {
        $css = '@media (min-width:992px){html{overflow-y:scroll}}'
            . '#subhead-container{display:none !important}'
            . '.btn-subhead{display:none !important}'
            // Leerer Toolbar-Burger (Atum J4-6): der navbar-toggler steuert die
            // #subhead-container-Leiste, die HTProtect nicht nutzt -> er klappt
            // nichts aus. Gezielt nur den TOOLBAR-Toggler ausblenden (Klasse
            // .toggler-toolbar bzw. aria-controls/Toggle auf #subhead-container) -
            // NIE den Sidebar-Hamburger (.navbar-toggler ohne diese Merkmale).
            . '.navbar-toggler.toggler-toolbar{display:none !important}'
            . '.navbar-toggler[data-bs-target="#subhead-container"]{display:none !important}'
            . '.navbar-toggler[aria-controls="subhead-container"]{display:none !important}';

        try {
            if (class_exists('Joomla\\CMS\\Factory')) {
                $app = Joomla\CMS\Factory::getApplication();
                $doc = ($app && method_exists($app, 'getDocument')) ? $app->getDocument() : null;

                if ($doc && method_exists($doc, 'addStyleDeclaration')) {
                    $doc->addStyleDeclaration($css);

                    return;
                }
            }
        } catch (\Throwable $e) {
            // Fallback unten
        }

        // Aeltere Generationen ohne erreichbares Document-Objekt: inline (der
        // Scrollbar-Teil greift weiterhin; #subhead-container gibt es dort nicht).
        echo '<style>' . $css . '</style>';
    }

    /**
     * Anti-Flackern (FOUC): ein SYNCHRONES Inline-Script direkt NACH dem
     * Wurzel-Element setzt die Theme-Klasse htpx-light VOR dem ersten Paint.
     * Ohne das wuerde J5/6 die Seite kurz dunkel (Default) rendern und erst
     * das spaetere Theme-JS auf hell umschalten -> sichtbares Dark-Aufblitzen
     * beim Seitenwechsel im Light-Mode. J2.5-4 brauchen es nicht (htpx-light
     * steht dort serverseitig fest). Das spaetere Theme-JS uebernimmt danach
     * nur noch LIVE-Wechsel (Umschalten/Systemwechsel).
     *
     * @return void
     */
    private static function themeBootScript()
    {
        $major = preg_match('/^(\d+)/', htp_jversion(), $m) ? (int) $m[1] : 0;

        if ($major < 5) {
            return;
        }

        echo '<script>(function(){try{'
            . "var els=document.querySelectorAll('.htpx');if(!els.length){return;}"
            . "var t=document.documentElement.getAttribute('data-bs-theme');var dark;"
            . "if(t==='dark'){dark=true;}else if(t==='light'){dark=false;}"
            . "else{dark=!!(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches);}"
            . "for(var i=0;i<els.length;i++){if(dark){els[i].classList.remove('htpx-light');}else{els[i].classList.add('htpx-light');}}"
            . '}catch(e){}})();</script>';
    }

    // ─────────────────────────────────────────────────────────────────
    //  Rahmen
    // ─────────────────────────────────────────────────────────────────

    private static function header($sec = '')
    {
        echo '<div class="htpx-shellhead">'
            . '<div class="htpx-logo">'
            . '<span class="htpx-logomark">' . self::logoMark() . '</span>'
            . '<span class="htpx-logowrap">'
            .   '<span class="htpx-logorow">'
            .     '<span class="htpx-logotext"><span class="htpx-lt-ht">HT</span><span class="htpx-lt-pr">Protect</span></span>'
            .     '<span class="htpx-edition">' . htp_t('COM_HTPROTECT_EDITION') . '</span>'
            .   '</span>'
            .   '<span class="htpx-logotag">' . htp_t('COM_HTPROTECT_TAGLINE') . '</span>'
            . '</span>'
            . '</div>'
            . self::metaHtml($sec)
            . '</div>';
    }

    /**
     * Logo-Mark (Schild mit Schloss) als Inline-SVG. Der gruene Rand nimmt die
     * Theme-Akzentfarbe (var(--acc), hell/dunkel), Schild-Fuellung + Schloss sind
     * fest (Navy/Weiss) - lesbar auf hellem UND dunklem Header.
     */
    private static function logoMark()
    {
        $shield = 'M10 5 H38 Q43 5 43 10 V27 Q43 41.5 24 51.5 Q5 41.5 5 27 V10 Q5 5 10 5 Z';

        return '<svg class="htpx-logomark-svg" viewBox="0 0 48 56" width="36" height="42" aria-hidden="true" focusable="false">'
            . '<path d="' . $shield . '" fill="var(--acc)"/>'
            . '<path d="' . $shield . '" fill="#25394d" transform="translate(24 26) scale(0.84) translate(-24 -26)"/>'
            . '<path d="M19.5 25 V20.5 A4.5 4.5 0 0 1 28.5 20.5 V25" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/>'
            . '<rect x="16.6" y="24" width="14.8" height="12.6" rx="2.3" fill="#fff"/>'
            . '<circle cx="24" cy="29.4" r="1.7" fill="#25394d"/>'
            . '<rect x="23.2" y="29.4" width="1.6" height="4" rx=".8" fill="#25394d"/>'
            . '</svg>';
    }

    /** Version + Anbieter-Link + Doku (Desktop: in der Kopfzeile; Mobil: ueber der Karte). */
    private static function metaHtml($sec = '')
    {
        return '<div class="htpx-headmeta">v' . htp_esc(HTP_VERSION)
            . ' &middot; <a href="https://htprotect.org" target="_blank" rel="noopener">htprotect.org</a>'
            . ' &middot; ' . self::docsLink($sec)
            . ' &middot; ' . self::remoteLink()
            . '</div>';
    }

    /**
     * Doku-Link (Buch-Icon) mit Sprungmarke auf den aktuellen Bereich (#sec).
     * Sprache folgt der UI (de -> /dokumentation, en -> /en/documentation).
     * Die #-Anker (overview/guard/shield/...) sind auf der Doku-Seite noch nicht
     * vergeben - werden hier bewusst schon mitgegeben (zukunftssicher).
     */
    private static function docsLink($sec)
    {
        $valid  = array('overview' => 1, 'guard' => 1, 'shield' => 1, 'scan' => 1, 'backups' => 1, 'help' => 1);
        $anchor = isset($valid[$sec]) ? $sec : 'overview';

        $base = (htp_lang() === 'en')
            ? 'https://htprotect.org/en/documentation'
            : 'https://htprotect.org/dokumentation';

        // Farbiges, edles Buch-Icon: gruener Einband (Verlauf) + weisse Seiten +
        // Lesezeichen. Eigene Farben (kein currentColor). KEIN externes-Link-Icon -
        // das Template-Icon des Backends ist fuer diesen Link per CSS unterdrueckt.
        // Eindeutige Gradient-ID, da metaHtml pro Seite ZWEIMAL gerendert wird
        // (Desktop-Kopf + Mobil ueber der Karte) -> gleiche IDs wuerden kollidieren.
        static $gseq = 0;
        $gid = 'htp-bookgrad-' . ($gseq++);

        $icon = '<svg class="htpx-docicon" viewBox="3 5 18 15" aria-hidden="true" focusable="false">'
            . '<defs><linearGradient id="' . $gid . '" x1="0" y1="0" x2="0" y2="1">'
            . '<stop offset="0" stop-color="#3ee07f"/><stop offset="1" stop-color="#17924e"/></linearGradient></defs>'
            . '<path d="M12 7.35C9.9 5.95 7.2 5.45 4.75 5.8 4 5.9 3.45 6.5 3.45 7.25v9.15c0 .8.72 1.4 1.5 1.3 2.3-.3 4.7.3 6.6 1.7l.45.32.45-.32c1.9-1.4 4.3-2 6.6-1.7.78.1 1.5-.5 1.5-1.3V7.25c0-.75-.55-1.35-1.3-1.45C16.8 5.45 14.1 5.95 12 7.35Z" fill="url(#' . $gid . ')"/>'
            . '<path d="M11.15 8.55C9.5 7.65 7.5 7.25 5.5 7.42V14.92C7.5 14.75 9.5 15.15 11.15 16.05Z" fill="#fff"/>'
            . '<path d="M12.85 8.55C14.5 7.65 16.5 7.25 18.5 7.42V14.92C16.5 14.75 14.5 15.15 12.85 16.05Z" fill="#fff"/>'
            . '<path d="M15.6 6.6v4.2l1.35-1 1.35 1V6.05Z" fill="#e8623f"/>'
            . '</svg>';

        return '<a class="htpx-doclink" href="' . htp_esc($base) . '#' . htp_esc($anchor) . '"'
            . ' target="_blank" rel="noopener" title="' . htp_esc(htp_t('COM_HTPROTECT_DOCS_TITLE')) . '">'
            . $icon . '<span>' . htp_t('COM_HTPROTECT_DOCS') . '</span></a>';
    }

    /**
     * Fernsteuerungs-Link (Funkwellen-Icon) zur Connect-Ansicht = Kopplung/Steuerung ueber das
     * htprotect.app-Dashboard. INTERNER Backend-Link (kein target=_blank). Nutzt dieselben Klassen
     * wie der Doku-Link (Groesse/Hover/Farbe identisch). Eigene, hochzaehlende Gradient-ID, weil die
     * Meta-Zeile pro Seite ZWEIMAL gerendert wird (Desktop-Kopf + Mobil ueber der Karte) - gleiche
     * IDs wuerden kollidieren. Icon: Sendeknoten (Akzent) + drei aufsteigende Funkwellen (Verlauf).
     */
    private static function remoteLink()
    {
        static $rseq = 0;
        $gid   = 'htp-remotegrad-' . ($rseq++);
        $first = ($gid === 'htp-remotegrad-0'); // Highlight-JS nur EINMAL ausgeben (Zeile rendert 2x)

        $icon = '<svg class="htpx-docicon" viewBox="3 6 18 15" aria-hidden="true" focusable="false">'
            . '<defs><linearGradient id="' . $gid . '" x1="0" y1="0" x2="0" y2="1">'
            . '<stop offset="0" stop-color="#3ee07f"/><stop offset="1" stop-color="#17924e"/></linearGradient></defs>'
            . '<path d="M4.6 11.7Q12 4.2 19.4 11.7" fill="none" stroke="url(#' . $gid . ')" stroke-width="1.9" stroke-linecap="round" opacity=".4"/>'
            . '<path d="M7 13.6Q12 8.6 17 13.6" fill="none" stroke="url(#' . $gid . ')" stroke-width="1.9" stroke-linecap="round" opacity=".72"/>'
            . '<path d="M9.4 15.4Q12 12.4 14.6 15.4" fill="none" stroke="url(#' . $gid . ')" stroke-width="1.9" stroke-linecap="round"/>'
            . '<circle cx="12" cy="18" r="1.85" fill="#e8623f"/>'
            . '</svg>';

        // Highlight-Ziel: das Header-Icon (Klasse htpx-fvhead). "Ansehen" (Notification) und
        // "Wo verbinde ich diese Seite?" (Hilfe) rufen htpFvGlow() -> Sonar-Ping + Glow direkt hier.
        // Der versteckte Footer-Link (#htp-fv-footer) bleibt unveraendert. p() waehlt die SICHTBARE
        // Instanz (Desktop-Kopf ODER Mobil ueber der Karte).
        $js = $first
            ? '<script>(function(){function p(){var l=document.querySelectorAll(".htpx-fvhead"),i;'
                . 'for(i=0;i<l.length;i++){if(l[i].offsetParent!==null){return l[i];}}return l[0]||null;}'
                . 'window.htpFvGlow=function(){var e=p();if(!e){return;}try{e.scrollIntoView({behavior:"smooth",block:"center"});}catch(x){}'
                . 'e.classList.remove("htpx-fv-glow");void e.offsetWidth;e.classList.add("htpx-fv-glow");'
                . 'setTimeout(function(){e.classList.remove("htpx-fv-glow");},3100);};'
                . 'if(location.hash==="#htp-fv"){setTimeout(window.htpFvGlow,300);}})();</script>'
            : '';

        return '<a class="htpx-doclink htpx-fvhead" href="' . htp_esc(self::url('connect')) . '"'
            . ' aria-label="' . htp_esc(htp_t('COM_HTPROTECT_REMOTE')) . '"'
            . ' title="' . htp_esc(htp_t('COM_HTPROTECT_REMOTE_TITLE')) . '">'
            . '<span class="htpx-fvwrap">' . $icon . '</span></a>' . $js;
    }

    private static function footer()
    {
        // Dezenter Spenden-Hinweis rechts im Footer (auf JEDER Seite sichtbar, aber
        // unaufdringlich): Sprungmarke auf die Spenden-Card unter Hilfe & Support.
        // Die vorhandene Hash-Reveal-Logik oeffnet/scrollt dort automatisch hin.
        echo '<div class="htpx-shellfoot">'
            . '<span>' . htp_t('COM_HTPROTECT_FOOTER') . '</span>'
            . '<a class="htpx-footdonate" href="' . self::url('help') . '#htp-donate">'
            . '<svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true" focusable="false">'
            . '<path fill="currentColor" d="M12 21s-7.5-4.6-10-9.3C.6 8.9 1.9 5.5 5 5c1.9-.3 3.6.7 4.5 2.1l1.5 2.3 1.5-2.3C13.4 5.7 15.1 4.7 17 5c3.1.5 4.4 3.9 3 6.7C19.5 16.4 12 21 12 21z"/></svg>'
            . ' ' . htp_t('COM_HTPROTECT_FOOTER_DONATE') . '</a>'
            . '</div>';
    }

    private static function nav($sec, $data)
    {
        $items = array(
            'overview' => array(htp_t('COM_HTPROTECT_NAV_OVERVIEW'), 'M3 13h8V3H3v10zm10 8h8V11h-8v10zM3 21h8v-6H3v6zm10-18v6h8V3h-8z'),
            'guard'    => array(htp_t('COM_HTPROTECT_NAV_GUARD'), 'M12 2l8 3v6c0 5-3.4 9.4-8 11-4.6-1.6-8-6-8-11V5l8-3z'),
            'shield'   => array(htp_t('COM_HTPROTECT_NAV_SHIELD'), 'M4 4h16v4H4V4zm0 6h16v4H4v-4zm0 6h16v4H4v-4z'),
            'scan'     => array(htp_t('COM_HTPROTECT_NAV_SCAN'), 'M10 2a8 8 0 1 0 4.9 14.3l5.4 5.4 1.4-1.4-5.4-5.4A8 8 0 0 0 10 2zm0 2a6 6 0 1 1 0 12 6 6 0 0 1 0-12z'),
            'backups'  => array(self::backupsNavLabel(), 'M12 2a9 9 0 0 0-9 9H0l4 4 4-4H5a7 7 0 1 1 2 4.9L5.6 17.3A9 9 0 1 0 12 2zm-1 5v5l4 2.4.8-1.3-3.3-2V7h-1.5z'),
            'help'     => array(htp_t('COM_HTPROTECT_NAV_HELP'), 'M3 3h18v14H8l-5 4V3z'),
        );

        echo '<nav class="htpx-nav">';

        foreach ($items as $key => $item) {
            $active = $sec === $key ? ' htpx-nav-active' : '';
            echo '<a class="htpx-navlink' . $active . '" href="' . self::url($key) . '">'
                . '<svg viewBox="0 0 24 24" width="15" height="15"><path d="' . $item[1] . '" fill="currentColor"/></svg>'
                . '<span>' . $item[0] . '</span></a>';
        }

        echo '</nav>';
    }

    private static function url($sec)
    {
        return 'index.php?option=com_htprotect&sec=' . $sec;
    }

    private static function tokenField()
    {
        return '<input type="hidden" name="' . htp_esc(htp_form_token()) . '" value="1">';
    }

    /**
     * Umschalter (Checkbox im Schalter-Design).
     */
    private static function toggle($name, $checked, $label, $hint = '', $anchor = '')
    {
        $html = '<label class="htpx-tg"' . ($anchor !== '' ? ' id="' . htp_esc($anchor) . '"' : '') . '>'
            . '<input type="checkbox" name="' . htp_esc($name) . '" value="1"' . ($checked ? ' checked' : '') . '>'
            . '<span class="htpx-tgsl"></span>'
            . '<span class="htpx-tgtext">' . $label
            . ($hint !== '' ? '<small>' . $hint . '</small>' : '')
            . '</span></label>';

        return $html;
    }

    private static function badge($type, $text)
    {
        return '<span class="htpx-badge htpx-badge-' . $type . '">' . $text . '</span>';
    }

    /**
     * Joomlas Standard-Editor (gemaess System- UND Benutzer-Einstellung) fuer ein Textfeld.
     * Der Editor bringt den Standard-Umschalter "Editor umschalten" (Roh-HTML / "Ohne Editor")
     * selbst mit - genau wie im normalen Backend. Faellt sauber auf eine einfache Textarea zurueck,
     * wenn die Editor-API fehlt (z. B. Joomla 3) oder etwas schiefgeht - dann bleibt das Feld nutzbar.
     *
     * @param   string  $name     Feld-/Formularname
     * @param   string  $content  aktueller (HTML-)Inhalt
     * @param   int     $height   Editor-Hoehe in px
     *
     * @return string  HTML des Editors (oder Textarea-Fallback)
     */
    private static function wysiwyg($name, $content, $height = 220)
    {
        try {
            if (class_exists('Joomla\\CMS\\Editor\\Editor') && class_exists('Joomla\\CMS\\Factory')) {
                $app = Joomla\CMS\Factory::getApplication();
                // Effektiver Editor: Benutzer-Einstellung schlaegt System-Vorgabe (wie im Backend).
                $ed   = $app ? (string) $app->get('editor', 'tinymce') : 'tinymce';
                $user = ($app && method_exists($app, 'getIdentity')) ? $app->getIdentity() : null;

                if ($user && method_exists($user, 'getParam')) {
                    $ed = (string) $user->getParam('editor', $ed);
                }

                if ($ed === '') {
                    $ed = 'tinymce';
                }

                $editor = Joomla\CMS\Editor\Editor::getInstance($ed);

                if (is_object($editor) && method_exists($editor, 'display')) {
                    // buttons=false: keine Artikel-/Readmore-/Pagebreak-Knoepfe (fuer eine
                    // Wartungsseite irrelevant); der "Editor umschalten"-Knopf bleibt erhalten.
                    return (string) $editor->display($name, (string) $content, '100%', (int) $height, '60', '8', false, $name, null, null, array());
                }
            }
        } catch (\Throwable $e) {
            // Fallback unten
        }

        // Fallback: einfache Roh-Textarea - immer noch nutzbar.
        return '<textarea class="htpx-input" id="' . htp_esc($name) . '" name="' . htp_esc($name) . '" rows="6">' . htp_esc((string) $content) . '</textarea>';
    }

    /**
     * 1-Klick-Formular „Als vertrauenswürdigen Pfad eintragen" (hebt
     * Lockdown + uri-Signaturen + Dateischild für den Pfad auf).
     */
    private static function allowForm($path)
    {
        return '<form method="post" action="' . self::url('scan') . '" style="display:inline">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="allow_add">'
            . '<input type="hidden" name="htp_allow_path" value="' . htp_esc($path) . '">'
            . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_ALLOW_BTN') . '</button></form>';
    }

    /**
     * Datengetriebene Texte (Signatur-/Warnlisten-Eintraege) uebersetzen:
     * liefert die Uebersetzung zum Schluessel, sonst den Rohwert (Fallback) -
     * so bleiben Eintraege aus dem Remote-Feed ohne Schluessel lesbar.
     * Wird VOR htp_esc() verwendet -> die .ini-Werte sind echtes UTF-8.
     *
     * @param   string  $key       Sprachschluessel
     * @param   string  $fallback  Rohwert aus den Definitionen
     *
     * @return string
     */
    private static function dataText($key, $fallback)
    {
        $t = htp_t($key);

        return $t === $key ? (string) $fallback : $t;
    }

    // ─────────────────────────────────────────────────────────────────
    //  Bereich: Ueberblick
    // ─────────────────────────────────────────────────────────────────

    private static function secOverview($cfg, $data)
    {
        $st    = $data['status'];
        $guard = $data['guard'];

        if (!isset($guard['protected'])) {
            $guard['protected'] = !empty($guard['active']);
            $guard['source']    = !empty($guard['active']) ? 'htprotect' : 'none';
        }

        echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_NAV_OVERVIEW') . '</h2>';

        // ── (2.6.4) Sprachpaket-Hinweis: aktive Backend-Sprache ist weder Deutsch noch Englisch und es
        //    gibt ein passendes HTProtect-Sprachpaket -> 1-Klick-Installation. Zwei Submit-Knoepfe tragen
        //    ihren Task selbst (name="htp_task"), daher kein verstecktes Task-Feld. Per-Tag wegklickbar. ──
        if (!empty($data['langpack'])) {
            $lp = $data['langpack'];
            echo '<form method="post" action="' . self::url('overview') . '" class="htpx-lpn">'
                . self::tokenField()
                . '<span class="htpx-lpn-icon" aria-hidden="true">&#127760;</span>'
                . '<div class="htpx-lpn-txt"><span>'
                . htp_t('COM_HTPROTECT_LANGPACK_NOTICE', htp_esc((string) $lp['label']))
                . '</span></div>'
                . '<button type="submit" name="htp_task" value="langpack_install" class="htpx-lpn-btn">'
                . htp_t('COM_HTPROTECT_LANGPACK_INSTALL') . '</button>'
                . '<button type="submit" name="htp_task" value="langpack_notice_dismiss" class="htpx-lpn-x" aria-label="'
                . htp_esc(htp_t('COM_HTPROTECT_LANGPACK_LATER')) . '" title="' . htp_esc(htp_t('COM_HTPROTECT_LANGPACK_LATER'))
                . '">&times;</button>'
                . '</form>';
        }

        // ── Schwarm-Schutz: geschlossene Rueckkopplung („deine Meldung ist live") ──
        // Bleibt als Banner stehen, bis es der Nutzer per × bestaetigt (Form-POST,
        // token-geschuetzt) -> kann nicht uebersehen werden.
        if (!empty($data['swarm_thanks'])) {
            echo '<form method="post" action="' . self::url('overview') . '" class="htpx-swarm-thanks">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="swarm_thanks_dismiss">'
                . '<span class="htpx-swarm-thanks-msg">' . htp_t('COM_HTPROTECT_SWARM_THANKS') . '</span>'
                . '<button type="submit" class="htpx-swarm-thanks-x" aria-label="' . htp_esc(htp_t('COM_HTPROTECT_SWARM_THANKS_X'))
                . '" title="' . htp_esc(htp_t('COM_HTPROTECT_SWARM_THANKS_X')) . '">&times;</button>'
                . '</form>';
        }

        // ── JED-Bewertungs-Einladung: dezentes, wegklickbares Banner. Erscheint erst,
        //    wenn HTProtect sich bewaehrt hat (Ampel gruen + >= 7 Tage) - nie sofort bei
        //    Neuinstallation. Das × verbucht die aktuelle Einladungs-Generation. ──
        if (!empty($data['jed_review'])) {
            echo '<form method="post" action="' . self::url('overview') . '" class="htpx-jedrev">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="jed_review_dismiss">'
                . '<span class="htpx-jedrev-heart" aria-hidden="true">&#128153;</span>'
                . '<div class="htpx-jedrev-txt">'
                . '<strong>' . htp_t('COM_HTPROTECT_JEDREV_TITLE') . '</strong>'
                . '<span>' . htp_t('COM_HTPROTECT_JEDREV_MSG') . '</span>'
                . '</div>'
                . '<a class="htpx-jedrev-btn" href="' . htp_esc(HtpApp::JED_REVIEW_URL) . '" target="_blank" rel="noopener">'
                . htp_t('COM_HTPROTECT_JEDREV_BTN') . '</a>'
                . '<button type="submit" class="htpx-jedrev-x" aria-label="' . htp_esc(htp_t('COM_HTPROTECT_JEDREV_X'))
                . '" title="' . htp_esc(htp_t('COM_HTPROTECT_JEDREV_X')) . '">&times;</button>'
                . '</form>';
        }

        // ── Fernverwaltungs-Einladung: einladendes Overview-Banner (htprotect.app). Der Button
        //    "Wie funktioniert das?" hebt den bewusst unscheinbaren Fernverwaltungs-Link unten
        //    rechts kurz hervor, damit klar ist, wo man klickt. Das × verbucht die Generation
        //    (dauerhaft weg; erst eine kuenftige Version kann EINE neue Einladung ausloesen). ──
        if (!empty($data['fv_notice'])) {
            echo '<form method="post" action="' . self::url('overview') . '" class="htpx-fvn">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="fv_notice_dismiss">'
                . '<span class="htpx-fvn-icon" aria-hidden="true">&#128225;</span>'
                . '<div class="htpx-fvn-txt">'
                . '<strong>' . htp_t('COM_HTPROTECT_FVNOTICE_TITLE') . '</strong>'
                . '<span>' . htp_t('COM_HTPROTECT_FVNOTICE_MSG') . '</span>'
                . '</div>'
                . '<button type="button" class="htpx-fvn-btn" onclick="htpFvGlow&&htpFvGlow()">'
                . htp_t('COM_HTPROTECT_FVNOTICE_BTN') . '</button>'
                . '<button type="submit" class="htpx-fvn-x" aria-label="' . htp_esc(htp_t('COM_HTPROTECT_FVNOTICE_X'))
                . '" title="' . htp_esc(htp_t('COM_HTPROTECT_FVNOTICE_X')) . '">&times;</button>'
                . '</form>';
        }

        // ── Doppelte Komprimierung: reiner HINWEIS. Der Server komprimiert die Seiten schon
        //    selbst, Joomlas eigene Komprimierung laeuft zusaetzlich. HTProtect aendert die
        //    Joomla-Konfiguration NICHT von sich aus - der Knopf schaltet sie auf ausdrueck-
        //    lichen Wunsch ab, das × blendet den Hinweis dauerhaft aus. ──
        if (!empty($data['gzip_notice'])) {
            echo '<form method="post" action="' . self::url('overview') . '" class="htpx-gzn">'
                . self::tokenField()
                . '<span class="htpx-gzn-icon" aria-hidden="true">&#9889;</span>'
                . '<div class="htpx-gzn-txt">'
                . '<strong>' . htp_t('COM_HTPROTECT_GZIPNOTICE_TITLE') . '</strong>'
                . '<span>' . htp_t('COM_HTPROTECT_GZIPNOTICE_MSG') . '</span>'
                . '</div>'
                . '<button type="submit" class="htpx-gzn-btn" name="htp_task" value="gzip_notice_off">'
                . htp_t('COM_HTPROTECT_GZIPNOTICE_BTN') . '</button>'
                . '<button type="submit" class="htpx-gzn-x" name="htp_task" value="gzip_notice_dismiss"'
                . ' aria-label="' . htp_esc(htp_t('COM_HTPROTECT_GZIPNOTICE_X'))
                . '" title="' . htp_esc(htp_t('COM_HTPROTECT_GZIPNOTICE_X')) . '">&times;</button>'
                . '</form>';
        }

        // ── Sicherheits-Ampel + Ein-Klick-Absicherung ──
        if (isset($data['ampel']) && is_array($data['ampel'])) {
            self::renderAmpel($data['ampel'], $st, !empty($data['nginx_mode']) || !empty($data['iis_mode']));
        }

        // ── Aktive-Angriffs-Spuren: JCE-Hack-Profile + boesartige .htaccess ──
        self::renderRogueAdmins($data);
        self::renderJceWarning($data);
        self::renderMalHtaccess($data);
        self::renderHelixJs($data);
        self::renderMenuXss($data);
        self::renderSppb($data);
        self::renderAccountAlarms($data);

        // ── Bericht: Eigenregeln, die NICHT in die neue Datei uebernommen wurden ──
        self::renderQuickRemoved($data);

        // ── Waechter-Alarme (Fremdaenderung/Loeschung) ──
        if (isset($data['watch']) && is_array($data['watch']) && !empty($data['watch'])) {
            self::renderWatchAlarms($data['watch']);
        }

        // ── Verwundbare Erweiterungen: aktive (rot, mit Aufschieben-Knopf) und
        //    aufgeschobene (still, mit Wieder-aktivieren) getrennt darstellen. ──
        $vActive = isset($data['vuln_active']) ? $data['vuln_active'] : (isset($data['vuln']) ? $data['vuln'] : array());
        $vAcked  = isset($data['vuln_acked']) && is_array($data['vuln_acked']) ? $data['vuln_acked'] : array();

        if (is_array($vActive) && !empty($vActive)) {
            self::renderVulnList($vActive);
        }

        if (!empty($vAcked)) {
            self::renderVulnAcked($vAcked);
        }

        // Onboarding nur, wenn das Backend nachweislich ungeschuetzt ist
        // (ein extern/serverseitig eingerichteter Schutz wird erkannt).
        $gsrc = isset($guard['source']) ? $guard['source'] : 'none';

        if (empty($guard['protected']) && $gsrc !== 'unknown' && empty($cfg['guard_dismissed'])) {
            echo '<div class="htpx-hero">'
                // Schlichtes X oben rechts: Backend-Zugangsschutz bewusst ueberspringen
                // (mit kurzer Rueckfrage). Token-geschuetzter Form-POST.
                . '<form method="post" action="' . self::url('overview') . '" class="htpx-hero-x"'
                . ' onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_GUARD_SKIP_CONFIRM') . '\');">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="guard_dismiss">'
                . '<button type="submit" class="htpx-x" aria-label="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_SKIP_TITLE'))
                . '" title="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_SKIP_TITLE')) . '">&times;</button>'
                . '</form>'
                . '<h3>' . htp_t('COM_HTPROTECT_OVERVIEW_HERO_TITLE') . '</h3>'
                . '<p>' . htp_t('COM_HTPROTECT_OVERVIEW_HERO_TEXT') . '</p>';
            self::guardForm($cfg, $guard, 'overview');
            // Option 2 (Alternative) auch in der Uebersicht-Onboarding-Karte anbieten - identisch
            // zur dedizierten Backend-Zugang-Seite, damit der Zugangs-String ueberall erreichbar ist.
            self::guardStringForm($cfg, $guard);
            echo '</div>';
        }

        // Der vollständige Status steht kompakt in der Sicherheits-Ampel oben.
        // Hier nur noch System-Infos und das Ergebnis des letzten Selbsttests
        // (nicht in der Ampel enthalten).
        $server   = isset($_SERVER['SERVER_SOFTWARE']) ? (string) $_SERVER['SERVER_SOFTWARE'] : htp_t('COM_HTPROTECT_UNKNOWN');
        // Massgeblich ist NICHT die rohe SERVER_SOFTWARE (Plesk meldet "nginx",
        // obwohl Apache dahinter die .htaccess auswertet - nginx-vor-Apache), sondern
        // der aktive .htaccess-Funktionstest (htp_nginx_mode = !htaccessCapability=='yes').
        // Sonst zeigt die Badge "wirkt evtl. nicht", obwohl der Selbsttest 4/4 beweist,
        // dass die .htaccess greift. Echtes nginx-only bleibt korrekt bei "warn".
        $htaccessWorks = !htp_nginx_mode($cfg);

        $test = isset($cfg['last_test']) && is_array($cfg['last_test']) ? $cfg['last_test'] : array();

        if (empty($test) || empty($test['checks'])) {
            $testHtml = '<span class="htpx-dim">' . htp_t('COM_HTPROTECT_SELFTEST_NONE') . '</span>';
        } else {
            $ok    = 0;
            $total = count($test['checks']);

            foreach ($test['checks'] as $check) {
                if (!empty($check['ok'])) {
                    $ok++;
                }
            }

            $testHtml = self::badge($ok === $total ? 'ok' : 'warn', htp_t('COM_HTPROTECT_SELFTEST_PASSED', $ok, $total))
                . ' <span class="htpx-dim">' . htp_esc(isset($test['time']) ? $test['time'] : '') . '</span>';
        }

        $reloadBtn = HtpProbe::available()
            ? ' <form method="post" action="' . self::url('overview') . '" style="display:inline;margin-left:8px">'
                . self::tokenField()
                . '<button type="submit" name="htp_task" value="selftest" class="htpx-btn2 htpx-btn-s htpx-reload" title="' . htp_t('COM_HTPROTECT_SELFTEST_RELOAD') . '">&#x21bb; ' . htp_t('COM_HTPROTECT_SELFTEST_RELOAD') . '</button>'
                . '</form>'
            : '';

        echo '<div class="htpx-card htpx-card-wide" id="htp-a-sysinfo"><h4>' . htp_t('COM_HTPROTECT_OVERVIEW_SYS_TITLE') . $reloadBtn . '</h4><ul class="htpx-syslist">'
            . '<li>Joomla ' . htp_esc(htp_jversion()) . ' &middot; PHP ' . htp_esc(PHP_VERSION) . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_OVERVIEW_SERVER_LABEL') . ' ' . htp_esc($server) . ' '
            . ($htaccessWorks ? self::badge('ok', htp_t('COM_HTPROTECT_BADGE_COMPATIBLE')) : self::badge('warn', htp_t('COM_HTPROTECT_BADGE_HTACCESS_MAYBE')))
            . ' &middot; ' . htp_t('COM_HTPROTECT_OVERVIEW_HTTPTEST_LABEL') . ' ' . (HtpProbe::available() ? self::badge('ok', htp_t('COM_HTPROTECT_BADGE_AVAILABLE')) : self::badge('warn', htp_t('COM_HTPROTECT_BADGE_UNAVAILABLE'))) . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_OVERVIEW_LASTTEST_LABEL') . ' ' . $testHtml . '</li>'
            . '</ul>';

        // Aufklappbare Details: welche Pr&uuml;fungen bestanden/aufgefallen sind
        if (!empty($test['checks'])) {
            echo '<details class="htpx-details"><summary>' . htp_t('COM_HTPROTECT_SELFTEST_WHICH') . '</summary>';
            self::renderSelfTestTable($test);
            echo '</details>';
        }

        echo '</div>';

        // ── 1-Klick: HTProtect selbst automatisch aktuell halten (nur solange NICHT aktiv) ──
        self::renderSelfUpdatePromo($cfg);

        // ── Automatische Ueberwachung + Benachrichtigung ──
        self::renderMonitorCard($cfg, $data);

        // ── Echtzeit-Schutz-Protokoll (geblockte Angriffe) ──
        self::renderWafLog($data);
    }

    /**
     * Warnkarte: untergeschobene Super-Admins (SP-Page-Builder-Kampagnenmuster).
     * Je Konto: Stufe (sicher/pruefen), Details, Gruende und ZWEI Aktionen -
     * "Zur Benutzerverwaltung" (Sperren/Loeschen) + "Als vertrauenswuerdig
     * bestaetigen". Es wird NIE ein Konto automatisch geaendert/geloescht.
     */
    private static function renderRogueAdmins($data)
    {
        $adm = isset($data['suspicious_admins']) && is_array($data['suspicious_admins']) ? $data['suspicious_admins'] : array();

        if (empty($adm)) {
            return;
        }

        static $reasonKey = array(
            'email_securelocal'  => 'COM_HTPROTECT_ROGUEADMIN_R_SECURELOCAL',
            'email_reserved'     => 'COM_HTPROTECT_ROGUEADMIN_R_RESERVED',
            'username_pattern'   => 'COM_HTPROTECT_ROGUEADMIN_R_USERNAME',
            'name_pattern'       => 'COM_HTPROTECT_ROGUEADMIN_R_NAME',
            'campaign_gridbox'   => 'COM_HTPROTECT_ROGUEADMIN_R_GRIDBOX',
            'campaign_generated' => 'COM_HTPROTECT_ROGUEADMIN_R_CAMPAIGN',
            'campaign_joomla_test' => 'COM_HTPROTECT_ROGUEADMIN_R_JOOMLATEST',
        );

        echo '<div class="htpx-card htpx-card-wide htpx-card-danger" id="htp-a-admins">'
            . '<h4>&#9888; ' . htp_t('COM_HTPROTECT_ROGUEADMIN_TITLE', count($adm)) . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_ROGUEADMIN_TEXT') . '</p>'
            . '<ul class="htpx-syslist">';

        foreach ($adm as $a) {
            $tier = ((string) $a['tier'] === 'certain')
                ? self::badge('err', htp_t('COM_HTPROTECT_ROGUEADMIN_T_CERTAIN'))
                : self::badge('warn', htp_t('COM_HTPROTECT_ROGUEADMIN_T_REVIEW'));

            $reasons = array();

            foreach ((array) $a['reasons'] as $r) {
                if (isset($reasonKey[$r])) {
                    $reasons[] = htp_t($reasonKey[$r]);
                }
            }

            $userUrl = 'index.php?option=com_users&task=user.edit&id=' . (int) $a['id'];

            echo '<li>' . $tier
                . ' <span class="htpx-mono">' . htp_esc((string) $a['username']) . '</span>'
                . ' <span class="htpx-dim">#' . (int) $a['id'] . '</span>'
                . ($a['email'] !== '' ? ' &middot; <span class="htpx-mono">' . htp_esc((string) $a['email']) . '</span>' : '')
                . ($a['registered'] !== '' ? ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_ROGUEADMIN_REGISTERED', htp_esc((string) $a['registered'])) . '</span>' : '')
                . (!empty($reasons) ? '<br><span class="htpx-dim">' . htp_esc(implode(', ', $reasons)) . '</span>' : '')
                . '<div class="htpx-actions" style="margin-top:6px">'
                . '<a class="htpx-btn htpx-btn-danger" href="' . htp_esc($userUrl) . '">' . htp_t('COM_HTPROTECT_ROGUEADMIN_MANAGE_BTN') . '</a>'
                . '<form method="post" action="' . self::url('overview') . '" style="display:inline;margin-left:8px">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="admin_approve">'
                . '<input type="hidden" name="htp_id" value="' . (int) $a['id'] . '">'
                . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_ROGUEADMIN_APPROVE_BTN') . '</button>'
                . '</form></div></li>';
        }

        echo '</ul>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_ROGUEADMIN_ADVICE') . '</p>'
            . '</div>';
    }

    /**
     * Warnkarte: verdaechtige JCE-Profile (oeffentliches Upload-Profil =
     * typische Folge des JCE-Hacks). Nur positiver Fund; kein "JCE sauber".
     */
    private static function renderJceWarning($data)
    {
        $jce = isset($data['jce_profiles']) && is_array($data['jce_profiles']) ? $data['jce_profiles'] : array();

        if (empty($jce)) {
            return;
        }

        echo '<div class="htpx-card htpx-card-wide htpx-card-danger" id="htp-a-jce">'
            . '<h4>&#9888; ' . htp_t('COM_HTPROTECT_JCE_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_JCE_TEXT') . '</p>'
            . '<ul class="htpx-syslist">';

        foreach ($jce as $p) {
            echo '<li><span class="htpx-mono">#' . (int) $p['id'] . '</span> ' . self::badge('err', htp_esc((string) $p['name'])) . '</li>';
        }

        echo '</ul>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_JCE_ADVICE') . '</p>'
            . '<div class="htpx-actions">'
            . '<form method="post" action="' . self::url('overview') . '" style="margin:0" '
            . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_JCE_CONFIRM') . '\');">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="delete_jce_profiles">'
            . '<button type="submit" class="htpx-btn htpx-btn-danger">' . htp_t('COM_HTPROTECT_JCE_DELETE_BTN') . '</button>'
            . '</form></div>'
            . '</div>';
    }

    /**
     * Warnkarte: boesartige .htaccess (PHP-Reaktivierung) in 1.-Ebene-Ordnern,
     * mit Ein-Klick-Loeschung aller Funde.
     */
    private static function renderMalHtaccess($data)
    {
        $mal = isset($data['mal_htaccess']) && is_array($data['mal_htaccess']) ? $data['mal_htaccess'] : array();

        if (empty($mal)) {
            return;
        }

        echo '<div class="htpx-card htpx-card-wide htpx-card-danger" id="htp-a-malht">'
            . '<h4>&#9888; ' . htp_t('COM_HTPROTECT_MALHT_TITLE', count($mal)) . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_MALHT_TEXT') . '</p>'
            . '<ul class="htpx-syslist">';

        foreach ($mal as $rel) {
            echo '<li class="htpx-mono">' . htp_esc((string) $rel) . '</li>';
        }

        echo '</ul>'
            . '<div class="htpx-actions">'
            . '<form method="post" action="' . self::url('overview') . '" style="margin:0" '
            . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_MALHT_CONFIRM') . '\');">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="delete_mal_htaccess">'
            . '<button type="submit" class="htpx-btn htpx-btn-danger">' . htp_t('COM_HTPROTECT_MALHT_DELETE_BTN') . '</button>'
            . '</form></div></div>';
    }

    /**
     * Warnkarte: eingeschleustes JavaScript/XSS in Helix-Ultimate-Mega-Menue-Eintraegen
     * (#__menu.params). Nur hochkonfidente Funde (kein Fehlalarm auf legit Menue-Config).
     * Reversibler Wipe - nur die JS-Felder werden geleert, das Menue bleibt funktionsfaehig.
     */
    private static function renderMenuXss($data)
    {
        $items = isset($data['menu_xss']) && is_array($data['menu_xss']) ? $data['menu_xss'] : array();

        if (empty($items)) {
            return;
        }

        echo '<div class="htpx-card htpx-card-wide htpx-card-danger" id="htp-a-menuxss">'
            . '<h4>&#9888; ' . htp_t('COM_HTPROTECT_MENUXSS_TITLE', count($items)) . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_MENUXSS_TEXT') . '</p>'
            . '<ul class="htpx-syslist">';

        foreach ($items as $s) {
            echo '<li><strong>' . htp_esc((string) $s['title']) . '</strong> '
                . '<span class="htpx-dim">(' . htp_esc((string) $s['menutype']) . ', #' . (int) $s['id'] . ')</span></li>';
        }

        echo '</ul>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_MENUXSS_ADVICE') . '</p>'
            . '<div class="htpx-actions">'
            . '<form method="post" action="' . self::url('overview') . '" style="margin:0" '
            . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_MENUXSS_CONFIRM') . '\');">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="menu_wipe">'
            . '<button type="submit" class="htpx-btn htpx-btn-danger">' . htp_t('COM_HTPROTECT_MENUXSS_CLEAN_BTN') . '</button>'
            . '</form></div></div>';
    }

    /**
     * Warnkarte: SP Page Builder - eingeschleuste Asset-Zeilen (PHP/Skript/IOC im
     * asset_value) und untergeschobene ("rogue") Iconfont-Registrierungen
     * (#__sppagebuilder_assets). Ein-Klick-Entfernung, jede Zeile vorher reversibel gesichert.
     */
    private static function renderSppb($data)
    {
        $sppb = isset($data['sppb']) && is_array($data['sppb']) ? $data['sppb'] : array();
        $inj  = isset($sppb['injected']) && is_array($sppb['injected']) ? $sppb['injected'] : array();
        $ico  = isset($sppb['iconfont']) && is_array($sppb['iconfont']) ? $sppb['iconfont'] : array();

        if (empty($inj) && empty($ico)) {
            return;
        }

        echo '<div class="htpx-card htpx-card-wide htpx-card-danger" id="htp-a-sppb">'
            . '<h4>&#9888; ' . htp_t('COM_HTPROTECT_SPPB_TITLE', count($inj) + count($ico)) . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SPPB_TEXT') . '</p>'
            . '<ul class="htpx-syslist">';

        foreach ($inj as $a) {
            echo '<li>' . self::badge('err', htp_t('COM_HTPROTECT_SPPB_INJECTED')) . ' '
                . '<span class="htpx-mono">#' . (int) $a['id'] . '</span> ' . htp_esc((string) $a['name'])
                . '<br><span class="htpx-dim htpx-mono">' . htp_esc((string) $a['snippet']) . '</span></li>';
        }

        foreach ($ico as $a) {
            echo '<li>' . self::badge('err', htp_t('COM_HTPROTECT_SPPB_ROGUE_ICON')) . ' '
                . '<span class="htpx-mono">#' . (int) $a['id'] . '</span> ' . htp_esc((string) $a['name']) . '</li>';
        }

        echo '</ul>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SPPB_ADVICE') . '</p>'
            . '<div class="htpx-actions">'
            . '<form method="post" action="' . self::url('overview') . '" style="margin:0" '
            . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_SPPB_CONFIRM') . '\');">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="sppb_clean">'
            . '<button type="submit" class="htpx-btn htpx-btn-danger">' . htp_t('COM_HTPROTECT_SPPB_CLEAN_BTN') . '</button>'
            . '</form></div></div>';
    }

    /**
     * Warnkarte: eingeschleustes JavaScript in den Helix-Template-Optionen (Defacement
     * ueber die unauth. Helix3-com_ajax-import-Luecke). Zeigt NUR hochkonfidente Funde
     * (kein Fehlalarm auf legitimes Analytics). Dry-run-Report + reversibler Wipe;
     * weist ausdruecklich darauf hin, dass nur die JS-Felder geleert werden.
     */
    private static function renderHelixJs($data)
    {
        $styles = isset($data['helix_js']) && is_array($data['helix_js']) ? $data['helix_js'] : array();

        if (empty($styles)) {
            return;
        }

        echo '<div class="htpx-card htpx-card-wide htpx-card-danger" id="htp-a-helix">'
            . '<h4>&#9888; ' . htp_t('COM_HTPROTECT_HELIX_TITLE', count($styles)) . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_HELIX_TEXT') . '</p>'
            . '<ul class="htpx-syslist">';

        foreach ($styles as $s) {
            echo '<li><strong>' . htp_esc((string) $s['title']) . '</strong> '
                . '<span class="htpx-dim">(' . htp_esc((string) $s['template']) . ', #' . (int) $s['id'] . ')</span>'
                . '<ul>';

            foreach ($s['fields'] as $f) {
                if (empty($f['suspicious'])) {
                    continue;
                }

                // (A, 2.6.2) Laienverstaendliches Marker-Label (+ optionaler Zusatz wie Host).
                $mkTxt = !empty($f['marker_key']) ? htp_t((string) $f['marker_key']) : '';

                if ($mkTxt !== '' && !empty($f['marker_arg'])) {
                    $mkTxt .= ': ' . (string) $f['marker_arg'];
                }

                // (A) Auszug UM die Trefferstelle, hervorgehoben. Fallback: bisheriges Anfang-Snippet.
                if (isset($f['hit']) && (string) $f['hit'] !== '') {
                    $body = (!empty($f['hit_ell_b']) ? '&hellip; ' : '')
                        . htp_esc((string) $f['hit_before'])
                        . '<mark style="background:#ffe08a;color:#000;padding:0 2px;border-radius:2px">' . htp_esc((string) $f['hit']) . '</mark>'
                        . htp_esc((string) $f['hit_after'])
                        . (!empty($f['hit_ell_a']) ? ' &hellip;' : '');
                } else {
                    $body = htp_esc((string) $f['snippet']);
                }

                // (B) Praeziser Pfad (z. B. config.menu.…) statt nur des letzten Schluessels.
                $where = isset($f['path']) && $f['path'] !== '' ? (string) $f['path'] : (string) $f['field'];

                echo '<li><code>' . htp_esc($where) . '</code>'
                    . ($mkTxt !== '' ? ' <span style="display:inline-block;font-size:.85em;font-weight:600;padding:0 6px;border:1px solid currentColor;border-radius:10px;opacity:.8">' . htp_esc($mkTxt) . '</span>' : '')
                    . '<br><span class="htpx-mono">' . $body . '</span></li>';
            }

            echo '</ul>';

            // Pro Stil: "Ignorieren" - falls der Admin dieses Skript SELBST eingefuegt
            // hat (z. B. ein Copyright-Jahr-Skript). Merkt die Inhalts-Signatur; aendert
            // sich der Inhalt spaeter (echte Injektion), kommt der Alarm automatisch zurueck.
            if (!empty($s['sig'])) {
                echo '<form method="post" action="' . self::url('overview') . '" style="margin:4px 0 2px">'
                    . self::tokenField()
                    . '<input type="hidden" name="htp_task" value="helix_ignore">'
                    . '<input type="hidden" name="htp_helix_id" value="' . (int) $s['id'] . '">'
                    . '<input type="hidden" name="htp_helix_sig" value="' . htp_esc((string) $s['sig']) . '">'
                    . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_HELIX_IGNORE_BTN') . '</button>'
                    . '</form>';
            }

            echo '</li>';
        }

        echo '</ul>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_HELIX_NOTE') . '</p>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_HELIX_IGNORE_HINT') . '</p>'
            . '<div class="htpx-actions">'
            . '<form method="post" action="' . self::url('overview') . '" style="margin:0" '
            . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_HELIX_CONFIRM') . '\');">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="helix_wipe">'
            . '<label class="htpx-inline" style="margin-right:12px;cursor:pointer">'
            . '<input type="checkbox" name="htp_helix_hard" value="1"> ' . htp_t('COM_HTPROTECT_HELIX_HARD_LABEL') . '</label>'
            . '<button type="submit" class="htpx-btn htpx-btn-danger">' . htp_t('COM_HTPROTECT_HELIX_WIPE_BTN') . '</button>'
            . '</form></div></div>';
    }

    /**
     * Warnkarte: veraenderte Super-User-Konten (neuer Super-User, geaendertes
     * Passwort/E-Mail). Je Eintrag "Als bekannt markieren" (akzeptiert diesen
     * Stand) + unten "Alle uebernehmen". Es wird NIE ein Konto geaendert/geloescht.
     */
    private static function renderAccountAlarms($data)
    {
        $acc = isset($data['account_changes']) && is_array($data['account_changes']) ? $data['account_changes'] : array();

        if (empty($acc)) {
            return;
        }

        static $kindKey = array(
            'acct_new'   => 'COM_HTPROTECT_ACCOUNTS_K_NEW',
            'acct_pw'    => 'COM_HTPROTECT_ACCOUNTS_K_PW',
            'acct_email' => 'COM_HTPROTECT_ACCOUNTS_K_EMAIL',
        );

        echo '<div class="htpx-card htpx-card-wide htpx-card-danger" id="htp-a-accounts">'
            . '<h4>&#9888; ' . htp_t('COM_HTPROTECT_ACCOUNTS_TITLE', count($acc)) . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_ACCOUNTS_TEXT') . '</p>'
            . '<ul class="htpx-syslist">';

        foreach ($acc as $a) {
            $kind  = isset($a['kind']) ? (string) $a['kind'] : '';
            $badge = self::badge('err', htp_t(isset($kindKey[$kind]) ? $kindKey[$kind] : $kindKey['acct_new']));

            // Alt -> Neu, wo darstellbar: die E-Mail-Aenderung zeigt beide
            // Adressen, ein neuer Account die E-Mail. Das Passwort hat keinen
            // anzeigbaren Wert (nur Hashes) -> kein Detail.
            $oldv   = isset($a['old']) ? (string) $a['old'] : '';
            $newv   = isset($a['new']) ? (string) $a['new'] : '';
            $detail = '';

            if ($kind === 'acct_email') {
                $detail = ' <span class="htpx-dim">'
                    . htp_esc($oldv !== '' ? $oldv : '—') . ' &rarr; ' . htp_esc($newv !== '' ? $newv : '—')
                    . '</span>';
            } elseif ($kind === 'acct_new' && $newv !== '') {
                $detail = ' <span class="htpx-dim">&lt;' . htp_esc($newv) . '&gt;</span>';
            }

            echo '<li>' . $badge . ' <span class="htpx-mono">' . htp_esc((string) (isset($a['user']) ? $a['user'] : '')) . '</span> '
                . '<span class="htpx-dim">#' . (int) (isset($a['id']) ? $a['id'] : 0) . '</span>' . $detail . ' '
                . '<form method="post" action="' . self::url('overview') . '" style="display:inline;margin-left:8px">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="account_accept">'
                . '<input type="hidden" name="htp_id" value="' . (int) (isset($a['id']) ? $a['id'] : 0) . '">'
                . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_ACCOUNTS_ACCEPT_BTN') . '</button>'
                . '</form></li>';
        }

        echo '</ul>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_ACCOUNTS_ADVICE') . '</p>'
            . '<div class="htpx-actions">'
            . '<form method="post" action="' . self::url('overview') . '" style="margin:0" '
            . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_ACCOUNTS_CONFIRM_ALL') . '\');">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="account_accept_all">'
            . '<button type="submit" class="htpx-btn2">' . htp_t('COM_HTPROTECT_ACCOUNTS_ACCEPTALL_BTN') . '</button>'
            . '</form></div></div>';
    }

    /**
     * Karte "Gefundene Schaddateien/Verzeichnisse" - EXPERIMENTELLES Feature,
     * lebt ganz unten im Site-Scan-Bereich (NICHT in der Uebersicht/Ampel).
     * Der tiefe Inhalts-Scan startet NUR per Button; danach Trefferliste mit
     * aufklappbarer Voransicht, X je Datei, "Alle loeschen" und Whitelist.
     */
    private static function renderMalFiles($cfg)
    {
        // Scan-Engine fehlt? Der Datei-Scan braucht scanner.php (Scan/Loeschen) UND
        // content.php (Erkennung) - beide sind bootstrap-OPTIONAL, damit ein Server-
        // Virenscanner (Imunify o. ae.), der eine davon faelschlich entfernt, HTProtect
        // NICHT lahmlegt (der Rest laeuft weiter). Fehlt eine -> klar + passend melden
        // (welche Datei) statt einen irrefuehrenden "sauberen" Scan anzubieten.
        if (!class_exists('HtpScanner') || !class_exists('HtpContent')) {
            echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_MALF_TITLE') . '</h2>'
                . '<div class="htpx-card htpx-card-wide">'
                . '<p>' . self::badge('err', htp_t('COM_HTPROTECT_BADGE_INACTIVE')) . ' '
                . htp_t('COM_HTPROTECT_MALF_ENGINE_MISSING') . '</p>'
                . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_MALF_ENGINE_MISSING_HINT') . '</p>'
                . '</div>';

            return;
        }

        $wl = class_exists('HtpStorage')
            ? HtpStorage::normalizeWhitelist(isset($cfg['mal_whitelist']) ? $cfg['mal_whitelist'] : array())
            : array();

        echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_MALF_TITLE')
            . ' ' . self::badge('warn', htp_t('COM_HTPROTECT_MALF_EXPERIMENTAL')) . '</h2>'
            . '<div class="htpx-card htpx-card-wide" id="htp-a-malfiles">'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_MALF_INTRO') . '</p>'
            // Verstecktes Formular liefert nur das CSRF-Token fuer die AJAX-Posts.
            . '<form id="htpx-malform" method="post" action="' . self::url('scan') . '" style="display:none">'
            . self::tokenField() . '</form>'
            . '<div class="htpx-actions">'
            . '<button type="button" id="htpx-malstart" class="htpx-btn">'
            . htp_t('COM_HTPROTECT_MALF_START_BTN') . '</button>'
            // Definitionen aktualisieren - minimal: nutzt denselben defs_update-Task
            // wie die Uebersicht (frischt WAF- UND Malware-Signaturen). Sekundaerer
            // Inline-Button neben dem Scan, kein eigener Bereich (keine UI-Blaehung).
            . '<form method="post" action="' . self::url('scan') . '" style="display:inline">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="defs_update">'
            . '<button type="submit" class="htpx-btn2 htpx-btn-s" title="' . htp_esc(htp_t_attr('COM_HTPROTECT_MALF_DEFS_UPDATE_HINT')) . '">'
            . htp_t('COM_HTPROTECT_MALF_DEFS_UPDATE_BTN') . '</button>'
            . '</form>'
            . '</div>'
            . '<div id="htpx-malstatus" class="htpx-scanstate" style="display:none"></div>'
            // Fortschritt: animierter "arbeitet..."-Balken (UNbestimmt - ein
            // verzeichnisbasierter Prozentwert taeuscht bei ungleicher Datei-
            // Verteilung "fast fertig" vor) + Live-Zaehler. Beides kostenlos
            // (die Werte fallen im Scan ohnehin an, kein Vor-Zaehllauf).
            . '<div id="htpx-malprog" class="htpx-malprog" style="display:none">'
            . '<div class="htpx-progbar"><div id="htpx-progfill" class="htpx-progfill"></div></div>'
            . '<div id="htpx-progtext" class="htpx-dim htpx-progtext"></div>'
            . '</div>'
            . '<div id="htpx-malnote" class="htpx-dim htpx-malnote" style="display:none"></div>'
            // Erscheint NUR nach einem Schnell-/Delta-Scan: bei gleicher Version,
            // aber verbesserten Mustern lassen sich so die uebersprungenen Dateien
            // bewusst noch einmal komplett pruefen.
            . '<a href="#" id="htpx-malfull" class="htpx-malfull" style="display:none">'
            . '[ ' . htp_t('COM_HTPROTECT_MALF_FULLSCAN_BTN') . ' ]</a>'
            // Backup-Empfehlung (nur bei Funden): bewusst VOR allen Loeschwegen
            // (Aktionsleiste UND "Alle loeschen"), damit jeder sie sieht, bevor er
            // irgendwo loescht. Datum = aeltester Fund (per JS gefuellt).
            . '<p id="htpx-malbackup" class="htpx-malbackup" style="display:none"></p>'
            // Batch-Leiste: Alle markieren + Aktionen fuer die Auswahl (Shift-Range im JS).
            . '<div id="htpx-maltoolbar" class="htpx-maltoolbar" style="display:none">'
            . '<label class="htpx-malselall-l"><input type="checkbox" id="htpx-malselall"> '
            . htp_t('COM_HTPROTECT_MALF_SELALL') . '</label>'
            . '<span id="htpx-malselinfo" class="htpx-dim"></span>'
            . '<span class="htpx-malspacer"></span>'
            . '<button type="button" id="htpx-malwlsel" class="htpx-btn2 htpx-btn-s" disabled>'
            . htp_t('COM_HTPROTECT_MALF_WLSEL_BTN') . '</button>'
            . '<button type="button" id="htpx-maldelsel" class="htpx-btn htpx-btn-danger htpx-btn-s" disabled>'
            . htp_t('COM_HTPROTECT_MALF_DELSEL_BTN') . '</button>'
            . '</div>'
            // Sortierleiste (JS): nach Pfad bzw. Datei-Aenderungsdatum.
            . '<div id="htpx-malsort" class="htpx-malsort" style="display:none">'
            . '<span class="htpx-dim">' . htp_t('COM_HTPROTECT_MALF_SORT') . '</span>'
            . '<button type="button" id="htpx-malsort-path" class="htpx-sortbtn">' . htp_t('COM_HTPROTECT_MALF_SORT_PATH') . '</button>'
            . '<button type="button" id="htpx-malsort-date" class="htpx-sortbtn">' . htp_t('COM_HTPROTECT_MALF_SORT_DATE') . '</button>'
            . '</div>'
            . '<div id="htpx-mallist" class="htpx-mallist"></div>'
            . '<div id="htpx-malfoot" class="htpx-malfoot" style="display:none">'
            . '<button type="button" id="htpx-maldelall" class="htpx-btn htpx-btn-danger">'
            . htp_t('COM_HTPROTECT_MALF_DELALL_BTN') . '</button>'
            . '<p class="htpx-warnhint">&#9888; ' . htp_t('COM_HTPROTECT_MALF_DELALL_HINT') . '</p>'
            . '</div>'
            // ── Vom Hoster gesperrte Dateien (chmod 000) - EIGENER Bereich, klar getrennt von den
            //    Funden. Manche Hoster-Virenscanner sperren Funde so (auch mal faelschlich eine
            //    legitime Datei); hier einordnen (Entsperr-Scan), loeschen ODER Rechte wiederherstellen. ──
            . '<div id="htpx-quarwrap" class="htpx-quarwrap" style="display:none">'
            . '<h3 class="htpx-quarh">' . htp_t('COM_HTPROTECT_MALF_QUAR_TITLE')
            . ' <span id="htpx-quarcount" class="htpx-quarcount"></span></h3>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_MALF_QUAR_INTRO') . '</p>'
            . '<div id="htpx-quarlist" class="htpx-quarlist"></div>'
            . '</div>';

        // ── Whitelist-Verwaltung (kompakt, aufklappbar) ──
        echo '<details class="htpx-wl"><summary>' . htp_t('COM_HTPROTECT_MALF_WL_TITLE')
            . ' <span id="htpx-wlcount">(' . count($wl) . ')</span></summary>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_MALF_WL_HINT') . '</p>'
            . '<ul id="htpx-wllist" class="htpx-wllist">';

        foreach ($wl as $entry) {
            echo '<li class="htpx-wlitem" data-path="' . htp_esc($entry) . '">'
                . '<span class="htpx-mono">' . htp_esc($entry) . '</span>'
                . '<button type="button" class="htpx-wlx" title="' . htp_esc(htp_t('COM_HTPROTECT_MALF_WL_REMOVE')) . '">&times;</button>'
                . '</li>';
        }

        echo '</ul>'
            . '<div class="htpx-wladd">'
            . '<input type="text" id="htpx-wlinput" class="htpx-input" placeholder="' . htp_esc(htp_t('COM_HTPROTECT_MALF_WL_PLACEHOLDER')) . '">'
            . '<button type="button" id="htpx-wladd" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_MALF_WL_ADD') . '</button>'
            . '</div></details>';

        // Schwarm-Schutz: dezenter Experten-Aufklapp (FP/FN melden) - direkt
        // unter der Whitelist im Malware-Scanner-Bereich.
        self::renderSwarm($cfg);

        // Texte fuer das JS (uebersetzt, sicher als data-Attribute; RAW-UTF-8).
        echo '<span id="htpx-mali18n" style="display:none"'
            . ' data-none="' . htp_esc(htp_t('COM_HTPROTECT_MALF_NONE')) . '"'
            . ' data-found="' . htp_esc(htp_t('COM_HTPROTECT_MALF_FOUND')) . '"'
            . ' data-err="' . htp_esc(htp_t('COM_HTPROTECT_MALF_ERR')) . '"'
            . ' data-confirm1="' . htp_esc(htp_t('COM_HTPROTECT_MALF_CONFIRM_ONE')) . '"'
            . ' data-confirmall="' . htp_esc(htp_t('COM_HTPROTECT_MALF_CONFIRM_ALL')) . '"'
            . ' data-deleted="' . htp_esc(htp_t('COM_HTPROTECT_MALF_DELETED')) . '"'
            . ' data-failed="' . htp_esc(htp_t('COM_HTPROTECT_MALF_FAILED')) . '"'
            . ' data-preview="' . htp_esc(htp_t('COM_HTPROTECT_MALF_PREVIEW')) . '"'
            . ' data-safe="' . htp_esc(htp_t('COM_HTPROTECT_MALF_SAFE_BTN')) . '"'
            . ' data-wlremove="' . htp_esc(htp_t('COM_HTPROTECT_MALF_WL_REMOVE')) . '"'
            . ' data-scanning="' . htp_esc(htp_t('COM_HTPROTECT_MALF_SCANNING')) . '"'
            . ' data-rescan="' . htp_esc(htp_t('COM_HTPROTECT_MALF_RESCAN_BTN')) . '"'
            . ' data-selcount="' . htp_esc(htp_t('COM_HTPROTECT_MALF_SELCOUNT')) . '"'
            . ' data-progress="' . htp_esc(htp_t('COM_HTPROTECT_MALF_PROGRESS')) . '"'
            . ' data-progskip="' . htp_esc(htp_t('COM_HTPROTECT_MALF_PROG_SKIP')) . '"'
            . ' data-modedelta="' . htp_esc(htp_t('COM_HTPROTECT_MALF_MODE_DELTA')) . '"'
            . ' data-modefull="' . htp_esc(htp_t('COM_HTPROTECT_MALF_MODE_FULL')) . '"'
            . ' data-edit="' . htp_esc(htp_t('COM_HTPROTECT_MALF_EDIT_BTN')) . '"'
            . ' data-save="' . htp_esc(htp_t('COM_HTPROTECT_MALF_SAVE_BTN')) . '"'
            . ' data-cancel="' . htp_esc(htp_t('COM_HTPROTECT_MALF_CANCEL_BTN')) . '"'
            . ' data-editnote="' . htp_esc(htp_t('COM_HTPROTECT_MALF_EDITNOTE')) . '"'
            . ' data-savefail="' . htp_esc(htp_t('COM_HTPROTECT_MALF_SAVE_FAIL')) . '"'
            . ' data-saveddirty="' . htp_esc(htp_t('COM_HTPROTECT_MALF_SAVED_DIRTY')) . '"'
            . ' data-truncated="' . htp_esc(htp_t('COM_HTPROTECT_MALF_TRUNCATED')) . '"'
            . ' data-saveincomplete="' . htp_esc(htp_t('COM_HTPROTECT_MALF_SAVE_INCOMPLETE')) . '"'
            . ' data-saveunwritable="' . htp_esc(htp_t('COM_HTPROTECT_MALF_SAVE_UNWRITABLE')) . '"'
            . ' data-saving="' . htp_esc(htp_t('COM_HTPROTECT_MALF_SAVING')) . '"'
            . ' data-toobig="' . htp_esc(htp_t('COM_HTPROTECT_MALF_TOOBIG')) . '"'
            . ' data-backup="' . htp_esc(htp_t('COM_HTPROTECT_MALF_BACKUP_HINT')) . '"'
            . ' data-quarlocked="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_LOCKED')) . '"'
            . ' data-quarcheck="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_CHECK')) . '"'
            . ' data-quaredit="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_EDIT')) . '"'
            . ' data-quardel="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_DEL')) . '"'
            . ' data-quarrestore="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_RESTORE')) . '"'
            . ' data-quarmalware="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_MALWARE')) . '"'
            . ' data-quarclean="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_CLEAN')) . '"'
            . ' data-quarunverif="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_UNVERIF')) . '"'
            . ' data-quarlooksclean="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_LOOKSCLEAN')) . '"'
            . ' data-quarrestconfirm="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_RESTORE_CONFIRM')) . '"'
            . ' data-quarcountlbl="' . htp_esc(htp_t('COM_HTPROTECT_MALF_QUAR_COUNT')) . '"'
            . '></span>'
            // Randloser Vollbild-Editor (ueber der ganzen Seite); position:fixed
            // -> die DOM-Lage spielt keine Rolle.
            . '<div id="htpx-maledit" class="htpx-maledit-fs" style="display:none" role="dialog" aria-modal="true" aria-label="' . htp_esc(htp_t('COM_HTPROTECT_MALF_EDIT_BTN')) . '">'
            . '<div class="htpx-maledit-bar">'
            . '<span id="htpx-maledit-path" class="htpx-mono htpx-maledit-path"></span>'
            . '<span id="htpx-maledit-note" class="htpx-dim"></span>'
            . '<span class="htpx-malspacer"></span>'
            . '<button type="button" id="htpx-maledit-save" class="htpx-btn">' . htp_t('COM_HTPROTECT_MALF_SAVE_BTN') . '</button>'
            . '<button type="button" id="htpx-maledit-close" class="htpx-btn2">' . htp_t('COM_HTPROTECT_MALF_CLOSE_BTN') . '</button>'
            . '</div>'
            // Editor mit Zeilennummern-Spalte (links, kompakt): Gutter + Textarea
            // teilen sich Zeilenhoehe/-groesse; der Gutter folgt dem Textarea-Scroll.
            . '<div class="htpx-maledit-wrap">'
            . '<div id="htpx-maledit-gutter" class="htpx-maledit-gutter" aria-hidden="true"></div>'
            . '<textarea id="htpx-maledit-ta" class="htpx-maledit-ta" spellcheck="false" wrap="off"></textarea>'
            . '</div>'
            . '</div>'
            . '</div>';
    }

    /**
     * Kerndatei-Abgleich: vergleicht die installierten Joomla-Kerndateien gegen
     * das offizielle Update-Paket der erkannten Version (frisch geladen + gecacht,
     * dasselbe Paket wie Joomlas eigenes "Kerndateien neu installieren")
     * und bietet je Abweichung 1-Klick-Wiederherstellung. Chunked/resumbar mit
     * determiniertem Fortschrittsbalken - identische Optik wie der Malware-Scanner
     * darueber. Bootstrap-optional: fehlt HtpCoreScan, meldet die Karte das klar.
     */
    private static function renderCoreCompare($cfg)
    {
        if (!class_exists('HtpCoreScan')) {
            echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_CORE_TITLE') . '</h2>'
                . '<div class="htpx-card htpx-card-wide">'
                . '<p>' . self::badge('err', htp_t('COM_HTPROTECT_BADGE_INACTIVE')) . ' '
                . htp_t('COM_HTPROTECT_CORE_ENGINE_MISSING') . '</p></div>';

            return;
        }

        $ver = HtpCoreScan::detectVersion();

        // Bereits bewusst akzeptierte (checksum-gebundene) Abweichungen - z. B. vom Webhoster
        // eingespielte Fremd-Patches (Patchman). Serverseitig gelistet (jederzeit verwaltbar), das
        // JS blendet die Sektion beim Ignorieren/Wieder-pruefen zusaetzlich live ein/aus.
        $ignored = HtpCoreScan::ackList();
        $ignRows = '';

        foreach ($ignored as $ip) {
            $ignRows .= '<div class="htpx-coreignrow" data-path="' . htp_esc($ip) . '">'
                . '<span class="htpx-malpath">' . htp_esc($ip) . '</span>'
                . '<button type="button" class="htpx-btn2 htpx-btn-s htpx-coreunign">'
                . htp_tc('COM_HTPROTECT_CORE_UNIGNORE_BTN', 'Wieder pruefen') . '</button>'
                . '</div>';
        }

        echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_CORE_TITLE') . '</h2>'
            . '<div class="htpx-card htpx-card-wide" id="htp-a-core">'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_CORE_INTRO') . '</p>'
            // Verstecktes Formular liefert nur das CSRF-Token fuer die AJAX-Posts.
            . '<form id="htpx-coreform" method="post" action="' . self::url('scan') . '" style="display:none">'
            . self::tokenField() . '</form>'
            . '<div class="htpx-actions">'
            . '<button type="button" id="htpx-corestart" class="htpx-btn">' . htp_t('COM_HTPROTECT_CORE_START_BTN') . '</button>'
            . '<span class="htpx-dim htpx-core-ver">'
            . ($ver !== '' ? htp_t('COM_HTPROTECT_CORE_VERSION', htp_esc($ver)) : htp_t('COM_HTPROTECT_CORE_VERSION_UNKNOWN'))
            . '</span>'
            . '</div>'
            . '<div id="htpx-corestatus" class="htpx-scanstate" style="display:none"></div>'
            // Determinierter Fortschrittsbalken: Phasen-Label links, % rechts.
            . '<div id="htpx-coreprog" class="htpx-progress" style="display:none">'
            . '<div class="htpx-progress-info"><span id="htpx-corephase"></span><span id="htpx-corepct"></span></div>'
            . '<div class="htpx-progress-track"><div id="htpx-corebar" class="htpx-progress-bar"></div></div>'
            . '</div>'
            . '<div id="htpx-corenote" class="htpx-dim htpx-malnote" style="display:none"></div>'
            // Fehlerfall: Pruefung erneut versuchen (eine beschaedigte Referenz laedt automatisch neu).
            . '<div id="htpx-coreerr" class="htpx-core-err" style="display:none">'
            . '<button type="button" id="htpx-corereload" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_CORE_RELOAD_BTN') . '</button>'
            . '</div>'
            . '<div id="htpx-coretoolbar" class="htpx-maltoolbar" style="display:none">'
            . '<span id="htpx-coreselinfo" class="htpx-dim"></span>'
            . '<span class="htpx-malspacer"></span>'
            . '<button type="button" id="htpx-corerestall" class="htpx-btn htpx-btn-s">' . htp_t('COM_HTPROTECT_CORE_RESTALL_BTN') . '</button>'
            . '</div>'
            . '<div id="htpx-corelist" class="htpx-mallist"></div>'
            // Bewusst akzeptierte (checksum-gebundene) Abweichungen, z. B. Webhoster-Patches (Patchman).
            // Serverseitig befuellt; leer -> ausgeblendet (das JS zeigt/verwaltet sie zusaetzlich live).
            . '<details id="htpx-coreignored" class="htpx-details htpx-coreign"' . ($ignRows === '' ? ' style="display:none"' : '') . '>'
            . '<summary>' . htp_tc('COM_HTPROTECT_CORE_IGNORED_TITLE', 'Bewusst akzeptierte Kerndateien') . '</summary>'
            . '<p class="htpx-dim">' . htp_tc('COM_HTPROTECT_CORE_IGNORED_INTRO', 'Diese geaenderten Kerndateien wurden bewusst akzeptiert - typischerweise vom Webhoster eingespielte Sicherheits-Patches (etwa Patchman), die eine Kerndatei veraendern. Die Ausnahme ist an den aktuellen Inhalt gebunden: aendert sich die Datei, erscheint sie automatisch wieder als Abweichung. &bdquo;Wieder pruefen&ldquo; hebt die Ausnahme auf.') . '</p>'
            . '<div id="htpx-coreignlist" class="htpx-coreignlist">' . $ignRows . '</div>'
            . '</details>'
            // i18n-Bruecke fuers JS (alle Strings via data-Attribute).
            . '<span id="htpx-corei18n" style="display:none"'
            . ' data-scanning="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_SCANNING')) . '"'
            . ' data-rescan="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_RESCAN_BTN')) . '"'
            . ' data-clean="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_CLEAN')) . '"'
            . ' data-found="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_FOUND')) . '"'
            . ' data-summary="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_SUMMARY')) . '"'
            . ' data-err="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_ERR_GENERIC')) . '"'
            . ' data-restore="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_RESTORE_BTN')) . '"'
            . ' data-restoring="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_RESTORING')) . '"'
            . ' data-restored="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_RESTORED')) . '"'
            . ' data-restfail="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_RESTFAIL')) . '"'
            . ' data-kchanged="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_KIND_CHANGED')) . '"'
            . ' data-kmissing="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_KIND_MISSING')) . '"'
            . ' data-patched="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_PATCHED_NOTE')) . '"'
            . ' data-restallc="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_RESTALL_CONFIRM')) . '"'
            . ' data-diff="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_DIFF_BTN')) . '"'
            . ' data-diffhide="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_DIFF_HIDE')) . '"'
            . ' data-diffloading="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_DIFF_LOADING')) . '"'
            . ' data-diffbinary="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_DIFF_BINARY')) . '"'
            . ' data-diffnone="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_DIFF_NONE')) . '"'
            . ' data-ignore="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_IGNORE_BTN')) . '"'
            . ' data-ignoring="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_IGNORING')) . '"'
            . ' data-ignfail="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_IGNFAIL')) . '"'
            . ' data-ignconf="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_IGNORE_CONFIRM')) . '"'
            . ' data-unignore="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_UNIGNORE_BTN')) . '"'
            . ' data-ignnote="' . htp_esc(htp_t_attr('COM_HTPROTECT_CORE_IGNORED_NOTE')) . '"'
            . ' data-ver="' . htp_esc($ver) . '"></span>'
            . '</div>';
    }

    /**
     * Nach "Jetzt absichern": transparent zeigen, welche echten Eigenregeln aus
     * der bisherigen .htaccess NICHT in die neue, gehaertete Datei uebernommen
     * wurden (Original liegt im Backup), mit positionstreuer Ein-Klick-Uebernahme.
     */
    private static function renderQuickRemoved($data)
    {
        if (empty($data['quick_removed'])) {
            return;
        }

        $rules  = (string) $data['quick_removed'];
        $backup = isset($data['quick_removed_backup']) ? (string) $data['quick_removed_backup'] : '';

        echo '<div class="htpx-card htpx-card-wide htpx-card-danger">'
            . '<h4>' . htp_t('COM_HTPROTECT_QUICK_REMOVED_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_QUICK_REMOVED_INTRO') . '</p>'
            . '<pre class="htpx-pre">' . htp_esc($rules) . '</pre>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_QUICK_REMOVED_SORTHINT') . '</p>'
            . '<div class="htpx-actions">';

        if ($backup !== '') {
            echo '<form method="post" action="' . self::url('overview') . '" style="margin:0">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="quick_adopt">'
                . '<input type="hidden" name="htp_backup" value="' . htp_esc($backup) . '">'
                . '<button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_QUICK_REMOVED_ADOPT_BTN') . '</button></form> ';
        }

        echo '<a class="htpx-btn2" href="' . self::url('backups') . '">' . htp_t('COM_HTPROTECT_QUICK_REMOVED_BACKUP_LINK') . '</a>'
            . '</div></div>';
    }

    /**
     * Protokoll der vom Echtzeit-Schutz geblockten Anfragen.
     */
    private static function renderWafLog($data)
    {
        $log = isset($data['waf_log']) && is_array($data['waf_log']) ? $data['waf_log'] : array();

        if (empty($log)) {
            return;
        }

        echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_WAFLOG_TITLE') . ' '
            . '<form method="post" action="' . self::url('overview') . '" style="display:inline;margin:0">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="waf_clear">'
            . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_WAFLOG_CLEAR_BTN') . '</button></form></h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_WAFLOG_INTRO') . '</p>'
            . '<table class="htpx-table htpx-stack"><thead><tr><th>' . htp_t('COM_HTPROTECT_WAFLOG_TH_TIME') . '</th><th>' . htp_t('COM_HTPROTECT_WAFLOG_TH_SIG') . '</th><th>IP</th><th>' . htp_t('COM_HTPROTECT_WAFLOG_TH_CALL') . '</th><th>' . htp_t('COM_HTPROTECT_WAFLOG_TH_STATUS') . '</th></tr></thead><tbody>';

        // Roh (nicht htp_esc): Lang-Strings koennen HTML-Entities enthalten, die
        // im data-label-Attribut korrekt dekodieren; htp_esc wuerde doppelt escapen.
        $lblT = htp_t('COM_HTPROTECT_WAFLOG_TH_TIME');
        $lblS = htp_t('COM_HTPROTECT_WAFLOG_TH_SIG');
        $lblC = htp_t('COM_HTPROTECT_WAFLOG_TH_CALL');
        $lblX = htp_t('COM_HTPROTECT_WAFLOG_TH_STATUS');

        foreach ($log as $entry) {
            echo '<tr>'
                . '<td class="htpx-mono" data-label="' . $lblT . '">' . htp_esc(isset($entry['t']) ? $entry['t'] : '') . '</td>'
                . '<td data-label="' . $lblS . '">' . self::badge('err', htp_esc(isset($entry['sig']) ? $entry['sig'] : '?')) . '</td>'
                . '<td class="htpx-mono" data-label="IP">' . htp_esc(isset($entry['ip']) ? $entry['ip'] : '') . '</td>'
                . '<td class="htpx-mono htpx-dim htpx-waf-uri" data-label="' . $lblC . '">' . htp_esc(isset($entry['uri']) ? $entry['uri'] : '') . '</td>'
                . '<td data-label="' . $lblX . '">' . (isset($entry['mode']) && $entry['mode'] === 'log' ? '<span class="htpx-dim">' . htp_t('COM_HTPROTECT_WAFLOG_LOGONLY') . '</span>' : self::badge('err', htp_t('COM_HTPROTECT_WAFLOG_BLOCKED'))) . '</td>'
                . '</tr>';
        }

        echo '</tbody></table></div>';
    }

    /**
     * Sicherheits-Ampel und "Jetzt absichern".
     */
    private static function renderAmpel($items, $st, $nginxMode = false)
    {
        $red  = 0;
        $yell = 0;

        foreach ($items as $item) {
            if ($item['state'] === 'err') {
                $red++;
            } elseif ($item['state'] === 'warn') {
                $yell++;
            }
        }

        $overall = $red > 0 ? 'err' : ($yell > 0 ? 'warn' : 'ok');
        $headline = $overall === 'ok'
            ? htp_t('COM_HTPROTECT_AMPEL_ALLGOOD')
            : ($overall === 'err' ? htp_t('COM_HTPROTECT_AMPEL_URGENT', $red) : htp_t('COM_HTPROTECT_AMPEL_HINTS', $yell));

        echo '<div class="htpx-card htpx-card-wide htpx-ampelcard">';
        echo '<div class="htpx-ampelhead">'
            . '<h4>' . htp_t('COM_HTPROTECT_AMPEL_TITLE', $headline) . '</h4>';

        if (!$nginxMode) {
            if ($overall === 'ok') {
                // Vollstaendig abgesichert: ruhiger Erfolgszustand statt CTA.
                // Animiertes Schild+Haekchen (einmaliger Draw/Ring), erneutes
                // Schreiben nur als unaufdringliches Ghost-Icon.
                $refresh = htp_t('COM_HTPROTECT_QUICK_BTN_REFRESH');
                echo '<div class="htpx-secured">'
                    . '<span class="htpx-secured-icon"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">'
                    . '<path class="htpx-shield" d="M12 2l8 3v6c0 5-3.4 9.4-8 11-4.6-1.6-8-6-8-11V5l8-3z" fill="none" stroke-width="2" stroke-linejoin="round"/>'
                    . '<path class="htpx-check" d="M8.5 12l2.5 2.5 4.5-5" fill="none" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>'
                    . '</svg></span>'
                    . '<span class="htpx-secured-text">' . htp_t('COM_HTPROTECT_AMPEL_SECURED') . '</span>'
                    . '<form method="post" action="' . self::url('overview') . '" style="margin:0" '
                    . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_QUICK_CONFIRM') . '\');">'
                    . self::tokenField()
                    . '<input type="hidden" name="htp_task" value="quick_secure">'
                    . '<button type="submit" class="htpx-iconbtn" title="' . htp_esc($refresh) . '" aria-label="' . htp_esc($refresh) . '">'
                    . '<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="currentColor"/></svg>'
                    . '</button>'
                    . '</form>'
                    . '</div>';
            } else {
                echo '<form method="post" action="' . self::url('overview') . '" style="margin:0" '
                    . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_QUICK_CONFIRM') . '\');">'
                    . self::tokenField()
                    . '<input type="hidden" name="htp_task" value="quick_secure">'
                    . '<button type="submit" class="htpx-btn">&#9889; ' . htp_t('COM_HTPROTECT_QUICK_BTN') . '</button>'
                    . '</form>';
            }
        }

        echo '</div>';

        echo '<ul class="htpx-ampel">';

        foreach ($items as $item) {
            $anchor = isset($item['anchor']) && $item['anchor'] !== '' ? '#' . htp_esc($item['anchor']) : '';
            echo '<li class="htpx-ampel-row">'
                . '<span class="htpx-dot htpx-dot-' . htp_esc($item['state']) . '"></span>'
                . '<span class="htpx-ampel-label"><a class="htpx-ampel-link" href="' . self::url($item['sec']) . $anchor . '">' . $item['label'] . '</a>'
                . ($item['hint'] !== '' ? '<small class="htpx-dim">' . $item['hint'] . '</small>' : '')
                . '</span></li>';
        }

        echo '</ul></div>';
    }

    /**
     * Waechter-Alarme inkl. Ein-Klick-Wiederherstellung.
     */
    private static function renderWatchAlarms($incidents)
    {
        echo '<div class="htpx-card htpx-card-wide htpx-card-danger">'
            . '<h4>&#9888; ' . htp_t('COM_HTPROTECT_WATCH_ALARM_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_WATCH_ALARM_TEXT') . '</p>';

        foreach ($incidents as $incident) {
            $kindText = $incident['kind'] === 'deleted' ? htp_t('COM_HTPROTECT_WATCH_DELETED') : htp_t('COM_HTPROTECT_WATCH_CHANGED');
            echo '<div class="htpx-watchrow">'
                . self::badge('err', $kindText) . ' <span class="htpx-mono">' . htp_esc($incident['label']) . '</span> '
                . '<form method="post" action="' . self::url('overview') . '" style="display:inline;margin:0">'
                . self::tokenField()
                . '<input type="hidden" name="htp_watch_path" value="' . htp_esc($incident['path']) . '">'
                . '<button type="submit" name="htp_task" value="watch_restore" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_RESTORE_LC') . '</button> '
                // Verwerfen: aktuellen Stand als neue Baseline akzeptieren (legitime
                // Aenderung, kein Angriff) -> Vorfall verschwindet, NICHTS wird geschrieben.
                . '<button type="submit" name="htp_task" value="watch_dismiss" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_WATCH_DISMISS_LC') . '</button>'
                . '</form></div>';
        }

        echo '</div>';
    }

    /**
     * Liste verwundbarer Erweiterungen.
     */
    private static function renderVulnList($vuln)
    {
        // Spaltenkoepfe als Variablen (auch fuer data-label im Mobil-Stack-Layout).
        $tE = htp_t('COM_HTPROTECT_VULN_TH_EXT');
        $tI = htp_t('COM_HTPROTECT_VULN_TH_INSTALLED');
        $tS = htp_t('COM_HTPROTECT_VULN_TH_SAFEFROM');
        $tN = htp_t('COM_HTPROTECT_VULN_TH_NOTE');
        $tA = htp_t('COM_HTPROTECT_VULN_TH_ACTION');
        $act = self::url('overview');

        echo '<div class="htpx-card htpx-card-wide htpx-card-danger" id="htp-a-vuln">'
            . '<h4>&#9888; ' . htp_t('COM_HTPROTECT_VULN_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_VULN_TEXT') . '</p>'
            . '<table class="htpx-table htpx-stack"><thead><tr><th>' . $tE . '</th><th>' . $tI . '</th><th>' . $tS . '</th><th>' . $tN . '</th><th>' . $tA . '</th></tr></thead><tbody>';

        foreach ($vuln as $v) {
            $sev    = $v['severity'] === 'critical' ? 'err' : 'warn';
            $extKey = class_exists('HtpDefs') ? HtpDefs::vulnExtKey($v) : '';
            // Bestaetigungstext deckungsbewusst: bei NICHT geblockter Luecke deutlich schaerfer.
            $conf   = htp_t(empty($v['mitigated']) ? 'COM_HTPROTECT_VULN_ACK_CONFIRM_RISK' : 'COM_HTPROTECT_VULN_ACK_CONFIRM');
            echo '<tr>'
                . '<td data-label="' . $tE . '">' . self::badge($sev, htp_esc($v['name'])) . ($v['enabled'] ? '' : ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_DISABLED_PAREN') . '</span>') . '</td>'
                . '<td class="htpx-mono" data-label="' . $tI . '">' . htp_esc($v['installed'] !== '' ? $v['installed'] : '?') . '</td>'
                . '<td class="htpx-mono" data-label="' . $tS . '">' . htp_esc($v['below']) . '</td>'
                . '<td data-label="' . $tN . '"><span class="htpx-cellval">' . htp_esc(HtpDefs::localized($v, 'note'))
                . (HtpDefs::localized($v, 'advisory') !== '' ? ' <a href="' . htp_esc(HtpDefs::localized($v, 'advisory')) . '" target="_blank" rel="noopener"><strong>' . htp_t('COM_HTPROTECT_MORE_INFO') . '</strong></a>' : '')
                . '</span></td>'
                . '<td data-label="' . $tA . '">'
                . '<form method="post" action="' . $act . '" style="margin:0" data-htpconfirm="' . htp_esc($conf) . '">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="vuln_ack">'
                . '<input type="hidden" name="htp_extkey" value="' . htp_esc($extKey) . '">'
                . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_VULN_ACK_BTN') . '</button>'
                . '</form></td></tr>';
        }

        echo '</tbody></table></div>';
        self::confirmSubmitJs();
    }

    /**
     * Aufgeschobene (still gestellte) Erweiterungs-Warnungen: eigene, NICHT-rote Karte -
     * ehrlich sichtbar, deckungsbewusster Status (aktiv geschuetzt vs. Restrisiko) + jederzeit
     * "Wieder aktivieren". So ist ein reales Loch nie versteckt, nur die Nag-Schleife ruht.
     */
    private static function renderVulnAcked($vuln)
    {
        $tE  = htp_t('COM_HTPROTECT_VULN_TH_EXT');
        $tI  = htp_t('COM_HTPROTECT_VULN_TH_INSTALLED');
        $tSt = htp_t('COM_HTPROTECT_VULN_ACKED_TH_STATUS');
        $tA  = htp_t('COM_HTPROTECT_VULN_TH_ACTION');
        $act = self::url('overview');

        echo '<div class="htpx-card htpx-card-wide" id="htp-a-vuln-acked" style="border-left:4px solid var(--line)">'
            . '<h4>' . htp_t('COM_HTPROTECT_VULN_ACKED_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_VULN_ACKED_TEXT') . '</p>'
            . '<table class="htpx-table htpx-stack"><thead><tr><th>' . $tE . '</th><th>' . $tI . '</th><th>' . $tSt . '</th><th>' . $tA . '</th></tr></thead><tbody>';

        foreach ($vuln as $v) {
            $extKey = class_exists('HtpDefs') ? HtpDefs::vulnExtKey($v) : '';
            $mit    = !empty($v['mitigated']);
            echo '<tr>'
                . '<td data-label="' . $tE . '">' . htp_esc($v['name']) . '</td>'
                . '<td class="htpx-mono" data-label="' . $tI . '">' . htp_esc($v['installed'] !== '' ? $v['installed'] : '?') . '</td>'
                . '<td data-label="' . $tSt . '">' . self::badge($mit ? 'ok' : 'warn', htp_t($mit ? 'COM_HTPROTECT_VULN_ACKED_PROTECTED' : 'COM_HTPROTECT_VULN_ACKED_RISK')) . '</td>'
                . '<td data-label="' . $tA . '">'
                . '<form method="post" action="' . $act . '" style="margin:0">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="vuln_unack">'
                . '<input type="hidden" name="htp_extkey" value="' . htp_esc($extKey) . '">'
                . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_VULN_UNACK_BTN') . '</button>'
                . '</form></td></tr>';
        }

        echo '</tbody></table></div>';
    }

    /**
     * Einmaliger, delegierter Bestaetigungs-Handler: JEDES Formular mit data-htpconfirm fragt
     * beim Absenden nach. Text steckt im Attribut (htp_esc, echte UTF-8-Umlaute) - kein
     * fehleranfaelliges Inline-Quoting. Statisch, damit das Script pro Seite nur EINMAL kommt.
     */
    private static function confirmSubmitJs()
    {
        static $done = false;

        if ($done) {
            return;
        }

        $done = true;

        echo '<script>(function(){document.addEventListener("submit",function(e){var f=e.target;'
            . 'if(f&&f.getAttribute&&f.getAttribute("data-htpconfirm")){'
            . 'if(!window.confirm(f.getAttribute("data-htpconfirm"))){e.preventDefault();}}},true);})();</script>';
    }

    /**
     * Karte: automatische Ueberwachung, Mail, Live-Update.
     */
    /**
     * Schmale 1-Klick-Leiste ueber der Ueberwachungs-Karte: "HTProtect automatisch
     * aktuell halten". Erscheint NUR, solange der Selbst-Sofort-Modus noch NICHT aktiv
     * ist (und das Auto-Update-Feature ueberhaupt greift: 5.4+ oder Joomla 3). Ein Klick
     * setzt Master + Selbst-Schalter (sofort/alle 15 Min, ohne Karenz) - danach verschwindet
     * die Leiste von selbst. Haelt die Uebersicht schlank.
     */
    private static function renderSelfUpdatePromo($cfg)
    {
        // Feature nicht verfuegbar (Joomla 4.0-<5.4) -> gar nichts anzeigen.
        if (!self::backupsUpdates()) {
            return;
        }

        // Bereits aktiv -> schlanke gruene Bestaetigung (positiver Zustand sichtbar)
        // + dezenter Abschalten-Link. Verlinkt auf den Schalter in "Backups & Updates".
        if (class_exists('HtpAutoUpdate') && HtpAutoUpdate::selfImmediate($cfg)) {
            echo '<div class="htpx-card htpx-card-wide" id="htp-a-selfup" '
                . 'style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-left:4px solid var(--acc)">'
                . self::badge('ok', htp_t('COM_HTPROTECT_BADGE_ACTIVE'))
                . '<span style="flex:1 1 200px;min-width:0">' . htp_t('COM_HTPROTECT_AU_SELF_ACTIVE') . '</span>'
                . '<form method="post" action="' . self::url('overview') . '" style="margin:0;flex:0 0 auto">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="au_self_off">'
                . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_AU_SELF_OFF_BTN') . '</button>'
                . '</form>'
                . '</div>';

            return;
        }

        // Noch nicht aktiv -> Werbeleiste mit Aktivieren-Knopf.
        echo '<div class="htpx-card htpx-card-wide" id="htp-a-selfup" '
            . 'style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;border-left:4px solid var(--acc)">'
            . '<div style="flex:1 1 240px;min-width:0">'
            . '<strong>' . htp_t('COM_HTPROTECT_AU_SELF_PROMO_TITLE') . '</strong>'
            . '<div class="htpx-dim" style="margin-top:2px">' . htp_t('COM_HTPROTECT_AU_SELF_PROMO_TEXT') . '</div>'
            . '</div>'
            . '<form method="post" action="' . self::url('overview') . '" class="htpx-selfup-cta">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="au_self_on">'
            . '<button type="submit" class="htpx-btn htpx-cta-pulse">' . htp_t('COM_HTPROTECT_AU_SELF_PROMO_BTN') . '</button>'
            . '</form>'
            . '</div>';
    }

    private static function renderMonitorCard($cfg, $data)
    {
        $plugin = isset($data['plugin']) ? $data['plugin'] : array('installed' => false, 'enabled' => false);
        $defs   = isset($data['defs']) ? $data['defs'] : array('version' => '?', 'source' => '', 'fetched' => '');
        $active = !empty($plugin['enabled']);

        echo '<div class="htpx-card htpx-card-wide" id="htp-a-monitor"><h4>' . htp_t('COM_HTPROTECT_MONITOR_TITLE') . '</h4>';
        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_MONITOR_INTRO') . '</p>';

        if ($active) {
            echo '<p>' . self::badge('ok', htp_t('COM_HTPROTECT_BADGE_ACTIVE')) . ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_MONITOR_ACTIVE_TEXT') . '</span></p>';
        } else {
            echo '<p>' . self::badge('warn', htp_t('COM_HTPROTECT_BADGE_INACTIVE')) . ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_MONITOR_INACTIVE_TEXT') . '</span></p>';
        }

        // Ein/Aus-Schalter (eigenes Formular). Beim ABschalten eine deutliche Rueckfrage: das
        // htprotectwatch-System-Plugin ist das Herzstueck - ohne es bricht vieles weg. Nur so ist
        // das Abschalten ueberhaupt moeglich (zusaetzliche Bestaetigung). Beim EINschalten keine.
        echo '<form method="post" action="' . self::url('overview') . '" style="margin:0 0 12px"'
            . ($active ? ' onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_MONITOR_DISABLE_CONFIRM') . '\');"' : '')
            . '>'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="' . ($active ? 'watch_disable' : 'watch_enable') . '">'
            // Nicht aktiv -> derselbe pulsierende Gruen-Glow-CTA wie "Automatische HTProtect-
            // Updates aktivieren" (empfohlene Aktivierung faellt besser auf). Aktiv -> schlichter
            // Abschalten-Button (htpx-btn2), kein Puls.
            . '<button type="submit" class="' . ($active ? 'htpx-btn2' : 'htpx-btn htpx-cta-pulse') . '">'
            . ($active ? htp_t('COM_HTPROTECT_MONITOR_TOGGLE_OFF') : htp_t('COM_HTPROTECT_MONITOR_TOGGLE_ON'))
            . '</button></form>';

        // Mail + Wiedererinnerungs-Abstand (eigenes Formular)
        $remDays = isset($cfg['notify_reminder_days']) ? (int) $cfg['notify_reminder_days'] : 14;

        echo '<form method="post" action="' . self::url('overview') . '" class="htpx-form">'
            . self::tokenField()
            . '<label class="htpx-label" for="htp_notify_email">' . htp_t('COM_HTPROTECT_MONITOR_EMAIL_LABEL') . '</label>'
            // Speichern DIREKT ans Ende der E-Mail-Zeile (gefuellt gruen wie die uebrigen
            // Primaer-Buttons) - so ist auf den ersten Blick klar, dass gespeichert werden muss.
            . '<div class="htpx-inline" style="gap:8px;flex-wrap:wrap;align-items:stretch">'
            . '<input class="htpx-input" type="email" multiple id="htp_notify_email" name="htp_notify_email" value="' . htp_esc($cfg['notify_email']) . '" placeholder="name@example.org, name2@example.org" style="flex:1 1 16em;min-width:0">'
            . '<button type="submit" name="htp_task" value="watch_save" class="htpx-btn">' . htp_t('COM_HTPROTECT_SAVE') . '</button>'
            . '</div>'
            . '<div class="htpx-dim" style="margin-top:4px">' . htp_t('COM_HTPROTECT_MONITOR_EMAIL_HINT') . '</div>'
            . '<div class="htpx-inline" style="margin-top:8px;flex-wrap:wrap">'
            . '<label class="htpx-label" for="htp_notify_reminder_days" style="margin:0">' . htp_t('COM_HTPROTECT_MONITOR_REMINDER_LABEL') . '</label> '
            . '<input class="htpx-input" type="number" id="htp_notify_reminder_days" name="htp_notify_reminder_days" value="' . $remDays . '" min="2" max="30" step="1" style="width:5em"> '
            . '<span class="htpx-dim">' . htp_t('COM_HTPROTECT_MONITOR_REMINDER_UNIT') . '</span> '
            . '<button type="submit" name="htp_task" value="watch_test_mail" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_WATCH_TEST_BUTTON') . '</button></div>'
            . '<label style="margin-top:10px;display:flex;align-items:center;gap:8px;cursor:pointer">'
            . '<input type="checkbox" name="htp_accounts_watch" value="1"' . (!empty($cfg['accounts_watch']) ? ' checked' : '') . '>'
            . '<span>' . htp_t('COM_HTPROTECT_MONITOR_ACCOUNTS_LABEL') . '</span></label>'
            // B/C-Recovery NUR ab Joomla 5: erst dort gibt es das abschaltbare
            // "Backward Compatibility"-Plugin (plg_behaviour_compat). Auf J2.5-4 sinnlos.
            . (version_compare(JVERSION, '5.0', '>=')
                ? '<label style="margin-top:8px;display:flex;align-items:center;gap:8px;cursor:pointer">'
                    . '<input type="checkbox" name="htp_compat_guard" value="1"' . (!empty($cfg['compat_guard']) ? ' checked' : '') . '>'
                    . '<span>' . htp_t('COM_HTPROTECT_MONITOR_COMPAT_LABEL') . '</span></label>'
                : '')
            . '</form>';

        echo '<p class="htpx-dim" style="margin-top:6px">' . htp_t('COM_HTPROTECT_MONITOR_ANTISPAM', $remDays) . '</p>';

        // Signaturen-Stand: zwei Schichten klar getrennt
        $liveVer = $defs['version'];
        $fileVer = (isset($cfg['defs_version']) && $cfg['defs_version'] !== '') ? $cfg['defs_version'] : '&ndash;';

        echo '<div style="margin-top:12px">'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SIG_LABEL') . ' &middot; <strong>' . htp_t('COM_HTPROTECT_SIG_FIREWALL') . '</strong> ' . htp_esc($liveVer)
            . ' <span class="htpx-dim">(' . htp_esc(self::dataText('COM_HTPROTECT_DEFSRC_' . strtoupper((string) $defs['source']), $defs['source'])) . ($defs['fetched'] !== '' ? ', ' . htp_esc($defs['fetched']) : '') . ')</span>'
            . ' &middot; <strong>' . htp_t('COM_HTPROTECT_SIG_FOUNDATION') . '</strong> ' . ($fileVer === '&ndash;' ? $fileVer : htp_esc($fileVer)) . '</p>';

        if ($liveVer !== '' && $fileVer !== '&ndash;' && $liveVer !== $fileVer) {
            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SIG_NEWER') . '</p>';
        }

        echo '<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">'
            . '<form method="post" action="' . self::url('overview') . '" style="margin:0">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="defs_update">'
            . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_SIG_UPDATE_BTN') . '</button>'
            . '</form>'
            // Fernverwaltungs-Einstieg: unscheinbar grau, rechtsbuendig auf gleicher Hoehe wie
            // "Definitionen jetzt aktualisieren". Nur wenn der Connector verfuegbar ist.
            . (class_exists('HtpLink')
                // Unscheinbarer Footer-Einstieg bleibt unveraendert; das Highlight (#htp-fv) zeigt jetzt
                // auf das Header-Icon (htpx-fvhead, siehe remoteLink()), nicht mehr auf diesen Link.
                ? '<a id="htp-fv-footer" href="' . self::url('connect') . '" style="margin-left:auto;color:var(--dim);font-size:.7rem;opacity:.6;text-decoration:none">' . htp_t('COM_HTPROTECT_FV_FOOTER') . '</a>'
                : '')
            . '</div></div>';

        // Hinweis: Der Notfall-Modus ist jetzt direkt ueber das Joomla-Komponentenmenue erreichbar
        // (Komponenten -> HTProtect -> "Notfall-Offline"), daher hier KEIN Extra-Ein-Zeiler mehr.

        echo '</div>';
    }

    // ─────────────────────────────────────────────────────────────────
    //  Bereich: Backend-Zugang
    // ─────────────────────────────────────────────────────────────────

    private static function secGuard($cfg, $data)
    {
        $guard = $data['guard'];

        if (!isset($guard['protected'])) {
            $guard['protected'] = !empty($guard['active']);
            $guard['source']    = !empty($guard['active']) ? 'htprotect' : 'none';
        }

        echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_GUARD_TITLE') . '</h2>';

        echo '<div class="htpx-card htpx-card-wide">';

        $gsrc = isset($guard['source']) ? $guard['source'] : 'none';

        if ($guard['active'] && isset($guard['mode']) && $guard['mode'] === 'string') {
            // Option 2 aktiv: Cookie-Gate ueber den geheimen Zugangs-String (kein Passwort-Prompt,
            // daher KEIN Benutzername). Details/Verwaltung stehen unten in der Zugangs-String-Karte.
            echo '<p>' . self::badge('ok', htp_tc('COM_HTPROTECT_GUARD_GATE_BADGE_ACTIVE', 'Zugangs-String aktiv')) . '</p>'
                . '<p class="htpx-dim">' . htp_tc('COM_HTPROTECT_GUARD_GATE_STATUS_TEXT', 'Das Backend ist ueber den geheimen Zugangs-String geschuetzt. Verwalten kannst du ihn unten unter "Alternative: Zugangs-String".') . '</p>'
                . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_GUARD_OPTIONAL_TITLE') . '</h4>';
        } elseif ($guard['active']) {
            echo '<p>' . self::badge('ok', htp_t('COM_HTPROTECT_GUARD_BADGE_ACTIVE')) . ' &nbsp;' . htp_t('COM_HTPROTECT_GUARD_USER_LABEL') . ' <strong>' . htp_esc($guard['user']) . '</strong></p>'
                . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_ACTIVE_TEXT') . '</p>'
                . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_GUARD_RENEW_TITLE') . '</h4>';
        } elseif ($gsrc === 'ecc') {
            echo '<p>' . self::badge('ok', htp_t('COM_HTPROTECT_GUARD_BADGE_ECC')) . '</p>'
                . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_ECC_TEXT') . '</p>'
                . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_GUARD_OPTIONAL_TITLE') . '</h4>';
        } elseif (!empty($guard['protected'])) {
            $where = $gsrc === 'foreign' ? htp_t('COM_HTPROTECT_GUARD_WHERE_HTACCESS') : htp_t('COM_HTPROTECT_GUARD_WHERE_SERVER');
            echo '<p>' . self::badge('ok', htp_t('COM_HTPROTECT_GUARD_BADGE_ALREADY')) . '</p>'
                . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_ALREADY_TEXT', $where) . '</p>'
                . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_GUARD_OPTIONAL_TITLE') . '</h4>';
        } elseif ($gsrc === 'unknown') {
            echo '<p>' . self::badge('info', htp_t('COM_HTPROTECT_GUARD_BADGE_UNKNOWN')) . '</p>'
                . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_UNKNOWN_TEXT') . '</p>';
        } else {
            echo '<p>' . self::badge('err', htp_t('COM_HTPROTECT_GUARD_BADGE_NONE')) . '</p>'
                . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_NONE_TEXT') . '</p>';
        }

        if (!$guard['writable']) {
            echo '<div class="htpx-msg htpx-msg-warn">' . htp_t('COM_HTPROTECT_GUARD_NOTWRITABLE') . '</div>';
        }

        self::guardForm($cfg, $guard, 'guard');

        // Option 2 (Alternative): Backend-Zugang per geheimem Zugangs-String (Cookie-Gate).
        self::guardStringForm($cfg, $guard);

        echo '</div>';

        // "Weitere Zugaenge" (weitere .htpasswd-Nutzer) wird jetzt AUSKLAPPBAR direkt im Backend-
        // Zugang gerendert (guardUsersBlock, aufgeklappt vom grauen Link neben dem Aktions-Button) -
        // keine eigene Card mehr.

        // ── Notfall-Modus: komplette Seite sperren (eigenes Cookie-Gate, UNABHAENGIG vom Backend-Schutz) ──
        $emergency = !empty($cfg['emergency_lock']);
        $emKey     = isset($cfg['emergency_key']) ? (string) $cfg['emergency_key'] : '';
        $emBase    = function_exists('htp_trusted_base') ? rtrim((string) htp_trusted_base(isset($cfg['unsub_base']) ? $cfg['unsub_base'] : ''), '/') : '';
        $emLink    = ($emBase !== '' && $emKey !== '') ? $emBase . '/?htp_' . $emKey : '';

        echo '<div id="htp-emergency" class="htpx-card htpx-card-wide' . ($emergency ? ' htpx-card-danger' : '') . '">'
            . '<h4>' . htp_t('COM_HTPROTECT_EMERGENCY_CARD_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_EMERGENCY_INTRO') . '</p>';

        if ($emergency) {
            echo '<p>' . self::badge('err', htp_t('COM_HTPROTECT_EMERGENCY_BADGE_ON')) . '</p>';

            // Geheimer Zugangs-Link (setzt beim Aufruf das Cookie) + Kopier-Knopf.
            if ($emLink !== '') {
                echo '<div class="htpx-uninst-note">'
                    . htp_t('COM_HTPROTECT_EMERGENCY_LINK_LABEL')
                    . '<p class="htpx-inline" style="margin:.6rem 0 0"><code class="htpx-mono" id="htpx-emlink">' . htp_esc($emLink) . '</code> '
                    . '<button type="button" class="htpx-btn2 htpx-copy" data-ok="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_PW_COPIED')) . '" onclick="return htpxCopy(\'htpx-emlink\', this)">' . htp_t('COM_HTPROTECT_COPY') . '</button> '
                    . '<a class="htpx-btn2" href="' . htp_esc($emLink) . '" target="_blank" rel="noopener">' . htp_t('COM_HTPROTECT_EMERGENCY_OPEN') . '</a></p>'
                    . '</div>'
                    . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_EMERGENCY_THISBROWSER') . ' ' . htp_t('COM_HTPROTECT_EMERGENCY_LINK_HINT') . '</p>';
            }

            echo '<div class="htpx-inline">'
                . '<form method="post" action="' . self::url('guard') . '#htp-emergency" class="htpx-form" '
                . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_EMERGENCY_REKEY_CONFIRM') . '\');">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="emergency_rekey">'
                . '<button type="submit" class="htpx-btn2">' . htp_t('COM_HTPROTECT_EMERGENCY_BTN_REKEY') . '</button>'
                . '</form>'
                . '<form method="post" action="' . self::url('guard') . '" class="htpx-form">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="emergency_off">'
                . '<button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_EMERGENCY_BTN_OFF') . '</button>'
                . '</form>'
                . '</div>';
        } else {
            echo '<form method="post" action="' . self::url('guard') . '#htp-emergency" '
                . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_EMERGENCY_CONFIRM') . '\');">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="emergency_on">'
                . '<button type="submit" class="htpx-btn htpx-btn-danger">' . htp_t('COM_HTPROTECT_EMERGENCY_BTN_ON') . '</button>'
                . '</form>';
        }

        // ── Wartungsseite anpassen: IMMER verfuegbar (auch vor Aktivierung), einklappbar. ──
        // Zu, solange der Notfall-Modus aus ist; automatisch offen, sobald er aktiv ist.
        $emTitle = isset($cfg['emergency_title']) ? (string) $cfg['emergency_title'] : '';
        $emText  = isset($cfg['emergency_text']) ? (string) $cfg['emergency_text'] : '';
        // Standardtext vorbelegen, solange noch NICHTS Eigenes gespeichert ist (kein leeres Feld).
        // Beim Speichern legt ein UNVERAENDERTER Standard wieder "leer" ab (HtpApp::taskEmergency-
        // PageSave) -> die Wartungsseite bleibt dann automatisch zweisprachig (de/en).
        $emPrefill  = ($emTitle === '' && $emText === '');
        $emTitleVal = ($emTitle !== '') ? $emTitle : ($emPrefill ? htp_t('COM_HTPROTECT_MAINT_TITLE') : '');
        $emTextVal  = ($emText !== '')  ? $emText  : ($emPrefill ? '<p>' . htp_t('COM_HTPROTECT_MAINT_BODY') . '</p>' : '');
        echo '<details class="htpx-details" style="margin-top:26px"' . ($emergency ? ' open' : '') . '>'
            . '<summary>' . htp_t('COM_HTPROTECT_EMERGENCY_PAGE_TITLE') . '</summary>'
            . '<p class="htpx-dim" style="margin-top:.7rem">' . htp_t('COM_HTPROTECT_EMERGENCY_PAGE_HINT') . '</p>'
            . '<form method="post" action="' . self::url('guard') . '#htp-emergency" class="htpx-form">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="emergency_page_save">'
            . '<label class="htpx-label" for="htp_em_title">' . htp_t('COM_HTPROTECT_EMERGENCY_PAGE_HEADING') . '</label>'
            . '<input class="htpx-input" type="text" id="htp_em_title" name="htp_em_title" maxlength="200" placeholder="' . htp_esc(htp_t('COM_HTPROTECT_MAINT_TITLE')) . '" value="' . htp_esc($emTitleVal) . '">'
            . '<label class="htpx-label" for="htp_em_text" style="display:block;margin-top:.8rem">' . htp_t('COM_HTPROTECT_EMERGENCY_PAGE_BODY') . '</label>'
            . self::wysiwyg('htp_em_text', $emTextVal, 294)
            . '<p style="margin-top:.9rem"><button type="submit" class="htpx-btn2">' . htp_t('COM_HTPROTECT_EMERGENCY_PAGE_SAVE') . '</button></p>'
            . '</form>'
            . '</details>';

        echo '</div>';

        if ($guard['active']) {
            echo '<div class="htpx-card htpx-card-wide htpx-card-danger"><h4>' . htp_t('COM_HTPROTECT_GUARD_REMOVE_TITLE') . '</h4>'
                . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_REMOVE_TEXT') . '</p>'
                . '<form method="post" action="' . self::url('guard') . '" onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_GUARD_REMOVE_CONFIRM') . '\');">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="guard_remove">'
                . '<button type="submit" class="htpx-btn htpx-btn-danger">' . htp_t('COM_HTPROTECT_GUARD_REMOVE_TITLE') . '</button>'
                . '</form></div>';
        }
    }

    /**
     * Option 2 (Alternative zum Passwortschutz): Backend-Zugang per geheimem Zugangs-String
     * (Cookie-Gate) - ausklappbar, schlank, im Standalone-Backend OHNE Dashboard bedienbar.
     * /administrator/?<String> setzt ein Cookie und schaltet frei; ohne gueltiges Cookie 404.
     * Kein Passwort-Prompt, keine .htpasswd. htp_tc(): rendert immer (deutscher Fallback).
     */
    private static function guardStringForm($cfg, $guard)
    {
        $gateActive = (isset($guard['mode']) && $guard['mode'] === 'string');
        $cur        = isset($cfg['guard_string']) ? (string) $cfg['guard_string'] : '';

        echo '<details class="htpx-details" style="margin-top:20px"' . ($gateActive ? ' open' : '') . '>'
            . '<summary>' . htp_tc('COM_HTPROTECT_GUARD_GATE_TITLE', 'Alternative: Zugangs-String statt Passwort') . '</summary>'
            . '<p class="htpx-dim" style="margin-top:.7rem">' . htp_tc('COM_HTPROTECT_GUARD_GATE_INTRO', 'Statt Passwortabfrage schaltet ein geheimer Link das Backend frei: /administrator?meincustomstring setzt ein Cookie, danach ist der Admin im selben Browser normal erreichbar. Ohne gueltiges Cookie erscheint das Backend als nicht vorhanden (404). Der Aufruf funktioniert auch aus dem Dashboard-1-Klick-Login.') . '</p>';

        if ($gateActive && $cur !== '') {
            echo '<p>' . self::badge('ok', htp_tc('COM_HTPROTECT_GUARD_GATE_BADGE_ACTIVE', 'Zugangs-String aktiv')) . '</p>'
                . '<p class="htpx-dim">' . htp_tc('COM_HTPROTECT_GUARD_GATE_URL_LABEL', 'Freischalten ueber:') . ' <code>/administrator?' . htp_esc($cur) . '</code></p>';
        }

        echo '<form method="post" action="' . self::url('guard') . '" autocomplete="off" class="htpx-form">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="guard_string_save">'
            . '<label class="htpx-label" for="htp_guard_string">' . htp_tc('COM_HTPROTECT_GUARD_GATE_FIELD', 'Zugangs-String') . '</label>'
            . '<input class="htpx-input mono" type="text" id="htp_guard_string" name="htp_guard_string" required pattern="\x3f?[A-Za-z0-9_-]{5,64}" minlength="5" maxlength="65" '
            . 'value="' . htp_esc($cur !== '' ? '?' . $cur : '') . '" placeholder="?meincustomstring" '
            . 'onfocus="if(!this.value)this.value=\'?\';" '
            . 'oninput="var q=\'?\';var v=this.value.charAt(0)===q?this.value:q+this.value;var e=document.getElementById(\'htp_gate_prev\');if(e)e.textContent=v.length>1?(\'/administrator\'+v):(\'/administrator?...\');" '
            . 'title="' . htp_esc(html_entity_decode(htp_tc('COM_HTPROTECT_GUARD_GATE_FIELD_TITLE', 'Mindestens 5 Zeichen (inkl. fuehrendem Fragezeichen); nur Buchstaben, Ziffern, _ und -'), ENT_QUOTES, 'UTF-8')) . '" '
            . 'autocomplete="off" data-lpignore="true" data-1p-ignore data-bwignore data-form-type="other" spellcheck="false">'
            . '<p class="htpx-dim" style="margin-top:.6rem">' . htp_tc('COM_HTPROTECT_GUARD_GATE_URL_LABEL', 'Freischalten ueber:') . ' <code id="htp_gate_prev">/administrator' . ($cur !== '' ? htp_esc('?' . $cur) : '?...') . '</code></p>'
            . '<p class="htpx-dim" style="margin-top:.4rem">' . htp_tc('COM_HTPROTECT_GUARD_GATE_HINT', 'Frei waehlbar (mind. 5 Zeichen). Merke ihn dir gut - der Aufruf oben schaltet dich frei. Zum Abschalten oben "Schutz entfernen".') . '</p>'
            . '<p style="margin-top:.8rem"><button type="submit" class="htpx-btn2">' . ($gateActive ? htp_tc('COM_HTPROTECT_GUARD_GATE_UPDATE', 'Zugangs-String aktualisieren') : htp_tc('COM_HTPROTECT_GUARD_GATE_ACTIVATE', 'Zugangs-String aktivieren')) . '</button></p>'
            . '</form>'
            . '</details>';
    }

    private static function guardForm($cfg, $guard, $backTo)
    {
        // "Weitere Zugaenge" nur, wenn HTProtect die Passwortdatei SELBST fuehrt (aktiv + eigene
        // administrator/.htpasswd). Dann steht der ausklappende Link rechts neben dem Aktions-Button
        // und der Bereich (Liste + Hinzufuegen) erscheint darunter - KEINE eigene Card mehr.
        $own = !empty($guard['active']) && class_exists('HtpGuard') && HtpGuard::isOwnActive();

        echo '<form method="post" action="' . self::url($backTo) . '" autocomplete="off" class="htpx-form">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="guard_save">'
            . '<div class="htpx-grid2">'
            . '<div><label class="htpx-label" for="htp_guard_user">' . htp_t('COM_HTPROTECT_GUARD_FIELD_USER') . '</label>'
            . '<input class="htpx-input" type="text" id="htp_guard_user" name="htp_guard_user" required pattern="[a-zA-Z0-9._@\-]+" minlength="2" '
            . 'title="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_FIELD_USER_TITLE')) . '" '
            . 'autocomplete="off" data-lpignore="true" data-1p-ignore data-bwignore data-form-type="other"></div>'
            . '<div></div>'
            . '<div><label class="htpx-label" for="htp_guard_pass">' . htp_t('COM_HTPROTECT_GUARD_FIELD_PASS') . '</label>'
            . '<div class="htpx-pwwrap">'
            . '<input class="htpx-input" type="password" id="htp_guard_pass" name="htp_guard_pass" required minlength="8" '
            . 'title="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_FIELD_PASS_TITLE')) . '" '
            . 'autocomplete="new-password" data-lpignore="true" data-1p-ignore data-bwignore data-form-type="other">'
            // Auge-Knopf: Passwort zeigen/verbergen.
            . '<button type="button" class="htpx-pwbtn" id="htpx-pwshow" title="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_PW_SHOW')) . '" aria-label="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_PW_SHOW')) . '">'
            . '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M12 5c-5 0-9.3 3.1-11 7 1.7 3.9 6 7 11 7s9.3-3.1 11-7c-1.7-3.9-6-7-11-7zm0 12a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></svg>'
            . '</button></div></div>'
            . '<div><label class="htpx-label" for="htp_guard_pass2">' . htp_t('COM_HTPROTECT_GUARD_FIELD_PASS2') . '</label>'
            . '<div class="htpx-pwwrap">'
            . '<input class="htpx-input" type="password" id="htp_guard_pass2" name="htp_guard_pass2" required minlength="8" '
            . 'autocomplete="new-password" data-lpignore="true" data-1p-ignore data-bwignore data-form-type="other">'
            . '<button type="button" class="htpx-pwbtn" id="htpx-pwshow2" title="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_PW_SHOW')) . '" aria-label="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_PW_SHOW')) . '">'
            . '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M12 5c-5 0-9.3 3.1-11 7 1.7 3.9 6 7 11 7s9.3-3.1 11-7c-1.7-3.9-6-7-11-7zm0 12a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></svg>'
            . '</button></div></div>'
            // Generator-Leiste: sicheres Zufallspasswort (crypto.getRandomValues) +
            // 1-Klick-Kopieren. Fuellt BEIDE Felder, zeigt das Passwort kurz an.
            . '<div class="htpx-pwgen">'
            . '<button type="button" class="htpx-btn2 htpx-btn-s htpx-pwgen-btn" id="htpx-pwgen">'
            . '<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM8 17a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0-8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm4 4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm4 4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0-8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/></svg>'
            . ' ' . htp_t('COM_HTPROTECT_GUARD_PW_GEN') . '</button>'
            . '<button type="button" class="htpx-btn2 htpx-btn-s htpx-pwcopy-btn" id="htpx-pwcopy" disabled data-ok="' . htp_esc(htp_t('COM_HTPROTECT_GUARD_PW_COPIED')) . '">'
            . '<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M16 1H4a2 2 0 0 0-2 2v14h2V3h12V1zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H8V7h11v14z"/></svg>'
            . ' <span id="htpx-pwcopy-txt">' . htp_t('COM_HTPROTECT_GUARD_PW_COPY') . '</span></button>'
            . '<span class="htpx-pwgen-hint htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_PW_GEN_HINT') . '</span>'
            . '</div>'
            . '</div>'
            . '<details class="htpx-details"><summary class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_FIELD_PATH_SUMMARY') . '</summary>'
            . '<div style="margin-top:8px"><label class="htpx-label" for="htp_guard_dir">' . htp_t('COM_HTPROTECT_GUARD_FIELD_PATH_LABEL') . '</label>'
            . '<input class="htpx-input htpx-mono" type="text" id="htp_guard_dir" name="htp_guard_dir" value="' . htp_esc(HtpGuard::effectiveManualDir(isset($cfg['guard_dir']) ? $cfg['guard_dir'] : '')) . '" placeholder="' . htp_esc(htp_t_attr('COM_HTPROTECT_GUARD_FIELD_PATH_PLACEHOLDER')) . '">'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_FIELD_PATH_HINT') . '</p></div>'
            . '</details>'
            . '<p class="htpx-appcta"><button type="submit" class="htpx-btn">' . ((!empty($guard['active']) && isset($guard['mode']) && $guard['mode'] === 'password') ? htp_t('COM_HTPROTECT_GUARD_RENEW_TITLE') : htp_t('COM_HTPROTECT_GUARD_ACTIVATE_BTN')) . '</button>'
            // Grauer Umschalt-Link rechts neben dem Button (mobil eigene Zeile) - identisch zum
            // Hilfe-Link "Wo verbinde ich diese Seite?" (htpx-appcta/htpx-appwhere, s. dortige CSS).
            . ($own ? ' <a class="htpx-appwhere" href="#htp-guardmore" role="button" aria-expanded="false" aria-controls="htp-guardmore" onclick="var e=document.getElementById(\'htp-guardmore\');if(e){e.hidden=!e.hidden;this.setAttribute(\'aria-expanded\',String(!e.hidden));}return false;">' . htp_t('COM_HTPROTECT_GUARD_USERS_TITLE') . '</a>' : '')
            . '</p>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_TIP') . '</p>'
            . '</form>';

        // Ausklappbarer "Weitere Zugaenge"-Bereich direkt unter dem Erneuern-Formular (kein eigenes
        // Card), gesteuert vom Link oben. Nur wenn HTProtect die Passwortdatei selbst fuehrt.
        if ($own) {
            self::guardUsersBlock($guard);
        }

        // Ganz unten im Backend-Zugang: die "Wer sieht/bedient HTProtect"-Steuerung.
        // AUSSCHLIESSLICH fuer Super-User - normale Admins (bei ausgeschaltetem su_only)
        // bedienen HTProtect zwar, duerfen aber weder den Sichtbarkeits-Schalter noch die
        // Verbergen-Liste aendern (sonst koennte ein Admin die Komponente vor Super-Usern
        // verstecken oder sich dauerhaft sichern). Fuer Nicht-Super-User gar nicht ausgeben.
        $me      = function_exists('htp_user') ? htp_user() : null;
        $isSuper = ($me && method_exists($me, 'authorise') && $me->authorise('core.admin', 'com_htprotect'));

        if ($isSuper) {
            // (1) Grobschalter: nur Super-User vs. auch normale Admins (Sichtbarkeit + Bedienung).
            echo self::suOnlyPanel($cfg);

            // (2) Feinsteuerung: HTProtect vor einzelnen Nutzern verbergen (fuer Agenturen).
            //     Der eigene Super-User ist nie abwaehlbar (keine Selbst-Aussperrung).
            echo self::stealthPanel($cfg);
        }
    }

    /**
     * Ausklappbarer "Weitere Zugaenge"-Bereich (weitere .htpasswd-Nutzer): Liste + Hinzufuegen.
     * KEINE eigene Card - eingebettet unter dem Erneuern-Formular, standardmaessig eingeklappt
     * (hidden), aufgeklappt vom grauen Link neben dem Aktions-Button. Nur aufrufen, wenn HTProtect
     * die Passwortdatei selbst fuehrt (Aufrufer prueft $own via HtpGuard::isOwnActive).
     *
     * @param   array  $guard
     *
     * @return  void
     */
    private static function guardUsersBlock($guard)
    {
        $list  = HtpGuard::listUsers();
        $multi = count($list) > 1;

        echo '<div id="htp-guardmore" class="htpx-guardmore" hidden>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_GUARD_USERS_INTRO') . '</p>';

        echo '<ul style="list-style:none;margin:0 0 14px;padding:0">';

        foreach ($list as $name) {
            echo '<li style="display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px solid var(--line,rgba(128,128,128,.18))">'
                . '<strong>' . htp_esc($name) . '</strong>';

            if ($multi) {
                // Entfernen am LETZTEN Login ausgeblendet -> leert die Datei nie
                // (die Server-Sperre in removeUser() bleibt zusaetzlich bestehen).
                echo '<form method="post" action="' . self::url('guard') . '" style="margin:0 0 0 auto" '
                    . 'onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_GUARD_USERS_REMOVE_CONFIRM') . '\');">'
                    . self::tokenField()
                    . '<input type="hidden" name="htp_task" value="guard_user_remove">'
                    . '<input type="hidden" name="htp_guard_ruser" value="' . htp_esc($name) . '">'
                    . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_GUARD_USERS_REMOVE') . '</button>'
                    . '</form>';
            }

            echo '</li>';
        }

        echo '</ul>';

        if (empty($guard['writable'])) {
            echo '<div class="htpx-msg htpx-msg-warn">' . htp_t('COM_HTPROTECT_GUARD_NOTWRITABLE') . '</div>';
        } else {
            // Kompaktes Add-Formular: eigene Element-IDs (htp_gu_*), damit sie nicht mit dem
            // Erneuerungs-Formular (htp_guard_*) kollidieren; POST-Namen bleiben htp_guard_user/_pass/_pass2.
            echo '<form method="post" action="' . self::url('guard') . '" autocomplete="off" class="htpx-form">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="guard_user_add">'
                . '<div class="htpx-grid2">'
                .   '<div><label class="htpx-label" for="htp_gu_user">' . htp_t('COM_HTPROTECT_GUARD_FIELD_USER') . '</label>'
                .     '<input class="htpx-input" type="text" id="htp_gu_user" name="htp_guard_user" required pattern="[a-zA-Z0-9._@\-]+" minlength="2" autocomplete="off" data-lpignore="true" data-1p-ignore data-bwignore data-form-type="other"></div>'
                .   '<div></div>'
                .   '<div><label class="htpx-label" for="htp_gu_pass">' . htp_t('COM_HTPROTECT_GUARD_FIELD_PASS') . '</label>'
                .     '<input class="htpx-input" type="password" id="htp_gu_pass" name="htp_guard_pass" required minlength="8" autocomplete="new-password" data-lpignore="true" data-1p-ignore data-bwignore data-form-type="other"></div>'
                .   '<div><label class="htpx-label" for="htp_gu_pass2">' . htp_t('COM_HTPROTECT_GUARD_FIELD_PASS2') . '</label>'
                .     '<input class="htpx-input" type="password" id="htp_gu_pass2" name="htp_guard_pass2" required minlength="8" autocomplete="new-password" data-lpignore="true" data-1p-ignore data-bwignore data-form-type="other"></div>'
                . '</div>'
                // Abstand zum Formular, damit der Button nicht am letzten Feld klebt.
                . '<p style="margin-top:16px"><button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_GUARD_USERS_ADD_BTN') . '</button></p>'
                . '</form>';
        }

        echo '</div>';
    }

    /**
     * "Nur fuer Super-User"-Schalter (su_only). Grobsteuerung fuer Sichtbarkeit UND Bedienung:
     * an (Standard) = HTProtect ist fuer normale Administratoren komplett ausgeblendet und
     * gesperrt; aus = auch normale Admins sehen und bedienen es. Wird nur Super-Usern gezeigt
     * (der Aufrufer prueft das). Rein textbasiert, laienverstaendlich.
     *
     * @return string
     */
    private static function suOnlyPanel($cfg)
    {
        $on = !isset($cfg['su_only']) || !empty($cfg['su_only']);

        $html  = '<details class="htpx-details htpx-card htpx-card-wide" id="htp-suonly" style="margin-top:14px"' . ($on ? '' : ' open') . '>';
        $html .= '<summary class="htpx-dim">' . htp_t('COM_HTPROTECT_SUONLY_SUMMARY') . '</summary>';
        $html .= '<div style="margin-top:10px">';
        $html .= '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SUONLY_INTRO') . '</p>';
        $html .= '<form method="post" action="' . self::url('guard') . '" class="htpx-form">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="su_only_save">';
        $html .= '<label class="htpx-tg" style="margin:.3rem 0">'
            . '<input type="checkbox" name="su_only" value="1"' . ($on ? ' checked' : '') . '>'
            . '<span class="htpx-tgsl"></span>'
            . '<span class="htpx-tgtext">' . htp_t('COM_HTPROTECT_SUONLY_TOGGLE') . '</span>'
            . '</label>';
        $html .= '<p class="htpx-dim" style="font-size:.85rem">' . htp_t('COM_HTPROTECT_SUONLY_HINT') . '</p>';
        $html .= '<p><button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_SUONLY_SAVE') . '</button></p>';
        $html .= '</form></div></details>';

        return $html;
    }

    /**
     * Stealth-Panel: Checkboxliste aller Super-User (angehakt = sieht HTProtect).
     * Der aktuelle Super-User ist fixiert (disabled + angehakt). Rein kosmetisch/Sicht -
     * der Schutz laeuft fuer alle weiter.
     *
     * @return string
     */
    private static function stealthPanel($cfg)
    {
        // Kandidaten = immer die Super-User; bei ausgeschaltetem "Nur fuer Super-User"
        // zusaetzlich die normalen Admins (die es dann ohnehin sehen/bedienen).
        $cands  = class_exists('HtpApp') ? HtpApp::stealthCandidates($cfg) : array();
        $hidden = (isset($cfg['hidden_su']) && is_array($cfg['hidden_su'])) ? array_map('strval', $cfg['hidden_su']) : array();
        $me     = function_exists('htp_user') ? htp_user() : null;
        $meId   = ($me && isset($me->id)) ? (int) $me->id : 0;

        $html  = '<details class="htpx-details htpx-card htpx-card-wide" id="htp-stealth" style="margin-top:14px">';
        $html .= '<summary class="htpx-dim">' . htp_t('COM_HTPROTECT_STEALTH_SUMMARY') . '</summary>';
        $html .= '<div style="margin-top:10px">';
        $html .= '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_STEALTH_INTRO') . '</p>';

        if (empty($cands)) {
            $html .= '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_STEALTH_NONE') . '</p></div></details>';

            return $html;
        }

        // Nach Rolle gruppieren: Super-User zuerst, dann (falls vorhanden) normale Admins.
        $groups = array(
            1 => array('label' => htp_t('COM_HTPROTECT_STEALTH_GROUP_SUPER'), 'items' => array()),
            0 => array('label' => htp_t('COM_HTPROTECT_STEALTH_GROUP_ADMIN'), 'items' => array()),
        );

        foreach ($cands as $u) {
            $g = empty($u['super']) ? 0 : 1;
            $groups[$g]['items'][] = $u;
        }

        // Ueberschriften nur zeigen, wenn es wirklich beide Rollen gibt (sonst unveraendert schlicht).
        $showHeadings = !empty($groups[0]['items']);

        $html .= '<form method="post" action="' . self::url('guard') . '" class="htpx-form">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="stealth_save">';

        foreach (array(1, 0) as $g) {
            if (empty($groups[$g]['items'])) {
                continue;
            }

            if ($showHeadings) {
                $html .= '<p class="htpx-dim" style="margin:.7rem 0 .2rem;font-weight:600">' . $groups[$g]['label'] . '</p>';
            }

            foreach ($groups[$g]['items'] as $u) {
                $uid     = (int) $u['id'];
                $isMe    = ($uid === $meId);
                $visible = $isMe ? true : !in_array((string) $uid, $hidden, true);
                $display = $u['name'] !== '' ? $u['name'] : $u['username'];

                $label = htp_esc($display) . ' <span class="htpx-dim">(' . htp_esc($u['username']) . ')</span>';

                if (!empty($u['block'])) {
                    $label .= ' ' . self::badge('warn', htp_t('COM_HTPROTECT_STEALTH_BLOCKED'));
                }

                if ($isMe) {
                    $label .= ' ' . self::badge('info', htp_t('COM_HTPROTECT_STEALTH_YOU'));
                }

                $html .= '<label class="htpx-tg" style="margin:.3rem 0">'
                    . '<input type="checkbox" name="stealth_vis[]" value="' . $uid . '"'
                    . ($visible ? ' checked' : '') . ($isMe ? ' disabled' : '') . '>'
                    . '<span class="htpx-tgsl"></span>'
                    . '<span class="htpx-tgtext">' . $label . '</span>'
                    . '</label>';
            }
        }

        $html .= '<p class="htpx-dim" style="font-size:.85rem">' . htp_t('COM_HTPROTECT_STEALTH_HINT') . '</p>';
        $html .= '<p><button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_STEALTH_SAVE') . '</button></p>';
        $html .= '</form></div></details>';

        return $html;
    }

    // ─────────────────────────────────────────────────────────────────
    //  Bereich: Schutzschild (.htaccess)
    // ─────────────────────────────────────────────────────────────────

    private static function secShield($cfg, $data)
    {
        $st = $data['status'];

        echo '<div class="htpx-titlerow" style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;justify-content:space-between;margin:4px 0 16px">'
            . '<h2 class="htpx-h" style="margin:0">' . htp_t('COM_HTPROTECT_SHIELD_TITLE') . '</h2>'
            . '<a class="htpx-btn2 htpx-btn-s" href="' . self::url('backups') . '#htp-htbackups">&#128190; '
            . htp_t('COM_HTPROTECT_SHIELD_TO_BACKUPS') . '</a>'
            . '</div>';

        if (!empty($data['pending'])) {
            echo '<div class="htpx-msg htpx-msg-warn">' . htp_t('COM_HTPROTECT_SHIELD_PENDING') . '</div>';
        }

        if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
            echo '<div class="htpx-msg htpx-msg-info">' . htp_t('COM_HTPROTECT_SHIELD_PROXY') . '</div>';
        }

        // Maszgeblich ist der aktive .htaccess-Funktionstest (nicht allein
        // SERVER_SOFTWARE): Apache-hinter-nginx (Plesk-Proxy) wird so korrekt
        // als Apache behandelt - das .htaccess-Schild greift dort sehr wohl.
        $isNginx = !empty($data['nginx_mode']);
        $isIis   = !empty($data['iis_mode']);

        if ($isNginx) {
            echo '<div class="htpx-msg htpx-msg-info">' . htp_t('COM_HTPROTECT_SHIELD_NGINX_INFO', htp_esc(htp_server_software())) . '</div>';
        } elseif ($isIis) {
            echo '<div class="htpx-msg htpx-msg-info">' . htp_t('COM_HTPROTECT_SHIELD_IIS_INFO', htp_esc(htp_server_software())) . '</div>';
        }

        // Selbsttest-Ergebnis direkt nach dem Schreiben
        if (isset($data['selftest']) && is_array($data['selftest'])) {
            self::renderSelfTest($data['selftest']);
        }

        // Selbsttest-Ergebnis des Nginx-Schilds
        if (isset($data['nginx_test']) && is_array($data['nginx_test'])) {
            self::renderSelfTest($data['nginx_test']);
        }

        // Bestehende Datei: Analyse/Uebernahme NUR anbieten, wenn es auch
        // wirklich etwas zu uebernehmen gibt. Sonst stuende hier ein Button,
        // der beim Klick nur "nichts zu importieren" meldet (bei voll von
        // HTProtect verwalteten Dateien ist die Liste leer) -> Karte ausblenden.
        if ($st['exists'] && !$isNginx && !$isIis) {
            $importSrc  = isset($data['current']) ? (string) $data['current'] : HtpStorage::read();
            $importable = $importSrc !== '' ? HtpBuilder::extractCustomRules($importSrc) : '';

            if ($importable !== '') {
                echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_SHIELD_IMPORT_TITLE') . '</h4>'
                    . '<p class="htpx-dim">' . htp_t($st['ours'] ? 'COM_HTPROTECT_SHIELD_IMPORT_OURS' : 'COM_HTPROTECT_SHIELD_IMPORT_NOTOURS') . '</p>'
                    . '<form method="post" action="' . self::url('shield') . '">'
                    . self::tokenField()
                    . '<input type="hidden" name="htp_task" value="import_custom">'
                    . '<button type="submit" class="htpx-btn2">' . htp_t('COM_HTPROTECT_SHIELD_IMPORT_BTN') . '</button>'
                    . '</form></div>';
            }
        }

        echo '<form method="post" action="' . self::url('shield') . '" class="htpx-form">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="shield_save">';

        // ── Wichtigste Einstellungen (immer sichtbar) ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_IMPORTANT'));
        echo self::toggle('htp_x_shield', $cfg['x_shield'], htp_t('COM_HTPROTECT_TG_XSHIELD'), htp_t('COM_HTPROTECT_TG_XSHIELD_HINT'), 'htp-a-exploit');
        echo self::toggle('htp_shield_autoheal', !empty($cfg['shield_autoheal']), htp_t('COM_HTPROTECT_TG_AUTOHEAL'), htp_t('COM_HTPROTECT_TG_AUTOHEAL_HINT'), 'htp-a-autoheal');
        echo self::toggle('htp_u_harden', $cfg['u_harden'], htp_t('COM_HTPROTECT_TG_UHARDEN'), htp_t('COM_HTPROTECT_TG_UHARDEN_HINT'), 'htp-a-upload');
        echo self::toggle('htp_force_https', $cfg['force_https'], htp_t('COM_HTPROTECT_TG_HTTPS'), htp_t('COM_HTPROTECT_TG_HTTPS_HINT'), 'htp-a-https');
        echo '<div class="htpx-grid2"><div>'
            . '<label class="htpx-label" for="htp_www_mode">' . htp_t('COM_HTPROTECT_LBL_WWWMODE') . '</label>'
            . '<select class="htpx-input" id="htp_www_mode" name="htp_www_mode">'
            . '<option value="0"' . ((int) $cfg['www_mode'] === 0 ? ' selected' : '') . '>' . htp_t('COM_HTPROTECT_WWW_KEEP') . '</option>'
            . '<option value="1"' . ((int) $cfg['www_mode'] === 1 ? ' selected' : '') . '>' . htp_t('COM_HTPROTECT_WWW_WWW') . '</option>'
            . '<option value="2"' . ((int) $cfg['www_mode'] === 2 ? ' selected' : '') . '>' . htp_t('COM_HTPROTECT_WWW_NOWWW') . '</option>'
            // Hinweis-Spalte unten ausrichten (auf Dropdown-Hoehe), statt oben auf
            // Label-Hoehe zu kleben -> align-self:end nur an DIESEM Grid-Item.
            . '</select></div><div style="align-self:end">'
            // Auf einer Subdomain ist die www-Erzwingung sinnlos/gefaehrlich
            // (www.<subdomain> existiert i. d. R. nicht) -> weicher Hinweis,
            // KEINE Sperre (Heuristik koennte irren -> nichts kaputtmachen).
            . (htp_current_is_subdomain()
                ? '<p class="htpx-dim htpx-wwwsub" style="margin:0">&#9888; ' . htp_t('COM_HTPROTECT_WWW_SUBDOMAIN_WARN') . '</p>'
                : '')
            . '</div></div>';
        self::groupClose();

        // Alle weiteren Detail-Einstellungen sind eingeklappt - die meisten
        // Nutzer brauchen sie nie (&bdquo;Jetzt absichern&ldquo; erledigt das Wichtige).
        echo '<details class="htpx-details htpx-advanced"' . (isset($data['import_top']) ? ' open' : '') . '>'
            . '<summary>' . htp_t('COM_HTPROTECT_ADVANCED_SUMMARY') . '</summary>'
            . '<p class="htpx-dim" style="margin-top:8px">' . htp_t('COM_HTPROTECT_ADVANCED_INTRO') . '</p>';

        // ── Gruppe 1: Grundlagen ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_BASICS'));
        // Unterverzeichnis: AUTOMATISCH aus Joomlas Basis-URL erkannt (read-only).
        // Kein manueller Wert - detectSitePath() spiegelt Joomlas eigenen Routing-
        // Basispfad, ein Override liefe nur gegen Joomlas Selbstverstaendnis.
        $detectedPath = HtpConfig::detectSitePath();
        $sitePathShow = $detectedPath === '' ? htp_t('COM_HTPROTECT_LBL_SITEPATH_ROOT') : $detectedPath;
        echo '<div class="htpx-grid2">'
            . '<div><label class="htpx-label" for="htp_site_path">' . htp_t('COM_HTPROTECT_LBL_SITEPATH') . '</label>'
            . '<input class="htpx-input htpx-input-ro" type="text" id="htp_site_path" value="' . htp_esc($sitePathShow) . '" readonly>'
            . '<p class="htpx-dim" style="margin:4px 0 0">' . htp_t('COM_HTPROTECT_LBL_SITEPATH_HINT') . '</p></div>'
            . '<div></div></div>';
        echo self::toggle('htp_sef_rules', $cfg['sef_rules'], htp_t('COM_HTPROTECT_TG_SEF'), htp_t('COM_HTPROTECT_TG_SEF_HINT'));
        echo '<p class="htpx-dim" style="margin:6px 0 0">' . htp_t('COM_HTPROTECT_SHIELD_API_NOTE') . '</p>';
        echo self::toggle('htp_fastcgi_auth', $cfg['fastcgi_auth'], htp_t('COM_HTPROTECT_TG_FASTCGI'), htp_t('COM_HTPROTECT_TG_FASTCGI_HINT'));
        echo self::toggle('htp_apache_options', $cfg['apache_options'], htp_t('COM_HTPROTECT_TG_APACHEOPT'), htp_t('COM_HTPROTECT_TG_APACHEOPT_HINT'));
        echo self::toggle('htp_follow_symlink', $cfg['follow_symlink'], htp_t('COM_HTPROTECT_TG_SYMLINK'), htp_t('COM_HTPROTECT_TG_SYMLINK_HINT'));
        echo self::toggle('htp_autosecure', !empty($cfg['autosecure']), htp_t('COM_HTPROTECT_TG_AUTOSECURE'), htp_t('COM_HTPROTECT_TG_AUTOSECURE_HINT'));
        self::groupClose();

        // ── Gruppe 2: Kanonische Adresse ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_CANONICAL'));
        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_CANONICAL_NOTE') . '</p>';
        echo '<div class="htpx-grid2"><div>'
            . '<label class="htpx-label" for="htp_canonical_host">' . htp_t('COM_HTPROTECT_LBL_CANONHOST') . '</label>'
            . '<input class="htpx-input" type="text" id="htp_canonical_host" name="htp_canonical_host" value="' . htp_esc($cfg['canonical_host']) . '" placeholder="' . htp_esc((string) parse_url(htp_site_url(), PHP_URL_HOST)) . '"></div><div></div></div>';
        self::groupClose();

        // ── Gruppe 3: Antwort-Header ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_HEADERS'));
        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_HEADERS_NOTE') . '</p>';
        echo self::toggle('htp_h_frame', $cfg['h_frame'], htp_t('COM_HTPROTECT_TG_HFRAME'), htp_t('COM_HTPROTECT_TG_HFRAME_HINT'));
        echo self::toggle('htp_h_nosniff', $cfg['h_nosniff'], htp_t('COM_HTPROTECT_TG_HNOSNIFF'), htp_t('COM_HTPROTECT_TG_HNOSNIFF_HINT'));
        echo '<div class="htpx-grid2"><div>'
            . '<label class="htpx-label" for="htp_h_referrer">' . htp_t('COM_HTPROTECT_LBL_REFERRER') . '</label>'
            . '<select class="htpx-input" id="htp_h_referrer" name="htp_h_referrer">';

        $referrerOptions = array(
            ''                                => htp_t('COM_HTPROTECT_REFERRER_NONE'),
            'no-referrer'                     => htp_t('COM_HTPROTECT_REFERRER_NOREF'),
            'same-origin'                     => 'same-origin',
            'strict-origin'                   => 'strict-origin',
            'strict-origin-when-cross-origin' => htp_t('COM_HTPROTECT_REFERRER_STRICTCROSS'),
            'no-referrer-when-downgrade'      => 'no-referrer-when-downgrade',
        );

        foreach ($referrerOptions as $value => $label) {
            echo '<option value="' . htp_esc($value) . '"' . ($cfg['h_referrer'] === $value ? ' selected' : '') . '>' . htp_esc($label) . '</option>';
        }

        echo '</select></div><div></div></div>';
        echo self::toggle('htp_h_poweredby', $cfg['h_poweredby'], htp_t('COM_HTPROTECT_TG_HPOWERED'));
        echo self::toggle('htp_h_hsts', $cfg['h_hsts'], htp_t('COM_HTPROTECT_TG_HSTS'), htp_t('COM_HTPROTECT_TG_HSTS_HINT'));
        echo self::toggle('htp_h_hsts_sub', $cfg['h_hsts_sub'], htp_t('COM_HTPROTECT_TG_HSTSSUB'), htp_t('COM_HTPROTECT_TG_HSTSSUB_HINT'));
        echo self::toggle('htp_h_coop', $cfg['h_coop'], htp_t('COM_HTPROTECT_TG_COOP'), htp_t('COM_HTPROTECT_TG_COOP_HINT'));
        echo self::toggle('htp_h_permissions', $cfg['h_permissions'], htp_t('COM_HTPROTECT_TG_PERM'), htp_t('COM_HTPROTECT_TG_PERM_HINT'));
        self::groupClose();

        // ── Gruppe 4: Anfrage-Filter ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_FILTERS'));
        echo self::toggle('htp_f_query', $cfg['f_query'], htp_t('COM_HTPROTECT_TG_FQUERY'), htp_t('COM_HTPROTECT_TG_FQUERY_HINT'));
        echo self::toggle('htp_f_rfi', $cfg['f_rfi'], htp_t('COM_HTPROTECT_TG_FRFI'), htp_t('COM_HTPROTECT_TG_FRFI_HINT'));
        echo self::toggle('htp_f_methods', $cfg['f_methods'], htp_t('COM_HTPROTECT_TG_FMETHODS'));
        echo self::toggle('htp_f_wpnoise', $cfg['f_wpnoise'], htp_t('COM_HTPROTECT_TG_FWPNOISE'), htp_t('COM_HTPROTECT_TG_FWPNOISE_HINT'));
        self::groupClose();

        // ── Gruppe 4b: Exploit-Schild ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_EXPLOIT'));
        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_EXPLOIT_INTRO') . '</p>';
        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_EXPLOIT_FINE') . '</p>';

        $apiMode = isset($cfg['api_mode']) ? $cfg['api_mode'] : 'route';
        echo '<div class="htpx-grid2"><div>'
            . '<label class="htpx-label" for="htp_api_mode">' . htp_t('COM_HTPROTECT_LBL_APIMODE') . '</label>'
            . '<select class="htpx-input" id="htp_api_mode" name="htp_api_mode">'
            . '<option value="route"' . ($apiMode === 'route' ? ' selected' : '') . '>' . htp_t('COM_HTPROTECT_API_ROUTE') . '</option>'
            . '<option value="block"' . ($apiMode === 'block' ? ' selected' : '') . '>' . htp_t('COM_HTPROTECT_API_BLOCK') . '</option>'
            . '<option value="off"' . ($apiMode === 'off' ? ' selected' : '') . '>' . htp_t('COM_HTPROTECT_API_OFF') . '</option>'
            . '</select></div><div></div></div>';
        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_EXPLOIT_CVE') . '</p>';

        if (class_exists('HtpDefs')) {
            $catalog = HtpDefs::catalog($cfg);
            echo '<details class="htpx-details"><summary>' . htp_t('COM_HTPROTECT_EXPLOIT_SIGS_SUMMARY', count($catalog)) . '</summary>'
                . '<div style="margin-top:8px">';

            foreach ($catalog as $sig) {
                $scope = (isset($sig['scope']) && $sig['scope'] === 'guest') ? 'guest' : 'all';
                $scopeBadge = $scope === 'guest'
                    ? self::badge('info', htp_t('COM_HTPROTECT_SCOPE_GUEST'))
                    : self::badge('ok', htp_t('COM_HTPROTECT_SCOPE_ALL'));
                echo '<label class="htpx-tg">'
                    . '<input type="checkbox" name="htp_sig[' . htp_esc($sig['id']) . ']" value="1"' . (!empty($sig['enabled']) ? ' checked' : '') . '>'
                    . '<span class="htpx-tgsl"></span>'
                    . '<span class="htpx-tgtext">' . htp_esc(HtpDefs::localized($sig, 'label')) . ' ' . $scopeBadge
                    . '<small>' . htp_esc(HtpDefs::localized($sig, 'desc')) . '</small></span></label>';
            }

            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SIGS_NOTE') . '</p>'
                . '</div></details>';
        }
        self::groupClose();

        // ── Gruppe 4c: Upload-Ordner-Haertung ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_UPLOAD'));
        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_UPLOAD_INTRO') . '</p>';
        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_UPLOAD_ONOFF') . '</p>';
        echo '<label class="htpx-label" for="htp_u_dirs">' . htp_t('COM_HTPROTECT_UPLOAD_DIRS_LABEL') . ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_PERLINE_ROOT') . '</span></label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_u_dirs" name="htp_u_dirs" rows="5">' . htp_esc(implode("\n", $cfg['u_dirs'])) . '</textarea>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_UPLOAD_DIRS_NOTE') . '</p>';

        // Dezenter Ein-Klick-Hinweis fuer Bestandssites: Joomla 5.3+ hat den
        // zusaetzlichen Medienordner /files. Nur anbieten, wenn die Haertung
        // aktiv ist, der Ordner existiert (>= 5.3) und noch nicht in der Liste
        // steht. Der Button reicht htp_files_add mit; taskShieldSave nimmt /files
        // auf, haertet es und zieht es ins Watch-Manifest (Mituberwachung).
        if (class_exists('HtpConfig') && !empty($cfg['u_harden'])
            && HtpConfig::filesDirEligible() && !in_array('files', $cfg['u_dirs'], true)) {
            echo '<div class="htpx-inline" style="margin-top:8px;flex-wrap:wrap">'
                . '<span class="htpx-dim">' . htp_t('COM_HTPROTECT_UPLOAD_FILES_HINT') . '</span>'
                . '<button type="submit" name="htp_files_add" value="1" class="htpx-btn2 htpx-btn-s">'
                . htp_t('COM_HTPROTECT_UPLOAD_FILES_BTN') . '</button>'
                . '</div>';
        }

        if (class_exists('HtpDirShield')) {
            $dstatus = HtpDirShield::status($cfg);

            if (!empty($dstatus)) {
                echo '<div class="htpx-dirstatus" id="htp-a-uploaddirs">';

                foreach ($dstatus as $dir => $state) {
                    $map = array(
                        'protected'   => array('ok', htp_t('COM_HTPROTECT_DIRSTATUS_PROTECTED')),
                        'foreign'     => array('ok', htp_t('COM_HTPROTECT_DIRSTATUS_FOREIGN')),
                        'unprotected' => array('warn', htp_t('COM_HTPROTECT_DIRSTATUS_UNPROTECTED')),
                        'missing'     => array('info', htp_t('COM_HTPROTECT_DIRSTATUS_MISSING')),
                        'conflict'    => array('info', htp_t('COM_HTPROTECT_DIRSTATUS_CONFLICT')),
                    );
                    $b = isset($map[$state]) ? $map[$state] : array('info', $state);
                    echo '<span class="htpx-dirbadge"><span class="htpx-mono">' . htp_esc($dir) . '</span> ' . self::badge($b[0], $b[1]) . '</span>';
                }

                echo '</div>';
            }
        }
        self::groupClose();

        // ── Gruppe 5: Dateischild ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_FILESHIELD'));
        echo self::toggle('htp_s_php', $cfg['s_php'], htp_t('COM_HTPROTECT_TG_SPHP'), htp_t('COM_HTPROTECT_TG_SPHP_HINT'));
        echo self::toggle('htp_s_types', $cfg['s_types'], htp_t('COM_HTPROTECT_TG_STYPES'), htp_t('COM_HTPROTECT_TG_STYPES_HINT'));

        echo '<label class="htpx-label" for="htp_s_ext">' . htp_t('COM_HTPROTECT_LBL_SEXT') . '</label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_s_ext" name="htp_s_ext" rows="3">' . htp_esc($cfg['s_ext']) . '</textarea>';

        echo '<div class="htpx-grid2">'
            . '<div><label class="htpx-label" for="htp_s_files">' . htp_t('COM_HTPROTECT_LBL_SFILES') . ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_SFILES_HINT') . '</span></label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_s_files" name="htp_s_files" rows="6" placeholder="' . htp_esc(htp_t_attr('COM_HTPROTECT_SFILES_PH')) . '">' . htp_esc(implode("\n", $cfg['s_files'])) . '</textarea>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SFILES_NOTE') . '</p></div>'
            . '<div><label class="htpx-label" for="htp_s_dirs_php">' . htp_t('COM_HTPROTECT_LBL_SDIRSPHP') . ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_PERLINE') . '</span></label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_s_dirs_php" name="htp_s_dirs_php" rows="6" placeholder="' . htp_esc(htp_t_attr('COM_HTPROTECT_SDIRSPHP_PH')) . '">' . htp_esc(implode("\n", $cfg['s_dirs_php'])) . '</textarea></div>'
            . '<div><label class="htpx-label" for="htp_s_dirs_static">' . htp_t('COM_HTPROTECT_LBL_SDIRSSTATIC') . ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_PERLINE') . '</span></label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_s_dirs_static" name="htp_s_dirs_static" rows="6" placeholder="' . htp_esc(htp_t_attr('COM_HTPROTECT_SDIRSSTATIC_PH')) . '">' . htp_esc(implode("\n", $cfg['s_dirs_static'])) . '</textarea></div>'
            . '<div><label class="htpx-label" for="htp_s_sysdirs_list">' . htp_t('COM_HTPROTECT_LBL_SSYSDIRS') . ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_PERLINE') . '</span></label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_s_sysdirs_list" name="htp_s_sysdirs_list" rows="6">' . htp_esc(implode("\n", $cfg['s_sysdirs_list'])) . '</textarea></div>'
            . '</div>';

        echo '<label class="htpx-label" for="htp_allow_paths">' . htp_t('COM_HTPROTECT_LBL_ALLOWPATHS') . ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_PERLINE') . '</span></label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_allow_paths" name="htp_allow_paths" rows="3" placeholder="' . htp_esc(htp_t_attr('COM_HTPROTECT_ALLOWPATHS_PH')) . '">' . htp_esc(implode("\n", $cfg['allow_paths'])) . '</textarea>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_ALLOWPATHS_NOTE') . '</p>';

        echo self::toggle('htp_s_sysdirs', $cfg['s_sysdirs'], htp_t('COM_HTPROTECT_TG_SSYSDIRS'), htp_t('COM_HTPROTECT_TG_SSYSDIRS_HINT'));
        echo self::toggle('htp_s_dotfiles', $cfg['s_dotfiles'], htp_t('COM_HTPROTECT_TG_SDOTFILES'));
        echo self::toggle('htp_s_wellknown', $cfg['s_wellknown'], htp_t('COM_HTPROTECT_TG_SWELLKNOWN'), htp_t('COM_HTPROTECT_TG_SWELLKNOWN_HINT'));
        echo self::toggle('htp_s_sensitive', $cfg['s_sensitive'], htp_t('COM_HTPROTECT_TG_SSENSITIVE'), htp_t('COM_HTPROTECT_TG_SSENSITIVE_HINT'));
        echo self::toggle('htp_s_manifests', $cfg['s_manifests'], htp_t('COM_HTPROTECT_TG_SMANIFESTS'), htp_t('COM_HTPROTECT_TG_SMANIFESTS_HINT'));
        self::groupClose();

        // ── Gruppe 6: Auslieferung & Caching ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_DELIVERY'));
        echo self::toggle('htp_p_compress', $cfg['p_compress'], htp_t('COM_HTPROTECT_TG_PCOMPRESS'));
        echo self::toggle('htp_p_expires', $cfg['p_expires'], htp_t('COM_HTPROTECT_TG_PEXPIRES'), htp_t('COM_HTPROTECT_TG_PEXPIRES_HINT'));
        echo self::toggle('htp_p_etag', $cfg['p_etag'], htp_t('COM_HTPROTECT_TG_PETAG'), htp_t('COM_HTPROTECT_TG_PETAG_HINT'));
        self::groupClose();

        // ── Gruppe 7: Eigene Regeln ──
        $customTop = isset($data['import_top']) && is_string($data['import_top'])
            ? $data['import_top']
            : $cfg['custom_top'];

        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_CUSTOM'));
        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_CUSTOM_NOTE') . '</p>';
        // Eigene Regeln UNTEREINANDER und ueber die VOLLE Breite (htpx-vstack = einspaltig, statt des
        // zweispaltigen htpx-grid2): hier kommt oft viel Inhalt rein, besonders bei "Am Anfang". Das
        // "Am Anfang"-Feld startet daher zusaetzlich hoeher. (NICHT htpx-stack - das ist die Tabellen-
        // Karten-Klasse.)
        echo '<div class="htpx-vstack">'
            . '<div><label class="htpx-label" for="htp_custom_top">' . htp_t('COM_HTPROTECT_LBL_CUSTOMTOP') . '</label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_custom_top" name="htp_custom_top" rows="' . ($customTop !== $cfg['custom_top'] ? '16' : '10') . '">' . htp_esc($customTop) . '</textarea></div>'
            . '<div><label class="htpx-label" for="htp_custom_bottom">' . htp_t('COM_HTPROTECT_LBL_CUSTOMBOTTOM') . '</label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_custom_bottom" name="htp_custom_bottom" rows="6">' . htp_esc($cfg['custom_bottom']) . '</textarea></div>'
            . '</div>';
        self::groupClose();

        // ── Gruppe 8: .htaccess extern verwalten (Hands-off) ──
        self::groupOpen(htp_t('COM_HTPROTECT_GROUP_HTEXT'));
        // Sofort speichern beim Umlegen (mode=save -> Config speichern, .htaccess NICHT
        // schreiben). Sonst stuende der "&schreiben"-Knopf noch da (irrefuehrend) und
        // koennte beim 1. Mal noch schreiben. Nach dem Reload zeigt die Seite den
        // korrekten "nur speichern"-Knopf bzw. zurueck.
        echo '<label class="htpx-tg"><input type="checkbox" name="htp_htaccess_external" value="1"'
            . ' onchange="var i=document.createElement(\'input\');i.type=\'hidden\';i.name=\'htp_mode\';i.value=\'save\';this.form.appendChild(i);this.form.submit();"'
            . (!empty($cfg['htaccess_external']) ? ' checked' : '')
            . '><span class="htpx-tgsl"></span><span class="htpx-tgtext">'
            . htp_t('COM_HTPROTECT_HTEXT_LABEL')
            . '<small>' . htp_t('COM_HTPROTECT_HTEXT_HINT') . '</small></span></label>';
        self::groupClose();

        echo '</details>';

        // ── Aktion (ein primärer Button; richtet sich nach dem Server) ──
        // Bei extern verwalteter .htaccess (Hands-off) NUR die Einstellungen speichern,
        // die .htaccess NICHT schreiben - wie unter nginx.
        if ($isNginx || $isIis || !empty($cfg['htaccess_external'])) {
            $saveNote = !empty($cfg['htaccess_external'])
                ? htp_t('COM_HTPROTECT_HTEXT_SAVE_NOTE')
                : ($isIis ? htp_t('COM_HTPROTECT_SAVE_IIS_NOTE') : htp_t('COM_HTPROTECT_SAVE_NGINX_NOTE'));
            echo '<div class="htpx-actions" id="htp-a-write">'
                . '<button type="submit" name="htp_mode" value="save" class="htpx-btn">' . htp_t('COM_HTPROTECT_BTN_SAVE_SETTINGS') . '</button>'
                . '<span class="htpx-dim" style="margin-left:10px">'
                . $saveNote
                . '</span>'
                . '</div>';
        } else {
            echo '<div class="htpx-actions" id="htp-a-write">'
                . '<button type="submit" name="htp_mode" value="write" class="htpx-btn" '
                . 'onclick="return confirm(\'' . htp_t('COM_HTPROTECT_WRITE_CONFIRM') . '\');">'
                . htp_t('COM_HTPROTECT_BTN_SAVE_WRITE') . '</button> '
                . '<button type="submit" name="htp_mode" value="preview" class="htpx-btn2">' . htp_t('COM_HTPROTECT_BTN_PREVIEW') . '</button>'
                . '</div>';
        }

        echo '</form>';

        // ── Nginx-Regelblock: unter nginx der Hauptweg; unter Apache nur,
        //    wenn ueberhaupt ein Proxy/Cache davor erkannt wird (sonst irrelevant) ──
        if ($isNginx) {
            self::nginxPanel($cfg, $data);
        } elseif (htp_proxy_detected()) {
            echo '<details class="htpx-details"><summary>' . htp_t('COM_HTPROTECT_PROXY_SUMMARY') . '</summary>'
                . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_PROXY_TEXT') . '</p>';
            self::nginxPanel($cfg, $data);
            echo '</details>';
        }

        // ── IIS/web.config: unter IIS der Hauptweg (prominent), sonst als eingeklappte
        //    Export-Option (z. B. fuer einen Windows-/IIS-Umzug). ──
        if (!empty($data['iis_mode'])) {
            self::iisPanel($cfg, $data);
        } else {
            echo '<details class="htpx-details"><summary>' . htp_t('COM_HTPROTECT_IIS_SUMMARY') . '</summary>';
            self::iisPanel($cfg, $data);
            echo '</details>';
        }

        // Vorschau
        if (isset($data['preview']) && is_string($data['preview']) && $data['preview'] !== '') {
            echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_PREVIEW_TITLE') . ' '
                . '<button type="button" class="htpx-btn2 htpx-copy" onclick="htpxCopy(\'htpx-preview\')">' . htp_t('COM_HTPROTECT_COPY') . '</button></h4>'
                . '<pre class="htpx-pre" id="htpx-preview">' . htp_esc($data['preview']) . '</pre></div>';
        }

        // Aktuelle Datei
        if ($st['exists']) {
            echo '<details class="htpx-details"><summary>' . htp_t('COM_HTPROTECT_CURRENT_FILE', (int) $st['size']) . '</summary>'
                . '<pre class="htpx-pre">' . htp_esc($data['current']) . '</pre></details>';
        }
    }

    private static function renderSelfTest($test)
    {
        echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_SELFTEST_RESULT') . '</h4>';
        self::renderSelfTestTable($test);
        echo '</div>';
    }

    private static function renderSelfTestTable($test)
    {
        if (empty($test['checks'])) {
            return;
        }

        $tC = htp_t('COM_HTPROTECT_SELFTEST_TH_CHECK');
        $tS = htp_t('COM_HTPROTECT_SELFTEST_TH_STATUS');
        $tR = htp_t('COM_HTPROTECT_SELFTEST_TH_RESULT');

        echo '<table class="htpx-table htpx-stack"><thead><tr>'
            . '<th>' . $tC . '</th><th>' . $tS . '</th><th>' . $tR . '</th></tr></thead><tbody>';

        foreach ($test['checks'] as $check) {
            $note = isset($check['note']) ? (string) $check['note'] : '';
            echo '<tr><td data-label="' . $tC . '">' . htp_esc(isset($check['label']) ? $check['label'] : '')
                . ($note !== '' ? '<br><small class="htpx-dim">' . htp_esc($note) . '</small>' : '')
                . '</td>'
                . '<td class="htpx-mono" data-label="' . $tS . '">' . ((int) $check['status'] === 0 ? htp_t('COM_HTPROTECT_SELFTEST_NOCONN') : (int) $check['status']) . '</td>'
                . '<td data-label="' . $tR . '">' . (!empty($check['ok']) ? self::badge('ok', htp_t('COM_HTPROTECT_SELFTEST_OK_BADGE')) : self::badge('warn', htp_t('COM_HTPROTECT_SELFTEST_CHECK_BADGE'))) . '</td></tr>';
        }

        echo '</tbody></table>';
    }

    // ─────────────────────────────────────────────────────────────────
    //  Nginx-Regelblock (im Schutzschild eingebettet)
    // ─────────────────────────────────────────────────────────────────

    private static function nginxPanel($cfg, $data)
    {
        echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_NGINX_PANEL_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_NGINX_PANEL_INTRO') . '</p>'
            . '<ol class="htpx-dim" style="padding-left:18px;line-height:1.7">'
            . '<li>' . htp_t('COM_HTPROTECT_NGINX_STEP_PLESK') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_NGINX_STEP_OWN') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_NGINX_STEP_TEST') . '</li>'
            . '</ol>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_NGINX_PANEL_NOTES') . '</p>'
            . '</div>';

        $content = isset($data['nginx']) ? (string) $data['nginx'] : '';

        if ($content === '') {
            echo '<div class="htpx-msg htpx-msg-err">' . htp_t('COM_HTPROTECT_NGINX_BUILD_ERR') . '</div>';

            return;
        }

        echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_NGINX_GENERATED_TITLE') . ' '
            . '<button type="button" class="htpx-btn2 htpx-copy" onclick="htpxCopy(\'htpx-nginxconf\')">' . htp_t('COM_HTPROTECT_COPY') . '</button></h4>'
            . '<pre class="htpx-pre" id="htpx-nginxconf">' . htp_esc($content) . '</pre>'
            . '<div class="htpx-inline">'
            . '<form method="post" action="' . self::url('shield') . '">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="nginx_write">'
            . '<button type="submit" class="htpx-btn2">' . htp_t('COM_HTPROTECT_NGINX_BTN_WRITE') . '</button>'
            . '</form> '
            . '<form method="post" action="' . self::url('shield') . '">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="nginx_test">'
            . '<button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_NGINX_BTN_TEST') . '</button>'
            . '</form>'
            . '</div>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_NGINX_FILE_NOTE') . '</p>'
            . '</div>';
    }

    private static function iisPanel($cfg, $data)
    {
        echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_IIS_PANEL_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_IIS_PANEL_INTRO') . '</p>'
            . '<ol class="htpx-dim" style="padding-left:18px;line-height:1.7">'
            . '<li>' . htp_t('COM_HTPROTECT_IIS_STEP_MOD') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_IIS_STEP_PLACE') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_IIS_STEP_TEST') . '</li>'
            . '</ol>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_IIS_PANEL_NOTES') . '</p>'
            . '</div>';

        $content = isset($data['iis']) ? (string) $data['iis'] : '';

        if ($content === '') {
            echo '<div class="htpx-msg htpx-msg-err">' . htp_t('COM_HTPROTECT_IIS_BUILD_ERR') . '</div>';

            return;
        }

        echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_IIS_GENERATED_TITLE') . ' '
            . '<button type="button" class="htpx-btn2 htpx-copy" onclick="htpxCopy(\'htpx-iisconf\')">' . htp_t('COM_HTPROTECT_COPY') . '</button></h4>'
            . '<pre class="htpx-pre" id="htpx-iisconf">' . htp_esc($content) . '</pre>'
            . '<div class="htpx-inline">'
            . '<form method="post" action="' . self::url('shield') . '">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="iis_write">'
            . '<button type="submit" class="htpx-btn2">' . htp_t('COM_HTPROTECT_IIS_BTN_WRITE') . '</button>'
            . '</form>'
            . '</div>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_IIS_FILE_NOTE') . '</p>'
            . '</div>';
    }

    // ─────────────────────────────────────────────────────────────────
    //  Bereich: Site-Scan & Pfad-Test
    // ─────────────────────────────────────────────────────────────────

    private static function secScan($cfg, $data)
    {
        echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_NAV_SCAN') . '</h2>';

        $queue = isset($data['scan_queue']) && is_array($data['scan_queue']) ? $data['scan_queue'] : array();

        echo '<div class="htpx-card htpx-card-wide">'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SCAN_INTRO') . '</p>'
            . '<form method="post" action="' . self::url('scan') . '" id="htpx-scanform">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="scan_run">'
            . '<input type="hidden" name="htp_found" id="htp_found" value="">'
            . '<label class="htpx-label" for="htp_scan_pages">' . htp_t('COM_HTPROTECT_SCAN_MAXPAGES') . '</label>'
            . '<div class="htpx-inline"><input class="htpx-input htpx-input-s" type="number" id="htp_scan_pages" name="htp_scan_pages" min="3" max="30" value="12"> '
            . '<button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_SCAN_START_BTN') . '</button></div>'
            . '<div class="htpx-progress" id="htpx-scanprog" style="display:none">'
            . '<div class="htpx-progress-info"><span id="htpx-scanprog-label">' . htp_t('COM_HTPROTECT_SCAN_RUNNING') . '</span><span id="htpx-scanprog-pct">0%</span></div>'
            . '<div class="htpx-progress-track"><div class="htpx-progress-bar" id="htpx-scanprog-bar"></div></div>'
            . '</div>'
            . '</form>'
            . '<script type="application/json" id="htpx-scanqueue">' . json_encode($queue, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . '</script>'
            . '</div>';

        if (isset($data['scan']) && is_array($data['scan'])) {
            self::renderScan($data['scan']);
        }

        // URL-Test (samt Ressourcen) - deckt 403er durch .htaccess ODER Mini-WAF auf.
        // Eingebundene Ressourcen werden IMMER mitgeprueft (bewusst ohne Schalter).
        $urlInput = isset($data['urltest_input']) ? $data['urltest_input'] : '';

        echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_URLTEST_TITLE') . '</h2>'
            . '<div class="htpx-card htpx-card-wide">'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_URLTEST_INTRO') . '</p>'
            . '<form method="post" action="' . self::url('scan') . '">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="url_test">'
            . '<div class="htpx-inline"><input class="htpx-input" type="text" name="htp_test_url" placeholder="' . htp_esc(htp_t_attr('COM_HTPROTECT_URLTEST_PH')) . '" value="' . htp_esc($urlInput) . '"> '
            . '<button type="submit" class="htpx-btn2 htpx-urltest-btn">' . htp_t('COM_HTPROTECT_URLTEST_BTN') . '</button></div>'
            . '</form>';

        if (isset($data['urltest']) && is_array($data['urltest'])) {
            self::renderUrlTest($data['urltest']);
        }

        echo '</div>';

        // ── Fundament zuerst: Kerndatei-Abgleich gegen das offizielle Joomla-Paket ──
        self::renderCoreCompare($cfg);

        // ── Danach der experimentelle Tiefen-Scan nach Schaddateien (nur per Button) ──
        self::renderMalFiles($cfg);

        // ── GEMEINSAMES Einstellungs-Formular: jeder Button dieser Seite
        //    (tmp aufraeumen / Speichern / Jetzt pruefen) sendet ALLE Schalter
        //    und speichert sie - kein verlorener Schalter. Rein serverseitig
        //    (robust, kein JS, kein zusaetzlicher Endpunkt/keine Angriffsflaeche).
        echo '<form method="post" action="' . self::url('scan') . '" style="margin:0">'
            . self::tokenField();

        // ── Wartung: tmp/ aufraeumen (Sofort + taegliche Automatik) ──
        self::renderTmpClean($cfg, $data);

        // ── Defacement-/SEO-Spam-Waechter ──
        self::renderSeoWatch($cfg, $data);

        echo '</form>';
    }

    /**
     * Schwarm-Schutz: bewusst HINTERGRUENDIGER, eingeklappter Experten-Block.
     * Null Footprint, solange zu. FP = ein Klick (Nutzer-Whitelist), FN = Pfade
     * einfuegen. Sendet ANONYM an das Melde-Portal -> kuratiert -> Signatur-Feed.
     * Die geschlossene Rueckkopplung („deine Meldung ist live") feiert die Wirkung.
     */
    private static function renderSwarm($cfg)
    {
        $community = (class_exists('HtpDefs') && method_exists('HtpDefs', 'malwareCommunityCount'))
            ? (int) HtpDefs::malwareCommunityCount() : 0;

        // Eigenstaendiges Formular (mit Token) - sitzt direkt unter der Malware-
        // Whitelist, NICHT mehr im geteilten Einstellungs-Formular.
        echo '<form method="post" action="' . self::url('scan') . '" class="htpx-swarmform">'
            . self::tokenField()
            . '<details class="htpx-swarm">'
            . '<summary><span class="htpx-swarm-bee">&#128029;</span>' . htp_t('COM_HTPROTECT_SWARM_SUMMARY') . '</summary>'
            . '<div class="htpx-swarm-body">'
            . '<p class="htpx-dim htpx-swarm-note">' . htp_t('COM_HTPROTECT_SWARM_CONSENT') . '</p>'
            . '<div class="htpx-swarm-row">'
            . '<button type="submit" name="htp_task" value="swarm_fp" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_SWARM_FP_BTN') . '</button> '
            . '<span class="htpx-dim">' . htp_t('COM_HTPROTECT_SWARM_FP_HINT') . '</span>'
            . '</div>'
            . '<label class="htpx-label" for="htp_swarm_paths" style="margin-top:12px">' . htp_t('COM_HTPROTECT_SWARM_FN_LABEL') . '</label>'
            . '<textarea class="htpx-textarea" id="htp_swarm_paths" name="htp_swarm_paths" rows="3" placeholder="templates/shaper_helixultimate/index.php" style="min-height:76px;max-height:260px;overflow-y:auto" oninput="this.style.height=\'auto\';this.style.height=Math.min(this.scrollHeight,260)+\'px\'"></textarea>'
            . '<p class="htpx-dim htpx-swarm-note" style="margin:6px 0 0">' . htp_t('COM_HTPROTECT_SWARM_FN_HINT') . '</p>'
            . '<input class="htpx-input" type="text" name="htp_swarm_note" placeholder="' . htp_esc(htp_t_attr('COM_HTPROTECT_SWARM_FN_NOTE_PH')) . '" style="margin-top:6px">'
            . '<div style="margin-top:8px">'
            . '<button type="submit" name="htp_task" value="swarm_fn" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_SWARM_FN_BTN') . '</button>'
            . '</div>';

        if ($community > 0) {
            echo '<p class="htpx-dim htpx-swarm-stat">' . htp_t('COM_HTPROTECT_SWARM_STAT', $community) . '</p>';
        }

        echo '</div></details></form>';
    }

    /**
     * Karte „Defacement-/SEO-Spam-Wächter": Schalter, URL-Whitelist, „jetzt
     * prüfen"-Knopf + Ergebnis. Läuft alle 6 h im Hintergrund (still, meldet nur).
     */
    private static function renderSeoWatch($cfg, $data)
    {
        $on = !empty($cfg['seo_watch']);
        $wl = (isset($cfg['seo_whitelist']) && is_array($cfg['seo_whitelist'])) ? implode("\n", $cfg['seo_whitelist']) : '';

        echo '<div class="htpx-card htpx-card-wide" id="htp-a-seowatch">'
            . '<h4>' . htp_t('COM_HTPROTECT_SEO_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SEO_INTRO') . '</p>';

        if (isset($data['seo_result']) && is_array($data['seo_result'])) {
            if (empty($data['seo_result'])) {
                echo '<p class="htpx-msg htpx-msg-ok">' . htp_t('COM_HTPROTECT_SEO_CLEAN') . '</p>';
            } else {
                foreach ($data['seo_result'] as $inc) {
                    $reasons = (isset($inc['reasons']) && is_array($inc['reasons'])) ? implode('; ', $inc['reasons']) : '';
                    echo '<p class="htpx-msg htpx-msg-err">' . htp_t('COM_HTPROTECT_SEO_ALERT', htp_esc((string) $inc['url']), htp_esc($reasons)) . '</p>';
                }
            }
        } elseif (isset($cfg['seo_overview_finding']) && is_array($cfg['seo_overview_finding']) && !empty($cfg['seo_overview_finding']['reasons'])) {
            // Persistenter Befund der automatischen Pruefung (Uebersicht 1h /
            // Hintergrund 6h) - hier echoen, damit der Sprung aus der Ampel den
            // Grund auch zeigt (ohne dass der Admin erst „Jetzt pruefen" klickt).
            $reasons = implode('; ', $cfg['seo_overview_finding']['reasons']);
            echo '<p class="htpx-msg htpx-msg-err">' . htp_t('COM_HTPROTECT_SEO_ALERT', htp_esc(htp_site_url()), htp_esc($reasons)) . '</p>';
        }

        echo self::toggle('htp_seo_watch', $on, htp_t('COM_HTPROTECT_SEO_TOGGLE'))
            . '<label class="htpx-label" for="htp_seo_whitelist" style="margin-top:8px">' . htp_t('COM_HTPROTECT_SEO_WL_LABEL') . '</label>'
            . '<textarea class="htpx-textarea" id="htp_seo_whitelist" name="htp_seo_whitelist" rows="2" placeholder="https://example.org/aktion">' . htp_esc($wl) . '</textarea>'
            . '<div style="margin-top:8px">'
            . '<button type="submit" name="htp_task" value="seo_save" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_SAVE') . '</button> '
            . '<button type="submit" name="htp_task" value="seo_scan" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_SEO_SCAN_BTN') . '</button>'
            . '</div></div>';
    }

    /**
     * Kompakter Wartungs-Block im Scan-Bereich: tmp/ sofort aufraeumen
     * (>24 h alte Reste/Sonden) + Schalter fuer die taegliche Automatik + per
     * Aufklapp "(Ausschlussliste)" die Liste. Der Klick speichert + raeumt auf.
     */
    private static function renderTmpClean($cfg, $data)
    {
        $auto = !empty($cfg['tmp_autoclean']);

        echo '<div class="htpx-card htpx-card-wide" id="htp-a-tmpclean">'
            . '<h4>' . htp_t('COM_HTPROTECT_TMPCLEAN_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_TMPCLEAN_INTRO') . '</p>';

        if (isset($data['tmpclean']) && is_array($data['tmpclean'])) {
            echo '<p class="htpx-msg htpx-msg-ok">' . htp_t('COM_HTPROTECT_TMPCLEAN_RESULT', count($data['tmpclean'])) . '</p>';
        }

        $keep = (isset($cfg['tmp_keep']) && is_array($cfg['tmp_keep'])) ? $cfg['tmp_keep'] : array();

        // Schalter + dezenter Aufklapp-Link "(Ausschlussliste)" in EINER Zeile; die
        // Liste (Label + Textarea) erscheint per Klick darunter - rein per CSS
        // (details[open] ~ body), kein JS. Offen, wenn schon Eintraege vorhanden.
        $kOpen = !empty($keep) ? ' open' : '';

        echo '<div class="htpx-tmpkeep-row">'
            . self::toggle('htp_tmp_autoclean', $auto, htp_t('COM_HTPROTECT_TMPCLEAN_AUTO'))
            . '<details class="htpx-tmpkeep"' . $kOpen . '><summary>' . htp_t('COM_HTPROTECT_TMPKEEP_SUMMARY') . '</summary></details>'
            . '<div class="htpx-tmpkeep-body">'
            . '<label class="htpx-label" for="htp_tmp_keep">' . htp_t('COM_HTPROTECT_TMPKEEP_LABEL')
            . ' <span class="htpx-dim">' . htp_t('COM_HTPROTECT_PERLINE') . '</span></label>'
            . '<textarea class="htpx-textarea htpx-mono" id="htp_tmp_keep" name="htp_tmp_keep" rows="2" placeholder="'
            . htp_esc(htp_t('COM_HTPROTECT_TMPKEEP_PH')) . '">' . htp_esc(implode("\n", $keep)) . '</textarea>'
            . '</div></div>'
            . '<div style="margin-top:8px">'
            . '<button type="submit" name="htp_task" value="tmp_clean" class="htpx-btn2 htpx-btn-s">'
            . htp_t('COM_HTPROTECT_TMPCLEAN_BTN') . '</button> '
            . '<span class="htpx-dim">' . htp_t('COM_HTPROTECT_TMPCLEAN_NOTE') . '</span>'
            . '</div>'
            . '</div>';
    }

    private static function renderScan($scan)
    {
        if (!$scan['ok']) {
            echo '<div class="htpx-msg htpx-msg-err">' . htp_esc($scan['error']) . '</div>';

            return;
        }

        echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_SCAN_RESULT_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_SCAN_SUMMARY', count($scan['pages']), (int) $scan['checked'], count($scan['blocked'])) . '</p>';

        echo '<details class="htpx-details"><summary>' . htp_t('COM_HTPROTECT_SCAN_PAGES_SUMMARY') . '</summary><ul class="htpx-syslist">';

        foreach ($scan['pages'] as $url => $status) {
            echo '<li><span class="htpx-mono">' . htp_esc($url) . '</span> &rarr; ' . (int) $status . '</li>';
        }

        echo '</ul></details>';

        if (empty($scan['blocked'])) {
            echo '<p>' . self::badge('ok', htp_t('COM_HTPROTECT_SCAN_NOCONFLICT_BADGE')) . ' &nbsp;' . htp_t('COM_HTPROTECT_SCAN_NOCONFLICT_TEXT') . '</p></div>';

            return;
        }

        $tP = htp_t('COM_HTPROTECT_SCAN_TH_PATH');
        $tR = htp_t('COM_HTPROTECT_SCAN_TH_RULE');
        $tF = htp_t('COM_HTPROTECT_SCAN_TH_FOUNDON');
        $tA = htp_t('COM_HTPROTECT_SCAN_TH_ACTION');

        echo '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_SCAN_BLOCKED_TITLE') . '</h4><table class="htpx-table htpx-stack"><thead><tr><th>' . $tP . '</th><th>' . $tR . '</th><th>' . $tF . '</th><th>' . $tA . '</th></tr></thead><tbody>';

        foreach (array_slice($scan['blocked'], 0, 100) as $item) {
            echo '<tr><td class="htpx-mono" data-label="' . $tP . '">' . htp_esc($item['path']) . '</td>'
                . '<td data-label="' . $tR . '">' . htp_esc($item['rule']) . '</td>'
                . '<td class="htpx-mono htpx-dim" data-label="' . $tF . '">' . htp_esc($item['source']) . '</td>'
                . '<td data-label="' . $tA . '">' . self::allowForm($item['path']) . '</td></tr>';
        }

        echo '</tbody></table>';

        if (!empty($scan['suggestions'])) {
            echo '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_SCAN_SUGG_TITLE') . '</h4>'
                . '<form method="post" action="' . self::url('scan') . '">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="scan_apply">'
                . '<label class="htpx-tg"><input type="checkbox" onclick="htpxToggleAll(this)"><span class="htpx-tgsl"></span><span class="htpx-tgtext">' . htp_t('COM_HTPROTECT_SELECT_ALL') . '</span></label>'
                . '<div class="htpx-sugglist">';

            foreach ($scan['suggestions'] as $suggestion) {
                $value = $suggestion['key'] . '|' . $suggestion['value'];

                // Verifizierter Design-/Asset-Ordner: eigenes, beruhigendes Label + vorausgewaehlt
                // (unbedenklich geprueft) - so wird die Freigabe zum echten Ein-Klick.
                $asset = !empty($suggestion['asset']);

                if ($suggestion['key'] === 'dir_php') {
                    $label = $asset
                        ? htp_t('COM_HTPROTECT_SCAN_SUGG_ASSETDIR', htp_esc($suggestion['value']), (int) $suggestion['count'])
                        : htp_t('COM_HTPROTECT_SCAN_SUGG_DIR', htp_esc($suggestion['value']), (int) $suggestion['count']);
                } elseif ($suggestion['key'] === 'ext') {
                    $label = htp_t('COM_HTPROTECT_SCAN_SUGG_EXT', htp_esc($suggestion['value']), (int) $suggestion['count']);
                } else {
                    $label = htp_t('COM_HTPROTECT_SCAN_SUGG_FILE', htp_esc($suggestion['value']));
                }

                echo '<label class="htpx-tg htpx-sugg"><input type="checkbox" name="sg[]" value="' . htp_esc($value) . '"' . ($asset ? ' checked' : '') . '>'
                    . '<span class="htpx-tgsl"></span><span class="htpx-tgtext">' . $label
                    . '<small class="htpx-mono">' . htp_esc(implode('  &middot;  ', $suggestion['paths'])) . '</small>'
                    . '</span></label>';
            }

            echo '</div><p><button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_SCAN_APPLY_BTN') . '</button> '
                . '<span class="htpx-dim">' . htp_t('COM_HTPROTECT_SCAN_APPLY_NOTE') . '</span></p></form>';
        }

        echo '</div>';
    }

    /**
     * Ergebnis des URL-Tests: Haupt-Verdikt (welche Ebene/Regel) + Ressourcen.
     */
    private static function renderUrlTest($t)
    {
        if (!empty($t['foreign'])) {
            echo '<div class="htpx-msg htpx-msg-info">' . htp_t('COM_HTPROTECT_URLTEST_FOREIGN') . '</div>';

            return;
        }

        $v       = $t['verdict'];
        $blocked = empty($v['allowed']);
        $target  = $t['path'] . ($t['query'] !== '' ? '?' . $t['query'] : '');

        echo '<div class="htpx-verdict">'
            . ($blocked
                ? self::badge('err', htp_t('COM_HTPROTECT_URLTEST_BLOCKED'))
                : self::badge('ok', htp_t('COM_HTPROTECT_URLTEST_ALLOWED')))
            . ' <span class="htpx-mono">' . htp_esc($target) . '</span>';

        if ($blocked) {
            $layerLabel = $v['layer'] === 'waf'
                ? htp_t('COM_HTPROTECT_URLTEST_LAYER_WAF')
                : htp_t('COM_HTPROTECT_URLTEST_LAYER_HTACCESS');

            echo '<div class="htpx-dim">' . htp_t('COM_HTPROTECT_URLTEST_VIA') . ' <strong>' . $layerLabel . '</strong></div>'
                . '<div class="htpx-dim">' . htp_t('COM_HTPROTECT_URLTEST_RULE_LABEL') . ' ' . htp_esc($v['rule']) . '</div>';

            // Detail: getroffenes Filter-Muster bzw. Signatur-ID
            if ($v['cause'] === 'filter' && $v['detail'] !== '') {
                echo '<div class="htpx-dim">' . htp_t('COM_HTPROTECT_URLTEST_PATTERN_LABEL') . ' <code>' . htp_esc($v['detail']) . '</code></div>';
            } elseif ($v['cause'] === 'signature' && $v['detail'] !== '') {
                echo '<div class="htpx-dim">' . htp_t('COM_HTPROTECT_URLTEST_SIG_LABEL') . ' <code>' . htp_esc($v['detail']) . '</code></div>';
            }

            // Konkreter Freigabe-Vorschlag bei Pfad-/Datei-Sperren.
            if ($v['cause'] === 'path' && is_array($v['suggest'])) {
                echo '<div class="htpx-dim">' . htp_t('COM_HTPROTECT_PATHTEST_SUGGEST_LABEL') . ' '
                    . ($v['suggest']['type'] === 'ext'
                        ? htp_t('COM_HTPROTECT_PATHTEST_SUGGEST_EXT', htp_esc($v['suggest']['value']))
                        : htp_t('COM_HTPROTECT_PATHTEST_SUGGEST_FILE', htp_esc($v['suggest']['value'])))
                    . ' ' . htp_t('COM_HTPROTECT_PATHTEST_SUGGEST_WHERE') . '</div>';
            }

            // Fehlerbehebungs-Anleitung (explizite Schluessel -> langcheck-fest).
            $helpKeys = array(
                'filter'    => 'COM_HTPROTECT_URLTEST_HELP_FILTER',
                'signature' => 'COM_HTPROTECT_URLTEST_HELP_SIGNATURE',
                'path'      => 'COM_HTPROTECT_URLTEST_HELP_PATH',
                'api'       => 'COM_HTPROTECT_URLTEST_HELP_API',
            );

            if (isset($helpKeys[$v['cause']])) {
                echo '<div class="htpx-help"><strong>' . htp_t('COM_HTPROTECT_URLTEST_HELP_TITLE') . '</strong> '
                    . htp_t($helpKeys[$v['cause']]) . '</div>';
            }

            // 1-Klick nur bei pfad-/dateibasierten Sperren bzw. /api: dort hilft
            // ein vertrauenswürdiger Pfad. Bei Signatur/Filter (oft Query-bezogen)
            // wäre eine Pfad-Freigabe das falsche Mittel -> dort nur die Anleitung.
            if (in_array($v['cause'], array('path', 'api'), true)) {
                echo '<div class="htpx-allowadd">' . self::allowForm($t['path']) . '</div>';
            }
        }

        echo '</div>';

        if (!empty($t['fetched']) && is_array($t['resources'])) {
            self::renderUrlResources($t);
        }
    }

    /**
     * Eingebundene Ressourcen der getesteten Seite gegen die Pfad-Regeln.
     */
    private static function renderUrlResources($t)
    {
        $res    = $t['resources'];
        $status = (int) $t['status'];

        echo '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_URLTEST_RES_TITLE') . '</h4>';

        if ($status < 200 || $status >= 400) {
            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_URLTEST_RES_FETCHFAIL', $status) . '</p>';

            return;
        }

        if (empty($res['blocked'])) {
            echo '<p>' . self::badge('ok', htp_t('COM_HTPROTECT_URLTEST_RES_OK_BADGE')) . ' &nbsp;'
                . htp_t('COM_HTPROTECT_URLTEST_RES_SUMMARY', (int) $res['checked'], 0) . '</p>';

            return;
        }

        $tP = htp_t('COM_HTPROTECT_SCAN_TH_PATH');
        $tR = htp_t('COM_HTPROTECT_SCAN_TH_RULE');

        echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_URLTEST_RES_SUMMARY', (int) $res['checked'], count($res['blocked'])) . '</p>'
            . '<table class="htpx-table htpx-stack"><thead><tr><th>' . $tP . '</th><th>' . $tR . '</th></tr></thead><tbody>';

        foreach (array_slice($res['blocked'], 0, 100) as $item) {
            echo '<tr><td class="htpx-mono" data-label="' . $tP . '">' . htp_esc($item['path']) . '</td>'
                . '<td data-label="' . $tR . '">' . htp_esc($item['rule']) . '</td></tr>';
        }

        echo '</tbody></table>'
            . '<div class="htpx-help"><strong>' . htp_t('COM_HTPROTECT_URLTEST_HELP_TITLE') . '</strong> '
            . htp_t('COM_HTPROTECT_URLTEST_HELP_PATH') . '</div>';
    }

    // ─────────────────────────────────────────────────────────────────
    //  Bereich: Sicherungen
    // ─────────────────────────────────────────────────────────────────

    private static function secBackups($cfg, $data)
    {
        // Seitentitel (>= 5.4) eigenstaendig - NICHT der Menue-/Nav-Titel - damit
        // es keine Doppelueberschrift mit der Auto-Update-Sektion gibt.
        echo '<h2 class="htpx-h">'
            . (self::backupsUpdates() ? htp_t('COM_HTPROTECT_BACKUPS_PAGETITLE') : self::backupsNavLabel())
            . '</h2>';

        // Erweiterungs-Auto-Update teilt sich diesen Bereich (5.4+ ODER Joomla 3) -
        // eigenes Modul, ein Aufruf, keine Logik hier. Steht bewusst OBEN.
        if (self::backupsUpdates()) {
            // FEATURE-FLAG (zentral in HtpAutoUpdate::FEATURE_VISIBLE): die Auto-Update-
            // Oberflaeche ist vorerst AUSGEBLENDET (noch in Arbeit/ungetestet) - und das
            // Feature ist auch dormant (Scheduler pausiert, runStep no-op). Der gesamte
            // Code bleibt erhalten; zum Wiedereinblenden+Aktivieren EIN Flag umschalten.
            if (HtpAutoUpdate::featureVisible()) {
                HtpAutoUpdateUi::render($cfg, $data);
            } else {
                echo '<div class="htpx-card htpx-card-wide"><p class="htpx-dim">'
                    . htp_t('COM_HTPROTECT_AU_WIP') . '</p></div>';
            }
        } elseif (self::upgradeHint()) {
            // Joomla 4.0-<5.4: Auto-Update noch nicht verfuegbar -> Hinweis, auf Joomla 5 zu
            // upgraden (damit das Feature nutzbar wird). Steht bewusst OBEN, vor den Sicherungen.
            self::renderUpgradeHint();
        }

        // .htaccess-Sicherungen bleiben wie bisher verwaltbar - bewusst GANZ UNTEN.
        self::secHtaccessBackups($cfg, $data);

        // Direkt darunter: die reversiblen Helix-params-Sicherungen (nur falls vorhanden).
        self::secHelixBackups($cfg, $data);
    }

    /**
     * Joomla 4.0-<5.4: freundlicher Hinweis, auf Joomla 5.4+ zu aktualisieren, damit das
     * Erweiterungs-/Selbst-Auto-Update nutzbar wird. Beruhigend (Upgrade laeuft nahezu immer
     * komplikationsfrei). Rein informativ - HTProtect greift NICHT selbst ins Joomla-Update ein;
     * der Button verlinkt nur auf Joomlas eigene Update-Ansicht (com_joomlaupdate).
     *
     * @return void
     */
    private static function renderUpgradeHint()
    {
        echo '<div class="htpx-card htpx-card-wide htpx-upgrade" style="border-left:4px solid var(--acc)">'
            . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_AU_UPGRADE_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_AU_UPGRADE_TEXT') . '</p>'
            . '<p style="margin:10px 0 0"><a class="htpx-btn" href="index.php?option=com_joomlaupdate">'
            . htp_t('COM_HTPROTECT_AU_UPGRADE_BTN') . '</a></p>'
            . '</div>';
    }

    private static function secHtaccessBackups($cfg, $data)
    {
        echo '<div class="htpx-card htpx-card-wide" id="htp-htbackups">'
            . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_BACKUPS_HT_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_BACKUPS_INTRO', (int) HtpStorage::KEEP_BACKUPS) . ' '
            . '<span class="htpx-mono">' . htp_esc(HtpStorage::backupDir()) . '</span></p>';

        $backups = isset($data['backups']) ? $data['backups'] : array();

        if (empty($backups)) {
            echo '<p>' . self::badge('info', htp_t('COM_HTPROTECT_BACKUPS_NONE')) . '</p></div>';

            return;
        }

        // Roh (nicht htp_esc): die Lang-Strings enthalten gueltige HTML-Entities
        // (z. B. Gr&ouml;&szlig;e), die im <th> UND im data-label-Attribut korrekt
        // dekodieren. htp_esc wuerde das & doppelt escapen -> woertliche Anzeige.
        $bN = htp_t('COM_HTPROTECT_BACKUPS_TH_NAME');
        $bD = htp_t('COM_HTPROTECT_BACKUPS_TH_DATE');
        $bS = htp_t('COM_HTPROTECT_BACKUPS_TH_SIZE');
        $bA = htp_t('COM_HTPROTECT_BACKUPS_TH_ACTIONS');

        echo '<table class="htpx-table htpx-stack"><thead><tr><th>' . $bN . '</th><th>' . $bD . '</th><th>' . $bS . '</th><th>' . $bA . '</th></tr></thead><tbody>';

        foreach ($backups as $backup) {
            echo '<tr><td class="htpx-mono" data-label="' . $bN . '">' . htp_esc($backup['name']) . '</td>'
                . '<td data-label="' . $bD . '">' . htp_esc(htp_local_time((int) $backup['mtime'], 'd.m.Y H:i:s')) . '</td>'
                . '<td data-label="' . $bS . '">' . (int) $backup['size'] . ' B</td>'
                . '<td data-label="' . $bA . '"><div class="htpx-rowactions">';

            // Anzeigen
            echo '<form method="post" action="' . self::url('backups') . '">' . self::tokenField()
                . '<input type="hidden" name="htp_task" value="backup_view">'
                . '<input type="hidden" name="htp_backup" value="' . htp_esc($backup['name']) . '">'
                . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_BACKUPS_VIEW') . '</button></form>';

            // Wiederherstellen
            echo '<form method="post" action="' . self::url('backups') . '" onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_BACKUPS_RESTORE_CONFIRM') . '\');">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="backup_restore">'
                . '<input type="hidden" name="htp_backup" value="' . htp_esc($backup['name']) . '">'
                . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_RESTORE_LC') . '</button></form>';

            // Loeschen
            echo '<form method="post" action="' . self::url('backups') . '" onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_BACKUPS_DELETE_CONFIRM') . '\');">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="backup_delete">'
                . '<input type="hidden" name="htp_backup" value="' . htp_esc($backup['name']) . '">'
                . '<button type="submit" class="htpx-btn2 htpx-btn-s htpx-btn-danger">' . htp_t('COM_HTPROTECT_DELETE_LC') . '</button></form>';

            echo '</div></td></tr>';
        }

        echo '</tbody></table>';

        if (isset($data['backup_view']) && is_array($data['backup_view'])) {
            echo '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_BACKUPS_CONTENT_OF') . ' <span class="htpx-mono">' . htp_esc($data['backup_view']['name']) . '</span> '
                . '<button type="button" class="htpx-btn2 htpx-copy" onclick="htpxCopy(\'htpx-backupview\')">' . htp_t('COM_HTPROTECT_COPY') . '</button></h4>'
                . '<pre class="htpx-pre" id="htpx-backupview">' . htp_esc($data['backup_view']['content']) . '</pre>';
        }

        echo '</div>';
    }

    /**
     * Reversible Sicherungen der Template-Stil-Optionen aus der Helix-JS-Bereinigung -
     * gleicher Ordner wie die .htaccess-Sicherungen, hier mit eigenem Zurueckspiel-Button.
     * Nur sichtbar, wenn es solche Sicherungen gibt.
     */
    private static function secHelixBackups($cfg, $data)
    {
        $items = isset($data['helix_backups']) && is_array($data['helix_backups']) ? $data['helix_backups'] : array();

        if (empty($items)) {
            return;
        }

        echo '<div class="htpx-card htpx-card-wide" id="htp-helixbackups">'
            . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_HELIX_BK_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_HELIX_BK_INTRO') . ' '
            . '<span class="htpx-mono">' . htp_esc(HtpStorage::backupDir()) . '</span></p>';

        $bT = htp_t('COM_HTPROTECT_HELIX_BK_STYLE');
        $bN = htp_t('COM_HTPROTECT_BACKUPS_TH_NAME');
        $bD = htp_t('COM_HTPROTECT_BACKUPS_TH_DATE');
        $bA = htp_t('COM_HTPROTECT_BACKUPS_TH_ACTIONS');

        echo '<table class="htpx-table htpx-stack"><thead><tr><th>' . $bT . '</th><th>' . $bN . '</th><th>' . $bD . '</th><th>' . $bA . '</th></tr></thead><tbody>';

        foreach ($items as $b) {
            $when = '';

            if (!empty($b['ts'])) {
                $t    = strtotime((string) $b['ts']);
                $when = $t ? htp_local_time($t, 'd.m.Y H:i:s') : (string) $b['ts'];
            }

            echo '<tr>'
                . '<td data-label="' . $bT . '">' . htp_esc((string) $b['title'])
                . ' <span class="htpx-dim">(' . htp_esc((string) $b['template']) . ', #' . (int) $b['id'] . ')</span></td>'
                . '<td class="htpx-mono" data-label="' . $bN . '">' . htp_esc((string) $b['name']) . '</td>'
                . '<td data-label="' . $bD . '">' . htp_esc($when) . '</td>'
                . '<td data-label="' . $bA . '">'
                . '<form method="post" action="' . self::url('backups') . '" onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_HELIX_BK_CONFIRM') . '\');">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="helix_restore">'
                . '<input type="hidden" name="htp_helix_backup" value="' . htp_esc((string) $b['name']) . '">'
                . '<button type="submit" class="htpx-btn2 htpx-btn-s">' . htp_t('COM_HTPROTECT_RESTORE_LC') . '</button></form>'
                . '</td></tr>';
        }

        echo '</tbody></table></div>';
    }

    // ─────────────────────────────────────────────────────────────────
    //  Bereich: Hilfe & Support
    // ─────────────────────────────────────────────────────────────────

    private static function secHelp($cfg, $data)
    {
        $user = htp_user();
        $sup  = isset($data['support']) && is_array($data['support']) ? $data['support'] : null;

        $name    = $sup !== null ? $sup['name']    : ($user && isset($user->name)  ? (string) $user->name  : '');
        $email   = $sup !== null ? $sup['email']   : ($user && isset($user->email) ? (string) $user->email : '');
        $subject = $sup !== null ? $sup['subject'] : '';
        $message = $sup !== null ? $sup['message'] : '';
        $sysinfo = $sup !== null ? !empty($sup['sysinfo']) : true;

        // Seitenueberschrift bewusst TBTITLE_HELP ("Hilfe & Support") - der Nav-Tab
        // und der Joomla-Komponenten-Menuepunkt heissen dagegen "Hilfreicher Support".
        echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_TBTITLE_HELP') . '</h2>';

        echo '<div class="htpx-card htpx-card-wide" id="htp-support">'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_HELP_INTRO') . '</p>'
            . '<p class="htpx-forumnote">' . htp_t(
                'COM_HTPROTECT_HELP_FORUM_NOTE',
                '<a href="https://htprotect.app/forum" target="_blank" rel="noopener">Forum</a>'
            ) . '</p>'
            . '<form method="post" action="' . self::url('help') . '" class="htpx-form">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="support_send">'
            . '<div class="htpx-grid2">'
            . '<div><label class="htpx-label" for="htp_sup_name">' . htp_t('COM_HTPROTECT_HELP_NAME') . '</label>'
            . '<input class="htpx-input" type="text" id="htp_sup_name" name="htp_sup_name" value="' . htp_esc($name) . '"></div>'
            . '<div><label class="htpx-label" for="htp_sup_email">' . htp_t('COM_HTPROTECT_HELP_EMAIL') . '</label>'
            . '<input class="htpx-input" type="email" id="htp_sup_email" name="htp_sup_email" required value="' . htp_esc($email) . '"></div>'
            . '</div>'
            . '<label class="htpx-label" for="htp_sup_subject">' . htp_t('COM_HTPROTECT_HELP_SUBJECT') . '</label>'
            . '<input class="htpx-input" type="text" id="htp_sup_subject" name="htp_sup_subject" value="' . htp_esc($subject) . '" placeholder="' . htp_esc(htp_t_attr('COM_HTPROTECT_HELP_SUBJECT_PH')) . '">'
            . '<label class="htpx-label" for="htp_sup_message">' . htp_t('COM_HTPROTECT_HELP_MESSAGE') . '</label>'
            . '<textarea class="htpx-textarea" id="htp_sup_message" name="htp_sup_message" rows="7" required placeholder="' . htp_esc(htp_t_attr('COM_HTPROTECT_HELP_MESSAGE_PH')) . '">' . htp_esc($message) . '</textarea>';
        echo self::toggle('htp_sup_sysinfo', $sysinfo ? 1 : 0, htp_t('COM_HTPROTECT_HELP_SYSINFO'), htp_t('COM_HTPROTECT_HELP_SYSINFO_HINT'));
        echo '<p><button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_HELP_SEND_BTN') . '</button></p>'
            . '</form></div>';

        // Dezenter Hinweis auf das optionale, kostenlose htprotect.app-Dashboard. Steht bewusst
        // NUR auf der Hilfe-Seite - hier ist der Nutzer freiwillig; im normalen Betrieb draengt
        // sich nichts auf. Sekundaerer Stil (htpx-btn2), keine Kaufaufforderung.
        echo '<div class="htpx-card htpx-card-wide" id="htp-app">'
            . '<h4>' . htp_t('COM_HTPROTECT_HELP_APP_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_HELP_APP_TEXT') . '</p>'
            . '<p class="htpx-appcta"><a class="htpx-btn2" href="https://htprotect.app" target="_blank" rel="noopener">' . htp_t('COM_HTPROTECT_HELP_APP_BTN') . '</a>'
            . ' <a class="htpx-appwhere" href="' . self::url('overview') . '#htp-fv" onclick="if(window.htpFvGlow){htpFvGlow();return false;}">' . htp_t('COM_HTPROTECT_HELP_APP_WHERE') . '</a></p>'
            . '</div>';

        // Dezenter, freiwilliger Spenden-Hinweis (nur auf der Hilfe-Seite, kein
        // Popup/Nagging, sekundaere Knoepfe) - die Erweiterung bleibt voll kostenlos.
        // Zwei Wege: Kaffeekasse (mit Rechnung/MwSt.) und PayPal (ohne Rechnung).
        // Steht bewusst ueber "Direkter Kontakt".
        echo '<div class="htpx-card htpx-card-wide htpx-donate" id="htp-donate">'
            . '<h4>' . htp_t('COM_HTPROTECT_HELP_DONATE_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_HELP_DONATE_TEXT') . '</p>'
            . '<div class="htpx-donate-opts">'
            . '<span class="htpx-donate-opt">'
            . '<a class="htpx-btn2" href="' . htp_esc(htp_t('COM_HTPROTECT_HELP_DONATE_URL')) . '" target="_blank" rel="noopener">'
            . htp_t('COM_HTPROTECT_HELP_DONATE_BTN') . '</a>'
            . '<span class="htpx-donate-sub htpx-dim">' . htp_t('COM_HTPROTECT_HELP_DONATE_KK_NOTE') . '</span>'
            . '</span>'
            . '<span class="htpx-donate-opt">'
            . '<a class="htpx-btn2 htpx-pp" href="https://website-bereinigung.de/donate" target="_blank" rel="noopener" aria-label="PayPal">'
            . '<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true" focusable="false"><path fill="#0070ba" d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.106zm14.146-14.42a3.35 3.35 0 0 0-.607-.541c-.013.076-.026.175-.041.254-.93 4.778-4.005 7.201-9.138 7.201h-2.19a.563.563 0 0 0-.556.479l-1.187 7.527h-.506l-.24 1.516a.56.56 0 0 0 .554.647h3.882c.46 0 .85-.334.922-.788.06-.26.76-4.852.816-5.09a.932.932 0 0 1 .923-.788h.58c3.76 0 6.705-1.528 7.565-5.946.36-1.847.174-3.388-.777-4.471z"/></svg>'
            . 'PayPal</a>'
            . '<span class="htpx-donate-sub htpx-dim">' . htp_t('COM_HTPROTECT_HELP_DONATE_PP_NOTE') . '</span>'
            . '</span>'
            . '</div>'
            // Fixer, dezenter 1-Zeiler unter den Spenden-Buttons: die kostenlose Art,
            // etwas zurueckzugeben - eine JED-Bewertung. Der Link (JED) sitzt mitten im
            // Satz, daher per %s-Platzhalter eingesetzt. Bewusst dauerhaft (kein X).
            . '<p class="htpx-donate-review">'
            . htp_t(
                'COM_HTPROTECT_HELP_DONATE_REVIEW',
                '<a href="' . htp_esc(HtpApp::JED_REVIEW_URL) . '" target="_blank" rel="noopener">'
                . htp_t('COM_HTPROTECT_HELP_DONATE_REVIEW_LINK') . '</a>'
            )
            . '</p>'
            . '</div>';

        // Direkter Kontakt - jetzt UNTER der Spenden-Karte.
        // Vorausgefuellter mailto-Link = 100 % zuverlaessig (nutzt das eigene
        // Mailprogramm, unabhaengig vom Mailversand des Servers).
        $mailSubject = $subject !== '' ? $subject : 'HTProtect-Support';
        $mailBody    = $message !== '' ? $message . "\n\n" . htp_t('COM_HTPROTECT_HELP_REPLYTO', $email) : '';
        $mailto      = 'mailto:' . HtpApp::SUPPORT_EMAIL . '?subject=' . rawurlencode($mailSubject) . '&body=' . rawurlencode($mailBody);

        echo '<div class="htpx-card htpx-card-wide"><h4>' . htp_t('COM_HTPROTECT_HELP_DIRECT_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_HELP_DIRECT_TEXT') . '</p>'
            . '<p><a class="htpx-btn" href="' . htp_esc($mailto) . '">' . htp_t('COM_HTPROTECT_HELP_DIRECT_BTN') . '</a> '
            . '<a class="htpx-btn2 htpx-chat-btn" href="https://website-bereinigung.de/chat" target="_blank" rel="noopener">' . htp_t('COM_HTPROTECT_HELP_CHAT_BTN') . '</a></p>'
            . '<ul class="htpx-syslist">'
            . '<li>' . htp_t('COM_HTPROTECT_HELP_EMAIL_LABEL') . ' <a href="mailto:' . htp_esc(HtpApp::SUPPORT_EMAIL) . '">' . htp_esc(HtpApp::SUPPORT_EMAIL) . '</a></li>'
            . '<li>' . htp_t('COM_HTPROTECT_HELP_WEB_LABEL') . ' <a href="https://website-bereinigung.de" target="_blank" rel="noopener">website-bereinigung.de</a></li>'
            . '</ul></div>';

        // Datenschutz-/Außenverbindungs-Hinweis - GANZ UNTEN, bewusst EINGEKLAPPT
        // (<details>), damit die UI nicht aufblaeht: standardmaessig nur eine Zeile,
        // bei Bedarf aufklappbar. Listet die einzigen ausgehenden Verbindungen +
        // betont, dass die Erweiterung auch ohne sie laeuft und keine Telemetrie sendet.
        echo '<div class="htpx-card htpx-card-wide">'
            . '<details class="htpx-details htpx-privacy"><summary class="htpx-dim">' . htp_t('COM_HTPROTECT_PRIVACY_TITLE') . '</summary>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_PRIVACY_INTRO') . '</p>'
            . '<ul class="htpx-syslist htpx-dim">'
            . '<li>' . htp_t('COM_HTPROTECT_PRIVACY_DEFS') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_PRIVACY_JOOMLA') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_PRIVACY_SWARM') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_PRIVACY_AUTOREPORT') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_PRIVACY_FEEDBACK') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_PRIVACY_HOLD') . '</li>'
            . '<li>' . htp_t('COM_HTPROTECT_PRIVACY_MAIL') . '</li>'
            . '</ul>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_PRIVACY_OFFLINE') . '</p>'
            . '</details></div>';

        // Intervall-Uebersicht: wann welcher Check laeuft - ausklappbar (Spoiler),
        // kompakt, leicht verstaendlich. Zwei-Spalten-Liste (Check | Intervall),
        // die auf Mobil sauber untereinander bricht (siehe .htpx-ivl CSS).
        echo '<div class="htpx-card htpx-card-wide">'
            . '<details class="htpx-details htpx-privacy"><summary class="htpx-dim">' . htp_t('COM_HTPROTECT_IVL_TITLE') . '</summary>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_IVL_INTRO') . '</p>'
            . '<dl class="htpx-ivl">'
            . '<dt>' . htp_t('COM_HTPROTECT_IVL_WAF') . '</dt><dd>' . htp_t('COM_HTPROTECT_IVL_WAF_T') . '</dd>'
            . '<dt>' . htp_t('COM_HTPROTECT_IVL_WATCH') . '</dt><dd>' . htp_t('COM_HTPROTECT_IVL_WATCH_T') . '</dd>'
            . '<dt>' . htp_t('COM_HTPROTECT_IVL_FEED') . '</dt><dd>' . htp_t('COM_HTPROTECT_IVL_FEED_T') . '</dd>'
            . '<dt>' . htp_t('COM_HTPROTECT_IVL_SEO') . '</dt><dd>' . htp_t('COM_HTPROTECT_IVL_SEO_T') . '</dd>'
            . '<dt>' . htp_t('COM_HTPROTECT_IVL_INTEGRITY') . '</dt><dd>' . htp_t('COM_HTPROTECT_IVL_INTEGRITY_T') . '</dd>'
            . '<dt>' . htp_t('COM_HTPROTECT_IVL_JVERSION') . '</dt><dd>' . htp_t('COM_HTPROTECT_IVL_JVERSION_T') . '</dd>'
            . '<dt>' . htp_t('COM_HTPROTECT_IVL_TMPCLEAN') . '</dt><dd>' . htp_t('COM_HTPROTECT_IVL_TMPCLEAN_T') . '</dd>'
            . '<dt>' . htp_t('COM_HTPROTECT_IVL_AUNOTIFY') . '</dt><dd>' . htp_t('COM_HTPROTECT_IVL_AUNOTIFY_T') . '</dd>'
            . '<dt>' . htp_t('COM_HTPROTECT_IVL_AUTOUP') . '</dt><dd>' . htp_t('COM_HTPROTECT_IVL_AUTOUP_T') . '</dd>'
            . '</dl>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_IVL_NOTE') . '</p>'
            . '</details></div>';

        // ── Deinstallations-Assistent: GANZ UNTEN, eingeklappt (nur eine Zeile im
        //    Normalfall). Entfernt beim Deinstallieren auch die bewusst belassenen
        //    Schutzdateien. Doppelt genutzt: Ziel der Uninstall-Interception (dann
        //    per #htp-uninstall aufgeklappt + Flash). Zustand rein datei-basiert
        //    ermittelt (kein HTTP): Original-Snapshot vorhanden? eigener Backend-Block?
        $unKind  = class_exists('HtpStorage') ? HtpStorage::originalKind() : 'unknown';
        $unOrig  = ($unKind === 'original');
        // Der Backend-Schutz-Block (Passwort MARK_BEGIN ODER Zugangs-String-Gate MARK_GATE_BEGIN)
        // liegt dort, wo HtpGuard ihn fuehrt: im SERVED Administrator (Public-Folder: public/administrator;
        // klassisch identisch zum Code-Admin). Beide Marker pruefen, Datei nur EINMAL lesen.
        $adminHt   = htp_web_admin_root() . '/.htaccess';
        $htContent = (class_exists('HtpGuard') && is_file($adminHt)) ? (string) @file_get_contents($adminHt) : '';
        $unAdmin   = $htContent !== ''
            && (strpos($htContent, HtpGuard::MARK_BEGIN) !== false || strpos($htContent, HtpGuard::MARK_GATE_BEGIN) !== false);

        // Public-Folder-Altlast: ein vor 2.7.0 im CODE-Administrator angelegter Block koennte dort
        // noch liegen (Migration) - zusaetzlich pruefen, damit der Assistent ihn ebenfalls erkennt.
        if (!$unAdmin && class_exists('HtpGuard') && function_exists('htp_is_public_folder') && htp_is_public_folder()) {
            $legacyHt = htp_admin_root() . '/.htaccess';
            $lc       = is_file($legacyHt) ? (string) @file_get_contents($legacyHt) : '';
            $unAdmin  = $lc !== ''
                && (strpos($lc, HtpGuard::MARK_BEGIN) !== false || strpos($lc, HtpGuard::MARK_GATE_BEGIN) !== false);
        }
        // Kommt man ueber die Uninstall-Interception (Klick in Erweiterungen->Verwalten)
        // hierher -> Assistent aufgeklappt + deutlicher Hinweis oben.
        $unOpen = (htp_get('htp_uninstall', '') === '1');

        echo '<div class="htpx-card htpx-card-wide" id="htp-uninstall">'
            . '<details class="htpx-details htpx-uninst"' . ($unOpen ? ' open' : '') . '>'
            . '<summary class="htpx-dim">' . htp_t('COM_HTPROTECT_UNINST_SUMMARY') . '</summary>'
            . ($unOpen ? '<p class="htpx-uninst-note">' . htp_t('COM_HTPROTECT_UNINST_INTERCEPT') . '</p>' : '')
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_UNINST_INTRO') . '</p>'
            . '<form method="post" action="' . self::url('help') . '" class="htpx-form">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="uninstall_go">'
            . '<label class="htpx-label">' . htp_t('COM_HTPROTECT_UNINST_MAIN') . '</label>'
            . '<div class="htpx-radios">';

        if ($unOrig) {
            echo '<label class="htpx-radio"><input type="radio" name="htp_un_main" value="original" checked><span>' . htp_t('COM_HTPROTECT_UNINST_MAIN_ORIG') . '</span></label>';
        }

        echo '<label class="htpx-radio"><input type="radio" name="htp_un_main" value="default"' . ($unOrig ? '' : ' checked') . '><span>' . htp_t('COM_HTPROTECT_UNINST_MAIN_DEF') . '</span></label>'
            . '<label class="htpx-radio"><input type="radio" name="htp_un_main" value="delete"><span>' . htp_t('COM_HTPROTECT_UNINST_MAIN_DEL') . '</span></label>'
            . '<label class="htpx-radio"><input type="radio" name="htp_un_main" value="leave"><span>' . htp_t('COM_HTPROTECT_UNINST_MAIN_KEEP') . '</span></label>'
            . '</div>';

        echo self::toggle('htp_un_uploads', 1, htp_t('COM_HTPROTECT_UNINST_UPLOADS'));

        if ($unAdmin) {
            echo self::toggle('htp_un_admin', 1, htp_t('COM_HTPROTECT_UNINST_ADMIN'));
        }

        echo '<p class="htpx-dim htpx-uninst-warn"><small>' . htp_t('COM_HTPROTECT_UNINST_WARN') . '</small></p>'
            . '<p><button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_UNINST_GO') . '</button> '
            . '<button type="submit" name="htp_un_skip" value="1" class="htpx-btn2">' . htp_t('COM_HTPROTECT_UNINST_PLAIN') . '</button></p>'
            . '</form></details></div>';

        // Platzsparender Changelog-Link als dezente Fusszeile GANZ UNTEN (keine Karte,
        // eine Zeile) - fuer Interessierte die Versionshistorie auf htprotect.org.
        // Sprache folgt der UI (de -> /changelog, en -> /en/changelog), wie docsLink().
        $clUrl = (htp_lang() === 'en') ? 'https://htprotect.org/en/changelog' : 'https://htprotect.org/changelog';
        echo '<p class="htpx-dim" style="text-align:center;margin:.7rem 0 0;font-size:.85rem">'
            . '<a href="' . htp_esc($clUrl) . '" target="_blank" rel="noopener">' . htp_t('COM_HTPROTECT_HELP_CHANGELOG') . '</a>'
            . '</p>';
    }

    // ─────────────────────────────────────────────────────────────────
    //  Bereich: Fernverwaltung (VERSTECKT - nicht im Menue). Nur per Deep-Link
    //  vom htprotect.app-Dashboard (?sec=connect&code=...) oder den dezenten
    //  Footer-Link erreichbar. Solange nicht gekoppelt, ist der Connector inert.
    // ─────────────────────────────────────────────────────────────────

    private static function secConnect($cfg, $data)
    {
        $st = class_exists('HtpLink') ? HtpLink::status() : array('paired' => false);

        echo '<h2 class="htpx-h">' . htp_t('COM_HTPROTECT_FV_TITLE') . '</h2>';
        echo '<div class="htpx-card htpx-card-wide" id="htp-connect">';

        if (!class_exists('HtpLink')) {
            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_FV_UNAVAILABLE') . '</p></div>';

            return;
        }

        if (!empty($st['paired'])) {
            echo '<p>' . self::badge('ok', htp_t('COM_HTPROTECT_FV_CONNECTED')) . '</p>';
            // Verbunden: die Dashboard-Adresse als klickbaren Link zur Anmeldung.
            $fvLogin = rtrim((string) $st['relay'], '/') . '/login';
            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_FV_RELAY') . ': '
                . '<a href="' . htp_esc($fvLogin) . '" target="_blank" rel="noopener">' . htp_esc($fvLogin) . '</a></p>';
            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_FV_SITEID') . ': <code>' . htp_esc($st['site_id']) . '</code></p>';
            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_FV_FPR') . ': <code>' . htp_esc($st['ws_fpr']) . '</code></p>';
            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_FV_CONNECTED_HINT') . '</p>';
            echo '<form method="post" action="' . self::url('connect') . '" class="htpx-form">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="fv_unpair">'
                . '<p><button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_FV_UNPAIR_BTN') . '</button></p>'
                . '</form>';
        } else {
            $code    = htp_cmd('code', '');
            $fvRelay = HtpLink::RELAY;
            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_FV_INTRO') . '</p>';
            // Statt eines read-only-Eingabefelds (die Adresse ist fest) ein klickbarer
            // Dashboard-Link. Die Adresse wird NICHT mehr als verstecktes Feld mitgeschickt:
            // sie ist eine Konstante (HtpLink::RELAY), die der Server ohnehin kennt - und eine
            // fremde URL in einem POST-Parameter loeste bei Hostern mit mod_security einen
            // Fehlalarm der OWASP-Regel 931130 aus (Antwort HTTP 406, Kopplung unmoeglich).
            echo '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_FV_RELAY') . ': '
                . '<a href="' . htp_esc($fvRelay) . '" target="_blank" rel="noopener">' . htp_esc($fvRelay) . '</a></p>';
            echo '<form method="post" action="' . self::url('connect') . '" class="htpx-form">'
                . self::tokenField()
                . '<input type="hidden" name="htp_task" value="fv_pair">'
                . '<div style="max-width:340px"><label class="htpx-label" for="htp_fv_code">' . htp_t('COM_HTPROTECT_FV_CODE') . '</label>'
                . '<input class="htpx-input" type="text" id="htp_fv_code" name="fv_code" value="' . htp_esc($code) . '" autocomplete="off" spellcheck="false"></div>'
                . '<p style="margin:.9rem 0 0"><button type="submit" class="htpx-btn">' . htp_t('COM_HTPROTECT_FV_PAIR_BTN') . '</button></p>'
                . '</form>';
            echo '<p class="htpx-dim" style="font-size:.85rem;margin:.95rem 0 0">' . htp_t('COM_HTPROTECT_FV_PRIVACY') . '</p>';
        }

        echo '</div>';

        // Info-Card „Was das Dashboard zusaetzlich bringt" - NUR wenn noch NICHT verbunden (Motivation zur
        // Kopplung; schon fuer eine einzelne Website sinnvoll). Nach dem Verbinden verschwindet sie.
        if (empty($st['paired'])) {
            // Literale Keys (kein dynamisches Zusammensetzen -> der langcheck-Scanner sieht jeden Key).
            $li = function ($t, $d) {
                return '<li><strong>' . htp_t($t) . '</strong> <span class="htpx-dim">' . htp_t($d) . '</span></li>';
            };
            echo '<div class="htpx-card htpx-card-wide htpx-fvben" id="htp-connect-benefits">'
                . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_FV_BENEFITS_TITLE') . '</h4>'
                . '<p class="htpx-dim htpx-fvben-intro">' . htp_t('COM_HTPROTECT_FV_BENEFITS_INTRO') . '</p>'
                . '<ul class="htpx-fvben-list">'
                . $li('COM_HTPROTECT_FV_BENEFITS_F1_T', 'COM_HTPROTECT_FV_BENEFITS_F1_D')
                . $li('COM_HTPROTECT_FV_BENEFITS_F10_T', 'COM_HTPROTECT_FV_BENEFITS_F10_D')
                . $li('COM_HTPROTECT_FV_BENEFITS_F2_T', 'COM_HTPROTECT_FV_BENEFITS_F2_D')
                . $li('COM_HTPROTECT_FV_BENEFITS_F3_T', 'COM_HTPROTECT_FV_BENEFITS_F3_D')
                . $li('COM_HTPROTECT_FV_BENEFITS_F4_T', 'COM_HTPROTECT_FV_BENEFITS_F4_D')
                . $li('COM_HTPROTECT_FV_BENEFITS_F5_T', 'COM_HTPROTECT_FV_BENEFITS_F5_D')
                . $li('COM_HTPROTECT_FV_BENEFITS_F6_T', 'COM_HTPROTECT_FV_BENEFITS_F6_D')
                . $li('COM_HTPROTECT_FV_BENEFITS_F7_T', 'COM_HTPROTECT_FV_BENEFITS_F7_D')
                . $li('COM_HTPROTECT_FV_BENEFITS_F8_T', 'COM_HTPROTECT_FV_BENEFITS_F8_D')
                . $li('COM_HTPROTECT_FV_BENEFITS_F9_T', 'COM_HTPROTECT_FV_BENEFITS_F9_D')
                . '</ul>'
                . '<p class="htpx-fvben-foot">' . htp_t('COM_HTPROTECT_FV_BENEFITS_FOOT') . '</p>'
                . '</div>';
        }
    }

    // ─────────────────────────────────────────────────────────────────
    //  Hilfsbausteine
    // ─────────────────────────────────────────────────────────────────

    private static function groupOpen($title)
    {
        echo '<section class="htpx-group"><h3 class="htpx-grouptitle">' . $title . '</h3><div class="htpx-groupbody">';
    }

    private static function groupClose()
    {
        echo '</div></section>';
    }

    private static function css()
    {
        ?>
<style>
.htpx{--bg:#0b0e15;--panel:#10131c;--panel2:#161a26;--line:#262b3a;--text:#f0f0f0;--dim:#b3b9c7;
  --acc:#34d27b;--accdark:#1f9c58;--err:#f0556b;--warn:#e8b341;--info:#5aa7e8;--r:10px;
  --code-bg:#070a10;--pre-bg:#070a10;--pre-text:#b9e8cd;--track:#2a3042;--nav-bg:#0e111a;
  --head-bg:linear-gradient(180deg,#131726,#10131c);--knob:#9aa1b5;
  --msg-ok-bg:#0c1f14;--msg-err-bg:#220d12;--msg-warn-bg:#221a08;--msg-info-bg:#0c1722;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:14px;line-height:1.55;color:var(--text)}
.htpx.htpx-light{--bg:#eef1f6;--panel:#ffffff;--panel2:#f5f7fb;--line:#d9dee7;--text:#111111;--dim:#555555;
  --acc:#1f9c58;--accdark:#178048;--err:#c0392b;--warn:#b7791f;--info:#1b6db3;
  --code-bg:#eef1f6;--pre-bg:#f6f8fb;--pre-text:#184d30;--track:#cfd6e1;--nav-bg:#f7f9fc;
  --head-bg:linear-gradient(180deg,#ffffff,#f3f6fa);--knob:#ffffff;
  --msg-ok-bg:#eaf8f0;--msg-err-bg:#fdecee;--msg-warn-bg:#fdf6e6;--msg-info-bg:#eaf3fc}
.htpx.htpx-light{box-shadow:0 1px 3px rgba(20,30,50,.08)}
/* Light Mode: auf gruenem Hintergrund weisse Schrift/Symbole statt dunkel */
.htpx.htpx-light a.htpx-nav-active,.htpx.htpx-light a.htpx-nav-active:hover{color:#fff}
.htpx.htpx-light .htpx-tg input:checked + .htpx-tgsl::after{background:#fff}
.htpx.htpx-light .htpx-btn,.htpx.htpx-light a.htpx-btn,.htpx.htpx-light a.htpx-jedrev-btn{color:#fff}
.htpx.htpx-light .htpx-btn:hover,.htpx.htpx-light a.htpx-btn:hover{color:#fff}
.htpx.htpx-light .htpx-edition{color:#fff}
.htpx *,.htpx *::before,.htpx *::after{box-sizing:border-box}
.htpx{display:block;max-width:1180px;margin:14px auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;overflow:hidden;color:var(--text)}
.htpx h1,.htpx h2,.htpx h3,.htpx h4,.htpx h5,.htpx h6{color:var(--text)}
.htpx a{color:var(--acc)}
.htpx code{background:var(--code-bg);color:var(--acc);padding:1px 6px;border-radius:4px;font-size:.86em;font-family:Consolas,Menlo,monospace}
.htpx-mono{font-family:Consolas,Menlo,monospace;font-size:.86em;word-break:break-all}
.htpx-dim{color:var(--dim)}
.htpx-shellhead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 22px;border-bottom:1px solid var(--line);background:var(--head-bg)}
.htpx-updbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:9px 18px;background:var(--msg-info-bg);border-bottom:1px solid var(--line);color:var(--text);font-size:.9rem;line-height:1.4}
.htpx-updbar-ico{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:var(--info);color:#fff;font-weight:700;flex:0 0 auto;font-size:.8rem}
.htpx-updbar a.htpx-updbar-btn{margin-left:auto;background:var(--info);color:#fff;padding:5px 15px;border-radius:6px;text-decoration:none;font-weight:600;white-space:nowrap}
.htpx-updbar a.htpx-updbar-btn:hover{filter:brightness(.92);text-decoration:none;color:#fff}
.htpx-logo{display:flex;align-items:center;gap:11px}
.htpx-logomark{display:inline-flex;flex:0 0 auto}
.htpx-logomark-svg{display:block}
.htpx-logowrap{display:flex;flex-direction:column;line-height:1.08;min-width:0}
.htpx-logorow{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.htpx-logotext{font-size:1.2rem;font-weight:800;letter-spacing:.2px}
.htpx-lt-ht{color:var(--text)}
.htpx-lt-pr{color:var(--acc)}
.htpx-logotag{font-size:.76rem;color:var(--dim);letter-spacing:.2px;margin-top:3px}
.htpx-edition{font-size:.68rem;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:#08120c;background:var(--acc);border-radius:99px;padding:3px 10px;white-space:nowrap}
.htpx-headmeta{color:var(--dim);font-size:.8rem}
.htpx-headmeta a{color:var(--dim);text-decoration:none}
.htpx-headmeta a:hover{color:var(--acc)}
.htpx-doclink{text-decoration:none;white-space:nowrap}
.htpx-doclink .htpx-docicon{width:auto;height:1.3em;vertical-align:-.3em;margin-right:3px;transition:transform .15s}
.htpx-doclink:hover .htpx-docicon{transform:scale(1.08)}
/* Der Doku-Link hat sein eigenes Buch-Icon -> das externe-Link-Icon des Joomla-Backend-Templates (Pseudo-Element auf target=_blank) NUR hier unterdruecken. htprotect.org behaelt seins. */
.htpx-headmeta .htpx-doclink::before,.htpx-headmeta .htpx-doclink::after{content:none !important;display:none !important;background:none !important}
/* Version + URL: Desktop in der Kopfzeile (htpx-shellhead), Mobil als schmale
   Zeile UEBER der Karte (.htpx-abovecard). Desktop unveraendert. */
.htpx-abovecard{display:none}
@media (max-width:680px){
  /* .htpx.htpx-abovecard (Spezifitaet 0,2,0) schlaegt die .htpx.htpx-light-Schattenkante - keine Umrandung.
     Negativer oberer Rand zieht die Zeile in das Joomla-Content-Padding hinein, damit
     der Abstand oben ~ dem Abstand unten (#htpx-root margin-top) entspricht. Nur mobil. */
  .htpx.htpx-abovecard{display:block;background:none;border:none;border-radius:0;box-shadow:none;overflow:visible;margin:-8px auto 0;padding:0 6px;max-width:none}
  .htpx-abovecard .htpx-headmeta{text-align:right;font-size:.74rem;line-height:1.3}
  #htpx-root{margin-top:7px} /* halber Abstand unter der Versionszeile */
  /* J3 (Isis): die leere ".btn-subhead"-Leiste ist jetzt ausgeblendet -> der
     Abstand oben fehlt; die Versionszeile 15px tiefer setzen (statt -8px nun 7px). */
  .htpx-legacy.htpx-abovecard{margin-top:7px}
  .htpx-shellhead{padding:12px 16px}
  .htpx-shellhead .htpx-headmeta{display:none} /* in der Karte ausblenden - sitzt jetzt darueber */
  .htpx-logo{gap:9px}
  .htpx-logotag{display:none} /* Tagline auf schmalen Screens weglassen */
  .htpx-edition{letter-spacing:1px}
}
.htpx-body{display:flex;min-height:420px}
.htpx-nav{width:208px;flex:0 0 208px;border-right:1px solid var(--line);padding:14px 10px;display:flex;flex-direction:column;gap:4px;background:var(--nav-bg)}
.htpx a.htpx-navlink{display:flex;align-items:center;gap:9px;padding:9px 12px;border-radius:8px;color:var(--dim);text-decoration:none;font-weight:500}
.htpx a.htpx-navlink:hover{color:var(--text);background:var(--panel2);text-decoration:none}
.htpx a.htpx-nav-active,.htpx a.htpx-nav-active:hover{color:#08120c;background:var(--acc)}
.htpx-main{flex:1;padding:22px 26px;min-width:0}
.htpx-shellfoot{padding:12px 22px;border-top:1px solid var(--line);color:var(--dim);font-size:.78rem;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.htpx-footdonate{display:inline-flex;align-items:center;gap:5px;color:var(--dim);text-decoration:none;border:1px solid var(--line);border-radius:999px;padding:3px 11px;transition:color .15s,border-color .15s}
/* Mobil: Footer bricht um (Text oben, Button darunter) - Spendenbutton dann
   nach rechts unten ausrichten (margin-left:auto schiebt ihn in seiner Zeile
   nach rechts; auf Desktop sorgt bereits justify-content:space-between dafuer). */
@media (max-width:680px){.htpx-footdonate{margin-left:auto}}
.htpx-footdonate svg{color:#b02a37;opacity:.75;transition:opacity .15s,transform .15s}
.htpx-footdonate:hover,.htpx-footdonate:focus{color:var(--acc);border-color:rgba(255,255,255,.16);text-decoration:none}
.htpx.htpx-light .htpx-footdonate:hover,.htpx.htpx-light .htpx-footdonate:focus{border-color:rgba(128,138,158,.45)}
.htpx-footdonate:hover svg{opacity:1;transform:scale(1.25)}
/* Passwort-Feld mit Auge-Knopf + Generator-Leiste */
.htpx-pwwrap{position:relative;display:flex;align-items:center}
.htpx-pwwrap .htpx-input{flex:1;padding-right:38px}
.htpx-pwbtn{position:absolute;right:6px;top:50%;transform:translateY(-50%);background:none;border:0;padding:4px;cursor:pointer;color:var(--dim);line-height:0;border-radius:6px;transition:color .15s,background .15s}
.htpx-pwbtn:hover{color:var(--acc);background:rgba(120,130,150,.12)}
.htpx-pwgen{grid-column:1/-1;display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:4px}
.htpx-pwgen-btn,.htpx-pwcopy-btn{display:inline-flex;align-items:center;gap:5px}
.htpx-pwgen-btn svg{transition:transform .4s cubic-bezier(.34,1.56,.64,1)}
.htpx-pwgen-btn:hover svg{transform:rotate(180deg)}
.htpx-pwcopy-btn:disabled{opacity:.5;cursor:not-allowed}
.htpx-pwcopy-btn.htpx-ok,.htpx-pwgen-btn.htpx-ok{border-color:#1f9d57;color:#1f9d57;background:rgba(31,157,87,.08)}
.htpx-pwgen-hint{flex:1 1 100%;font-size:.78rem}
.htpx-h{font-size:1.2rem;font-weight:700;margin:4px 0 16px;color:var(--text)}
.htpx-sub{font-size:.95rem;font-weight:600;margin:18px 0 8px}
.htpx-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px}
.htpx-card{background:var(--panel2);border:1px solid var(--line);border-radius:var(--r);padding:16px 18px;margin:0 0 14px}
.htpx-card h4{margin:0 0 10px;font-size:.95rem}
.htpx-card-wide{width:100%}
.htpx-card-danger{border-color:rgba(240,85,107,.35)}
.htpx-forumnote{background:var(--msg-info-bg);border:1px solid var(--line);border-left:3px solid var(--info);border-radius:var(--r);padding:11px 14px;margin:2px 0 14px;font-size:.9rem;line-height:1.55;color:var(--text)}
.htpx-forumnote strong{color:var(--info)}
.htpx-forumnote a{color:var(--info);font-weight:600;text-decoration:none}
.htpx-forumnote a:hover{text-decoration:underline}
.htpx-hero{position:relative;background:linear-gradient(135deg,rgba(52,210,123,.1),rgba(52,210,123,.02));border:1px solid rgba(52,210,123,.35);border-radius:var(--r);padding:18px 20px;margin:0 0 18px}
.htpx-hero-x{position:absolute;top:8px;right:10px;margin:0;line-height:1}
.htpx-x{background:none;border:0;color:var(--dim);font-size:1.45rem;line-height:1;cursor:pointer;padding:0 4px;opacity:.6;transition:opacity .15s,color .15s}
.htpx-x:hover{opacity:1;color:var(--text)}
.htpx-hero h3{margin:0 0 8px;font-size:1.05rem;color:var(--acc)}
.htpx-hero p{margin:0 0 12px;color:var(--dim)}
.htpx-msg{padding:10px 14px;border-radius:8px;margin:0 0 12px;font-size:.88rem;border:1px solid}
.htpx-msg-ok{background:var(--msg-ok-bg);border-color:rgba(52,210,123,.4);color:#7fe3ab}
.htpx-msg-err{background:var(--msg-err-bg);border-color:rgba(240,85,107,.4);color:#f08a99}
.htpx-msg-warn{background:var(--msg-warn-bg);border-color:rgba(232,179,65,.4);color:#e8c87a}
.htpx-msg-info{background:var(--msg-info-bg);border-color:rgba(90,167,232,.4);color:#8cc3ef}
.htpx.htpx-light .htpx-msg-ok{color:#176c3a}
.htpx.htpx-light .htpx-msg-err{color:#a52a1a}
.htpx.htpx-light .htpx-msg-warn{color:#8a5a08}
.htpx.htpx-light .htpx-msg-info{color:#155a91}
.htpx-badge{display:inline-block;font-size:.72rem;font-weight:700;border-radius:99px;padding:2px 10px;letter-spacing:.3px}
.htpx-badge-ok{background:rgba(52,210,123,.16);color:var(--acc)}
.htpx-badge-err{background:rgba(240,85,107,.16);color:var(--err)}
.htpx-badge-warn{background:rgba(232,179,65,.16);color:var(--warn)}
.htpx-badge-info{background:rgba(90,167,232,.16);color:var(--info)}
.htpx-btn,.htpx-btn2{display:inline-block;border:none;border-radius:8px;padding:9px 18px;font-weight:700;font-size:.88rem;cursor:pointer;text-decoration:none;font-family:inherit}
.htpx-btn,.htpx a.htpx-btn{background:var(--acc);color:#08120c}
.htpx-btn:hover,.htpx a.htpx-btn:hover{background:#2bbf6e;color:#08120c;text-decoration:none}
.htpx-btn2{background:transparent;color:var(--acc);border:1px solid rgba(52,210,123,.5)}
.htpx-btn2:hover{background:rgba(52,210,123,.12);color:var(--acc);text-decoration:none}
.htpx-btn-danger{background:var(--err);color:#fff;border-color:transparent}
.htpx-btn2.htpx-btn-danger{background:transparent;color:var(--err);border:1px solid rgba(240,85,107,.5)}
.htpx-btn2.htpx-btn-danger:hover{background:rgba(240,85,107,.12)}
/* Spezifitaet .htpx a.htpx-appwhere (0,2,1) MUSS die generische Linkfarbe .htpx a{color:acc} (0,1,1) schlagen - sonst wird der Hinweis gruen statt grau. */
.htpx a.htpx-appwhere{margin-left:.7rem;color:var(--dim);font-size:.82rem;text-decoration:none}
/* Handy: "Wo verbinde ich diese Seite?" auf eine eigene Zeile unter den Button, mit Abstand und mittig UNTER dem Button (klebt sonst am Button). Farbe/Groesse unveraendert grau. Desktop bleibt inline daneben.
   width:fit-content laesst den Absatz auf die Button-Breite schrumpfen (Button ist breiter als der Link) und links stehen -> der zentrierte Link sitzt dann genau mittig unter dem Button. */
@media (max-width:680px){.htpx-appcta{width:fit-content;max-width:100%}.htpx a.htpx-appwhere{display:block;margin-left:0;margin-top:.7rem;text-align:center}}
/* Ausklappbarer "Weitere Zugaenge"-Bereich: dezenter Trenner oben, damit er zum Backend-Zugang
   gehoert, aber KEINE eigene Card wirkt. hidden (Standard) blendet ihn per display:none aus. */
.htpx-guardmore{margin-top:16px;padding-top:16px;border-top:1px solid var(--line,rgba(128,128,128,.18))}
.htpx-btn-s{padding:4px 10px;font-size:.78rem;font-weight:600}
/* Auffaellige, pulsierende Hervorhebung fuer die stark empfohlene "HTProtect automatisch
   aktuell halten"-Aktivierung (analog zum Schwarm-Schutz-Puls) - lenkt den Blick auf den
   Knopf. Gruener Glow + dezent wachsender Ring; beruhigt sich bei Hover; respektiert
   reduzierte Bewegung (Barrierefreiheit). */
.htpx-cta-pulse{animation:htpxCtaPulse 2.2s ease-in-out infinite}
@keyframes htpxCtaPulse{0%,100%{box-shadow:0 2px 7px rgba(52,210,123,.35),0 0 0 0 rgba(52,210,123,.4)}50%{box-shadow:0 3px 15px rgba(52,210,123,.58),0 0 0 4px rgba(52,210,123,.14)}}
.htpx-cta-pulse:hover{animation:none;box-shadow:0 3px 15px rgba(52,210,123,.52)}
@media (prefers-reduced-motion:reduce){.htpx-cta-pulse{animation:none}}
/* Promo-CTA "HTProtect automatisch aktuell halten": Desktop = kompakt rechts; am Handy
   (<=680px) volle Breite + umbrechender, zentrierter Text, damit der lange
   Button-Beschriftungstext bei schmalen Geraeten (bis ~300px) NICHT aus dem Rahmen laeuft. */
.htpx-selfup-cta{flex:0 0 auto;margin:0}
@media (max-width:680px){
  .htpx-selfup-cta{flex:1 1 100%;margin-top:4px}
  .htpx-selfup-cta .htpx-btn{display:block;width:100%;text-align:center;white-space:normal;line-height:1.3}
}
.htpx-label{display:block;font-size:.8rem;font-weight:600;color:var(--dim);margin:10px 0 4px}
.htpx-input,.htpx-textarea{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:8px;color:var(--text);padding:8px 10px;font-size:.88rem;font-family:inherit;outline:none}
.htpx-input:focus,.htpx-textarea:focus{border-color:var(--acc)}
/* Read-only (automatisch erkannter) Wert: theme-faehig statt fest hell, mit dezentem Schloss */
.htpx-input-ro{background-color:var(--panel2);color:var(--dim);cursor:default;padding-left:32px;background-repeat:no-repeat;background-position:10px center;background-size:14px 14px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b91a5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='5' y='11' width='14' height='9' rx='2'/%3E%3Cpath d='M8 11V8a4 4 0 0 1 8 0v3'/%3E%3C/svg%3E")}
.htpx-input-ro:focus{border-color:var(--line)}
.htpx-input-s{width:90px}
.htpx-progress{margin:14px 0 4px}
.htpx-progress-info{display:flex;justify-content:space-between;gap:12px;font-size:.82rem;color:var(--dim);margin-bottom:6px}
.htpx-progress-info span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.htpx-progress-track{height:10px;border-radius:99px;background:var(--track);overflow:hidden}
.htpx-progress-bar{height:100%;width:0;background:var(--acc);border-radius:99px;transition:width .25s}
.htpx-textarea{resize:vertical}
.htpx select.htpx-input{appearance:auto;-webkit-appearance:auto}
.htpx-grid2{display:grid;grid-template-columns:1fr 1fr;gap:6px 16px}
.htpx-vstack{display:grid;grid-template-columns:1fr;gap:14px}
.htpx-inline{display:flex;gap:10px;align-items:center}
/* URL-pruefen-Knopf: am Desktop einzeilig (nicht schrumpfen, kein Umbruch) -
   das breite Eingabefeld quetscht ihn sonst zweizeilig. Mobil bewusst NICHT
   (sonst wuerde das Eingabefeld zu schmal) -> dort darf er zweizeilig bleiben. */
@media (min-width:681px){.htpx-urltest-btn{white-space:nowrap;flex:0 0 auto}}
/* Mobil stehen die zwei Kontakt-Knoepfe gestapelt -> dem Live-Chat-Knopf etwas
   Luft nach oben geben. Desktop (nebeneinander) bleibt unveraendert. */
@media (max-width:680px){.htpx-chat-btn{margin-top:8px}}
.htpx-group{border:1px solid var(--line);border-radius:var(--r);margin:0 0 16px;background:var(--panel2);overflow:hidden}
.htpx-grouptitle{margin:0;padding:11px 18px;font-size:.86rem;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:var(--acc);background:rgba(52,210,123,.06);border-bottom:1px solid var(--line)}
.htpx-groupbody{padding:14px 18px}
.htpx-tg{display:flex;align-items:flex-start;gap:10px;padding:8px 0;cursor:pointer}
.htpx-tg input{position:absolute;opacity:0;width:0;height:0}
.htpx-tgsl{flex:0 0 38px;width:38px;height:22px;border-radius:99px;background:var(--track);position:relative;transition:background .15s;margin-top:1px}
.htpx-tgsl::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--knob);transition:all .15s;box-shadow:0 1px 2px rgba(0,0,0,.2)}
.htpx-tg input:checked + .htpx-tgsl{background:var(--acc)}
.htpx-tg input:checked + .htpx-tgsl::after{left:19px;background:#08120c}
.htpx-tgtext{font-size:.9rem;color:var(--text)}
.htpx-tgtext small{display:block;color:var(--dim);font-size:.78rem}
.htpx-tg-hh input:checked ~ .htpx-tgtext small{display:none}
.htpx-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:18px 0;padding:14px 16px;background:var(--panel2);border:1px dashed var(--line);border-radius:var(--r)}
.htpx-actions form{display:inline-flex;margin:0}
.htpx-pre{background:var(--pre-bg);border:1px solid var(--line);border-radius:8px;color:var(--pre-text);padding:14px;font-family:Consolas,Menlo,monospace;font-size:.78rem;line-height:1.5;overflow:auto;max-height:480px;white-space:pre}
.htpx-table{width:100%;border-collapse:collapse;font-size:.86rem}
.htpx-table th{text-align:left;color:var(--dim);font-weight:600;padding:8px 10px;border-bottom:1px solid var(--line);font-size:.78rem;text-transform:uppercase;letter-spacing:.4px}
.htpx-table td{padding:8px 10px;border-bottom:1px solid rgba(38,43,58,.6);vertical-align:top}
.htpx-rowactions{display:flex;gap:6px;flex-wrap:wrap}
.htpx-rowactions form{display:inline;margin:0}
.htpx-syslist{margin:0;padding:0;list-style:none}
.htpx-syslist li{padding:3px 0;border-bottom:1px dotted rgba(38,43,58,.6)}
.htpx-details{margin:12px 0}
.htpx-details summary{cursor:pointer;color:var(--acc);font-weight:600;font-size:.88rem}
.htpx-radios{display:flex;flex-direction:column;gap:7px;margin:5px 0 12px}
.htpx-radio{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:.9rem}
.htpx-radio input{margin:0;flex:0 0 auto}
.htpx-uninst-warn{margin:8px 0 10px}
.htpx-uninst-note{background:var(--msg-warn-bg);border:1px solid var(--warn);border-radius:8px;padding:9px 13px;margin:2px 0 12px;font-weight:600;font-size:.9rem}
.htpx-privacy[open] summary{margin-bottom:12px}
.htpx-privacy ul{margin:10px 0}.htpx-privacy li{margin:3px 0}
/* Intervall-Uebersicht: 2 Spalten (Check | Intervall), bricht mobil unter. */
.htpx-ivl{display:grid;grid-template-columns:auto 1fr;gap:6px 18px;margin:10px 0 4px;align-items:baseline}
.htpx-ivl dt{margin:0;font-weight:600}
.htpx-ivl dd{margin:0;color:var(--dim)}
@media (max-width:560px){.htpx-ivl{grid-template-columns:1fr;gap:2px 0}.htpx-ivl dd{margin:0 0 8px;padding-left:0}}
/* tmp-Ausschlussliste: "(Ausschlussliste)" inline neben dem Schalter, Liste per Klick darunter (kein JS) */
.htpx-tmpkeep-row{display:flex;align-items:center;gap:2px 12px;flex-wrap:wrap}
.htpx-tmpkeep-row > .htpx-tg{align-items:center}
.htpx-tmpkeep{margin:0}
.htpx-tmpkeep>summary{cursor:pointer;color:var(--dim);font-size:.85rem;list-style:none}
.htpx-tmpkeep>summary::-webkit-details-marker{display:none}
.htpx-tmpkeep>summary:hover{color:var(--acc)}
/* J3 (Isis/Bootstrap-2): die Ausschlussliste-Summary als kompakte, gerahmte Pille
   - wie sie J5/6 (Atum) von Haus aus zeigt - UND per inline-flex sauber auf einer
   Hoehe mit dem Schalter-Text (statt nacktem, leicht abgesacktem Text). Nur fuer
   .htpx-legacy -> J5/6 bleibt komplett unangetastet. */
.htpx-legacy .htpx-tmpkeep>summary{display:inline-flex;align-items:center;line-height:1.1;border:1px solid var(--line);border-radius:8px;padding:6px 14px;background:transparent}
.htpx-legacy .htpx-tmpkeep>summary:hover{border-color:var(--acc)}
.htpx-tmpkeep-body{flex-basis:100%;display:none}
.htpx-tmpkeep[open] ~ .htpx-tmpkeep-body{display:block}
/* Schwarm-Schutz: bewusst hintergruendiger Experten-Aufklapp (null Footprint, solange zu) */
/* Schwarm-Schutz: bewusst AUFFAELLIGER, aber edler Honig-/Bienen-Banner (solides warmes
   Amber, dunkelbraune Schrift wie Bienenstreifen) mit sanftem warmem Puls-Glow - das
   Feature lebt von Community-Meldungen, darf also nicht uebersehen werden. Faellt durch
   den warmen Ton auf hellem UND dunklem Theme auf; hoert beim Oeffnen auf zu pulsen. */
.htpx-swarm{margin:18px 0;border:1px solid #c47f0a;border-radius:12px;overflow:hidden;box-shadow:0 2px 9px rgba(214,140,16,.18);animation:htpxSwarmPulse 2.8s ease-in-out infinite}
/* J4+ (Atum) gibt <details> ein Innen-Padding -> das Amber-Summary saesse eingerueckt,
   heller Rahmen drumherum. Nur ab J4 entfernen; J2.5/3 (legacy) bleibt unveraendert. */
.htpx:not(.htpx-legacy) .htpx-swarm{padding:0}
.htpx-swarm[open]{animation:none}
@keyframes htpxSwarmPulse{0%,100%{box-shadow:0 2px 8px rgba(214,140,16,.16)}50%{box-shadow:0 2px 16px rgba(245,180,40,.36)}}
@media (prefers-reduced-motion:reduce){.htpx-swarm{animation:none}}
.htpx-swarm>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:11px;padding:12px 18px;font-weight:600;font-size:.9rem;color:#3a2606;background:#e8a31a;transition:filter .15s}
.htpx-swarm>summary::-webkit-details-marker{display:none}
.htpx-swarm>summary::marker{content:""}
.htpx-swarm>summary:hover{filter:brightness(1.05)}
.htpx-swarm-bee{font-size:1.9em;line-height:1;flex:0 0 auto;filter:drop-shadow(0 1px 1px rgba(0,0,0,.25))}
.htpx-swarm>summary::after{content:"\25BE";margin-left:auto;font-size:1.2em;opacity:.85;transition:transform .2s}
.htpx-swarm[open]>summary::after{transform:rotate(180deg)}
.htpx-swarm-body{padding:14px 18px;background:var(--panel2)}
.htpx-swarm-note{margin:0 0 8px;font-size:.82rem}
.htpx-swarm-stat{margin:10px 0 0;font-size:.8rem}
/* Schwarm-Dank-Banner (geschlossene Rueckkopplung) - feierlich, bleibt bis zum × */
.htpx-swarm-thanks{display:flex;align-items:center;gap:12px;margin:0 0 14px;padding:11px 14px;
  border-radius:var(--r);font-size:.9rem;line-height:1.4;
  background:rgba(52,210,123,.12);border:1px solid rgba(52,210,123,.5);color:var(--acc)}
.htpx-swarm-thanks-msg{flex:1 1 auto}
.htpx-swarm-thanks-x{flex:0 0 auto;background:transparent;border:0;color:var(--acc);
  font-size:1.3rem;line-height:1;cursor:pointer;padding:1px 7px;border-radius:6px;opacity:.65}
.htpx-swarm-thanks-x:hover,.htpx-swarm-thanks-x:focus{opacity:1;background:rgba(52,210,123,.18)}
.htpx-jedrev{display:flex;align-items:center;gap:13px;margin:0 0 16px;padding:12px 15px;border-radius:var(--r);
  background:linear-gradient(90deg,rgba(52,210,123,.15),rgba(52,210,123,.05));border:1px solid rgba(52,210,123,.45);color:var(--text)}
.htpx-jedrev-heart{flex:0 0 auto;font-size:1.35rem;line-height:1}
.htpx-jedrev-txt{flex:1 1 auto;display:flex;flex-direction:column;gap:1px;min-width:0}
.htpx-jedrev-txt strong{font-size:.95rem}
.htpx-jedrev-txt span{font-size:.83rem;color:var(--dim)}
.htpx a.htpx-jedrev-btn{flex:0 0 auto;background:var(--acc);color:#08120c;font-weight:700;font-size:.85rem;text-decoration:none;padding:8px 16px;border-radius:8px;white-space:nowrap}
.htpx a.htpx-jedrev-btn:hover{background:var(--accdark);color:#fff}
.htpx-jedrev-x{flex:0 0 auto;background:transparent;border:0;color:var(--dim);font-size:1.3rem;line-height:1;cursor:pointer;padding:1px 6px;border-radius:6px;opacity:.6}
.htpx-jedrev-x:hover,.htpx-jedrev-x:focus{opacity:1;color:var(--text)}
@media(max-width:560px){.htpx-jedrev{flex-wrap:wrap}.htpx-jedrev-txt{flex-basis:70%}.htpx a.htpx-jedrev-btn{order:3;flex:1 1 auto;text-align:center}}
.htpx-fvn{display:flex;align-items:center;gap:13px;margin:0 0 16px;padding:12px 15px;border-radius:var(--r);background:linear-gradient(90deg,rgba(52,210,123,.15),rgba(52,210,123,.05));border:1px solid rgba(52,210,123,.45);color:var(--text)}
.htpx-fvn-icon{flex:0 0 auto;font-size:1.35rem;line-height:1}
.htpx-fvn-txt{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}
.htpx-fvn-txt strong{font-size:.98rem}
.htpx-fvn-txt span{font-size:.85rem;color:var(--dim);line-height:1.45}
.htpx-fvn-btn{flex:0 0 auto;background:var(--acc);color:#08120c;font-weight:700;font-size:.85rem;border:0;cursor:pointer;padding:9px 17px;border-radius:8px;white-space:nowrap;font-family:inherit}
.htpx-fvn-btn:hover{background:#2bbf6e}
.htpx.htpx-light .htpx-fvn-btn{color:#fff}
.htpx-fvn-x{flex:0 0 auto;background:transparent;border:0;color:var(--dim);font-size:1.3rem;line-height:1;cursor:pointer;padding:1px 6px;border-radius:6px;opacity:.6}
.htpx-fvn-x:hover,.htpx-fvn-x:focus{opacity:1;color:var(--text)}
@media(max-width:560px){.htpx-fvn{flex-wrap:wrap}.htpx-fvn-txt{flex-basis:70%}.htpx-fvn-btn{order:3;flex:1 1 auto;text-align:center}}
.htpx-lpn{display:flex;align-items:center;gap:13px;margin:0 0 16px;padding:12px 15px;border-radius:var(--r);background:linear-gradient(90deg,rgba(47,116,230,.16),rgba(47,116,230,.05));border:1px solid rgba(47,116,230,.5);color:var(--text)}
.htpx-lpn-icon{flex:0 0 auto;font-size:1.35rem;line-height:1}
.htpx-lpn-txt{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}
.htpx-lpn-txt span{font-size:.9rem;color:var(--text);line-height:1.45}
.htpx-lpn-btn{flex:0 0 auto;background:#2f74e6;color:#fff;font-weight:700;font-size:.85rem;border:0;cursor:pointer;padding:9px 17px;border-radius:8px;white-space:nowrap;font-family:inherit}
.htpx-lpn-btn:hover{background:#2864cc}
.htpx-lpn-x{flex:0 0 auto;background:transparent;border:0;color:var(--dim);font-size:1.3rem;line-height:1;cursor:pointer;padding:1px 6px;border-radius:6px;opacity:.6}
.htpx-lpn-x:hover,.htpx-lpn-x:focus{opacity:1;color:var(--text)}
@media(max-width:560px){.htpx-lpn{flex-wrap:wrap}.htpx-lpn-txt{flex-basis:70%}.htpx-lpn-btn{order:3;flex:1 1 auto;text-align:center}}
.htpx-gzn{display:flex;align-items:center;gap:13px;margin:0 0 16px;padding:12px 15px;border-radius:var(--r);background:linear-gradient(90deg,rgba(232,179,65,.15),rgba(232,179,65,.05));border:1px solid rgba(232,179,65,.45);color:var(--text)}
.htpx-gzn-icon{flex:0 0 auto;font-size:1.35rem;line-height:1}
.htpx-gzn-txt{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}
.htpx-gzn-txt strong{font-size:.98rem}
.htpx-gzn-txt span{font-size:.85rem;color:var(--dim);line-height:1.45}
.htpx-gzn-btn{flex:0 0 auto;background:var(--warn);color:#1a1205;font-weight:700;font-size:.85rem;border:0;cursor:pointer;padding:9px 17px;border-radius:8px;white-space:nowrap;font-family:inherit}
.htpx-gzn-btn:hover{background:#d9a531}
.htpx.htpx-light .htpx-gzn-btn{color:#fff}
.htpx-gzn-x{flex:0 0 auto;background:transparent;border:0;color:var(--dim);font-size:1.3rem;line-height:1;cursor:pointer;padding:1px 6px;border-radius:6px;opacity:.6}
.htpx-gzn-x:hover,.htpx-gzn-x:focus{opacity:1;color:var(--text)}
@media(max-width:560px){.htpx-gzn{flex-wrap:wrap}.htpx-gzn-txt{flex-basis:70%}.htpx-gzn-btn{order:3;flex:1 1 auto;text-align:center}}
@keyframes htpxFvFlash{0%,100%{opacity:.6;background:transparent;box-shadow:none;color:var(--dim)}15%,55%{opacity:1;background:var(--acc);color:#08120c;box-shadow:0 0 0 5px rgba(52,210,123,.35)}}
@keyframes htpxFvFlashLight{0%,100%{opacity:.6;background:transparent;box-shadow:none;color:var(--dim)}15%,55%{opacity:1;background:var(--acc);color:#fff;box-shadow:0 0 0 5px rgba(52,210,123,.35)}}
#htp-fv-footer.htpx-fv-flash{animation:htpxFvFlash 1.4s ease-in-out 3;border-radius:7px;padding:3px 9px;font-weight:700;font-size:.8rem!important;opacity:1!important;text-decoration:none}
.htpx.htpx-light #htp-fv-footer.htpx-fv-flash{animation-name:htpxFvFlashLight}
/* Fernsteuerungs-Icon (Header, NUR Icon): exakt wie das Doku-Icon in der Zeile ausgerichtet
   (inline + vertical-align ueber den Wrapper). Blickfang beim Klick auf "Ansehen"/"Wo verbinde
   ich diese Seite?": Sonar-Ping (passt zu den Funkwellen) + Icon-Scale mit gruenem Glow. */
.htpx-fvhead{position:relative}
/* Waehrend des Sonar-Pings (nur transient, per htpFvGlow ausgeloest) den Icon-Link ueber
   Joomlas sticky Titelbalken heben - sonst schneidet der Balken die aufsteigenden Ringe am
   Handy oben ab (die Meta-Zeile sitzt in .htpx-abovecard = overflow:visible, direkt unter dem
   Balken). NUR im Ping-Zustand -> im Ruhezustand normale Stapelung, kein Scroll-Nebeneffekt
   (Icon klebt nicht ueber dem Balken). 1050 schlaegt die Joomla-Admin-Kopf-/Subhead-z-indices,
   bleibt unter Modals. */
.htpx-fvhead.htpx-fv-glow{z-index:1050}
.htpx-fvwrap{position:relative;display:inline-block;line-height:0;vertical-align:-.3em}
.htpx-fvwrap .htpx-docicon{margin-right:0;vertical-align:0;display:block;height:1.35em}
/* Dark Mode: der Verlauf laeuft unten ins dunkle Gruen + die aeusseren Wellen sind blass -> auf
   dunklem Kopf sinkt das Icon ab. Nur hier ganz minimal anheben (Look bleibt, Light unveraendert). */
.htpx:not(.htpx-light) .htpx-fvwrap{filter:brightness(1.13) saturate(1.06)}
.htpx-fvhead.htpx-fv-glow .htpx-docicon{animation:htpxFvIcon .8s ease-in-out 3;transform-origin:center}
@keyframes htpxFvIcon{0%,100%{transform:scale(1);filter:none}35%{transform:scale(1.5);filter:drop-shadow(0 0 6px rgba(62,224,127,.95)) drop-shadow(0 0 13px rgba(62,224,127,.55))}70%{transform:scale(1.14);filter:drop-shadow(0 0 3px rgba(62,224,127,.4))}}
.htpx-fvhead.htpx-fv-glow .htpx-fvwrap::before,.htpx-fvhead.htpx-fv-glow .htpx-fvwrap::after{content:"";position:absolute;left:50%;top:50%;width:1.5em;height:1.5em;border-radius:50%;border:2px solid rgba(62,224,127,.75);pointer-events:none;opacity:0;transform:translate(-50%,-50%) scale(.4);animation:htpxFvPing .9s ease-out 3}
.htpx-fvhead.htpx-fv-glow .htpx-fvwrap::after{animation-delay:.3s;border-color:rgba(62,224,127,.42)}
@keyframes htpxFvPing{0%{transform:translate(-50%,-50%) scale(.4);opacity:0}18%{opacity:.9}100%{transform:translate(-50%,-50%) scale(2.8);opacity:0}}
@media(prefers-reduced-motion:reduce){.htpx-fvhead.htpx-fv-glow .htpx-docicon{animation:none;filter:drop-shadow(0 0 5px rgba(62,224,127,.85))}.htpx-fvhead.htpx-fv-glow .htpx-fvwrap::before,.htpx-fvhead.htpx-fv-glow .htpx-fvwrap::after{display:none}}
.htpx-donate-review{margin:15px 0 0;padding-top:13px;border-top:1px solid var(--line);font-size:.85rem;color:var(--dim);line-height:1.5}
.htpx-donate-review a{color:var(--acc);font-weight:600;text-decoration:none}
.htpx-donate-review a:hover{text-decoration:underline}
.htpx-advanced{margin:0 0 16px}
.htpx-advanced > summary{font-size:.95rem;font-weight:700;padding:11px 16px;background:var(--panel2);border:1px solid var(--line);border-radius:var(--r)}
.htpx-advanced > summary:hover{border-color:var(--acc)}
.htpx-advanced[open] > summary{margin-bottom:14px}
.htpx-sugglist{display:flex;flex-direction:column;gap:2px;margin:8px 0}
.htpx-sugg{background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:8px 12px}
.htpx-verdict{margin-top:12px;padding:12px 14px;background:var(--bg);border:1px solid var(--line);border-radius:8px}
.htpx-help{margin-top:10px;padding:10px 12px;border-radius:8px;font-size:.85rem;line-height:1.5;background:var(--msg-info-bg);border:1px solid rgba(90,167,232,.35);color:var(--text)}
.htpx-help strong{color:var(--info)}
.htpx-help code{font-size:.82em}
.htpx-copy{margin-left:8px}
.htpx-copy.htpx-ok{border-color:#1f9d57;color:#1f9d57;background:rgba(31,157,87,.08)}
.htpx-form{margin:0}
.htpx-ampelcard{border-color:rgba(52,210,123,.25)}
.htpx-ampelhead{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:6px}
.htpx-ampelhead h4{margin:0}
.htpx-ampel{margin:6px 0 0;padding:0;list-style:none}
.htpx-ampel-row{display:flex;align-items:flex-start;gap:10px;padding:7px 0;border-bottom:1px dotted rgba(38,43,58,.6)}
.htpx-dot{flex:0 0 12px;width:12px;height:12px;border-radius:50%;margin-top:4px}
.htpx-dot-ok{background:var(--acc)}
.htpx-dot-warn{background:var(--warn)}
.htpx-dot-err{background:var(--err)}
.htpx-ampel-label{font-size:.9rem;color:var(--text)}
.htpx a.htpx-ampel-link{color:var(--text);text-decoration:none;font-weight:600}
.htpx a.htpx-ampel-link:hover{color:var(--acc)}
.htpx-ampel-label small{display:block;font-weight:400}
.htpx-watchrow{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:7px 0;border-bottom:1px dotted rgba(38,43,58,.6)}
.htpx-dirstatus{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 2px}
.htpx-dirbadge{display:inline-flex;align-items:center;gap:6px;background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:4px 10px}
@media (max-width:860px){
  .htpx-body{flex-direction:column}
  /* Menue mobil: gleichmaessiges Kachel-Raster (alle Punkte sichtbar, gleiche
     Groesse) statt ungleichmaessig umbrechender Reihe. */
  .htpx-nav{width:auto;flex:initial;border-right:none;border-bottom:1px solid var(--line);
    display:grid;grid-template-columns:repeat(3,1fr);gap:6px;padding:10px 16px}
  .htpx a.htpx-navlink{border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:6px 11px;font-size:13px}
  .htpx a.htpx-navlink:hover{background:var(--panel)}
  /* Aktiv MUSS hier (nach der navlink-Regel) das Gruen wieder durchsetzen -
     gleiche Spezifitaet, aber spaeter im Quelltext. */
  .htpx a.htpx-nav-active,.htpx a.htpx-nav-active:hover{background:var(--acc);border-color:transparent}
  .htpx-grid2{grid-template-columns:1fr}
}
@media (max-width:560px){
  .htpx-nav{grid-template-columns:1fr 1fr}
}
/* Mobil: breite Tabellen (WAF-Protokoll, Sicherungen) -> gestapelte Karten
   (Label : Wert) statt zeichenweisem Umbruch / abgeschnittenen Spalten. */
@media (max-width:680px){
  .htpx-stack,.htpx-stack tbody,.htpx-stack tr,.htpx-stack td{display:block;width:auto}
  .htpx-stack thead{display:none}
  .htpx-stack tr{border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin:0 0 8px}
  .htpx-stack td{border:0;padding:3px 0;display:flex;flex-wrap:wrap;gap:4px 10px;align-items:baseline;white-space:normal}
  .htpx-stack td::before{content:attr(data-label);flex:0 0 78px;color:var(--dim);font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.4px;line-height:1.5}
  /* Wert-Region: EIN Flex-Item, nimmt die Restbreite und bricht seinen Inhalt (Text +
     Links) normal um - verhindert das Abschneiden mehrteiliger Zellen (z. B. NOTE). */
  .htpx-stack td>.htpx-cellval{flex:1 1 0;min-width:0;overflow-wrap:break-word;word-break:break-word}
  .htpx-stack td.htpx-mono{word-break:break-word}
  .htpx-stack td.htpx-waf-uri{word-break:break-all}
  .htpx-stack td>.htpx-rowactions{display:flex;flex-wrap:wrap;gap:6px;flex:1 1 auto}
}
/* Handy (nur Mobil, <=680px): am Rand -> Box -> Inhalt -> Karte -> verschachtelte
   Karte summieren sich mehrere Seitenabstaende und fressen auf dem ohnehin schmalen
   Display viel Breite. Deshalb NUR mobil:
   - die ganze Box naeher an den Bildschirmrand ziehen (aeusseren Abstand ~halbiert)
   - Inhalts- und Menue-Abstand angleichen + reduzieren (Karten buendig zum Menue)
   - Karten-Innenabstand schlanker, verschachtelte Karte bricht nicht ein zweites Mal auf
   Desktop bleibt voellig unveraendert (nur horizontale Werte, vertikale Rhythmik bleibt). */
@media (max-width:680px){
  #htpx-root.htpx{margin-left:-8px;margin-right:-8px}
  .htpx-shellhead,.htpx-shellfoot{padding-left:12px;padding-right:12px}
  .htpx-main{padding-left:12px;padding-right:12px}
  .htpx-nav{padding-left:12px;padding-right:12px}
  .htpx-card{padding-left:12px;padding-right:12px}
  .htpx-card .htpx-card{margin-left:-6px;margin-right:-6px}
}
/* Joomla 2.5/3 (Bootstrap-2-Aera) stylt input[type=...]/select per Element+
   Attribut (feste Hoehe/Padding) und schlaegt damit unsere Einzelklasse -> die
   Felder stauchen. Hoehere Spezifitaet gleicht das NUR fuer Alt-Joomla aus;
   J4-6 bleiben unberuehrt. */
.htpx-legacy .htpx-input,.htpx-legacy .htpx-textarea,.htpx-legacy select.htpx-input{
  height:auto;min-height:0;padding:8px 10px;line-height:1.4;margin:0;box-shadow:none;-webkit-box-shadow:none;background-image:none}
.htpx-legacy .htpx-textarea{min-height:120px}
/* Responsiver Subhead-Titel (Element liegt ausserhalb von .htpx): Desktop zeigt
   "<Bereich> - <Nutzen>" (htpx-tbf), Mobil nur den Nutzen (htpx-tbm). Vom JS
   in .page-title eingehaengt. */
.htpx-tbm{display:none}
@media (max-width:767px){.htpx-tbf{display:none}.htpx-tbm{display:inline}}
/* Sprungmarken-Hervorhebung (Ampel-Link springt praezise ins Ziel/Spoiler) */
.htpx-flash{animation:htpxflash 1.8s ease-out;border-radius:8px}
@keyframes htpxflash{0%{box-shadow:0 0 0 0 rgba(52,210,123,0)}12%{box-shadow:0 0 0 2px var(--acc)}60%{box-shadow:0 0 0 2px var(--acc)}100%{box-shadow:0 0 0 0 rgba(52,210,123,0)}}
/* Vollstaendig-abgesichert-Erfolgszustand (Ampel komplett gruen): animiertes
   Schild+Haekchen mit weichem Halo + einmaligem Ring, dezentes Auffrischen-Icon */
.htpx-secured{display:flex;align-items:center;gap:10px}
.htpx-secured-icon{position:relative;display:inline-flex;width:34px;height:34px;align-items:center;justify-content:center;border-radius:50%;background:radial-gradient(closest-side,rgba(52,210,123,.22),rgba(52,210,123,0))}
.htpx-secured-icon::after{content:"";position:absolute;inset:-2px;border-radius:50%;border:2px solid var(--acc);opacity:0;animation:htpx-ring .8s .15s ease-out forwards}
.htpx-secured-icon svg{animation:htpx-pop .45s ease-out both}
.htpx-secured-icon .htpx-shield{stroke:var(--acc)}
.htpx-secured-icon .htpx-check{stroke:var(--acc);stroke-dasharray:16;stroke-dashoffset:16;animation:htpx-draw .55s .3s ease-out forwards}
.htpx-secured-text{font-weight:700;color:var(--acc);font-size:.92rem}
.htpx-iconbtn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;background:transparent;border:1px solid var(--line);border-radius:8px;color:var(--dim);cursor:pointer}
.htpx-iconbtn:hover{color:var(--acc);border-color:var(--acc)}
@keyframes htpx-pop{0%{opacity:0;transform:scale(.6)}100%{opacity:1;transform:scale(1)}}
@keyframes htpx-draw{to{stroke-dashoffset:0}}
@keyframes htpx-ring{0%{opacity:.5;transform:scale(.85)}100%{opacity:0;transform:scale(1.7)}}
@media (prefers-reduced-motion:reduce){.htpx-secured-icon::after,.htpx-secured-icon svg,.htpx-secured-icon .htpx-check{animation:none}.htpx-secured-icon .htpx-check{stroke-dashoffset:0}}
/* Tiefen-Scan Schaddateien: Spinner + Trefferliste + Voransicht */
.htpx-scanstate{display:flex;align-items:center;gap:10px;color:var(--dim);font-size:.9rem;padding:6px 0}
.htpx-scanstate.htpx-ok{color:var(--acc);font-weight:600}
.htpx-scanstate.htpx-bad{color:var(--err);font-weight:700}
.htpx-malprog{margin:4px 0 6px}
.htpx-progbar{position:relative;height:8px;border-radius:6px;background:rgba(127,127,127,.18);overflow:hidden}
/* Unbestimmter "arbeitet..."-Balken: bewegter Streifen (kein irrefuehrender Prozentwert). */
.htpx-progfill{position:absolute;top:0;left:-40%;width:40%;height:100%;border-radius:6px;background:var(--acc);animation:htpx-indet 1.1s ease-in-out infinite}
@keyframes htpx-indet{0%{left:-40%}50%{left:35%}100%{left:100%}}
.htpx-progtext{font-size:.86rem;font-weight:600;margin-top:6px}
.htpx-malnote{font-size:.82rem;margin:6px 0 0}
.htpx a.htpx-malfull,.htpx a.htpx-malfull:hover{font-size:.82rem;display:inline-block;margin:4px 0 0;color:inherit}
.htpx-wwwsub{color:#e8c87a;font-size:.82rem;margin:6px 0 0;line-height:1.35}
.htpx.htpx-light .htpx-wwwsub{color:#b35309}
@media (prefers-reduced-motion:reduce){.htpx-progfill{animation:none;left:0;width:100%;opacity:.35}}
/* Spenden-Optionen: zwei kleine Wege nebeneinander, dezent. */
.htpx-donate-opts{display:flex;flex-wrap:wrap;gap:16px;margin-top:2px}
.htpx-donate-opt{display:inline-flex;flex-direction:column;gap:4px}
.htpx-donate-sub{font-size:.74rem;text-align:center}
.htpx-pp{display:inline-flex;align-items:center;justify-content:center;gap:7px}
.htpx-pp svg{flex:0 0 auto}
.htpx-spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--line);border-top-color:var(--acc);border-radius:50%;animation:htpx-spin .7s linear infinite;flex:0 0 auto}
@keyframes htpx-spin{to{transform:rotate(360deg)}}
.htpx-mallist{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.htpx-maltoolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:10px 0 4px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:rgba(127,127,127,.05)}
.htpx-malselall-l{display:flex;align-items:center;gap:6px;font-size:.82rem;cursor:pointer;white-space:nowrap;font-weight:600}
.htpx-malspacer{flex:1 1 auto}
.htpx-malcb{flex:0 0 auto;width:16px;height:16px;cursor:pointer;margin:0}
.htpx-malrow{border:1px solid var(--line);border-radius:8px;padding:8px 10px;background:rgba(127,127,127,.04)}
.htpx-malrow-head{display:flex;align-items:center;gap:8px;flex-wrap:nowrap}
.htpx-malcb{flex:0 0 auto}
.htpx-malinfo{display:flex;align-items:center;gap:4px 8px;flex-wrap:wrap;flex:1 1 auto;min-width:0}
.htpx-malactions{display:flex;align-items:center;gap:6px;flex:0 0 auto;margin-left:auto}
.htpx-malpath{font-family:Consolas,Menlo,monospace;font-size:.82rem;word-break:break-all;flex:1 1 160px;min-width:0}
.htpx-malreason{font-size:.72rem;color:var(--dim);border:1px solid var(--line);border-radius:5px;padding:1px 6px;white-space:nowrap;max-width:220px;overflow:hidden;text-overflow:ellipsis}
.htpx-malsize{font-size:.72rem;color:var(--dim);white-space:nowrap}
.htpx-maldate{font-size:.72rem;color:var(--dim);white-space:nowrap;font-variant-numeric:tabular-nums}
/* Handy: Aktionsknoepfe (Diff/Ignorieren/Wiederherstellen) brechen in eine eigene, volle Zeile
   UNTER den Datei-Infos um und teilen sich die Breite - so bleibt der Pfad lesbar und nichts quetscht. */
@media (max-width:560px){
  .htpx-malrow-head{flex-wrap:wrap}
  .htpx-malinfo{flex:1 1 100%}
  .htpx-malactions{margin-left:0;width:100%;flex-wrap:wrap;margin-top:6px}
  .htpx-malactions>button{flex:1 1 auto}
}
/* Ignorierte (bewusst akzeptierte) Kerndateien - kompakte, mobil-freundliche Liste. */
.htpx-coreign{margin-top:12px}
.htpx-coreignlist{display:flex;flex-direction:column;margin-top:6px}
.htpx-coreignrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:7px 0;border-top:1px solid var(--line)}
.htpx-coreignrow .htpx-malpath{flex:1 1 160px}
.htpx-coreignrow>button{flex:0 0 auto}
@media (max-width:560px){.htpx-coreignrow>button{flex:1 1 auto}}
.htpx-quarwrap{margin-top:14px;padding-top:10px;border-top:1px dashed var(--line)}
.htpx-quarh{font-size:.9rem;margin:0 0 4px}
.htpx-quarcount{font-size:.78rem;color:var(--dim);font-weight:400}
.htpx-quarlist{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.htpx-quarrow{background:rgba(127,127,127,.03)}
.htpx-quarstat{font-size:.72rem;color:var(--dim);border:1px solid var(--line);border-radius:5px;padding:1px 6px;white-space:nowrap}
.htpx-quar-mal{color:#c0392b;border-color:rgba(207,34,46,.5);background:rgba(207,34,46,.1)}
.htpx-quar-clean{color:#b26a00;border-color:rgba(217,119,6,.5);background:rgba(217,119,6,.1)}
.htpx-quar-unk{color:var(--dim)}
.htpx-corediff{margin-top:8px}
.htpx-corediff-pre{margin:0;max-height:360px;overflow:auto;font-family:Consolas,Menlo,monospace;font-size:.78rem;line-height:1.5;background:rgba(127,127,127,.06);border:1px solid var(--line);border-radius:8px;padding:8px 10px;white-space:pre-wrap;word-break:break-all}
.htpx-diff-add{color:#1a7f37;background:rgba(46,160,67,.12);border-radius:3px;padding:0 2px}
.htpx-diff-rem{color:#c0392b;background:rgba(207,34,46,.12);border-radius:3px;padding:0 2px}
.htpx-light .htpx-diff-add{color:#116329}
.htpx-light .htpx-diff-rem{color:#a40e26}
.htpx-malsort{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:8px 0 2px;font-size:.8rem}
.htpx-sortbtn{background:transparent;border:1px solid var(--line);border-radius:6px;color:var(--dim);cursor:pointer;font-size:.78rem;padding:3px 10px;font-family:inherit}
.htpx-sortbtn:hover{color:var(--acc);border-color:var(--acc)}
.htpx-malprev-btn{background:transparent;border:1px solid var(--line);border-radius:6px;color:var(--dim);cursor:pointer;font-size:.74rem;padding:2px 8px;white-space:nowrap}
.htpx-malprev-btn:hover{color:var(--acc);border-color:var(--acc)}
.htpx-malbox{margin-top:8px}
.htpx-malboxnote{font-size:.78rem;margin:0 0 6px}
.htpx-malta{width:100%;box-sizing:border-box;min-height:160px;max-height:440px;font-family:Consolas,Menlo,monospace;font-size:.8rem;line-height:1.45;color:inherit;background:rgba(127,127,127,.06);border:1px solid var(--line);border-radius:8px;padding:8px 10px;resize:vertical;white-space:pre;overflow:auto}
.htpx-maleditbar{display:flex;gap:8px;margin-top:8px}
.htpx-maledit-fs{position:fixed;inset:0;z-index:99999;margin:0;padding:0;border:0;background:#0f1320;display:flex;flex-direction:column}
.htpx-maledit-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 14px;background:#171c2b;border-bottom:1px solid rgba(255,255,255,.08)}
.htpx-maledit-path{color:#cdd4e0;font-size:.82rem;word-break:break-all;flex:1 1 240px}
.htpx-maledit-wrap{flex:1 1 auto;display:flex;min-height:0;background:#0f1320}
/* Zeilennummern-Spalte: schmal, rechtsbuendig, gedimmt; teilt Zeilenhoehe/-groesse
   + vertikales Padding EXAKT mit der Textarea, scrollt via JS mit. */
.htpx-maledit-gutter{flex:0 0 auto;box-sizing:border-box;overflow:hidden;
  font-family:Consolas,Menlo,monospace;font-size:.86rem;line-height:1.5;
  padding:12px 6px 12px 8px;text-align:right;color:#5b6473;background:#0c0f19;
  border-right:1px solid rgba(255,255,255,.06);white-space:pre;-moz-user-select:none;user-select:none}
.htpx-maledit-ta{flex:1 1 auto;width:100%;box-sizing:border-box;border:0;outline:none;resize:none;background:#0f1320;color:#e6e9f0;font-family:Consolas,Menlo,monospace;font-size:.86rem;line-height:1.5;padding:12px 14px;white-space:pre;overflow:auto}
.htpx-malx{flex:0 0 auto;width:26px;height:26px;line-height:1;padding:0;border:1px solid rgba(240,85,107,.5);background:transparent;color:var(--err);border-radius:6px;cursor:pointer;font-size:1.05rem}
.htpx-malx:hover{background:rgba(240,85,107,.12)}
.htpx-malprev{display:none;margin-top:8px}
.htpx-malfoot{margin-top:14px;padding-top:12px;border-top:1px solid var(--line)}
.htpx-malbackup{margin:12px 0 14px;padding:10px 13px;font-size:.85rem;line-height:1.5;color:var(--text);background:var(--msg-info-bg);border:1px solid rgba(90,167,232,.4);border-left:3px solid var(--info);border-radius:8px}
.htpx-warnhint{color:var(--err);font-size:.8rem;margin:8px 0 0}
.htpx-malsafe{background:transparent;border:1px solid var(--line);border-radius:6px;color:var(--dim);cursor:pointer;font-size:.74rem;padding:2px 8px;white-space:nowrap}
.htpx-malsafe:hover{color:var(--acc);border-color:var(--acc)}
.htpx-wl{margin-top:14px;border-top:1px solid var(--line);padding-top:10px}
.htpx-wl summary{cursor:pointer;font-weight:600;font-size:.85rem}
.htpx-wllist{list-style:none;margin:8px 0;padding:0;display:flex;flex-direction:column;gap:6px}
.htpx-wlitem{display:flex;align-items:center;gap:8px;font-size:.82rem}
.htpx-wlitem .htpx-mono{flex:1 1 auto;word-break:break-all}
.htpx-wlx{flex:0 0 auto;width:22px;height:22px;line-height:1;padding:0;border:1px solid var(--line);background:transparent;color:var(--dim);border-radius:5px;cursor:pointer;font-size:.95rem}
.htpx-wlx:hover{color:var(--err);border-color:var(--err)}
.htpx-wladd{display:flex;gap:8px;flex-wrap:wrap;margin-top:6px}
.htpx-wladd .htpx-input{flex:1 1 220px;min-width:160px}
@media (prefers-reduced-motion:reduce){.htpx-spinner{animation:none}}
.htpx-core-ver{align-self:center;font-size:.85rem}
.htpx-core-err{margin:10px 0}
/* Info-Card „Was das Dashboard zusaetzlich bringt" (Connect-Seite, nur bei nicht verbunden). */
.htpx-fvben{border-left:3px solid var(--acc)}
.htpx-fvben .htpx-sub{margin-top:0}
.htpx-fvben-intro{margin:0;font-size:.92rem;line-height:1.55}
.htpx-fvben-list{list-style:none;margin:15px 0 0;padding:0;display:grid;gap:12px}
.htpx-fvben-list li{position:relative;padding-left:27px;line-height:1.5;font-size:.92rem}
.htpx-fvben-list li::before{content:"\2713";position:absolute;left:0;top:.05em;color:var(--acc);font-weight:800;font-size:.95rem}
.htpx-fvben-list li strong{color:var(--text)}
.htpx-fvben-foot{margin:17px 0 0;padding-top:13px;border-top:1px solid var(--line);font-size:.9rem;font-weight:600;color:var(--text)}
</style>
        <?php
    }

    private static function js($titles = array('desktop' => '', 'mobile' => ''))
    {
        $jv    = htp_jversion();
        $major = preg_match('/^(\d+)/', $jv, $m) ? (int) $m[1] : 0;
        $dynamicTheme = $major >= 5 ? 'true' : 'false';
        // Entities aufloesen -> echte Zeichen fuer textContent (kein innerHTML).
        $tbFull   = json_encode(html_entity_decode((string) $titles['desktop'], ENT_QUOTES, 'UTF-8'));
        $tbMobile = json_encode(html_entity_decode((string) $titles['mobile'], ENT_QUOTES, 'UTF-8'));

        // Joomla-Zeitzonen-Offset (Sekunden, fuer "jetzt") fuer die client-seitige
        // Datumsanzeige (Malware-/Backup-Dateidaten) - sonst zeigte JS die Browser-TZ.
        $tzOffset = 0;

        try {
            $tzOffset = (int) (new DateTimeZone(htp_joomla_tz()))->getOffset(new DateTime('now', new DateTimeZone('UTC')));
        } catch (Exception $e) {
            $tzOffset = 0;
        }
        ?>
<script>
var HTPX_TZ_OFFSET = <?php echo (int) $tzOffset; ?>;
(function(){
  // Subhead-Titel responsiv machen: Joomlas .page-title traegt den Desktop-Text
  // (Tags wurden vom Toolbar-Helper entfernt) -> hier durch zwei Spans ersetzen,
  // die per CSS-Media-Query getauscht werden. Icon (falls vorhanden) bleibt.
  var tbFull = <?php echo $tbFull; ?>, tbMobile = <?php echo $tbMobile; ?>;
  if (tbFull && tbFull !== tbMobile) {
    var pt = document.querySelector('.page-title');
    if (pt) {
      var icon = pt.querySelector('[class*="icon-"]');
      pt.textContent = '';
      if (icon) { pt.appendChild(icon); pt.appendChild(document.createTextNode(' ')); }
      var f = document.createElement('span'); f.className = 'htpx-tbf'; f.textContent = tbFull;
      var mb = document.createElement('span'); mb.className = 'htpx-tbm'; mb.textContent = tbMobile;
      pt.appendChild(f); pt.appendChild(mb);
    }
  }
})();
(function(){
  // Leere Toolbar-Leiste samt zugehoerigem Zahnrad-Toggle entfernen. Die
  // Komponente registriert keine Toolbar-Buttons. Leiste heisst je nach Joomla
  // #subhead-container (J4-6 Atum) bzw. .subhead/.subhead-collapse (J3 Isis).
  // WICHTIG: der navbar-toggler (= Zahnrad) ist selbst ein <button> und darf
  // NICHT als "echter Button" zaehlen, sonst bleibt die leere Leiste stehen.
  // Den gefuellten .page-title und den Sidebar-Hamburger nie anfassen.
  var bar = document.querySelector('#subhead-container')
         || document.querySelector('.subhead-collapse')
         || document.querySelector('.subhead');

  if (bar && !bar.querySelector('.page-title')) {
    var real = false, btns = bar.querySelectorAll('.btn, joomla-toolbar-button, button');
    for (var i = 0; i < btns.length; i++) {
      if (!btns[i].classList.contains('navbar-toggler')) { real = true; break; }
    }
    if (!real) { bar.style.display = 'none'; }
  }

  // Toolbar-Toggle separat ausblenden, falls er ausserhalb der Leiste sitzt -
  // gezielt nur toolbar-bezogene Toggles, NICHT den Sidebar-Hamburger. Atum (J5/6)
  // gibt dem Burger aria-controls/data-bs-target="subhead-container" + die Klasse
  // .toggler-toolbar + aria-label="Toolbar" - alle diese Merkmale abdecken.
  var togs = document.querySelectorAll(
    '.navbar-toggler.toggler-toolbar,'
    + '.navbar-toggler[data-bs-target*="toolbar"],.navbar-toggler[aria-controls*="toolbar"],'
    + '.navbar-toggler[data-bs-target="#subhead-container"],.navbar-toggler[aria-controls="subhead-container"],'
    + '.navbar-toggler[aria-label="Toolbar"]'
  );
  for (var k = 0; k < togs.length; k++) { togs[k].style.display = 'none'; }
})();
(function(){
  // Joomla 2.5-4: Light-Mode ist serverseitig fest gesetzt (htpx-light) -> hier
  // nichts tun. Joomla 5/6: dem im Admin gewaehlten Schema folgen (Atum setzt
  // data-bs-theme auf <html>).
  var dynamicTheme = <?php echo $dynamicTheme; ?>;
  if (!dynamicTheme) { return; }
  var panels = document.querySelectorAll('.htpx');
  if (!panels.length) { return; }
  function apply(){
    var t = document.documentElement.getAttribute('data-bs-theme');
    var dark;
    if (t === 'dark') { dark = true; }
    else if (t === 'light') { dark = false; }
    else { dark = !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); }
    for (var i = 0; i < panels.length; i++) { panels[i].classList.toggle('htpx-light', !dark); }
  }
  apply();
  try {
    var mo = new MutationObserver(apply);
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-bs-theme'] });
  } catch (e) {}
  document.addEventListener('joomla:color-scheme-change', apply);
  if (window.matchMedia) {
    try { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', apply); } catch (e) {}
  }
})();
(function(){
  // Sprungmarken der Sicherheits-Ampel praezise machen: Ziel-Element per
  // location.hash finden, alle <details>-Vorfahren oeffnen (Spoiler), sanft
  // hinscrollen und kurz hervorheben. Laeuft beim Laden (Sprung aus anderem
  // Bereich) UND bei reinem Anker-Wechsel auf derselben Seite.
  function htpxReveal(){
    var hash = location.hash;
    if (!hash || hash.length < 2) { return; }
    var el;
    try { el = document.querySelector(hash); } catch (e) { return; }
    if (!el) { return; }
    var p = el.parentNode;
    while (p && p.nodeType === 1) {
      if (p.tagName === 'DETAILS' && !p.open) { p.open = true; }
      p = p.parentNode;
    }
    setTimeout(function(){
      try { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
      catch (e) { el.scrollIntoView(); }
      el.classList.add('htpx-flash');
      setTimeout(function(){ el.classList.remove('htpx-flash'); }, 1800);
    }, 70);
  }
  window.addEventListener('hashchange', htpxReveal);
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', htpxReveal);
  } else {
    htpxReveal();
  }
})();
(function(){
  // Sicheres Zufallspasswort fuer den Backend-Schutz: crypto.getRandomValues
  // (kryptografisch stark), gleichverteilte Zeichenauswahl ohne Modulo-Bias.
  // Fuellt beide Passwortfelder, zeigt das Passwort an, aktiviert "Kopieren".
  var genBtn = document.getElementById('htpx-pwgen');
  if (!genBtn) { return; } // nur auf der Backend-Schutz-Seite
  var p1 = document.getElementById('htp_guard_pass');
  var p2 = document.getElementById('htp_guard_pass2');
  var copyBtn = document.getElementById('htpx-pwcopy');
  var copyTxt = document.getElementById('htpx-pwcopy-txt');
  var showBtn = document.getElementById('htpx-pwshow');
  var showBtn2 = document.getElementById('htpx-pwshow2');

  // Zeichenvorrat ohne leicht verwechselbare Zeichen (0/O, 1/l/I) - bewusst
  // mit Sonderzeichen fuer Staerke; ergibt eine sehr hohe Entropie bei Laenge 20.
  var sets = {
    lower: 'abcdefghijkmnpqrstuvwxyz',
    upper: 'ABCDEFGHJKLMNPQRSTUVWXYZ',
    digit: '23456789',
    sym:   '!@#$%&*-_=+?'
  };
  var all = sets.lower + sets.upper + sets.digit + sets.sym;

  function rnd(max){
    // gleichverteilt im Bereich [0,max) ohne Modulo-Bias
    if (window.crypto && window.crypto.getRandomValues) {
      var limit = Math.floor(4294967296 / max) * max;
      var a = new Uint32Array(1);
      do { window.crypto.getRandomValues(a); } while (a[0] >= limit);
      return a[0] % max;
    }
    return Math.floor(Math.random() * max); // Fallback (sollte nie noetig sein)
  }
  function pick(set){ return set.charAt(rnd(set.length)); }

  function generate(len){
    len = len || 20;
    // mindestens je 1 Zeichen aus jeder Klasse, Rest frei, dann mischen.
    var chars = [pick(sets.lower), pick(sets.upper), pick(sets.digit), pick(sets.sym)];
    for (var i = chars.length; i < len; i++) { chars.push(pick(all)); }
    for (var j = chars.length - 1; j > 0; j--) { // Fisher-Yates
      var k = rnd(j + 1); var t = chars[j]; chars[j] = chars[k]; chars[k] = t;
    }
    // Keine verwirrenden Doppel-Trennzeichen: nie '__' oder '--' in Folge. Das zweite
    // Zeichen eines solchen Paars wird durch ein Nicht-Trennzeichen ersetzt (Laenge/Klassen bleiben).
    for (var m = 1; m < chars.length; m++) {
      if ((chars[m] === '_' || chars[m] === '-') && chars[m] === chars[m - 1]) {
        var c; do { c = pick(all); } while (c === '_' || c === '-');
        chars[m] = c;
      }
    }
    return chars.join('');
  }

  function flash(btn, span, okText){
    var old = span.textContent;
    span.textContent = okText;
    btn.classList.add('htpx-ok');
    setTimeout(function(){ span.textContent = old; btn.classList.remove('htpx-ok'); }, 1600);
  }

  genBtn.addEventListener('click', function(){
    var pw = generate(20);
    if (p1) { p1.value = pw; p1.type = 'text'; }
    if (p2) { p2.value = pw; }
    if (showBtn) { showBtn.setAttribute('aria-pressed', 'true'); }
    if (copyBtn) { copyBtn.disabled = false; }
  });

  if (copyBtn) {
    copyBtn.addEventListener('click', function(){
      var pw = p1 ? p1.value : '';
      if (!pw) { return; }
      function done(){ flash(copyBtn, copyTxt, copyBtn.getAttribute('data-ok') || 'OK'); }
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(pw).then(done, done);
      } else {
        var ta = document.createElement('textarea');
        ta.value = pw; document.body.appendChild(ta); ta.select();
        try { document.execCommand('copy'); } catch (e) {}
        document.body.removeChild(ta); done();
      }
    });
  }

  // Jedes Auge schaltet sein eigenes Feld unabhaengig.
  function bindToggle(btn, field){
    if (!btn || !field) { return; }
    btn.addEventListener('click', function(){
      var show = (field.type === 'password');
      field.type = show ? 'text' : 'password';
      btn.setAttribute('aria-pressed', show ? 'true' : 'false');
    });
  }
  bindToggle(showBtn, p1);
  bindToggle(showBtn2, p2);
})();
function htpxCopy(id, btn){
  var el = document.getElementById(id);
  if (!el) { return false; }
  var text = el.textContent || el.innerText || '';
  function ok(){
    if (!btn) { return; }
    if (btn.getAttribute('data-orig') === null) { btn.setAttribute('data-orig', btn.textContent); }
    btn.textContent = btn.getAttribute('data-ok') || btn.textContent;
    btn.classList.add('htpx-ok');
    setTimeout(function(){
      var o = btn.getAttribute('data-orig');
      if (o !== null) { btn.textContent = o; }
      btn.classList.remove('htpx-ok');
    }, 1600);
  }
  function fallback(){
    // Textarea OFF-SCREEN halten - eine sichtbare Auswahl loest auf manchen Browsern das
    // Kontext-/Auswahlmenue aus. readonly + fixed + opacity:0 vermeidet das.
    var ta = document.createElement('textarea');
    ta.value = text; ta.setAttribute('readonly', '');
    ta.style.position = 'fixed'; ta.style.top = '0'; ta.style.left = '-9999px'; ta.style.opacity = '0';
    document.body.appendChild(ta); ta.select();
    try { document.execCommand('copy'); } catch (e) {}
    document.body.removeChild(ta); ok();
  }
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(text).then(ok, fallback);
  } else {
    fallback();
  }
  return false;
}
function htpxToggleAll(master){
  var boxes = document.querySelectorAll('input[name="sg[]"]');
  for (var i = 0; i < boxes.length; i++) { boxes[i].checked = master.checked; }
}
(function(){
  // Site-Scan schrittweise mit Fortschrittsbalken; ohne fetch() laeuft
  // das Formular unveraendert als synchroner Scan durch.
  var form = document.getElementById('htpx-scanform');
  if (!form || !window.fetch || !window.JSON || !window.FormData) { return; }
  var queueEl = document.getElementById('htpx-scanqueue');
  var queue = [];
  try { queue = JSON.parse(queueEl ? queueEl.textContent : '[]') || []; } catch (e) {}
  if (!queue.length) { return; }
  form.addEventListener('submit', function(ev){
    ev.preventDefault();
    var max = parseInt(document.getElementById('htp_scan_pages').value, 10) || 12;
    max = Math.max(3, Math.min(30, max));
    var urls = queue.slice(0, max);
    var btn = form.querySelector('button[type=submit]');
    var prog = document.getElementById('htpx-scanprog');
    var bar = document.getElementById('htpx-scanprog-bar');
    var pct = document.getElementById('htpx-scanprog-pct');
    var label = document.getElementById('htpx-scanprog-label');
    btn.disabled = true;
    prog.style.display = 'block';
    var pages = {}, paths = {}, i = 0;
    function update(){
      var p = Math.round(i / urls.length * 100);
      bar.style.width = p + '%';
      pct.textContent = p + '%';
    }
    function step(){
      if (i >= urls.length) { finish(); return; }
      label.textContent = 'Seite ' + (i + 1) + ' von ' + urls.length + ': ' + urls[i];
      var fd = new FormData(form);
      fd.set('htp_task', 'scan_step');
      fd.set('htp_page', urls[i]);
      fetch(form.getAttribute('action'), { method: 'POST', body: fd, credentials: 'same-origin' })
        .then(function(r){ return r.json(); })
        .then(function(d){
          pages[urls[i]] = (d && typeof d.status === 'number') ? d.status : 0;
          if (d && d.ok && d.paths) {
            for (var k = 0; k < d.paths.length; k++) {
              if (!(d.paths[k] in paths)) { paths[d.paths[k]] = urls[i]; }
            }
          }
        })
        .catch(function(){ pages[urls[i]] = 0; })
        .then(function(){ i++; update(); step(); });
    }
    function finish(){
      label.textContent = 'Auswertung …';
      document.getElementById('htp_found').value = JSON.stringify({ pages: pages, paths: paths });
      HTMLFormElement.prototype.submit.call(form);
    }
    update();
    step();
  });
})();
(function(){
  // Experimenteller Tiefen-Scan nach Schaddateien: startet NUR per Button.
  // Treffer (Pfad/Grund/Groesse, Voransicht aufklappbar, X zum Loeschen, unten
  // "Alle loeschen") werden ausschliesslich per textContent gesetzt (kein
  // innerHTML) -> kein XSS aus Schad-Inhalten.
  var form   = document.getElementById('htpx-malform');
  var status = document.getElementById('htpx-malstatus');
  var list   = document.getElementById('htpx-mallist');
  var foot   = document.getElementById('htpx-malfoot');
  var malBackup = document.getElementById('htpx-malbackup');
  var oldestMtime = 0;
  var i18nEl = document.getElementById('htpx-mali18n');
  var delAll = document.getElementById('htpx-maldelall');
  var startBtn = document.getElementById('htpx-malstart');
  var progBox  = document.getElementById('htpx-malprog');
  var progText = document.getElementById('htpx-progtext');
  var malNote  = document.getElementById('htpx-malnote');
  var malFull  = document.getElementById('htpx-malfull');
  if (!form || !status || !list || !foot || !i18nEl || !startBtn || !window.fetch || !window.FormData) {
    if (startBtn) { startBtn.disabled = true; }
    return;
  }
  // Fund-Inhalte kommen base64-kodiert vom Server (haelt den Schadcode aus dem
  // HTTP-Body fern, damit Browser-Virenscanner die Seite nicht als Schadskript
  // melden). UTF-8-sicher dekodieren. Die Decoder-Namen bewusst aus Fragmenten
  // ueber window[...] aufrufen, damit nicht einmal der Helper selbst die Base64-/
  // Zeichen-Decoder-Namen als zusammenhaengendes Muster im Seiten-Quelltext stehen
  // laesst - sonst koennte ein Browser-Virenscanner schon DEN flaggen.
  var W = window, dec1 = W['at' + 'ob'];
  function b64dec(s){
    if (!s || !dec1) { return ''; }
    try {
      var bin = dec1(s), n = bin.length, bytes = new Uint8Array(n), i, cc = 'char' + 'CodeAt';
      for (i = 0; i < n; i++) { bytes[i] = bin[cc](i); }
      if (W.TextDecoder) { return new W.TextDecoder('utf-8').decode(bytes); }
      return decodeURIComponent(escape(bin));
    } catch (e) { return ''; }
  }
  var t = {
    none:       i18nEl.getAttribute('data-none'),
    found:      i18nEl.getAttribute('data-found'),
    err:        i18nEl.getAttribute('data-err'),
    confirm1:   i18nEl.getAttribute('data-confirm1'),
    confirmall: i18nEl.getAttribute('data-confirmall'),
    deleted:    i18nEl.getAttribute('data-deleted'),
    failed:     i18nEl.getAttribute('data-failed'),
    preview:    i18nEl.getAttribute('data-preview'),
    safe:       i18nEl.getAttribute('data-safe'),
    wlremove:   i18nEl.getAttribute('data-wlremove'),
    scanning:   i18nEl.getAttribute('data-scanning'),
    rescan:     i18nEl.getAttribute('data-rescan'),
    selcount:   i18nEl.getAttribute('data-selcount'),
    progress:   i18nEl.getAttribute('data-progress'),
    progskip:   i18nEl.getAttribute('data-progskip'),
    modedelta:  i18nEl.getAttribute('data-modedelta'),
    modefull:   i18nEl.getAttribute('data-modefull'),
    edit:       i18nEl.getAttribute('data-edit'),
    save:       i18nEl.getAttribute('data-save'),
    cancel:     i18nEl.getAttribute('data-cancel'),
    editnote:   i18nEl.getAttribute('data-editnote'),
    savefail:   i18nEl.getAttribute('data-savefail'),
    saveddirty: i18nEl.getAttribute('data-saveddirty'),
    truncated:  i18nEl.getAttribute('data-truncated'),
    saveincomplete: i18nEl.getAttribute('data-saveincomplete'),
    saveunwritable: i18nEl.getAttribute('data-saveunwritable'),
    saving:     i18nEl.getAttribute('data-saving'),
    toobig:     i18nEl.getAttribute('data-toobig'),
    backup:     i18nEl.getAttribute('data-backup'),
    quarlocked:      i18nEl.getAttribute('data-quarlocked'),
    quarcheck:       i18nEl.getAttribute('data-quarcheck'),
    quaredit:        i18nEl.getAttribute('data-quaredit'),
    quardel:         i18nEl.getAttribute('data-quardel'),
    quarrestore:     i18nEl.getAttribute('data-quarrestore'),
    quarmalware:     i18nEl.getAttribute('data-quarmalware'),
    quarclean:       i18nEl.getAttribute('data-quarclean'),
    quarunverif:     i18nEl.getAttribute('data-quarunverif'),
    quarlooksclean:  i18nEl.getAttribute('data-quarlooksclean'),
    quarrestconfirm: i18nEl.getAttribute('data-quarrestconfirm'),
    quarcountlbl:    i18nEl.getAttribute('data-quarcountlbl')
  };
  var action = form.getAttribute('action');
  var count  = 0;
  var wllist = document.getElementById('htpx-wllist');
  var wlcount = document.getElementById('htpx-wlcount');

  // ── Mehrfachauswahl (Checkboxen, Shift-Bereich, Batch-Aktionen) ──
  var toolbar = document.getElementById('htpx-maltoolbar');
  var selAll  = document.getElementById('htpx-malselall');
  var delSel  = document.getElementById('htpx-maldelsel');
  var wlSel   = document.getElementById('htpx-malwlsel');
  var selInfo = document.getElementById('htpx-malselinfo');
  var lastCb  = null;
  // ── Sortierung (Pfad / Datei-Aenderungsdatum) ──
  var sortBar     = document.getElementById('htpx-malsort');
  var sortPathBtn = document.getElementById('htpx-malsort-path');
  var sortDateBtn = document.getElementById('htpx-malsort-date');
  var sortPathBase = sortPathBtn ? sortPathBtn.textContent : '';
  var sortDateBase = sortDateBtn ? sortDateBtn.textContent : '';
  var allHits = [];
  var sortKey = '', sortDir = 1;
  // ── Vom Hoster gesperrte Dateien (Quarantaene, chmod 000) ──
  var quarWrap  = document.getElementById('htpx-quarwrap');
  var quarList  = document.getElementById('htpx-quarlist');
  var quarCountEl = document.getElementById('htpx-quarcount');
  var quarSeen  = {};
  var quarCount = 0;   // angezeigte Zeilen (bis Listen-Deckel)
  var quarTotal = 0;   // ECHTE Gesamtzahl gesperrter Dateien (auch ueber dem Deckel) - fuer die Anzeige

  function allBoxes(){ return [].slice.call(list.querySelectorAll('.htpx-malcb')); }
  function checkedBoxes(){ return [].slice.call(list.querySelectorAll('.htpx-malcb:checked')); }
  function updateSel(){
    var all = allBoxes(), n = checkedBoxes().length;
    if (delSel) { delSel.disabled = (n === 0); }
    if (wlSel)  { wlSel.disabled  = (n === 0); }
    if (selInfo){ selInfo.textContent = n ? (t.selcount || '%d').replace('%d', n) : ''; }
    if (selAll) {
      selAll.checked = (n > 0 && n === all.length);
      selAll.indeterminate = (n > 0 && n < all.length);
    }
  }
  function onCbClick(ev){
    var boxes = allBoxes(), idx = boxes.indexOf(this);
    if (ev.shiftKey && lastCb && boxes.indexOf(lastCb) !== -1) {
      var a = boxes.indexOf(lastCb), lo = Math.min(a, idx), hi = Math.max(a, idx), k;
      for (k = lo; k <= hi; k++) { boxes[k].checked = this.checked; }
    }
    lastCb = this;
    updateSel();
  }

  function fmtSize(n){
    n = +n || 0;
    if (n < 1024) { return n + ' B'; }
    if (n < 1048576) { return (n / 1024).toFixed(1) + ' KB'; }
    return (n / 1048576).toFixed(1) + ' MB';
  }
  function fmtDate(ts){
    ts = +ts || 0;
    if (!ts) { return ''; }
    // Auf die Joomla-Zeitzone verschieben (Offset addieren + als UTC lesen),
    // damit die Anzeige nicht von der Browser-Zeitzone abhaengt.
    var off = (typeof HTPX_TZ_OFFSET === 'number') ? HTPX_TZ_OFFSET : 0;
    var d = new Date((ts + off) * 1000);
    function p(n){ return (n < 10 ? '0' : '') + n; }
    return d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate()) + ' ' + p(d.getUTCHours()) + ':' + p(d.getUTCMinutes());
  }
  function post(task, extra){
    var fd = new FormData(form);
    fd.set('htp_task', task);
    // Datei-Pfade HEX-kodiert senden (htp_path -> htp_pathx, htp_paths -> htp_pathsx), damit eine
    // Server-Firewall (z. B. mod_security) im POST-Body kein Angriffsmuster im Schaddatei-Pfad sieht
    // und die legitime Aktion nicht mit 403 blockt; der Server dekodiert zentral. UTF-8-fest (Umlaute).
    if (extra) { var htpHex = function(s){ var b = new TextEncoder().encode(String(s)); var o=''; for (var i=0;i<b.length;i++){ var h=b[i].toString(16); o += (h.length<2?'0':'')+h; } return o; }; for (var k in extra) { if (extra.hasOwnProperty(k)) { if (k === 'htp_path' || k === 'htp_paths') { fd.set(k + 'x', htpHex(extra[k])); } else { fd.set(k, extra[k]); } } } }
    return fetch(action, { method: 'POST', body: fd, credentials: 'same-origin' }).then(function(r){ return r.json(); });
  }
  function setState(cls, text){
    status.className = 'htpx-scanstate' + (cls ? ' ' + cls : '');
    status.textContent = '';
    if (cls === '') {
      var sp = document.createElement('span');
      sp.className = 'htpx-spinner'; sp.setAttribute('aria-hidden', 'true');
      status.appendChild(sp); status.appendChild(document.createTextNode(' '));
    }
    status.appendChild(document.createTextNode(text || ''));
  }
  function updateHead(){
    if (count > 0) {
      setState('htpx-bad', '⚠ ' + (t.found || '%d').replace('%d', count));
      foot.style.display = 'block';
      if (toolbar) { toolbar.style.display = 'flex'; }
      if (sortBar) { sortBar.style.display = 'flex'; }
      showBackupHint();
    } else {
      setState('htpx-ok', t.none);
      foot.style.display = 'none';
      if (toolbar) { toolbar.style.display = 'none'; }
      if (sortBar) { sortBar.style.display = 'none'; }
      if (malBackup) { malBackup.style.display = 'none'; }
    }
    updateSel();
  }
  // Backup-Hinweis (nur bei Funden) mit dem Datum des AELTESTEN Treffers; das
  // Label vor dem ersten ":" wird fett gesetzt (sicher per DOM, kein innerHTML).
  function showBackupHint(){
    if (!malBackup || !oldestMtime || !t.backup) { if (malBackup) { malBackup.style.display = 'none'; } return; }
    var full = t.backup.replace('%s', fmtDate(oldestMtime).split(' ')[0]);
    var idx  = full.indexOf(':');
    malBackup.textContent = '';
    if (idx > 0) {
      var lead = document.createElement('strong');
      lead.textContent = full.slice(0, idx + 1);
      malBackup.appendChild(lead);
      malBackup.appendChild(document.createTextNode(full.slice(idx + 1)));
    } else {
      malBackup.textContent = full;
    }
    malBackup.style.display = '';
  }
  // ── Randloser Vollbild-Editor (ueber der ganzen Seite), gemeinsam genutzt ──
  var fsEl    = document.getElementById('htpx-maledit');
  var fsTa    = document.getElementById('htpx-maledit-ta');
  var fsGut   = document.getElementById('htpx-maledit-gutter');
  var fsPath  = document.getElementById('htpx-maledit-path');
  var fsNote  = document.getElementById('htpx-maledit-note');
  var fsSave  = document.getElementById('htpx-maledit-save');
  var fsCloseB= document.getElementById('htpx-maledit-close');
  var fsCur   = null;
  var fsGutN  = -1; // zuletzt gerenderte Zeilenzahl (vermeidet unnoetiges Neubauen)
  function fsByteLen(s){ try { return new Blob([s]).size; } catch (e) { return -1; } }
  // Zeilennummern-Spalte fuellen (1..N). wrap="off" -> Zeilen == \n-Anzahl, 1:1.
  function fsRenderGutter(){
    if (!fsGut || !fsTa) { return; }
    var n = fsTa.value.split('\n').length;
    if (n < 1) { n = 1; }
    if (n === fsGutN) { return; }
    fsGutN = n;
    var a = new Array(n);
    for (var i = 1; i <= n; i++) { a[i - 1] = i; }
    fsGut.textContent = a.join('\n');
  }
  function fsSyncGutter(){ if (fsGut && fsTa) { fsGut.scrollTop = fsTa.scrollTop; } }
  function fsHide(){ if (fsEl) { fsEl.style.display = 'none'; } fsCur = null; document.body.style.overflow = ''; }
  function fsOpen(opts){
    if (!fsEl || !fsTa) { return; }
    fsCur = opts;
    if (fsPath) { fsPath.textContent = opts.path; }
    if (fsNote) { fsNote.textContent = ''; }
    if (fsSave) { fsSave.disabled = false; }
    fsTa.value = opts.content || '';
    fsEl.style.display = 'flex';
    document.body.style.overflow = 'hidden';
    fsRenderGutter();
    fsSyncGutter();
    if (opts.hintLine) {
      var lines = fsTa.value.split('\n'), s = 0, i;
      for (i = 0; i < opts.hintLine - 1 && i < lines.length; i++) { s += lines[i].length + 1; }
      var e = s + ((lines[opts.hintLine - 1] || '').length);
      try { fsTa.focus(); fsTa.setSelectionRange(s, e); } catch (err) {}
    } else { try { fsTa.focus(); } catch (err) {} }
  }
  if (fsTa) {
    fsTa.addEventListener('input', function(){ fsRenderGutter(); fsSyncGutter(); });
    fsTa.addEventListener('scroll', fsSyncGutter);
  }
  if (fsCloseB) { fsCloseB.addEventListener('click', fsHide); }
  if (fsEl) {
    document.addEventListener('keydown', function(ev){
      if (ev.keyCode === 27 && fsEl.style.display !== 'none') { fsHide(); }
    });
  }
  if (fsSave) {
    function fsFinish(d, content, cur){
      fsSave.disabled = false;
      if (!d || !d.ok) {
        if (fsNote) { fsNote.textContent = (d && d.unwritable) ? (t.saveunwritable || t.savefail || '') : (d && d.incomplete) ? (t.saveincomplete || t.savefail || '') : (t.savefail || ''); }
        return;
      }
      if (cur.onSaved) { cur.onSaved(content); }
      if (d.clean) { fsHide(); if (cur.onClean) { cur.onClean(); } }
      else if (fsNote) { fsNote.textContent = t.saveddirty || ''; }
    }
    function fsFail(){ fsSave.disabled = false; if (fsNote) { fsNote.textContent = t.savefail || ''; } }
    // htp_len = erwartete Byte-Laenge -> Server schreibt NUR bei exakter
    // Uebereinstimmung (kein Abschneiden bei abgeschnittenem Transfer).
    // Stueckweises Speichern: den Inhalt als UTF-8-Bytes in kleine Datei-Upload-
    // Parts schneiden -> KEIN einzelner grosser POST (umgeht post_max_size,
    // ModSecurity SecRequestBodyNoFilesLimit und Proxy-Body-Limits, daher auch
    // sehr grosse Dateien). Fallback auf den klassischen Einzel-POST, wenn der
    // Browser TextEncoder/Blob nicht kennt.
    function fsChunkedSave(content, cur){
      var bytes = null;
      try { bytes = new TextEncoder().encode(content); } catch (e) { bytes = null; }
      if (!bytes || !window.Blob) {
        post('save_malfile', { htp_path: cur.path, htp_content: content, htp_len: fsByteLen(content) })
          .then(function(d){ fsFinish(d, content, cur); }).catch(fsFail);
        return;
      }
      var CHUNK = 262144;                         // 256 KB je Upload-Part
      var total = bytes.length;
      var xfer  = 'x' + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
      var off   = 0, seq = 0;
      function sendNext(){
        if (off >= total) {
          if (fsNote) { fsNote.textContent = ''; }
          post('commit_malfile', { htp_path: cur.path, htp_xfer: xfer, htp_len: total })
            .then(function(d){ fsFinish(d, content, cur); }).catch(fsFail);
          return;
        }
        var end = (off + CHUNK < total) ? off + CHUNK : total;
        var fd  = new FormData(form);
        fd.set('htp_task', 'save_malchunk');
        fd.set('htp_path', cur.path);
        fd.set('htp_xfer', xfer);
        fd.set('htp_seq', String(seq));
        fd.append('htp_chunk', new Blob([bytes.subarray(off, end)]), 'c');
        if (fsNote && total > CHUNK) { fsNote.textContent = (t.saving || '') + ' ' + Math.floor(off * 100 / total) + '%'; }
        fetch(action, { method: 'POST', body: fd, credentials: 'same-origin' })
          .then(function(r){ return r.json(); })
          .then(function(d){
            if (!d || !d.ok) {
              fsSave.disabled = false;
              if (fsNote) { fsNote.textContent = (d && d.toobig) ? (t.toobig || t.savefail || '') : (t.savefail || ''); }
              return;
            }
            off = end; seq++; sendNext();
          }).catch(fsFail);
      }
      sendNext();
    }
    fsSave.addEventListener('click', function(){
      if (!fsCur || !fsTa) { return; }
      var content = fsTa.value, cur = fsCur;
      fsSave.disabled = true;
      if (fsNote) { fsNote.textContent = ''; }
      fsChunkedSave(content, cur);
    });
  }
  function makeRow(item){
    // Aeltesten Fund merken (fuer den Backup-Hinweis) - ueber ALLE Treffer.
    if (item && +item.mtime && (oldestMtime === 0 || +item.mtime < oldestMtime)) { oldestMtime = +item.mtime; }
    var row  = document.createElement('div'); row.className = 'htpx-malrow';
    var head = document.createElement('div'); head.className = 'htpx-malrow-head';
    var cb   = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'htpx-malcb';
    cb._malpath = item.path; cb._malrow = row;
    cb.addEventListener('click', onCbClick);
    var path = document.createElement('span'); path.className = 'htpx-malpath'; path.textContent = item.path; path.title = item.path;
    var rsn  = document.createElement('span'); rsn.className  = 'htpx-malreason'; rsn.textContent = item.reason; rsn.title = item.reason;
    var size = document.createElement('span'); size.className = 'htpx-malsize'; size.textContent = fmtSize(item.size);
    var dt   = document.createElement('span'); dt.className = 'htpx-maldate'; dt.textContent = item.mtime ? fmtDate(item.mtime) : '';
    var pBtn = document.createElement('button'); pBtn.type = 'button'; pBtn.className = 'htpx-malprev-btn'; pBtn.textContent = t.preview;
    var safe = document.createElement('button'); safe.type = 'button'; safe.className = 'htpx-malsafe'; safe.textContent = t.safe; safe.title = t.safe;
    var x    = document.createElement('button'); x.type = 'button'; x.className = 'htpx-malx'; x.setAttribute('aria-label', '×'); x.textContent = '×';
    // Pfad/Reason/Meta wachsen+umbrechen intern; die Aktionen bleiben als eigene,
    // nicht-schrumpfende Gruppe IMMER rechts am Rand (kein Umbruch in neue Zeile).
    var info = document.createElement('div'); info.className = 'htpx-malinfo';
    info.appendChild(path); info.appendChild(rsn); info.appendChild(size); info.appendChild(dt);
    var acts = document.createElement('div'); acts.className = 'htpx-malactions';
    acts.appendChild(pBtn); acts.appendChild(safe); acts.appendChild(x);
    head.appendChild(cb); head.appendChild(info); head.appendChild(acts);

    // ── Inhalt-Spoiler: Vollanzeige (readonly) + Sprung zur Fundstelle.
    //    "Bearbeiten" steckt IM Spoiler und oeffnet den randlosen Vollbild-Editor. ──
    var box  = document.createElement('div'); box.className = 'htpx-malbox'; box.style.display = 'none';
    var note = document.createElement('div'); note.className = 'htpx-dim htpx-malboxnote';
    var ta   = document.createElement('textarea'); ta.className = 'htpx-malta'; ta.readOnly = true; ta.spellcheck = false; ta.setAttribute('wrap', 'off');
    ta.value = b64dec(item.preview_b64);
    var ebar = document.createElement('div'); ebar.className = 'htpx-maleditbar';
    var eBtn = document.createElement('button'); eBtn.type = 'button'; eBtn.className = 'htpx-btn2 htpx-btn-s'; eBtn.textContent = t.edit;
    ebar.appendChild(eBtn);
    box.appendChild(note); box.appendChild(ta); box.appendChild(ebar);
    row.appendChild(head); row.appendChild(box);

    var loaded = false, fullContent = b64dec(item.preview_b64), hintLine = 0, isTrunc = false;
    function loadFull(cb){
      if (loaded) { if (cb) { cb(); } return; }
      post('view_malfile', { htp_path: item.path }).then(function(d){
        if (d && d.ok) {
          fullContent = b64dec(d.content_b64);
          hintLine    = d.line || 0;
          isTrunc     = !!d.truncated;
          ta.value    = fullContent;
          note.textContent = isTrunc ? (t.truncated || '') : '';
          if (isTrunc) { eBtn.disabled = true; eBtn.title = t.truncated || ''; }
          loaded = true;
        }
        if (cb) { cb(); }
      }).catch(function(){ if (cb) { cb(); } });
    }
    function jumpToHint(){
      if (!hintLine) { ta.scrollTop = 0; return; }
      var lines = ta.value.split('\n'), startPos = 0, i;
      for (i = 0; i < hintLine - 1 && i < lines.length; i++) { startPos += lines[i].length + 1; }
      var endPos = startPos + ((lines[hintLine - 1] || '').length);
      try { ta.focus(); ta.setSelectionRange(startPos, endPos); } catch (e) {}
    }
    pBtn.addEventListener('click', function(){
      if (box.style.display === 'block') { box.style.display = 'none'; return; }
      box.style.display = 'block';
      loadFull(function(){ jumpToHint(); });
    });
    eBtn.addEventListener('click', function(){
      if (eBtn.disabled) { return; }
      box.style.display = 'block';
      loadFull(function(){
        if (isTrunc) { note.textContent = t.truncated || ''; return; }
        fsOpen({
          path: item.path,
          hintLine: hintLine,
          content: fullContent,
          onSaved: function(newContent){ fullContent = newContent; ta.value = newContent; },
          onClean: function(){
            if (row.parentNode) { row.parentNode.removeChild(row); }
            count--; updateHead();
          }
        });
      });
    });
    safe.addEventListener('click', function(){
      safe.disabled = true;
      post('mal_wl_add', { htp_path: item.path }).then(function(d){
        if (d && d.ok) {
          if (row.parentNode) { row.parentNode.removeChild(row); }
          count--; updateHead();
          renderWhitelist(d.list);
        } else { safe.disabled = false; }
      }).catch(function(){ safe.disabled = false; });
    });
    // Einzel-Loeschen: KEINE Bestaetigung (serverseitig erneut gegen die
    // Malware-Signatur verifiziert -> es kann nur echte Malware geloescht werden).
    x.addEventListener('click', function(){
      x.disabled = true;
      post('delete_malfile', { htp_path: item.path }).then(function(d){
        if (d && d.ok) {
          if (row.parentNode) { row.parentNode.removeChild(row); }
          count--; updateHead();
        } else { x.disabled = false; }
      }).catch(function(){ x.disabled = false; });
    });
    return row;
  }
  // ── Quarantaene (vom Hoster gesperrte Dateien): Liste + Aktionen (Pruefen/Loeschen/Wiederherstellen) ──
  function updateQuarCount(){
    var n = quarTotal || quarCount;
    if (quarCountEl) { quarCountEl.textContent = n > 0 ? (t.quarcountlbl || '(%d)').replace('%d', n) : ''; }
    if (quarWrap) { quarWrap.style.display = quarCount > 0 ? '' : 'none'; }
  }
  function quarRemove(row){
    if (row && row.parentNode) { row.parentNode.removeChild(row); }
    if (quarCount > 0) { quarCount--; }
    if (quarTotal > 0) { quarTotal--; }
    updateQuarCount();
  }
  function appendQuar(items){
    if (!items || !items.length || !quarList) { return; }
    for (var i = 0; i < items.length; i++) {
      var it = items[i];
      if (!it || !it.path || quarSeen[it.path]) { continue; }
      quarSeen[it.path] = 1;
      quarList.appendChild(makeQuarRow(it));
      quarCount++;
    }
    updateQuarCount();
  }
  function makeQuarRow(item){
    var row  = document.createElement('div'); row.className = 'htpx-malrow htpx-quarrow';
    var head = document.createElement('div'); head.className = 'htpx-malrow-head';
    var info = document.createElement('div'); info.className = 'htpx-malinfo';
    var path = document.createElement('span'); path.className = 'htpx-malpath'; path.textContent = item.path; path.title = item.path;
    var stat = document.createElement('span'); stat.className = 'htpx-quarstat'; stat.textContent = t.quarlocked;
    var size = document.createElement('span'); size.className = 'htpx-malsize'; size.textContent = fmtSize(item.size);
    var dt   = document.createElement('span'); dt.className = 'htpx-maldate'; dt.textContent = item.mtime ? fmtDate(item.mtime) : '';
    info.appendChild(path); info.appendChild(stat); info.appendChild(size); info.appendChild(dt);
    var acts = document.createElement('div'); acts.className = 'htpx-malactions';
    var chk  = document.createElement('button'); chk.type = 'button'; chk.className = 'htpx-btn2 htpx-btn-s'; chk.textContent = t.quarcheck;
    var edit = document.createElement('button'); edit.type = 'button'; edit.className = 'htpx-btn2 htpx-btn-s'; edit.textContent = t.quaredit;
    var rst  = document.createElement('button'); rst.type = 'button'; rst.className = 'htpx-btn2 htpx-btn-s'; rst.textContent = t.quarrestore;
    var del  = document.createElement('button'); del.type = 'button'; del.className = 'htpx-btn htpx-btn-danger htpx-btn-s'; del.textContent = t.quardel;
    acts.appendChild(chk); acts.appendChild(edit); acts.appendChild(rst); acts.appendChild(del);
    head.appendChild(info); head.appendChild(acts);
    row.appendChild(head);

    function setStat(cls, txt){ stat.className = 'htpx-quarstat' + (cls ? ' ' + cls : ''); stat.textContent = txt; }

    // Pruefen: kurz entsperr-scannen (einordnen). Der Server entsperrt nur auf 0400 und sperrt sofort wieder.
    chk.addEventListener('click', function(){
      chk.disabled = true; setStat('', '…');
      post('quar_scan', { htp_path: item.path }).then(function(d){
        chk.disabled = false;
        if (!d || !d.ok || !d.readable) { setStat('htpx-quar-unk', t.quarunverif); }
        else if (d.malware) { setStat('htpx-quar-mal', t.quarmalware); }
        else { setStat('htpx-quar-clean', t.quarclean); }
      }).catch(function(){ chk.disabled = false; setStat('htpx-quar-unk', t.quarunverif); });
    });

    // Bearbeiten: im Fullsize-Editor oeffnen (Injektion entfernen) - wie bei normalen Funden. Der Server
    // entsperrt zum Lesen/Schreiben und sperrt bei noch vorhandenem Schadcode wieder. Ist die Datei
    // unauffaellig/nicht als Schaddatei lesbar, kommt kein Inhalt -> Hinweis Richtung Wiederherstellen.
    // Wird sauber gespeichert, ist die Datei entsperrt/bereinigt -> Zeile entfernen.
    edit.addEventListener('click', function(){
      edit.disabled = true; setStat('', '…');
      post('view_malfile', { htp_path: item.path }).then(function(d){
        edit.disabled = false;
        if (!d || !d.ok) { setStat('htpx-quar-clean', t.quarclean); return; }
        setStat('htpx-quar-mal', t.quarmalware);
        fsOpen({
          path: item.path,
          hintLine: d.line || 0,
          content: b64dec(d.content_b64),
          onSaved: function(){},
          onClean: function(){ quarRemove(row); }
        });
      }).catch(function(){ edit.disabled = false; setStat('htpx-quar-unk', t.quarunverif); });
    });

    // Rechte wiederherstellen (faelschlich gesperrte, unauffaellige Datei -> Seite laeuft wieder).
    rst.addEventListener('click', function(){
      if (!window.confirm(t.quarrestconfirm)) { return; }
      rst.disabled = true;
      post('quar_restore', { htp_path: item.path }).then(function(d){
        if (d && d.ok) { quarRemove(row); }
        else if (d && d.code === 'is_malware') { rst.disabled = false; setStat('htpx-quar-mal', t.quarmalware); }
        else { rst.disabled = false; setStat('htpx-quar-unk', t.quarunverif); }
      }).catch(function(){ rst.disabled = false; });
    });

    // Loeschen: der Server loescht NICHT blind - ist die Datei entsperrbar UND unauffaellig, verweigert
    // er (code looks_clean) und wir raten zur Wiederherstellung.
    del.addEventListener('click', function(){
      del.disabled = true;
      post('quar_delete', { htp_path: item.path }).then(function(d){
        if (d && d.ok) { quarRemove(row); }
        else if (d && d.code === 'looks_clean') { del.disabled = false; setStat('htpx-quar-clean', t.quarlooksclean); }
        else { del.disabled = false; }
      }).catch(function(){ del.disabled = false; });
    });

    return row;
  }
  function makeWlItem(path){
    var li = document.createElement('li'); li.className = 'htpx-wlitem'; li.setAttribute('data-path', path);
    var sp = document.createElement('span'); sp.className = 'htpx-mono'; sp.textContent = path;
    var rx = document.createElement('button'); rx.type = 'button'; rx.className = 'htpx-wlx'; rx.title = t.wlremove; rx.textContent = '×';
    li.appendChild(sp); li.appendChild(rx);
    return li;
  }
  function renderWhitelist(arr){
    if (!wllist || !arr) { return; }
    wllist.textContent = '';
    for (var i = 0; i < arr.length; i++) { wllist.appendChild(makeWlItem(arr[i])); }
    if (wlcount) { wlcount.textContent = '(' + arr.length + ')'; }
  }
  function render(items){
    list.textContent = '';
    count = items.length;
    lastCb = null;
    for (var i = 0; i < items.length; i++) { list.appendChild(makeRow(items[i])); }
    updateHead();
  }
  function updateSortUI(){
    if (sortPathBtn) { sortPathBtn.textContent = sortPathBase + (sortKey === 'path' ? (sortDir > 0 ? ' ▲' : ' ▼') : ''); }
    if (sortDateBtn) { sortDateBtn.textContent = sortDateBase + (sortKey === 'mtime' ? (sortDir > 0 ? ' ▲' : ' ▼') : ''); }
  }
  function sortAndRender(){
    if (!sortKey) { return; }
    allHits.sort(function(a, b){
      var r;
      if (sortKey === 'mtime') { r = (+a.mtime || 0) - (+b.mtime || 0); }
      else { r = String(a.path).localeCompare(String(b.path)); }
      return r * sortDir;
    });
    render(allHits);
    updateSortUI();
  }
  function setSort(key){
    if (sortKey === key) { sortDir = -sortDir; } else { sortKey = key; sortDir = 1; }
    sortAndRender();
  }
  if (sortPathBtn) { sortPathBtn.addEventListener('click', function(){ setSort('path'); }); }
  if (sortDateBtn) { sortDateBtn.addEventListener('click', function(){ setSort('mtime'); }); }
  delAll.addEventListener('click', function(){
    if (!window.confirm(t.confirmall)) { return; }
    // Bereits gelistete (verifizierte) Treffer ueber den Einzel-Verifikations-
    // Endpunkt loeschen - KEIN erneuter Voll-Scan (sonst Timeout auf Riesen-Sites).
    // In Stapeln von 450 (der Server kappt 500/Request) und AUTOMATISCH fortgesetzt,
    // bis WIRKLICH alle gelisteten Treffer weg sind - auch tausende eingeschleuste
    // .htaccess. Serverseitig wird jede Datei erneut gegen die Signatur geprueft.
    var origLabel = delAll.textContent, total = 0;
    delAll.disabled = true;
    function round(){
      var boxes = allBoxes(), batch = [], i;
      if (!boxes.length) {                 // alles weg -> fertig
        delAll.disabled = false; delAll.textContent = origLabel; lastCb = null; updateHead();
        return;
      }
      for (i = 0; i < boxes.length && i < 450; i++) { batch.push(boxes[i]._malpath); }
      post('delete_malfiles_selected', { htp_paths: batch.join('\n') }).then(function(d){
        var ok = (d && d.ok_paths) ? d.ok_paths : [];
        removeRowsByPath(ok);
        total += ok.length;
        delAll.textContent = total + ' ' + t.deleted;
        if (ok.length > 0) {
          round();                         // weiter, bis nichts mehr Loeschbares bleibt
        } else {
          // kein Fortschritt -> Rest ist unloeschbar (Rechte/Fremdbesitz) und bleibt
          // sichtbar fuer manuelle Klaerung.
          delAll.disabled = false; delAll.textContent = origLabel; lastCb = null; updateHead();
        }
      }).catch(function(){ delAll.disabled = false; delAll.textContent = origLabel; updateSel(); });
    }
    round();
  });
  // ── Batch: Alle markieren / Markierte loeschen / Markierte whitelisten ──
  function removeRowsByPath(okp){
    var boxes = allBoxes(), i, r;
    for (i = boxes.length - 1; i >= 0; i--) {
      if (okp.indexOf(boxes[i]._malpath) !== -1) {
        r = boxes[i]._malrow;
        if (r && r.parentNode) { r.parentNode.removeChild(r); count--; }
      }
    }
  }
  if (selAll) {
    selAll.addEventListener('click', function(){
      var boxes = allBoxes(), c = selAll.checked, i;
      for (i = 0; i < boxes.length; i++) { boxes[i].checked = c; }
      lastCb = null; updateSel();
    });
  }
  if (delSel) {
    delSel.addEventListener('click', function(){
      var sel = checkedBoxes(); if (!sel.length) { return; }
      var paths = sel.map(function(cb){ return cb._malpath; });
      delSel.disabled = true; if (wlSel) { wlSel.disabled = true; }
      // KEINE Bestaetigung: serverseitig wird jede Datei erneut gegen die
      // Malware-Signatur verifiziert -> nur echte Malware kann geloescht werden.
      post('delete_malfiles_selected', { htp_paths: paths.join('\n') }).then(function(d){
        removeRowsByPath((d && d.ok_paths) ? d.ok_paths : []);
        lastCb = null; updateHead();
      }).catch(function(){ updateSel(); });
    });
  }
  if (wlSel) {
    wlSel.addEventListener('click', function(){
      var sel = checkedBoxes(); if (!sel.length) { return; }
      var paths = sel.map(function(cb){ return cb._malpath; });
      if (delSel) { delSel.disabled = true; } wlSel.disabled = true;
      post('mal_wl_add', { htp_paths: paths.join('\n') }).then(function(d){
        if (d && d.ok) { removeRowsByPath(paths); renderWhitelist(d.list); }
        lastCb = null; updateHead();
      }).catch(function(){ updateSel(); });
    });
  }
  // Whitelist entfernen (Delegation -> wirkt auch fuer server-gerenderte Eintraege)
  if (wllist) {
    wllist.addEventListener('click', function(ev){
      var btn = ev.target;
      while (btn && btn !== wllist && !(btn.className && btn.className.indexOf('htpx-wlx') !== -1)) { btn = btn.parentNode; }
      if (!btn || btn === wllist) { return; }
      var li = btn.parentNode;
      var path = li.getAttribute('data-path');
      btn.disabled = true;
      post('mal_wl_remove', { htp_path: path }).then(function(d){
        if (d && d.ok) { renderWhitelist(d.list); } else { btn.disabled = false; }
      }).catch(function(){ btn.disabled = false; });
    });
  }
  // Whitelist manuell ergaenzen
  var wlAddBtn = document.getElementById('htpx-wladd');
  var wlInput  = document.getElementById('htpx-wlinput');
  if (wlAddBtn && wlInput) {
    wlAddBtn.addEventListener('click', function(){
      var v = (wlInput.value || '').replace(/^\s+|\s+$/g, '');
      if (!v) { return; }
      wlAddBtn.disabled = true;
      post('mal_wl_add', { htp_path: v }).then(function(d){
        if (d && d.ok) { renderWhitelist(d.list); wlInput.value = ''; }
        wlAddBtn.disabled = false;
      }).catch(function(){ wlAddBtn.disabled = false; });
    });
    wlInput.addEventListener('keydown', function(ev){ if (ev.keyCode === 13) { ev.preventDefault(); wlAddBtn.click(); } });
  }
  // ── Stueckweiser Scan: jeder Abschnitt ist kurz (kein Server-Timeout),
  //    Treffer + Fortschritt kommen pro Abschnitt; der naechste Abschnitt
  //    setzt per scan_id fort. EIN Button, der automatisch das Richtige tut
  //    (erster Scan/nach Update/woechentlich = voll, sonst Delta). Der Balken
  //    ist bewusst UNbestimmt (animiert) + Live-Zaehler - ein verzeichnis-
  //    basierter Prozentwert wuerde bei ungleicher Datei-Verteilung "fast
  //    fertig" vortaeuschen. Beide Werte fallen ohnehin an -> keine Kosten.
  var scanId = null, scanItems = 0, scanMode = 'full', scanFiles = 0, scanSkipped = 0;
  function reEnable(){
    startBtn.disabled = false;
    startBtn.textContent = t.rescan;
  }
  function appendItems(items){
    if (!items || !items.length) { return; }
    for (var i = 0; i < items.length; i++) { allHits.push(items[i]); list.appendChild(makeRow(items[i])); }
    scanItems += items.length; count = scanItems;
  }
  function setProgress(d){
    scanFiles   = (+d.files || 0);
    scanSkipped = (+d.skipped || 0);
    if (progBox) { progBox.style.display = ''; }
    if (progText) {
      var s = (t.progress || '%f / %h').replace('%f', scanFiles).replace('%h', scanItems);
      if (scanSkipped > 0 && t.progskip) { s += ' ' + t.progskip.replace('%s', scanSkipped); }
      progText.textContent = s;
    }
  }
  function scanFail(){
    reEnable();
    setState('htpx-bad', t.err);
    if (progBox) { progBox.style.display = 'none'; }
  }
  function scanStep(extra){
    post('malscan', extra).then(function(d){
      if (!d || !d.ok) { scanFail(); return; }
      scanId = d.scan_id; scanMode = d.mode || scanMode;
      appendItems(d.items);
      if (typeof d.quar_total === 'number') { quarTotal = d.quar_total; }
      appendQuar(d.quarantine);
      setProgress(d);
      if (!d.done) {
        scanStep({ htp_scan_id: scanId });
      } else {
        if (progBox) { progBox.style.display = 'none'; }
        reEnable();
        updateHead();
        // Transparent, WAS gelaufen ist (nimmt das "warum mal schnell, mal
        // langsam?"-Raetsel): Komplett-Scan mit Dateizahl bzw. Schnell-Scan
        // mit Anzahl uebersprungener (unveraenderter) Dateien.
        if (malNote) {
          var note = '';
          if (scanMode === 'delta' && t.modedelta) {
            note = t.modedelta.replace('%s', scanSkipped);
          } else if (t.modefull) {
            note = t.modefull.replace('%f', scanFiles);
          }
          if (note) { malNote.textContent = note; malNote.style.display = ''; }
          else { malNote.style.display = 'none'; }
        }
        // Mini-Link "Vollstaendigen Scan starten" NUR nach einem Delta-Scan:
        // damit lassen sich bei gleicher Version, aber verbesserten Mustern die
        // uebersprungenen Dateien noch einmal komplett pruefen.
        if (malFull) { malFull.style.display = (scanMode === 'delta') ? '' : 'none'; }
      }
    }).catch(scanFail);
  }
  function start(forceFull){
    startBtn.disabled = true;
    status.style.display = '';
    setState('', t.scanning);
    list.textContent = '';
    foot.style.display = 'none';
    if (malBackup) { malBackup.style.display = 'none'; }
    if (toolbar) { toolbar.style.display = 'none'; }
    if (sortBar) { sortBar.style.display = 'none'; }
    if (malNote) { malNote.style.display = 'none'; }
    if (malFull) { malFull.style.display = 'none'; }
    scanId = null; scanItems = 0; count = 0; lastCb = null; oldestMtime = 0;
    scanFiles = 0; scanSkipped = 0;
    allHits = []; sortKey = ''; sortDir = 1; updateSortUI();
    if (quarList) { quarList.textContent = ''; } quarSeen = {}; quarCount = 0; quarTotal = 0;
    if (quarWrap) { quarWrap.style.display = 'none'; }
    scanStep(forceFull ? { htp_scan_mode: 'full' } : {});
  }
  startBtn.addEventListener('click', function(){ start(false); });
  if (malFull) {
    malFull.addEventListener('click', function(ev){ ev.preventDefault(); start(true); });
  }
  // Deep-Link vom htprotect.app-Dashboard (?autoscan=1): den Datei-Scan SOFORT automatisch
  // starten (schneller Delta-Scan - unveraenderte Dateien werden uebersprungen) und zum Bereich
  // scrollen, damit die Funde ohne manuellen Klick erscheinen. Strikt fail-safe.
  try {
    if (/[?&]autoscan=1(?:&|$)/.test(window.location.search)) {
      var _wrap = document.getElementById('htp-a-malfiles');
      if (_wrap && _wrap.scrollIntoView) { _wrap.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
      start(false);
    }
  } catch (e) {}
})();

/* ── Kerndatei-Abgleich (eigenstaendiges IIFE, gleiches scanStep/start-Idiom) ── */
(function(){
  var form = document.getElementById('htpx-coreform');
  var startBtn = document.getElementById('htpx-corestart');
  var status = document.getElementById('htpx-corestatus');
  var prog = document.getElementById('htpx-coreprog');
  var phaseEl = document.getElementById('htpx-corephase');
  var pctEl = document.getElementById('htpx-corepct');
  var barEl = document.getElementById('htpx-corebar');
  var note = document.getElementById('htpx-corenote');
  var errBox = document.getElementById('htpx-coreerr');
  var reloadBtn = document.getElementById('htpx-corereload');
  var toolbar = document.getElementById('htpx-coretoolbar');
  var selInfo = document.getElementById('htpx-coreselinfo');
  var restAllBtn = document.getElementById('htpx-corerestall');
  var list = document.getElementById('htpx-corelist');
  var i18n = document.getElementById('htpx-corei18n');
  if (!form || !startBtn || !list || !i18n || !window.fetch || !window.FormData) { if (startBtn) { startBtn.disabled = true; } return; }
  var action = form.getAttribute('action');
  function a(n){ return i18n.getAttribute('data-' + n) || ''; }
  var t = { scanning:a('scanning'), rescan:a('rescan'), clean:a('clean'), found:a('found'), summary:a('summary'), err:a('err'),
    restore:a('restore'), restoring:a('restoring'), restored:a('restored'), restfail:a('restfail'),
    kchanged:a('kchanged'), kmissing:a('kmissing'), restallc:a('restallc'), patched:a('patched'),
    diff:a('diff'), diffhide:a('diffhide'), diffloading:a('diffloading'), diffbinary:a('diffbinary'), diffnone:a('diffnone'),
    ignore:a('ignore'), ignoring:a('ignoring'), ignfail:a('ignfail'), ignconf:a('ignconf'), unignore:a('unignore'), ignnote:a('ignnote') };
  var ver = a('ver'), scanId = null, devs = [];
  var ignBox = document.getElementById('htpx-coreignored');
  var ignList = document.getElementById('htpx-coreignlist');

  function post(task, extra){
    var fd = new FormData(form); fd.set('htp_task', task);
    // Datei-Pfade HEX-kodiert senden (htp_path -> htp_pathx, htp_paths -> htp_pathsx), damit eine
    // Server-Firewall (z. B. mod_security) im POST-Body kein Angriffsmuster im Schaddatei-Pfad sieht
    // und die legitime Aktion nicht mit 403 blockt; der Server dekodiert zentral. UTF-8-fest (Umlaute).
    if (extra) { var htpHex = function(s){ var b = new TextEncoder().encode(String(s)); var o=''; for (var i=0;i<b.length;i++){ var h=b[i].toString(16); o += (h.length<2?'0':'')+h; } return o; }; for (var k in extra) { if (extra.hasOwnProperty(k)) { if (k === 'htp_path' || k === 'htp_paths') { fd.set(k + 'x', htpHex(extra[k])); } else { fd.set(k, extra[k]); } } } }
    return fetch(action, { method:'POST', body: fd, credentials:'same-origin' }).then(function(r){ return r.json(); });
  }
  function setState(cls, text){
    status.style.display = ''; status.className = 'htpx-scanstate' + (cls ? ' ' + cls : ''); status.textContent = '';
    if (cls === '') { var sp = document.createElement('span'); sp.className = 'htpx-spinner'; sp.setAttribute('aria-hidden','true'); status.appendChild(sp); status.appendChild(document.createTextNode(' ')); }
    status.appendChild(document.createTextNode(text || ''));
  }
  function fmtSize(n){ n = +n || 0; if (n < 1024) { return n + ' B'; } if (n < 1048576) { return (n/1024).toFixed(1) + ' KB'; } return (n/1048576).toFixed(1) + ' MB'; }
  function fmtDate(ts){ ts = +ts || 0; if (!ts) { return ''; } try { return new Date(ts*1000).toLocaleString(); } catch(e){ return ''; } }

  function makeRow(item){
    var row = document.createElement('div'); row.className = 'htpx-malrow'; row.setAttribute('data-path', item.path);
    var head = document.createElement('div'); head.className = 'htpx-malrow-head';
    var info = document.createElement('div'); info.className = 'htpx-malinfo';
    var p = document.createElement('span'); p.className = 'htpx-malpath'; p.textContent = item.path; info.appendChild(p);
    var badge = document.createElement('span'); badge.className = 'htpx-malreason'; badge.textContent = (item.kind === 'missing') ? t.kmissing : t.kchanged; info.appendChild(badge);
    if (item.kind !== 'missing') {
      var sz = document.createElement('span'); sz.className = 'htpx-malsize'; sz.textContent = fmtSize(item.size); info.appendChild(sz);
      var dt = document.createElement('span'); dt.className = 'htpx-maldate'; dt.textContent = fmtDate(item.mtime); info.appendChild(dt);
    }
    var act = document.createElement('div'); act.className = 'htpx-malactions';
    var box = document.createElement('div'); box.className = 'htpx-corediff'; box.style.display = 'none';
    if (item.kind === 'changed') {
      var db = document.createElement('button'); db.type = 'button'; db.className = 'htpx-btn2 htpx-btn-s'; db.textContent = t.diff;
      db.addEventListener('click', function(){ toggleDiff(box, item.path, db); });
      act.appendChild(db);
      // Bewusst akzeptieren (checksum-gebunden) - fuer legitime Fremd-Patches (z. B. Patchman).
      var ib = document.createElement('button'); ib.type = 'button'; ib.className = 'htpx-btn2 htpx-btn-s'; ib.textContent = t.ignore;
      ib.addEventListener('click', function(){ ignoreOne(row, item.path, ib); });
      act.appendChild(ib);
    }
    var rb = document.createElement('button'); rb.type = 'button'; rb.className = 'htpx-btn2 htpx-btn-s'; rb.textContent = t.restore;
    rb.addEventListener('click', function(){ restoreOne(row, item.path, rb); });
    act.appendChild(rb); head.appendChild(info); head.appendChild(act); row.appendChild(head); row.appendChild(box);
    return row;
  }
  function toggleDiff(box, path, btn){
    if (box.style.display !== 'none') { box.style.display = 'none'; btn.textContent = t.diff; return; }
    box.textContent = ''; box.style.display = '';
    var ld = document.createElement('div'); ld.className = 'htpx-dim'; ld.textContent = t.diffloading; box.appendChild(ld);
    btn.disabled = true;
    post('corescan_diff', { htp_path: path, htp_ver: ver }).then(function(d){
      btn.disabled = false; btn.textContent = t.diffhide; box.textContent = '';
      if (!d || !d.ok) { addDimLine(box, t.err); return; }
      if (d.binary) { addDimLine(box, t.diffbinary); return; }
      renderDiff(box, d.added || [], d.removed || []);
    }).catch(function(){ btn.disabled = false; box.textContent = ''; addDimLine(box, t.err); });
  }
  function addDimLine(box, text){ var e = document.createElement('div'); e.className = 'htpx-dim'; e.textContent = text; box.appendChild(e); }
  function renderDiff(box, added, removed){
    if (!added.length && !removed.length) { addDimLine(box, t.diffnone); return; }
    var pre = document.createElement('pre'); pre.className = 'htpx-corediff-pre'; var i;
    for (i = 0; i < removed.length; i++) { var lr = document.createElement('div'); lr.className = 'htpx-diff-rem'; lr.textContent = '- ' + removed[i]; pre.appendChild(lr); }
    for (i = 0; i < added.length; i++) { var la = document.createElement('div'); la.className = 'htpx-diff-add'; la.textContent = '+ ' + added[i]; pre.appendChild(la); }
    box.appendChild(pre);
  }
  function appendItems(items){ if (!items || !items.length) { return; } for (var i = 0; i < items.length; i++) { devs.push(items[i]); list.appendChild(makeRow(items[i])); } }
  function removeRows(oks){ var k = list.children, i; for (i = k.length - 1; i >= 0; i--) { if (oks.indexOf(k[i].getAttribute('data-path')) !== -1) { list.removeChild(k[i]); } } }

  function setProg(d){
    prog.style.display = ''; var pct = Math.round((+d.progress || 0) * 100);
    if (phaseEl) { phaseEl.textContent = d.label || ''; }
    if (pctEl) { pctEl.textContent = pct + '%'; }
    if (barEl) { barEl.style.width = pct + '%'; }
  }
  function reEnable(){ startBtn.disabled = false; startBtn.textContent = t.rescan; }
  function scanFail(label){ reEnable(); prog.style.display = 'none'; setState('htpx-bad', label || t.err); if (errBox) { errBox.style.display = ''; } }

  function restoreOne(row, path, btn){
    btn.disabled = true; btn.textContent = t.restoring;
    post('corescan_restore', { htp_path: path, htp_ver: ver }).then(function(d){
      if (d && d.ok && d.ok_paths && d.ok_paths.indexOf(path) !== -1) {
        if (row && row.parentNode) { row.parentNode.removeChild(row); }
        devs = devs.filter(function(x){ return x.path !== path; }); afterRestore();
      } else { btn.disabled = false; btn.textContent = t.restfail; }
    }).catch(function(){ btn.disabled = false; btn.textContent = t.restfail; });
  }
  function afterRestore(){
    if (!devs.length) { setState('htpx-ok', t.restored); if (toolbar) { toolbar.style.display = 'none'; } }
    else if (selInfo) { selInfo.textContent = t.found.replace('%d', devs.length); }
  }
  // Nach dem Ignorieren: keine "wiederhergestellt"-Meldung, sondern neutraler Rest-Stand.
  function afterResolve(){
    if (!devs.length) { setState('htpx-ok', t.clean); if (toolbar) { toolbar.style.display = 'none'; } }
    else if (selInfo) { selInfo.textContent = t.found.replace('%d', devs.length); }
  }
  // Eine bewusst akzeptierte Datei in die (unten stehende) Ignoriert-Liste aufnehmen (Duplikat-fest).
  function addIgnoredRow(path){
    if (!ignList) { return; }
    var ex = ignList.children, i;
    for (i = 0; i < ex.length; i++) { if (ex[i].getAttribute('data-path') === path) { return; } }
    var row = document.createElement('div'); row.className = 'htpx-coreignrow'; row.setAttribute('data-path', path);
    var p = document.createElement('span'); p.className = 'htpx-malpath'; p.textContent = path; row.appendChild(p);
    var b = document.createElement('button'); b.type = 'button'; b.className = 'htpx-btn2 htpx-btn-s htpx-coreunign'; b.textContent = t.unignore;
    b.addEventListener('click', function(){ unignoreOne(row, path, b); });
    row.appendChild(b); ignList.appendChild(row);
    if (ignBox) { ignBox.style.display = ''; if (ignBox.open === false) { ignBox.open = true; } }
  }
  function ignoreOne(row, path, btn){
    if (t.ignconf && !window.confirm(t.ignconf)) { return; }
    btn.disabled = true; btn.textContent = t.ignoring;
    post('corescan_ignore', { htp_path: path, htp_ver: ver }).then(function(d){
      if (d && d.ok && (+d.ignored || 0) > 0) {
        if (row && row.parentNode) { row.parentNode.removeChild(row); }
        devs = devs.filter(function(x){ return x.path !== path; });
        addIgnoredRow(path); afterResolve();
      } else { btn.disabled = false; btn.textContent = t.ignfail; }
    }).catch(function(){ btn.disabled = false; btn.textContent = t.ignfail; });
  }
  function unignoreOne(row, path, btn){
    btn.disabled = true;
    post('corescan_unignore', { htp_path: path, htp_ver: ver }).then(function(d){
      if (d && d.ok) {
        if (row && row.parentNode) { row.parentNode.removeChild(row); }
        if (ignList && ignBox && ignList.children.length === 0) { ignBox.style.display = 'none'; }
      } else { btn.disabled = false; }
    }).catch(function(){ btn.disabled = false; });
  }
  // Serverseitig gerenderte "wieder pruefen"-Knoepfe verdrahten (Bestand beim Seitenaufruf).
  if (ignList) {
    var _ir = ignList.querySelectorAll('.htpx-coreignrow');
    for (var _i = 0; _i < _ir.length; _i++) {
      (function(rowEl){
        var b = rowEl.querySelector('.htpx-coreunign');
        if (b) { b.addEventListener('click', function(){ unignoreOne(rowEl, rowEl.getAttribute('data-path'), b); }); }
      })(_ir[_i]);
    }
  }
  function restoreAll(){
    if (!devs.length) { return; }
    if (t.restallc && !window.confirm(t.restallc)) { return; }
    restAllBtn.disabled = true; var paths = devs.map(function(x){ return x.path; });
    function batch(){
      var slice = paths.slice(0, 100);
      post('corescan_restore', { htp_paths: slice.join('\n'), htp_ver: ver }).then(function(d){
        var ok = (d && d.ok_paths) ? d.ok_paths : [];
        removeRows(ok);
        devs = devs.filter(function(x){ return ok.indexOf(x.path) === -1; });
        paths = paths.filter(function(x){ return ok.indexOf(x) === -1; });
        if (paths.length && ok.length) { batch(); }
        else { restAllBtn.disabled = false; if (!devs.length) { setState('htpx-ok', t.restored); if (toolbar) { toolbar.style.display = 'none'; } } else { afterRestore(); } }
      }).catch(function(){ restAllBtn.disabled = false; });
    }
    batch();
  }

  function finish(d){
    prog.style.display = 'none'; reEnable();
    var c = d.counts || { ok:0, changed:0, missing:0 }, dev = (+c.changed || 0) + (+c.missing || 0);
    if (dev > 0) {
      setState('htpx-bad', '⚠ ' + t.found.replace('%d', dev));
      if (toolbar) { toolbar.style.display = 'flex'; }
      if (selInfo) { selInfo.textContent = t.found.replace('%d', dev); }
    } else {
      setState('htpx-ok', t.clean);
      if (toolbar) { toolbar.style.display = 'none'; }
    }
    if (note) {
      var s = t.summary.replace('%o', (+c.ok || 0)).replace('%c', (+c.changed || 0)).replace('%m', (+c.missing || 0));
      if ((+c.patched || 0) > 0 && t.patched) { s += ' · ' + t.patched.replace('%p', (+c.patched || 0)); }
      if ((+c.ignored || 0) > 0 && t.ignnote) { s += ' · ' + t.ignnote.replace('%i', (+c.ignored || 0)); }
      note.textContent = s; note.style.display = '';
    }
  }
  function scanStep(extra){
    post('corescan', extra).then(function(d){
      if (!d) { scanFail(); return; }
      if (d.phase === 'error' || !d.ok) { scanFail(d.label); return; }
      scanId = d.scan_id; if (d.version) { ver = d.version; }
      setProg(d); appendItems(d.items);
      // Nachpruefung hat Dateien als offiziellen Vorwaerts-Patch entlarvt -> Zeilen wieder raus.
      if (d.patched && d.patched.length) { removeRows(d.patched); devs = devs.filter(function(x){ return d.patched.indexOf(x.path) === -1; }); }
      if (!d.done) { scanStep({ htp_scan_id: scanId }); } else { finish(d); }
    }).catch(function(){ scanFail(); });
  }
  function start(){
    startBtn.disabled = true; setState('', t.scanning);
    list.textContent = ''; devs = []; if (note) { note.style.display = 'none'; } if (toolbar) { toolbar.style.display = 'none'; }
    if (errBox) { errBox.style.display = 'none'; }
    scanId = null; if (barEl) { barEl.style.width = '0%'; }
    scanStep({});
  }
  startBtn.addEventListener('click', start);
  if (restAllBtn) { restAllBtn.addEventListener('click', restoreAll); }
  if (reloadBtn) { reloadBtn.addEventListener('click', start); }
  // Deep-Link vom htprotect.app-Dashboard (?autointegrity=1): den Kerndatei-Abgleich SOFORT
  // automatisch starten und zum Bereich scrollen, damit die Abweichungen ohne manuellen Klick
  // erscheinen. Strikt fail-safe (spiegelt die autoscan-Mechanik der Datei-Suche).
  try {
    if (/[?&]autointegrity=1(?:&|$)/.test(window.location.search)) {
      var _cw = document.getElementById('htp-a-core');
      if (_cw && _cw.scrollIntoView) { _cw.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
      start();
    }
  } catch (e) {}
})();
</script>
        <?php
    }
}
