<?php

/*
 * BlackKitt
 *
 * Author:    Kitt Apps
 * Website:   https://kittapps.com
 * Support:   support@kittapps.com
 *
 * Copyright (c) 2026 Kitt Apps. All rights reserved.
 *
 * This software is licensed, not sold. Use is governed by the licence you
 * purchased (Regular or Extended). Redistributing or reselling it, in whole
 * or in part, is not permitted.
 */

/**
 * BlackKitt edge {{VERSION}} for "{{SITE_LABEL}}".
 *
 * Upload this single file as index.php to the site's web root (an empty folder
 * such as public_html) and open the site in a browser. That's the whole install.
 * Pages come from the BlackKitt Hub and are cached on this server; if the Hub
 * is ever unreachable, cached pages keep being served.
 */

define('BK_HUB', '{{HUB_URL}}');
define('BK_KEY', '{{API_KEY}}');
define('BK_SECRET', '{{SECRET}}');
define('BK_VERSION', '{{VERSION}}');
define('BK_NAME', {{SITE_NAME}});
define('BK_DIR', __DIR__ . '/bk-cache');
define('BK_SYNC_EVERY', 120);
define('BK_KEEP_FOR', 172800);
define('BK_MAX_FILES', 50000);
define('BK_VARY', ['page', 'q', 'genre', 'year', 'country', 'sort', 'type', 'season', 'kind', 'dept', 'since', 'after']);

error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
ini_set('display_errors', '0');

bk_boot();

function bk_boot()
{
    if (!bk_installed() && !bk_install()) {
        return;
    }

    $path = '/' . ltrim((string) parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH), '/');

    if (isset($_GET['bk-agent']) && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
        bk_agent();
        return;
    }

    if (strpos($path, '/themes/') === 0 || strpos($path, '/storage/') === 0 || strpos($path, '/build/') === 0) {
        bk_static($path);
        return;
    }

    if (strpos($path, '/_bk/') === 0 || $path === '/robots.txt' || $path === '/sitemap.xml' || strpos($path, '/sitemaps/') === 0) {
        bk_runtime(ltrim(strpos($path, '/_bk/') === 0 ? substr($path, 5) : $path, '/'));
        bk_after_response();
        return;
    }

    bk_page($path);
    bk_after_response();
}

/* ---------------------------------------------------------------- pages */

function bk_page($path)
{
    $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';

    if ($method !== 'GET' && $method !== 'HEAD') {
        http_response_code(405);
        header('Allow: GET, HEAD');
        return;
    }

    $query = array_intersect_key($_GET, array_flip(BK_VARY));
    ksort($query);
    $key = md5(bk_host() . $path . '?' . http_build_query($query));
    $file = BK_DIR . '/pages/' . substr($key, 0, 2) . '/' . $key;
    $cached = bk_read_cache($file);

    if ($cached && $cached['expires'] > time()) {
        bk_emit($cached, 'HIT');
        return;
    }

    // While the Hub is down, don't make every visitor wait on it.
    $down = BK_DIR . '/hub-down';
    if ($cached && is_file($down) && time() - (int) filemtime($down) < 30) {
        bk_emit($cached, 'STALE');
        return;
    }

    $fetched = bk_hub('GET', '/api/v1/edge/page', ['path' => $path, 'query' => http_build_query($query)]);

    // The Hub marks its own answers (a maintenance page included); a bare 5xx means it is down.
    $answered = $fetched && (isset($fetched['headers']['x-bk-status']) || $fetched['status'] < 500);

    if (!$answered) {
        @touch($down);
    } elseif (is_file($down)) {
        @unlink($down);
    }

    if ($answered && $fetched['status'] === 401) {
        bk_refused();
        bk_unavailable();
        return;
    }

    if ($answered) {
        $page = [
            'status' => $fetched['status'],
            'type' => $fetched['headers']['content-type'] ?? 'text/html; charset=UTF-8',
            'location' => $fetched['headers']['location'] ?? null,
            'body' => $fetched['body'],
            'expires' => time() + max(30, (int) ($fetched['headers']['x-bk-ttl'] ?? 600)),
            'headers' => array_intersect_key($fetched['headers'], array_flip(['content-security-policy', 'x-robots-tag', 'access-control-allow-origin', 'retry-after'])),
        ];

        if (($fetched['headers']['x-bk-cacheable'] ?? '0') === '1' && bk_known_host()) {
            bk_write_cache($file, $page);
        }

        bk_emit($page, 'MISS');
        return;
    }

    if ($cached) {
        bk_emit($cached, 'STALE');
        return;
    }

    bk_unavailable();
}

