<?php
declare(strict_types=1);

date_default_timezone_set('Asia/Seoul');

$netcityRequestPath = parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH);
$netcityRequestPath = is_string($netcityRequestPath) && $netcityRequestPath !== '' ? $netcityRequestPath : '/';

$wide3G5StandardProxy = __DIR__ . '/wide3_g5_standard_proxy.php';
if (is_file($wide3G5StandardProxy)) {
    require_once $wide3G5StandardProxy;
    netcity_wide3_g5_standard_proxy_dispatch($netcityRequestPath);
}

function netcity_send_local_asset(string $file): void
{
    if (!is_file($file) || !is_readable($file)) {
        http_response_code(404);
        exit;
    }

    $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
    $contentType = match ($extension) {
        'svg' => 'image/svg+xml',
        'png' => 'image/png',
        'jpg', 'jpeg' => 'image/jpeg',
        'webp' => 'image/webp',
        default => 'application/octet-stream',
    };

    if (!headers_sent()) {
        header_remove('X-Powered-By');
        header('Content-Type: ' . $contentType);
        header('Content-Length: ' . (string) filesize($file));
        header('Cache-Control: public, max-age=86400');
    }

    readfile($file);
    exit;
}

if ($netcityRequestPath === '/assets/card-ubuntu-guide-editor.svg') {
    netcity_send_local_asset(__DIR__ . '/assets/card-ubuntu-guide-editor.svg');
}

if ($netcityRequestPath === '/assets/card-gd-c100-ubuntu.svg') {
    netcity_send_local_asset(__DIR__ . '/assets/card-gd-c100-ubuntu.svg');
}

if ($netcityRequestPath === '/assets/card-codex-workspace-live.svg') {
    netcity_send_local_asset(__DIR__ . '/assets/card-codex-workspace-live.svg');
}

if ($netcityRequestPath === '/assets/card-youtube-webcam-live.svg') {
    netcity_send_local_asset(__DIR__ . '/assets/card-youtube-webcam-live.svg');
}

$ubuntuEditorAssetPrefix = '/ubuntu-guide-editor/assets/';
if (str_starts_with($netcityRequestPath, $ubuntuEditorAssetPrefix)) {
    $assetName = basename($netcityRequestPath);
    netcity_send_local_asset(__DIR__ . '/ubuntu-guide-editor/assets/' . $assetName);
}

if ($netcityRequestPath === '/ubuntu-guide-editor' || $netcityRequestPath === '/ubuntu-guide-editor/') {
    require __DIR__ . '/ubuntu-guide-editor/index.php';
    exit;
}

if ($netcityRequestPath === '/gd-c100-ubuntu-camera' || $netcityRequestPath === '/gd-c100-ubuntu-camera/') {
    require __DIR__ . '/gd-c100-ubuntu-camera/index.php';
    exit;
}

if ($netcityRequestPath === '/codex-workspace-live' || $netcityRequestPath === '/codex-workspace-live/') {
    require __DIR__ . '/codex-workspace-live/index.php';
    exit;
}

if ($netcityRequestPath === '/youtube-webcam-live' || $netcityRequestPath === '/youtube-webcam-live/') {
    require __DIR__ . '/youtube-webcam-live/index.php';
    exit;
}

if ($netcityRequestPath === '/youtube-webcam-live/board' || $netcityRequestPath === '/youtube-webcam-live/board/') {
    require __DIR__ . '/youtube-webcam-live/board/index.php';
    exit;
}

$messengerConfigFile = __DIR__ . '/netcity-messenger/config.php';
if (is_file($messengerConfigFile)) {
    require_once $messengerConfigFile;
}

$portalMessengerReady = defined('NCM_DB_HOST') && function_exists('mysqli_connect');
if ($portalMessengerReady && session_status() !== PHP_SESSION_ACTIVE) {
    session_name('NCMSESSID');
    session_cache_limiter('');
    session_start();
}

if (!headers_sent()) {
    header_remove('X-Powered-By');
    header('Cache-Control: private, max-age=10, stale-while-revalidate=60');
    header('Vary: Accept-Encoding');
}

function h(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function portal_messenger_flash(?string $message = null, string $type = 'ok'): ?array
{
    if ($message !== null) {
        $_SESSION['portal_messenger_flash'] = ['message' => $message, 'type' => $type];
        return null;
    }

    $flash = $_SESSION['portal_messenger_flash'] ?? null;
    unset($_SESSION['portal_messenger_flash']);

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

function portal_messenger_token(): string
{
    if (empty($_SESSION['portal_messenger_csrf'])) {
        $_SESSION['portal_messenger_csrf'] = bin2hex(random_bytes(32));
    }

    return (string) $_SESSION['portal_messenger_csrf'];
}

function portal_messenger_check_token(): void
{
    $token = (string) ($_POST['portal_messenger_csrf'] ?? '');
    if ($token === '' || !hash_equals(portal_messenger_token(), $token)) {
        throw new RuntimeException('로그인 보안 토큰이 올바르지 않습니다. 다시 시도해주세요.');
    }
}

function portal_messenger_table(string $name): string
{
    return '`' . NCM_DB_PREFIX . $name . '`';
}

function portal_messenger_db(): mysqli
{
    static $db = null;

    if ($db instanceof mysqli) {
        return $db;
    }

    if (function_exists('mysqli_report')) {
        mysqli_report(MYSQLI_REPORT_OFF);
    }

    $db = @new mysqli(NCM_DB_HOST, NCM_DB_USER, NCM_DB_PASSWORD, NCM_DB_NAME);
    if ($db->connect_errno) {
        throw new RuntimeException('메신저 DB 연결에 실패했습니다.');
    }

    $db->set_charset('utf8mb4');

    return $db;
}

function portal_messenger_current_user(): ?array
{
    if (!defined('NCM_DB_HOST') || empty($_SESSION['ncm_user_id'])) {
        return null;
    }

    try {
        $db = portal_messenger_db();
        $users = portal_messenger_table('users');
        $id = (int) $_SESSION['ncm_user_id'];
        $stmt = $db->prepare("SELECT id, username, display_name, role, created_at, last_login_at FROM {$users} WHERE id = ?");
        if (!$stmt) {
            return null;
        }
        $stmt->bind_param('i', $id);
        $stmt->execute();
        $result = $stmt->get_result();
        $user = $result ? $result->fetch_assoc() : null;
        $stmt->close();

        if (!is_array($user)) {
            unset($_SESSION['ncm_user_id']);
            return null;
        }

        return $user;
    } catch (Throwable) {
        return null;
    }
}

function portal_messenger_unread_count(int $userId): int
{
    try {
        $db = portal_messenger_db();
        $messages = portal_messenger_table('messages');
        $stmt = $db->prepare("SELECT COUNT(*) AS cnt FROM {$messages} WHERE recipient_id = ? AND recipient_deleted_at IS NULL AND read_at IS NULL");
        if (!$stmt) {
            return 0;
        }
        $stmt->bind_param('i', $userId);
        $stmt->execute();
        $result = $stmt->get_result();
        $row = $result ? $result->fetch_assoc() : [];
        $stmt->close();

        return (int) ($row['cnt'] ?? 0);
    } catch (Throwable) {
        return 0;
    }
}

function portal_messenger_redirect(): never
{
    header('Location: ./#messenger-login');
    exit;
}

function portal_messenger_handle_post(): void
{
    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['portal_messenger_action'])) {
        return;
    }

    try {
        if (!defined('NCM_DB_HOST') || !function_exists('mysqli_connect')) {
            throw new RuntimeException('메신저 설정을 찾을 수 없습니다.');
        }

        portal_messenger_check_token();
        $action = (string) $_POST['portal_messenger_action'];

        if ($action === 'logout') {
            unset($_SESSION['ncm_user_id']);
            $_SESSION['portal_messenger_csrf'] = bin2hex(random_bytes(32));
            portal_messenger_flash('메신저 로그아웃이 완료되었습니다.');
            portal_messenger_redirect();
        }

        if ($action !== 'login') {
            throw new RuntimeException('알 수 없는 메신저 요청입니다.');
        }

        $username = strtolower(trim((string) ($_POST['username'] ?? '')));
        $password = (string) ($_POST['password'] ?? '');
        if ($username === '' || $password === '') {
            throw new RuntimeException('아이디와 비밀번호를 입력해주세요.');
        }

        $db = portal_messenger_db();
        $users = portal_messenger_table('users');
        $stmt = $db->prepare("SELECT id, password_hash FROM {$users} WHERE username = ?");
        if (!$stmt) {
            throw new RuntimeException('메신저 로그인 준비에 실패했습니다.');
        }
        $stmt->bind_param('s', $username);
        $stmt->execute();
        $result = $stmt->get_result();
        $user = $result ? $result->fetch_assoc() : null;
        $stmt->close();

        if (!is_array($user) || !password_verify($password, (string) $user['password_hash'])) {
            throw new RuntimeException('아이디 또는 비밀번호가 올바르지 않습니다.');
        }

        session_regenerate_id(true);
        $_SESSION['ncm_user_id'] = (int) $user['id'];
        $_SESSION['ncm_csrf'] = bin2hex(random_bytes(32));
        $_SESSION['portal_messenger_csrf'] = bin2hex(random_bytes(32));

        $id = (int) $user['id'];
        $stmt = $db->prepare("UPDATE {$users} SET last_login_at = NOW() WHERE id = ?");
        if ($stmt) {
            $stmt->bind_param('i', $id);
            $stmt->execute();
            $stmt->close();
        }

        portal_messenger_flash('메신저 로그인이 완료되었습니다.');
    } catch (Throwable $e) {
        portal_messenger_flash($e->getMessage(), 'warn');
    }

    portal_messenger_redirect();
}

function server_cache_path(string $name): string
{
    $dir = rtrim(sys_get_temp_dir(), '/') . '/netcity-server-cache-' . substr(sha1(__DIR__), 0, 12);
    if (!is_dir($dir)) {
        @mkdir($dir, 0770, true);
    }

    return $dir . '/' . preg_replace('/[^a-zA-Z0-9_.-]+/', '-', $name) . '.json';
}

function read_server_cache(string $name): array
{
    $path = server_cache_path($name);
    if (!is_file($path) || !is_readable($path)) {
        return [];
    }

    $data = json_decode((string) @file_get_contents($path), true);
    return is_array($data) ? $data : [];
}

function write_server_cache(string $name, array $data): void
{
    $path = server_cache_path($name);
    $tmp = $path . '.tmp.' . getmypid();
    if (@file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === false) {
        return;
    }

    @rename($tmp, $path);
    @chmod($path, 0666);
}

function remember_server_cache(string $name, int $ttl, callable $producer): array
{
    $now = time();
    $cached = read_server_cache($name);
    if (
        isset($cached['created_at'], $cached['data'])
        && is_array($cached['data'])
        && ($now - (int) $cached['created_at']) < $ttl
    ) {
        return $cached['data'];
    }

    $data = $producer();
    if (is_array($data)) {
        write_server_cache($name, [
            'created_at' => $now,
            'data' => $data,
        ]);
        return $data;
    }

    return [];
}

function update_server_page_counter(string $dir): array
{
    $today = date('Y-m-d');
    $now = date('Y-m-d H:i:s');
    $fallback = [
        'total' => 0,
        'today' => 0,
        'date' => $today,
        'updated_at' => $now,
    ];

    if (!is_dir($dir) && !@mkdir($dir, 0777, true) && !is_dir($dir)) {
        return $fallback;
    }

    $file = rtrim($dir, '/') . '/server-page-counter.json';
    $fp = @fopen($file, 'c+');
    if (!is_resource($fp)) {
        return $fallback;
    }

    if (!flock($fp, LOCK_EX)) {
        fclose($fp);
        return $fallback;
    }

    $raw = stream_get_contents($fp);
    $data = is_string($raw) && $raw !== '' ? json_decode($raw, true) : [];
    if (!is_array($data)) {
        $data = [];
    }

    $lastDate = (string) ($data['date'] ?? $today);
    $total = max(0, (int) ($data['total'] ?? 0));
    $todayCount = $lastDate === $today ? max(0, (int) ($data['today'] ?? 0)) : 0;
    $data = [
        'total' => $total + 1,
        'today' => $todayCount + 1,
        'date' => $today,
        'updated_at' => $now,
    ];

    ftruncate($fp, 0);
    rewind($fp);
    fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
    fflush($fp);
    flock($fp, LOCK_UN);
    fclose($fp);
    @chmod($file, 0666);

    return $data;
}

function normalize_text(string $value): string
{
    $value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $value = strip_tags($value);
    $value = preg_replace('/^\s*[#>*+\-]+\s*/u', '', $value) ?? $value;
    $value = preg_replace('/\s+/u', ' ', $value) ?? $value;

    return trim($value);
}

function read_text_description(string $file): ?string
{
    if (!is_file($file) || !is_readable($file)) {
        return null;
    }

    $contents = @file_get_contents($file, false, null, 0, 20000);

    if ($contents === false) {
        return null;
    }

    $lines = preg_split('/\R/u', $contents) ?: [];

    foreach ($lines as $line) {
        $line = normalize_text($line);

        if ($line !== '') {
            return $line;
        }
    }

    return null;
}

function read_markup_title(string $file): ?string
{
    if (!is_file($file) || !is_readable($file)) {
        return null;
    }

    $contents = @file_get_contents($file, false, null, 0, 20000);

    if ($contents === false) {
        return null;
    }

    if (preg_match('/<title[^>]*>(.*?)<\/title>/isu', $contents, $matches) === 1) {
        $title = normalize_text($matches[1]);

        if ($title !== '') {
            return $title;
        }
    }

    if (preg_match('/<h1[^>]*>(.*?)<\/h1>/isu', $contents, $matches) === 1) {
        $heading = normalize_text($matches[1]);

        if ($heading !== '') {
            return $heading;
        }
    }

    return null;
}

function folder_description(string $name, string $path): string
{
    $folder = basename($path);

    if (preg_match('/^g5-site(\d{2})$/', $folder, $matches) === 1) {
        return sprintf(
            '그누보드5 기반의 G5 Site %s 계정 페이지입니다. 192.168.0.197 서버 안에서 사이트별 게시판, 웹진, 회원/관리 화면을 분리해 테스트하는 G5 사이트 계정입니다.',
            $matches[1]
        );
    }

    if (preg_match('/^g7(?:-|$)/i', $folder) === 1) {
        return '그누보드7 기반의 메인 실험 페이지입니다. G7 템플릿, 관리자 로그인, API, 게시판 구성을 확인하는 192.168.0.197 서버의 G7 실행 링크입니다.';
    }

    if ($folder === 'netboard') {
        return 'Netcity 자체 게시판형 운영 페이지입니다. HOME, YOUTUBE, NOTICE, NEWS, DEV, DATA 메뉴로 운영 자료와 게시글을 관리하는 netboard 링크입니다.';
    }

    $textCandidates = [
        $path . '/description.txt',
        $path . '/DESCRIPTION.txt',
        $path . '/README.md',
        $path . '/readme.md',
        $path . '/README.txt',
        $path . '/README',
    ];

    foreach ($textCandidates as $candidate) {
        $description = read_text_description($candidate);

        if ($description !== null) {
            return $description;
        }
    }

    $titleCandidates = [
        $path . '/index.php',
        $path . '/index.html',
    ];

    foreach ($titleCandidates as $candidate) {
        $description = read_markup_title($candidate);

        if ($description !== null) {
            return $description;
        }
    }

    return sprintf(
        '%s 폴더입니다. 폴더 안에 description.txt 또는 README.md를 두면 이 위치에 설명이 자동으로 표시됩니다.',
        $name
    );
}

function display_folder_name(string $name): string
{
    return preg_replace('/^g(?=\d)/', 'G', $name, 1) ?? $name;
}

function public_path_from_root(string $root, string $file): string
{
    $relative = ltrim(str_replace('\\', '/', substr($file, strlen($root))), '/');
    $path = './' . implode('/', array_map('rawurlencode', explode('/', $relative)));
    $version = is_file($file) ? @filemtime($file) : false;

    return $version ? $path . '?v=' . $version : $path;
}

function php_define_value(string $contents, string $name): ?string
{
    $pattern = '/define\\(\\s*[\'"]' . preg_quote($name, '/') . '[\'"]\\s*,\\s*([\'"])(.*?)\\1\\s*\\)/s';

    if (preg_match($pattern, $contents, $matches) !== 1) {
        return null;
    }

    return stripcslashes($matches[2]);
}

function active_g5_theme_from_db(string $root, string $entry): ?string
{
    if (!function_exists('mysqli_connect')) {
        return null;
    }

    $dbConfig = $root . DIRECTORY_SEPARATOR . $entry . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'dbconfig.php';
    if (!is_file($dbConfig) || !is_readable($dbConfig)) {
        return null;
    }

    $contents = (string) @file_get_contents($dbConfig, false, null, 0, 12000);
    if ($contents === '') {
        return null;
    }

    $host = php_define_value($contents, 'G5_MYSQL_HOST');
    $user = php_define_value($contents, 'G5_MYSQL_USER');
    $password = php_define_value($contents, 'G5_MYSQL_PASSWORD');
    $database = php_define_value($contents, 'G5_MYSQL_DB');
    $prefix = php_define_value($contents, 'G5_TABLE_PREFIX') ?? 'g5_';

    if (!$host || !$user || !$database) {
        return null;
    }

    if (function_exists('mysqli_report')) {
        mysqli_report(MYSQLI_REPORT_OFF);
    }

    $connection = @mysqli_connect($host, $user, (string) $password, $database);
    if (!$connection) {
        return null;
    }

    @mysqli_set_charset($connection, 'utf8mb4');
    $table = preg_replace('/[^A-Za-z0-9_]/', '', $prefix . 'config') ?: 'g5_config';
    $result = @mysqli_query($connection, 'SELECT cf_theme FROM ' . $table . ' LIMIT 1');
    $row = $result ? @mysqli_fetch_assoc($result) : null;
    if ($result) {
        @mysqli_free_result($result);
    }
    @mysqli_close($connection);

    $theme = is_array($row) ? (string) ($row['cf_theme'] ?? '') : '';

    return preg_match('/^[A-Za-z0-9_-]+$/', $theme) === 1 ? $theme : null;
}

function active_g5_theme_from_home(string $entry): ?string
{
    if (preg_match('/^g5-site\d+$/', $entry) !== 1) {
        return null;
    }

    $url = 'http://127.0.0.1/' . rawurlencode($entry) . '/';
    $context = stream_context_create([
        'http' => [
            'timeout' => 0.45,
            'ignore_errors' => true,
            'header' => "Connection: close\r\n",
        ],
    ]);
    $html = @file_get_contents($url, false, $context, 0, 60000);

    if (!is_string($html) || $html === '') {
        return null;
    }

    $entryPattern = preg_quote($entry, '#');
    if (preg_match('#/(?:' . $entryPattern . '/)?theme/([A-Za-z0-9_-]+)/#', $html, $matches) === 1) {
        return $matches[1];
    }

    if (preg_match('#theme/([A-Za-z0-9_-]+)/#', $html, $matches) === 1) {
        return $matches[1];
    }

    return null;
}

function active_g5_theme_for_folder(string $root, string $entry): ?string
{
    if (preg_match('/^g5-site\d+$/', $entry) !== 1) {
        return null;
    }

    $cache = remember_server_cache('active-theme-v2-' . $entry, 10, static function () use ($root, $entry): array {
        $theme = active_g5_theme_from_db($root, $entry) ?? active_g5_theme_from_home($entry);

        return $theme ? ['theme' => $theme] : [];
    });

    $theme = (string) ($cache['theme'] ?? '');

    return preg_match('/^[A-Za-z0-9_-]+$/', $theme) === 1 ? $theme : null;
}

function theme_screenshot_for_folder(string $root, string $entry): ?string
{
    $themeDir = $root . DIRECTORY_SEPARATOR . $entry . DIRECTORY_SEPARATOR . 'theme';

    if (!is_dir($themeDir)) {
        return null;
    }

    $activeTheme = active_g5_theme_for_folder($root, $entry);
    if ($activeTheme !== null) {
        $activeScreenshot = $themeDir . DIRECTORY_SEPARATOR . $activeTheme . DIRECTORY_SEPARATOR . 'screenshot.png';

        if (is_file($activeScreenshot)) {
            return public_path_from_root($root, $activeScreenshot);
        }
    }

    $preferredThemes = [
        'netcity_white',
        'webzine',
        'smartcompanytheme',
        'basic',
    ];

    foreach ($preferredThemes as $theme) {
        $screenshot = $themeDir . DIRECTORY_SEPARATOR . $theme . DIRECTORY_SEPARATOR . 'screenshot.png';

        if (is_file($screenshot)) {
            return public_path_from_root($root, $screenshot);
        }
    }

    $themes = scandir($themeDir) ?: [];
    sort($themes, SORT_NATURAL | SORT_FLAG_CASE);

    foreach ($themes as $theme) {
        if ($theme === '.' || $theme === '..') {
            continue;
        }

        $screenshot = $themeDir . DIRECTORY_SEPARATOR . $theme . DIRECTORY_SEPARATOR . 'screenshot.png';

        if (is_file($screenshot)) {
            return public_path_from_root($root, $screenshot);
        }
    }

    return null;
}

function card_image_for_folder(string $entry, ?string $cardClass, string $root): string
{
    if (preg_match('/^g5-site\d+$/', $entry) === 1) {
        return theme_screenshot_for_folder($root, $entry) ?? '/assets/card-gnuboard-theme.svg';
    }

    if ($cardClass === 'card--g7') {
        return theme_screenshot_for_folder($root, $entry) ?? '/assets/card-g7-site.svg';
    }

    if ($cardClass === 'card--private-bbs') {
        return '/assets/card-private-bbs.svg';
    }

    if (stripos($entry, 'mail') !== false) {
        return '/assets/card-mail-server.svg';
    }

    return '/assets/card-web-folder.svg';
}

function local_link_exists(string $href, string $root): bool
{
    $path = parse_url($href, PHP_URL_PATH);

    if (!is_string($path) || $path === '') {
        return false;
    }

    if (str_starts_with($path, './')) {
        $target = $root . '/' . rawurldecode(substr($path, 2));
    } elseif (str_starts_with($path, '/')) {
        $target = $root . rawurldecode($path);
    } else {
        $target = $root . '/' . rawurldecode($path);
    }

    return is_dir($target) || is_file($target);
}