function bk_emit(array $page, $state)
{
    http_response_code((int) $page['status']);
    header('Content-Type: ' . $page['type']);
    header('X-BK-Edge: ' . $state);
    header('Cache-Control: public, max-age=0');

    if (!empty($page['location'])) {
        header('Location: ' . bk_local_url($page['location']));
    }

    foreach ($page['headers'] ?? [] as $name => $value) {
        header(ucwords($name, '-') . ': ' . $value);
    }

    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'HEAD') {
        echo $page['body'];
    }
}

function bk_unavailable()
{
    http_response_code(503);
    header('Retry-After: 120');
    header('Content-Type: text/html; charset=UTF-8');
    echo '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex"><title>Back soon</title></head>'
        . '<body style="margin:0;min-height:100vh;display:grid;place-items:center;font-family:system-ui,sans-serif;background:#0f1219;color:#e9edf5">'
        . '<main style="text-align:center;padding:24px"><h1 style="font-size:20px;margin:0 0 8px">We’ll be right back</h1><p style="margin:0;color:#9aa5bb">The site is updating. Please try again in a minute.</p></main></body></html>';
}

/** The Hub no longer accepts this file's key (revoked, or the site was deleted): stop serving copies. */
function bk_refused()
{
    bk_log('The Hub refused this site key. Upload a freshly downloaded installer.');
    bk_purge();
}

/* ------------------------------------------------------ runtime + static */

function bk_runtime($endpoint)
{
    $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
    $cacheable = in_array($endpoint, ['robots.txt', 'sitemap.xml'], true) || strpos($endpoint, 'sitemaps/') === 0;
    $file = BK_DIR . '/runtime/' . md5(bk_host() . $endpoint);

    if ($cacheable && ($cached = bk_read_cache($file)) && $cached['expires'] > time()) {
        bk_emit($cached, 'HIT');
        return;
    }

    $body = $method === 'POST' ? file_get_contents('php://input') : null;
    $response = bk_hub($method, '/api/v1/edge/runtime/' . $endpoint, $_GET, $body, $_SERVER['CONTENT_TYPE'] ?? null);

    if (!$response || $response['status'] === 401) {
        if ($response) {
            bk_refused();
        }

        bk_unavailable();
        return;
    }

    $page = [
        'status' => $response['status'],
        'type' => $response['headers']['content-type'] ?? 'application/json',
        'location' => $response['headers']['location'] ?? null,
        'body' => $response['body'],
        'expires' => time() + 3600,
    ];

    if ($cacheable && $response['status'] === 200 && bk_known_host()) {
        bk_write_cache($file, $page);
    }

    foreach (['cache-control', 'x-robots-tag', 'referrer-policy', 'content-security-policy', 'retry-after'] as $header) {
        if (isset($response['headers'][$header])) {
            header(ucwords($header, '-') . ': ' . $response['headers'][$header]);
        }
    }

    http_response_code($page['status']);
    header('Content-Type: ' . $page['type']);

    if ($page['location']) {
        header('Location: ' . $page['location']);
    }

    echo $page['body'];
}