function remote_link_is_open(string $href): bool
{
    static $cache = [];
    static $diskCache = null;

    if (array_key_exists($href, $cache)) {
        return $cache[$href];
    }

    $parts = parse_url($href);
    $scheme = strtolower((string) ($parts['scheme'] ?? 'http'));
    $host = (string) ($parts['host'] ?? '');
    $port = (int) ($parts['port'] ?? ($scheme === 'https' ? 443 : 80));

    if ($host === '' || $port <= 0) {
        return $cache[$href] = false;
    }

    if ($diskCache === null) {
        $diskCache = read_server_cache('link-status');
    }

    $targetKey = $scheme . '://' . $host . ':' . $port;
    $now = time();
    $ttl = preg_match('/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host) === 1 ? 45 : 120;
    $cached = $diskCache[$targetKey] ?? null;
    if (
        is_array($cached)
        && isset($cached['checked_at'], $cached['available'])
        && ($now - (int) $cached['checked_at']) < $ttl
    ) {
        return $cache[$href] = (bool) $cached['available'];
    }

    $target = ($scheme === 'https' ? 'ssl://' : 'tcp://') . $host . ':' . $port;
    $errno = 0;
    $errstr = '';
    $timeout = preg_match('/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host) === 1 ? 0.12 : 0.22;
    $socket = @stream_socket_client($target, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT);
    $available = false;

    if (is_resource($socket)) {
        fclose($socket);
        $available = true;
    }

    $diskCache[$targetKey] = [
        'available' => $available,
        'checked_at' => $now,
    ];
    write_server_cache('link-status', $diskCache);

    return $cache[$href] = $available;
}

function link_is_available(string $href, string $root): bool
{
    if (preg_match('/^https?:\/\//i', $href) === 1) {
        return remote_link_is_open($href);
    }

    return local_link_exists($href, $root);
}

function status_class(bool $available): string
{
    return $available ? '' : ' is-offline';
}

function status_text(bool $available): string
{
    return $available ? '접속 가능' : '접속 대기';
}

function compact_duration(?int $seconds): string
{
    if ($seconds === null) {
        return '확인 중';
    }
    if ($seconds < 60) {
        return $seconds . '초 전';
    }
    if ($seconds < 3600) {
        return (int) floor($seconds / 60) . '분 전';
    }
    return (int) floor($seconds / 3600) . '시간 전';
}

function count_json_files(string $dir): int
{
    return count(glob($dir . '/*.json') ?: []);
}

function read_json_assoc(string $file): array
{
    if (!is_file($file)) {
        return [];
    }

    $data = json_decode((string) @file_get_contents($file), true);
    return is_array($data) ? $data : [];
}

function read_json_items(string $file): array
{
    $data = read_json_assoc($file);

    if ($data === []) {
        return [];
    }

    $items = array_is_list($data) ? $data : array_values($data);

    return array_values(array_filter($items, static fn($item): bool => is_array($item)));
}

function count_jsonl_records(string $file): int
{
    if (!is_file($file) || !is_readable($file)) {
        return 0;
    }

    $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

    if (!is_array($lines)) {
        return 0;
    }

    return count(array_filter($lines, static fn(string $line): bool => trim($line) !== ''));
}

function schedule_job_status_counts(array $jobs): array
{
    $now = time();
    $counts = [
        'waiting' => 0,
        'running' => 0,
        'done' => 0,
    ];

    foreach ($jobs as $job) {
        if (!is_array($job)) {
            continue;
        }

        $start = strtotime((string) ($job['scheduled_at'] ?? ''));
        if ($start === false) {
            $counts['waiting']++;
            continue;
        }

        $duration = max(5, (int) ($job['duration_minutes'] ?? 30));
        $end = $start + ($duration * 60);

        if ($now < $start) {
            $counts['waiting']++;
        } elseif ($now <= $end) {
            $counts['running']++;
        } else {
            $counts['done']++;
        }
    }

    return $counts;
}

function wide3_capture_summary(string $baseDir): array
{
    $dataDir = $baseDir . '/data';
    $heartbeatFile = $dataDir . '/worker-heartbeat.json';
    $heartbeat = read_json_assoc($heartbeatFile);
    $heartbeatAge = is_file($heartbeatFile) ? max(0, time() - filemtime($heartbeatFile)) : null;
    $workerLive = $heartbeatAge !== null && $heartbeatAge < 150;
    $pending = count_json_files($dataDir . '/jobs');
    $done = count_json_files($dataDir . '/jobs-done');
    $failed = count_json_files($dataDir . '/jobs-failed');
    $processed = $done + $failed;
    $total = max(1, $pending + $processed);

    return [
        'pending' => $pending,
        'done' => $done,
        'failed' => $failed,
        'progress' => round(($processed / $total) * 100, 1),
        'worker_live' => $workerLive,
        'heartbeat_age' => $heartbeatAge,
        'status' => (string) ($heartbeat['status'] ?? ($workerLive ? 'running' : 'standby')),
        'current_title' => (string) ($heartbeat['current']['title'] ?? ''),
        'current_url' => (string) ($heartbeat['current']['url'] ?? ''),
    ];
}

function reporter_photo_journal_summary(string $baseDir): array
{
    $scheduleDir = $baseDir . '/schedule-article/data';
    $codexDataDir = $baseDir . '/codex-intake/data';
    $jobs = read_json_items($scheduleDir . '/jobs.json');
    $jobCounts = schedule_job_status_counts($jobs);
    $articles = read_json_items($codexDataDir . '/generated_articles.json');
    $mobileIntakes = read_json_items($scheduleDir . '/mobile-intake.json');
    $commands = read_json_items($scheduleDir . '/codex-command-queue.json');
    $boardPosts = read_json_items($baseDir . '/board/data/posts.json');
    $codexJsonl = count_jsonl_records($codexDataDir . '/codex_jobs.jsonl');
    $latestFiles = array_values(array_filter([
        $scheduleDir . '/jobs.json',
        $scheduleDir . '/mobile-intake.json',
        $scheduleDir . '/codex-command-queue.json',
        $codexDataDir . '/generated_articles.json',
        $codexDataDir . '/codex_jobs.jsonl',
        $baseDir . '/board/data/posts.json',
    ], static fn(string $file): bool => is_file($file)));
    $latestTime = $latestFiles === [] ? (@filemtime($baseDir) ?: null) : max(array_map('filemtime', $latestFiles));

    $registered = count(array_filter($articles, static fn(array $article): bool => (string) ($article['status'] ?? '') === 'registered'));
    $queued = count(array_filter($articles, static fn(array $article): bool => (string) ($article['status'] ?? 'queued') === 'queued'));
    $sending = count(array_filter($articles, static fn(array $article): bool => in_array((string) ($article['status'] ?? ''), ['sending', 'publishing'], true)));

    return [
        'jobs_total' => count($jobs),
        'jobs_waiting' => $jobCounts['waiting'],
        'jobs_running' => $jobCounts['running'],
        'jobs_done' => $jobCounts['done'],
        'articles_total' => count($articles),
        'articles_queued' => $queued,
        'articles_sending' => $sending,
        'articles_registered' => $registered,
        'mobile_intakes' => count($mobileIntakes),
        'commands' => count($commands) + $codexJsonl,
        'board_posts' => count($boardPosts),
        'latest_age' => is_int($latestTime) ? max(0, time() - $latestTime) : null,
    ];
}

function codex_workspace_summary(string $baseDir): array
{
    $postsFile = $baseDir . '/data/posts.json';
    $posts = read_json_items($postsFile);
    $latestFiles = array_values(array_filter([
        $baseDir . '/index.php',
        $postsFile,
    ], static fn(string $file): bool => is_file($file)));
    $latestTime = $latestFiles === [] ? (@filemtime($baseDir) ?: null) : max(array_map('filemtime', $latestFiles));

    return [
        'board_posts' => count($posts),
        'latest_age' => is_int($latestTime) ? max(0, time() - $latestTime) : null,
    ];
}

$root = __DIR__;
$self = basename(__FILE__);
if ($portalMessengerReady) {
    portal_messenger_handle_post();
}
$portalMessengerUser = $portalMessengerReady ? portal_messenger_current_user() : null;
$portalMessengerFlash = $portalMessengerReady ? portal_messenger_flash() : null;
$portalMessengerUnread = $portalMessengerUser ? portal_messenger_unread_count((int) $portalMessengerUser['id']) : 0;
$portalMessengerToken = $portalMessengerReady ? portal_messenger_token() : '';
$serverPageCounter = update_server_page_counter($root . '/.server-counter');
$wide3Capture = remember_server_cache('wide3-capture-summary', 5, function () use ($root): array {
    return wide3_capture_summary($root . '/wide3-site-capture');
});
$reporterJournal = remember_server_cache('reporter-journal-summary', 5, function () use ($root): array {
    return reporter_photo_journal_summary($root . '/reporter-photo-journal-server');
});
$codexWorkspace = remember_server_cache('codex-workspace-summary', 5, function () use ($root): array {
    return codex_workspace_summary($root . '/codex-workspace-live');
});
$manualLinks = [
    [
        'label' => 'GD-C100 Ubuntu',
        'name' => 'postmarketOS',
        'href' => 'https://postmarketos.netcity.co.kr/index.php',
        'description' => '오래된 스마트폰에 postmarketOS를 올려 Nginx, PHP-FPM, MariaDB, OpenSSH가 직접 응답하는 저전력 웹서버로 운영합니다. Ubuntu PC는 공개 HTTPS 프록시를 맡고, 실제 페이지 생성은 Galaxy Wide3의 리눅스 서버가 처리합니다.',
        'class' => 'card--gd-c100',
        'image' => '/assets/card-gd-c100-ubuntu.svg',
        'updated_at' => @filemtime($root . '/gd-c100-ubuntu-camera') ?: time(),
    ],
    [
        'label' => 'YouTube Webcam',
        'name' => '유튜브 웹캠 라이브 방송',
        'href' => './youtube-webcam-live/',
        'description' => 'Galaxy IP Webcam /webcam-live/video가 켜지면 ffmpeg RTMP 릴레이로 YouTube Live 송출까지 이어지는 설명서와 운영 게시판입니다.',
        'class' => 'card--youtube-webcam-live',
        'image' => '/assets/card-youtube-webcam-live.svg',
        'updated_at' => @filemtime($root . '/youtube-webcam-live/index.php') ?: (@filemtime($root . '/youtube-webcam-live') ?: time()),
    ],
    [
        'label' => 'CODEX Live',
        'name' => '실시간 CODEX 작업현장',
        'href' => './codex-workspace-live/',
        'description' => '메인 캠프 위젯에서 분리한 CODEX 작업현장입니다. GD-C100 라이브 스트림, 운영 설명서, JSON 게시판을 모바일 서버 카드에서 관리합니다.',
        'class' => 'card--codex-workspace',
        'image' => '/assets/card-codex-workspace-live.svg',
        'updated_at' => @filemtime($root . '/codex-workspace-live') ?: time(),
    ],
    [
        'label' => 'Wide3 Capture',
        'name' => 'Samsung Galaxy Wide3 사이트 수집 캡쳐 서버',
        'href' => 'https://mobile.netcity.co.kr/wide3-site-capture/',
        'description' => 'URL 큐를 캡쳐하고 성공 항목을 link.netcity.co.kr 지역정보 카테고리에 자동 등록하는 캡쳐 엔진입니다.',
        'class' => 'card--wide3-capture',
        'image' => '/assets/card-capture-engine.svg',
        'updated_at' => @filemtime($root . '/wide3-site-capture') ?: time(),
    ],
    [
        'label' => 'Photo Article',
        'name' => '사진 기반 기사 생성 서버',
        'href' => 'https://mobile.netcity.co.kr/photo-article-server/',
        'description' => '사진과 현장 메모를 기사 제목, 리드문, 본문, 캡션 초안으로 정리하고 리스트에서 수정·삭제하는 기사 엔진입니다.',
        'class' => 'card--photo-article',
        'image' => '/assets/card-article-engine.svg',
        'updated_at' => @filemtime($root . '/photo-article-server') ?: time(),
    ],
    [
        'label' => 'Photo Journal',
        'name' => '취재 스케쥴 기사화 서버',
        'href' => 'https://mobile.netcity.co.kr/reporter-photo-journal-server/',
        'description' => '대표 페이지에서 스마트폰 기록, 서버 연동, 스케쥴 기사화, CODEX 대기열, 매체 등록 흐름을 한 번에 안내하는 취재 스케쥴 기사화 서버 링크입니다.',
        'class' => 'card--photo-journal',
        'image' => '/assets/card-article-engine.svg',
        'updated_at' => @filemtime($root . '/reporter-photo-journal-server') ?: time(),
    ],
    [
        'label' => 'File Engine',
        'name' => '탐사차 기지·1차 이주권 개인 데이터센터 파일처리 기술',
        'href' => 'https://mobile.netcity.co.kr/datacenter-file-processing/',
        'description' => '탐사차 기지 자료를 1차 개인 데이터센터에 저장·분류·검색·권한분리·백업하는 파일 엔진입니다. CODEX 자동화와 개인 임무 운영까지 연결합니다.',
        'class' => 'card--file-processing',
        'image' => '/assets/card-file-datacenter.svg',
        'updated_at' => @filemtime($root . '/datacenter-file-processing') ?: time(),
    ],
    [
        'label' => 'Future Net',
        'name' => '개인 데이터센터와 넷으로 연결되는 초고도사회',
        'href' => 'https://mobile.netcity.co.kr/future-net-datacenter-rover/',
        'description' => '개인 데이터센터가 넷으로 연결되고, 넷폰과 항공모함 2척급 탐사차 기지가 그 데이터센터에 연결 가능한 구조를 정리한 링크 서버입니다.',
        'class' => 'card--future-net-rover',
        'image' => '/assets/card-future-net.svg',
        'updated_at' => @filemtime($root . '/future-net-datacenter-rover') ?: time(),
    ],
    [
        'label' => 'Settlement',
        'name' => '이주 환경 준비물',
        'href' => 'https://mobile.netcity.co.kr/settlement-environment/',
        'description' => '이주용 베낭, 최고급 침낭, 개인 기록용 PC, 비상식량을 사진과 함께 정리한 준비 페이지입니다. 가져갈 수 있는 것과 무상 보급되는 물자를 분리해 안내합니다.',
        'class' => 'card--settlement',
        'image' => '/assets/settlement-backpack-web.jpg',
        'updated_at' => @filemtime($root . '/settlement-environment') ?: time(),
    ],
    [
        'label' => 'AI Netphone',
        'name' => 'AI 기술 접목 넷폰',
        'href' => 'https://mobile.netcity.co.kr/ai-netphone-intelligence/',
        'description' => 'AI 기술과 접목된 넷폰이 일반 통신폰과 어떻게 다른지 설명하는 링크 서버입니다. 통화·앱 중심의 일반폰을 넘어 1차 이주권 개인 데이터센터 인증, 탐사차 기지 접속, 서버 점검, 기록 보존, 개인 임무 수행을 다루는 운영 단말로 정리합니다.',
        'class' => 'card--ai-netphone',
        'image' => '/assets/card-ai-netphone.svg',
        'updated_at' => @filemtime($root . '/ai-netphone-intelligence') ?: time(),
    ],
    [
        'label' => 'Architect List',
        'name' => '건축 웹사이트 리스트',
        'href' => 'https://mobile.netcity.co.kr/architect-websitelist/',
        'description' => '그누보드가 아닌 독립 PHP 페이지로 만든 건축 주제별 웹사이트 아카이브입니다. 검색, 분류, 상세 보기로 건축 관련 사이트 기록을 빠르게 열람합니다.',
        'class' => 'card--architect-websitelist',
        'image' => '/architect-websitelist/assets/netcity24-websitelist.png',
        'updated_at' => @filemtime($root . '/architect-websitelist') ?: time(),
    ],
    [
        'label' => 'Mail Server',
        'name' => 'Netcity 메일 서버',
        'href' => 'https://mail.netcity.co.kr/',
        'description' => 'mail.netcity.co.kr 메일 운영 콘솔입니다. 웹 수신함, Gmail 브릿지, 메일 계정·별칭 관리 화면으로 연결하고 webmaster@netcity.co.kr 수신 흐름을 확인합니다.',
        'class' => 'card--mail-server',
        'image' => '/assets/card-mail-server.svg',
        'updated_at' => @filemtime($root . '/mail-relay') ?: time(),
    ],
    [
        'label' => 'Messenger',
        'name' => 'Netcity Messenger 메신저함',
        'href' => './netcity-messenger/',
        'description' => '192.168.0.197 서버 안에서 메신저용 계정끼리만 주고받는 내부 메일형 메시지함입니다. 회원가입, 로그인, 메신저함, 보낸함, 본문 열람, 첨부파일 업로드와 열람을 지원합니다.',
        'class' => 'card--messenger',
        'image' => '/assets/card-messenger.svg',
        'updated_at' => @filemtime($root . '/netcity-messenger') ?: time(),
    ],
    [
        'label' => 'Termux Docs',
        'name' => '안드로이드 Termux APM 구축 문서',
        'href' => 'https://mobile.netcity.co.kr/usb-android-termux-server/',
        'description' => 'Termux 설치부터 PHP, Nginx, MariaDB, SSH 구동 방식까지 정리한 운영 문서입니다. 실제 실행 링크는 위 PHP 8082와 Nginx 8083 카드에서 확인합니다.',
        'class' => 'card--termux',
        'image' => '/assets/card-termux.svg',
        'updated_at' => @filemtime($root . '/usb-android-termux-server') ?: time(),
    ],
    [
        'label' => 'Galaxy Wi-Fi 194',
        'name' => '192.168.0.100 갤럭시 현재 Wi-Fi 서버 현황판',
        'href' => './android-usb-195/',
        'description' => '갤럭시 현재 Wi-Fi IP 192.168.0.100 기준 현황판입니다. DHCP 예약이 끝나면 192.168.0.194를 목표 주소로 사용할 수 있으며, 8080은 경량 HTTP, 8082/8083은 Termux PHP와 Nginx 서버입니다.',
        'class' => 'card--relay195',
        'image' => '/assets/card-usb-relay.svg',
        'updated_at' => @filemtime($root . '/android-usb-195') ?: time(),
    ],
    [
        'label' => 'Wi-Fi LAN Direct',
        'name' => '안드로이드 공유기 Wi-Fi 직접 IP',
        'href' => './android-usb-direct-router-ip/',
        'description' => '안드로이드폰이 공유기 DHCP에서 192.168.0.xxx Wi-Fi IP를 받아 직접 서버로 응답하는 구조를 정리한 안내 페이지입니다.',
        'class' => 'card--directlan',
        'image' => '/assets/card-usb-relay.svg',
        'updated_at' => @filemtime($root . '/android-usb-direct-router-ip') ?: time(),
    ],
    [
        'label' => 'Virtual noVNC',
        'name' => '서버 가상 모니터를 안드로이드폰에서 보기',
        'href' => './android-phone-monitor/',
        'description' => '192.168.0.197 우분투 서버 안에 TigerVNC 가상 화면을 만들고 noVNC로 공개해, 안드로이드폰 브라우저에서 서버 가상 모니터를 바로 보는 구성입니다.',
        'class' => 'card--monitor',
        'image' => '/assets/card-monitor-display.svg',
        'updated_at' => @filemtime($root . '/android-phone-monitor') ?: time(),
    ],
    [
        'label' => 'Android Display',
        'name' => '안드로이드폰 액정 화면 표시 방식',
        'href' => './android-phone-display-monitor/',
        'description' => '가상 모니터와 별도로 Deskreen, Weylus, TethrLink, VNC 같은 도구로 안드로이드폰 액정을 서버/PC 화면 표시 장치처럼 활용할 수 있는 방법을 정리한 페이지입니다.',
        'class' => 'card--phone-display',
        'image' => '/assets/card-monitor-display.svg',
        'updated_at' => @filemtime($root . '/android-phone-display-monitor') ?: time(),
    ],
];
$skipFolders = [
    'gd-c100-ubuntu-camera' => true,
    'youtube-webcam-live' => true,
    'codex-workspace-live' => true,
    'usb-android-galaxy-wide3' => true,
    'lg-t540-ubuntu-server' => true,
    'samsung-vscode-server' => true,
    'wide3-site-capture' => true,
    'photo-article-server' => true,
    'reporter-photo-journal-server' => true,
    'datacenter-file-processing' => true,
    'future-net-datacenter-rover' => true,
    'settlement-environment' => true,
    'ai-netphone-intelligence' => true,
    'ai-netphone-runtime' => true,
    'architect-websitelist' => true,
    'usb-android-termux-server' => true,
    'android-usb-195' => true,
    'android-usb-direct-router-ip' => true,
    'android-phone-monitor' => true,
    'android-phone-display-monitor' => true,
    'netcity-messenger' => true,
    'usb-android-wifi-php8082' => true,
    'usb-android-wifi-nginx8083' => true,
    'usb-android-wifi-direct-php8082' => true,
    'usb-android-wifi-direct-nginx8083' => true,
    'android-usb-195-php8082' => true,
    'android-usb-195-nginx8083' => true,
    'favicons' => true,
];
$folders = [];
$g7Folders = [];
$entries = scandir($root) ?: [];

foreach ($entries as $entry) {
    if ($entry === '.' || $entry === '..' || $entry === $self) {
        continue;
    }

    if ($entry !== '' && $entry[0] === '.') {
        continue;
    }

    if (str_contains($entry, '.bak-')) {
        continue;
    }

    if (isset($skipFolders[$entry])) {
        continue;
    }

    $path = $root . DIRECTORY_SEPARATOR . $entry;

    if (!is_dir($path)) {
        continue;
    }

    $cardClass = preg_match('/^g7(?:-|$)/i', $entry) ? 'card--g7' : null;
    $cardLabel = $cardClass === 'card--g7' ? 'G7' : 'Folder';

    if (preg_match('/^g5-site\d+$/', $entry) === 1) {
        $cardClass = 'card--g5-site';
        $cardLabel = 'G5 Theme';
    }

    if ($entry === 'g5-site01') {
        $cardClass = 'card--g5-site01';
        $cardLabel = 'G5 Site';
    }

    if ($entry === 'g5-site02') {
        $cardClass = 'card--g5-site02';
        $cardLabel = 'G5 Site';
    }

    if ($entry === 'private-bbs') {
        $cardClass = 'card--private-bbs';
        $cardLabel = 'PC통신 BBS';
    }

    if ($entry === 'mail-relay') {
        $cardClass = 'card--mail-server';
        $cardLabel = 'Mail Server';
    }

    $displayName = display_folder_name($entry);
    if ($entry === 'g5-site01') {
        $displayName = 'G5-site01 사이트 계정';
    }
    if ($entry === 'g5-site02') {
        $displayName = 'G5-site02 테마 사이트 계정';
    }
    if ($entry === 'private-bbs') {
        $displayName = '사설 PC통신 BBS';
    }
    if ($entry === 'lg-t540-ubuntu-server') {
        $displayName = 'CGI / Perl Ubuntu Server';
    }
    if ($entry === 'mail-relay') {
        $displayName = 'Netcity 메일 서버';
    }

    $cardHref = './' . rawurlencode($entry) . '/';
    $cardDescription = folder_description($displayName, $path);

    if ($entry === 'mail-relay') {
        $cardHref = 'https://mail.netcity.co.kr/';
        $cardDescription = 'mail.netcity.co.kr 메일서버 페이지로 바로 연결합니다. 웹 수신함, Gmail 브릿지, 계정·별칭 관리 화면을 한 번에 여는 메일 운영 링크입니다.';
    }

    $card = [
        'label' => $cardLabel,
        'name' => $displayName,
        'href' => $cardHref,
        'description' => $cardDescription,
        'updated_at' => @filemtime($path) ?: time(),
        'class' => $cardClass,
        'image' => card_image_for_folder($entry, $cardClass, $root),
    ];

    if ($entry === 'g5-site02') {
        $card['target'] = '_blank';
        $card['rel'] = 'noopener';
    }

    if ($cardClass === 'card--g7') {
        $g7Folders[] = $card;
        continue;
    }

    $folders[] = $card;
}

usort($folders, static function (array $left, array $right): int {
    return strnatcasecmp($left['name'], $right['name']);
});

usort($g7Folders, static function (array $left, array $right): int {
    return strnatcasecmp($left['name'], $right['name']);
});

$topCards = $manualLinks;
$relayCards = array_merge($folders, $g7Folders);
$samsungVscodeUrl = './samsung-vscode-server/';
$samsungVscodeAvailable = link_is_available($samsungVscodeUrl, $root);
$samsungVoiceUrl = './samsung-vscode-server/voice-stt-server/';
$samsungVoiceAvailable = link_is_available($samsungVoiceUrl, $root);
$wifiPhpUrl = 'http://192.168.0.100:8082/';
$wifiNginxUrl = 'http://192.168.0.100:8083/';
$usb195PhpUrl = 'http://192.168.0.100:8082/';
$usb195NginxUrl = 'http://192.168.0.100:8083/';
$wifiPhpAvailable = link_is_available($wifiPhpUrl, $root);
$wifiNginxAvailable = link_is_available($wifiNginxUrl, $root);
$usb195PhpAvailable = link_is_available($usb195PhpUrl, $root);
$usb195NginxAvailable = link_is_available($usb195NginxUrl, $root);
?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Netcity Links</title>
    <link rel="icon" type="image/svg+xml" href="/netcity-mobile-favicon.svg?v=20260507">
    <link rel="shortcut icon" href="/netcity-mobile-favicon.svg?v=20260507">
    <meta name="theme-color" content="#dbeafe">
    <style>
        :root {
            --bg: #ffffff;
            --surface: #ffffff;
            --surface-soft: #fafafa;
            --text: #111111;
            --muted: #666666;
            --line: #e5e7eb;
            --brand: #111111;
            --brand-soft: #f3f4f6;
            --shadow: 0 10px 30px rgba(17, 17, 17, 0.05);
        }

        * {
            box-sizing: border-box;
        }

        html {
            scroll-behavior: smooth;
            scroll-padding-top: 86px;
        }

        body {
            margin: 0;
            min-height: 100vh;
            background: var(--bg);
            color: var(--text);
            font-family: "SUIT Variable", "Pretendard", "Noto Sans KR", sans-serif;
        }

        .top-category-bar {
            position: sticky;
            top: 0;
            z-index: 30;
            border-bottom: 1px solid rgba(226, 232, 240, .92);
            background: rgba(255, 255, 255, .94);
            backdrop-filter: blur(14px);
            box-shadow: 0 10px 28px rgba(15, 23, 42, .06);
        }

        .top-category-bar__inner {
            display: flex;
            align-items: center;
            gap: 10px;
            width: min(1120px, calc(100% - 32px));
            min-height: 58px;
            margin: 0 auto;
        }

        .top-category-bar__home,
        .top-category-bar__link {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-height: 36px;
            border: 1px solid #e2e8f0;
            background: #ffffff;
            color: #0f172a;
            font-size: 13px;
            font-weight: 950;
            line-height: 1;
            text-decoration: none;
            white-space: nowrap;
        }

        .top-category-bar__home {
            flex: 0 0 auto;
            gap: 7px;
            padding: 0 13px;
            background: #0f172a;
            border-color: #0f172a;
            color: #ffffff;
        }

        .top-category-bar__home svg {
            width: 17px;
            height: 17px;
            display: block;
        }

        .top-category-bar__links {
            display: flex;
            align-items: center;
            gap: 8px;
            overflow-x: auto;
            padding: 10px 2px;
            scrollbar-width: thin;
        }

        .top-category-bar__link {
            padding: 0 12px;
        }

        .top-category-bar__link:hover,
        .top-category-bar__link:focus-visible,
        .top-category-bar__home:hover,
        .top-category-bar__home:focus-visible {
            border-color: #93c5fd;
            outline: 2px solid #bfdbfe;
            outline-offset: 2px;
        }

        .floating-tools {
            position: fixed;
            right: max(16px, calc((100vw - 1120px) / 2 + 16px));
            bottom: 18px;
            z-index: 40;
            display: grid;
            gap: 8px;
            padding: 8px;
            border: 1px solid rgba(226, 232, 240, .95);
            background: rgba(255, 255, 255, .94);
            box-shadow: 0 16px 38px rgba(15, 23, 42, .16);
            backdrop-filter: blur(14px);
        }

        .floating-tools__button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 46px;
            height: 46px;
            border: 0;
            background: #0f172a;
            color: #ffffff;
            text-decoration: none;
        }

        .floating-tools__button svg {
            width: 21px;
            height: 21px;
            display: block;
        }

        .floating-tools__button:hover,
        .floating-tools__button:focus-visible {
            background: #1d4ed8;
            outline: 2px solid #bfdbfe;
            outline-offset: 2px;
        }

        .shell {
            width: min(1120px, calc(100% - 32px));
            margin: 0 auto;
            padding: 28px 0 64px;
        }

        .phone-network-notice {
            display: grid;
            grid-template-columns: minmax(0, 1.2fr) minmax(280px, .8fr);
            gap: 18px;
            align-items: stretch;
            margin: 0 0 18px;
            padding: 18px;
            border: 1px solid #bae6fd;
            border-left: 6px solid #0284c7;
            border-radius: 8px;
            background: #f0f9ff;
            box-shadow: 0 14px 34px rgba(2, 132, 199, .10);
        }

        .phone-network-notice__body {
            display: grid;
            gap: 8px;
            min-width: 0;
        }

        .phone-network-notice__label {
            display: inline-flex;
            align-items: center;
            width: fit-content;
            min-height: 28px;
            padding: 0 10px;
            border-radius: 8px;
            background: #0f172a;
            color: #ffffff;
            font-size: 12px;
            font-weight: 1000;
            line-height: 1;
        }

        .phone-network-notice__title {
            margin: 0;
            color: #0f172a;
            font-size: 22px;
            font-weight: 1000;
            line-height: 1.25;
            letter-spacing: 0;
        }

        .phone-network-notice__copy {
            margin: 0;
            color: #164e63;
            font-size: 15px;
            font-weight: 750;
            line-height: 1.65;
            word-break: keep-all;
        }

        .phone-network-notice__routes {
            display: grid;
            gap: 8px;
            align-content: center;
        }

        .phone-network-notice__route {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            min-height: 44px;
            padding: 10px 12px;
            border: 1px solid rgba(14, 116, 144, .22);
            border-radius: 8px;
            background: #ffffff;
            color: #0f172a;
            font-size: 14px;
            font-weight: 900;
        }

        .phone-network-notice__route code {
            flex: 0 0 auto;
            padding: 4px 7px;
            border-radius: 6px;
            background: #ecfeff;
            color: #155e75;
            font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
            font-size: 13px;
            font-weight: 900;
        }

        .hero {
            display: grid;
            gap: 22px;
            padding: 28px;
            border: 1px solid var(--line);
            border-radius: 24px;
            background: var(--surface);
            box-shadow: var(--shadow);
        }

        .hero-top {
            display: grid;
            grid-template-columns: minmax(0, 1fr) minmax(300px, 420px);
            gap: 24px;
            align-items: start;
        }

        .hero-copy {
            min-width: 0;
        }

        .hero-video {
            overflow: hidden;
            border: 1px solid #dbe4ef;
            border-radius: 0;
            background: #020617;
            box-shadow: 0 12px 30px rgba(15, 23, 42, 0.14);
        }

        .hero-video__frame {
            position: relative;
            aspect-ratio: 16 / 9;
            background: #020617;
        }

        .hero-video__frame iframe {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            border: 0;
        }

        .hero-video__caption {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            padding: 10px 12px;
            background: #0f172a;
            color: #e2e8f0;
            font-size: 12px;
            font-weight: 900;
            line-height: 1.35;
        }

        .hero-video__caption a {
            flex: 0 0 auto;
            color: #67e8f9;
            text-decoration: none;
        }

        .hero-camera-card {
            display: grid;
            grid-template-columns: minmax(0, 1fr) minmax(190px, 260px);
            gap: 18px;
            align-items: center;
            padding: 18px;
            border: 1px solid #7dd3fc;
            border-radius: 8px;
            background:
                linear-gradient(135deg, rgba(14, 165, 233, 0.12), rgba(20, 184, 166, 0.12), rgba(250, 204, 21, 0.1)),
                #f0fdfa;
            color: #083344;
            text-decoration: none;
            box-shadow: 0 14px 34px rgba(8, 145, 178, 0.12);
        }

        .hero-camera-card:hover,
        .hero-camera-card:focus-visible {
            border-color: #22d3ee;
            box-shadow: 0 18px 38px rgba(8, 145, 178, 0.18);
            outline: 2px solid rgba(34, 211, 238, 0.28);
            outline-offset: 2px;
        }

        .hero-camera-card__body {
            display: grid;
            gap: 8px;
            min-width: 0;
        }

        .hero-camera-card__label {
            display: inline-flex;
            align-items: center;
            width: fit-content;
            min-height: 28px;
            padding: 0 10px;
            border-radius: 8px;
            background: #083344;
            color: #ecfeff;
            font-size: 12px;
            font-weight: 900;
            letter-spacing: 0.08em;
            text-transform: uppercase;
        }

        .hero-camera-card__title {
            margin: 0;
            color: #082f49;
            font-size: clamp(22px, 3vw, 34px);
            line-height: 1.15;
            letter-spacing: 0;
        }

        .hero-camera-card__copy {
            margin: 0;
            color: #155e75;
            font-size: 15px;
            font-weight: 750;
            line-height: 1.65;
            word-break: keep-all;
        }

        .hero-camera-card__action {
            display: inline-flex;
            align-items: center;
            width: fit-content;
            min-height: 36px;
            padding: 0 12px;
            border-radius: 8px;
            background: #0f766e;
            color: #ecfdf5;
            font-size: 13px;
            font-weight: 900;
        }

        .hero-camera-card__visual {
            display: flex;
            align-items: center;
            justify-content: center;
            aspect-ratio: 16 / 9;
            margin: 0;
            padding: 8px;
            border: 1px solid rgba(34, 211, 238, 0.32);
            border-radius: 8px;
            background: rgba(255, 255, 255, 0.82);
            overflow: hidden;
        }

        .hero-camera-card__visual img {
            display: block;
            width: 100%;
            height: 100%;
            object-fit: contain;
        }

        .messenger-login {
            display: grid;
            grid-template-columns: minmax(0, 1fr) minmax(300px, 420px);
            gap: 16px;
            align-items: stretch;
            padding: 16px;
            border: 1px solid #dbeafe;
            background: #f8fbff;
            box-shadow: inset 0 1px 0 rgba(255, 255, 255, .8);
        }

        .messenger-login__copy {
            min-width: 0;
            padding: 4px 0;
        }

        .messenger-login__label {
            margin: 0 0 7px;
            color: #2563eb;
            font-size: 12px;
            font-weight: 1000;
            letter-spacing: .08em;
            text-transform: uppercase;
        }

        .messenger-login__title {
            margin: 0;
            color: #0f172a;
            font-size: 24px;
            line-height: 1.25;
            letter-spacing: 0;
        }

        .messenger-login__text {
            margin: 9px 0 0;
            max-width: 640px;
            color: #475569;
            font-size: 14px;
            font-weight: 750;
            line-height: 1.7;
            word-break: keep-all;
        }

        .messenger-login__panel {
            display: grid;
            gap: 10px;
            padding: 14px;
            border: 1px solid #bfdbfe;
            background: #ffffff;
        }

        .messenger-login__flash {
            margin: 0;
            padding: 10px 11px;
            border: 1px solid #bbf7d0;
            background: #f0fdf4;
            color: #166534;
            font-size: 13px;
            font-weight: 900;
            line-height: 1.5;
        }

        .messenger-login__flash.is-warn {
            border-color: #fecaca;
            background: #fff1f2;
            color: #9f1239;
        }

        .messenger-login__form {
            display: grid;
            grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
            gap: 8px;
            align-items: stretch;
        }

        .messenger-login__field {
            width: 100%;
            min-height: 42px;
            border: 1px solid #cbd5e1;
            background: #f8fafc;
            color: #0f172a;
            padding: 0 11px;
            font: inherit;
            font-size: 13px;
            font-weight: 850;
            outline: 0;
        }

        .messenger-login__field:focus {
            border-color: #2563eb;
            background: #ffffff;
            box-shadow: 0 0 0 3px rgba(37, 99, 235, .14);
        }

        .messenger-login__button,
        .messenger-login__link {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-height: 42px;
            border: 1px solid #0f172a;
            background: #0f172a;
            color: #ffffff;
            padding: 0 14px;
            font: inherit;
            font-size: 13px;
            font-weight: 1000;
            text-decoration: none;
            white-space: nowrap;
            cursor: pointer;
        }

        .messenger-login__link {
            border-color: #bfdbfe;
            background: #eff6ff;
            color: #1d4ed8;
        }

        .messenger-login__button:hover,
        .messenger-login__button:focus-visible,
        .messenger-login__link:hover,
        .messenger-login__link:focus-visible {
            outline: 2px solid #bfdbfe;
            outline-offset: 2px;
        }

        .messenger-login__status {
            display: grid;
            gap: 10px;
        }

        .messenger-login__identity {
            display: grid;
            grid-template-columns: auto 1fr;
            gap: 10px;
            align-items: center;
        }

        .messenger-login__avatar {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 46px;
            height: 46px;
            background: #0f172a;
            color: #ffffff;
            font-size: 16px;
            font-weight: 1000;
        }

        .messenger-login__name {
            margin: 0;
            color: #0f172a;
            font-size: 16px;
            font-weight: 1000;
            line-height: 1.25;
        }

        .messenger-login__meta {
            margin: 3px 0 0;
            color: #64748b;
            font-size: 12px;
            font-weight: 900;
            line-height: 1.45;
        }

        .messenger-login__actions {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        .messenger-login__actions form {
            margin: 0;
        }

        .messenger-login__button.is-light {
            border-color: #e2e8f0;
            background: #ffffff;
            color: #334155;
        }

        .eyebrow {
            margin: 0 0 10px;
            font-size: 12px;
            font-weight: 800;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            color: var(--muted);
        }

        h1 {
            margin: 0;
            font-size: clamp(30px, 6vw, 56px);
            line-height: 1;
            letter-spacing: 0;
            word-break: keep-all;
        }

        .server-subtitle {
            display: flex;
            flex-wrap: wrap;
            align-items: baseline;
            gap: 8px;
            margin-top: 10px;
        }

        .server-ip {
            font-size: 18px;
            font-weight: 900;
            line-height: 1.2;
            letter-spacing: 0;
            color: #374151;
        }

        .server-tag {
            font-size: 18px;
            font-weight: 800;
            line-height: 1.2;
            letter-spacing: 0;
            color: #4b5563;
        }

        .lead {
            max-width: 760px;
            margin: 18px 0 0;
            font-size: 16px;
            line-height: 1.75;
            color: var(--muted);
        }

        .tip {
            margin-top: 20px;
            padding: 18px;
            border-radius: 18px;
            background: #f8fafc;
            color: var(--text);
        }

        .pc-spec {
            position: relative;
            min-height: 82px;
            padding: 20px 144px 20px 20px;
        }

        .pc-spec__title {
            margin: 0;
            font-size: 15px;
            font-weight: 900;
            line-height: 1.65;
            color: #111827;
        }

        .pc-spec__mission {
            margin: 10px 0 0;
            max-width: 760px;
            color: #334155;
            font-size: 14px;
            font-weight: 800;
            line-height: 1.75;
            word-break: keep-all;
        }

        .pc-spec__settlement {
            margin: 12px 0 0;
            max-width: 820px;
            padding: 14px 16px;
            border-left: 5px solid #0f766e;
            background: #ecfdf5;
            color: #134e4a;
            font-size: 14px;
            font-weight: 850;
            line-height: 1.75;
            word-break: keep-all;
        }

        .pc-spec__audience {
            margin: 10px 0 0;
            max-width: 820px;
            padding: 13px 16px;
            border-left: 5px solid #2563eb;
            background: #eff6ff;
            color: #1e3a8a;
            font-size: 14px;
            font-weight: 850;
            line-height: 1.75;
            word-break: keep-all;
        }

        .pc-env-button {
            position: absolute;
            top: 16px;
            right: 16px;
            display: inline-flex;
            align-items: center;
            gap: 8px;
            min-height: 38px;
            padding: 0 13px;
            border: 0;
            border-radius: 0;
            background: #e7f0ff;
            color: #1d4ed8;
            font: inherit;
            font-size: 13px;
            font-weight: 900;
            cursor: pointer;
        }

        .pc-env-button:hover,
        .pc-env-button:focus-visible {
            background: #d9e8ff;
            outline: 2px solid #93c5fd;
            outline-offset: 2px;
        }

        .pc-env-button__icon {
            width: 20px;
            height: 20px;
            display: block;
        }

        .pc-env-modal {
            width: min(820px, calc(100% - 32px));
            max-height: min(760px, calc(100vh - 48px));
            padding: 0;
            border: 0;
            border-radius: 0;
            background: #ffffff;
            color: #111827;
            box-shadow: 0 28px 80px rgba(15, 23, 42, 0.28);
        }

        .pc-env-modal::backdrop {
            background: rgba(15, 23, 42, 0.46);
            backdrop-filter: blur(4px);
        }

        .pc-env-modal__inner {
            padding: 24px;
        }

        .pc-env-modal__head {
            display: flex;
            align-items: flex-start;
            justify-content: space-between;
            gap: 18px;
            margin-bottom: 18px;
        }

        .pc-env-modal__label {
            margin: 0 0 7px;
            font-size: 12px;
            font-weight: 900;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: #2563eb;
        }

        .pc-env-modal__title {
            margin: 0;
            font-size: 24px;
            line-height: 1.25;
            letter-spacing: 0;
            color: #111827;
        }

        .pc-env-modal__summary {
            margin: 10px 0 0;
            font-size: 14px;
            font-weight: 800;
            line-height: 1.7;
            color: #475569;
        }

        .pc-env-modal__close {
            flex: 0 0 auto;
            width: 38px;
            height: 38px;
            border: 0;
            border-radius: 0;
            background: #f1f5f9;
            color: #0f172a;
            font-size: 26px;
            line-height: 1;
            cursor: pointer;
        }

        .pc-env-modal__close:hover,
        .pc-env-modal__close:focus-visible {
            background: #e2e8f0;
            outline: 2px solid #93c5fd;
            outline-offset: 2px;
        }

        .pc-spec__grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
            gap: 10px;
        }

        .pc-spec__item {
            display: grid;
            gap: 5px;
            padding: 13px;
            background: #ffffff;
        }

        .pc-spec__label {
            font-size: 12px;
            font-weight: 900;
            color: #64748b;
        }

        .pc-spec__value {
            font-size: 14px;
            font-weight: 800;
            line-height: 1.55;
            color: #111827;
        }

        .quick-status {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
            gap: 14px;
            margin-top: 28px;
        }

        .quick-status .card {
            min-height: 278px;
            border: 0;
            border-radius: 0;
            box-shadow: 0 10px 26px rgba(15, 23, 42, 0.04);
        }

        .quick-status__summary {
            pointer-events: none;
        }

        .quick-status__summary:hover {
            transform: none;
            box-shadow: 0 10px 26px rgba(15, 23, 42, 0.04);
        }

        .card__address {
            display: inline-flex;
            align-self: flex-start;
            margin-top: auto;
            padding: 9px 12px;
            background: rgba(255, 255, 255, 0.72);
            font-size: 13px;
            font-weight: 800;
            color: #065f46;
            word-break: break-all;
        }

        .card__links {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-top: 2px;
        }

        .card__link {
            display: inline-flex;
            align-items: center;
            min-height: 34px;
            padding: 0 11px;
            background: rgba(255, 255, 255, 0.72);
            color: #065f46;
            font-size: 13px;
            font-weight: 800;
            line-height: 1.25;
            text-decoration: none;
            word-break: break-all;
        }

        .card__link:hover {
            background: #ffffff;
        }

        .card__link.is-offline {
            filter: grayscale(1);
            opacity: 0.68;
            background: #e5e7eb;
            color: #4b5563 !important;
        }

        .card__status {
            display: inline-flex;
            align-self: flex-start;
            padding: 7px 10px;
            background: rgba(255, 255, 255, 0.72);
            color: #166534;
            font-size: 12px;
            font-weight: 900;
        }

        .card__build {
            display: grid;
            gap: 7px;
            margin-top: auto;
            padding: 12px;
            background: rgba(255, 255, 255, 0.58);
            color: #234036;
            font-size: 13px;
            line-height: 1.45;
        }

        .card__build span {
            display: block;
        }

        .card.is-offline .card__status {
            color: #4b5563;
        }

        .capture-feature {
            display: grid;
            grid-template-columns: minmax(0, 1.55fr) minmax(280px, 0.85fr);
            gap: 16px;
            align-items: stretch;
            margin-top: 30px;
        }

        .capture-feature-card {
            position: relative;
            display: grid;
            grid-template-columns: minmax(0, 1fr) minmax(260px, 360px);
            gap: 22px;
            overflow: hidden;
            padding: 24px;
            border: 1px solid #dbe4ef;
            border-radius: 14px;
            background:
                linear-gradient(135deg, rgba(248, 250, 252, 0.98), rgba(255, 255, 255, 0.96)),
                #ffffff;
            box-shadow: 0 12px 30px rgba(15, 23, 42, 0.07);
        }

        .capture-feature-card::before {
            content: "";
            position: absolute;
            inset: 0 0 auto;
            height: 4px;
            background: linear-gradient(90deg, #0f766e, #2563eb);
        }

        .capture-feature-card::after {
            content: none;
        }

        .photo-article-feature-card {
            position: relative;
            display: grid;
            align-content: space-between;
            gap: 18px;
            overflow: hidden;
            min-height: 100%;
            padding: 24px;
            border: 1px solid #e2e8f0;
            border-radius: 14px;
            background:
                linear-gradient(135deg, rgba(255, 247, 237, 0.96), rgba(255, 255, 255, 0.98)),
                #ffffff;
            box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
        }

        .photo-article-feature-card::before {
            content: "";
            position: absolute;
            inset: 0 0 auto;
            height: 4px;
            background: linear-gradient(90deg, #ea580c, #475569);
        }

        .photo-article-feature__body {
            position: relative;
            z-index: 1;
            display: grid;
            gap: 14px;
        }

        .photo-article-feature__eyebrow {
            display: inline-flex;
            align-self: flex-start;
            min-height: 31px;
            align-items: center;
            padding: 0 11px;
            background: #ffedd5;
            color: #9a3412;
            font-size: 12px;
            font-weight: 900;
        }

        .photo-article-feature__title {
            margin: 0;
            color: #111827;
            font-size: clamp(24px, 2.6vw, 32px);
            line-height: 1.12;
            letter-spacing: 0;
            word-break: keep-all;
        }

        .photo-article-feature__copy {
            margin: 0;
            color: #475569;
            font-size: 14px;
            font-weight: 800;
            line-height: 1.7;
        }

        .photo-article-feature__preview {
            position: relative;
            z-index: 1;
            display: grid;
            gap: 8px;
            padding: 14px;
            background: #ffffff;
            border: 1px solid rgba(234, 88, 12, 0.22);
        }

        .photo-article-feature__preview span {
            display: block;
            color: #9a3412;
            font-size: 12px;
            font-weight: 900;
        }

        .photo-article-feature__preview strong {
            color: #0f172a;
            font-size: 18px;
            line-height: 1.25;
            word-break: keep-all;
        }

        .photo-article-feature__preview p {
            margin: 0;
            color: #64748b;
            font-size: 13px;
            font-weight: 800;
            line-height: 1.55;
        }

        .photo-article-feature__steps {
            display: flex;
            flex-wrap: wrap;
            gap: 7px;
        }

        .photo-article-feature__steps span {
            display: inline-flex;
            align-items: center;
            min-height: 30px;
            padding: 0 9px;
            background: #f8fafc;
            border: 1px solid #e2e8f0;
            color: #334155;
            font-size: 12px;
            font-weight: 900;
        }

        .photo-article-feature__actions {
            position: relative;
            z-index: 1;
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        .photo-article-feature__button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-height: 40px;
            padding: 0 13px;
            border: 1px solid rgba(15, 23, 42, 0.14);
            background: #ffffff;
            color: #0f172a;
            font-size: 13px;
            font-weight: 900;
            text-decoration: none;
        }

        .photo-article-feature__button--primary {
            border-color: #ea580c;
            background: #ea580c;
            color: #ffffff;
        }

        .mobility-engine-feature {
            margin-top: 30px;
        }

        .mobility-engine-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 14px;
            align-items: stretch;
            margin-top: 18px;
        }

        .mobility-engine-card {
            position: relative;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            gap: 13px;
            min-height: 312px;
            padding: 20px;
            border: 1px solid var(--line);
            border-radius: 0;
            background: #ffffff;
            box-shadow: 0 10px 26px rgba(15, 23, 42, 0.04);
        }

        .mobility-engine-card::before {
            content: none;
        }

        .mobility-engine-card:nth-child(1) {
            background: #f8fafc;
            border-color: #cbd5e1;
        }

        .mobility-engine-card:nth-child(2) {
            background: #eef6ff;
            border-color: #b7d8ff;
        }

        .mobility-engine-card:nth-child(3) {
            background: #f0fdfa;
            border-color: #99f6e4;
        }

        .mobility-engine__body,
        .mobility-engine__panel {
            position: relative;
            z-index: 1;
        }

        .mobility-engine__body {
            display: grid;
            gap: 8px;
        }

        .mobility-engine__eyebrow {
            display: inline-flex;
            align-self: flex-start;
            padding: 7px 10px;
            background: #111827;
            color: #f9fafb;
            font-size: 12px;
            font-weight: 900;
            line-height: 1;
            letter-spacing: 0;
            text-transform: none;
        }

        .mobility-engine-card:nth-child(2) .mobility-engine__eyebrow {
            background: #1e3a8a;
        }

        .mobility-engine-card:nth-child(3) .mobility-engine__eyebrow {
            background: #0f766e;
        }

        .mobility-engine__title {
            margin: 0;
            color: #111827;
            font-size: 23px;
            line-height: 1.18;
            letter-spacing: 0;
            word-break: keep-all;
        }

        .mobility-engine__copy {
            margin: 0;
            max-width: 720px;
            color: #475569;
            font-size: 14px;
            font-weight: 800;
            line-height: 1.55;
        }

        .mobility-engine__flow {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
            gap: 7px;
        }

        .mobility-engine__step {
            display: inline-flex;
            align-items: center;
            min-height: 34px;
            padding: 7px 10px;
            border: 0;
            background: rgba(255, 255, 255, 0.72);
            color: #334155;
            font-size: 13px;
            font-weight: 800;
            line-height: 1.3;
        }

        .mobility-engine__actions {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-top: auto;
        }

        .mobility-engine__button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-height: 34px;
            padding: 0 11px;
            border: 0;
            background: #ffffff;
            color: #0f172a;
            font-size: 13px;
            font-weight: 900;
            text-decoration: none;
        }

        .mobility-engine__button--primary {
            background: #111827;
            color: #f9fafb;
        }

        .mobility-engine-card:nth-child(2) .mobility-engine__button--primary {
            background: #1e3a8a;
            color: #eff6ff;
        }

        .mobility-engine-card:nth-child(2) .mobility-engine__button--runtime {
            background: #0f766e;
            color: #ecfeff;
            box-shadow: inset 0 -2px 0 rgba(6, 78, 59, 0.28);
        }

        .mobility-engine-card:nth-child(3) .mobility-engine__button--primary {
            background: #0f766e;
            color: #ecfeff;
        }

        .mobility-engine__panel {
            display: grid;
            gap: 7px;
            align-content: start;
            padding: 0;
            border: 0;
            background: transparent;
        }

        .mobility-engine__metric {
            display: grid;
            grid-template-columns: 92px minmax(0, 1fr);
            gap: 9px;
            padding: 9px 10px;
            background: rgba(255, 255, 255, 0.72);
            color: #334155;
            font-size: 13px;
            line-height: 1.45;
        }

        .mobility-engine__metric span {
            color: #64748b;
            font-size: 12px;
            font-weight: 900;
        }

        .mobility-engine__metric strong {
            color: #334155;
            font-size: 13px;
            font-weight: 800;
            line-height: 1.45;
            word-break: keep-all;
        }

        .mobility-engine-grid .mobility-engine-card {
            gap: 14px;
            min-height: 312px;
            padding: 20px;
        }

        .mobility-engine-grid .mobility-engine__title {
            font-size: 23px;
            line-height: 1.18;
        }

        .mobility-engine-grid .mobility-engine__copy {
            max-width: none;
            font-size: 14px;
            line-height: 1.65;
        }

        .mobility-engine-grid .mobility-engine__button {
            min-height: 36px;
            padding: 0 11px;
            font-size: 12px;
        }

        .mobility-engine-grid .mobility-engine__step {
            min-height: 30px;
            padding: 0 9px;
            font-size: 12px;
        }

        .mobility-engine-grid .mobility-engine__panel {
            padding: 0;
        }

        .mobility-engine-grid .mobility-engine__metric strong {
            font-size: 13px;
        }

        .mobility-engine-empty-slot {
            min-height: 312px;
            border: 1px dashed #a9e5c2;
            border-radius: 0;
            background: #effbf5;
            box-shadow: 0 10px 26px rgba(15, 23, 42, 0.04);
        }

        .technical-engine-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 14px;
            align-items: stretch;
            margin-top: 18px;
        }

        .technical-engine-card {
            position: relative;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            gap: 13px;
            min-height: 312px;
            padding: 20px;
            border: 1px solid var(--line);
            border-radius: 0;
            background: #ffffff;
            color: inherit;
            text-decoration: none;
            box-shadow: 0 10px 26px rgba(15, 23, 42, 0.04);
        }

        .technical-engine-card:hover {
            transform: none;
            box-shadow: 0 10px 26px rgba(15, 23, 42, 0.04);
        }

        .technical-engine-card::before {
            content: none;
        }

        .technical-engine-card--file {
            background: #f8fafc;
            border-color: #cbd5e1;
        }

        .technical-engine-card--editor {
            background:
                linear-gradient(135deg, rgba(6, 78, 59, 0.96), rgba(12, 74, 110, 0.94)),
                #062b38;
            border-color: #5eead4;
            color: #ecfeff;
            box-shadow: 0 14px 34px rgba(8, 47, 73, 0.18);
        }

        .technical-engine-card--capture {
            background: #eef6ff;
            border-color: #b7d8ff;
        }

        .technical-engine-card--photo {
            background: #effbf5;
            border-color: #a9e5c2;
        }

        .technical-engine-card--journal {
            background: #fff7ed;
            border-color: #fdba74;
        }

        .technical-engine-card--codex {
            background:
                linear-gradient(135deg, rgba(15, 23, 42, 0.96), rgba(14, 116, 144, 0.9) 54%, rgba(180, 83, 9, 0.82)),
                #0f172a;
            border-color: #67e8f9;
            color: #ecfeff;
            box-shadow: 0 14px 34px rgba(15, 23, 42, 0.2);
        }

        .technical-engine-card--file:hover {
            border-color: #cbd5e1;
        }

        .technical-engine-card--editor:hover {
            border-color: #99f6e4;
            box-shadow: 0 16px 38px rgba(8, 47, 73, 0.22);
        }

        .technical-engine-card--editor::before,
        .technical-engine-card--editor::after {
            content: "";
            position: absolute;
            inset: 0;
            pointer-events: none;
        }

        .technical-engine-card--editor::before {
            z-index: 0;
            background:
                linear-gradient(rgba(94, 234, 212, 0.17) 1px, transparent 1px),
                linear-gradient(90deg, rgba(125, 211, 252, 0.14) 1px, transparent 1px),
                linear-gradient(120deg, transparent 0 40%, rgba(255, 255, 255, 0.18) 48%, transparent 56%);
            background-size: 30px 30px, 30px 30px, 220% 100%;
            opacity: 0.74;
            animation: editorCardGrid 9s linear infinite;
        }

        .technical-engine-card--editor::after {
            z-index: 0;
            inset: auto 18px 18px auto;
            width: 132px;
            height: 54px;
            border: 1px solid rgba(190, 242, 100, 0.48);
            background:
                repeating-linear-gradient(90deg, rgba(190, 242, 100, 0.24) 0 2px, transparent 2px 10px),
                rgba(15, 23, 42, 0.36);
            opacity: 0.86;
            transform: skewX(-10deg);
        }

        .technical-engine-card--editor > * {
            position: relative;
            z-index: 1;
        }

        @keyframes editorCardGrid {
            0% { background-position: 0 0, 0 0, 180% 0; }
            100% { background-position: 30px 30px, -30px 30px, -40% 0; }
        }

        @media (prefers-reduced-motion: reduce) {
            .technical-engine-card--editor::before {
                animation: none;
            }
        }

        .technical-engine-card--capture:hover {
            border-color: #b7d8ff;
        }

        .technical-engine-card--photo:hover {
            border-color: #a9e5c2;
        }

        .technical-engine-card--journal:hover {
            border-color: #fdba74;
        }

        .technical-engine-card--codex:hover {
            border-color: #a7f3d0;
            box-shadow: 0 16px 38px rgba(15, 23, 42, 0.24);
        }

        .technical-engine__eyebrow {
            display: inline-flex;
            align-self: flex-start;
            padding: 7px 10px;
            background: #111827;
            color: #f9fafb;
            font-size: 12px;
            font-weight: 900;
            line-height: 1;
        }

        .technical-engine-card--file .technical-engine__eyebrow {
            background: #111827;
            color: #f9fafb;
        }

        .technical-engine-card--editor .technical-engine__eyebrow {
            background: rgba(15, 23, 42, 0.86);
            color: #ccfbf1;
            border: 1px solid rgba(94, 234, 212, 0.42);
        }

        .technical-engine-card--capture .technical-engine__eyebrow {
            background: #1e3a8a;
            color: #eff6ff;
        }

        .technical-engine-card--photo .technical-engine__eyebrow {
            background: #065f46;
            color: #ecfdf5;
        }

        .technical-engine-card--journal .technical-engine__eyebrow {
            background: #9a3412;
            color: #fff7ed;
        }

        .technical-engine-card--codex .technical-engine__eyebrow {
            background: rgba(236, 254, 255, 0.92);
            color: #0f172a;
        }

        .technical-engine__title {
            margin: 0;
            color: #111827;
            font-size: 23px;
            line-height: 1.18;
            letter-spacing: 0;
            word-break: keep-all;
        }

        .technical-engine-card--file .technical-engine__title {
            color: #111827;
        }

        .technical-engine-card--capture .technical-engine__title {
            color: #111827;
        }

        .technical-engine-card--editor .technical-engine__title {
            color: #f8fafc;
            text-shadow: 0 2px 16px rgba(15, 23, 42, 0.48);
        }

        .technical-engine-card--photo .technical-engine__title,
        .technical-engine-card--journal .technical-engine__title {
            color: #111827;
        }

        .technical-engine-card--codex .technical-engine__title {
            color: #f8fafc;
        }

        .technical-engine__copy {
            margin: 0;
            color: #475569;
            font-size: 14px;
            font-weight: 800;
            line-height: 1.55;
        }

        .technical-engine-card--file .technical-engine__copy {
            color: #475569;
        }

        .technical-engine-card--capture .technical-engine__copy {
            color: #475569;
        }

        .technical-engine-card--editor .technical-engine__copy {
            color: #dffcf8;
            text-shadow: 0 1px 10px rgba(15, 23, 42, 0.42);
        }

        .technical-engine-card--photo .technical-engine__copy,
        .technical-engine-card--journal .technical-engine__copy {
            color: #475569;
        }

        .technical-engine-card--codex .technical-engine__copy {
            color: #dbeafe;
        }

        .technical-engine__flow {
            display: grid;
            gap: 7px;
        }

        .technical-engine__actions {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        .technical-engine__step {
            display: inline-flex;
            align-items: center;
            min-height: 34px;
            padding: 7px 10px;
            border: 0;
            background: rgba(255, 255, 255, 0.72);
            color: #334155;
            font-size: 13px;
            font-weight: 800;
            line-height: 1.3;
        }

        .technical-engine-card--editor .technical-engine__step {
            background: rgba(15, 23, 42, 0.58);
            color: #ecfeff;
            box-shadow: inset 0 0 0 1px rgba(125, 211, 252, 0.22);
            text-shadow: 0 1px 8px rgba(15, 23, 42, 0.44);
        }

        .technical-engine-card--codex .technical-engine__step {
            background: rgba(255, 255, 255, 0.16);
            color: #ecfeff;
        }

        .technical-engine__actions {
            margin-top: auto;
        }

        .technical-engine__button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-height: 34px;
            padding: 0 11px;
            border: 0;
            background: #ffffff;
            color: #0f172a;
            font-size: 13px;
            font-weight: 900;
            text-decoration: none;
        }

        .technical-engine__button--primary {
            background: #0f172a;
            color: #f9fafb;
        }

        .technical-engine-card--editor .technical-engine__button {
            background: rgba(236, 254, 255, 0.92);
            color: #0f172a;
            box-shadow: inset 0 0 0 1px rgba(15, 118, 110, 0.16);
        }

        .technical-engine-card--capture .technical-engine__button--primary {
            background: #1e3a8a;
            color: #eff6ff;
        }

        .technical-engine-card--editor .technical-engine__button--primary {
            background: #ecfeff;
            color: #134e4a;
        }

        .technical-engine-card--photo .technical-engine__button--primary {
            background: #065f46;
            color: #ecfdf5;
        }

        .technical-engine-card--journal .technical-engine__button--primary {
            background: #9a3412;
            color: #fff7ed;
        }

        .technical-engine-card--codex .technical-engine__button {
            background: rgba(255, 255, 255, 0.92);
            color: #0f172a;
        }

        .technical-engine-card--codex .technical-engine__button--primary {
            background: #ecfeff;
            color: #0f172a;
        }

        .technical-engine__status {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
            min-height: 36px;
            padding: 9px 10px;
            background: rgba(255, 255, 255, 0.72);
            color: #334155;
            font-size: 13px;
            font-weight: 800;
            line-height: 1.35;
        }

        .technical-engine__stats {
            grid-template-columns: repeat(3, minmax(0, 1fr));
        }

        .technical-engine__stats .capture-feature__stat {
            padding: 9px 10px;
            border: 0;
            background: rgba(255, 255, 255, 0.72);
        }

        .technical-engine__stats .capture-feature__stat strong {
            font-size: 18px;
            line-height: 1.15;
        }

        .technical-engine__meter {
            padding: 9px 10px;
            border: 0;
            background: rgba(255, 255, 255, 0.72);
        }

        .technical-engine__current {
            margin: 0;
            padding: 9px 10px;
            background: rgba(255, 255, 255, 0.72);
            color: #334155;
            font-size: 13px;
            font-weight: 800;
            line-height: 1.45;
        }

        .journal-engine__stats {
            display: grid;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 7px;
        }

        .journal-engine__stat {
            display: grid;
            gap: 4px;
            min-height: 58px;
            padding: 9px 10px;
            background: rgba(255, 255, 255, 0.72);
            color: #334155;
        }

        .journal-engine__stat span {
            color: #9a3412;
            font-size: 11px;
            font-weight: 950;
            line-height: 1.25;
        }

        .journal-engine__stat strong {
            color: #111827;
            font-size: 18px;
            font-weight: 950;
            line-height: 1.1;
        }

        .journal-engine__current {
            border-left: 4px solid #fb923c;
        }

        .capture-feature__main,
        .capture-feature__side {
            position: relative;
            z-index: 1;
        }

        .capture-feature__main {
            display: grid;
            gap: 15px;
        }

        .capture-feature__eyebrow {
            display: inline-flex;
            align-self: flex-start;
            min-height: 31px;
            align-items: center;
            padding: 0 11px;
            background: #e2e8f0;
            color: #334155;
            font-size: 12px;
            font-weight: 900;
            letter-spacing: 0;
        }

        .capture-feature__title {
            margin: 0;
            color: #0f172a;
            font-size: clamp(26px, 3.4vw, 38px);
            line-height: 1.08;
            letter-spacing: 0;
            word-break: keep-all;
        }

        .capture-feature__copy {
            max-width: 760px;
            margin: 0;
            color: #334155;
            font-size: 15px;
            font-weight: 800;
            line-height: 1.75;
        }

        .capture-feature__flow {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        .capture-feature__step {
            display: inline-flex;
            align-items: center;
            min-height: 34px;
            padding: 0 11px;
            background: rgba(255, 255, 255, 0.78);
            border: 1px solid rgba(148, 163, 184, 0.38);
            color: #0f172a;
            font-size: 13px;
            font-weight: 900;
        }

        .capture-feature__actions {
            display: flex;
            flex-wrap: wrap;
            gap: 9px;
        }

        .capture-feature__button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-height: 42px;
            padding: 0 14px;
            border: 1px solid rgba(15, 23, 42, 0.16);
            background: #ffffff;
            color: #0f172a;
            font-size: 14px;
            font-weight: 900;
            text-decoration: none;
            box-shadow: none;
        }

        .capture-feature__button--primary {
            border-color: #0f766e;
            background: #0f766e;
            color: #ffffff;
        }

        .capture-feature__button--blue {
            border-color: #334155;
            background: #334155;
            color: #ffffff;
        }

        .capture-feature__side {
            display: grid;
            gap: 10px;
            align-content: start;
        }

        .capture-feature__status {
            display: inline-flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            min-height: 42px;
            padding: 0 12px;
            background: #0f172a;
            color: #f8fafc;
            font-size: 13px;
            font-weight: 900;
        }

        .capture-feature__dot {
            display: inline-block;
            vertical-align: -1px;
            margin-right: 7px;
            width: 10px;
            height: 10px;
            background: #94a3b8;
            border-radius: 999px;
        }

        .capture-feature__dot.is-on {
            background: #22c55e;
            box-shadow: 0 0 0 5px rgba(34, 197, 94, 0.18);
        }

        .capture-feature__stats {
            display: grid;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 8px;
        }

        .capture-feature__stat {
            display: grid;
            gap: 4px;
            padding: 11px;
            background: rgba(255, 255, 255, 0.78);
            border: 1px solid rgba(148, 163, 184, 0.32);
        }

        .capture-feature__stat span {
            color: #64748b;
            font-size: 12px;
            font-weight: 900;
        }

        .capture-feature__stat strong {
            color: #0f172a;
            font-size: 23px;
            line-height: 1;
        }

        .capture-feature__target {
            display: grid;
            gap: 6px;
            padding: 12px;
            background: rgba(255, 255, 255, 0.76);
            border: 1px solid rgba(20, 184, 166, 0.24);
            color: #115e59;
            font-size: 13px;
            font-weight: 900;
            line-height: 1.45;
        }

        .capture-feature__target a {
            color: #1d4ed8;
            text-decoration: none;
            word-break: break-all;
        }

        .capture-feature__ops {
            display: grid;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 8px;
        }

        .capture-feature__op {
            display: grid;
            gap: 4px;
            min-height: 62px;
            padding: 10px;
            background: rgba(255, 255, 255, 0.78);
            border: 1px solid rgba(37, 99, 235, 0.18);
        }

        .capture-feature__op span {
            color: #475569;
            font-size: 11px;
            font-weight: 900;
        }

        .capture-feature__op strong {
            color: #1e3a8a;
            font-size: 16px;
            line-height: 1.15;
            word-break: keep-all;
        }

        .capture-feature__latest {
            display: grid;
            grid-template-columns: 96px minmax(0, 1fr);
            gap: 10px;
            padding: 10px;
            background: rgba(255, 255, 255, 0.82);
            border: 1px solid rgba(249, 115, 22, 0.2);
        }

        .capture-feature__thumb {
            display: grid;
            place-items: center;
            min-height: 70px;
            background: #e0f2fe;
            color: #0f172a;
            font-size: 12px;
            font-weight: 900;
            text-align: center;
            overflow: hidden;
        }

        .capture-feature__thumb img {
            width: 100%;
            height: 100%;
            min-height: 70px;
            object-fit: cover;
            display: none;
        }

        .capture-feature__thumb img[src] {
            display: block;
        }

        .capture-feature__latest-body {
            display: grid;
            gap: 6px;
            min-width: 0;
            align-content: center;
        }

        .capture-feature__latest-title {
            margin: 0;
            color: #0f172a;
            font-size: 14px;
            font-weight: 900;
            line-height: 1.35;
            word-break: keep-all;
        }

        .capture-feature__latest-meta,
        .capture-feature__latest-url {
            margin: 0;
            color: #64748b;
            font-size: 12px;
            font-weight: 800;
            line-height: 1.35;
            word-break: break-all;
        }

        .capture-feature__latest-links {
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
        }

        .capture-feature__mini-link {
            display: inline-flex;
            align-items: center;
            min-height: 28px;
            padding: 0 9px;
            background: #0f172a;
            color: #ffffff;
            font-size: 12px;
            font-weight: 900;
            text-decoration: none;
        }

        .capture-feature__work {
            display: grid;
            gap: 8px;
        }

        .capture-feature__work-item {
            display: grid;
            gap: 5px;
            padding: 10px;
            background: rgba(255, 255, 255, 0.78);
            border: 1px solid rgba(14, 165, 233, 0.22);
            color: #0f172a;
        }

        .capture-feature__work-item.is-failed {
            border-color: rgba(225, 29, 72, 0.2);
            background: rgba(255, 241, 242, 0.82);
        }

        .capture-feature__work-label {
            display: inline-flex;
            align-self: flex-start;
            min-height: 24px;
            align-items: center;
            padding: 0 8px;
            background: #0369a1;
            color: #ffffff;
            font-size: 11px;
            font-weight: 900;
        }

        .capture-feature__work-item.is-failed .capture-feature__work-label {
            background: #be123c;
        }

        .capture-feature__work-title {
            margin: 0;
            color: #0f172a;
            font-size: 13px;
            font-weight: 900;
            line-height: 1.35;
            word-break: keep-all;
        }

        .capture-feature__work-url,
        .capture-feature__work-meta {
            margin: 0;
            color: #64748b;
            font-size: 12px;
            font-weight: 800;
            line-height: 1.35;
            word-break: break-all;
        }

        .capture-feature__meter {
            display: grid;
            gap: 8px;
            padding: 12px;
            background: rgba(15, 23, 42, 0.88);
            color: #f8fafc;
        }

        .capture-feature__meter-head {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            font-size: 12px;
            font-weight: 900;
        }

        .capture-feature__track {
            height: 10px;
            overflow: hidden;
            background: rgba(255, 255, 255, 0.18);
        }

        .capture-feature__bar {
            height: 100%;
            width: 0;
            background: linear-gradient(90deg, #22c55e, #06b6d4, #f97316);
            transition: width 0.35s ease;
        }

        .capture-feature__pulse {
            display: none;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 7px;
        }

        .capture-feature__pulse span {
            display: block;
            min-height: 6px;
            background: #dbeafe;
        }

        .capture-feature__pulse span:nth-child(1) {
            background: #67e8f9;
        }

        .capture-feature__pulse span:nth-child(2) {
            background: #86efac;
        }

        .capture-feature__pulse span:nth-child(3) {
            background: #fdba74;
        }

        .quick-status + .grid {
            margin-top: 18px;
        }

        .grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
            gap: 18px;
            margin-top: 28px;
        }

        .grid--after-divider {
            margin-top: 18px;
        }

        .section-divider {
            display: flex;
            align-items: center;
            gap: 14px;
            flex-wrap: wrap;
            margin: 34px 0 0;
            color: #374151;
            font-size: 13px;
            font-weight: 900;
            letter-spacing: 0;
        }

        .section-divider__title {
            white-space: nowrap;
        }

        .section-divider__badges {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            flex-wrap: wrap;
        }

        .section-divider__note {
            flex-basis: 100%;
            margin-top: -4px;
            color: #64748b;
            font-size: 12px;
            font-weight: 850;
            line-height: 1.55;
            word-break: keep-all;
        }

        .section-divider__badge {
            display: inline-flex;
            align-items: center;
            min-height: 24px;
            padding: 4px 9px;
            border: 1px solid #d1d5db;
            border-radius: 999px;
            background: #ffffff;
            color: #111827;
            font-size: 12px;
            font-weight: 900;
            line-height: 1;
            box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06);
            white-space: nowrap;
        }

        .section-divider__badge--public {
            border-color: #f59e0b;
            background: #fffbeb;
            color: #92400e;
            box-shadow: 0 10px 22px rgba(245, 158, 11, 0.16);
        }

        .section-divider--public-relay {
            display: grid;
            grid-template-columns: auto minmax(0, 1fr);
            align-items: center;
            gap: 8px 12px;
            padding: 14px 16px;
            border: 1px solid #fde68a;
            background: linear-gradient(135deg, #fffbeb 0%, #ffffff 58%, #eff6ff 100%);
            box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05);
        }

        .section-divider--public-relay::before,
        .section-divider--public-relay::after {
            display: none;
        }

        .section-divider--public-relay .section-divider__title {
            grid-column: 1;
        }

        .section-divider--public-relay .section-divider__badges {
            grid-column: 2;
            min-width: 0;
        }

        .section-divider--public-relay .section-divider__note {
            grid-column: 1 / -1;
            margin-top: 0;
        }

        .section-divider::before,
        .section-divider::after {
            content: "";
            flex: 1;
            height: 1px;
            background: #d1d5db;
        }

        .card {
            display: flex;
            flex-direction: column;
            gap: 14px;
            min-height: 220px;
            padding: 24px;
            border: 1px solid var(--line);
            border-radius: 24px;
            background: var(--surface);
            color: inherit;
            text-decoration: none;
            box-shadow: var(--shadow);
            transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
        }

        .card:hover {
            transform: translateY(-4px);
            border-color: #cfcfcf;
            box-shadow: 0 16px 36px rgba(17, 17, 17, 0.08);
        }

        .card.is-offline {
            filter: grayscale(1);
            opacity: 0.68;
            background: #f3f4f6 !important;
            border-color: #d1d5db !important;
            box-shadow: none !important;
        }

        .card.is-offline:hover {
            transform: none;
            box-shadow: none !important;
        }

        .card.is-offline .card__label {
            background: #4b5563 !important;
            color: #f9fafb !important;
        }

        .card.is-offline .card__name,
        .card.is-offline .card__description,
        .card.is-offline .card__meta,
        .card.is-offline .card__address {
            color: #4b5563 !important;
        }

        .card__visual {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%;
            min-height: 118px;
            aspect-ratio: 16 / 9;
            margin: -2px 0 0;
            padding: 8px;
            border: 1px solid rgba(148, 163, 184, 0.28);
            background:
                linear-gradient(135deg, rgba(255, 255, 255, 0.88), rgba(241, 245, 249, 0.72));
            overflow: hidden;
        }

        .card__visual img {
            display: block;
            width: 100%;
            height: 100%;
            object-fit: contain;
        }

        .quick-status .card__visual {
            min-height: 102px;
        }

        .mobility-engine-card .card__visual,
        .technical-engine-card .card__visual {
            max-width: 280px;
            min-height: 112px;
            margin: 2px 0 8px;
            border-color: rgba(15, 23, 42, 0.08);
            background: rgba(255, 255, 255, 0.68);
        }

        .technical-engine-card .card__visual {
            max-width: none;
        }

        .technical-engine-card--editor .card__visual {
            border-color: rgba(204, 251, 241, 0.42);
            background:
                linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(226, 252, 247, 0.82));
            box-shadow:
                inset 0 0 0 1px rgba(15, 118, 110, 0.08),
                0 12px 28px rgba(8, 47, 73, 0.18);
        }

        .device-overview {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 14px;
            margin-top: 18px;
        }

        .device-card {
            display: flex;
            flex-direction: column;
            gap: 13px;
            min-height: 312px;
            padding: 20px;
            border: 1px solid var(--line);
            border-radius: 0;
            background: #ffffff;
            box-shadow: 0 10px 26px rgba(15, 23, 42, 0.04);
        }

        .device-card--pc {
            background: #f8fafc;
            border-color: #cbd5e1;
        }

        .device-card--wide3 {
            background: #eef6ff;
            border-color: #b7d8ff;
        }

        .device-card--lg {
            background: #effbf5;
            border-color: #a9e5c2;
        }

        .device-card__head {
            display: grid;
            gap: 8px;
        }

        .device-card__type {
            display: inline-flex;
            align-self: flex-start;
            padding: 7px 10px;
            background: #111827;
            color: #f9fafb;
            font-size: 12px;
            font-weight: 900;
            line-height: 1;
        }

        .device-card--wide3 .device-card__type {
            background: #1e3a8a;
        }

        .device-card--lg .device-card__type {
            background: #065f46;
        }

        .device-card__name {
            margin: 0;
            font-size: 23px;
            line-height: 1.18;
            letter-spacing: 0;
            color: #111827;
            word-break: keep-all;
        }

        .device-card__role {
            margin: 0;
            color: #475569;
            font-size: 14px;
            font-weight: 800;
            line-height: 1.55;
        }

        .device-card__visual {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%;
            aspect-ratio: 16 / 9;
            min-height: 124px;
            margin: 2px 0 0;
            padding: 10px;
            border: 1px solid rgba(148, 163, 184, 0.3);
            background: rgba(255, 255, 255, 0.58);
            overflow: hidden;
        }

        .device-card__visual img {
            display: block;
            width: 100%;
            height: 100%;
            object-fit: contain;
        }

        .device-card--pc .device-card__visual {
            background: linear-gradient(135deg, rgba(226, 232, 240, 0.95), rgba(248, 250, 252, 0.84));
        }

        .device-card--wide3 .device-card__visual {
            background: linear-gradient(135deg, rgba(219, 234, 254, 0.9), rgba(255, 255, 255, 0.78));
        }

        .device-card--lg .device-card__visual {
            background: linear-gradient(135deg, rgba(220, 252, 231, 0.9), rgba(255, 255, 255, 0.76));
        }

        .device-card__specs {
            display: grid;
            gap: 7px;
            margin: 0;
        }

        .device-card__spec {
            display: grid;
            grid-template-columns: 92px minmax(0, 1fr);
            gap: 9px;
            padding: 9px 10px;
            background: rgba(255, 255, 255, 0.72);
            color: #334155;
            font-size: 13px;
            line-height: 1.45;
        }

        .device-card__spec dt {
            color: #64748b;
            font-weight: 900;
        }

        .device-card__spec dd {
            margin: 0;
            font-weight: 800;
            word-break: break-word;
        }

        .device-card__spec dd a {
            color: #1d4ed8;
            text-decoration: underline;
            text-underline-offset: 3px;
        }

        .device-card__links {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-top: auto;
        }

        .device-card__links--panel {
            display: grid;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 10px;
            width: 100%;
        }

        .device-card__link {
            display: inline-flex;
            align-items: center;
            min-height: 34px;
            padding: 0 11px;
            border: 0;
            background: #ffffff;
            color: #0f172a;
            font: inherit;
            font-size: 13px;
            font-weight: 900;
            line-height: 1.25;
            text-decoration: none;
            cursor: pointer;
        }

        .device-card__link:hover {
            background: #f8fafc;
        }

        .device-card__link-panel {
            display: grid;
            gap: 6px;
            min-height: 92px;
            padding: 12px 13px;
            border: 1px solid #d8e3e1;
            border-radius: 14px;
            background: linear-gradient(135deg, #ffffff 0%, #f8fffd 100%);
            color: #0f172a;
            text-decoration: none;
            box-shadow: 0 12px 26px rgba(15, 23, 42, 0.06);
            transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
        }

        .device-card__link-panel:hover {
            transform: translateY(-2px);
            border-color: #99f6e4;
            box-shadow: 0 16px 32px rgba(15, 118, 110, 0.12);
        }

        .device-card__link-kicker {
            color: #0f766e;
            font-size: 11px;
            font-weight: 900;
            letter-spacing: 0.08em;
            text-transform: uppercase;
        }

        .device-card__link-title {
            color: #0f172a;
            font-size: 14px;
            font-weight: 900;
            line-height: 1.35;
        }

        .device-card__link-copy {
            color: #475569;
            font-size: 12px;
            line-height: 1.5;
        }

        .card--usb {
            background:
                linear-gradient(135deg, rgba(14, 165, 233, 0.14), rgba(99, 102, 241, 0.12)),
                #eef6ff;
            border-color: #b7d8ff;
            box-shadow: 0 16px 40px rgba(37, 99, 235, 0.12);
        }

        .card--usb:hover {
            border-color: #7cb8ff;
            box-shadow: 0 20px 44px rgba(37, 99, 235, 0.18);
        }

        .card--usb .card__label {
            background: rgba(15, 23, 42, 0.9);
            color: #f8fafc;
        }

        .card--usb .card__name {
            color: #0f172a;
        }

        .card--usb .card__description {
            color: #1e3a5f;
        }

        .card--usb .card__meta {
            color: #335c85;
        }

        .card--gd-c100 {
            background:
                linear-gradient(135deg, rgba(14, 165, 233, 0.16), rgba(20, 184, 166, 0.13), rgba(250, 204, 21, 0.12)),
                #f0fdfa;
            border-color: #7dd3fc;
            box-shadow: 0 16px 40px rgba(8, 145, 178, 0.14);
        }

        .card--gd-c100:hover {
            border-color: #22d3ee;
            box-shadow: 0 20px 44px rgba(8, 145, 178, 0.2);
        }

        .card--gd-c100 .card__label {
            background: rgba(8, 47, 73, 0.95);
            color: #ecfeff;
        }

        .card--gd-c100 .card__name {
            color: #083344;
        }

        .card--gd-c100 .card__description {
            color: #155e75;
        }

        .card--gd-c100 .card__meta {
            color: #0f766e;
        }

        .card--gd-c100 .card__visual {
            border-color: rgba(34, 211, 238, 0.32);
            background:
                radial-gradient(circle at 18% 20%, rgba(250, 204, 21, 0.2), transparent 30%),
                linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(224, 242, 254, 0.72));
        }

        .card--youtube-webcam-live {
            background:
                linear-gradient(135deg, rgba(220, 38, 38, 0.14), rgba(109, 40, 217, 0.13), rgba(8, 145, 178, 0.12)),
                #fff7f7;
            border-color: #fecaca;
            box-shadow: 0 16px 40px rgba(127, 29, 29, 0.13);
        }

        .card--youtube-webcam-live:hover {
            border-color: #f87171;
            box-shadow: 0 20px 44px rgba(127, 29, 29, 0.2);
        }

        .card--youtube-webcam-live .card__label {
            background: #b91c1c;
            color: #fff7ed;
        }

        .card--youtube-webcam-live .card__name {
            color: #7f1d1d;
        }

        .card--youtube-webcam-live .card__description {
            color: #4c1d95;
        }

        .card--youtube-webcam-live .card__meta {
            color: #be123c;
        }

        .card--youtube-webcam-live .card__visual {
            border-color: rgba(248, 113, 113, 0.34);
            background:
                linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(245, 243, 255, 0.78), rgba(236, 254, 255, 0.72));
        }

        .card--codex-workspace {
            background:
                linear-gradient(135deg, rgba(15, 23, 42, 0.92), rgba(14, 116, 144, 0.84), rgba(180, 83, 9, 0.24)),
                #0f172a;
            border-color: #67e8f9;
            box-shadow: 0 16px 40px rgba(15, 23, 42, 0.18);
        }

        .card--codex-workspace:hover {
            border-color: #a7f3d0;
            box-shadow: 0 20px 44px rgba(15, 23, 42, 0.24);
        }

        .card--codex-workspace .card__label {
            background: rgba(236, 254, 255, 0.94);
            color: #0f172a;
        }

        .card--codex-workspace .card__name {
            color: #f8fafc;
        }

        .card--codex-workspace .card__description {
            color: #dbeafe;
        }

        .card--codex-workspace .card__meta {
            color: #a7f3d0;
        }

        .card--codex-workspace .card__visual {
            border-color: rgba(103, 232, 249, 0.3);
            background: rgba(15, 23, 42, 0.28);
        }

        .card--wide3-capture {
            background:
                linear-gradient(135deg, rgba(6, 182, 212, 0.18), rgba(132, 204, 22, 0.14)),
                #f0fdfa;
            border-color: #99f6e4;
            box-shadow: 0 16px 40px rgba(13, 148, 136, 0.13);
        }

        .card--wide3-capture:hover {
            border-color: #5eead4;
            box-shadow: 0 20px 44px rgba(13, 148, 136, 0.19);
        }

        .card--wide3-capture .card__label {
            background: rgba(15, 118, 110, 0.95);
            color: #ecfeff;
        }

        .card--wide3-capture .card__name {
            color: #134e4a;
        }

        .card--wide3-capture .card__description {
            color: #115e59;
        }

        .card--wide3-capture .card__meta {
            color: #0f766e;
        }

        .card--samsung-vscode {
            background:
                linear-gradient(135deg, rgba(37, 99, 235, 0.15), rgba(14, 116, 144, 0.12)),
                #eff6ff;
            border-color: #bfdbfe;
            box-shadow: 0 16px 40px rgba(37, 99, 235, 0.12);
        }

        .card--samsung-vscode:hover {
            border-color: #93c5fd;
            box-shadow: 0 20px 44px rgba(37, 99, 235, 0.18);
        }

        .card--samsung-vscode .card__label {
            background: rgba(30, 64, 175, 0.95);
            color: #eff6ff;
        }

        .card--samsung-vscode .card__name {
            color: #1e3a8a;
        }

        .card--samsung-vscode .card__description {
            color: #1e3a8a;
        }

        .card--samsung-vscode .card__meta {
            color: #0e7490;
        }

        .card--samsung-voice {
            background:
                linear-gradient(135deg, rgba(14, 165, 233, 0.14), rgba(99, 102, 241, 0.12)),
                #f0f9ff;
            border-color: #bae6fd;
            box-shadow: 0 16px 40px rgba(2, 132, 199, 0.12);
        }

        .card--samsung-voice:hover {
            border-color: #7dd3fc;
            box-shadow: 0 20px 44px rgba(2, 132, 199, 0.18);
        }

        .card--samsung-voice .card__label {
            background: rgba(3, 105, 161, 0.95);
            color: #f0f9ff;
        }

        .card--samsung-voice .card__name,
        .card--samsung-voice .card__description {
            color: #075985;
        }

        .card--samsung-voice .card__meta {
            color: #2563eb;
        }

        .card--smart-dev-empty {
            justify-content: center;
            border-style: dashed;
            background:
                linear-gradient(135deg, rgba(148, 163, 184, 0.12), rgba(226, 232, 240, 0.3)),
                #f8fafc;
            border-color: #cbd5e1;
            box-shadow: none;
        }

        .card--smart-dev-empty:hover {
            transform: none;
            border-color: #94a3b8;
            box-shadow: none;
        }

        .card--smart-dev-empty .card__label {
            background: #e2e8f0;
            color: #475569;
        }

        .card--smart-dev-empty .card__name {
            color: #475569;
        }

        .card--smart-dev-empty .card__description,
        .card--smart-dev-empty .card__meta {
            color: #64748b;
        }

        .card--photo-article {
            background:
                linear-gradient(135deg, rgba(251, 146, 60, 0.17), rgba(71, 85, 105, 0.1)),
                #fff7ed;
            border-color: #fed7aa;
            box-shadow: 0 16px 40px rgba(194, 65, 12, 0.11);
        }

        .card--photo-article:hover {
            border-color: #fdba74;
            box-shadow: 0 20px 44px rgba(194, 65, 12, 0.16);
        }

        .card--photo-article .card__label {
            background: rgba(154, 52, 18, 0.94);
            color: #fff7ed;
        }

        .card--photo-article .card__name {
            color: #7c2d12;
        }

        .card--photo-article .card__description {
            color: #9a3412;
        }

        .card--photo-article .card__meta {
            color: #c2410c;
        }

        .card--photo-journal {
            background:
                linear-gradient(135deg, rgba(249, 115, 22, 0.14), rgba(15, 118, 110, 0.12)),
                #fff7ed;
            border-color: #fdba74;
            box-shadow: 0 16px 40px rgba(194, 65, 12, 0.11);
        }

        .card--photo-journal:hover {
            border-color: #fb923c;
            box-shadow: 0 20px 44px rgba(194, 65, 12, 0.16);
        }

        .card--photo-journal .card__label {
            background: rgba(15, 118, 110, 0.94);
            color: #ecfdf5;
        }

        .card--photo-journal .card__name {
            color: #7c2d12;
        }

        .card--photo-journal .card__description {
            color: #9a3412;
        }

        .card--photo-journal .card__meta {
            color: #0f766e;
        }

        .card--file-processing {
            background:
                linear-gradient(135deg, rgba(14, 116, 144, 0.16), rgba(15, 23, 42, 0.08)),
                #ecfeff;
            border-color: #a5f3fc;
            box-shadow: 0 16px 40px rgba(14, 116, 144, 0.12);
        }

        .card--file-processing:hover {
            border-color: #67e8f9;
            box-shadow: 0 20px 44px rgba(14, 116, 144, 0.18);
        }

        .card--file-processing .card__label {
            background: rgba(14, 116, 144, 0.95);
            color: #ecfeff;
        }

        .card--file-processing .card__name {
            color: #164e63;
        }

        .card--file-processing .card__description {
            color: #155e75;
        }

        .card--file-processing .card__meta {
            color: #0e7490;
        }

        .card--future-net-rover {
            background:
                linear-gradient(135deg, rgba(8, 47, 73, 0.95), rgba(15, 23, 42, 0.96)),
                radial-gradient(circle at 20% 14%, rgba(34, 211, 238, 0.22), transparent 36%),
                #07111f;
            border-color: #164e63;
            box-shadow: 0 16px 42px rgba(8, 47, 73, 0.22);
        }

        .card--future-net-rover:hover {
            border-color: #22d3ee;
            box-shadow: 0 22px 52px rgba(8, 145, 178, 0.24);
        }

        .card--future-net-rover .card__label {
            background: rgba(34, 211, 238, 0.94);
            color: #06202b;
        }

        .card--future-net-rover .card__name {
            color: #ecfeff;
        }

        .card--future-net-rover .card__description {
            color: #bae6fd;
        }

        .card--future-net-rover .card__meta {
            color: #67e8f9;
        }

        .card--ai-netphone {
            background:
                linear-gradient(135deg, rgba(37, 99, 235, 0.16), rgba(20, 184, 166, 0.14)),
                #eff6ff;
            border-color: #bfdbfe;
            box-shadow: 0 16px 40px rgba(37, 99, 235, 0.13);
        }

        .card--ai-netphone:hover {
            border-color: #60a5fa;
            box-shadow: 0 20px 44px rgba(37, 99, 235, 0.19);
        }

        .card--ai-netphone .card__label {
            background: rgba(37, 99, 235, 0.95);
            color: #eff6ff;
        }

        .card--ai-netphone .card__name {
            color: #1e3a8a;
        }

        .card--ai-netphone .card__description {
            color: #1d4ed8;
        }

        .card--ai-netphone .card__meta {
            color: #0f766e;
        }

        .card--architect-websitelist {
            background:
                linear-gradient(135deg, rgba(0, 87, 255, 0.12), rgba(15, 118, 110, 0.12)),
                #f6fbff;
            border-color: #b9d8ff;
            box-shadow: 0 16px 40px rgba(0, 87, 255, 0.11);
        }

        .card--architect-websitelist:hover {
            border-color: #72a8ff;
            box-shadow: 0 20px 44px rgba(0, 87, 255, 0.17);
        }

        .card--architect-websitelist .card__label {
            background: rgba(0, 87, 255, 0.95);
            color: #f8fbff;
        }

        .card--architect-websitelist .card__name {
            color: #123a78;
        }

        .card--architect-websitelist .card__description {
            color: #164e63;
        }

        .card--architect-websitelist .card__meta {
            color: #0f766e;
        }

        .card--architect-websitelist .card__visual {
            padding: 0;
            background: #ffffff;
            border-color: rgba(0, 87, 255, 0.14);
        }

        .card--architect-websitelist .card__visual img {
            object-fit: cover;
            object-position: top center;
        }

        .card--mail-server {
            background:
                linear-gradient(135deg, rgba(15, 23, 42, 0.12), rgba(11, 111, 104, 0.14)),
                #f8fafc;
            border-color: #cbd5e1;
            box-shadow: 0 16px 40px rgba(15, 23, 42, 0.11);
        }

        .card--mail-server:hover {
            border-color: #94a3b8;
            box-shadow: 0 20px 44px rgba(15, 23, 42, 0.17);
        }

        .card--mail-server .card__label {
            background: rgba(15, 23, 42, 0.95);
            color: #f8fafc;
        }

        .card--mail-server .card__name {
            color: #0f172a;
        }

        .card--mail-server .card__description {
            color: #334155;
        }

        .card--mail-server .card__meta {
            color: #0f766e;
        }

        .card--termux {
            background:
                linear-gradient(135deg, rgba(20, 184, 166, 0.16), rgba(34, 197, 94, 0.12)),
                #effbf5;
            border-color: #a9e5c2;
            box-shadow: 0 16px 40px rgba(22, 163, 74, 0.12);
        }

        .card--termux:hover {
            border-color: #71d49e;
            box-shadow: 0 20px 44px rgba(22, 163, 74, 0.18);
        }

        .card--termux .card__label {
            background: rgba(6, 95, 70, 0.94);
            color: #ecfdf5;
        }

        .card--termux .card__name {
            color: #064e3b;
        }

        .card--termux .card__description {
            color: #166534;
        }

        .card--termux .card__meta {
            color: #15803d;
        }

        .card--nginx {
            background:
                linear-gradient(135deg, rgba(14, 165, 233, 0.14), rgba(99, 102, 241, 0.12)),
                #eef6ff;
            border-color: #b9d8ff;
            box-shadow: 0 16px 40px rgba(29, 78, 216, 0.12);
        }

        .card--nginx:hover {
            border-color: #82b8ff;
            box-shadow: 0 20px 44px rgba(29, 78, 216, 0.18);
        }

        .card--nginx .card__label {
            background: rgba(30, 64, 175, 0.94);
            color: #eff6ff;
        }

        .card--nginx .card__name {
            color: #1e3a8a;
        }

        .card--nginx .card__description {
            color: #1e40af;
        }

        .card--nginx .card__meta,
        .card--nginx .card__address,
        .card--nginx .card__link {
            color: #1d4ed8;
        }

        .card--relay195 {
            background:
                linear-gradient(135deg, rgba(45, 212, 191, 0.16), rgba(59, 130, 246, 0.12)),
                #f0fbff;
            border-color: #a7e4ef;
            box-shadow: 0 16px 40px rgba(14, 116, 144, 0.12);
        }

        .card--relay195:hover {
            border-color: #67d5e7;
            box-shadow: 0 20px 44px rgba(14, 116, 144, 0.18);
        }

        .card--relay195 .card__label {
            background: rgba(21, 94, 117, 0.94);
            color: #ecfeff;
        }

        .card--relay195 .card__name {
            color: #164e63;
        }

        .card--relay195 .card__description {
            color: #155e75;
        }

        .card--relay195 .card__meta {
            color: #0e7490;
        }

        .card--directlan {
            background:
                linear-gradient(135deg, rgba(37, 99, 235, 0.15), rgba(14, 165, 233, 0.12)),
                #f0f7ff;
            border-color: #abd5ff;
            box-shadow: 0 16px 40px rgba(37, 99, 235, 0.12);
        }

        .card--directlan:hover {
            border-color: #78bbff;
            box-shadow: 0 20px 44px rgba(37, 99, 235, 0.18);
        }

        .card--directlan .card__label {
            background: rgba(30, 64, 175, 0.94);
            color: #eff6ff;
        }

        .card--directlan .card__name {
            color: #1e3a8a;
        }

        .card--directlan .card__description {
            color: #1e40af;
        }

        .card--directlan .card__meta {
            color: #1d4ed8;
        }

        .card--monitor {
            background:
                linear-gradient(135deg, rgba(20, 184, 166, 0.24), rgba(56, 189, 248, 0.2)),
                #e9fbff;
            border-color: #8edbeb;
            box-shadow: 0 16px 40px rgba(8, 145, 178, 0.14);
        }

        .card--monitor:hover {
            border-color: #45c6de;
            box-shadow: 0 20px 44px rgba(8, 145, 178, 0.2);
        }

        .card--monitor .card__label {
            background: rgba(15, 118, 110, 0.94);
            color: #ecfeff;
        }

        .card--monitor .card__name {
            color: #134e4a;
        }

        .card--monitor .card__description {
            color: #155e75;
        }

        .card--monitor .card__meta {
            color: #0e7490;
        }

        .card--phone-display {
            background:
                linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(245, 158, 11, 0.14)),
                #f0fdf4;
            border-color: #9addb8;
            box-shadow: 0 16px 40px rgba(22, 101, 52, 0.12);
        }

        .card--phone-display:hover {
            border-color: #62c58f;
            box-shadow: 0 20px 44px rgba(22, 101, 52, 0.18);
        }

        .card--phone-display .card__label {
            background: rgba(21, 128, 61, 0.94);
            color: #f0fdf4;
        }

        .card--phone-display .card__name {
            color: #14532d;
        }

        .card--phone-display .card__description {
            color: #166534;
        }

        .card--phone-display .card__meta {
            color: #15803d;
        }

        .card--lg-ubuntu {
            background:
                linear-gradient(135deg, rgba(99, 102, 241, 0.14), rgba(20, 184, 166, 0.16)),
                #f5fbff;
            border-color: #a8d8e8;
            box-shadow: 0 16px 40px rgba(14, 116, 144, 0.12);
        }

        .card--lg-ubuntu:hover {
            border-color: #6fc3d9;
            box-shadow: 0 20px 44px rgba(14, 116, 144, 0.18);
        }

        .card--lg-ubuntu .card__label {
            background: rgba(49, 46, 129, 0.94);
            color: #eef2ff;
        }

        .card--lg-ubuntu .card__name {
            color: #1e1b4b;
        }

        .card--lg-ubuntu .card__description {
            color: #155e75;
        }

        .card--lg-ubuntu .card__meta {
            color: #0f766e;
        }

        .card--g5-site {
            background:
                linear-gradient(135deg, rgba(14, 165, 233, 0.14), rgba(245, 158, 11, 0.1)),
                #f8fbff;
            border-color: #bfd7f5;
            box-shadow: 0 16px 40px rgba(37, 99, 235, 0.1);
        }

        .card--g5-site:hover {
            border-color: #93c5fd;
            box-shadow: 0 20px 44px rgba(37, 99, 235, 0.16);
        }

        .card--g5-site .card__label {
            background: rgba(15, 23, 42, 0.92);
            color: #f8fafc;
        }

        .card--g5-site .card__name {
            color: #1e3a8a;
        }

        .card--g5-site .card__description {
            color: #334155;
        }

        .card--g5-site .card__meta {
            color: #2563eb;
        }

        .card--g5-site .card__visual,
        .card--g5-site01 .card__visual,
        .card--g5-site02 .card__visual {
            padding: 0;
            border-color: rgba(15, 23, 42, 0.12);
            background: #ffffff;
        }

        .card--g5-site .card__visual img,
        .card--g5-site01 .card__visual img,
        .card--g5-site02 .card__visual img {
            object-fit: cover;
            object-position: top center;
        }

        .card--g5-site01 {
            background:
                linear-gradient(135deg, rgba(59, 130, 246, 0.18), rgba(14, 165, 233, 0.14)),
                #eef7ff;
            border-color: #a9d7ff;
            box-shadow: 0 16px 40px rgba(37, 99, 235, 0.13);
        }

        .card--g5-site01:hover {
            border-color: #6cb7ff;
            box-shadow: 0 20px 44px rgba(37, 99, 235, 0.19);
        }

        .card--g5-site01 .card__label {
            background: rgba(30, 64, 175, 0.94);
            color: #eff6ff;
        }

        .card--g5-site01 .card__name {
            color: #1e3a8a;
        }

        .card--g5-site01 .card__description {
            color: #1e40af;
        }

        .card--g5-site01 .card__meta {
            color: #1d4ed8;
        }

        .card--g5-site02 {
            background:
                linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.14)),
                #f0fdf4;
            border-color: #86efac;
            box-shadow: 0 16px 40px rgba(22, 163, 74, 0.13);
        }

        .card--g5-site02:hover {
            border-color: #4ade80;
            box-shadow: 0 20px 44px rgba(22, 163, 74, 0.19);
        }

        .card--g5-site02 .card__label {
            background: rgba(21, 128, 61, 0.94);
            color: #ecfdf5;
        }

        .card--g5-site02 .card__name {
            color: #14532d;
        }

        .card--g5-site02 .card__description {
            color: #166534;
        }

        .card--g5-site02 .card__meta {
            color: #15803d;
        }

        .card--g7 {
            background:
                linear-gradient(135deg, rgba(245, 158, 11, 0.14), rgba(249, 115, 22, 0.12)),
                #fff5e8;
            border-color: #f6c58c;
            box-shadow: 0 16px 40px rgba(234, 88, 12, 0.12);
        }

        .card--g7:hover {
            border-color: #f0a95a;
            box-shadow: 0 20px 44px rgba(234, 88, 12, 0.18);
        }

        .card--g7 .card__label {
            background: rgba(120, 53, 15, 0.92);
            color: #fff7ed;
        }

        .card--g7 .card__name {
            color: #7c2d12;
        }

        .card--g7 .card__description {
            color: #9a3412;
        }

        .card--g7 .card__meta {
            color: #b45309;
        }

        .card--private-bbs {
            background:
                linear-gradient(135deg, rgba(15, 23, 42, 0.08), rgba(37, 99, 235, 0.1)),
                #f7f9fc;
            border-color: #b9c4d6;
            box-shadow: 0 16px 40px rgba(15, 23, 42, 0.1);
        }

        .card--private-bbs:hover {
            border-color: #8ea1bd;
            box-shadow: 0 20px 44px rgba(15, 23, 42, 0.16);
        }

        .card--private-bbs .card__label {
            background: #1d2737;
            color: #f8fafc;
        }

        .card--private-bbs .card__name {
            color: #111827;
        }

        .card--private-bbs .card__description {
            color: #334155;
        }

        .card--private-bbs .card__meta {
            color: #475569;
        }

        .card__label {
            display: inline-flex;
            align-self: flex-start;
            padding: 7px 12px;
            background: var(--brand-soft);
            font-size: 12px;
            font-weight: 700;
            color: var(--text);
        }

        .card__name {
            margin: 0;
            font-size: 28px;
            line-height: 1.08;
            letter-spacing: -0.04em;
            word-break: keep-all;
        }

        .card__description {
            margin: 0;
            flex: 1;
            font-size: 15px;
            line-height: 1.7;
            color: var(--muted);
        }

        .card__meta {
            font-size: 13px;
            color: var(--muted);
        }

        .empty {
            margin-top: 28px;
            padding: 28px;
            border: 1px dashed var(--line);
            border-radius: 24px;
            background: var(--surface-soft);
            color: var(--muted);
            font-size: 16px;
            line-height: 1.7;
        }

        .site-footer {
            display: grid;
            gap: 18px;
            margin-top: 24px;
            padding: 22px;
            border: 1px solid #dbeafe;
            background: rgba(255, 255, 255, .9);
            box-shadow: 0 16px 34px rgba(15, 23, 42, .08);
            color: #1f2937;
        }

        .site-counter {
            display: grid;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 10px;
        }

        .site-counter__item {
            display: grid;
            gap: 5px;
            padding: 12px;
            border: 1px solid var(--line);
            background: var(--surface-soft);
        }

        .site-counter__item span {
            color: var(--muted);
            font-size: 12px;
            font-weight: 950;
        }

        .site-counter__item strong {
            color: var(--brand);
            font-size: 24px;
            line-height: 1;
        }

        .business-info {
            display: grid;
            gap: 10px;
        }

        .business-info__line {
            display: flex;
            flex-wrap: wrap;
            gap: 8px 16px;
            margin: 0;
            color: #334155;
            font-size: 13px;
            font-weight: 850;
            line-height: 1.7;
        }

        .business-info__line strong {
            color: #0f172a;
            font-weight: 1000;
        }

        .site-copyright {
            margin: 0;
            color: #64748b;
            font-size: 12px;
            font-weight: 950;
            letter-spacing: .04em;
        }

        @media (max-width: 640px) {
            html {
                scroll-padding-top: 78px;
            }

            .top-category-bar__inner {
                width: min(100% - 20px, 1120px);
                align-items: stretch;
                flex-direction: column;
                gap: 0;
                padding: 8px 0;
            }

            .top-category-bar__home {
                align-self: flex-start;
                min-height: 34px;
            }

            .top-category-bar__links {
                width: 100%;
                padding: 8px 1px 2px;
            }

            .top-category-bar__link {
                min-height: 34px;
                padding: 0 10px;
                font-size: 12px;
            }

            .floating-tools {
                right: 12px;
                bottom: 12px;
                padding: 7px;
            }

            .floating-tools__button {
                width: 42px;
                height: 42px;
            }

            .shell {
                width: min(100% - 20px, 1120px);
                padding: 20px 0 36px;
            }

            .hero,
            .card,
            .empty {
                border-radius: 20px;
            }

            .hero {
                padding: 22px;
            }

            .phone-network-notice {
                grid-template-columns: 1fr;
                padding: 14px;
            }

            .phone-network-notice__route {
                align-items: flex-start;
                flex-direction: column;
            }

            .phone-network-notice__route code {
                width: 100%;
                overflow-wrap: anywhere;
            }

            .hero-top {
                grid-template-columns: 1fr;
            }

            .hero-video {
                width: 100%;
            }

            .hero-video__caption {
                align-items: flex-start;
                flex-direction: column;
            }

            .hero-camera-card {
                grid-template-columns: 1fr;
                padding: 14px;
            }

            .hero-camera-card__visual {
                order: -1;
            }

            .hero-camera-card__action {
                width: 100%;
                justify-content: center;
            }

            .messenger-login {
                grid-template-columns: 1fr;
                padding: 14px;
            }

            .messenger-login__title {
                font-size: 21px;
            }

            .messenger-login__form {
                grid-template-columns: 1fr;
            }

            .messenger-login__button,
            .messenger-login__link {
                width: 100%;
            }

            .quick-status {
                grid-template-columns: 1fr;
            }

            .device-overview {
                grid-template-columns: 1fr;
            }

            .capture-feature {
                grid-template-columns: 1fr;
            }

            .mobility-engine-grid {
                grid-template-columns: 1fr;
            }

            .technical-engine-grid {
                grid-template-columns: 1fr;
            }

            .section-divider--public-relay {
                grid-template-columns: 1fr;
            }

            .section-divider--public-relay .section-divider__title,
            .section-divider--public-relay .section-divider__badges,
            .section-divider--public-relay .section-divider__note {
                grid-column: 1;
            }

            .journal-engine__stats {
                grid-template-columns: repeat(2, minmax(0, 1fr));
            }

            .capture-feature-card {
                grid-template-columns: 1fr;
                padding: 20px;
                border-radius: 20px;
            }

            .mobility-engine-card {
                padding: 20px;
                border-radius: 0;
            }

            .photo-article-feature-card {
                padding: 20px;
                border-radius: 20px;
            }

            .capture-feature__stats {
                grid-template-columns: 1fr 1fr 1fr;
            }

            .capture-feature__pulse {
                grid-template-columns: 1fr 1fr 1fr;
            }

            .capture-feature__ops {
                grid-template-columns: 1fr 1fr 1fr;
            }

            .capture-feature__latest {
                grid-template-columns: 1fr;
            }

            .device-card {
                min-height: 0;
            }

            .device-card__spec {
                grid-template-columns: 1fr;
            }

            .device-card__links--panel {
                grid-template-columns: 1fr;
            }

            .pc-spec {
                padding: 74px 18px 18px;
            }

            .pc-env-button {
                top: 18px;
                left: 18px;
                right: auto;
            }

            .pc-env-modal__inner {
                padding: 18px;
            }

            .site-footer {
                padding: 18px;
            }

            .site-counter {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <nav class="top-category-bar" aria-label="분류별 메뉴">
        <div class="top-category-bar__inner">
            <a class="top-category-bar__home" href="#home" aria-label="홈으로 이동">
                <svg viewBox="0 0 24 24" aria-hidden="true">
                    <path d="M4 11.2 12 4l8 7.2V20h-5.2v-5.6H9.2V20H4z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
                </svg>
                <span>홈</span>
            </a>
            <div class="top-category-bar__links">
                <a class="top-category-bar__link" href="#devices">장비별 현황</a>
                <a class="top-category-bar__link" href="https://mobile.netcity.co.kr/settlement-environment/">이주 준비물</a>
                <a class="top-category-bar__link" href="#mobility">이동수단·엔진</a>
                <a class="top-category-bar__link" href="#technical">기술 엔진</a>
                <a class="top-category-bar__link" href="#gd-c100">GD-C100</a>
                <a class="top-category-bar__link" href="./youtube-webcam-live/">YouTube 웹캠</a>
                <a class="top-category-bar__link" href="#codex-live">CODEX 작업현장</a>
                <a class="top-category-bar__link" href="#smartphone-dev">스마트폰 개발</a>
                <a class="top-category-bar__link" href="#termux-wifi">Termux 와이파이</a>
                <a class="top-category-bar__link" href="#messenger-login">메신저 로그인</a>
                <a class="top-category-bar__link" href="#primary-links">주요 서버 생성</a>
                <a class="top-category-bar__link" href="#relay-links">중계서버 생성</a>
            </div>
        </div>
    </nav>
    <main class="shell">
        <section class="phone-network-notice" aria-label="스마트폰 서버 네트워크 운영 방식 안내">
            <div class="phone-network-notice__body">
                <span class="phone-network-notice__label">현재 운영 기준</span>
                <h2 class="phone-network-notice__title">두 스마트폰 서버는 지금 일반 Wi-Fi IP로 운영 중입니다.</h2>
                <p class="phone-network-notice__copy">
                    이 페이지에 남아 있는 USB 역테더링, tun0, ADB 포워딩, PC 연결 IP 내용은 예전에 PC에 연결된 역테더링으로 IP를 받아 운영했던 기록입니다.
                    현재 공개 페이지는 그대로 열리며, 갤럭시 Wide3와 LG-T540은 공유기에서 받은 Wi-Fi 주소를 이 197 서버가 도메인으로 연결합니다.
                    현재 응답 주소는 갤럭시 Wide3 <code>192.168.0.100</code>, LG-T540 <code>192.168.0.104</code>입니다.
                    예약 목표 주소는 갤럭시 Wide3 <code>192.168.0.194</code>, LG-T540 <code>192.168.0.195</code>이며, 예약 전에는 현재 Wi-Fi 주소로 연결합니다.
                    가정용 환경에서 여러 서버를 계속 운영하는 데에는 어려움이 따르며, 특히 전력 사용량이 가장 큰 문제입니다.
                    그래서 초저전력으로 오래 켜둘 수 있는 스마트폰 서버를 실험하고 있습니다.
                </p>
            </div>
            <div class="phone-network-notice__routes" aria-label="현재 스마트폰 서버 Wi-Fi 주소">
                <div class="phone-network-notice__route">
                    <span>Samsung Galaxy Wide3</span>
                    <code>192.168.0.100:8080/8082/8083</code>
                </div>
                <div class="phone-network-notice__route">
                    <span>LG-T540 Ubuntu 서버</span>
                    <code>192.168.0.104:8088</code>
                </div>
            </div>
        </section>
        <section class="hero" id="home">
            <div class="hero-top">
                <div class="hero-copy">
                    <p class="eyebrow">Netcity Entry</p>
                    <h1>mobile.netcity.co.kr 서버</h1>
                    <div class="server-subtitle" aria-label="Server address and role">
                        <span class="server-ip">(192.168.0.197)</span>
                        <span class="server-tag">우분투 22.04 중계서버-PC</span>
                    </div>
                    <p class="lead">
                        이 페이지는 서버 내부 링크와 연결 지점을 모아두는 시작 페이지입니다.
                        Wi-Fi 스마트폰 서버인 갤럭시 와이드3와 LG-T540 Ubuntu Touch 서버형은 장비별 현황에서 상태만 확인하도록 정리했고, 메인 링크 카드에서는 제외했습니다.
                        진행은 CODEX를 이용하였습니다.
                    </p>
                </div>
                <aside class="hero-video" aria-label="탐사차 스케일 참조 영상">
                    <div class="hero-video__frame">
                        <iframe
                            src="https://www.youtube.com/embed/r1UR2Z7mZ1E?controls=1&amp;modestbranding=1&amp;rel=0"
                            title="탐사차 스케일 참조 영상"
                            loading="lazy"
                            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
                            referrerpolicy="strict-origin-when-cross-origin"
                            allowfullscreen></iframe>
                    </div>
                    <div class="hero-video__caption">
                        <span>탐사차 스케일 참조 영상</span>
                        <a href="https://youtu.be/r1UR2Z7mZ1E" target="_blank" rel="noopener">YouTube</a>
                    </div>
                </aside>
            </div>
              <a class="hero-camera-card" id="gd-c100" href="https://netcity.co.kr/index.php" target="_blank" aria-label="postmarketOS
Phone Server">
                <span class="hero-camera-card__body">
                    <span class="hero-camera-card__label">Ubuntu 24.04 Desktop</span>
                    <strong class="hero-camera-card__title">Ubuntu Desktop
main Server</strong>
                    <span class="hero-camera-card__copy">우분투 24.04 테스크탑에 웹서버를 직접 운영중에 있습니다.</span>
                    <span class="hero-camera-card__action">공개 페이지 열기</span>
                </span>
                
                <figure class="hero-camera-card__visual">
                    <img src="/assets/355aafba-5fc0-4843-b50f-b3d154f6e363.jpeg" alt="GD-C100 Ubuntu 서버 웹캠 운영 이미지" loading="lazy" decoding="async">
                </figure>
            </a>       

            <a class="hero-camera-card" id="gd-c100" href="https://postmarketos.netcity.co.kr/" target="_blank" aria-label="postmarketOS
Phone Server">
                <span class="hero-camera-card__body">
                    <span class="hero-camera-card__label">postmarketOS</span>
                    <strong class="hero-camera-card__title">postmarketOS 웹페이지</strong>
                    <span class="hero-camera-card__copy">오래된 스마트폰에 postmarketOS를 올려 Nginx, PHP-FPM, MariaDB, OpenSSH가 직접 응답하는 저전력 웹서버로 운영합니다. Ubuntu PC는 공개 HTTPS 프록시를 맡고, 실제 페이지 생성은 Galaxy Wide3의 리눅스 서버가 처리합니다. 설치부터 운영까지 웹페이지 운영 게시</span>
                    <span class="hero-camera-card__action">공개 페이지 열기</span>
                </span>
                
                <figure class="hero-camera-card__visual">
                    <img src="/assets/3029c745-a8f3-4e84-aa21-1fbe0f1aa5a3.jpeg" alt="GD-C100 Ubuntu 서버 웹캠 운영 이미지" loading="lazy" decoding="async">
                </figure>
            </a>

            <a class="hero-camera-card" id="gd-c100" href="#" target="_blank" aria-label="postmarketOS
Phone Server">
                <span class="hero-camera-card__body">
                    <span class="hero-camera-card__label">Debian</span>
                    <strong class="hero-camera-card__title">Debian Web Server - 상시 운영 안함
</strong>
                    <span class="hero-camera-card__copy">오래된 노트북에 Debian 운영, 설치부터 운영까지 웹페이지 운영 게시</span>
                    <span class="hero-camera-card__action">공개 페이지 열기</span>
                </span>
                
                <figure class="hero-camera-card__visual">
                    <img src="/assets/b675c773-6096-41a0-8fb0-5bf527071138.jpeg" alt="GD-C100 Ubuntu 서버 웹캠 운영 이미지" loading="lazy" decoding="async">
                </figure>
            </a>

            <a class="hero-camera-card" id="gd-c100" href="https://netcity.co.kr/termux/index.php" target="_blank" aria-label="postmarketOS
Phone Server">
                <span class="hero-camera-card__body">
                    <span class="hero-camera-card__label">Termux</span>
                    <strong class="hero-camera-card__title">Termux Web Server - 전기세에 부담이 안가니 켜 놓기는 함 
</strong>
                    <span class="hero-camera-card__copy">LGT540 구 스마트폰 기기에 Termux를 통해 Nginx, PHP-FPM, MariaDB 등 설치 운영, 재부팅시 하나하나 실행을 해야 한다는 점이 불편함. 설치부터 운영까지 웹페이지 운영 게시</span>
                    <span class="hero-camera-card__action">공개 페이지 열기</span>
                </span>
                
                <figure class="hero-camera-card__visual">
                    <img src="/assets/1200x600wa.png" alt="GD-C100 Ubuntu 서버 웹캠 운영 이미지" loading="lazy" decoding="async">
                </figure>
            </a>     
            <img src="/assets/os.png">  
            postmarketOS: 알파인 리눅스 기반, 가장 광범위한 기기 지원 목표.

Ubuntu Touch: UBports 커뮤니티 주도, 가장 완성도 높은 사용자 경험 제공.

Sailfish OS: 제스처 기반 인터페이스, 안드로이드 앱 호환성 지원.

Mobian: 데비안 기반, 데스크탑 리눅스 환경과 가장 유사한 경험.

PureOS: 보안과 프라이버시 중심, Librem 5 공식 OS.

Manjaro ARM: 아치 리눅스 기반의 모바일 최적화 버전.

Fedora Mobility: 페도라 프로젝트의 모바일화 시도.

Mobile NixOS: 선언적 설정(Declarative configuration)이 가능한 리눅스.  

9. Phosh: GNOME 기반의 모바일 UI, 많은 리눅스 OS가 표준으로 채택.

10. Plasma Mobile: KDE 커뮤니티 개발, 고도의 자유도와 컨버전스 제공.

11. Sxmo: 미니멀리즘 중심의 모바일 UI, 텍스트 기반 제어 가능.

12. Lomiri: 과거 Unity8을 계승한 Ubuntu Touch의 핵심 UI.

13. LineageOS: 가장 유명한 커스텀 롬, 리눅스 커널 기반.

14. GrapheneOS: 보안 강화에 특화된 AOSP 기반 OS.

15. DivestOS: 프라이버시와 오래된 기기 지원에 집중한 프로젝트.

16. CalyxOS: 보안과 편의성을 동시에 고려한 OS.

17. iodéOS: 광고 차단 및 데이터 추적 방지 기능 포함.

18. Replicant: 완전한 자유 소프트웨어(FOSS)만을 고집하는 프로젝트.


            <section class="messenger-login" id="messenger-login" aria-label="Netcity Messenger login">
                <div class="messenger-login__copy">
                    <p class="messenger-login__label">Netcity Messenger</p>
                    <h2 class="messenger-login__title">서버 메신저 계정</h2>
                    <p class="messenger-login__text">
                        netcity-messenger 계정으로 로그인하면 이 메인 페이지는 그대로 유지되고,
                        현재 접속 계정과 메신저함 상태만 바로 확인할 수 있습니다.
                    </p>
                </div>
                <div class="messenger-login__panel">
                    <?php if ($portalMessengerFlash): ?>
                        <p class="messenger-login__flash<?= ($portalMessengerFlash['type'] ?? '') === 'warn' ? ' is-warn' : '' ?>"><?= h((string) ($portalMessengerFlash['message'] ?? '')) ?></p>
                    <?php endif; ?>
                    <?php if (!$portalMessengerReady): ?>
                        <p class="messenger-login__flash is-warn">메신저 설정을 읽을 수 없습니다.</p>
                    <?php elseif ($portalMessengerUser): ?>
                        <div class="messenger-login__status">
                            <div class="messenger-login__identity">
                                <span class="messenger-login__avatar" aria-hidden="true"><?= h(mb_substr((string) $portalMessengerUser['display_name'], 0, 1)) ?></span>
                                <div>
                                    <p class="messenger-login__name"><?= h((string) $portalMessengerUser['display_name']) ?></p>
                                    <p class="messenger-login__meta">
                                        @<?= h((string) $portalMessengerUser['username']) ?>
                                        · <?= h((string) $portalMessengerUser['role']) ?>
                                        · 안 읽은 메신저 <?= number_format($portalMessengerUnread) ?>개
                                    </p>
                                </div>
                            </div>
                            <div class="messenger-login__actions">
                                <a class="messenger-login__link" href="./netcity-messenger/">메신저함 열기</a>
                                <a class="messenger-login__link" href="./netcity-messenger/?action=compose">메시지 작성</a>
                                <form method="post" action="./#messenger-login">
                                    <input type="hidden" name="portal_messenger_csrf" value="<?= h($portalMessengerToken) ?>">
                                    <input type="hidden" name="portal_messenger_action" value="logout">
                                    <button class="messenger-login__button is-light" type="submit">로그아웃</button>
                                </form>
                            </div>
                        </div>
                    <?php else: ?>
                        <form class="messenger-login__form" method="post" action="./#messenger-login">
                            <input type="hidden" name="portal_messenger_csrf" value="<?= h($portalMessengerToken) ?>">
                            <input type="hidden" name="portal_messenger_action" value="login">
                            <input class="messenger-login__field" type="text" name="username" autocomplete="username" placeholder="아이디" required>
                            <input class="messenger-login__field" type="password" name="password" autocomplete="current-password" placeholder="비밀번호" required>
                            <button class="messenger-login__button" type="submit">로그인</button>
                        </form>
                        <div class="messenger-login__actions">
                            <a class="messenger-login__link" href="./netcity-messenger/?action=register">회원가입</a>
                            <a class="messenger-login__link" href="./netcity-messenger/">메신저함</a>
                        </div>
                    <?php endif; ?>
                </div>
            </section>
            <div class="tip pc-spec">
                <button class="pc-env-button" type="button" data-pc-env-open aria-haspopup="dialog" aria-controls="pc-env-modal">
                    <svg class="pc-env-button__icon" viewBox="0 0 24 24" aria-hidden="true">
                        <path d="M4 5.5h16v10H4z" fill="none" stroke="currentColor" stroke-width="2"/>
                        <path d="M9 19h6M12 15.5V19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square"/>
                    </svg>
                    <span>PC환경</span>
                </button>
                <p class="pc-spec__title">이 PC 운영 현황: Ubuntu 22.04.5 LTS 기반의 중계서버-PC로, 웹 프록시와 스마트폰 Wi-Fi 서버 연결을 함께 담당합니다.</p>
                <p class="pc-spec__mission">개인 웹진 운영은 전 인류 의무로서 무조건입니다. 그것은 그 벌을 피하기 위함입니다. 이곳은 그 환경을 알리기 위함입니다.</p>
                <p class="pc-spec__settlement">이주 환경은 자신이 만들어 가야 하는 환경입니다. 서버와 장비, 물자와 식량은 보급될 수 있지만 생활·운영·생산 체계 자체를 대신 만들어 주지는 않습니다. 탐사 현장에서 누가 대신 기지를 완성해 주지 않듯, 각자는 지급받은 기반을 연결하고 기록하고 운용하며 자기 환경을 구축해야 합니다.</p>
                <p class="pc-spec__audience">이 페이지는 전문 개발자만을 위한 안내가 아닙니다. 여성을 비롯한 일반 사용자, 기술 직군이 아닌 사람, 서버 운영을 처음 접하는 사람도 자신의 장비와 보급 자원을 기준으로 웹진, 연락 수신함, 기록 보관, 생활 운영 환경을 단계적으로 만들어 갈 수 있도록 돕는 것을 목표로 합니다.</p>
                <dialog class="pc-env-modal" id="pc-env-modal" aria-labelledby="pc-env-modal-title">
                    <div class="pc-env-modal__inner">
                        <div class="pc-env-modal__head">
                            <div>
                                <p class="pc-env-modal__label">PC Environment</p>
                                <h2 class="pc-env-modal__title" id="pc-env-modal-title">192.168.0.197 PC 환경</h2>
                                <p class="pc-env-modal__summary">Ubuntu 22.04.5 LTS 기반의 중계서버-PC이며, 웹 프록시와 스마트폰 Wi-Fi 서버 연결을 함께 담당합니다.</p>
                            </div>
                            <button class="pc-env-modal__close" type="button" data-pc-env-close aria-label="PC 환경 닫기">×</button>
                        </div>
                        <div class="pc-spec__grid">
                            <div class="pc-spec__item">
                                <span class="pc-spec__label">운영체제</span>
                                <span class="pc-spec__value">Ubuntu 22.04.5 LTS Jammy, Linux 5.15.0-177-generic, x86_64</span>
                            </div>
                            <div class="pc-spec__item">
                                <span class="pc-spec__label">장비 / CPU</span>
                                <span class="pc-spec__value">netcity-DeskTop-System, Desktop, Intel Celeron D 3.33GHz, 1 CPU / 1 Core / 1 Thread</span>
                            </div>
                            <div class="pc-spec__item">
                                <span class="pc-spec__label">메모리 / 스왑</span>
                                <span class="pc-spec__value">RAM 1.9GiB, 사용 394MiB, 가용 1.3GiB / Swap 2.0GiB, 사용 277MiB</span>
                            </div>
                            <div class="pc-spec__item">
                                <span class="pc-spec__label">디스크</span>
                                <span class="pc-spec__value">/dev/sda1 110G, 사용 18G, 여유 87G, 사용률 17%</span>
                            </div>
                            <div class="pc-spec__item">
                                <span class="pc-spec__label">네트워크 IP</span>
                                <span class="pc-spec__value">enp4s0: 192.168.0.199/24 / Galaxy Wide3 현재 Wi-Fi: 192.168.0.100, 예약 목표: 192.168.0.194 / LG-T540 현재 Wi-Fi: 192.168.0.104, 예약 목표: 192.168.0.195 / Docker: 172.17.0.1, 172.18.0.1</span>
                            </div>
                            <div class="pc-spec__item">
                                <span class="pc-spec__label">설치된 주요 프로그램</span>
                                <span class="pc-spec__value">Nginx 1.18.0, PHP CLI 8.1.2, MySQL Client 8.0.45, Docker 29.1.3, OpenSSH 8.9p1, ADB 1.0.41, Python 3.10.12</span>
                            </div>
                            <div class="pc-spec__item">
                                <span class="pc-spec__label">연결 안드로이드폰</span>
                                <span class="pc-spec__value">Samsung Galaxy Wide3 / SM-J737S: 현재 Wi-Fi 192.168.0.100, 예약 목표 192.168.0.194, Android 8080 경량 HTTP 응답, Termux PHP 8082, Termux nginx 8083. LG-T540 Ubuntu Touch 서버형: 현재 Wi-Fi 192.168.0.104, 예약 목표 192.168.0.195, Android 5.0.1 + Termux + proot Ubuntu 22.04.5 armhf, BusyBox HTTP 8088.</span>
                            </div>
                            <div class="pc-spec__item">
                                <span class="pc-spec__label">실행 중인 주요 서비스</span>
                                <span class="pc-spec__value">nginx, php-fpm, mariadbd, docker, sshd, cron, snapd, 스마트폰 Wi-Fi 프록시</span>
                            </div>
                            <div class="pc-spec__item">
                                <span class="pc-spec__label">사용 중인 포트</span>
                                <span class="pc-spec__value">443 웹, 22 SSH, Wide3 현재 Wi-Fi 192.168.0.100:8080/8082/8083, LG-T540 현재 Wi-Fi 192.168.0.104:8088 및 192.168.0.104:8022</span>
                            </div>
                        </div>
                    </div>
                </dialog>
            </div>
        </section>

        <div class="section-divider" id="devices" aria-label="Device inventory section">장비별 현황 (PC 사양 / 스마트폰 기종)</div>
        <section class="device-overview" aria-label="Device inventory">
            <article class="device-card device-card--pc">
                <div class="device-card__head">
                    <span class="device-card__type">Relay PC</span>
                    <h2 class="device-card__name">192.168.0.197 우분투 중계서버-PC</h2>
                    <figure class="device-card__visual">
                        <img src="/assets/device-pc-storage.svg" alt="192.168.0.197 우분투 중계서버 PC 저장장치 이미지" loading="lazy" decoding="async">
                    </figure>
                    <p class="device-card__role">웹 포털, nginx 프록시, 스마트폰 Wi-Fi 서버의 공개 고유 주소를 담당하는 기준 서버입니다.</p>
                </div>
                <dl class="device-card__specs">
                    <div class="device-card__spec"><dt>OS</dt><dd>Ubuntu 22.04.5 LTS / Linux 5.15.0-177-generic</dd></div>
                    <div class="device-card__spec"><dt>CPU</dt><dd>Intel Celeron D 3.33GHz / 1 Core</dd></div>
                    <div class="device-card__spec"><dt>RAM</dt><dd>약 1.9GiB / Swap 2.0GiB</dd></div>
                    <div class="device-card__spec"><dt>역할</dt><dd>443 웹, 22 SSH, 스마트폰 Wi-Fi 프록시, MariaDB</dd></div>
                    <div class="device-card__spec"><dt>연결폰</dt><dd>Wide3와 LG-T540 모두 공유기 Wi-Fi IP를 받은 스마트폰 서버 노드</dd></div>
                </dl>
                <div class="device-card__links">
                    <a class="device-card__link" href="/">메인 포털</a>
                    <a class="device-card__link" href="https://mobile.netcity.co.kr/photo-article-server/">사진 기사 서버</a>
                    <button class="device-card__link" type="button" data-pc-env-open>PC환경 보기</button>
                </div>
            </article>
            <article class="device-card device-card--wide3">
                <div class="device-card__head">
                    <span class="device-card__type">Android Phone 1</span>
                    <h2 class="device-card__name">Samsung Galaxy Wide3 / SM-J737S</h2>
                    <figure class="device-card__visual">
                        <img src="/assets/device-galaxy-wide3.svg" alt="Samsung Galaxy Wide3 SM-J737S 스마트폰 기종 이미지" loading="lazy" decoding="async">
                    </figure>
                    <p class="device-card__role">공유기 Wi-Fi에서 현재 IP 192.168.0.100을 받은 뒤 8080 경량 HTTP, 8082 Termux PHP, 8083 Termux nginx를 함께 운영하는 스마트폰 서버입니다. 예약 목표는 192.168.0.194입니다.</p>
                </div>
                <dl class="device-card__specs">
                    <div class="device-card__spec"><dt>Android</dt><dd>Android 10 / SDK 29</dd></div>
                    <div class="device-card__spec"><dt>Wi-Fi IP</dt><dd>현재 192.168.0.100 / 예약 목표 192.168.0.194</dd></div>
                    <div class="device-card__spec"><dt>연결</dt><dd>Wi-Fi 8080 경량 HTTP, 8082 Termux PHP, 8083 Termux nginx</dd></div>
                    <div class="device-card__spec"><dt>방식</dt><dd>Android 경량 HTTP 응답 / Termux Nginx·PHP / Termux code-server 웹 IDE</dd></div>
                    <div class="device-card__spec"><dt>고유주소</dt><dd>mobile.netcity.co.kr/usb-android-galaxy-wide3/</dd></div>
                    <div class="device-card__spec"><dt>개발서버</dt><dd>VS Code 서버는 위 스마트폰 개발 서버 카드에서 별도 관리</dd></div>
                    <div class="device-card__spec"><dt>G5</dt><dd>Termux Nginx 8083 아래 `g5-01` ~ `g5-10` 일반 그누보드5 별도 설치</dd></div>
                    <div class="device-card__spec"><dt>G5 SE</dt><dd>Termux Nginx 8083 아래 `g5-se01` ~ `g5-se10` 설치 완료</dd></div>
                </dl>
                <div class="device-card__links device-card__links--panel">
                    <a class="device-card__link-panel" href="https://mobile.netcity.co.kr/usb-android-galaxy-wide3/">
                        <span class="device-card__link-kicker">Wide3</span>
                        <span class="device-card__link-title">Wide3 홈</span>
                        <span class="device-card__link-copy">스마트폰 서버 첫 화면과 자체 구축 현황을 확인합니다.</span>
                    </a>
                    <a class="device-card__link-panel" href="/usb-android-galaxy-wide3/g5-hub/">
                        <span class="device-card__link-kicker">G5 Hub</span>
                        <span class="device-card__link-title">그누보드5 허브</span>
                        <span class="device-card__link-copy">SE와 분리해서 새로 만든 일반 그누보드5 10개 사이트를 엽니다.</span>
                    </a>
                    <a class="device-card__link-panel" href="/usb-android-galaxy-wide3/g5-se-hub/">
                        <span class="device-card__link-kicker">G5 SE Hub</span>
                        <span class="device-card__link-title">그누보드5 SE 허브</span>
                        <span class="device-card__link-copy">Wide3 안에 만든 10개 그누보드5 SE 사이트를 한 번에 엽니다.</span>
                    </a>
                    <a class="device-card__link-panel" href="/usb-android-galaxy-wide3/g5-se01/">
                        <span class="device-card__link-kicker">G5 SE 01</span>
                        <span class="device-card__link-title">첫 번째 사이트</span>
                        <span class="device-card__link-copy">분리된 데이터베이스로 구축한 첫 번째 그누보드 인스턴스입니다.</span>
                    </a>
                </div>
            </article>
            <article class="device-card device-card--lg">
                <div class="device-card__head">
                    <span class="device-card__type">Ubuntu Touch Type</span>
                    <h2 class="device-card__name">LG-T540 Ubuntu Touch 서버형</h2>
                    <figure class="device-card__visual">
                        <img src="/assets/device-lg-t540.svg" alt="LG-T540 Ubuntu Touch 서버형 기종 이미지" loading="lazy" decoding="async">
                    </figure>
                    <p class="device-card__role">저전력 스마트폰 서버 분류상 Ubuntu Touch 서버형입니다. 공유기 Wi-Fi에서 목표 IP 192.168.0.195를 받은 Android 5.0.1 위에 Termux와 proot Ubuntu 22.04.5 armhf를 올려 서버 전용 노드로 운용합니다.</p>
                </div>
                <dl class="device-card__specs">
                    <div class="device-card__spec"><dt>분류</dt><dd>저전력 Ubuntu Touch 서버형</dd></div>
                    <div class="device-card__spec"><dt>Android</dt><dd>Android 5.0.1 / SDK 21 / armeabi-v7a</dd></div>
                    <div class="device-card__spec"><dt>Ubuntu</dt><dd>Ubuntu 22.04.5 LTS armhf / proot</dd></div>
                    <div class="device-card__spec"><dt>Wi-Fi IP</dt><dd>목표 192.168.0.195 / 현재 fallback 192.168.0.104</dd></div>
                    <div class="device-card__spec"><dt>연결</dt><dd>Wi-Fi 8088 HTTP, Wi-Fi 8022 SSH 포트 열림</dd></div>
                    <div class="device-card__spec"><dt>고유주소</dt><dd>mobile.netcity.co.kr/lg-t540-ubuntu-server/</dd></div>
                </dl>
            </article>
        </section>

        <div class="section-divider" id="mobility" aria-label="Mobility and engine section">이동수단과 엔진</div>
        <section class="mobility-engine-grid" aria-label="Mobility and engine links">
            <article class="mobility-engine-card" aria-label="Future net mobility and engine link">
                <div class="mobility-engine__body">
                    <span class="mobility-engine__eyebrow">Mobility Engine Link</span>
                    <h2 class="mobility-engine__title">개인 데이터센터와 넷으로 연결되는 초고도사회</h2>
                    <figure class="card__visual"><img src="/assets/card-future-net.svg" alt="개인 데이터센터와 탐사차 기지 연결 이미지" loading="lazy" decoding="async"></figure>
                    <p class="mobility-engine__copy">
                        항공모함 2척급 탐사차 기지와 개인 데이터센터를 넷으로 연결하는 이동 거점 구상입니다.
                    </p>
                    <div class="mobility-engine__flow" aria-label="mobility engine flow">
                        <span class="mobility-engine__step">항공모함 2척급</span>
                        <span class="mobility-engine__step">개인 데이터센터</span>
                        <span class="mobility-engine__step">넷폰 관제</span>
                    </div>
                    <div class="mobility-engine__actions">
                        <a class="mobility-engine__button mobility-engine__button--primary" href="https://mobile.netcity.co.kr/future-net-datacenter-rover/">페이지 열기</a>
                        <a class="mobility-engine__button" href="https://mobile.netcity.co.kr/future-net-datacenter-rover/?page=reference">참조 영상</a>
                        <a class="mobility-engine__button" href="https://mobile.netcity.co.kr/future-net-datacenter-rover/board/">게시판</a>
                    </div>
                </div>
                <aside class="mobility-engine__panel" aria-label="Future net mobility summary">
                    <div class="mobility-engine__metric"><span>이동수단</span><strong>초거대 탐사차</strong></div>
                    <div class="mobility-engine__metric"><span>엔진 역할</span><strong>넷 연결·넷폰·보급</strong></div>
                    <div class="mobility-engine__metric"><span>사회 구상</span><strong>개인 데이터센터</strong></div>
                </aside>
            </article>

            <article class="mobility-engine-card" aria-label="AI netphone intelligence link">
                <div class="mobility-engine__body">
                    <span class="mobility-engine__eyebrow">AI Netphone Link</span>
                    <h2 class="mobility-engine__title">AI 기술 접목 넷폰</h2>
                    <figure class="card__visual"><img src="/assets/card-ai-netphone.svg" alt="AI 넷폰 운영 단말 이미지" loading="lazy" decoding="async"></figure>
                    <p class="mobility-engine__copy">
                        일반 통신폰을 넘어 AI 보조, 1차 개인 데이터센터 인증, 탐사차 기지 접속, 서버 임무를 다루는 운영 단말입니다.
                    </p>
                    <div class="mobility-engine__flow" aria-label="AI netphone flow">
                        <span class="mobility-engine__step">AI 판단 보조</span>
                        <span class="mobility-engine__step">1차 이주권 개인 데이터센터</span>
                        <span class="mobility-engine__step">임무 수행</span>
                    </div>
                    <div class="mobility-engine__actions">
                        <a class="mobility-engine__button mobility-engine__button--primary" href="https://mobile.netcity.co.kr/ai-netphone-intelligence/">페이지 열기</a>
                        <a class="mobility-engine__button mobility-engine__button--runtime" href="https://mobile.netcity.co.kr/ai-netphone-runtime/">넷폰 구동환경</a>
                        <a class="mobility-engine__button" href="https://mobile.netcity.co.kr/ai-netphone-intelligence/difference/">일반폰과 차이</a>
                        <a class="mobility-engine__button" href="https://mobile.netcity.co.kr/ai-netphone-intelligence/ai-core/">AI 지능 엔진</a>
                        <a class="mobility-engine__button" href="https://mobile.netcity.co.kr/ai-netphone-intelligence/mission/">라이프와 임무</a>
                        <a class="mobility-engine__button" href="https://mobile.netcity.co.kr/ai-netphone-intelligence/board/">게시판</a>
                    </div>
                </div>
                <aside class="mobility-engine__panel" aria-label="AI netphone summary">
                    <div class="mobility-engine__metric"><span>기기 성격</span><strong>AI 운영 단말</strong></div>
                    <div class="mobility-engine__metric"><span>일반폰 차이</span><strong>통신보다 운영</strong></div>
                    <div class="mobility-engine__metric"><span>연결 대상</span><strong>1차 이주권 개인 데이터센터</strong></div>
                </aside>
            </article>

            <article class="mobility-engine-card" aria-label="Smartphone article upload engine link">
                <div class="mobility-engine__body">
                    <span class="mobility-engine__eyebrow">Mobile Reporter Engine</span>
                    <h2 class="mobility-engine__title">취재 스케쥴 기사화 대표 링크</h2>
                    <figure class="card__visual"><img src="/assets/card-article-engine.svg" alt="스마트폰 취재 기록 기사화 이미지" loading="lazy" decoding="async"></figure>
                    <p class="mobility-engine__copy">
                        대표 페이지에서 스마트폰 기록, 서버 저장, CODEX 대기열, 스케쥴 기사화, 매체 등록 흐름을
                        한 번에 확인하도록 연결합니다.
                    </p>
                    <div class="mobility-engine__flow" aria-label="mobile reporter engine flow">
                        <span class="mobility-engine__step">대표 페이지</span>
                        <span class="mobility-engine__step">스마트폰 기록</span>
                        <span class="mobility-engine__step">서버 연동</span>
                        <span class="mobility-engine__step">기사화 흐름</span>
                    </div>
                    <div class="mobility-engine__actions">
                        <a class="mobility-engine__button mobility-engine__button--primary" href="https://mobile.netcity.co.kr/reporter-photo-journal-server/">대표 페이지 열기</a>
                    </div>
                </div>
                <aside class="mobility-engine__panel" aria-label="Smartphone article upload summary">
                    <div class="mobility-engine__metric"><span>스케줄</span><strong>대기 <?= number_format((int) $reporterJournal['jobs_waiting']) ?> · 진행 <?= number_format((int) $reporterJournal['jobs_running']) ?> · 완료 <?= number_format((int) $reporterJournal['jobs_done']) ?></strong></div>
                    <div class="mobility-engine__metric"><span>모바일</span><strong>업로드 기록 <?= number_format((int) $reporterJournal['mobile_intakes']) ?>건</strong></div>
                    <div class="mobility-engine__metric"><span>기사 리스트</span><strong>생성 <?= number_format((int) $reporterJournal['articles_total']) ?> · 등록 <?= number_format((int) $reporterJournal['articles_registered']) ?></strong></div>
                    <div class="mobility-engine__metric"><span>처리 대기</span><strong>CODEX 명령 <?= number_format((int) $reporterJournal['commands']) ?>건</strong></div>
                </aside>
            </article>
        </section>

        <div class="section-divider" id="technical" aria-label="Technical engine section">기술적 엔진</div>
        <section class="technical-engine-grid" aria-label="Technical engine links">
            <article class="technical-engine-card technical-engine-card--file" aria-label="Datacenter file processing engine link">
                <span class="technical-engine__eyebrow">File Engine</span>
                <h2 class="technical-engine__title">탐사차 기지·1차 개인 데이터센터 파일처리</h2>
                <figure class="card__visual"><img src="/assets/card-file-datacenter.svg" alt="데이터센터 파일처리 이미지" loading="lazy" decoding="async"></figure>
                <p class="technical-engine__copy">
                    탐사차 기지 자료를 1차 이주권 개인 데이터센터에 저장·분류·검색·권한분리·백업하는 파일 엔진입니다.
                    민감 자료까지 구분하고 CODEX 기반 점검·자동화로 확장합니다.
                </p>
                <div class="technical-engine__flow" aria-label="file processing summary">
                    <span class="technical-engine__step">탐사차 기지</span>
                    <span class="technical-engine__step">분류·검색</span>
                    <span class="technical-engine__step">백업·권한</span>
                </div>
                <div class="technical-engine__actions">
                    <a class="technical-engine__button technical-engine__button--primary" href="https://mobile.netcity.co.kr/datacenter-file-processing/">페이지</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/datacenter-file-processing/?page=personal-dc">1차 개인DC</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/datacenter-file-processing/board/">게시판</a>
                </div>
            </article>

            <article class="technical-engine-card technical-engine-card--editor" aria-label="Ubuntu Guide Editor web document editor link">
                <span class="technical-engine__eyebrow">Web Editor</span>
                <h2 class="technical-engine__title">Ubuntu Guide Editor 웹 문서 편집기</h2>
                <figure class="card__visual"><img src="/assets/card-ubuntu-guide-editor.svg" alt="Ubuntu Guide Editor 웹 문서 편집기 이미지" loading="lazy" decoding="async"></figure>
                <p class="technical-engine__copy">
                    서버 주소나 /var/www/html 아래 웹 파일을 열어 문서 편집과 코드 편집으로 바로 저장하는 운영 도구입니다.
                    설명서, 안내 페이지, 테마 파일을 앱처럼 빠르게 확인하고 수정합니다.
                </p>
                <div class="technical-engine__flow" aria-label="Ubuntu Guide Editor summary">
                    <span class="technical-engine__step">서버 URL 열기</span>
                    <span class="technical-engine__step">문서·코드 편집</span>
                    <span class="technical-engine__step">저장 전 백업</span>
                </div>
                <div class="technical-engine__actions">
                    <a class="technical-engine__button technical-engine__button--primary" href="https://mobile.netcity.co.kr/ubuntu-guide-editor/">페이지</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/ubuntu-server-setup-guide/web-file-editor.php">파일 열기</a>
                    <a class="technical-engine__button" href="https://news.netcity.co.kr/view.php?bo_table=board_it&id=63">기사</a>
                </div>
            </article>

            <article class="technical-engine-card technical-engine-card--codex" id="codex-live" aria-label="Realtime CODEX workspace link">
                <span class="technical-engine__eyebrow">Live Workspace</span>
                <h2 class="technical-engine__title">실시간 CODEX 작업현장</h2>
                <figure class="card__visual"><img src="/assets/card-codex-workspace-live.svg" alt="실시간 CODEX 작업현장 카드 이미지" loading="lazy" decoding="async"></figure>
                <p class="technical-engine__copy">
                    메인 캠프 위젯에서 분리한 GD-C100 기반 작업현장입니다.
                    라이브 프리뷰, 운영 설명서, 작업 기록 게시판을 모바일 서버 카드에서 열도록 정리했습니다.
                </p>
                <div class="technical-engine__flow" aria-label="CODEX workspace summary">
                    <span class="technical-engine__step">메인 위젯 제외</span>
                    <span class="technical-engine__step">GD-C100 라이브</span>
                    <span class="technical-engine__step">게시판 <?= number_format((int) $codexWorkspace['board_posts']) ?>건</span>
                    <span class="technical-engine__step">최근 <?= h(compact_duration($codexWorkspace['latest_age'])) ?></span>
                </div>
                <div class="technical-engine__actions">
                    <a class="technical-engine__button technical-engine__button--primary" href="https://mobile.netcity.co.kr/codex-workspace-live/">페이지</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/codex-workspace-live/#manual">설명서</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/codex-workspace-live/board/">게시판</a>
                    <a class="technical-engine__button" href="https://www.netcity.co.kr/gd-c100-ubuntu-camera/live-stream/">라이브</a>
                </div>
            </article>

            <article class="technical-engine-card technical-engine-card--capture" aria-label="Wide3 site capture engine link">
                <span class="technical-engine__eyebrow">Capture Engine</span>
                <h2 class="technical-engine__title">사이트 캡쳐 자동등록 서버</h2>
                <figure class="card__visual"><img src="/assets/card-capture-engine.svg" alt="사이트 캡쳐 자동등록 이미지" loading="lazy" decoding="async"></figure>
                <p class="technical-engine__copy">
                    수원 지역 홈페이지를 큐에 넣고 화면 캡쳐 후 성공 자료만 link.netcity.co.kr 지역정보 카테고리에 자동 등록합니다.
                </p>
                <div class="technical-engine__status" data-wide3-live-state>
                    <span><span class="capture-feature__dot<?= $wide3Capture['worker_live'] ? ' is-on' : '' ?>" data-wide3-dot></span><span data-wide3-worker>워커 <?= $wide3Capture['worker_live'] ? '실행 중' : '확인 필요' ?></span></span>
                    <span data-wide3-age><?= h(compact_duration($wide3Capture['heartbeat_age'])) ?></span>
                </div>
                <div class="capture-feature__stats technical-engine__stats">
                    <div class="capture-feature__stat"><span>대기</span><strong data-wide3-pending><?= number_format((int) $wide3Capture['pending']) ?></strong></div>
                    <div class="capture-feature__stat"><span>완료</span><strong data-wide3-done><?= number_format((int) $wide3Capture['done']) ?></strong></div>
                    <div class="capture-feature__stat"><span>실패</span><strong data-wide3-failed><?= number_format((int) $wide3Capture['failed']) ?></strong></div>
                </div>
                <div class="capture-feature__meter technical-engine__meter">
                    <div class="capture-feature__meter-head">
                        <span>진행률</span>
                        <strong data-wide3-progress-label><?= h((string) $wide3Capture['progress']) ?>%</strong>
                    </div>
                    <div class="capture-feature__track" aria-label="Wide3 capture progress">
                        <div class="capture-feature__bar" data-wide3-progress style="width: <?= h((string) $wide3Capture['progress']) ?>%"></div>
                    </div>
                </div>
                <p class="technical-engine__current" data-wide3-current>현재 상태: <?= h($wide3Capture['status']) ?><?= $wide3Capture['current_title'] !== '' ? ' / ' . h($wide3Capture['current_title']) : '' ?></p>
                <div class="technical-engine__actions">
                    <a class="technical-engine__button technical-engine__button--primary" href="https://mobile.netcity.co.kr/wide3-site-capture/">페이지</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/wide3-site-capture/#bulk">대량 등록</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/wide3-site-capture/#register">실시간</a>
                </div>
            </article>

            <article class="technical-engine-card technical-engine-card--photo" aria-label="Photo article generation engine link">
                <span class="technical-engine__eyebrow">Photo Article</span>
                <h2 class="technical-engine__title">사진 기반 기사 생성 서버</h2>
                <figure class="card__visual"><img src="/assets/card-article-engine.svg" alt="사진 기반 기사 생성 이미지" loading="lazy" decoding="async"></figure>
                <p class="technical-engine__copy">
                    사진과 현장 메모를 기사 제목, 리드문, 본문 구성, 캡션 초안으로 바꾸는 제작 엔진입니다.
                    리스트·수정·삭제 흐름으로 결과물을 관리합니다.
                </p>
                <div class="technical-engine__flow" aria-label="photo article summary">
                    <span class="technical-engine__step">사진 입력</span>
                    <span class="technical-engine__step">메모 정리</span>
                    <span class="technical-engine__step">기사 초안</span>
                </div>
                <div class="technical-engine__actions">
                    <a class="technical-engine__button technical-engine__button--primary" href="https://mobile.netcity.co.kr/photo-article-server/">페이지</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/photo-article-server/#compose">작성</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/photo-article-server/#list">리스트</a>
                    <a class="technical-engine__button" href="https://mobile.netcity.co.kr/photo-article-server/board/">게시판</a>
                </div>
            </article>

            <article class="technical-engine-card technical-engine-card--journal" aria-label="Reporter photo journal article engine link">
                <span class="technical-engine__eyebrow">Photo Journal</span>
                <h2 class="technical-engine__title">취재 스케쥴 기사화</h2>
                <figure class="card__visual"><img src="/assets/card-article-engine.svg" alt="취재 스케쥴 기사화 이미지" loading="lazy" decoding="async"></figure>
                <p class="technical-engine__copy">
                    대표 페이지를 기준으로 현장 기록, 서버 저장, CODEX 기사화 대기열, 스케쥴 노출, 매체 등록 흐름을 정리합니다.
                    세부 기능은 해당 서버 페이지 안에서 이어집니다.
                </p>
                <div class="technical-engine__flow" aria-label="photo journal summary">
                    <span class="technical-engine__step">대표 페이지</span>
                    <span class="technical-engine__step">서버 연동</span>
                    <span class="technical-engine__step">스케쥴 기사화</span>
                    <span class="technical-engine__step">기록 보존</span>
                </div>
                <div class="journal-engine__stats" aria-label="photo journal status summary">
                    <div class="journal-engine__stat"><span>생성 기사</span><strong><?= number_format((int) $reporterJournal['articles_total']) ?></strong></div>
                    <div class="journal-engine__stat"><span>등록 완료</span><strong><?= number_format((int) $reporterJournal['articles_registered']) ?></strong></div>
                    <div class="journal-engine__stat"><span>스케줄</span><strong><?= number_format((int) $reporterJournal['jobs_total']) ?></strong></div>
                    <div class="journal-engine__stat"><span>모바일 업로드</span><strong><?= number_format((int) $reporterJournal['mobile_intakes']) ?></strong></div>
                    <div class="journal-engine__stat"><span>CODEX 대기</span><strong><?= number_format((int) $reporterJournal['commands']) ?></strong></div>
                    <div class="journal-engine__stat"><span>게시판 기록</span><strong><?= number_format((int) $reporterJournal['board_posts']) ?></strong></div>
                </div>
                <p class="technical-engine__current journal-engine__current">최근 반영: <?= h(compact_duration($reporterJournal['latest_age'])) ?> · 대표 페이지에서 기록과 기사화 흐름 안내</p>
                <div class="technical-engine__actions">
                    <a class="technical-engine__button technical-engine__button--primary" href="https://mobile.netcity.co.kr/reporter-photo-journal-server/">대표 페이지</a>
                </div>
            </article>
        </section>

        <div class="section-divider" id="smartphone-dev" aria-label="Smartphone development server section">스마트폰 개발 서버</div>
        <section class="grid grid--after-divider" aria-label="Smartphone development server link">
            <article class="card card--samsung-vscode<?= status_class($samsungVscodeAvailable) ?>">
                <span class="card__label">VS Code Server</span>
                <h2 class="card__name">Samsung Galaxy Wide3 VS Code 서버</h2>
                <figure class="card__visual"><img src="/assets/card-code-voice.svg" alt="스마트폰 VS Code 서버 이미지" loading="lazy" decoding="async"></figure>
                <p class="card__description">
                    Samsung Galaxy Wide3에 Termux code-server 4.112.0을 설치해 스마트폰 브라우저에서 VS Code처럼 사용하는 개발 서버입니다.
                    폰 내부 code-server 8084와 197 중계서버 공개 프록시 구조, 운영 페이지, 게시판 기록을 카드로 분리했습니다.
                </p>
                <span class="card__status"><?= status_text($samsungVscodeAvailable) ?></span>
                <div class="card__build">
                    <span>스마트폰 내부 -> Wide3 code-server 8084</span>
                    <span>197 중계 확인 -> 공개 프록시 경로</span>
                    <span>공개 페이지 -> mobile.netcity.co.kr/samsung-vscode-server/</span>
                </div>
                <div class="card__links">
                    <a class="card__link" href="<?= h($samsungVscodeUrl) ?>">서버 페이지</a>
                    <a class="card__link" href="<?= h($samsungVoiceUrl) ?>">음성인식</a>
                    <a class="card__link" href="./samsung-vscode-server/board/">게시판</a>
                </div>
            </article>
            <article class="card card--samsung-voice<?= status_class($samsungVoiceAvailable) ?>">
                <span class="card__label">Voice STT Server</span>
                <h2 class="card__name">Samsung Galaxy Wide3 서버형 음성인식</h2>
                <figure class="card__visual"><img src="/assets/card-code-voice.svg" alt="스마트폰 음성인식 서버 이미지" loading="lazy" decoding="async"></figure>
                <p class="card__description">
                    삼성폰에서 바로 녹음하거나 음성·영상 파일을 올리면 197 서버가 파일과 작업 정보를 보관합니다.
                    브라우저 음성 명령은 곧바로 Codex 명령 대기열에 쌓고, 녹음 파일은 STT 전사 후 기사 초안 또는 Codex 작업 지시로 넘기는 입력 서버입니다.
                </p>
                <span class="card__status"><?= status_text($samsungVoiceAvailable) ?></span>
                <div class="card__build">
                    <span>스마트폰 첫 화면 -> 음성인식 STT 바로가기 등록 완료</span>
                    <span>브라우저 음성 명령 -> codex-command-queue.json 저장</span>
                    <span>음성·영상 업로드 -> jobs.json 대기열 -> STT 전사 대기</span>
                    <span>처리 모드 -> 기사 초안 / Codex 명령 / 기록 보존 선택</span>
                    <span>공개 페이지 -> mobile.netcity.co.kr/samsung-vscode-server/voice-stt-server/</span>
                </div>
                <div class="card__links">
                    <a class="card__link" href="<?= h($samsungVoiceUrl) ?>">음성인식 서버</a>
                    <a class="card__link" href="<?= h($samsungVoiceUrl) ?>#codex-command">Codex 명령</a>
                    <a class="card__link" href="<?= h($samsungVoiceUrl) ?>#codex-queue">명령 대기열</a>
                    <a class="card__link" href="<?= h($samsungVoiceUrl) ?>#upload">음성 등록</a>
                    <a class="card__link" href="<?= h($samsungVoiceUrl) ?>#queue">대기열</a>
                </div>
            </article>
            <article class="card card--smart-dev-empty" aria-label="스마트폰 개발 서버 추가 예정 카드">
                <span class="card__label">Empty Slot</span>
                <h2 class="card__name">스마트폰 개발 서버 추가 예정</h2>
                <figure class="card__visual"><img src="/assets/card-empty-slot.svg" alt="추가 서버 준비 이미지" loading="lazy" decoding="async"></figure>
                <p class="card__description">
                    다음 스마트폰 개발 서버나 앱 연동 서버를 연결할 수 있도록 비워둔 카드입니다.
                    새 장비가 붙으면 이 위치에 서버 페이지, 게시판, 바로가기 링크를 추가합니다.
                </p>
                <span class="card__meta">추가 장비 / 앱 연동 대기</span>
            </article>
        </section>

        <div class="section-divider" id="termux-wifi" aria-label="Android phone termux Wi-Fi server section">안드로이드폰 Termux 와이파이 실행 링크</div>
        <section class="quick-status" aria-label="Android Termux direct access">
            <article class="card card--termux<?= status_class($wifiPhpAvailable) ?>">
                <span class="card__label">와이파이 PHP 8082</span>
                <h2 class="card__name">와이파이(Wi-Fi) 직접 PHP 8082 페이지</h2>
                <figure class="card__visual"><img src="/assets/card-android-server.svg" alt="와이파이 안드로이드 PHP 서버 이미지" loading="lazy" decoding="async"></figure>
                <p class="card__description">안드로이드폰이 공유기 와이파이(Wi-Fi)에서 받은 현재 주소 192.168.0.100의 PHP 8082에 직접 접속하는 경로입니다. 예약 목표는 192.168.0.194입니다.</p>
                <span class="card__status"><?= status_text($wifiPhpAvailable) ?></span>
                <div class="card__build">
                    <span>독립 안내 페이지 -> /usb-android-wifi-direct-php8082/</span>
                    <span>현재 와이파이(Wi-Fi) 주소 -> 192.168.0.100:8082</span>
                    <span>중계서버를 거치지 않는 직접 접속</span>
                </div>
                <div class="card__links">
                    <a class="card__link" href="./usb-android-wifi-direct-php8082/">와이파이 8082 독립 페이지</a>
                    <a class="card__link<?= status_class($wifiPhpAvailable) ?>" href="<?= h($wifiPhpUrl) ?>">실제 주소 열기</a>
                </div>
            </article>
            <article class="card card--relay195<?= status_class($usb195PhpAvailable) ?>">
                <span class="card__label">Galaxy PHP 8082</span>
                <h2 class="card__name">갤럭시 실서버 PHP 8082 페이지</h2>
                <figure class="card__visual"><img src="/assets/card-usb-relay.svg" alt="Wi-Fi Galaxy PHP 서버 이미지" loading="lazy" decoding="async"></figure>
                <p class="card__description">192.168.0.100 갤럭시 현재 Wi-Fi IP의 8082 포트로 직접 붙는 실서버 경로입니다.</p>
                <span class="card__status"><?= status_text($usb195PhpAvailable) ?></span>
                <div class="card__build">
                    <span>독립 안내 페이지 -> /android-usb-195-php8082/</span>
                    <span>갤럭시 실행 주소 -> 192.168.0.100:8082</span>
                    <span>Wi-Fi 직접 접속</span>
                </div>
                <div class="card__links">
                    <a class="card__link" href="./android-usb-195-php8082/">Wi-Fi 100 8082 독립 페이지</a>
                    <a class="card__link<?= status_class($usb195PhpAvailable) ?>" href="<?= h($usb195PhpUrl) ?>">실제 주소 열기</a>
                </div>
            </article>
            <article class="card card--nginx<?= status_class($wifiNginxAvailable) ?>">
                <span class="card__label">와이파이 Nginx 8083</span>
                <h2 class="card__name">와이파이(Wi-Fi) 직접 Nginx 8083 페이지</h2>
                <figure class="card__visual"><img src="/assets/card-termux.svg" alt="와이파이 안드로이드 Nginx 서버 이미지" loading="lazy" decoding="async"></figure>
                <p class="card__description">안드로이드폰 현재 와이파이(Wi-Fi) IP 192.168.0.100의 8083 포트로 Termux Nginx에 직접 접속하는 경로입니다. 예약 목표는 192.168.0.194입니다.</p>
                <span class="card__status"><?= status_text($wifiNginxAvailable) ?></span>
                <div class="card__build">
                    <span>독립 안내 페이지 -> /usb-android-wifi-direct-nginx8083/</span>
                    <span>현재 와이파이(Wi-Fi) 주소 -> 192.168.0.100:8083</span>
                    <span>안드로이드폰 Nginx 직접 응답</span>
                </div>
                <div class="card__links">
                    <a class="card__link" href="./usb-android-wifi-direct-nginx8083/">와이파이 8083 독립 페이지</a>
                    <a class="card__link<?= status_class($wifiNginxAvailable) ?>" href="<?= h($wifiNginxUrl) ?>">실제 주소 열기</a>
                </div>
            </article>
            <article class="card card--relay195<?= status_class($usb195NginxAvailable) ?>">
                <span class="card__label">Galaxy Nginx 8083</span>
                <h2 class="card__name">갤럭시 실서버 Nginx 8083 페이지</h2>
                <figure class="card__visual"><img src="/assets/card-usb-relay.svg" alt="Wi-Fi Galaxy Nginx 서버 이미지" loading="lazy" decoding="async"></figure>
                <p class="card__description">192.168.0.100 갤럭시 현재 Wi-Fi IP의 8083 포트로 Termux Nginx가 직접 응답하는 실서버 경로입니다.</p>
                <span class="card__status"><?= status_text($usb195NginxAvailable) ?></span>
                <div class="card__build">
                    <span>독립 안내 페이지 -> /android-usb-195-nginx8083/</span>
                    <span>갤럭시 실행 주소 -> 192.168.0.100:8083</span>
                    <span>Wi-Fi 직접 접속</span>
                </div>
                <div class="card__links">
                    <a class="card__link" href="./android-usb-195-nginx8083/">Wi-Fi 100 8083 독립 페이지</a>
                    <a class="card__link<?= status_class($usb195NginxAvailable) ?>" href="<?= h($usb195NginxUrl) ?>">실제 주소 열기</a>
                </div>
            </article>
        </section>

        <?php if ($topCards !== []): ?>
            <div class="section-divider" id="primary-links" aria-label="Primary server link section">주요 서버 생성 링크 (Wi-Fi 스마트폰 / 메일)</div>
            <section class="grid" aria-label="Primary Server Links">
                <?php foreach ($topCards as $card): ?>
                    <?php $available = link_is_available((string) $card['href'], $root); ?>
                    <a class="card<?= isset($card['class']) ? ' ' . h((string) $card['class']) : '' ?><?= status_class($available) ?>" href="<?= h($card['href']) ?>"<?= !empty($card['target']) ? ' target="' . h((string) $card['target']) . '"' : '' ?><?= !empty($card['rel']) ? ' rel="' . h((string) $card['rel']) . '"' : '' ?>>
                        <span class="card__label"><?= h($card['label']) ?></span>
                        <h2 class="card__name"><?= h($card['name']) ?></h2>
                        <?php if (!empty($card['image'])): ?>
                            <figure class="card__visual"><img src="<?= h((string) $card['image']) ?>" alt="<?= h((string) $card['name']) ?> 대표 이미지" loading="lazy" decoding="async"></figure>
                        <?php endif; ?>
                        <p class="card__description"><?= h($card['description']) ?></p>
                        <span class="card__status"><?= status_text($available) ?></span>
                        <div class="card__meta">수정 시각 <?= date('Y-m-d H:i', (int) $card['updated_at']) ?></div>
                    </a>
                <?php endforeach; ?>
            </section>
            <?php if ($relayCards !== []): ?>
                <div class="section-divider section-divider--public-relay" id="relay-links" aria-label="Relay server section">
                    <span class="section-divider__title">일반인을 위한 중계서버 생성 링크</span>
                    <span class="section-divider__badges" aria-label="relay server board types">
                        <span class="section-divider__badge section-divider__badge--public">일반인 영역</span>
                        <span class="section-divider__badge">그누보드 G5</span>
                        <span class="section-divider__badge">G7</span>
                        <span class="section-divider__badge">리빌더</span>
                        <span class="section-divider__badge">넷보드</span>
                    </span>
                    <span class="section-divider__note">전문 개발자가 아니어도 사이트 계정, 게시판, 중계서버 구성을 바로 확인하고 시작하는 공개형 생성 영역입니다.</span>
                </div>
                <section class="grid grid--after-divider" aria-label="Relay Server Links">
                    <?php foreach ($relayCards as $card): ?>
                        <?php $available = link_is_available((string) $card['href'], $root); ?>
                        <a class="card<?= isset($card['class']) ? ' ' . h((string) $card['class']) : '' ?><?= status_class($available) ?>" href="<?= h($card['href']) ?>"<?= !empty($card['target']) ? ' target="' . h((string) $card['target']) . '"' : '' ?><?= !empty($card['rel']) ? ' rel="' . h((string) $card['rel']) . '"' : '' ?>>
                            <span class="card__label"><?= h($card['label']) ?></span>
                            <h2 class="card__name"><?= h($card['name']) ?></h2>
                            <?php if (!empty($card['image'])): ?>
                                <figure class="card__visual"><img src="<?= h((string) $card['image']) ?>" alt="<?= h((string) $card['name']) ?> 대표 이미지" loading="lazy" decoding="async"></figure>
                            <?php endif; ?>
                            <p class="card__description"><?= h($card['description']) ?></p>
                            <span class="card__status"><?= status_text($available) ?></span>
                            <div class="card__meta">수정 시각 <?= date('Y-m-d H:i', (int) $card['updated_at']) ?></div>
                        </a>
                    <?php endforeach; ?>
                </section>
            <?php endif; ?>
        <?php else: ?>
            <section class="empty">
                아직 표시할 폴더가 없습니다. 하위 폴더를 하나 만들면 이 페이지에 자동으로 링크가 추가됩니다.
            </section>
        <?php endif; ?>

        <footer class="site-footer" aria-label="Netcity server footer">
            <section class="site-counter" aria-label="server page counter">
                <div class="site-counter__item">
                    <span>누적 방문</span>
                    <strong><?= number_format((int) $serverPageCounter['total']) ?></strong>
                </div>
                <div class="site-counter__item">
                    <span>오늘 방문</span>
                    <strong><?= number_format((int) $serverPageCounter['today']) ?></strong>
                </div>
                <div class="site-counter__item">
                    <span>갱신 시각</span>
                    <strong><?= h(date('H:i', strtotime((string) $serverPageCounter['updated_at']))) ?></strong>
                </div>
            </section>
            <section class="business-info" aria-label="Netcity business information">
                <p class="business-info__line">
                    <span><strong>대표자.</strong> 김홍범</span>
                    <span><strong>편집인.</strong> 김홍범</span>
                    <span><strong>개발자.</strong> 넷보드 김홍범</span>
                    <span><strong>사업자번호.</strong> 123-45-67890</span>
                    <span><strong>주소.</strong> 수원특례시 장안구 만석로 210번길 인근</span>
                </p>
                <p class="business-info__line">
                    <span><strong>EMAIL.</strong> webmaster@netcity.co.kr</span>
                    <span><strong>TEL.</strong> 031-1234-5678</span>
                </p>
                <p class="site-copyright">COPYRIGHT(C) NETCITY ALL RIGHTS RESERVED.</p>
            </section>
        </footer>
    </main>
    <div class="floating-tools" aria-label="페이지 빠른 이동">
        <a class="floating-tools__button" href="#home" aria-label="상단으로 이동" title="상단으로 이동">
            <svg viewBox="0 0 24 24" aria-hidden="true">
                <path d="M12 5 5.5 11.5M12 5l6.5 6.5M12 5v14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
        </a>
    </div>
    <script>
        (() => {
            const dialog = document.getElementById('pc-env-modal');
            const openButtons = document.querySelectorAll('[data-pc-env-open]');
            const closeButton = document.querySelector('[data-pc-env-close]');

            if (!dialog || openButtons.length === 0 || !closeButton) {
                return;
            }

            const openDialog = () => {
                if (typeof dialog.showModal === 'function') {
                    dialog.showModal();
                } else {
                    dialog.setAttribute('open', 'open');
                }
            };

            const closeDialog = () => {
                if (typeof dialog.close === 'function') {
                    dialog.close();
                } else {
                    dialog.removeAttribute('open');
                }
            };

            openButtons.forEach((openButton) => {
                openButton.addEventListener('click', openDialog);
            });
            closeButton.addEventListener('click', closeDialog);
            dialog.addEventListener('click', (event) => {
                if (event.target === dialog) {
                    closeDialog();
                }
            });
        })();

        (() => {
            const nf = new Intl.NumberFormat('ko-KR');
            const els = {
                dot: document.querySelector('[data-wide3-dot]'),
                worker: document.querySelector('[data-wide3-worker]'),
                age: document.querySelector('[data-wide3-age]'),
                pending: document.querySelector('[data-wide3-pending]'),
                done: document.querySelector('[data-wide3-done]'),
                failed: document.querySelector('[data-wide3-failed]'),
                progress: document.querySelector('[data-wide3-progress]'),
                progressLabel: document.querySelector('[data-wide3-progress-label]'),
                current: document.querySelector('[data-wide3-current]'),
                throughput: document.querySelector('[data-wide3-throughput]'),
                eta: document.querySelector('[data-wide3-eta]'),
                linkRate: document.querySelector('[data-wide3-link-rate]'),
                latestImg: document.querySelector('[data-wide3-latest-img]'),
                latestTitle: document.querySelector('[data-wide3-latest-title]'),
                latestMeta: document.querySelector('[data-wide3-latest-meta]'),
                latestUrl: document.querySelector('[data-wide3-latest-url]'),
                thumbPlaceholder: document.querySelector('[data-wide3-thumb-placeholder]'),
                captureLink: document.querySelector('[data-wide3-capture-link]'),
                recordLink: document.querySelector('[data-wide3-record-link]'),
                categoryLink: document.querySelector('[data-wide3-category-link]'),
                nextTitle: document.querySelector('[data-wide3-next-title]'),
                nextUrl: document.querySelector('[data-wide3-next-url]'),
                nextMeta: document.querySelector('[data-wide3-next-meta]'),
                failedTitle: document.querySelector('[data-wide3-failed-title]'),
                failedUrl: document.querySelector('[data-wide3-failed-url]'),
                failedMeta: document.querySelector('[data-wide3-failed-meta]'),
            };

            if (!els.pending || !els.current) {
                return;
            }

            const durationText = (seconds) => {
                if (seconds === null || seconds === undefined || Number.isNaN(Number(seconds))) return '확인 중';
                const value = Math.max(0, Number(seconds));
                if (value < 60) return `${Math.floor(value)}초 전`;
                if (value < 3600) return `${Math.floor(value / 60)}분 전`;
                return `${Math.floor(value / 3600)}시간 전`;
            };

            const etaText = (seconds, pending) => {
                if (Number(pending || 0) <= 0) return '완료';
                if (seconds === null || seconds === undefined || Number.isNaN(Number(seconds))) return '계산 중';
                const value = Math.max(0, Number(seconds));
                if (value < 60) return `${Math.ceil(value)}초`;
                if (value < 3600) return `${Math.ceil(value / 60)}분`;
                const hours = Math.floor(value / 3600);
                const minutes = Math.ceil((value % 3600) / 60);
                return minutes > 0 ? `${hours}시간 ${minutes}분` : `${hours}시간`;
            };

            const renderLatestCapture = (records) => {
                const record = Array.isArray(records) && records.length > 0 ? records[0] : null;
                if (!record) return;

                const link = record.link_submit || null;
                const linkStatus = link ? `link ${link.status || '확인'}` : 'link 대기';
                const categoryUrl = link?.category_url || 'https://link.netcity.co.kr/?route=category&category=%EC%88%98%EC%9B%90%EC%8B%9C';

                if (els.latestTitle) els.latestTitle.textContent = record.title || '최근 완료 캡쳐';
                if (els.latestMeta) els.latestMeta.textContent = `${record.created_at || ''} · ${linkStatus}`;
                if (els.latestUrl) els.latestUrl.textContent = record.url || '';
                if (els.latestImg && record.capture_url) {
                    els.latestImg.src = record.capture_url;
                    if (els.thumbPlaceholder) els.thumbPlaceholder.style.display = 'none';
                }
                if (els.captureLink && record.capture_url) els.captureLink.href = record.capture_url;
                if (els.recordLink) els.recordLink.href = 'https://mobile.netcity.co.kr/wide3-site-capture/#records';
                if (els.categoryLink) els.categoryLink.href = categoryUrl;
            };

            const renderWorkItem = (job, titleNode, urlNode, metaNode, emptyText) => {
                const record = job?.record || {};
                if (!job || !record) {
                    if (titleNode) titleNode.textContent = emptyText;
                    if (urlNode) urlNode.textContent = '';
                    if (metaNode) metaNode.textContent = '';
                    return;
                }

                if (titleNode) titleNode.textContent = record.title || '작업 제목 확인 중';
                if (urlNode) urlNode.textContent = record.url || '';
                if (metaNode) {
                    const target = job.link_target?.category_name ? ` · 지역정보 > ${job.link_target.category_name}` : '';
                    const time = job._mtime || job.queued_at || record.queued_at || '';
                    metaNode.textContent = `${time}${target}`;
                }
            };

            const renderWide3 = (data) => {
                if (!data) return;
                const live = Boolean(data.worker_live);
                const heartbeat = data.heartbeat || {};
                const current = heartbeat.current || {};
                const status = heartbeat.status || (Number(data.pending || 0) > 0 ? 'queued' : 'idle');
                const metrics = data.metrics || {};
                const heartbeatTime = heartbeat.time ? Date.parse(String(heartbeat.time).replace(' ', 'T')) : NaN;
                const ageSeconds = Number.isNaN(heartbeatTime) ? null : Math.max(0, Math.round((Date.now() - heartbeatTime) / 1000));
                const progress = Math.max(0, Math.min(100, Number(data.progress || 0)));

                els.dot?.classList.toggle('is-on', live);
                if (els.worker) els.worker.textContent = live ? '워커 실행 중' : '워커 확인 필요';
                if (els.age) els.age.textContent = durationText(ageSeconds);
                if (els.pending) els.pending.textContent = nf.format(data.pending || 0);
                if (els.done) els.done.textContent = nf.format(data.done || 0);
                if (els.failed) els.failed.textContent = nf.format(data.failed || 0);
                if (els.progress) els.progress.style.width = `${progress}%`;
                if (els.progressLabel) els.progressLabel.textContent = `${progress.toFixed(1)}%`;
                if (els.current) {
                    const title = current.title ? ` / ${current.title}` : '';
                    const url = current.url ? ` · ${current.url}` : '';
                    els.current.textContent = `현재 상태: ${status}${title}${url}`;
                }
                if (els.throughput) els.throughput.textContent = `${nf.format(Number(metrics.throughput_per_hour || 0))}/h`;
                if (els.eta) els.eta.textContent = etaText(metrics.eta_seconds, data.pending);
                if (els.linkRate) {
                    els.linkRate.textContent = metrics.link_success_rate === null || metrics.link_success_rate === undefined
                        ? '-'
                        : `${Number(metrics.link_success_rate).toFixed(1)}%`;
                }
                renderLatestCapture(data.records);
                renderWorkItem(
                    Array.isArray(data.pending_jobs) ? data.pending_jobs[0] : null,
                    els.nextTitle,
                    els.nextUrl,
                    els.nextMeta,
                    Number(data.pending || 0) > 0 ? '대기 작업 정보를 불러오는 중입니다.' : '현재 대기열이 비어 있습니다.'
                );
                renderWorkItem(
                    Array.isArray(data.failed_jobs) ? data.failed_jobs[0] : null,
                    els.failedTitle,
                    els.failedUrl,
                    els.failedMeta,
                    Number(data.failed || 0) > 0 ? '실패 작업 정보를 불러오는 중입니다.' : '최근 실패 작업이 없습니다.'
                );
            };

            const pollWide3 = () => {
                fetch('/wide3-site-capture/?api=status', {cache: 'no-store'})
                    .then((response) => response.json())
                    .then(renderWide3)
                    .catch(() => {
                        if (els.worker) els.worker.textContent = '워커 확인 재시도';
                    });
            };

            pollWide3();
            setInterval(pollWide3, 10000);
        })();
    </script>
<script src="https://www.netcity.co.kr/widget/netcity_future_notice.js?v=20260508-lineword" defer></script>
</body>
</html>