/** Theme assets and uploads: fetched once, then served from disk forever (their paths are versioned). */
function bk_static($path)
{
    if (strpos($path, '..') !== false) {
        http_response_code(400);
        return;
    }

    $file = BK_DIR . '/static' . $path;

    if (!is_file($file)) {
        $response = bk_fetch('GET', rtrim(BK_HUB, '/') . $path, [], null, null, false);

        if (!$response || $response['status'] !== 200) {
            http_response_code(404);
            return;
        }

        @mkdir(dirname($file), 0755, true);
        @file_put_contents($file . '.tmp', $response['body']);
        @rename($file . '.tmp', $file);
    }

    $types = ['css' => 'text/css', 'js' => 'application/javascript', 'svg' => 'image/svg+xml', 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'webp' => 'image/webp', 'gif' => 'image/gif', 'woff2' => 'font/woff2', 'woff' => 'font/woff', 'ico' => 'image/x-icon', 'vtt' => 'text/vtt', 'json' => 'application/json'];
    $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));

    header('Content-Type: ' . ($types[$ext] ?? 'application/octet-stream'));
    header('Cache-Control: public, max-age=31536000, immutable');
    readfile($file);
}

/* ------------------------------------------------------------- the hub */

function bk_hub($method, $path, array $query = [], $body = null, $contentType = null)
{
    $url = rtrim(BK_HUB, '/') . $path . ($query ? '?' . http_build_query($query) : '');

    return bk_fetch($method, $url, [
        'Authorization: Bearer ' . BK_KEY,
        'X-BK-Base: ' . bk_origin(),
        'X-BK-Client-IP: ' . bk_client_ip(),
        'X-BK-Edge: ' . BK_VERSION,
        'Accept: ' . ($_SERVER['HTTP_ACCEPT'] ?? 'text/html'),
        'User-Agent: ' . substr($_SERVER['HTTP_USER_AGENT'] ?? 'BlackKitt-Edge', 0, 300),
    ], $body, $contentType, true);
}

function bk_fetch($method, $url, array $headers, $body, $contentType, $timeoutShort)
{
    $collected = [];
    $curl = curl_init($url);

    if ($contentType) {
        $headers[] = 'Content-Type: ' . $contentType;
    }

    curl_setopt_array($curl, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => $timeoutShort ? 20 : 60,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_ENCODING => '',
        CURLOPT_HEADERFUNCTION => function ($curl, $line) use (&$collected) {
            $parts = explode(':', $line, 2);
            if (count($parts) === 2) {
                $collected[strtolower(trim($parts[0]))] = trim($parts[1]);
            }
            return strlen($line);
        },
    ]);

    if ($body !== null) {
        curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
    }

    $response = curl_exec($curl);
    $status = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
    $error = curl_error($curl);
    curl_close($curl);

    if ($response === false || $status === 0) {
        bk_log('Hub unreachable: ' . $error);
        return null;
    }

    return ['status' => $status, 'headers' => $collected, 'body' => $response];
}

/* ------------------------------------------------ agent + background sync */

/** Signed commands from the Hub: purge, update this file, or report status. Each one works once. */
function bk_agent()
{
    $body = file_get_contents('php://input');
    $signature = $_SERVER['HTTP_X_BK_SIGNATURE'] ?? '';
    $payload = json_decode($body, true);

    header('Content-Type: application/json');

    $nonce = is_array($payload) ? (string) ($payload['nonce'] ?? '') : '';

    if (!hash_equals(hash_hmac('sha256', $body, BK_SECRET), $signature) || abs(time() - (int) ($payload['ts'] ?? 0)) > 300 || !preg_match('/^[A-Za-z0-9]{16,64}$/', $nonce)) {
        http_response_code(403);
        echo json_encode(['ok' => false, 'message' => 'Bad signature']);
        return;
    }

    if (!bk_fresh_nonce($nonce)) {
        http_response_code(409);
        echo json_encode(['ok' => false, 'message' => 'This command was already used.']);
        return;
    }

    $command = $payload['cmd'] ?? '';

    if ($command === 'update') {
        $result = bk_self_update($payload);
        http_response_code($result['ok'] ? 200 : 422);
        echo json_encode($result);
        return;
    }

    if ($command === 'purge') {
        bk_purge();
    }

    echo json_encode(['ok' => true, 'version' => BK_VERSION, 'php' => PHP_VERSION, 'cache' => bk_cache_stats()]);
}

/** Remembers command nonces for the signature window, so a captured command can't be replayed. */
function bk_fresh_nonce($nonce)
{
    if (!($handle = @fopen(BK_DIR . '/nonces.json', 'c+'))) {
        return false;
    }

    flock($handle, LOCK_EX);
    $seen = json_decode((string) stream_get_contents($handle), true);
    $seen = is_array($seen) ? $seen : [];

    foreach ($seen as $used => $at) {
        if (time() - (int) $at > 600) {
            unset($seen[$used]);
        }
    }

    $fresh = !isset($seen[$nonce]);

    if ($fresh) {
        $seen[$nonce] = time();
        ftruncate($handle, 0);
        rewind($handle);
        fwrite($handle, json_encode($seen));
    }

    flock($handle, LOCK_UN);
    fclose($handle);

    return $fresh;
}

/** Replaces this file with the newer edge the Hub signed. The key, secret and Hub address stay as they are. */
function bk_self_update(array $payload)
{
    $code = (string) ($payload['code'] ?? '');
    $version = (string) ($payload['version'] ?? '');

    if (strpos($code, '<?php') !== 0 || strpos($code, "\nbk_boot();") === false || !preg_match('/^\d+\.\d+\.\d+$/', $version)) {
        return ['ok' => false, 'version' => BK_VERSION, 'message' => 'The update arrived incomplete.'];
    }

    $code = strtr($code, [
        '{{' . 'HUB_URL}}' => bk_quote(BK_HUB),
        '{{' . 'API_KEY}}' => bk_quote(BK_KEY),
        '{{' . 'SECRET}}' => bk_quote(BK_SECRET),
    ]);
    $temp = __FILE__ . '.' . getmypid() . '.tmp';
    // Swapped in one step; where the running file can't be renamed over (Windows), copied over it.
    $written = @file_put_contents($temp, $code) !== false && (@rename($temp, __FILE__) || @copy($temp, __FILE__));
    @unlink($temp);

    if (!$written) {
        return ['ok' => false, 'version' => BK_VERSION, 'message' => 'Could not replace ' . basename(__FILE__) . '. Make it writable for PHP.'];
    }

    if (function_exists('opcache_invalidate')) {
        @opcache_invalidate(__FILE__, true);
    }

    bk_purge();

    return ['ok' => true, 'version' => $version];
}

function bk_quote($value)
{
    return str_replace(['\\', "'"], ['\\\\', "\\'"], (string) $value);
}

/** After the visitor has their page: heartbeat and pull purge events every couple of minutes. */
function bk_after_response()
{
    $state = bk_state();

    if (time() - (int) ($state['synced'] ?? 0) < BK_SYNC_EVERY) {
        return;
    }

    $state['synced'] = time();
    bk_save_state($state);

    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    }

    bk_sync($state);
}

function bk_sync(array $state)
{
    $stats = bk_cache_stats();
    $beat = bk_hub('POST', '/api/v1/edge/heartbeat', [], json_encode([
        'version' => BK_VERSION,
        'php' => PHP_VERSION,
        'cache_files' => $stats['files'],
        'cache_bytes' => $stats['bytes'],
        'disk_free' => (int) @disk_free_space(__DIR__),
        'errors' => array_slice(bk_recent_errors(), -10),
    ]), 'application/json');

    if ($beat && $beat['status'] === 401) {
        bk_refused();
        return;
    }

    $hub = $beat && $beat['status'] === 200 ? json_decode($beat['body'], true) : null;

    if (is_array($hub)) {
        $state = bk_remember_hub($state, $hub);
    }

    if ($stats['files'] > BK_MAX_FILES) {
        bk_purge();
    } elseif (time() - (int) ($state['swept'] ?? 0) > 3600) {
        bk_sweep();
        $state['swept'] = time();
    }

    $events = bk_hub('GET', '/api/v1/edge/events', ['since' => (int) ($state['cursor'] ?? 0)]);
    $decoded = $events && $events['status'] === 200 ? json_decode($events['body'], true) : null;

    if (is_array($decoded)) {
        foreach ($decoded['events'] ?? [] as $event) {
            if (($event['type'] ?? '') === 'cache.purge') {
                bk_purge();
                break;
            }
        }

        $state['cursor'] = (int) ($decoded['cursor'] ?? 0);
    }

    bk_save_state($state);
}

/** Keeps what the heartbeat said: the site's domains, and whether it is in maintenance (a change clears the cache). */
function bk_remember_hub(array $state, array $hub)
{
    if (isset($hub['hosts']) && is_array($hub['hosts'])) {
        $state['hosts'] = array_values(array_map('strval', $hub['hosts']));
    }

    $maintenance = !empty($hub['maintenance']);

    if ($maintenance !== !empty($state['maintenance'])) {
        bk_purge();
    }

    $state['maintenance'] = $maintenance;

    return $state;
}

/* ----------------------------------------------------------------- cache */

function bk_read_cache($file)
{
    if (!is_file($file)) {
        return null;
    }

    $data = @unserialize((string) @file_get_contents($file), ['allowed_classes' => false]);

    return is_array($data) ? $data : null;
}

function bk_write_cache($file, array $page)
{
    @mkdir(dirname($file), 0755, true);
    $tmp = $file . '.' . getmypid() . '.tmp';

    if (@file_put_contents($tmp, serialize($page)) !== false) {
        @rename($tmp, $file);
    }
}

/** Only the site's own domains get cached copies, so odd Host headers can't fill the disk. */
function bk_known_host()
{
    $hosts = bk_state()['hosts'] ?? null;

    return !is_array($hosts) || in_array(bk_host(), $hosts, true);
}

/** Moves the page cache aside and deletes it, so visitors never see a half-emptied cache. */
function bk_purge()
{
    $trash = BK_DIR . '/trash-' . time() . '-' . mt_rand(1000, 9999);

    if (is_dir(BK_DIR . '/pages')) {
        @rename(BK_DIR . '/pages', $trash);
    }

    @mkdir(BK_DIR . '/pages', 0755, true);
    bk_delete_tree($trash);
    bk_delete_tree(BK_DIR . '/runtime');
}

/** Deletes copies nobody has asked for in a while (old 404s, one-off search pages). */
function bk_sweep()
{
    foreach (['pages', 'runtime'] as $dir) {
        if (!is_dir(BK_DIR . '/' . $dir)) {
            continue;
        }

        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator(BK_DIR . '/' . $dir, FilesystemIterator::SKIP_DOTS)) as $item) {
            if ($item->isFile() && $item->getMTime() < time() - BK_KEEP_FOR) {
                @unlink($item->getPathname());
            }
        }
    }
}

function bk_delete_tree($dir)
{
    if (!is_dir($dir)) {
        return;
    }

    $items = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);

    foreach ($items as $item) {
        $item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
    }

    @rmdir($dir);
}

function bk_cache_stats()
{
    $files = 0;
    $bytes = 0;

    if (is_dir(BK_DIR . '/pages')) {
        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator(BK_DIR . '/pages', FilesystemIterator::SKIP_DOTS)) as $item) {
            $files++;
            $bytes += $item->getSize();
        }
    }

    return ['files' => $files, 'bytes' => $bytes];
}

function bk_state()
{
    $state = @json_decode((string) @file_get_contents(BK_DIR . '/state.json'), true);

    return is_array($state) ? $state : [];
}

function bk_save_state(array $state)
{
    @file_put_contents(BK_DIR . '/state.json', json_encode($state), LOCK_EX);
}

function bk_log($message)
{
    @file_put_contents(BK_DIR . '/errors.log', date('c') . ' ' . $message . "\n", FILE_APPEND | LOCK_EX);
}

function bk_recent_errors()
{
    $lines = @file(BK_DIR . '/errors.log', FILE_IGNORE_NEW_LINES) ?: [];

    return array_slice($lines, -10);
}

/* --------------------------------------------------------------- helpers */

function bk_host()
{
    return strtolower((string) ($_SERVER['HTTP_HOST'] ?? 'localhost'));
}

function bk_origin()
{
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'
        || ($_SERVER['HTTP_CF_VISITOR'] ?? '') === '{"scheme":"https"}';

    return ($https ? 'https://' : 'http://') . bk_host();
}

function bk_client_ip()
{
    foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'] as $key) {
        if (!empty($_SERVER[$key]) && filter_var($_SERVER[$key], FILTER_VALIDATE_IP)) {
            return $_SERVER[$key];
        }
    }

    return '0.0.0.0';
}

/** Redirects from the Hub point at this site's own domain. */
function bk_local_url($location)
{
    return strpos($location, 'http') === 0 ? $location : bk_origin() . '/' . ltrim($location, '/');
}

/* ------------------------------------------------------------------ setup */

/** Set up once per installer file: the key's fingerprint marks this copy as connected. */
function bk_installed()
{
    return trim((string) @file_get_contents(BK_DIR . '/.installed')) === bk_fingerprint();
}

function bk_fingerprint()
{
    return substr(hash('sha256', BK_HUB . '|' . BK_KEY), 0, 16);
}

/** Checks the server and connects to the Hub. Only when something fails does a visitor see the setup page. */
function bk_install()
{
    $checks = [
        ['PHP 7.4 or newer', version_compare(PHP_VERSION, '7.4.0', '>='), 'This server runs PHP ' . PHP_VERSION . '. Choose a newer PHP version in your hosting panel.'],
        ['cURL extension', function_exists('curl_init') && function_exists('json_encode'), 'Enable the curl extension in your hosting panel (PHP extensions).'],
        ['Can write its cache folder', (is_dir(BK_DIR . '/pages') || @mkdir(BK_DIR . '/pages', 0755, true)) && is_writable(BK_DIR), 'Make this folder writable: ' . BK_DIR],
    ];

    $ready = !in_array(false, array_column($checks, 1), true);

    if ($ready) {
        $response = bk_hub('POST', '/api/v1/edge/heartbeat', [], json_encode(['version' => BK_VERSION, 'php' => PHP_VERSION]), 'application/json');
        $ready = $response && $response['status'] === 200;
        $checks[] = ['Connected to BlackKitt', $ready, $response
            ? 'The Hub answered with ' . $response['status'] . '. If you downloaded a new installer, upload that file instead.'
            : 'Could not reach the BlackKitt Hub. Check that this server can make outgoing HTTPS requests.'];
    }

    if (!$ready) {
        bk_setup_page($checks);
        return false;
    }

    $hub = json_decode($response['body'], true);
    bk_save_state(bk_remember_hub(bk_state(), is_array($hub) ? $hub : []));

    if (!is_file(__DIR__ . '/.htaccess')) {
        @file_put_contents(__DIR__ . '/.htaccess', "# BlackKitt edge\n<IfModule mod_rewrite.c>\nRewriteEngine On\nRewriteRule ^bk-cache/ - [F,L]\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteRule ^ index.php [L]\n</IfModule>\n");
    }

    @file_put_contents(BK_DIR . '/.htaccess', "Require all denied\nDeny from all\n");
    @file_put_contents(BK_DIR . '/.installed', bk_fingerprint());

    return true;
}

function bk_setup_page(array $checks)
{
    http_response_code(503);
    header('Content-Type: text/html; charset=UTF-8');
    header('X-Robots-Tag: noindex');
    echo '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex"><title>BlackKitt setup</title></head>'
        . '<body style="margin:0;min-height:100vh;display:grid;place-items:center;font-family:system-ui,sans-serif;background:#f4f5f7;color:#1a2030">'
        . '<main style="width:100%;max-width:520px;padding:24px"><h1 style="font-size:20px;margin:0 0 4px">' . htmlspecialchars(BK_NAME) . '</h1><p style="margin:0 0 16px;color:#4d586b">BlackKitt edge ' . BK_VERSION . '</p><ul style="list-style:none;margin:0;padding:0;border:1px solid #d8dce3;border-radius:6px;background:#fff">';

    foreach ($checks as $check) {
        echo '<li style="padding:10px 14px;border-top:1px solid #eef0f3"><strong style="color:' . ($check[1] ? '#15803d' : '#be123c') . '">' . ($check[1] ? '✓' : '✗') . '</strong> ' . htmlspecialchars($check[0])
            . ($check[1] ? '' : '<div style="margin-top:4px;font-size:13px;color:#4d586b">' . htmlspecialchars($check[2]) . '</div>') . '</li>';
    }

    echo '</ul><p style="margin:16px 0 0;color:#4d586b">Fix the items above, then reload this page.</p></main></body></html>';
}
