<?php
declare(strict_types=1);

require_once dirname(__DIR__) . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'bootstrap.php';
app_bootstrap();
$adminRequestId = strtoupper(bin2hex(random_bytes(8)));
$adminFileLogger = new \App\Support\FileLogger();
header('X-Request-ID: ' . $adminRequestId);
// The panel is normally reached through public/index.php, which loads the
// view helpers first. When the document root is the project root the panel
// is entered directly, so it must not depend on that: quaeriva_url() is
// used on this page and would otherwise be undefined.
require_once dirname(__DIR__) . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'helpers.php';

// When the document root is the project root, Apache serves this directory
// directly at /admin, bypassing the router. That is fine while the panel is
// called "admin", but it would defeat APP_ADMIN_PATH: the renamed panel would
// still answer on the old, guessable path. Refuse that case.
if (quaeriva_admin_path() !== 'admin' && !defined('QUAERIVA_ADMIN_ROUTED')) {
    http_response_code(404);
    exit;
}

require_once __DIR__ . DIRECTORY_SEPARATOR . 'AdminAccess.php';

use Admin\AdminAccess;
use App\Ads\AdCreativeRepository;
use App\Ads\AdPlacementService;
use App\Audit\AuditLogger;
use App\Community\PostingPolicyService;
use App\Admin\AdminRepository;
use App\Admin\AdminPagination;
use App\Admin\AdminDashboardService;
use App\Admin\EditorialSeedService;
use App\Admin\SeedImportService;
use App\Database\Connection;
use App\Database\DatabaseUpgradeService;
use App\Database\SchemaHealthService;
use App\Security\CsrfService;
use App\Security\HtmlSanitizer;
use App\Widgets\WidgetRegistry;
use App\Languages\LanguageRepository;
use App\Languages\LanguageManagementService;
use App\Notifications\NotificationService;
use App\Settings\SettingsRepository;
use App\Support\LocaleContext;
use App\Support\ReleaseManifest;
use App\Support\Translator;
use App\Support\TranslatableException;
use App\Support\FileLogger;
use App\Support\CoreHooks;
use App\Support\Hooks;
use App\Web\Session;
use App\Questions\QuestionService;
use App\Media\ImageUploadService;
use App\System\DiagnosticService;
use App\Theme\LayoutPresetService;
use App\Theme\ThemeAppearanceService;
use App\Theme\ThemeManager;
use App\Theme\ThemePackageService;
use App\Theme\ThemeTokenService;
use App\Theme\ThemePresetService;
use App\Theme\ThemeProfilePackageService;
use App\Theme\ThemeProfileRepository;
use App\Theme\ThemeProfileService;
use App\Topics\TopicRepository;
use App\Topics\TopicService;

Session::start();
$user = AdminAccess::sessionUser();
$site = app_config('site');
$decision = AdminAccess::decision($user);
$requestPath = (string) ($_SERVER['REQUEST_URI'] ?? (app_base_path() . '/admin'));
$requestMethod = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
$requestedAction = preg_replace('/[^a-z0-9_.-]/i', '', substr((string) ($_POST['admin_action'] ?? ''), 0, 80));
if ($requestMethod === 'POST') {
    $adminFileLogger->info('Admin mutation received.', [
        'request_id' => $adminRequestId,
        'action' => $requestedAction,
        'path' => (string) (parse_url($requestPath, PHP_URL_PATH) ?: ''),
        'actor_id' => (int) ($user['id'] ?? 0),
        'access_allowed' => (bool) ($decision['allowed'] ?? false),
    ]);
}
header('Cache-Control: private, no-store, max-age=0');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('Referrer-Policy: strict-origin-when-cross-origin');
// The panel renders no operator-supplied script or ad markup, so it takes the
// strict policy unconditionally: an injected <script> there would be running
// with an administrator's session.
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'");
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');

if (!$decision['allowed']) {
    admin_render_access_denied($decision, $site);
}

$preflightLocale = new LocaleContext(
    is_array($site['supported_locales'] ?? null) ? $site['supported_locales'] : ['ar', 'en'],
    (string) ($site['locale'] ?? 'ar'),
    null,
    $_COOKIE['quaeriva_locale'] ?? ($_COOKIE['locale'] ?? null),
    is_array($user) ? (string) ($user['preferred_locale'] ?? '') : null,
    null,
);
$preflightTranslator = new Translator($preflightLocale->uiLocale(), (string) ($site['locale'] ?? 'ar'));
$preflightCsrf = new CsrfService();
$preflightCsrfToken = $preflightCsrf->token();

try {
    $pdo = Connection::connect(app_config('database'));
    $user = Session::refresh($pdo);
    $decision = AdminAccess::decision($user);
    if (!$decision['allowed']) {
        admin_render_access_denied($decision, $site);
    }
    $schemaHealth = (new SchemaHealthService($pdo, app_root_path('database', 'migrations')))->inspect();

    $databaseUpgradeRequested = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST'
        && (string) ($_POST['admin_action'] ?? '') === 'database_upgrade';
    if ($databaseUpgradeRequested) {
        if (!is_array($user) || (string) ($user['role'] ?? '') !== 'admin') {
            http_response_code(403);
            admin_render_database_upgrade_page($preflightTranslator, $preflightLocale, $schemaHealth, $preflightCsrfToken, $preflightTranslator->get('admin.errors.forbidden'), false, 403);
        }
        $preflightCsrfStatus = $preflightCsrf->validationStatus((string) ($_POST['_csrf'] ?? ''));
        if ($preflightCsrfStatus !== CsrfService::VALID) {
            $adminFileLogger->warning('Admin CSRF validation rejected.', ['request_id' => $adminRequestId, 'action' => 'database_upgrade', 'status' => $preflightCsrfStatus]);
            admin_render_database_upgrade_page($preflightTranslator, $preflightLocale, $schemaHealth, $preflightCsrfToken, $preflightTranslator->get('auth.csrf_error'), true, 403);
        }

        $upgradeResult = (new DatabaseUpgradeService($pdo, app_root_path('database', 'migrations')))->upgrade();
        $schemaHealth = is_array($upgradeResult['health'] ?? null)
            ? $upgradeResult['health']
            : (new SchemaHealthService($pdo, app_root_path('database', 'migrations')))->inspect();
        if (!($schemaHealth['runtime_compatible'] ?? false)) {
            admin_render_database_upgrade_page($preflightTranslator, $preflightLocale, $schemaHealth, $preflightCsrfToken, $preflightTranslator->get('system.database_upgrade_failed'), true, 503);
        }
        header('Location: ' . admin_url('system', ['database_upgrade' => 'success']), true, 303);
        exit;
    }

    if (!($schemaHealth['runtime_compatible'] ?? false)) {
        admin_render_database_upgrade_page(
            $preflightTranslator,
            $preflightLocale,
            $schemaHealth,
            $preflightCsrfToken,
            null,
            is_array($user) && (string) ($user['role'] ?? '') === 'admin',
            503
        );
    }
    $schemaUpgradeWarning = !$schemaHealth['healthy'];
    $repository = new AdminRepository($pdo);
    $settings = $repository->settings();
    quaeriva_load_plugins((array) $settings->get('plugins.active', []));
} catch (Throwable $throwable) {
    $errorReference = strtoupper(bin2hex(random_bytes(6)));
    error_log('[Quaeriva admin bootstrap][' . $errorReference . '] ' . $throwable::class . ': ' . $throwable->getMessage());
    $safeUpgradeError = $preflightTranslator->get('system.database_upgrade_failed') . ' '
        . $preflightTranslator->get('system.error_reference', ['reference' => $errorReference]);
    if (isset($schemaHealth) && is_array($schemaHealth)) {
        admin_render_database_upgrade_page(
            $preflightTranslator,
            $preflightLocale,
            $schemaHealth,
            $preflightCsrfToken,
            $safeUpgradeError,
            is_array($user) && (string) ($user['role'] ?? '') === 'admin',
            503
        );
    }
    http_response_code(503);
    echo htmlspecialchars('Quaeriva administration is temporarily unavailable. ' . $preflightTranslator->get('system.error_reference', ['reference' => $errorReference]), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    exit;
}

$languageRepository = null;
$languages = [
    ['code' => 'ar', 'name' => 'Arabic', 'native_name' => 'العربية', 'direction' => 'rtl', 'is_enabled' => 1, 'sort_order' => 10],
    ['code' => 'en', 'name' => 'English', 'native_name' => 'English', 'direction' => 'ltr', 'is_enabled' => 1, 'sort_order' => 20],
];
try {
    $languageRepository = new LanguageRepository($pdo);
    $dbLanguages = $languageRepository->enabled();
    if ($dbLanguages !== []) {
        $languages = $dbLanguages;
    }
} catch (Throwable) {
    // Migration 012 is required for advanced language management; other admin pages remain available before it is applied.
}

$supportedLocales = array_values(array_map(static fn (array $row): string => (string) $row['code'], $languages));
$localeDirections = [];
foreach ($languages as $language) {
    $localeDirections[(string) $language['code']] = (string) ($language['direction'] ?? 'ltr');
}
$site['supported_locales'] = $supportedLocales;
$site['locale_directions'] = $localeDirections;
$site['languages'] = $languages;

$runtimeSiteName = trim((string) $settings->get('site.name', $site['name'] ?? 'Quaeriva'));
if ($runtimeSiteName !== '') {
    $site['name'] = $runtimeSiteName;
}
$runtimeSiteDescription = trim((string) $settings->get('site.description', $site['description'] ?? ''));
if ($runtimeSiteDescription !== '') {
    $site['description'] = $runtimeSiteDescription;
}

$runtimeAdminPath = trim((string) $settings->get('site.admin_path', ''));
if ($runtimeAdminPath !== '') {
    $site['admin_path'] = $runtimeAdminPath;
    $GLOBALS['quaeriva_admin_path_override'] = $runtimeAdminPath;
}

// The panel mints slugs too (category and tag creation), so it needs the same
// default as the public front controller or the two would disagree.
\App\Slug\Slugger::useUnicodeByDefault((string) $settings->get('seo.slug_style', 'unicode') !== 'ascii');
$GLOBALS['quaeriva_question_permalink'] = (string) $settings->get('seo.question_permalink', 'slug');
$GLOBALS['quaeriva_question_prefix'] = (string) $settings->get('seo.question_prefix', 'q');

$runtimeTimezone = trim((string) $settings->get('site.timezone', $site['timezone'] ?? 'UTC'));
if (in_array($runtimeTimezone, DateTimeZone::listIdentifiers(), true)) {
    $site['timezone'] = $runtimeTimezone;
    date_default_timezone_set($runtimeTimezone);
} else {
    $site['timezone'] = 'UTC';
    date_default_timezone_set('UTC');
}

$runtimeLocale = strtolower(trim((string) $settings->get('site.default_locale', $site['locale'] ?? 'ar')));
if (in_array($runtimeLocale, $supportedLocales, true)) {
    $site['locale'] = $runtimeLocale;
} elseif ($supportedLocales !== []) {
    $site['locale'] = $supportedLocales[0];
}
$adminDefaultLocale = strtolower(trim((string) $settings->get('admin.default_locale', $site['locale'] ?? 'ar')));
if (!in_array($adminDefaultLocale, $supportedLocales, true)) {
    $adminDefaultLocale = (string) ($supportedLocales[0] ?? 'ar');
}
$preferredAdminLocale = null;
if ($languageRepository instanceof LanguageRepository && is_array($user) && isset($user['id'])) {
    try {
        $preferredAdminLocale = $languageRepository->adminLocaleForUser((int) $user['id']);
    } catch (Throwable) {
        $preferredAdminLocale = null;
    }
}
$locale = new LocaleContext(
    $supportedLocales,
    $adminDefaultLocale,
    null,
    isset($_COOKIE['quaeriva_admin_locale']) ? (string) $_COOKIE['quaeriva_admin_locale'] : null,
    $preferredAdminLocale,
    null,
    $localeDirections,
);
$translatorOverrides = [];
$fallbackOverrides = [];
$adminFallbackLocale = $adminDefaultLocale;
if ($languageRepository instanceof LanguageRepository) {
    try {
        $adminFallbackLocale = $languageRepository->fallbackLocaleFor($locale->uiLocale(), $adminDefaultLocale);
        $translatorOverrides = $languageRepository->overrides($locale->uiLocale());
        $fallbackOverrides = $languageRepository->overrides($adminFallbackLocale);
    } catch (Throwable) {
        $translatorOverrides = [];
        $fallbackOverrides = [];
        $adminFallbackLocale = $adminDefaultLocale;
    }
}
$translator = new Translator($locale->uiLocale(), $adminFallbackLocale, null, $translatorOverrides, $fallbackOverrides);
$GLOBALS['quaeriva_admin_locale'] = $locale->uiLocale();
$csrf = new CsrfService();
$csrfToken = $csrf->token();
$isAdmin = strtolower((string) ($user['role'] ?? '')) === 'admin';
$isModerator = in_array(strtolower((string) ($user['role'] ?? '')), ['admin', 'moderator'], true);
$pages = ['overview', 'questions', 'topics', 'answers', 'comments', 'categories', 'tags', 'users', 'reports', 'messages', 'notifications', 'languages', 'settings', 'widgets'];
if ($isAdmin) { $pages[] = 'editorial'; $pages[] = 'appearance'; $pages[] = 'menus'; $pages[] = 'ads'; $pages[] = 'custom-code'; $pages[] = 'plugins'; $pages[] = 'audit'; $pages[] = 'system'; }
$page = (string) ($_GET['page'] ?? 'overview');
if (!in_array($page, $pages, true)) {
    $page = 'overview';
}
$mailSettingsSiteName = trim((string) ($site['name'] ?? ''));
if ($mailSettingsSiteName === '') {
    $mailSettingsSiteName = trim($translator->get('app.name'));
}

/**
 * Sidebar grouping.
 *
 * A flat list of eighteen links is hard to scan, so the navigation is grouped
 * into a small number of collapsible sections. Membership is filtered against
 * $pages, so a moderator simply sees smaller groups rather than links that
 * would 403. Overview stays outside the groups as the landing page.
 */
$navGroups = [];
foreach ([
    'content' => ['questions', 'topics', 'answers', 'comments', 'categories', 'tags'],
    'community' => ['users', 'editorial', 'reports', 'messages', 'notifications'],
    'appearance' => ['appearance', 'menus', 'widgets', 'custom-code', 'ads'],
    'system' => ['settings', 'languages', 'plugins', 'audit', 'system'],
] as $groupKey => $groupPages) {
    $allowed = array_values(array_filter($groupPages, static fn (string $p): bool => in_array($p, $pages, true)));
    if ($allowed !== []) {
        $navGroups[$groupKey] = $allowed;
    }
}

if ($page === 'languages' && isset($_GET['download_language_pack']) && $languageRepository instanceof LanguageRepository) {
    if (!$isAdmin || !$csrf->validate((string) ($_GET['_csrf'] ?? ''))) {
        http_response_code(403);
        echo '<!doctype html><html lang="' . $e($locale->htmlAttributes()['lang'] ?? 'en') . '" dir="' . $e($locale->htmlAttributes()['dir'] ?? 'ltr') . '"><head><meta charset="utf-8"><meta name="robots" content="noindex,nofollow"><title>' . $e($site['name'] ?? 'Quaeriva') . '</title></head><body><main><h1>' . $e($translator->get('admin.errors.forbidden')) . '</h1></main></body></html>';
        exit;
    }
    $targetLocale = strtolower(str_replace('_', '-', trim((string) $_GET['download_language_pack'])));
    $sourceLocale = strtolower(str_replace('_', '-', trim((string) ($_GET['source_locale'] ?? 'en'))));
    if (!$languageRepository->exists($targetLocale)) {
        http_response_code(404);
        echo $e($translator->get('admin.language_not_found'));
        exit;
    }
    if (!$languageRepository->exists($sourceLocale)) {
        $sourceLocale = (string) ($site['locale'] ?? 'ar');
    }
    $target = $languageRepository->find($targetLocale) ?? [];
    $sourceDictionary = admin_flatten_translation_dictionary(admin_load_language_dictionary($sourceLocale));
    if ($sourceDictionary === []) {
        $sourceDictionary = $languageRepository->overrides($sourceLocale);
    }
    $targetDictionary = admin_flatten_translation_dictionary(admin_load_language_dictionary($targetLocale));
    $targetOverrides = $languageRepository->overrides($targetLocale);
    $translations = [];
    foreach ($sourceDictionary as $key => $sourceValue) {
        $translations[$key] = (string) ($targetOverrides[$key] ?? $targetDictionary[$key] ?? $sourceValue);
    }
    foreach ($targetOverrides as $key => $value) {
        if (!array_key_exists($key, $translations)) $translations[$key] = $value;
    }
    ksort($translations, SORT_STRING);
    $pack = [
        'format' => 'quaeriva-language-pack',
        'schema' => 1,
        'locale' => $targetLocale,
        'source_locale' => $sourceLocale,
        'language' => [
            'name' => (string) ($target['name'] ?? $targetLocale),
            'native_name' => (string) ($target['native_name'] ?? $targetLocale),
            'direction' => (string) ($target['direction'] ?? 'ltr'),
            'fallback_locale' => $target['fallback_locale'] ?? null,
        ],
        'instructions' => [
            'Translate values in translations only. Do not rename, add, or remove translation keys.',
            'Preserve placeholders exactly, including tokens such as :name, :count, :status and punctuation used by the UI.',
            'Return valid UTF-8 JSON in this same structure. Do not add markdown fences or commentary.',
        ],
        'translations' => $translations,
    ];
    $filename = 'quaeriva-' . preg_replace('/[^a-z0-9-]+/i', '-', $targetLocale) . '-language.json';
    header('Content-Type: application/json; charset=UTF-8');
    header('Content-Disposition: attachment; filename="' . $filename . '"');
    header('X-Content-Type-Options: nosniff');
    echo json_encode($pack, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
    exit;
}
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET' && array_key_exists('lang', $_GET)) {
    $cleanQuery = $_GET;
    unset($cleanQuery['page'], $cleanQuery['lang']);
    header('Location: ' . admin_url($page, $cleanQuery), true, 302);
    exit;
}
$themeExportRequested = $page === 'appearance' && isset($_GET['export_theme']);
if ($themeExportRequested) {
    $escapeExport = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
    if (!$isAdmin || !$csrf->validate((string) ($_GET['_csrf'] ?? ''))) {
        http_response_code(403);
        echo $escapeExport($translator->get('admin.errors.forbidden'));
        exit;
    }
    $exportDefaults = ThemeAppearanceService::defaults((array) ($site['theme'] ?? []));
    $exportAppearance = [];
    foreach (array_keys($exportDefaults) as $key) $exportAppearance[$key] = $settings->get('theme.' . $key, $exportDefaults[$key]);
    $exportAppearance['active'] = ThemeManager::resolveActiveName((string) $settings->get('theme.active', 'default'), array_values(array_unique(array_values((array) ($site['locale_directions'] ?? [])))));
    $exportManifest = ThemeManager::loadManifest((string) $exportAppearance['active']);
    $packageJson = ThemePackageService::export($exportAppearance, $exportManifest);
    header('Content-Type: application/json; charset=UTF-8');
    header('Content-Disposition: attachment; filename="quaeriva-theme-' . rawurlencode((string) $exportAppearance['active']) . '.json"');
    header('X-Content-Type-Options: nosniff');
    echo $packageJson;
    exit;
}
$profileExportRequested = $page === 'appearance' && isset($_GET['export_profile']);
if ($profileExportRequested) {
    $escapeExport = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
    if (!$isAdmin || !$csrf->validate((string) ($_GET['_csrf'] ?? ''))) {
        http_response_code(403);
        echo $escapeExport($translator->get('admin.errors.forbidden'));
        exit;
    }
    try {
        $profileId = max(0, (int) $_GET['export_profile']);
        $profileRepository = new ThemeProfileRepository($pdo);
        $profile = $profileRepository->find($profileId);
        if ($profile === null) throw new RuntimeException('Theme profile not found.');
        $profileJson = (new ThemeProfileService($profileRepository, $settings))->export($profileId);
        $filename = 'quaeriva-theme-profile-' . preg_replace('/[^a-z0-9-]+/i', '-', (string) ($profile['slug'] ?? ('theme-' . $profileId))) . '.json';
        header('Content-Type: application/json; charset=UTF-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('X-Content-Type-Options: nosniff');
        echo $profileJson;
        exit;
    } catch (Throwable $exception) {
        http_response_code(400);
        echo $escapeExport($exception->getMessage());
        exit;
    }
}
$flash = (string) ($_GET['flash'] ?? '');
$error = '';
// Every successful mutation used to report "saved", which is wrong for the
// sixty-odd actions that do not save anything -- sending a test email,
// importing a language pack, deleting a widget. A branch may set this to one
// of the tokens below; anything else keeps the original message.
$flashToken = 'success';

if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') {
    $action = (string) ($_POST['admin_action'] ?? '');
    $csrfStatus = $csrf->validationStatus((string) ($_POST['_csrf'] ?? ''));
    if ($csrfStatus !== CsrfService::VALID) {
        $adminFileLogger->warning('Admin CSRF validation rejected.', ['request_id' => $adminRequestId, 'action' => $requestedAction, 'status' => $csrfStatus]);
        $error = $translator->get('auth.csrf_error');
    } else {
        $targetPage = in_array((string) ($_POST['page'] ?? ''), $pages, true) ? (string) $_POST['page'] : $page;
        $semanticAuditBefore = null;
        $semanticAuditAfter = null;
        $semanticAuditAction = null;
        $semanticAuditTargetId = null;
        try {
            if (!$isAdmin && in_array($action, ['question_delete', 'user_create', 'user_update', 'user_status', 'user_verify', 'user_delete', 'user_trust', 'editorial_persona_create', 'editorial_seed_create', 'editorial_seed_preview', 'editorial_seed_import', 'tag_merge', 'site_settings_save', 'membership_settings_save', 'maintenance_settings_save', 'mail_settings_save', 'community_settings_save', 'notification_settings_save', 'notification_test', 'seo_settings_save', 'language_save', 'language_defaults_save', 'language_toggle', 'language_delete', 'translation_override_save', 'translation_override_delete', 'translation_pack_import', 'layout_save', 'layout_delete', 'section_save', 'section_delete', 'widget_save', 'widget_translation_save', 'widget_toggle', 'widget_delete', 'theme_import', 'theme_profile_create', 'theme_profile_duplicate', 'theme_profile_apply', 'theme_profile_update', 'theme_profile_restore', 'theme_profile_import', 'theme_profile_delete', 'appearance_settings_save', 'appearance_settings_reset', 'ads_settings_save', 'widget_reorder', 'ad_creative_save', 'ad_creative_toggle', 'ad_creative_delete', 'custom_code_save', 'custom_code_delete', 'custom_code_toggle', 'plugin_toggle', 'menu_reorder'], true)) {
                throw new RuntimeException('Only administrators can change these settings.');
            }

            switch ($action) {
                case 'question_create':
                    $authorId = max(1, (int) ($_POST['user_id'] ?? $user['id']));
                    $qTitle = trim((string) ($_POST['title'] ?? ''));
                    $qBody = trim((string) ($_POST['body'] ?? ''));
                    $qCategory = (int) ($_POST['category_id'] ?? 0) > 0 ? (int) $_POST['category_id'] : null;
                    $qLocale = (string) ($_POST['content_locale'] ?? $currentLocale);
                    $qStatus = in_array((string) ($_POST['status'] ?? ''), ['published', 'pending', 'closed'], true) ? (string) $_POST['status'] : 'published';
                    $qTags = array_filter(array_map('intval', (array) ($_POST['tag_ids'] ?? [])));
                    if ($qTitle === '') throw new InvalidArgumentException($translator->get('questions.title_required'));
                    if ($qBody === '') throw new InvalidArgumentException($translator->get('questions.body_required'));
                    $qService = new QuestionService($pdo);
                    $qService->create($authorId, $qTitle, $qBody, $qCategory, $qLocale, false, $qTags, $qStatus);
                    break;
                case 'question_moderate':
                    $moderationAction = (string) ($_POST['moderation'] ?? '');
                    $questionId = (int) ($_POST['question_id'] ?? 0);
                    $repository->moderateQuestion((int) $user['id'], $moderationAction, $questionId, $_POST);
                    $repository->markNotificationsResolvedForUrl('review_question=' . $questionId);
                    if ($moderationAction === 'publish' && $questionId > 0) {
                        try {
                            $questionStatement = $pdo->prepare("SELECT slug FROM questions WHERE id=:id AND status='published' LIMIT 1");
                            $questionStatement->execute(['id' => $questionId]);
                            $publishedSlug = trim((string) $questionStatement->fetchColumn());
                            if ($publishedSlug !== '') {
                                (new NotificationService($pdo, $settings))->notifyTaxonomyFollowers(
                                    $questionId,
                                    $translator->get('notifications.followed_question_title'),
                                    admin_absolute_public_url('q/' . rawurlencode($publishedSlug))
                                );
                            }
                        } catch (Throwable $throwable) {
                            (new FileLogger())->error('Publication notification failed.', $throwable, ['question_id' => $questionId]);
                        }
                    }
                    break;
                case 'topic_create':
                    if (!$isModerator) throw new RuntimeException($translator->get('admin.errors.forbidden'));
                    $topicPolicy = PostingPolicyService::fromSettings($settings);
                    $topicService = new TopicService($pdo, $topicPolicy);
                    $topicId = $topicService->create(
                        max(1, (int) ($_POST['user_id'] ?? $user['id'])),
                        trim((string) ($_POST['title'] ?? '')),
                        trim((string) ($_POST['body'] ?? '')),
                        (int) ($_POST['category_id'] ?? 0) > 0 ? (int) $_POST['category_id'] : null,
                        (string) ($_POST['content_locale'] ?? ($site['locale'] ?? 'ar')),
                        (string) ($_POST['kind'] ?? 'discussion'),
                        in_array((string) ($_POST['status'] ?? 'published'), ['published', 'pending', 'hidden', 'draft'], true) ? (string) $_POST['status'] : 'published'
                    );
                    $semanticAuditTargetId = $topicId;
                    $semanticAuditAction = 'topic.created';
                    $semanticAuditAfter = $topicService->find($topicId);
                    break;
                case 'topic_update':
                    if (!$isModerator) throw new RuntimeException($translator->get('admin.errors.forbidden'));
                    $topicId = max(0, (int) ($_POST['topic_id'] ?? 0));
                    $topicService = new TopicService($pdo, PostingPolicyService::fromSettings($settings));
                    $semanticAuditBefore = $topicService->find($topicId);
                    $topicService->update(
                        (int) $user['id'], $topicId,
                        trim((string) ($_POST['title'] ?? '')), trim((string) ($_POST['body'] ?? '')),
                        (int) ($_POST['category_id'] ?? 0) > 0 ? (int) $_POST['category_id'] : null,
                        (string) ($_POST['content_locale'] ?? ($site['locale'] ?? 'ar')),
                        (string) ($_POST['kind'] ?? 'discussion'),
                        (string) ($_POST['status'] ?? 'published')
                    );
                    $semanticAuditTargetId = $topicId;
                    $semanticAuditAction = 'topic.updated';
                    $semanticAuditAfter = $topicService->find($topicId);
                    break;
                case 'topic_moderate':
                    if (!$isModerator) throw new RuntimeException($translator->get('admin.errors.forbidden'));
                    $topicId = max(0, (int) ($_POST['topic_id'] ?? 0));
                    $moderation = (string) ($_POST['moderation'] ?? '');
                    $topicService = new TopicService($pdo, PostingPolicyService::fromSettings($settings));
                    $semanticAuditBefore = $topicService->find($topicId);
                    if ($moderation === 'publish' || $moderation === 'hide') {
                        $topicService->setStatus((int) $user['id'], $topicId, $moderation === 'publish' ? 'published' : 'hidden');
                        $semanticAuditAction = $moderation === 'publish' ? 'topic.published' : 'topic.hidden';
                    } elseif ($moderation === 'pin' || $moderation === 'unpin') {
                        $topicService->setPinned((int) $user['id'], $topicId, $moderation === 'pin');
                        $semanticAuditAction = $moderation === 'pin' ? 'topic.pinned' : 'topic.unpinned';
                    } elseif ($moderation === 'lock' || $moderation === 'unlock') {
                        $topicService->setLocked((int) $user['id'], $topicId, $moderation === 'lock');
                        $semanticAuditAction = $moderation === 'lock' ? 'topic.locked' : 'topic.unlocked';
                    } elseif ($moderation === 'delete') {
                        $topicService->delete((int) $user['id'], $topicId);
                        $semanticAuditAction = 'topic.deleted';
                    } else {
                        throw new TranslatableException('topics.invalid_status');
                    }
                    $semanticAuditTargetId = $topicId;
                    $semanticAuditAfter = $moderation === 'delete' ? null : $topicService->find($topicId);
                    break;
                case 'question_seo_save':
                    $repository->saveQuestionSeo($_POST);
                    break;
                case 'question_delete':
                    $flashToken = 'deleted';
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $repository->deleteQuestion((int) ($_POST['question_id'] ?? 0));
                    break;
                case 'answer_best':
                    $repository->setBestAnswer((int) ($_POST['answer_id'] ?? 0));
                    break;
                case 'answer_unset_best':
                    $repository->unsetBestAnswer((int) ($_POST['answer_id'] ?? 0));
                    break;
                case 'answer_status':
                    $repository->setAnswerStatus((int) ($_POST['answer_id'] ?? 0), (string) ($_POST['status'] ?? 'pending'));
                    break;
                case 'answer_delete':
                    $flashToken = 'deleted';
                    $repository->deleteAnswer((int) ($_POST['answer_id'] ?? 0));
                    break;
                case 'comment_status':
                    $repository->updateCommentStatus((int) ($_POST['comment_id'] ?? 0), (string) ($_POST['status'] ?? 'hidden'));
                    break;
                case 'comment_delete':
                    $flashToken = 'deleted';
                    $repository->deleteComment((int) ($_POST['comment_id'] ?? 0));
                    break;
                case 'category_save':
                    $categoryInput = $_POST;
                    if (!empty($_POST['is_root_category'])) {
                        $categoryInput['parent_id'] = 0;
                    } elseif ((int) ($_POST['parent_id'] ?? 0) <= 0) {
                        throw new InvalidArgumentException($translator->get('admin.category_parent_required'));
                    }
                    $repository->saveCategory($categoryInput);
                    break;
                case 'category_delete':
                    $flashToken = 'deleted';
                    $repository->deleteCategory((int) ($_POST['category_id'] ?? 0));
                    break;
                case 'tag_save':
                    $repository->saveTag($_POST);
                    break;
                case 'tag_delete':
                    $flashToken = 'deleted';
                    $repository->deleteTag((int) ($_POST['tag_id'] ?? 0));
                    break;
                case 'tag_merge':
                    $repository->mergeTags((int) ($_POST['source_tag_id'] ?? 0), (int) ($_POST['target_tag_id'] ?? 0), !empty($_POST['preserve_source_name']));
                    break;
                case 'user_update':
                    if ((int) ($_POST['user_id'] ?? 0) === (int) $user['id'] && ((string) ($_POST['status'] ?? '') !== 'active' || (string) ($_POST['role'] ?? '') !== 'admin')) {
                        throw new InvalidArgumentException('You cannot remove your own active administrator access.');
                    }
                    $repository->updateUser((int) ($_POST['user_id'] ?? 0), (string) ($_POST['role'] ?? ''), (string) ($_POST['status'] ?? ''), (string) ($_POST['preferred_locale'] ?? 'ar'), (string) ($_POST['name'] ?? ''), (string) ($_POST['email'] ?? ''), (string) ($_POST['password'] ?? ''), isset($_POST['email_verified']) ? (bool) $_POST['email_verified'] : null);
                    break;
                case 'user_create':
                    $repository->createUser((string) ($_POST['name'] ?? ''), (string) ($_POST['email'] ?? ''), (string) ($_POST['password'] ?? ''), (string) ($_POST['role'] ?? 'member'), (string) ($_POST['status'] ?? 'active'), (string) ($_POST['preferred_locale'] ?? 'ar'));
                    break;
                case 'user_verify':
                    $repository->updateUser((int) ($_POST['user_id'] ?? 0), (string) ($_POST['role'] ?? 'member'), (string) ($_POST['status'] ?? 'active'), (string) ($_POST['preferred_locale'] ?? 'ar'), null, null, null, !empty($_POST['verified']));
                    break;
                case 'user_status':
                    if ((int) ($_POST['user_id'] ?? 0) === (int) $user['id'] && (string) ($_POST['status'] ?? '') !== 'active') {
                        throw new InvalidArgumentException('You cannot suspend or ban your own administrator account.');
                    }
                    $repository->setUserStatus((int) ($_POST['user_id'] ?? 0), (string) ($_POST['status'] ?? ''));
                    break;
                case 'user_delete':
                    $flashToken = 'deleted';
                    if ((int) ($_POST['user_id'] ?? 0) === (int) $user['id']) {
                        throw new InvalidArgumentException('You cannot delete your own administrator account.');
                    }
                    $repository->deleteUser((int) ($_POST['user_id'] ?? 0));
                    break;
                case 'user_trust':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $trustUserId = max(0, (int) ($_POST['user_id'] ?? 0));
                    if ($trustUserId === (int) $user['id'] && !empty($_POST['login_disabled'])) throw new InvalidArgumentException('You cannot disable your own administrator login.');
                    $repository->updateUserTrust($trustUserId, (int) ($_POST['trust_level'] ?? 0), !empty($_POST['login_disabled']));
                    break;
                case 'editorial_persona_create':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    (new EditorialSeedService($pdo))->createPersona((int) $user['id'], trim((string) ($_POST['name'] ?? '')), trim((string) ($_POST['email'] ?? '')), (string) ($_POST['preferred_locale'] ?? $currentLocale));
                    break;
                case 'editorial_seed_create':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $seedService = new EditorialSeedService($pdo);
                    $seedType = strtolower(trim((string) ($_POST['seed_type'] ?? 'question')));
                    $seedPersona = max(0, (int) ($_POST['persona_id'] ?? 0));
                    $seedStatus = in_array((string) ($_POST['status'] ?? 'published'), ['published', 'pending'], true) ? (string) $_POST['status'] : 'published';
                    if ($seedType === 'answer') {
                        $seedService->createAnswer((int) $user['id'], $seedPersona, max(0, (int) ($_POST['question_id'] ?? 0)), trim((string) ($_POST['body'] ?? '')), $seedStatus);
                    } elseif ($seedType === 'topic') {
                        $seedService->createTopic((int) $user['id'], $seedPersona, trim((string) ($_POST['title'] ?? '')), trim((string) ($_POST['body'] ?? '')), (int) ($_POST['category_id'] ?? 0) > 0 ? (int) $_POST['category_id'] : null, (string) ($_POST['content_locale'] ?? $currentLocale), 'discussion', $seedStatus);
                    } else {
                        $seedTags = array_values(array_filter(array_map('intval', preg_split('/[,;\s]+/', trim((string) ($_POST['tag_ids'] ?? ''))) ?: [])));
                        $seedService->createQuestion((int) $user['id'], $seedPersona, trim((string) ($_POST['title'] ?? '')), trim((string) ($_POST['body'] ?? '')), (int) ($_POST['category_id'] ?? 0) > 0 ? (int) $_POST['category_id'] : null, (string) ($_POST['content_locale'] ?? $currentLocale), array_slice($seedTags, 0, 5), $seedStatus);
                    }
                    break;
                case 'editorial_seed_preview':
                case 'editorial_seed_import':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $seedPayload = (string) ($_POST['seed_payload'] ?? '');
                    $seedFormat = strtolower((string) ($_POST['seed_format'] ?? 'json'));
                    $importParser = new SeedImportService();
                    $parsedSeed = $seedFormat === 'csv' ? $importParser->previewCsv($seedPayload) : $importParser->previewJson($seedPayload);
                    $seedResult = (new EditorialSeedService($pdo))->importRows((int) $user['id'], $parsedSeed['rows'], $action === 'editorial_seed_preview');
                    $seedResult['parser_invalid'] = (int) ($parsedSeed['invalid'] ?? 0);
                    $seedResult['parser_errors'] = (array) ($parsedSeed['errors'] ?? []);
                    $_SESSION['editorial_seed_result'] = $seedResult;
                    $flashToken = $action === 'editorial_seed_import' ? 'imported' : 'success';
                    break;
                case 'report_update':
                    $reportId = (int) ($_POST['report_id'] ?? 0);
                    $repository->updateReport($reportId, (string) ($_POST['status'] ?? ''), (string) ($_POST['notes'] ?? ''));
                    $repository->markNotificationsResolvedForUrl('report_id=' . $reportId);
                    break;
                case 'contact_message_update':
                    $messageId = (int) ($_POST['message_id'] ?? 0);
                    $repository->updateContactMessage($messageId, (string) ($_POST['status'] ?? ''), (string) ($_POST['notes'] ?? ''));
                    $repository->markNotificationsResolvedForUrl('message_id=' . $messageId);
                    break;
                case 'plugin_toggle':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $pluginSlug = strtolower(trim((string) ($_POST['plugin'] ?? '')));
                    $enable = !empty($_POST['enable']);
                    $activePlugins = (array) $settings->get('plugins.active', []);
                    sort($activePlugins, SORT_STRING);
                    $discoveredPlugins = quaeriva_load_plugins($activePlugins, false);
                    if ($pluginSlug === '' || !isset($discoveredPlugins[$pluginSlug])) {
                        throw new TranslatableException('admin.plugin_not_found');
                    }
                    $semanticAuditBefore = ['slug' => $pluginSlug, 'enabled' => in_array($pluginSlug, $activePlugins, true)];
                    if ($enable) {
                        if (!in_array($pluginSlug, $activePlugins, true)) {
                            $activePlugins[] = $pluginSlug;
                        }
                    } else {
                        $activePlugins = array_values(array_diff($activePlugins, [$pluginSlug]));
                    }
                    $settings->put('plugins.active', $activePlugins, 'json');
                    sort($activePlugins, SORT_STRING);
                    $semanticAuditAfter = ['slug' => $pluginSlug, 'enabled' => in_array($pluginSlug, $activePlugins, true)];
                    $semanticAuditAction = $enable ? 'plugin.enabled' : 'plugin.disabled';
                    break;
                case 'notification_read':
                    $repository->markNotificationRead((int) $user['id'], (int) ($_POST['notification_id'] ?? 0));
                    break;
                case 'notifications_mark_all_read':
                    $repository->markAllNotificationsRead((int) $user['id']);
                    break;
                case 'site_settings_save':
                    $settings->put('site.name', trim((string) ($_POST['site_name'] ?? 'Quaeriva')) ?: 'Quaeriva', 'string');
                    $siteDescription = trim((string) ($_POST['site_description'] ?? ''));
                    if (function_exists('mb_strlen') ? mb_strlen($siteDescription, 'UTF-8') > 300 : strlen($siteDescription) > 300) {
                        throw new TranslatableException('admin.validation_site_description_too_long');
                    }
                    $settings->put('site.description', $siteDescription, 'string');

                    $requestedTimezone = trim((string) ($_POST['timezone'] ?? 'UTC'));
                    if (in_array($requestedTimezone, DateTimeZone::listIdentifiers(), true)) {
                        $settings->put('site.timezone', $requestedTimezone, 'string');
                        $site['timezone'] = $requestedTimezone;
                        date_default_timezone_set($requestedTimezone);
                    } else {
                        throw new TranslatableException('admin.validation_timezone_invalid');
                    }

                    // The panel path is stored in the settings table and mirrored to .env and config/site.local.php as a fallback for when the database is unreachable.
                    if ($isAdmin && array_key_exists('admin_path', $_POST)) {
                        $requestedAdminPath = strtolower(trim((string) $_POST['admin_path'], "/ \t\n\r\0\x0B")) ?: 'admin';

                        if (preg_match('/^[a-z0-9][a-z0-9_-]{0,31}$/', $requestedAdminPath) !== 1) {
                            throw new TranslatableException('admin.validation_admin_path_invalid');
                        }

                        if (in_array($requestedAdminPath, ['install', 'api', 'assets', 'public', 'login', 'register', 'logout'], true)) {
                            throw new TranslatableException('admin.validation_admin_path_reserved');
                        }

                        // Read the effective path BEFORE touching the override: quaeriva_admin_path()
                        // reads $GLOBALS['quaeriva_admin_path_override'] first, so setting it here
                        // and comparing afterwards made the condition below always false.
                        $previousAdminPath = quaeriva_admin_path();

                        // Always persist in database settings table
                        $settings->put('site.admin_path', $requestedAdminPath === 'admin' ? '' : $requestedAdminPath, 'string');
                        $GLOBALS['quaeriva_admin_path_override'] = $requestedAdminPath;

                        if ($requestedAdminPath !== $previousAdminPath) {
                            $envFile = dirname(__DIR__) . DIRECTORY_SEPARATOR . '.env';
                            @app_env_write($envFile, [
                                'APP_ADMIN_PATH' => $requestedAdminPath === 'admin' ? null : $requestedAdminPath,
                            ]);

                            $siteLocalPath = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'site.local.php';
                            $existingSiteLocal = is_file($siteLocalPath) ? (require $siteLocalPath) : [];
                            if (!is_array($existingSiteLocal)) {
                                $existingSiteLocal = [];
                            }
                            $existingSiteLocal['admin_path'] = $requestedAdminPath;
                            $export = "<?php\ndeclare(strict_types=1);\n\nreturn " . var_export($existingSiteLocal, true) . ";\n";
                            @file_put_contents($siteLocalPath, $export, LOCK_EX);

                            // The current request still belongs to the old path,
                            // so send the operator to the new one deliberately.
                            $adminPathChangedTo = $requestedAdminPath;
                        }
                    }
                    break;
                case 'theme_profile_create':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $profileRepository = new ThemeProfileRepository($pdo);
                    $profileService = new ThemeProfileService($profileRepository, $settings);
                    $profileName = trim((string) ($_POST['profile_name'] ?? ''));
                    $profileDescription = trim((string) ($_POST['profile_description'] ?? ''));
                    $presetKey = trim((string) ($_POST['preset_key'] ?? ''));
                    $preset = $presetKey !== '' ? ThemePresetService::get($presetKey) : null;
                    $fallbackTokens = $settings->get('theme.tokens', ThemeTokenService::fromLegacyAppearance(admin_theme_snapshot($settings, $site)));
                    $profileTokens = is_array($preset['tokens'] ?? null) ? $preset['tokens'] : (is_array($fallbackTokens) ? $fallbackTokens : ThemeTokenService::defaults());
                    if ($profileName === '' && is_array($preset)) $profileName = (string) $preset['name'];
                    if ($profileDescription === '' && is_array($preset)) $profileDescription = (string) $preset['description'];
                    $semanticAuditTargetId = $profileService->create($profileName, $profileDescription, $profileTokens, (int) $user['id']);
                    $semanticAuditAction = 'theme.profile_created';
                    break;
                case 'theme_profile_duplicate':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $semanticAuditTargetId = (new ThemeProfileService(new ThemeProfileRepository($pdo), $settings))->duplicate((int) ($_POST['profile_id'] ?? 0), (int) $user['id']);
                    $semanticAuditAction = 'theme.profile_duplicated';
                    break;
                case 'theme_profile_apply':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $profileId = max(0, (int) ($_POST['profile_id'] ?? 0));
                    (new ThemeProfileService(new ThemeProfileRepository($pdo), $settings))->apply($profileId, (int) $user['id']);
                    $semanticAuditTargetId = $profileId;
                    $semanticAuditAction = 'theme.profile_applied';
                    break;
                case 'theme_profile_update':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $profileId = max(0, (int) ($_POST['profile_id'] ?? 0));
                    $profileService = new ThemeProfileService(new ThemeProfileRepository($pdo), $settings);
                    $profile = $profileService->require($profileId);
                    $profileTokens = admin_theme_profile_tokens_from_post($_POST, (array) ($profile['tokens'] ?? ThemeTokenService::defaults()));
                    $profileService->update($profileId, trim((string) ($_POST['profile_name'] ?? $profile['name'])), trim((string) ($_POST['profile_description'] ?? ($profile['description'] ?? ''))), $profileTokens, (int) $user['id']);
                    $semanticAuditTargetId = $profileId;
                    $semanticAuditAction = 'theme.profile_updated';
                    break;
                case 'theme_profile_restore':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $profileId = max(0, (int) ($_POST['profile_id'] ?? 0));
                    (new ThemeProfileService(new ThemeProfileRepository($pdo), $settings))->restore($profileId, max(0, (int) ($_POST['version_id'] ?? 0)), (int) $user['id']);
                    $semanticAuditTargetId = $profileId;
                    $semanticAuditAction = 'theme.profile_restored';
                    break;
                case 'theme_profile_import':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $profileJson = trim((string) ($_POST['theme_profile_json'] ?? ''));
                    if (isset($_FILES['theme_profile_file']) && is_array($_FILES['theme_profile_file']) && (int) ($_FILES['theme_profile_file']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
                        if ((int) ($_FILES['theme_profile_file']['size'] ?? 0) > 512 * 1024) throw new InvalidArgumentException('Theme profile package is too large.');
                        $profileJson = (string) @file_get_contents((string) ($_FILES['theme_profile_file']['tmp_name'] ?? ''));
                    }
                    if ($profileJson === '') throw new InvalidArgumentException('Theme profile package is required.');
                    $semanticAuditTargetId = (new ThemeProfileService(new ThemeProfileRepository($pdo), $settings))->import($profileJson, (int) $user['id']);
                    $semanticAuditAction = 'theme.profile_imported';
                    $flashToken = 'imported';
                    break;
                case 'theme_profile_delete':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $profileId = max(0, (int) ($_POST['profile_id'] ?? 0));
                    (new ThemeProfileService(new ThemeProfileRepository($pdo), $settings))->delete($profileId);
                    $semanticAuditTargetId = $profileId;
                    $semanticAuditAction = 'theme.profile_deleted';
                    $flashToken = 'deleted';
                    break;
                case 'theme_import':
                    $flashToken = 'imported';
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $semanticAuditBefore = admin_theme_snapshot($settings, $site);
                    $themePackageJson = trim((string) ($_POST['theme_package_json'] ?? ''));
                    if (isset($_FILES['theme_package']) && is_array($_FILES['theme_package']) && (int) ($_FILES['theme_package']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
                        if ((int) ($_FILES['theme_package']['size'] ?? 0) > 512 * 1024) throw new TranslatableException('admin.appearance_validation_package_size');
                        $themePackageJson = (string) @file_get_contents((string) ($_FILES['theme_package']['tmp_name'] ?? ''));
                    }
                    if ($themePackageJson === '') throw new TranslatableException('admin.appearance_validation_package_required');
                    $importDefaults = ThemeAppearanceService::defaults((array) ($site['theme'] ?? []));
                    $imported = ThemePackageService::import($themePackageJson, $importDefaults);
                    $importedTheme = strtolower(trim((string) ($imported['appearance']['active'] ?? ($imported['theme']['name'] ?? 'default'))));
                    $availableThemes = array_map(static fn (array $theme): string => (string) ($theme['name'] ?? ''), ThemeManager::available(array_values(array_unique(array_values((array) ($site['locale_directions'] ?? []))))));
                    if (!in_array($importedTheme, $availableThemes, true)) throw new TranslatableException('admin.appearance_validation_theme');
                    foreach ($imported['appearance'] as $key => $value) {
                        if ($key === 'active') $settings->put('theme.active', $importedTheme, 'string');
                        else $settings->put('theme.' . $key, $value, 'string');
                    }
                    $semanticAuditAfter = admin_theme_snapshot($settings, $site);
                    $semanticAuditAction = 'theme.changed';
                    break;
                case 'appearance_settings_save':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $semanticAuditBefore = admin_theme_snapshot($settings, $site);
                    $appearanceDefaults = ThemeAppearanceService::defaults((array) ($site['theme'] ?? []));
                    $appearance = ThemeAppearanceService::normalize($_POST, $appearanceDefaults);
                    // The screen is tabbed, so a save carries only the fields of
                    // the tab being submitted. Any key the form did not post keeps
                    // its stored value instead of snapping back to the default,
                    // which would silently wipe the other tabs.
                    foreach (array_keys($appearanceDefaults) as $appearanceKey) {
                        if (!array_key_exists($appearanceKey, $_POST)) {
                            $appearance[$appearanceKey] = (string) $settings->get('theme.' . $appearanceKey, $appearanceDefaults[$appearanceKey]);
                        }
                    }
                    // An uploaded logo or favicon replaces the URL field for that
                    // slot; the stored value stays a plain URL, so nothing
                    // downstream needs to know an upload happened.
                    if (($_POST['logo_remove'] ?? '') === '1') {
                        $appearance['logo_url'] = '';
                        $appearance['logo_name'] = '';
                    }
                    foreach (['logo_url' => 'logo_file', 'favicon_url' => 'favicon_file'] as $urlKey => $fileKey) {
                        if (!isset($_FILES[$fileKey]) || !is_array($_FILES[$fileKey])) {
                            continue;
                        }
                        $uploadedUrl = (new ImageUploadService('branding'))->store($_FILES[$fileKey]);
                        if ($uploadedUrl !== '') {
                            $appearance[$urlKey] = $uploadedUrl;
                            if ($urlKey === 'logo_url') {
                                $appearance['logo_name'] = basename((string) ($_FILES[$fileKey]['name'] ?? ''));
                            }
                        }
                    }
                    $activeTheme = strtolower(trim((string) ($_POST['theme_active'] ?? $settings->get('theme.active', 'default'))));
                    $availableThemes = array_map(static fn (array $theme): string => (string) ($theme['name'] ?? ''), ThemeManager::available(array_values(array_unique(array_values((array) ($site['locale_directions'] ?? []))))));
                    if (!in_array($activeTheme, $availableThemes, true)) {
                        throw new TranslatableException('admin.appearance_validation_theme');
                    }
                    $previousTheme = (string) $settings->get('theme.active', 'default');
                    $appearance['active'] = $activeTheme;
                    foreach ($appearance as $key => $value) {
                        if ($key === 'active') {
                            $settings->put('theme.active', $value, 'string');
                            continue;
                        }
                        $settings->put('theme.' . $key, $value, 'string');
                    }
                    $appliedThemeProfile = false;
                    if (isset($_POST['theme_active']) && $activeTheme !== $previousTheme) {
                        // A theme carries its profile package inside its own directory.
                        $profileFile = app_public_path('themes', $activeTheme, $activeTheme . '.json');
                        if (is_file($profileFile)) {
                            try {
                                $pkg = ThemeProfilePackageService::import((string) file_get_contents($profileFile));
                                $settings->put('theme.tokens', $pkg['tokens'], 'json');
                                try {
                                    $profRepo = new ThemeProfileRepository($pdo);
                                    $existingProf = null;
                                    foreach ($profRepo->all() as $p) {
                                        if (strtolower((string) $p['name']) === $activeTheme || (string) ($p['slug'] ?? '') === $activeTheme) {
                                            $existingProf = $p;
                                            break;
                                        }
                                    }
                                    $targetProfId = $existingProf !== null ? (int) $existingProf['id'] : $profRepo->create($pkg['name'], $pkg['description'], $pkg['tokens'], (int) $user['id']);
                                    $profRepo->activate($targetProfId);
                                    $settings->put('theme.profile_id', $targetProfId, 'integer');
                                } catch (Throwable) {}
                                $appliedThemeProfile = true;
                            } catch (Throwable) {}
                        }
                    }
                    if (!$appliedThemeProfile) {
                        $existingTokens = $settings->get('theme.tokens', ThemeTokenService::fromLegacyAppearance($appearance));
                        $settings->put('theme.tokens', ThemeTokenService::applyLegacyAppearance(is_array($existingTokens) ? $existingTokens : [], $appearance), 'json');
                    }
                    $semanticAuditAfter = admin_theme_snapshot($settings, $site);
                    $semanticAuditAction = 'theme.changed';
                    break;
                case 'appearance_settings_reset':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $semanticAuditBefore = admin_theme_snapshot($settings, $site);
                    foreach (array_keys(ThemeAppearanceService::defaults((array) ($site['theme'] ?? []))) as $key) {
                        $settings->forget('theme.' . $key);
                    }
                    $settings->forget('theme.active');
                    $settings->forget('theme.tokens');
                    $settings->forget('theme.profile_id');
                    try { (new ThemeProfileRepository($pdo))->clearActive(); } catch (Throwable) { /* Migration 037 may not be installed yet. */ }
                    $semanticAuditAfter = admin_theme_snapshot($settings, $site);
                    $semanticAuditAction = 'theme.changed';
                    break;
                case 'membership_settings_save':
                    $requireVerification = !empty($_POST['require_email_verification']);
                    if ($requireVerification && !(bool) $settings->get('mail.enabled', false)) {
                        throw new InvalidArgumentException($translator->get('admin.mail_required_for_verification'));
                    }
                    $settings->put('membership.registration_enabled', !empty($_POST['registration_enabled']), 'boolean');
                    $settings->put('membership.require_email_verification', $requireVerification, 'boolean');
                    $turnstileEnabled = !empty($_POST['turnstile_enabled']);
                    $turnstileSiteKey = trim((string) ($_POST['turnstile_site_key'] ?? ''));
                    if ($turnstileEnabled && $turnstileSiteKey === '') throw new InvalidArgumentException($translator->get('admin.turnstile_keys_required'));
                    $settings->put('security.turnstile_enabled', $turnstileEnabled, 'boolean');
                    $settings->put('security.turnstile_site_key', $turnstileSiteKey, 'string');
                    $turnstileSecret = trim((string) ($_POST['turnstile_secret_key'] ?? ''));
                    if (!empty($_POST['clear_turnstile_secret_key'])) $settings->put('security.turnstile_secret_key', '', 'string', true);
                    elseif ($turnstileSecret !== '') $settings->put('security.turnstile_secret_key', $turnstileSecret, 'string', true);
                    break;
                case 'mail_settings_save':
                    $mailEnabled = !empty($_POST['mail_enabled']);
                    $fromEmail = trim((string) ($_POST['mail_from_email'] ?? ''));
                    $siteFallbackName = trim((string) ($site['name'] ?? ''));
                    if ($siteFallbackName === '') {
                        $siteFallbackName = trim($translator->get('app.name'));
                    }
                    $fromName = trim((string) ($_POST['mail_from_name'] ?? $siteFallbackName));
                    if ($mailEnabled && ($fromEmail === '' || filter_var($fromEmail, FILTER_VALIDATE_EMAIL) === false)) {
                        throw new InvalidArgumentException($translator->get('admin.mail_from_required'));
                    }
                    if ((function_exists('mb_strlen') ? mb_strlen($fromName, 'UTF-8') : strlen($fromName)) > 120) {
                        throw new InvalidArgumentException($translator->get('admin.validation_invalid'));
                    }
                    if (!$mailEnabled && (bool) $settings->get('membership.require_email_verification', false)) {
                        throw new InvalidArgumentException($translator->get('admin.mail_cannot_disable_verification'));
                    }
                    $transport = strtolower(trim((string) ($_POST['mail_transport'] ?? 'mail')));
                    if (!in_array($transport, ['mail', 'smtp'], true)) throw new TranslatableException('admin.mail_transport_invalid');
                    $smtpHostInput = trim((string) ($_POST['mail_smtp_host'] ?? ''));
                    $smtpPortInput = max(1, min(65535, (int) ($_POST['mail_smtp_port'] ?? 587)));
                    $encryption = strtolower(trim((string) ($_POST['mail_smtp_encryption'] ?? 'starttls')));
                    if (!in_array($encryption, ['none', 'starttls', 'ssl'], true)) throw new TranslatableException('admin.mail_encryption_invalid');
                    $authMethod = strtolower(trim((string) ($_POST['mail_smtp_auth'] ?? 'login')));
                    if (!in_array($authMethod, ['login', 'plain'], true)) throw new TranslatableException('admin.mail_auth_invalid');
                    $smtpUsernameInput = trim((string) ($_POST['mail_smtp_username'] ?? ''));
                    $smtpPassword = (string) ($_POST['mail_smtp_password'] ?? '');
                    // The SMTP mailer skips authentication entirely when it has no
                    // username, which a submission server answers with a bare
                    // "AUTH is required" that says nothing about the missing field.
                    // Refuse the incomplete configuration here instead, while the
                    // operator is still looking at the form. Validation runs before
                    // any put() so a rejected save leaves nothing half-written.
                    if ($transport === 'smtp') {
                        if ($smtpHostInput === '') throw new TranslatableException('admin.mail_smtp_host_required');
                        if ($smtpUsernameInput === '') throw new TranslatableException('admin.mail_smtp_username_required');
                        if ($smtpPassword === '' && trim((string) $settings->get('mail.smtp.password', '')) === '') {
                            throw new TranslatableException('admin.mail_smtp_password_required');
                        }
                    }
                    $settings->put('mail.enabled', $mailEnabled, 'boolean');
                    $settings->put('mail.from_email', $fromEmail, 'string');
                    $settings->put('mail.from_name', $fromName !== '' ? $fromName : $siteFallbackName, 'string');
                    $settings->put('mail.transport', $transport, 'string');
                    $settings->put('mail.smtp.host', $smtpHostInput, 'string');
                    $settings->put('mail.smtp.port', $smtpPortInput, 'integer');
                    $settings->put('mail.smtp.encryption', $encryption, 'string');
                    $settings->put('mail.smtp.auth', $authMethod, 'string');
                    $settings->put('mail.smtp.username', $smtpUsernameInput, 'string');
                    if ($smtpPassword !== '') $settings->put('mail.smtp.password', $smtpPassword, 'string', true);
                    break;
                case 'ads_settings_save':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    // Placement configuration only. The advertisements
                    // themselves are rows now, saved by ad_creative_save.
                    $settings->put('ads.enabled', !empty($_POST['ads_enabled']), 'boolean');
                    $settings->put('ads.between_answers.interval', (string) max(1, min(20, (int) ($_POST['ads_between_answers_interval'] ?? 2))), 'string');
                    $settings->put('ads.between_answers.max_insertions', (string) max(1, min(10, (int) ($_POST['ads_between_answers_max_insertions'] ?? 3))), 'string');
                    // Switching rotation off is the other way a placement could end
                    // up with two running advertisements, so it is refused while
                    // more than one is enabled rather than leaving the extra ones
                    // enabled but permanently unrenderable.
                    $adRepository = new AdCreativeRepository($pdo);
                    foreach (AdPlacementService::placements() as $placement) {
                        $wantsRotation = !empty($_POST['ads_' . $placement . '_rotation']);
                        if (!$wantsRotation) {
                            $running = 0;
                            foreach ($adRepository->all($placement) as $adRow) {
                                if (!empty($adRow['is_enabled'])) $running++;
                            }
                            if ($running > 1) {
                                throw new TranslatableException('admin.ads_validation_rotation_off', ['placement' => $translator->get('admin.ads_placement_' . $placement)]);
                            }
                        }
                        $settings->put('ads.' . $placement . '.rotation', $wantsRotation, 'boolean');
                    }
                    break;
                case 'widget_reorder':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    // Positions come from the submitted order, so a widget the
                    // form did not list is untouched.
                    $reorderIds = array_values(array_filter(array_map('intval', (array) ($_POST['order'] ?? []))));
                    $reorderStatement = $pdo->prepare('UPDATE widgets SET sort_order = :sort_order, updated_at = NOW() WHERE id = :id');
                    foreach ($reorderIds as $reorderPosition => $reorderId) {
                        if ($reorderId <= 0) continue;
                        $reorderStatement->execute(['sort_order' => ($reorderPosition + 1) * 10, 'id' => $reorderId]);
                    }
                    break;
                case 'menu_reorder':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    (new \App\Theme\MenuRepository($pdo))->reorder((array) ($_POST['order'] ?? []), 'primary');
                    $flashToken = 'saved';
                    break;
                case 'ad_creative_save':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $adPlacement = trim((string) ($_POST['placement'] ?? ''));
                    $adRotation = (bool) $settings->get('ads.' . $adPlacement . '.rotation', false);
                    $_POST['id'] = (new AdCreativeRepository($pdo))->save($_POST, $adRotation);
                    break;
                case 'ad_creative_toggle':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    // Enabling runs the same capacity guard as a save, so the
                    // one-advertisement-per-placement rule cannot be sidestepped
                    // through the quick toggle.
                    $toggleRepository = new AdCreativeRepository($pdo);
                    $toggleId = max(0, (int) ($_POST['id'] ?? 0));
                    $toggleRow = $toggleRepository->find($toggleId);
                    if ($toggleRow === null) throw new RuntimeException($translator->get('admin.not_found'));
                    $toggleEnable = empty($toggleRow['is_enabled']);
                    if ($toggleEnable) {
                        $toggleRepository->assertPlacementCapacity(
                            (string) $toggleRow['placement'],
                            $toggleId,
                            (bool) $settings->get('ads.' . $toggleRow['placement'] . '.rotation', false)
                        );
                    }
                    $toggleRepository->setEnabled($toggleId, $toggleEnable);
                    $_POST['id'] = $toggleId;
                    break;
                case 'ad_creative_delete':
                    $flashToken = 'deleted';
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    (new AdCreativeRepository($pdo))->delete(max(0, (int) ($_POST['id'] ?? 0)));
                    break;
                case 'custom_code_save':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $customCodeId = max(0, (int) ($_POST['id'] ?? 0));
                    $customCodeName = trim((string) ($_POST['name'] ?? ''));
                    $customCodePosition = strtolower(trim((string) ($_POST['position'] ?? '')));
                    $customCode = (string) ($_POST['code'] ?? '');
                    if ($customCodeName === '') throw new InvalidArgumentException($translator->get('admin.custom_code_validation_name'));
                    if (admin_unicode_length($customCodeName) > 191) throw new InvalidArgumentException($translator->get('admin.custom_code_validation_name_too_long'));
                    if (!in_array($customCodePosition, ['head', 'body_start', 'body_end'], true)) throw new InvalidArgumentException($translator->get('admin.custom_code_validation_position'));
                    if (strlen($customCode) > 100000) throw new InvalidArgumentException($translator->get('admin.ads_validation_code_too_long'));
                    if (str_contains($customCode, '<?') || str_contains($customCode, '?>')) throw new InvalidArgumentException($translator->get('admin.ads_validation_php_tags'));
                    $customCodeEnabled = !empty($_POST['is_enabled']);
                    $customCodeSort = max(-100000, min(100000, (int) ($_POST['sort_order'] ?? 0)));
                    // Empty means unrestricted, which is how existing snippets
                    // keep running everywhere after the scoping columns landed.
                    $customCodeList = static function (string $raw, array $allowed): string {
                        $values = array_values(array_unique(array_filter(array_map(
                            static fn (string $value): string => strtolower(trim($value)),
                            explode(',', $raw)
                        ), static fn (string $value): bool => $value !== '' && ($allowed === [] || in_array($value, $allowed, true)))));
                        return implode(',', $values);
                    };
                    $customCodePages = $customCodeList((string) ($_POST['pages'] ?? ''), \App\Support\CustomCodeService::PAGES);
                    $customCodeLocales = $customCodeList((string) ($_POST['locales'] ?? ''), []);
                    $customCodeAudience = in_array((string) ($_POST['audience'] ?? 'any'), \App\Support\CustomCodeService::AUDIENCES, true)
                        ? (string) $_POST['audience']
                        : 'any';
                    if ($customCodeId > 0) {
                        $customCodeStatement = $pdo->prepare(
                            'UPDATE custom_code_snippets SET name=:name, position=:position, code=:code, pages=:pages, locales=:locales, audience=:audience, is_enabled=:is_enabled, sort_order=:sort_order, updated_at=NOW() WHERE id=:id'
                        );
                        $customCodeStatement->execute([
                            'id' => $customCodeId,
                            'name' => $customCodeName,
                            'position' => $customCodePosition,
                            'code' => $customCode,
                            'pages' => $customCodePages,
                            'locales' => $customCodeLocales,
                            'audience' => $customCodeAudience,
                            'is_enabled' => $customCodeEnabled ? 1 : 0,
                            'sort_order' => $customCodeSort,
                        ]);
                        if ($customCodeStatement->rowCount() === 0) {
                            $exists = $pdo->prepare('SELECT id FROM custom_code_snippets WHERE id=:id LIMIT 1');
                            $exists->execute(['id' => $customCodeId]);
                            if ($exists->fetchColumn() === false) throw new RuntimeException($translator->get('admin.not_found'));
                        }
                    } else {
                        $customCodeStatement = $pdo->prepare(
                            'INSERT INTO custom_code_snippets (name, position, code, pages, locales, audience, is_enabled, sort_order, created_at, updated_at) VALUES (:name, :position, :code, :pages, :locales, :audience, :is_enabled, :sort_order, NOW(), NOW())'
                        );
                        $customCodeStatement->execute([
                            'name' => $customCodeName,
                            'position' => $customCodePosition,
                            'code' => $customCode,
                            'pages' => $customCodePages,
                            'locales' => $customCodeLocales,
                            'audience' => $customCodeAudience,
                            'is_enabled' => $customCodeEnabled ? 1 : 0,
                            'sort_order' => $customCodeSort,
                        ]);
                        $customCodeId = (int) $pdo->lastInsertId();
                    }
                    if ($customCodeId <= 0) throw new RuntimeException($translator->get('admin.operation_failed'));
                    $_POST['id'] = $customCodeId;
                    break;
                case 'menu_item_save':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    (new \App\Theme\MenuRepository($pdo))->save($_POST);
                    break;
                case 'menu_item_delete':
                    $flashToken = 'deleted';
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    (new \App\Theme\MenuRepository($pdo))->delete(max(0, (int) ($_POST['id'] ?? 0)));
                    break;
                case 'custom_code_delete':
                    $flashToken = 'deleted';
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $customCodeId = max(0, (int) ($_POST['id'] ?? 0));
                    $customCodeStatement = $pdo->prepare('DELETE FROM custom_code_snippets WHERE id=:id');
                    $customCodeStatement->execute(['id' => $customCodeId]);
                    if ($customCodeStatement->rowCount() !== 1) throw new RuntimeException($translator->get('admin.not_found'));
                    break;
                case 'custom_code_toggle':
                    if (!$isAdmin) throw new RuntimeException($translator->get('admin.admin_only'));
                    $customCodeId = max(0, (int) ($_POST['id'] ?? 0));
                    $customCodeStatement = $pdo->prepare('UPDATE custom_code_snippets SET is_enabled = IF(is_enabled = 1, 0, 1), updated_at=NOW() WHERE id=:id');
                    $customCodeStatement->execute(['id' => $customCodeId]);
                    if ($customCodeStatement->rowCount() !== 1) throw new RuntimeException($translator->get('admin.not_found'));
                    break;
                case 'maintenance_settings_save':
                    $settings->put('maintenance.enabled', !empty($_POST['maintenance_enabled']), 'boolean');
                    $maintenanceMessage = trim((string) ($_POST['maintenance_message'] ?? ''));
                    if ((function_exists('mb_strlen') ? mb_strlen($maintenanceMessage, 'UTF-8') : strlen($maintenanceMessage)) > 1000) {
                        throw new TranslatableException('admin.validation_maintenance_too_long');
                    }
                    $settings->put('maintenance.message', $maintenanceMessage, 'string');
                    break;
                case 'community_settings_save':
                    $modPolicy = in_array((string) ($_POST['question_moderation'] ?? ''), ['all', 'new_members', 'none'], true) ? (string) $_POST['question_moderation'] : 'all';
                    $settings->put('community.question_moderation', $modPolicy, 'string');
                    $settings->put('community.comments_enabled', !empty($_POST['comments_enabled']), 'boolean');
                    $settings->put('community.follows_enabled', !empty($_POST['follows_enabled']), 'boolean');
                    $settings->put('community.duplicate_suggestions_enabled', !empty($_POST['duplicate_suggestions_enabled']), 'boolean');
                    $spamModerateScore = max(10, min(90, (int) ($_POST['spam_moderate_score'] ?? 35)));
                    $spamRejectScore = max($spamModerateScore + 5, min(100, (int) ($_POST['spam_reject_score'] ?? 80)));
                    $settings->put('community.spam_moderate_score', $spamModerateScore, 'integer');
                    $settings->put('community.spam_reject_score', $spamRejectScore, 'integer');
                    $qualityDefaults = PostingPolicyService::defaults();
                    $qualitySettingKeys = [
                        'question_title_min' => 'quality.question_title_min',
                        'question_title_max' => 'quality.question_title_max',
                        'question_body_min' => 'quality.question_body_min',
                        'question_body_max' => 'quality.question_body_max',
                        'answer_min' => 'quality.answer_min',
                        'answer_max' => 'quality.answer_max',
                        'comment_min' => 'quality.comment_min',
                        'comment_max' => 'quality.comment_max',
                        'tags_min' => 'quality.tags_min',
                        'tags_max' => 'quality.tags_max',
                        'question_rate_hour' => 'quality.question_rate_hour',
                        'answer_rate_hour' => 'quality.answer_rate_hour',
                        'comment_rate_hour' => 'quality.comment_rate_hour',
                        'comment_rate_minute' => 'quality.comment_rate_minute',
                        'tag_create_rate_hour' => 'quality.tag_create_rate_hour',
                        'comment_max_links' => 'quality.comment_max_links',
                        'duplicate_content_minutes' => 'quality.duplicate_content_minutes',
                        'banned_words' => 'quality.banned_words',
                        'banned_domains' => 'quality.banned_domains',
                    ];
                    $postedQualityRules = [];
                    foreach ($qualityDefaults as $qualityKey => $qualityDefault) {
                        $field = 'quality_' . $qualityKey;
                        if (is_int($qualityDefault)) {
                            $postedQualityRules[$qualityKey] = isset($_POST[$field]) ? (int) $_POST[$field] : $qualityDefault;
                            continue;
                        }
                        $value = trim((string) ($_POST[$field] ?? ''));
                        if (strlen($value) > 20000) throw new InvalidArgumentException($translator->get('admin.validation_invalid'));
                        $postedQualityRules[$qualityKey] = $value;
                    }
                    $normalizedQualityRules = (new PostingPolicyService($postedQualityRules))->rules();
                    foreach ($qualityDefaults as $qualityKey => $qualityDefault) {
                        $settingKey = $qualitySettingKeys[$qualityKey] ?? ('quality.' . $qualityKey);
                        $value = $normalizedQualityRules[$qualityKey] ?? $qualityDefault;
                        $settings->put($settingKey, $value, is_int($qualityDefault) ? 'integer' : 'string');
                    }
                    break;
                case 'notification_settings_save':
                    $adminEmail = trim((string) ($_POST['admin_email'] ?? ''));
                    if ($adminEmail !== '' && filter_var($adminEmail, FILTER_VALIDATE_EMAIL) === false) {
                        throw new InvalidArgumentException('Administrator notification email is invalid.');
                    }
                    $settings->put('contact.enabled', !empty($_POST['contact_enabled']), 'boolean');
                    $settings->put('notifications.database_enabled', !empty($_POST['database_enabled']), 'boolean');
                    $settings->put('notifications.email_enabled', !empty($_POST['email_enabled']), 'boolean');
                    $settings->put('notifications.admin_email', $adminEmail, 'string');
                    $settings->put('notifications.telegram_enabled', !empty($_POST['telegram_enabled']), 'boolean');
                    $telegramToken = trim((string) ($_POST['telegram_bot_token'] ?? ''));
                    if (!empty($_POST['clear_telegram_bot_token'])) $settings->put('notifications.telegram_bot_token', '', 'string', true);
                    elseif ($telegramToken !== '') $settings->put('notifications.telegram_bot_token', $telegramToken, 'string', true);
                    $telegramChat = trim((string) ($_POST['telegram_chat_id'] ?? ''));
                    if (!empty($_POST['clear_telegram_chat_id'])) $settings->put('notifications.telegram_chat_id', '', 'string', true);
                    elseif ($telegramChat !== '') $settings->put('notifications.telegram_chat_id', $telegramChat, 'string', true);
                    $settings->put('notifications.new_question', !empty($_POST['notify_new_question']), 'boolean');
                    $settings->put('notifications.new_answer', !empty($_POST['notify_new_answer']), 'boolean');
                    $settings->put('notifications.new_comment', !empty($_POST['notify_new_comment']), 'boolean');
                    $settings->put('notifications.new_report', !empty($_POST['notify_new_report']), 'boolean');
                    $settings->put('notifications.contact_message', !empty($_POST['notify_contact_message']), 'boolean');
                    break;
                case 'notification_test':
                    $flashToken = 'sent';
                    $channel = strtolower(trim((string) ($_POST['channel'] ?? '')));
                    if (!in_array($channel, ['database', 'email', 'telegram'], true)) {
                        throw new InvalidArgumentException('Invalid notification test channel.');
                    }
                    $ok = (new NotificationService($pdo, $settings))->testChannel(
                        $channel,
                        $translator->get('admin.notification_test_title', ['site' => $mailSettingsSiteName]),
                        $translator->get('admin.notification_test_body', ['site' => $mailSettingsSiteName])
                    );
                    if (!$ok) throw new RuntimeException($translator->get('admin.notification_test_failed'));
                    break;
                case 'admin_locale_switch':
                    if (!$languageRepository instanceof LanguageRepository) {
                        throw new RuntimeException('Language management migration is not applied.');
                    }
                    $requestedAdminLocale = strtolower(str_replace('_', '-', trim((string) ($_POST['admin_locale'] ?? ''))));
                    $languageRepository->setAdminLocaleForUser((int) $user['id'], $requestedAdminLocale);
                    setcookie('quaeriva_admin_locale', $requestedAdminLocale, [
                        'expires' => time() + 31536000,
                        'path' => app_base_path() === '' ? '/' : app_base_path() . '/',
                        'secure' => !empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off',
                        'httponly' => true,
                        'samesite' => 'Lax',
                    ]);
                    break;
                case 'language_defaults_save':
                    if (!$languageRepository instanceof LanguageRepository) {
                        throw new RuntimeException('Language management migration is not applied.');
                    }
                    $publicDefault = strtolower(str_replace('_', '-', trim((string) ($_POST['public_default_locale'] ?? ''))));
                    $adminDefault = strtolower(str_replace('_', '-', trim((string) ($_POST['admin_default_locale'] ?? ''))));
                    $enabledCodes = array_map(static fn (array $row): string => (string) $row['code'], $languageRepository->enabled());
                    if (!in_array($publicDefault, $enabledCodes, true) || !in_array($adminDefault, $enabledCodes, true)) {
                        throw new InvalidArgumentException('Default languages must be enabled.');
                    }
                    $languageMode = strtolower(trim((string) ($_POST['language_mode'] ?? 'multilingual')));
                    if (!in_array($languageMode, ['single', 'multilingual'], true)) {
                        throw new InvalidArgumentException('Language mode is invalid.');
                    }
                    $settings->put('site.default_locale', $publicDefault, 'string');
                    $settings->put('admin.default_locale', $adminDefault, 'string');
                    $settings->put('site.language_mode', $languageMode, 'string');
                    $settings->put('site.language_switcher_enabled', $languageMode === 'multilingual' && !empty($_POST['language_switcher_enabled']), 'boolean');
                    $settings->put('site.browser_locale_detection', $languageMode === 'multilingual' && !empty($_POST['browser_locale_detection']), 'boolean');
                    break;
                case 'language_save':
                    if (!$languageRepository instanceof LanguageRepository) {
                        throw new RuntimeException('Language management migration is not applied.');
                    }
                    $languageCode = strtolower(str_replace('_', '-', trim((string) ($_POST['code'] ?? ''))));
                    if (strtolower(trim((string) $settings->get('site.language_mode', 'multilingual'))) === 'single' && !empty($_POST['is_enabled'])) {
                        $existingLanguage = $languageRepository->find($languageCode);
                        if (!is_array($existingLanguage) || empty($existingLanguage['is_enabled'])) {
                            // Enabling a second language is an explicit request
                            // to leave single-language mode. Keeping this
                            // transition in the same POST prevents the UI from
                            // asking the administrator to submit the form twice.
                            $settings->put('site.language_mode', 'multilingual', 'string');
                            $settings->put('site.language_switcher_enabled', true, 'boolean');
                        }
                    }
                    if (empty($_POST['is_enabled'])) {
                        $publicDefault = strtolower((string) $settings->get('site.default_locale', 'ar'));
                        $adminDefault = strtolower((string) $settings->get('admin.default_locale', $publicDefault));
                        if ($languageCode === $publicDefault || $languageCode === $adminDefault) {
                            throw new TranslatableException('admin.language_error_default');
                        }
                        $dependents = $languageRepository->fallbackDependents($languageCode);
                        if ($dependents !== []) {
                            throw new TranslatableException('admin.language_error_fallback');
                        }
                    }
                    $languageRepository->save($_POST);
                    break;
                case 'language_toggle':
                    if (!$languageRepository instanceof LanguageRepository) {
                        throw new TranslatableException('admin.language_migration_required');
                    }
                    $toggleResult = (new LanguageManagementService($pdo, $languageRepository, $settings))->toggle((string) ($_POST['code'] ?? ''));
                    $semanticAuditBefore = $toggleResult['before'];
                    $semanticAuditAfter = $toggleResult['after'];
                    $semanticAuditAction = !empty($toggleResult['after']['is_enabled']) ? 'language.enabled' : 'language.disabled';
                    break;
                case 'language_delete':
                    $flashToken = 'deleted';
                    if (!$languageRepository instanceof LanguageRepository) {
                        throw new RuntimeException('Language management migration is not applied.');
                    }
                    $languageRepository->delete(
                        (string) ($_POST['code'] ?? ''),
                        (string) $settings->get('site.default_locale', $site['locale'] ?? 'ar'),
                        (string) $settings->get('admin.default_locale', $site['locale'] ?? 'ar')
                    );
                    break;
                case 'translation_override_save':
                    if (!$languageRepository instanceof LanguageRepository) {
                        throw new RuntimeException('Language management migration is not applied.');
                    }
                    $languageRepository->saveOverride((string) ($_POST['locale'] ?? ''), (string) ($_POST['translation_key'] ?? ''), (string) ($_POST['translation_value'] ?? ''), (int) $user['id']);
                    break;
                case 'translation_override_delete':
                    $flashToken = 'deleted';
                    if (!$languageRepository instanceof LanguageRepository) {
                        throw new RuntimeException('Language management migration is not applied.');
                    }
                    $languageRepository->deleteOverride((string) ($_POST['locale'] ?? ''), (string) ($_POST['translation_key'] ?? ''));
                    break;
                case 'translation_pack_import':
                    $flashToken = 'imported';
                    if (!$languageRepository instanceof LanguageRepository) {
                        throw new RuntimeException('Language management migration is not applied.');
                    }
                    $rawPack = trim((string) ($_POST['language_pack_json'] ?? ''));
                    if (isset($_FILES['language_pack']) && is_array($_FILES['language_pack']) && (int) ($_FILES['language_pack']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
                        $tmp = (string) ($_FILES['language_pack']['tmp_name'] ?? '');
                        if ($tmp === '' || !is_uploaded_file($tmp)) throw new InvalidArgumentException('Uploaded language pack is invalid.');
                        if ((int) ($_FILES['language_pack']['size'] ?? 0) > 2 * 1024 * 1024) throw new TranslatableException('admin.validation_language_pack_too_large');
                        $rawPack = (string) file_get_contents($tmp);
                    }
                    if ($rawPack === '') throw new InvalidArgumentException('Choose a language JSON file or paste its JSON content.');
                    $pack = json_decode($rawPack, true, 512, JSON_THROW_ON_ERROR);
                    if (!is_array($pack) || ($pack['format'] ?? '') !== 'quaeriva-language-pack' || (int) ($pack['schema'] ?? 0) !== 1) {
                        throw new InvalidArgumentException('Unsupported Quaeriva language pack format.');
                    }
                    $localeCode = strtolower(str_replace('_', '-', trim((string) ($pack['locale'] ?? ''))));
                    $languageMeta = is_array($pack['language'] ?? null) ? $pack['language'] : [];
                    if (!$languageRepository->exists($localeCode)) {
                        $languageRepository->save([
                            'code' => $localeCode,
                            'name' => (string) ($languageMeta['name'] ?? strtoupper($localeCode)),
                            'native_name' => (string) ($languageMeta['native_name'] ?? strtoupper($localeCode)),
                            'direction' => (string) ($languageMeta['direction'] ?? 'ltr'),
                            'fallback_locale' => (string) ($languageMeta['fallback_locale'] ?? $settings->get('site.default_locale', 'ar')),
                            'is_enabled' => 1,
                            'sort_order' => 100,
                        ]);
                    }
                    $translations = $pack['translations'] ?? null;
                    if (!is_array($translations) || count($translations) > 5000) throw new InvalidArgumentException('Language pack translations are missing or too large.');
                    $sourceLocale = strtolower(str_replace('_', '-', trim((string) ($pack['source_locale'] ?? $settings->get('site.default_locale', 'ar')))));
                    $sourceDictionary = admin_flatten_translation_dictionary(admin_load_language_dictionary($sourceLocale));
                    if ($sourceDictionary === []) {
                        $sourceDictionary = admin_flatten_translation_dictionary(admin_load_language_dictionary((string) $settings->get('site.default_locale', 'ar')));
                    }
                    foreach ($translations as $key => $value) {
                        if (!is_string($key) || (!is_string($value) && !is_numeric($value))) throw new InvalidArgumentException('Language pack contains an invalid translation entry.');
                        $value = (string) $value;
                        if (admin_unicode_length($value) > 20000) throw new InvalidArgumentException('A translation value is too long.');
                        if ($sourceDictionary !== [] && !array_key_exists($key, $sourceDictionary)) {
                            throw new InvalidArgumentException('Language pack contains an unknown translation key: ' . $key);
                        }
                        if (isset($sourceDictionary[$key]) && admin_translation_placeholders((string) $sourceDictionary[$key]) !== admin_translation_placeholders($value)) {
                            throw new InvalidArgumentException('Translation placeholders do not match for key: ' . $key);
                        }
                        $languageRepository->saveOverride($localeCode, $key, $value, (int) $user['id']);
                    }
                    $targetPage = 'languages';
                    break;
                case 'seo_settings_save':
                    $defaultTitle = trim((string) ($_POST['seo_default_title'] ?? ''));
                    $ogImage = trim((string) ($_POST['seo_og_image'] ?? ''));
                    if (function_exists('mb_strlen') ? mb_strlen($defaultTitle, 'UTF-8') > 255 : strlen($defaultTitle) > 255) {
                        throw new InvalidArgumentException('Default SEO title is too long.');
                    }
                    if ($ogImage !== '' && filter_var($ogImage, FILTER_VALIDATE_URL) === false) {
                        throw new InvalidArgumentException('Open Graph image must be a valid URL.');
                    }
                    $settings->put('seo.default_title', $defaultTitle, 'string');
                    $settings->put('seo.robots_mode', in_array((string) ($_POST['seo_robots_mode'] ?? 'index,follow'), ['index,follow', 'noindex,follow', 'index,nofollow', 'noindex,nofollow'], true) ? (string) $_POST['seo_robots_mode'] : 'index,follow', 'string');
                    $settings->put('seo.sitemap_enabled', !empty($_POST['seo_sitemap_enabled']), 'boolean');
                    $settings->put('seo.og_image', $ogImage, 'string');
                    $settings->put('seo.slug_style', ((string) ($_POST['seo_slug_style'] ?? 'unicode')) === 'ascii' ? 'ascii' : 'unicode', 'string');
                    $settings->put('seo.question_permalink', in_array((string) ($_POST['seo_question_permalink'] ?? 'slug'), ['slug', 'id-slug', 'id', 'id-dash-slug'], true) ? (string) $_POST['seo_question_permalink'] : 'slug', 'string');
                    $requestedPrefix = strtolower(trim((string) ($_POST['seo_question_prefix'] ?? 'q'), "/ \t\n\r\0\x0B")) ?: 'q';
                    if (!quaeriva_permalink_prefix_is_valid($requestedPrefix)) {
                        throw new TranslatableException('admin.validation_question_prefix_invalid');
                    }
                    $settings->put('seo.question_prefix', $requestedPrefix, 'string');
                    $settings->put('security.strict_csp', !empty($_POST['security_strict_csp']), 'boolean');
                    $settings->put('seo.twitter_site', trim((string) ($_POST['seo_twitter_site'] ?? '')), 'string');
                    $settings->put('seo.google_verification', trim((string) ($_POST['seo_google_verification'] ?? '')), 'string');
                    $settings->put('seo.bing_verification', trim((string) ($_POST['seo_bing_verification'] ?? '')), 'string');
                    break;
                case 'layout_save':
                    $repository->saveLayout($_POST);
                    break;
                case 'layout_delete':
                    $flashToken = 'deleted';
                    $repository->deleteLayout((int) ($_POST['layout_id'] ?? 0));
                    break;
                case 'section_save':
                    $repository->saveSection($_POST);
                    break;
                case 'section_delete':
                    $flashToken = 'deleted';
                    $repository->deleteSection((int) ($_POST['section_id'] ?? 0));
                    break;
                case 'widget_save':
                    $widgetId = (int) ($_POST['id'] ?? 0);
                    $semanticAuditBefore = $widgetId > 0 ? $repository->widget($widgetId) : null;
                    $semanticAuditAction = $widgetId > 0 ? 'widget.updated' : 'widget.created';
                    $widgetId = $repository->saveWidget($_POST);
                    $semanticAuditAfter = $repository->widget($widgetId);
                    $semanticAuditTargetId = $widgetId;
                    break;
                case 'widget_translation_save':
                    $widgetTranslationResult = $repository->saveWidgetTranslation(
                        (int) ($_POST['widget_id'] ?? 0),
                        (string) ($_POST['locale'] ?? ''),
                        (string) ($_POST['translation_title'] ?? ''),
                        is_array($_POST['translation_config'] ?? null) ? $_POST['translation_config'] : []
                    );
                    $semanticAuditBefore = $widgetTranslationResult['before'];
                    $semanticAuditAfter = $widgetTranslationResult['after'];
                    break;
                case 'widget_toggle':
                    $widgetId = (int) ($_POST['widget_id'] ?? 0);
                    $semanticAuditBefore = $repository->widget($widgetId);
                    $repository->toggleWidget($widgetId);
                    $semanticAuditAfter = $repository->widget($widgetId);
                    break;
                case 'widget_delete':
                    $flashToken = 'deleted';
                    $widgetId = (int) ($_POST['widget_id'] ?? 0);
                    $semanticAuditBefore = $repository->widget($widgetId);
                    $semanticAuditAction = 'widget.deleted';
                    $semanticAuditTargetId = $widgetId;
                    $repository->deleteWidget($widgetId);
                    $semanticAuditAfter = null;
                    break;
                default:
                    throw new InvalidArgumentException('Unknown administration action.');
            }

            try {
                $auditTarget = admin_audit_target($action, $_POST);
                if ($semanticAuditTargetId !== null && $semanticAuditTargetId > 0) {
                    $auditTarget['id'] = $semanticAuditTargetId;
                }
                $safeAuditPayload = admin_audit_payload($_POST);
                $logger = new AuditLogger($pdo);
                $logger->log(
                    (int) ($user['id'] ?? 0),
                    is_string($semanticAuditAction) ? $semanticAuditAction : 'admin.' . $action,
                    $auditTarget['type'],
                    $auditTarget['id'],
                    is_array($semanticAuditBefore) ? $semanticAuditBefore : null,
                    is_string($semanticAuditAction)
                        ? (is_array($semanticAuditAfter) ? $semanticAuditAfter : null)
                        : (is_array($semanticAuditAfter) ? $semanticAuditAfter : $safeAuditPayload),
                    (string) ($_SERVER['REMOTE_ADDR'] ?? '')
                );
                if (admin_is_moderation_action($action) && ($auditTarget['id'] ?? null) !== null) {
                    $reason = trim((string) ($_POST['reason'] ?? $_POST['notes'] ?? $_POST['status'] ?? ''));
                    $logger->moderation((int) ($user['id'] ?? 0), $action, (string) ($auditTarget['type'] ?? 'content'), (int) $auditTarget['id'], $reason, $safeAuditPayload);
                }
            } catch (Throwable $auditThrowable) {
                // Audit migration may still be pending during an upgrade. Never turn a successful admin mutation into a failure.
                $adminFileLogger->warning('Admin audit write failed.', ['request_id' => $adminRequestId, 'action' => $requestedAction, 'exception' => $auditThrowable::class]);
            }

            $adminFileLogger->info('Admin mutation succeeded.', ['request_id' => $adminRequestId, 'action' => $requestedAction, 'actor_id' => (int) ($user['id'] ?? 0)]);

            [$redirectPage, $redirectQuery] = admin_return_target($targetPage);

            // quaeriva_admin_path() cached the old segment when this request
            // started, so admin_url() would send the operator to a path that
            // now 404s. Build the destination from the new value instead.
            if (isset($adminPathChangedTo)) {
                $base = app_base_path();
                $target = ($base === '' ? '' : $base) . '/' . $adminPathChangedTo
                    . '?page=' . rawurlencode($redirectPage) . '&flash=success';
                header('Location: ' . $target, true, 303);
                exit;
            }

            admin_redirect($redirectPage, admin_flash_token($flashToken), $redirectQuery);
        } catch (Throwable $throwable) {
            $adminFileLogger->error('Admin mutation failed.', $throwable, ['request_id' => $adminRequestId, 'action' => $requestedAction, 'actor_id' => (int) ($user['id'] ?? 0)]);
            $error = admin_local_error($translator, $throwable);
        }
    }
}

$stats = $repository->stats();
try { $dashboardSnapshot = (new AdminDashboardService($pdo, $settings))->snapshot(); } catch (Throwable) { $dashboardSnapshot = ['attention'=>[],'health'=>[]]; }
$attributes = $locale->htmlAttributes();
$e = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
$t = static fn (string $key, array $replace = []): string => htmlspecialchars($translator->get($key, $replace), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
$pageTitle = $translator->get('admin.pages.' . $page);
$themeAppearance = ThemeAppearanceService::defaults((array) ($site['theme'] ?? []));
try {
    $themeAppearance = ThemeAppearanceService::normalize([
        'brand_text' => (string) $settings->get('theme.brand_text', $themeAppearance['brand_text']),
    ], $themeAppearance);
} catch (Throwable) {
    // Invalid appearance data must not make the administration shell unavailable.
}
$adminUnreadNotifications = 0;
$adminRecentNotifications = [];
try {
    $adminUnreadNotifications = $repository->unreadNotificationsCount((int) ($user['id'] ?? 0));
    $adminRecentNotifications = $repository->recentUnreadNotifications((int) ($user['id'] ?? 0), 5);
} catch (Throwable) {
    $adminUnreadNotifications = 0;
    $adminRecentNotifications = [];
}
?>
<!doctype html>
<html lang="<?= $e($attributes['lang']) ?>" dir="<?= $e($attributes['dir']) ?>" data-delete-confirm="<?= $t('admin.delete_confirm') ?>">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="robots" content="noindex,nofollow">
    <title><?= $e($pageTitle) ?> · <?= $e($site['name'] ?? 'Quaeriva') ?></title>
    <?= \App\Theme\ThemeAppearanceService::fontHeadTags((string) $settings->get('theme.font_stack', 'system')) ?>
    <link rel="stylesheet" href="<?= $e(admin_asset_url('quaeriva-admin.css')) ?>">
    <script src="<?= $e(admin_script_url('quaeriva-admin.js')) ?>" defer></script>
</head>
<body>
<div class="admin-shell">
    <aside class="admin-sidebar" aria-label="<?= $t('admin.sidebar_label') ?>">
        <div class="admin-brand-block" style="display: grid; gap: .75rem;">
            <a class="admin-brand" href="<?= $e(admin_url('overview')) ?>">
                <span class="admin-brand-mark" aria-hidden="true"><?= $e(quaeriva_utf8_substr(trim((string) ($themeAppearance['brand_text'] ?? '')) !== '' ? (string) $themeAppearance['brand_text'] : (string) ($site['name'] ?? 'Q'), 0, 1)) ?></span>
                <span><strong><?= $e($site['name'] ?? 'Quaeriva') ?></strong><small><?= $t('admin.eyebrow') ?></small></span>
            </a>
            <a class="admin-view-site" href="<?= $e(quaeriva_url()) ?>" target="_blank" rel="noopener">
                <span><?= $e($t('admin.view_site')) ?></span> <span aria-hidden="true">↗</span>
            </a>
        </div>
        <nav class="admin-nav" data-admin-nav>
            <?php
            $navBadge = static function (string $navPage) use ($adminUnreadNotifications, $e): string {
                if ($navPage !== 'notifications' || $adminUnreadNotifications < 1) return '';
                return '<span class="admin-nav-badge" aria-label="' . $e($adminUnreadNotifications) . '">'
                    . $e($adminUnreadNotifications > 99 ? '99+' : $adminUnreadNotifications) . '</span>';
            };
            ?>
            <a class="admin-nav-link admin-nav-solo" href="<?= $e(admin_url('overview')) ?>"<?= $page === 'overview' ? ' aria-current="page"' : '' ?>><span><?= $t('admin.pages.overview') ?></span></a>
            <?php foreach ($navGroups as $groupKey => $groupPages): ?>
                <?php
                // The group holding the current page opens on load; the rest stay
                // closed. A collapsed group still surfaces its unread count so a
                // notification is never hidden behind a closed section.
                $groupOpen = in_array($page, $groupPages, true);
                $groupUnread = in_array('notifications', $groupPages, true) ? $adminUnreadNotifications : 0;
                ?>
                <details class="admin-nav-group"<?= $groupOpen ? ' open' : '' ?>>
                    <summary>
                        <span><?= $t('admin.nav_groups.' . $groupKey) ?></span>
                        <?php if ($groupUnread > 0): ?><span class="admin-nav-badge" aria-label="<?= $e($groupUnread) ?>"><?= $e($groupUnread > 99 ? '99+' : $groupUnread) ?></span><?php endif; ?>
                    </summary>
                    <div class="admin-nav-group-items">
                        <?php foreach ($groupPages as $navPage): ?>
                            <a class="admin-nav-link" href="<?= $e(admin_url($navPage)) ?>"<?= $page === $navPage ? ' aria-current="page"' : '' ?>><span><?= $t('admin.pages.' . $navPage) ?></span><?= $navBadge($navPage) ?></a>
                        <?php endforeach; ?>
                    </div>
                </details>
            <?php endforeach; ?>
        </nav>
        <form class="admin-language" method="post" action="<?= $e(admin_url($page)) ?>">
            <input type="hidden" name="_csrf" value="<?= $e($csrfToken) ?>">
            <input type="hidden" name="admin_action" value="admin_locale_switch">
            <input type="hidden" name="page" value="<?= $e($page) ?>">
            <label for="admin-locale-select"><?= $t('common.language') ?></label>
            <select id="admin-locale-select" name="admin_locale">
                <?php foreach ($languages as $language): $code = (string) ($language['code'] ?? ''); ?>
                    <option value="<?= $e($code) ?>"<?= $code === $translator->locale() ? ' selected' : '' ?>><?= $e((string) ($language['native_name'] ?? $language['name'] ?? $code)) ?></option>
                <?php endforeach; ?>
            </select>
            <button class="admin-button-small" type="submit"><?= $t('admin.actions.apply') ?></button>
        </form>
        <div class="admin-user"><strong><bdi dir="auto"><?= $e($user['name'] ?? $user['email'] ?? $translator->get('admin.user')) ?></bdi></strong><span><?= $e($user['role'] ?? '') ?></span></div>
    </aside>
    <main class="admin-main">
        <header class="admin-topbar">
            <div><div class="admin-eyebrow"><?= $t('admin.eyebrow') ?> · v<?= $e((string) ($site['version'] ?? app_version())) ?></div><h1><?= $e($pageTitle) ?></h1><p class="admin-muted"><?= $t('admin.subtitle', ['site' => $mailSettingsSiteName]) ?></p></div>
            <div class="admin-topbar-actions">
                <details class="admin-notification-center">
                    <summary class="admin-notification-trigger" aria-label="<?= $t('admin.notifications_topbar') ?>">
                        <span aria-hidden="true">🔔</span><span><?= $t('admin.notifications_topbar') ?></span>
                        <?php if ($adminUnreadNotifications > 0): ?><b class="admin-notification-badge"><?= $e($adminUnreadNotifications > 99 ? '99+' : $adminUnreadNotifications) ?></b><?php endif; ?>
                    </summary>
                    <div class="admin-notification-popover">
                        <div class="admin-notification-head"><strong><?= $t('admin.notifications_topbar') ?></strong><span><?= $e($adminUnreadNotifications) ?></span></div>
                        <?php if ($adminRecentNotifications === []): ?>
                            <p class="admin-notification-empty"><?= $t('admin.no_unread_notifications') ?></p>
                        <?php else: ?>
                            <div class="admin-notification-list">
                                <?php foreach ($adminRecentNotifications as $notification):
                                    $notificationUrl = trim((string) ($notification['url'] ?? ''));
                                    if ($notificationUrl === '') $notificationUrl = admin_url('notifications');
                                ?>
                                    <a class="admin-notification-item" href="<?= $e($notificationUrl) ?>">
                                        <strong><bdi dir="auto"><?= $e((string) ($notification['title'] ?? '')) ?></bdi></strong>
                                        <?php if (trim((string) ($notification['body'] ?? '')) !== ''): ?><span><bdi dir="auto"><?= $e((string) ($notification['body'] ?? '')) ?></bdi></span><?php endif; ?>
                                        <time><?= $e((string) ($notification['created_at'] ?? '')) ?></time>
                                    </a>
                                <?php endforeach; ?>
                            </div>
                        <?php endif; ?>
                        <a class="admin-notification-all" href="<?= $e(admin_url('notifications')) ?>"><?= $t('admin.view_all_notifications') ?></a>
                    </div>
                </details>
                <div class="admin-status"><?= $t('admin.database_ready') ?></div>
            </div>
        </header>
        <?php if ($error !== ''): ?>
            <div class="admin-alert admin-alert-error" role="alert"><?= $e($error) ?></div>
        <?php endif; ?>
        <?php if (admin_flash_key($flash) !== null): ?>
            <div class="admin-alert admin-alert-success" role="status"><?= $t(admin_flash_key($flash)) ?></div>
        <?php endif; ?>
        <?php if ($page === 'system' && (string)($_GET['database_upgrade'] ?? '') === 'success'): ?>
            <?php
            $upgradeRegions = (int) ($_GET['regions'] ?? 0);
            $upgradeWidgets = (int) ($_GET['widgets'] ?? 0);
            $successMsg = ($upgradeRegions > 0 || $upgradeWidgets > 0)
                ? $t('system.database_upgrade_created_summary', ['regions' => $upgradeRegions, 'widgets' => $upgradeWidgets])
                : $t('system.database_upgrade_success');
            ?>
            <div class="admin-alert admin-alert-success" role="status"><?= $successMsg ?></div>
        <?php endif; ?>
        <?php if (!empty($schemaUpgradeWarning)): ?>
            <div class="admin-alert admin-schema-upgrade-notice" role="status">
                <div><strong><?= $t('system.database_upgrade_recommended') ?></strong><span><?= $t('system.database_upgrade_recommended_message') ?></span><?php if (($schemaHealth['pending'] ?? []) !== []): ?><small><bdi><?= $e(implode(', ', $schemaHealth['pending'])) ?></bdi></small><?php endif; ?></div>
                <?php if ($isAdmin): ?><form method="post" action="<?= $e(admin_path_url()) ?>"><input type="hidden" name="_csrf" value="<?= $e($csrfToken) ?>"><input type="hidden" name="admin_action" value="database_upgrade"><button class="admin-button admin-button-small" type="submit"><?= $t('system.database_upgrade_action') ?></button></form><?php endif; ?>
            </div>
        <?php endif; ?>

        <?php if ($page === 'overview'): ?>
            <?php admin_render_overview($stats, $repository, $t, $e, $csrfToken, $dashboardSnapshot); ?>
        <?php elseif ($page === 'questions'): ?>
            <?php admin_render_questions($repository, $translator, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'topics'): ?>
            <?php admin_render_topics($pdo, $languages, $translator, $t, $e, $csrfToken, $isModerator); ?>
        <?php elseif ($page === 'answers'): ?>
            <?php admin_render_answers($repository, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'comments'): ?>
            <?php admin_render_comments($repository, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'categories'): ?>
            <?php admin_render_categories($repository, $languages, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'tags'): ?>
            <?php admin_render_tags($repository, $languages, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'users'): ?>
            <?php admin_render_users($repository, $languages, $t, $e, $csrfToken, (int) $user['id'], $isAdmin); ?>
        <?php elseif ($page === 'editorial' && $isAdmin): ?>
            <?php admin_render_editorial($pdo, $repository, $languages, $t, $e, $csrfToken, (int) $user['id']); ?>
        <?php elseif ($page === 'reports'): ?>
            <?php admin_render_reports($repository, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'messages'): ?>
            <?php admin_render_messages($repository, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'notifications'): ?>
            <?php admin_render_notifications($repository, $translator, $t, $e, $csrfToken, (int) $user['id']); ?>
        <?php elseif ($page === 'languages'): ?>
            <?php admin_render_languages($languageRepository, $settings, $site, $languages, $t, $e, $csrfToken, $isAdmin); ?>
        <?php elseif ($page === 'settings'): ?>
            <?php admin_render_settings($settings, $site, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'appearance' && $isAdmin): ?>
            <?php admin_render_appearance($settings, $site, $t, $e, $csrfToken, $pdo); ?>
        <?php elseif ($page === 'widgets'): ?>
            <?php admin_render_widgets($repository, $languages, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'system'): ?>
            <?php admin_render_system($pdo, $languages, $t, $e, $csrfToken, $isAdmin); ?>
        <?php elseif ($page === 'ads' && $isAdmin): ?>
            <?php admin_render_ads($settings, $t, $e, $csrfToken, $pdo); ?>
        <?php elseif ($page === 'menus' && $isAdmin): ?>
            <?php admin_render_menus($pdo, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'custom-code' && $isAdmin): ?>
            <?php admin_render_custom_code($pdo, $t, $e, $csrfToken); ?>
        <?php elseif ($page === 'plugins' && $isAdmin): ?>
            <?php admin_render_plugins($settings, $t, $e, $csrfToken, $isAdmin); ?>
        <?php elseif ($page === 'audit' && $isAdmin): ?>
            <?php admin_render_audit($repository, $t, $e); ?>
        <?php endif; ?>
    </main>
</div>
</body>
</html>
<?php

/** @param array{status:int} $decision @param array<string,mixed> $site */
function admin_render_access_denied(array $decision, array $site): never
{
    $locale = new LocaleContext(
        is_array($site['supported_locales'] ?? null) ? $site['supported_locales'] : ['ar', 'en'],
        (string) ($site['locale'] ?? 'ar'),
        isset($_GET['lang']) ? (string) $_GET['lang'] : null,
        $_COOKIE['quaeriva_locale'] ?? ($_COOKIE['locale'] ?? null),
        null,
        null,
    );
    $translator = new Translator($locale->uiLocale(), (string) ($site['locale'] ?? 'ar'));
    $attributes = $locale->htmlAttributes();
    $e = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
    $status = (int) ($decision['status'] ?? 403);
    $message = $translator->get($status === 401 ? 'admin.errors.unauthenticated' : 'admin.errors.forbidden');
    $loginUrl = admin_public_url('login', ['redirect' => admin_path_url()]);
    http_response_code($status);
    echo '<!doctype html><html lang="' . $e($attributes['lang']) . '" dir="' . $e($attributes['dir']) . '"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>' . $e($site['name'] ?? 'Quaeriva') . '</title><link rel="stylesheet" href="' . $e(admin_asset_url('quaeriva-admin.css')) . '"></head><body><main class="admin-main"><section class="admin-card"><div class="admin-card-body"><p class="admin-eyebrow">Quaeriva · v' . $e((string) ($site['version'] ?? app_version())) . '</p><h1>' . $e($message) . '</h1><a class="admin-button" href="' . $e($loginUrl) . '">' . $e($translator->get('auth.login')) . '</a></div></section></main></body></html>';
    exit;
}

function admin_render_database_upgrade_page(
    Translator $translator,
    LocaleContext $locale,
    array $health,
    string $csrfToken,
    ?string $error,
    bool $canUpgrade,
    int $status = 503
): never {
    $e = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
    $attributes = $locale->htmlAttributes();
    $pending = array_values(array_map('strval', (array) ($health['pending'] ?? [])));
    $blocking = array_values(array_map('strval', (array) ($health['blocking_issues'] ?? [])));
    http_response_code($status);
    echo '<!doctype html><html lang="' . $e($attributes['lang']) . '" dir="' . $e($attributes['dir']) . '"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>' . $e($translator->get('system.database_upgrade_title')) . '</title><link rel="stylesheet" href="' . $e(admin_asset_url('quaeriva-admin.css')) . '"></head><body><main class="admin-main"><section class="admin-card"><div class="admin-card-body">';
    echo '<p class="admin-eyebrow">Quaeriva · v' . $e(app_version()) . '</p><h1>' . $e($translator->get('system.database_upgrade_title')) . '</h1><p>' . $e($translator->get('system.database_upgrade_message')) . '</p>';
    if ($error !== null && trim($error) !== '') echo '<div class="admin-alert admin-alert-error" role="alert">' . $e($error) . '</div>';
    if ($pending !== []) echo '<p><strong>' . $e($translator->get('system.database_upgrade_pending')) . ':</strong> <bdi>' . $e(implode(', ', $pending)) . '</bdi></p>';
    if ($blocking !== []) echo '<ul><li>' . implode('</li><li>', array_map($e, $blocking)) . '</li></ul>';
    if ($canUpgrade) {
        echo '<form method="post" action="' . $e(admin_path_url()) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="database_upgrade"><button class="admin-button" type="submit">' . $e($translator->get('system.database_upgrade_action')) . '</button></form>';
    }
    echo '<p class="admin-muted"><code>' . $e($translator->get('system.database_upgrade_cli_hint')) . '</code></p></div></section></main></body></html>';
    exit;
}

function admin_path_url(): string
{
    return admin_url('overview');
}

function admin_url(string $page = 'overview', array $query = []): string
{
    $base = app_base_path();
    $url = ($base === '' ? '' : $base) . '/' . quaeriva_admin_path() . '?page=' . rawurlencode($page);
    if ($query !== []) {
        $url .= '&' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
    }
    return $url;
}

function admin_public_url(string $path, array $query = []): string
{
    $base = app_base_path();
    $url = ($base === '' ? '' : $base) . '/' . trim($path, '/');
    if ($query !== []) {
        $url .= '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
    }
    return $url;
}

function admin_absolute_public_url(string $path, array $query = []): string
{
    $configured = rtrim(trim((string) app_env('APP_URL', '')), '/');
    if (preg_match('~^https?://[^/]+$~i', $configured) !== 1) {
        $host = trim((string) ($_SERVER['HTTP_HOST'] ?? ''));
        $scheme = !empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off' ? 'https' : 'http';
        $configured = preg_match('/^[A-Za-z0-9.-]+(?::\d{1,5})?$/', $host) === 1 ? $scheme . '://' . $host : '';
    }
    return $configured . admin_public_url($path, $query);
}

function admin_asset_url(string $asset): string
{
    $base = app_base_path();
    return ($base === '' ? '' : $base) . '/assets/css/' . rawurlencode($asset)
        . '?v=' . rawurlencode(quaeriva_asset_version('assets/css/' . $asset));
}

function admin_script_url(string $asset): string
{
    $base = app_base_path();
    return ($base === '' ? '' : $base) . '/assets/js/' . rawurlencode($asset)
        . '?v=' . rawurlencode(quaeriva_asset_version('assets/js/' . $asset));
}

/**
 * @param array<string, string|int> $query Extra query parameters for the target.
 */
function admin_redirect(string $page, string $flash, array $query = []): never
{
    header('Location: ' . admin_url($page, $query + ['flash' => $flash]), true, 303);
    exit;
}

/**
 * Where a save should return to.
 *
 * A widget added from the appearance layout tab used to land on the widgets
 * screen, leaving the operator to navigate back to the region they were
 * arranging. The originating screen is carried through the form so the save
 * returns to it. Only known destinations are honoured, so the parameter cannot
 * be used to bounce an administrator anywhere else.
 *
 * @return array{0: string, 1: array<string, string>}
 */
function admin_return_target(string $fallbackPage): array
{
    return match ((string) ($_POST['return'] ?? '')) {
        'appearance_layout' => ['appearance', ['tab' => 'layout']],
        'widget_translation' => ['widgets', ['edit_widget' => (string) max(1, (int) ($_POST['widget_id'] ?? 0)), 'widget_locale' => (string) ($_POST['locale'] ?? '')]],
        default => [$fallbackPage, admin_return_tab()],
    };
}

/**
 * Success tokens a mutation may report, and the message each renders.
 *
 * Whitelisted both ways: an unknown token coming back through the query string
 * renders nothing rather than a translation key, and a branch cannot invent a
 * token that has no message.
 *
 * @return array<string, string>
 */
function admin_flash_messages(): array
{
    return [
        'success' => 'admin.saved',
        'sent' => 'admin.flash_sent',
        'imported' => 'admin.flash_imported',
        'deleted' => 'admin.flash_deleted',
    ];
}

function admin_flash_token(string $token): string
{
    return array_key_exists($token, admin_flash_messages()) ? $token : 'success';
}

function admin_flash_key(string $flash): ?string
{
    return admin_flash_messages()[$flash] ?? null;
}

/**
 * The tab a save came from.
 *
 * Settings and appearance are tabbed screens whose forms post to
 * admin_url($page, ['tab' => ...]), so the tab is present in the query string
 * of the POST itself. Rebuilding the redirect without it dropped the operator
 * back on the first tab after every single save.
 *
 * @return array<string, string>
 */
function admin_return_tab(): array
{
    $tab = trim((string) ($_POST['tab'] ?? $_GET['tab'] ?? ''));

    return preg_match('/^[a-z][a-z0-9_-]{0,31}$/', $tab) === 1 ? ['tab' => $tab] : [];
}

function admin_unicode_length(string $value): int
{
    return function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
}

function admin_unicode_substr(string $value, int $start, int $length): string
{
    return function_exists('mb_substr') ? mb_substr($value, $start, $length, 'UTF-8') : substr($value, $start, $length);
}

function admin_pagination(array $rows, int $total, callable $e): array
{
    $pagination = AdminPagination::fromQuery($_GET, $total);
    admin_pagination_controls($pagination, $e);
    return $pagination->slice($rows);
}

function admin_pagination_controls(AdminPagination $pagination, callable $e, ?callable $t = null): void
{
    $pages = $pagination->pages();
    if ($pages <= 1) return;
    $pageName = (string) ($_GET['page'] ?? 'overview');
    $query = $_GET;
    unset($query['page']);
    $link = static function (int $page) use ($pageName, $query, $e): string {
        $query['admin_page'] = $page;
        return $e(admin_url($pageName, $query));
    };
    $label = static function (string $key, string $fallback) use ($t): string {
        return $t !== null ? $t($key) : $fallback;
    };
    $visible = [1, $pages];
    for ($page = max(1, $pagination->page - 2); $page <= min($pages, $pagination->page + 2); $page++) $visible[] = $page;
    $visible = array_values(array_unique($visible));
    sort($visible);
    echo '<nav class="admin-pagination" aria-label="' . $e($label('admin.pagination.label', 'Pagination')) . '">';
    if ($pagination->page > 1) {
        echo '<a class="admin-button admin-button-small admin-pagination-edge" href="' . $link(1) . '">' . $e($label('admin.pagination.first', 'First')) . '</a>';
        echo '<a class="admin-button admin-button-small admin-pagination-edge" href="' . $link($pagination->page - 1) . '">' . $e($label('admin.pagination.previous', 'Previous')) . '</a>';
    }
    $previous = 0;
    foreach ($visible as $page) {
        if ($previous > 0 && $page > $previous + 1) echo '<span class="admin-pagination-gap" aria-hidden="true">…</span>';
        $current = $page === $pagination->page;
        echo '<a class="admin-button admin-button-small' . ($current ? ' is-active' : '') . '" href="' . $link($page) . '"' . ($current ? ' aria-current="page"' : '') . '>' . $e($page) . '</a>';
        $previous = $page;
    }
    if ($pagination->page < $pages) {
        echo '<a class="admin-button admin-button-small admin-pagination-edge" href="' . $link($pagination->page + 1) . '">' . $e($label('admin.pagination.next', 'Next')) . '</a>';
        echo '<a class="admin-button admin-button-small admin-pagination-edge" href="' . $link($pages) . '">' . $e($label('admin.pagination.last', 'Last')) . '</a>';
    }
    echo '</nav>';
}

/** @param array<string,mixed> $input @return array{type:?string,id:?int} */
function admin_audit_target(string $action, array $input): array
{
    $map = [
        'question_moderate'=>['question','question_id'], 'question_seo_save'=>['question','question_id'], 'question_delete'=>['question','question_id'],
        'topic_create'=>['topic','id'], 'topic_update'=>['topic','topic_id'], 'topic_moderate'=>['topic','topic_id'],
        'answer_best'=>['answer','answer_id'], 'answer_unset_best'=>['answer','answer_id'], 'answer_status'=>['answer','answer_id'], 'answer_delete'=>['answer','answer_id'],
        'comment_status'=>['comment','comment_id'], 'comment_delete'=>['comment','comment_id'],
        'category_save'=>['category','id'], 'category_delete'=>['category','category_id'],
        'tag_save'=>['tag','id'], 'tag_delete'=>['tag','tag_id'], 'tag_merge'=>['tag','source_tag_id'],
        'user_create'=>['user',null], 'user_update'=>['user','user_id'], 'user_status'=>['user','user_id'], 'user_verify'=>['user','user_id'], 'user_delete'=>['user','user_id'],
        'report_update'=>['report','report_id'], 'contact_message_update'=>['contact_message','message_id'],
        'notification_read'=>['notification','notification_id'], 'notifications_mark_all_read'=>['notification',null], 'language_save'=>['language',null], 'language_toggle'=>['language',null], 'language_delete'=>['language',null],
        'translation_override_save'=>['translation',null], 'translation_override_delete'=>['translation',null], 'translation_pack_import'=>['translation_pack',null],
        'layout_save'=>['layout','id'], 'layout_delete'=>['layout','layout_id'], 'section_save'=>['layout_section','id'], 'section_delete'=>['layout_section','section_id'],
        'widget_save'=>['widget','id'], 'widget_translation_save'=>['widget','widget_id'], 'widget_toggle'=>['widget','widget_id'], 'widget_delete'=>['widget','widget_id'], 'widget_reorder'=>['widget',null], 'theme_import'=>['appearance',null], 'theme_profile_create'=>['theme_profile',null], 'theme_profile_duplicate'=>['theme_profile','profile_id'], 'theme_profile_apply'=>['theme_profile','profile_id'], 'theme_profile_update'=>['theme_profile','profile_id'], 'theme_profile_restore'=>['theme_profile','profile_id'], 'theme_profile_import'=>['theme_profile',null], 'theme_profile_delete'=>['theme_profile','profile_id'], 'appearance_settings_save'=>['appearance',null], 'appearance_settings_reset'=>['appearance',null], 'ads_settings_save'=>['advertising',null], 'ad_creative_save'=>['ad_creative','id'], 'ad_creative_delete'=>['ad_creative','id'], 'ad_creative_toggle'=>['ad_creative','id'],
        'menu_item_save'=>['menu_item','id'], 'menu_item_delete'=>['menu_item','id'], 'menu_reorder'=>['menu_item',null],
        'custom_code_save'=>['custom_code','id'], 'custom_code_delete'=>['custom_code','id'], 'custom_code_toggle'=>['custom_code','id'],
        'plugin_toggle'=>['plugin',null],
    ];
    if (!isset($map[$action])) return ['type'=>'settings','id'=>null];
    [$type,$key]=$map[$action];
    $id=$key===null?null:max(0,(int)($input[$key]??0));
    return ['type'=>$type,'id'=>$id>0?$id:null];
}

/** @param array<string,mixed> $input @return array<string,mixed> */
function admin_audit_payload(array $input): array
{
    $blocked=['_csrf','password','password_hash','telegram_bot_token','telegram_chat_id','api_key','token','secret','language_pack_json','admin_action'];
    $safe=[];
    foreach($input as $key=>$value){
        if(!is_string($key)||in_array(strtolower($key),$blocked,true)||str_contains(strtolower($key),'password')||str_contains(strtolower($key),'token'))continue;
        if(is_scalar($value)||$value===null){
            $text=(string)$value;
            $safe[$key]=admin_unicode_length($text)>1000?admin_unicode_substr($text,0,1000).'...':$text;
        } elseif(is_array($value)) {
            $safe[$key]=array_slice(array_values(array_filter(array_map(static fn($v)=>is_scalar($v)?(string)$v:null,$value),static fn($v)=>$v!==null)),0,50);
        }
    }
    return $safe;
}

function admin_is_moderation_action(string $action): bool
{
    return in_array($action,['question_moderate','topic_moderate','answer_best','answer_status','answer_delete','comment_status','comment_delete','report_update','user_status','user_delete','tag_merge'],true);
}

/** @return array<string,mixed> */
function admin_theme_snapshot(SettingsRepository $settings, array $site): array
{
    $defaults = ThemeAppearanceService::defaults((array) ($site['theme'] ?? []));
    $snapshot = ['active' => (string) $settings->get('theme.active', 'default')];
    foreach ($defaults as $key => $default) {
        $snapshot[$key] = $settings->get('theme.' . $key, $default);
    }
    return $snapshot;
}

/**
 * Guard failures that must explain themselves.
 *
 * Everything else is deliberately reduced to a generic message so internal
 * detail never reaches the panel, but these are administrator decisions with a
 * specific remedy: telling someone "the operation failed" when the real
 * problem is "another language still falls back to this one" leaves them with
 * nowhere to go.
 */
function admin_local_error(Translator $translator, Throwable $throwable): string
{
    if ($throwable instanceof TranslatableException) {
        return $translator->get($throwable->translationKey, $throwable->replace);
    }
    $message = strtolower($throwable->getMessage());

    $explained = [
        'placement already has a running advertisement' => 'admin.ads_validation_capacity',
        'needs either code or an image' => 'admin.ads_validation_content',
        'a default language cannot be disabled' => 'admin.language_error_default',
        'still used as a fallback' => 'admin.language_error_fallback',
        'fallback language must be enabled' => 'admin.language_error_fallback_disabled',
        'cannot fall back to itself' => 'admin.language_error_self_fallback',
    ];

    foreach ($explained as $needle => $key) {
        if (str_contains($message, $needle)) {
            // Only the translated text is returned. The exception message is
            // never echoed, even in part: the languages table already shows
            // each language's fallback, so the administrator can see which
            // ones depend on this one without leaking internal strings.
            return $translator->get($key);
        }
    }

    foreach ([
        'required' => 'admin.validation_required',
        'cannot' => 'admin.validation_invalid',
        'invalid' => 'admin.validation_invalid',
        'unknown' => 'admin.validation_invalid',
        'only administrators' => 'admin.admin_only',
        'not found' => 'admin.not_found',
    ] as $needle => $key) {
        if (str_contains($message, $needle)) {
            return $translator->get($key);
        }
    }
    return $translator->get('admin.operation_failed');
}

/** @param array<string, int> $stats */
function admin_render_overview(array $stats, AdminRepository $repository, callable $t, callable $e, string $csrfToken, array $dashboardSnapshot = []): void
{
    $cards = [
        'users' => 'admin.stats.users', 'questions' => 'admin.stats.questions', 'answers' => 'admin.stats.answers', 'published_questions' => 'admin.stats.published',
        'pending_questions' => 'admin.stats.pending', 'open_reports' => 'admin.stats.reports', 'categories' => 'admin.stats.categories', 'tags' => 'admin.stats.tags',
    ];
    echo '<div class="admin-grid">';
    foreach ($cards as $key => $label) {
        echo '<div class="admin-stat"><strong>' . $e($stats[$key] ?? 0) . '</strong><span>' . $t($label) . '</span></div>';
    }
    echo '</div>';

    $attention = is_array($dashboardSnapshot['attention'] ?? null) ? $dashboardSnapshot['attention'] : [];
    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>Needs attention</h2><p class="admin-muted">Operational work that should be reviewed first.</p></div></div><div class="admin-grid admin-attention-grid">';
    foreach ([['pending_content','Pending moderation','questions'],['open_reports','Open reports','reports'],['unanswered','Unanswered questions','questions'],['new_members','New real members (7d)','users']] as [$key,$label,$target]) echo '<a class="admin-stat admin-stat-link" href="' . $e(admin_url($target)) . '"><strong>' . $e($attention[$key] ?? 0) . '</strong><span>' . $e($label) . '</span></a>';
    echo '</div></section>';
    $health = is_array($dashboardSnapshot['health'] ?? null) ? $dashboardSnapshot['health'] : [];
    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>System health</h2><p class="admin-muted">Runtime requirements and configured integrations.</p></div><a class="admin-link" href="' . $e(admin_url('system')) . '">System</a></div><div class="system-health-grid">';
    foreach ($health as $item) { $status=(string)($item['status']??'info'); echo '<div class="system-health-item"><span class="health-dot health-' . $e($status) . '" aria-hidden="true"></span><div><strong>' . $e($item['label']??'') . '</strong><small>' . $e($item['detail']??'') . '</small></div></div>'; }
    echo '</div></section>';

    $queue = $repository->actionQueue(5);
    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.queue_title') . '</h2><p class="admin-muted">' . $t('admin.overview_hint') . '</p></div><a class="admin-link" href="' . $e(admin_url('questions', ['status' => 'pending'])) . '">' . $t('admin.view_queue') . '</a></div>';
    echo '<div class="admin-action-queue-grid">';

    // Col 1: Pending Questions
    echo '<div class="admin-action-queue-col"><div class="admin-action-queue-col-head"><h3>' . $t('admin.queue_pending_questions') . ' (' . count($queue['pending_questions']) . ')</h3><a class="admin-link" href="' . $e(admin_url('questions', ['status' => 'pending'])) . '">' . $t('admin.view_all') . '</a></div>';
    if ($queue['pending_questions'] === []) {
        echo '<p class="admin-muted">' . $t('admin.queue_empty_questions') . '</p>';
    } else {
        echo '<div class="admin-action-queue-list">';
        foreach ($queue['pending_questions'] as $q) {
            $qid = (int) $q['id'];
            echo '<div class="admin-action-queue-item"><strong dir="auto">' . $e($q['title']) . '</strong><small>' . $e($q['author_name']) . ' · ' . $e($q['category_name']) . ' · ' . $e($q['created_at']) . '</small><div class="admin-actions">';
            admin_action_form('question_moderate', 'publish', $qid, $csrfToken, $t('admin.actions.publish'), 'admin-button-small');
            echo '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('questions', ['status' => 'pending', 'review_question' => $qid])) . '">' . $t('admin.actions.review') . '</a>';
            echo '</div></div>';
        }
        echo '</div>';
    }
    echo '</div>';

    // Col 2: Open Reports
    echo '<div class="admin-action-queue-col"><div class="admin-action-queue-col-head"><h3>' . $t('admin.queue_open_reports') . ' (' . count($queue['open_reports']) . ')</h3><a class="admin-link" href="' . $e(admin_url('reports')) . '">' . $t('admin.view_all') . '</a></div>';
    if ($queue['open_reports'] === []) {
        echo '<p class="admin-muted">' . $t('admin.queue_empty_reports') . '</p>';
    } else {
        echo '<div class="admin-action-queue-list">';
        foreach ($queue['open_reports'] as $rep) {
            $rid = (int) $rep['id'];
            echo '<div class="admin-action-queue-item"><strong>' . $e($t('admin.report_types.' . $rep['target_type'])) . ' — ' . $e($t('questions.report_reasons.' . $rep['reason'])) . '</strong><small>' . $e($rep['reporter_name']) . ' · ' . $e($rep['created_at']) . '</small><div class="admin-actions"><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('reports', ['edit_report' => $rid])) . '">' . $t('admin.actions.review') . '</a></div></div>';
        }
        echo '</div>';
    }
    echo '</div>';

    // Col 3: Contact Messages
    echo '<div class="admin-action-queue-col"><div class="admin-action-queue-col-head"><h3>' . $t('admin.queue_contact_messages') . ' (' . count($queue['open_messages']) . ')</h3><a class="admin-link" href="' . $e(admin_url('messages')) . '">' . $t('admin.view_all') . '</a></div>';
    if ($queue['open_messages'] === []) {
        echo '<p class="admin-muted">' . $t('admin.queue_empty_messages') . '</p>';
    } else {
        echo '<div class="admin-action-queue-list">';
        foreach ($queue['open_messages'] as $msg) {
            $mid = (int) $msg['id'];
            echo '<div class="admin-action-queue-item"><strong dir="auto">' . $e($msg['subject'] ?: $t('admin.no_subject')) . '</strong><small>' . $e($msg['name']) . ' · ' . $e($msg['created_at']) . '</small><div class="admin-actions"><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('messages', ['edit_message' => $mid])) . '">' . $t('admin.actions.review') . '</a></div></div>';
        }
        echo '</div>';
    }
    echo '</div>';

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

function admin_render_questions(AdminRepository $repository, Translator $translator, callable $t, callable $e, string $csrfToken): void
{
    $status = (string) ($_GET['status'] ?? '');
    $search = trim((string) ($_GET['q'] ?? ''));
    $categoryId = max(0, (int) ($_GET['category_id'] ?? 0));
    $sort = (string) ($_GET['sort'] ?? 'newest');
    $sortOptions = ['newest', 'oldest', 'answers', 'votes', 'views', 'title'];
    if (!in_array($sort, $sortOptions, true)) $sort = 'newest';
    $filters = ['status' => $status, 'search' => $search, 'category_id' => $categoryId, 'sort' => $sort];
    $total = $repository->countQuestions($filters);
    $pagination = AdminPagination::fromQuery($_GET, $total, 25);
    $questions = $repository->paginatedQuestions($filters, $pagination->page, $pagination->perPage);
    $categories = $repository->categories();
    $filterCount = (int) ($status !== '' ? 1 : 0) + (int) ($search !== '' ? 1 : 0) + (int) ($categoryId > 0 ? 1 : 0) + (int) ($sort !== 'newest' ? 1 : 0) + (int) ($pagination->perPage !== 25 ? 1 : 0);
    $rangeStart = $total > 0 ? (($pagination->page - 1) * $pagination->perPage) + 1 : 0;
    $rangeEnd = $total > 0 ? min($total, $pagination->page * $pagination->perPage) : 0;
    $reviewQuestionId = max(0, (int) ($_GET['review_question'] ?? 0));
    $users = $repository->users();

    echo '<section class="admin-card admin-questions-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.questions') . '</h2><p class="admin-muted">' . $t('admin.questions_management_note') . '</p></div><span class="admin-count" aria-label="' . $t('admin.questions_total') . '">' . $e($total) . '</span></div>';

    echo '<details class="admin-card admin-create-question"><summary class="admin-button admin-button-secondary">' . $t('admin.create_question_title') . '</summary><div class="admin-card-body"><form method="post" action="' . $e(admin_url('questions')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="question_create"><input type="hidden" name="page" value="questions"><div class="admin-form-grid">';
    echo '<label class="admin-field"><span>' . $t('admin.fields.user') . '</span><select name="user_id">';
    foreach ($users as $u) {
        $uid = (int) ($u['id'] ?? 0);
        if ($uid <= 0) continue;
        echo '<option value="' . $e($uid) . '">' . $e((string) ($u['name'] ?: $u['username'])) . ' (' . $e((string) ($u['email'] ?? '')) . ')</option>';
    }
    echo '</select></label>';
    echo '<label class="admin-field"><span>' . $t('admin.fields.category') . '</span><select name="category_id"><option value="">' . $t('questions.no_category') . '</option>';
    foreach ($categories as $cat) {
        $cid = (int) ($cat['id'] ?? 0);
        if ($cid <= 0) continue;
        echo '<option value="' . $e($cid) . '">' . $e((string) ($cat['name'] ?? '')) . '</option>';
    }
    echo '</select></label>';
    echo '<label class="admin-field"><span>' . $t('admin.fields.initial_status') . '</span><select name="status"><option value="published">' . $t('admin.status.published') . '</option><option value="pending">' . $t('admin.status.pending') . '</option><option value="closed">' . $t('admin.status.closed') . '</option></select></label>';
    echo '<label class="admin-field admin-field-full"><span>' . $t('questions.title') . '</span><input name="title" required maxlength="255" placeholder="' . $t('questions.title_placeholder') . '"></label>';
    echo '<label class="admin-field admin-field-full"><span>' . $t('questions.body') . '</span><textarea name="body" required rows="5" placeholder="' . $t('questions.body_placeholder') . '"></textarea></label>';
    echo '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></details>';

    echo '<div class="admin-question-filters"><form method="get" action="' . $e(admin_url('questions')) . '" class="admin-question-filter-form"><input type="hidden" name="page" value="questions"><div class="admin-question-filter-grid">';
    echo '<label class="admin-field admin-question-search"><span>' . $t('admin.filter_search') . '</span><input type="search" name="q" value="' . $e($search) . '" placeholder="' . $t('admin.questions_search_placeholder') . '"></label>';
    echo '<label class="admin-field"><span>' . $t('admin.filter_status') . '</span><select name="status"><option value="">' . $t('admin.all_statuses') . '</option>';
    foreach (['pending', 'published', 'rejected', 'closed', 'duplicate'] as $option) echo '<option value="' . $e($option) . '"' . ($status === $option ? ' selected' : '') . '>' . $e($translator->get('admin.status.' . $option)) . '</option>';
    echo '</select></label>';
    echo '<label class="admin-field"><span>' . $t('admin.filter_category') . '</span><select name="category_id"><option value="">' . $t('admin.all_categories') . '</option>';
    foreach ($categories as $category) { $id = (int) ($category['id'] ?? 0); if ($id <= 0) continue; echo '<option value="' . $e($id) . '"' . ($categoryId === $id ? ' selected' : '') . '>' . $e((string) ($category['name'] ?? '')) . '</option>'; }
    echo '</select></label>';
    echo '<label class="admin-field"><span>' . $t('admin.filter_sort') . '</span><select name="sort">';
    foreach ($sortOptions as $option) echo '<option value="' . $e($option) . '"' . ($sort === $option ? ' selected' : '') . '>' . $t('admin.question_sort.' . $option) . '</option>';
    echo '</select></label>';
    echo '<label class="admin-field"><span>' . $t('admin.filter_page_size') . '</span><select name="per_page">';
    foreach ([25, 50, 100] as $pageSize) echo '<option value="' . $e($pageSize) . '"' . ($pagination->perPage === $pageSize ? ' selected' : '') . '>' . $e($pageSize) . '</option>';
    echo '</select></label></div><div class="admin-question-filter-actions"><button type="submit">' . $t('admin.filter') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('questions')) . '">' . $t('admin.actions.reset_filters') . '</a><span class="admin-filter-count">' . $t('admin.questions_active_filters', ['count' => $filterCount]) . '</span></div></form></div>';
    echo '<div class="admin-pagination-row"><p class="admin-pagination-summary">' . $t('admin.pagination.summary', ['from' => $rangeStart, 'to' => $rangeEnd, 'total' => $total]) . '</p>';
    admin_pagination_controls($pagination, $e, $t);
    echo '</div><div class="admin-table-wrap"><table class="admin-table admin-questions-table"><thead><tr><th>' . $t('admin.table.question') . '</th><th>' . $t('admin.table.status') . '</th><th>' . $t('admin.table.metrics') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    if ($questions === []) echo '<tr><td colspan="4" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    foreach ($questions as $question) {
        $id = (int) $question['id'];
        $isReviewingThis = ($reviewQuestionId === $id);
        echo '<tr data-question-id="' . $e($id) . '"><td><div class="admin-question-title"><strong>' . $e($question['title']) . '</strong><small>' . $e($question['author_name']) . ' · ' . $e($question['category_name']) . ' · ' . $e($question['content_locale']) . '</small></div>';
        if (!empty($question['slug'])) echo '<small class="admin-code">/' . $e('q/' . $question['slug']) . '</small>';
        if ($question['status'] === 'pending' || $isReviewingThis) {
            echo '<details class="admin-question-review"' . ($isReviewingThis ? ' open' : '') . '><summary>' . $t('admin.actions.review') . '</summary><div class="admin-question-review-body" dir="auto">' . nl2br($e((string) ($question['body'] ?? ''))) . '</div></details>';
        }
        echo '<details class="admin-seo-editor"><summary>' . $t('admin.actions.seo') . '</summary><form method="post" action="' . $e(admin_url('questions')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="question_seo_save"><input type="hidden" name="page" value="questions"><input type="hidden" name="question_id" value="' . $e($id) . '"><label class="admin-field"><span>' . $t('admin.fields.meta_title') . '</span><input name="meta_title" maxlength="255" value="' . $e($question['meta_title'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.meta_description') . '</span><textarea name="meta_description" maxlength="191">' . $e($question['meta_description'] ?? '') . '</textarea></label><label class="admin-field"><span>' . $t('admin.fields.canonical_url') . '</span><input name="canonical_url" maxlength="500" value="' . $e($question['canonical_url'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.seo_index') . '</span><select name="seo_index"><option value="inherit"' . (($question['seo_index'] ?? 'inherit') === 'inherit' ? ' selected' : '') . '>' . $t('admin.seo_index_inherit') . '</option><option value="index"' . (($question['seo_index'] ?? '') === 'index' ? ' selected' : '') . '>' . $t('admin.seo_index') . '</option><option value="noindex"' . (($question['seo_index'] ?? '') === 'noindex' ? ' selected' : '') . '>' . $t('admin.seo_noindex') . '</option></select></label><button class="admin-button-small" type="submit">' . $t('admin.actions.save') . '</button></form></details>';
        echo '</td><td><span class="admin-badge admin-question-status">' . $e($translator->get('admin.status.' . $question['status'])) . '</span></td><td><div class="admin-question-metrics"><span>' . $e($question['answers_count']) . ' ' . $t('common.answers') . '</span><span>' . $e($question['votes_count']) . ' ' . $t('common.votes') . '</span><span>' . $e($question['views_count']) . ' ' . $t('common.views') . '</span></div></td><td><div class="admin-actions">';
        if ($question['status'] === 'pending') {
            admin_action_form('question_moderate', 'publish', $id, $csrfToken, $t('admin.actions.publish'), 'admin-button-small');
            echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('questions')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="question_moderate"><input type="hidden" name="page" value="questions"><input type="hidden" name="moderation" value="reject"><input type="hidden" name="question_id" value="' . $e($id) . '"><input name="reason" required maxlength="255" placeholder="' . $t('admin.fields.rejection_reason') . '"><button class="admin-button-small admin-button-danger" type="submit">' . $t('admin.actions.reject') . '</button></form>';
        } elseif ($question['status'] === 'published') {
            admin_action_form('question_moderate', 'close', $id, $csrfToken, $t('admin.actions.close'), 'admin-button-small admin-button-secondary');
            echo '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_public_url('q/' . rawurlencode((string) $question['slug']))) . '">' . $t('admin.actions.view') . '</a>';
        } elseif (in_array($question['status'], ['rejected', 'closed', 'duplicate'], true)) {
            admin_action_form('question_moderate', 'reopen', $id, $csrfToken, $t('admin.actions.reopen'), 'admin-button-small admin-button-secondary');
        }
        admin_action_form('question_delete', '', $id, $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger admin-confirm-delete');
        echo '</div></td></tr>';
    }
    echo '</tbody></table></div></section>';
}

function admin_render_topics(PDO $pdo, array $languages, Translator $translator, callable $t, callable $e, string $csrfToken, bool $isModerator): void
{
    $status = trim((string) ($_GET['status'] ?? ''));
    $kind = trim((string) ($_GET['kind'] ?? ''));
    $locale = trim((string) ($_GET['locale'] ?? ''));
    $search = trim((string) ($_GET['q'] ?? ''));
    $categoryId = max(0, (int) ($_GET['category_id'] ?? 0));
    $filters = ['status' => $status !== '' ? $status : 'all', 'kind' => $kind, 'locale' => $locale, 'search' => $search, 'category_id' => $categoryId];
    $topicRepository = new TopicRepository($pdo);
    // listing() runs its own COUNT and clamps the page, so one call answers
    // both questions. Asking twice -- once only to read the total for the
    // paginator -- doubled every query on a page that renders on each visit.
    $requestedPage = max(1, (int) ($_GET['admin_page'] ?? 1));
    $requestedPerPage = max(10, min(100, (int) ($_GET['per_page'] ?? 25)));
    $listing = $topicRepository->listing($filters, $requestedPage, $requestedPerPage);
    $topicPagination = AdminPagination::fromQuery($_GET, (int) ($listing['total'] ?? 0), 25);
    $categories = (new AdminRepository($pdo))->categories();
    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.topics') . '</h2><p class="admin-muted">' . $t('admin.topics_management_note') . '</p></div><span class="admin-count">' . $e($listing['total']) . '</span></div>';
    if ($isModerator) {
        echo '<details class="admin-card admin-create-question"><summary class="admin-button admin-button-secondary">' . $t('admin.create_topic_title') . '</summary><div class="admin-card-body"><form method="post" action="' . $e(admin_url('topics')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="topic_create"><input type="hidden" name="page" value="topics"><div class="admin-form-grid">';
        // Only the create form needs the author list, and only a moderator sees
        // it. Loading it unconditionally spent a 200-row user query on every
        // visitor to this page, including those who cannot post.
        $users = (new AdminRepository($pdo))->users();
        echo '<label class="admin-field"><span>' . $t('admin.fields.user') . '</span><select name="user_id">'; foreach ($users as $u) { $uid = (int) ($u['id'] ?? 0); if ($uid > 0) echo '<option value="' . $uid . '">' . $e((string) ($u['name'] ?? $u['username'] ?? '')) . '</option>'; } echo '</select></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.category') . '</span><select name="category_id"><option value="0">' . $t('topics.all_categories') . '</option>'; foreach ($categories as $cat) { echo '<option value="' . (int) ($cat['id'] ?? 0) . '">' . $e((string) ($cat['name'] ?? '')) . '</option>'; } echo '</select></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.content_locale') . '</span><select name="content_locale">'; foreach ($languages as $language) { $code = (string) ($language['code'] ?? ''); echo '<option value="' . $e($code) . '">' . $e((string) ($language['native_name'] ?? $code)) . '</option>'; } echo '</select></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.topic_kind') . '</span><select name="kind"><option value="discussion">' . $t('topics.discussion') . '</option><option value="announcement">' . $t('topics.announcement') . '</option></select></label><label class="admin-field"><span>' . $t('admin.fields.initial_status') . '</span><select name="status"><option value="published">' . $t('admin.status.published') . '</option><option value="pending">' . $t('admin.status.pending') . '</option><option value="hidden">' . $t('admin.status.hidden') . '</option></select></label>';
        echo '<label class="admin-field admin-field-full"><span>' . $t('topics.title_label') . '</span><input name="title" required maxlength="255"></label><label class="admin-field admin-field-full"><span>' . $t('topics.body_label') . '</span><textarea name="body" required rows="5"></textarea></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></details>';
    }
    echo '<div class="admin-question-filters"><form method="get" action="' . $e(admin_url('topics')) . '" class="admin-question-filter-form"><input type="hidden" name="page" value="topics"><div class="admin-question-filter-grid"><label class="admin-field admin-question-search"><span>' . $t('admin.filter_search') . '</span><input type="search" name="q" value="' . $e($search) . '"></label><label class="admin-field"><span>' . $t('admin.filter_topic_status') . '</span><select name="status"><option value="">' . $t('admin.all_statuses') . '</option>'; foreach (['published','pending','hidden','draft'] as $option) echo '<option value="' . $option . '"' . ($status === $option ? ' selected' : '') . '>' . $e($translator->get('admin.status.' . $option)) . '</option>'; echo '</select></label><label class="admin-field"><span>' . $t('admin.filter_category') . '</span><select name="category_id"><option value="0">' . $t('admin.all_categories') . '</option>'; foreach ($categories as $cat) { $id = (int) ($cat['id'] ?? 0); echo '<option value="' . $id . '"' . ($categoryId === $id ? ' selected' : '') . '>' . $e((string) ($cat['name'] ?? '')) . '</option>'; } echo '</select></label><label class="admin-field"><span>' . $t('admin.fields.topic_kind') . '</span><select name="kind"><option value="">' . $t('topics.all_kinds') . '</option><option value="discussion"' . ($kind === 'discussion' ? ' selected' : '') . '>' . $t('topics.discussion') . '</option><option value="announcement"' . ($kind === 'announcement' ? ' selected' : '') . '>' . $t('topics.announcement') . '</option></select></label><label class="admin-field"><span>' . $t('admin.fields.content_locale') . '</span><select name="locale"><option value="">' . $t('topics.all_locales') . '</option>'; foreach ($languages as $language) { $code = (string) ($language['code'] ?? ''); echo '<option value="' . $e($code) . '"' . ($locale === $code ? ' selected' : '') . '>' . $e((string) ($language['native_name'] ?? $code)) . '</option>'; } echo '</select></label></div><div class="admin-question-filter-actions"><button type="submit">' . $t('admin.filter') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('topics')) . '">' . $t('admin.actions.reset_filters') . '</a></div></form></div>';
    echo '<div class="admin-pagination-row"><p class="admin-pagination-summary">' . $t('admin.pagination.summary', ['from' => (int) ($listing['total'] ?? 0) > 0 ? (($topicPagination->page - 1) * $topicPagination->perPage + 1) : 0, 'to' => min((int) ($listing['total'] ?? 0), $topicPagination->page * $topicPagination->perPage), 'total' => (int) ($listing['total'] ?? 0)]) . '</p>';
    admin_pagination_controls($topicPagination, $e, $t);
    echo '</div><div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>' . $t('admin.fields.topic_title') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.table.metrics') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    if (($listing['items'] ?? []) === []) echo '<tr><td colspan="4" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    foreach ($listing['items'] as $topic) {
        $id = (int) ($topic['id'] ?? 0); $topicSlug = (string) ($topic['slug'] ?? ''); $topicStatus = (string) ($topic['status'] ?? 'published');
        echo '<tr><td><strong>' . $e($topic['title'] ?? '') . '</strong><small>' . $e($topic['author_name'] ?? '') . ' · ' . $e($topic['category_name'] ?? '') . ' · ' . $e($topic['content_locale'] ?? '') . '</small><small class="admin-code">/d/' . $e($topicSlug) . '</small>';
        if ($isModerator) echo '<details><summary>' . $t('admin.actions.edit') . '</summary><form method="post" action="' . $e(admin_url('topics')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="topic_update"><input type="hidden" name="page" value="topics"><input type="hidden" name="topic_id" value="' . $id . '"><input type="hidden" name="category_id" value="' . (int) ($topic['category_id'] ?? 0) . '"><input type="hidden" name="status" value="' . $e($topic['status'] ?? 'published') . '"><label class="admin-field"><span>' . $t('topics.title_label') . '</span><input name="title" required maxlength="255" value="' . $e($topic['title'] ?? '') . '"></label><label class="admin-field"><span>' . $t('topics.body_label') . '</span><textarea name="body" rows="4" required>' . $e($topic['body'] ?? '') . '</textarea></label><label class="admin-field"><span>' . $t('admin.fields.topic_kind') . '</span><select name="kind"><option value="discussion"' . (($topic['kind'] ?? '') === 'discussion' ? ' selected' : '') . '>' . $t('topics.discussion') . '</option><option value="announcement"' . (($topic['kind'] ?? '') === 'announcement' ? ' selected' : '') . '>' . $t('topics.announcement') . '</option></select></label><input type="hidden" name="content_locale" value="' . $e($topic['content_locale'] ?? '') . '"><button class="admin-button-small" type="submit">' . $t('admin.actions.save') . '</button></form></details>';
        echo '</td><td><span class="admin-badge">' . $e($translator->get('admin.status.' . $topicStatus)) . '</span>' . ((int) ($topic['is_pinned'] ?? 0) === 1 ? ' <span class="admin-badge">' . $t('topics.pinned') . '</span>' : '') . ((int) ($topic['is_locked'] ?? 0) === 1 ? ' <span class="admin-badge">' . $t('topics.locked') . '</span>' : '') . '</td><td>' . $e((int) ($topic['comments_count'] ?? 0)) . ' ' . $t('topics.comments') . ' · ' . $e((int) ($topic['views_count'] ?? 0)) . ' ' . $t('common.views') . '</td><td><div class="admin-actions">';
        if ($topicStatus !== 'published') admin_action_form('topic_moderate', 'publish', $id, $csrfToken, $t('admin.actions.publish'), 'admin-button-small', 'topic_id'); else admin_action_form('topic_moderate', 'hide', $id, $csrfToken, $t('admin.actions.hide'), 'admin-button-small admin-button-secondary', 'topic_id');
        admin_action_form('topic_moderate', (int) ($topic['is_pinned'] ?? 0) === 1 ? 'unpin' : 'pin', $id, $csrfToken, (int) ($topic['is_pinned'] ?? 0) === 1 ? $t('admin.actions.unpin') : $t('admin.actions.pin'), 'admin-button-small admin-button-secondary', 'topic_id');
        admin_action_form('topic_moderate', (int) ($topic['is_locked'] ?? 0) === 1 ? 'unlock' : 'lock', $id, $csrfToken, (int) ($topic['is_locked'] ?? 0) === 1 ? $t('admin.actions.unlock') : $t('admin.actions.lock'), 'admin-button-small admin-button-secondary', 'topic_id');
        if ($topicSlug !== '') echo '<a class="admin-button admin-button-small admin-button-secondary" target="_blank" rel="noopener" href="' . $e(admin_public_url('d/' . rawurlencode($topicSlug))) . '">' . $t('admin.actions.view') . '</a>';
        admin_action_form('topic_moderate', 'delete', $id, $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger admin-confirm-delete', 'topic_id');
        echo '</div></td></tr>';
    }
    echo '</tbody></table></div></section>';
}

function admin_render_answers(AdminRepository $repository, callable $t, callable $e, string $csrfToken): void
{
    $total = $repository->countRows('answers');
    $answers = admin_pagination($repository->answers(), $total, $e);
    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.answers') . '</h2><p class="admin-muted">' . $t('admin.answers_management_note') . '</p></div><span class="admin-count">' . $e($total) . '</span></div><div class="admin-table-wrap"><table class="admin-table admin-answers-table"><thead><tr><th>' . $t('admin.table.answer') . '</th><th>' . $t('admin.table.question') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    if ($answers === []) echo '<tr><td colspan="3" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    foreach ($answers as $answer) {
        $answerBody = (string) $answer['body'];
        $answerPreview = function_exists('mb_strimwidth')
            ? mb_strimwidth($answerBody, 0, 180, '...', 'UTF-8')
            : (admin_unicode_length($answerBody) > 180 ? admin_unicode_substr($answerBody, 0, 177) . '...' : $answerBody);
        $answerStatus = (string) ($answer['status'] ?? 'published');
        echo '<tr><td><strong>' . $e($answerPreview) . '</strong><small>' . $e($answer['author_name']) . ' · ' . $e($answer['created_at']) . ' · <span class="admin-badge">' . $e($answerStatus) . '</span></small></td><td>' . $e($answer['question_title']) . '<small>' . $e($answer['votes_count']) . ' ' . $t('common.votes') . ($answer['is_best'] ? ' · ' . $t('questions.best_answer') : '') . '</small></td><td><div class="admin-actions">';
        if ($answerStatus === 'published') {
            if (!$answer['is_best']) admin_action_form('answer_best', '', (int) $answer['id'], $csrfToken, $t('admin.actions.accept'), 'admin-button-small', 'answer_id');
            else admin_action_form('answer_unset_best', '', (int) $answer['id'], $csrfToken, $t('admin.actions.unset_best'), 'admin-button-small admin-button-secondary', 'answer_id');
            echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('answers')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="answer_status"><input type="hidden" name="page" value="answers"><input type="hidden" name="answer_id" value="' . (int) $answer['id'] . '"><input type="hidden" name="status" value="pending"><button class="admin-button-small admin-button-secondary" type="submit">' . $t('admin.actions.hide') . '</button></form>';
        } else {
            echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('answers')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="answer_status"><input type="hidden" name="page" value="answers"><input type="hidden" name="answer_id" value="' . (int) $answer['id'] . '"><input type="hidden" name="status" value="published"><button class="admin-button-small" type="submit">' . $t('admin.actions.publish') . '</button></form>';
        }
        admin_action_form('answer_delete', '', (int) $answer['id'], $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger', 'answer_id');
        echo '</div></td></tr>';
    }
    echo '</tbody></table></div></section>';
}

function admin_render_comments(AdminRepository $repository, callable $t, callable $e, string $csrfToken): void
{
    try { $comments = admin_pagination($repository->comments(), $repository->countRows('comments'), $e); } catch (Throwable $throwable) { (new FileLogger())->error('Admin comments query failed.', $throwable); $comments = null; }
    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.comments') . '</h2><p class="admin-muted">' . $t('admin.comments_note') . '</p></div></div><div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>' . $t('admin.fields.content') . '</th><th>' . $t('admin.fields.author') . '</th><th>' . $t('admin.fields.target') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    if ($comments === null) echo '<tr><td colspan="5" class="admin-alert admin-alert-error">' . $t('admin.list_error') . '</td></tr>';
    foreach ($comments ?? [] as $comment) {
        $id=(int)($comment['id']??0); $status=(string)($comment['status']??'published'); $slug=(string)($comment['question_slug']??''); $commentType=(string)($comment['commentable_type']??'');
        echo '<tr><td><div class="admin-cell-copy" dir="auto">' . $e($comment['body'] ?? '') . '</div><small>' . $e($comment['created_at'] ?? '') . '</small></td><td>' . $e($comment['author_name'] ?? '') . '</td><td><strong>' . $e($comment['question_title'] ?? '') . '</strong><small>' . $e($commentType === 'topic' ? $t('admin.report_types.topic') : $commentType) . ' #' . $e($comment['commentable_id'] ?? '') . '</small></td><td><span class="admin-badge' . ($status === 'hidden' ? ' admin-badge-danger' : '') . '">' . $e($status) . '</span></td><td><div class="admin-actions">';
        if ($slug !== '') echo '<a class="admin-button admin-button-small admin-button-secondary" target="_blank" rel="noopener" href="' . $e(quaeriva_url(($commentType === 'topic' ? 'd/' : 'q/') . rawurlencode($slug))) . '">' . $t('admin.actions.view') . '</a>';
        $nextStatus = $status === 'published' ? 'hidden' : 'published';
        $nextAction = $status === 'published' ? 'admin.actions.hide' : 'admin.actions.publish';
        echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('comments')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="comment_status"><input type="hidden" name="page" value="comments"><input type="hidden" name="comment_id" value="' . $id . '"><input type="hidden" name="status" value="' . $nextStatus . '"><button class="admin-button admin-button-small admin-button-secondary" type="submit">' . $t($nextAction) . '</button></form>';
        admin_action_form('comment_delete','',$id,$csrfToken,$t('admin.actions.delete'),'admin-button-small admin-button-danger admin-confirm-delete','comment_id');
        echo '</div></td></tr>';
    }
    if ($comments === []) echo '<tr><td colspan="5" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';
}

function admin_render_categories(AdminRepository $repository, array $languages, callable $t, callable $e, string $csrfToken): void
{
    $categories = admin_pagination($repository->categories(), $repository->countRows('categories'), $e);
    $categoryMap = [];
    $children = [];
    foreach ($categories as $row) {
        $id = (int) ($row['id'] ?? 0);
        if ($id <= 0) continue;
        $categoryMap[$id] = $row;
        $parentId = (int) ($row['parent_id'] ?? 0);
        if ($parentId > 0) $children[$parentId][] = $id;
    }

    $orderedCategories = [];
    $treeVisited = [];
    $walkCategories = static function (int $parentId, int $depth = 0) use (&$walkCategories, &$children, &$categoryMap, &$orderedCategories, &$treeVisited): void {
        foreach ($children[$parentId] ?? [] as $childId) {
            $childId = (int) $childId;
            if ($childId <= 0 || isset($treeVisited[$childId]) || !isset($categoryMap[$childId])) continue;
            $treeVisited[$childId] = true;
            $row = $categoryMap[$childId];
            $row['_admin_depth'] = $depth;
            $orderedCategories[] = $row;
            $walkCategories($childId, $depth + 1);
        }
    };
    // Roots first, then any legacy orphan rows that reference a missing parent.
    $rootIds = [];
    foreach ($categories as $row) {
        $id = (int) ($row['id'] ?? 0);
        $parentId = (int) ($row['parent_id'] ?? 0);
        if ($id > 0 && ($parentId <= 0 || !isset($categoryMap[$parentId]))) $rootIds[] = $id;
    }
    $children[0] = $rootIds;
    $walkCategories(0, 0);
    foreach ($categories as $row) {
        $id = (int) ($row['id'] ?? 0);
        if ($id <= 0 || isset($treeVisited[$id])) continue;
        $row['_admin_depth'] = 0;
        $orderedCategories[] = $row;
    }

    $categoryPath = static function (array $category) use (&$categoryMap): string {
        $parts = [(string) ($category['name'] ?? '')];
        $visited = [];
        $parentId = (int) ($category['parent_id'] ?? 0);
        while ($parentId > 0 && isset($categoryMap[$parentId]) && !isset($visited[$parentId])) {
            $visited[$parentId] = true;
            array_unshift($parts, (string) ($categoryMap[$parentId]['name'] ?? ''));
            $parentId = (int) ($categoryMap[$parentId]['parent_id'] ?? 0);
        }
        return implode(' / ', array_values(array_filter($parts, static fn (string $part): bool => $part !== '')));
    };

    $descendantIds = static function (int $rootId) use (&$children): array {
        $result = [];
        $queue = $children[$rootId] ?? [];
        while ($queue !== []) {
            $id = (int) array_shift($queue);
            if ($id <= 0 || isset($result[$id])) continue;
            $result[$id] = true;
            foreach ($children[$id] ?? [] as $childId) $queue[] = (int) $childId;
        }
        return array_keys($result);
    };

    $languageOptions = static function (string $selected = '') use ($languages, $e): string {
        $html = '';
        foreach ($languages as $language) {
            $code = (string) ($language['code'] ?? '');
            if ($code === '') continue;
            $label = (string) ($language['native_name'] ?? $language['name'] ?? $code);
            $html .= '<option value="' . $e($code) . '"' . ($selected === $code ? ' selected' : '') . '>' . $e($label) . ' (' . $e($code) . ')</option>';
        }
        return $html;
    };

    $editId = max(0, (int) ($_GET['edit_category'] ?? 0));
    $newCategory = isset($_GET['new_category']);
    $editing = $editId > 0 && isset($categoryMap[$editId]) ? $categoryMap[$editId] : null;
    $prefillParentId = max(0, (int) ($_GET['parent_id'] ?? 0));
    $prefillLocale = strtolower(trim((string) ($_GET['content_locale'] ?? '')));
    if ($editing !== null) {
        $prefillParentId = (int) ($editing['parent_id'] ?? 0);
        $prefillLocale = (string) ($editing['content_locale'] ?? '');
    } elseif ($prefillParentId > 0 && isset($categoryMap[$prefillParentId])) {
        $prefillLocale = (string) ($categoryMap[$prefillParentId]['content_locale'] ?? $prefillLocale);
    }
    if ($prefillLocale === '' && isset($languages[0])) $prefillLocale = (string) ($languages[0]['code'] ?? 'ar');

    $excluded = $editing !== null ? array_merge([$editId], $descendantIds($editId)) : [];
    $parentOptions = static function (int $selectedParent = 0) use ($categories, $categoryPath, $excluded, $e, $t): string {
        $html = '<option value="">' . $t('admin.choose_parent') . '</option>';
        foreach ($categories as $candidate) {
            $candidateId = (int) ($candidate['id'] ?? 0);
            if ($candidateId <= 0 || in_array($candidateId, $excluded, true)) continue;
            $candidateLocale = (string) ($candidate['content_locale'] ?? '');
            $label = $categoryPath($candidate) . ' [' . $candidateLocale . ']';
            $html .= '<option value="' . $e($candidateId) . '"' . ($selectedParent === $candidateId ? ' selected' : '') . '>' . $e($label) . '</option>';
        }
        return $html;
    };

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.categories') . '</h2><p class="admin-muted">' . $t('admin.category_hierarchy_note') . '</p></div><a class="admin-button" href="' . $e(admin_url('categories', ['new_category' => 1])) . '">' . $t('admin.actions.create') . '</a></div></section>';

    if ($newCategory || $editing !== null) {
        $id = (int) ($editing['id'] ?? 0);
        $name = (string) ($editing['name'] ?? '');
        $slug = (string) ($editing['slug'] ?? '');
        $sort = (int) ($editing['sort_order'] ?? 0);
        $description = (string) ($editing['description'] ?? '');
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><div><h2>' . ($editing !== null ? $t('admin.category_edit') : $t('admin.category_new')) . '</h2>';
        if ($editing !== null) echo '<p class="admin-muted">' . $e($categoryPath($editing)) . '</p>';
        echo '</div><a class="admin-link" href="' . $e(admin_url('categories')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body">';
        echo '<form method="post" data-category-form action="' . $e(admin_url('categories')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="category_save"><input type="hidden" name="page" value="categories">';
        if ($id > 0) echo '<input type="hidden" name="id" value="' . $e($id) . '">';
        echo '<div class="admin-form-grid">';
        echo '<label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="name" required maxlength="120" value="' . $e($name) . '"></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.slug') . '</span><input name="slug" maxlength="191" value="' . $e($slug) . '"></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.locale') . '</span><select name="content_locale" data-category-locale>' . $languageOptions($prefillLocale) . '</select></label>';
        $isRoot = $prefillParentId <= 0;
        echo '<label class="admin-field admin-checkbox admin-root-category-toggle"><input type="checkbox" name="is_root_category" value="1" data-category-root' . ($isRoot ? ' checked' : '') . '><span>' . $t('admin.fields.root_category') . '</span></label>';
        echo '<label class="admin-field" data-category-parent-field><span>' . $t('admin.fields.parent_category') . '</span><select name="parent_id" data-category-parent' . ($isRoot ? ' disabled' : '') . '>' . $parentOptions($prefillParentId) . '</select><small>' . $t('admin.category_parent_help') . '</small></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.sort') . '</span><input name="sort_order" type="number" value="' . $e($sort) . '"></label>';
        echo '<label class="admin-field admin-field-full"><span>' . $t('admin.fields.description') . '</span><textarea name="description">' . $e($description) . '</textarea></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.meta_title') . '</span><input name="seo_title" maxlength="255" value="' . $e($editing['seo_title'] ?? '') . '"></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.seo_index') . '</span><select name="seo_index"><option value="inherit"' . (($editing['seo_index'] ?? 'inherit') === 'inherit' ? ' selected' : '') . '>' . $t('admin.seo_index_inherit') . '</option><option value="index"' . (($editing['seo_index'] ?? '') === 'index' ? ' selected' : '') . '>' . $t('admin.seo_index') . '</option><option value="noindex"' . (($editing['seo_index'] ?? '') === 'noindex' ? ' selected' : '') . '>' . $t('admin.seo_noindex') . '</option></select></label>';
        echo '<label class="admin-field admin-field-full"><span>' . $t('admin.fields.meta_description') . '</span><textarea name="seo_description" maxlength="191">' . $e($editing['seo_description'] ?? '') . '</textarea></label>';
        echo '<label class="admin-field admin-field-full"><span>' . $t('admin.fields.canonical_url') . '</span><input name="canonical_url" maxlength="500" dir="ltr" value="' . $e($editing['canonical_url'] ?? '') . '"></label>';
        echo '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('categories')) . '">' . $t('admin.actions.cancel') . '</a></div></form></div></section>';
    }

    echo '<section class="admin-card"><div class="admin-table-wrap"><table class="admin-table admin-category-table"><thead><tr><th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.fields.parent_category') . '</th><th>' . $t('admin.fields.locale') . '</th><th>' . $t('admin.questions_count') . '</th><th>' . $t('admin.fields.sort') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    if ($categories === []) echo '<tr><td colspan="6" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    foreach ($orderedCategories as $category) {
        $id = (int) $category['id'];
        $parentId = (int) ($category['parent_id'] ?? 0);
        $depth = max(0, (int) ($category['_admin_depth'] ?? 0));
        echo '<tr data-category-depth="' . $e($depth) . '"><td><div class="admin-category-name" style="--category-depth:' . $e($depth) . '"><strong>' . ($depth > 0 ? '<span class="admin-tree-branch" aria-hidden="true">↳</span>' : '') . $e($category['name']) . '</strong><small>' . $e($categoryPath($category)) . '</small><small class="admin-code">' . $e($category['slug']) . '</small></div></td>';
        echo '<td>' . ($parentId > 0 && isset($categoryMap[$parentId]) ? $e($categoryMap[$parentId]['name']) : '<span class="admin-muted">' . $t('admin.no_parent_short') . '</span>') . '</td>';
        echo '<td><span class="admin-badge">' . $e($category['content_locale']) . '</span></td><td>' . $e($category['questions_count']) . '</td><td>' . $e($category['sort_order']) . '</td><td><div class="admin-actions">';
        echo '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('categories', ['edit_category' => $id])) . '">' . $t('admin.actions.edit') . '</a>';
        echo '<a class="admin-button admin-button-small admin-button-secondary" data-action="add_child" href="' . $e(admin_url('categories', ['new_category' => 1, 'add_child' => 1, 'parent_id' => $id, 'content_locale' => (string) $category['content_locale']])) . '">' . $t('admin.actions.add_child') . '</a>';
        admin_action_form('category_delete', '', $id, $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger admin-confirm-delete', 'category_id');
        echo '</div></td></tr>';
    }
    echo '</tbody></table></div></section>';
}

function admin_render_tags(AdminRepository $repository, array $languages, callable $t, callable $e, string $csrfToken): void
{
    $tags = admin_pagination($repository->tags(), $repository->countRows('tags'), $e);
    $tagMap = [];
    foreach ($tags as $tag) $tagMap[(int) ($tag['id'] ?? 0)] = $tag;
    $editId = max(0, (int) ($_GET['edit_tag'] ?? 0));
    $newTag = isset($_GET['new_tag']);
    $editing = $editId > 0 && isset($tagMap[$editId]) ? $tagMap[$editId] : null;
    $languageOptions = static function (string $selected = '') use ($languages, $e): string {
        $html = '';
        foreach ($languages as $language) {
            $code = (string) ($language['code'] ?? '');
            if ($code === '') continue;
            $label = (string) ($language['native_name'] ?? $language['name'] ?? $code);
            $html .= '<option value="' . $e($code) . '"' . ($selected === $code ? ' selected' : '') . '>' . $e($label) . ' (' . $e($code) . ')</option>';
        }
        return $html;
    };

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.tags') . '</h2><p class="admin-muted">' . $t('admin.tags_note') . '</p></div><a class="admin-button" href="' . $e(admin_url('tags', ['new_tag' => 1])) . '">' . $t('admin.actions.create') . '</a></div></section>';

    if ($newTag || $editing !== null) {
        $id = (int) ($editing['id'] ?? 0);
        $selectedLocale = (string) ($editing['content_locale'] ?? ($languages[0]['code'] ?? 'ar'));
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><h2>' . ($editing !== null ? $t('admin.tag_edit') : $t('admin.tag_new')) . '</h2><a class="admin-link" href="' . $e(admin_url('tags')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('tags')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="tag_save"><input type="hidden" name="page" value="tags">';
        if ($id > 0) echo '<input type="hidden" name="id" value="' . $e($id) . '">';
        echo '<div class="admin-form-grid">';
        echo '<label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="name" required maxlength="64" value="' . $e($editing['name'] ?? '') . '"></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.slug') . '</span><input name="slug" maxlength="191" value="' . $e($editing['slug'] ?? '') . '"></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.locale') . '</span><select name="content_locale">' . $languageOptions($selectedLocale) . '</select></label>';
        echo '<label class="admin-field admin-field-full"><span>' . $t('admin.fields.description') . '</span><textarea name="description" rows="3">' . $e($editing['description'] ?? '') . '</textarea></label>';
        echo '<label class="admin-field admin-field-full"><span>' . $t('admin.fields.tag_synonyms') . '</span><textarea name="synonyms" rows="3" placeholder="' . $e($t('admin.tag_synonyms_help')) . '">' . $e($editing['synonyms'] ?? '') . '</textarea><small>' . $t('admin.tag_synonyms_help') . '</small></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.meta_title') . '</span><input name="seo_title" maxlength="255" value="' . $e($editing['seo_title'] ?? '') . '"></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.seo_index') . '</span><select name="seo_index"><option value="inherit"' . (($editing['seo_index'] ?? 'inherit') === 'inherit' ? ' selected' : '') . '>' . $t('admin.seo_index_inherit') . '</option><option value="index"' . (($editing['seo_index'] ?? '') === 'index' ? ' selected' : '') . '>' . $t('admin.seo_index') . '</option><option value="noindex"' . (($editing['seo_index'] ?? '') === 'noindex' ? ' selected' : '') . '>' . $t('admin.seo_noindex') . '</option></select></label>';
        echo '<label class="admin-field admin-field-full"><span>' . $t('admin.fields.meta_description') . '</span><textarea name="seo_description" maxlength="191">' . $e($editing['seo_description'] ?? '') . '</textarea></label>';
        echo '<label class="admin-field admin-field-full"><span>' . $t('admin.fields.canonical_url') . '</span><input name="canonical_url" maxlength="500" dir="ltr" value="' . $e($editing['canonical_url'] ?? '') . '"></label>';
        echo '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('tags')) . '">' . $t('admin.actions.cancel') . '</a></div></form>';
        if ($editing !== null && $id > 0) {
            $mergeTargets = array_values(array_filter($tags, static fn(array $candidate): bool => (int) ($candidate['id'] ?? 0) !== $id && (string) ($candidate['content_locale'] ?? '') === $selectedLocale));
            if ($mergeTargets !== []) {
                echo '<hr><form method="post" class="admin-merge-form" action="' . $e(admin_url('tags')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="tag_merge"><input type="hidden" name="page" value="tags"><input type="hidden" name="source_tag_id" value="' . $e($id) . '"><h3>' . $t('admin.tag_merge_title') . '</h3><p class="admin-muted">' . $t('admin.tag_merge_note') . '</p><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.merge_target') . '</span><select name="target_tag_id" required><option value="">' . $t('admin.choose_tag') . '</option>';
                foreach ($mergeTargets as $candidate) echo '<option value="' . $e($candidate['id']) . '">' . $e($candidate['name']) . '</option>';
                echo '</select></label><label class="admin-field admin-checkbox"><input type="checkbox" name="preserve_source_name" value="1" checked><span>' . $t('admin.fields.preserve_as_synonym') . '</span></label></div><button class="admin-button admin-button-danger admin-confirm-delete" type="submit">' . $t('admin.actions.merge') . '</button></form>';
            }
        }
        echo '</div></section>';
    }

    echo '<section class="admin-card"><div class="admin-table-wrap"><table class="admin-table admin-tags-table"><thead><tr><th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.fields.slug') . '</th><th>' . $t('admin.fields.locale') . '</th><th>' . $t('admin.table.metrics') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    foreach ($tags as $tag) {
        $id = (int) $tag['id'];
        echo '<tr><td><strong>' . $e($tag['name']) . '</strong></td><td><code>' . $e($tag['slug']) . '</code></td><td><span class="admin-badge">' . $e($tag['content_locale']) . '</span></td><td>' . $e($tag['questions_count']) . ' ' . $t('admin.questions_count') . '</td><td><div class="admin-actions"><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('tags', ['edit_tag' => $id])) . '">' . $t('admin.actions.edit') . '</a>';
        admin_action_form('tag_delete', '', $id, $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger admin-confirm-delete', 'tag_id');
        echo '</div></td></tr>';
    }
    if ($tags === []) echo '<tr><td colspan="5" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';
}

function admin_render_users(AdminRepository $repository, array $languages, callable $t, callable $e, string $csrfToken, int $currentUserId, bool $isAdmin): void
{
    $search = trim((string) ($_GET['q'] ?? ''));
    $total = $repository->countRows('users', null, $search);
    $users = admin_pagination($repository->users($search), $total, $e);
    $userMap = [];
    foreach ($users as $row) $userMap[(int) ($row['id'] ?? 0)] = $row;
    $editId = max(0, (int) ($_GET['edit_user'] ?? 0));
    $newUser = isset($_GET['new_user']) && $isAdmin;
    $editing = $editId > 0 && isset($userMap[$editId]) ? $userMap[$editId] : null;
    $languageOptions = static function (string $selected = '') use ($languages, $e): string {
        $html = '';
        foreach ($languages as $language) {
            $code = (string) ($language['code'] ?? '');
            if ($code === '') continue;
            $label = (string) ($language['native_name'] ?? $language['name'] ?? $code);
            $html .= '<option value="' . $e($code) . '"' . ($selected === $code ? ' selected' : '') . '>' . $e($label) . ' (' . $e($code) . ')</option>';
        }
        return $html;
    };

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.users') . '</h2><p class="admin-muted">' . $t('admin.users_note') . '</p></div><span class="admin-count">' . $e($total) . '</span>' . ($isAdmin ? '<a class="admin-button admin-button-small" href="' . $e(admin_url('users', ['new_user' => 1])) . '">' . $t('admin.actions.create') . '</a>' : '') . '</div><div class="admin-toolbar"><form method="get" action="' . $e(admin_url('users')) . '"><input type="hidden" name="page" value="users"><label>' . $t('admin.filter_search') . '<input type="search" name="q" value="' . $e($search) . '"></label><button type="submit">' . $t('admin.filter') . '</button></form></div></section>';

    if ($newUser) {
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-body"><form method="post" action="' . $e(admin_url('users')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="user_create"><input type="hidden" name="page" value="users"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="name" required></label><label class="admin-field"><span>' . $t('admin.fields.email') . '</span><input type="email" name="email" required></label><label class="admin-field"><span>' . $t('admin.fields.password') . '</span><input type="password" name="password" minlength="8" required></label><label class="admin-field"><span>' . $t('admin.fields.role') . '</span><select name="role"><option value="member">' . $t('admin.roles.member') . '</option><option value="moderator">' . $t('admin.roles.moderator') . '</option><option value="admin">' . $t('admin.roles.admin') . '</option></select></label><label class="admin-field"><span>' . $t('admin.fields.locale') . '</span><select name="preferred_locale">' . $languageOptions() . '</select></label></div><button type="submit">' . $t('admin.actions.create') . '</button></form></div></section>';
    }

    if ($editing !== null && $isAdmin) {
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><div><h2>' . $t('admin.user_edit') . '</h2><p class="admin-muted"><bdi dir="auto">' . $e($editing['email']) . '</bdi></p></div><a class="admin-link" href="' . $e(admin_url('users')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('users')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="user_update"><input type="hidden" name="page" value="users"><input type="hidden" name="user_id" value="' . $e($editing['id']) . '"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="name" value="' . $e($editing['name']) . '"></label><label class="admin-field"><span>' . $t('admin.fields.email') . '</span><input type="email" dir="ltr" name="email" value="' . $e($editing['email']) . '"></label><label class="admin-field"><span>' . $t('admin.fields.password') . '</span><input type="password" name="password" autocomplete="new-password"></label><label class="admin-field admin-checkbox"><input type="hidden" name="email_verified" value="0"><input type="checkbox" name="email_verified" value="1"' . (!empty($editing['email_verified_at']) ? ' checked' : '') . '><span>' . $t('admin.fields.email_verified') . '</span></label>';
        echo '<label class="admin-field"><span>' . $t('admin.fields.role') . '</span><select name="role">';
        foreach (['member', 'moderator', 'admin'] as $role) echo '<option value="' . $e($role) . '"' . ((string) $editing['role'] === $role ? ' selected' : '') . '>' . $t('admin.roles.' . $role) . '</option>';
        echo '</select></label><label class="admin-field"><span>' . $t('admin.fields.status') . '</span><select name="status">';
        foreach (['active', 'suspended', 'banned'] as $status) echo '<option value="' . $e($status) . '"' . ((string) $editing['status'] === $status ? ' selected' : '') . '>' . $t('admin.user_status.' . $status) . '</option>';
        echo '</select></label><label class="admin-field"><span>' . $t('admin.fields.locale') . '</span><select name="preferred_locale">' . $languageOptions((string) $editing['preferred_locale']) . '</select></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('users')) . '">' . $t('admin.actions.cancel') . '</a></div></form></div></section>';
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><div><h3>' . $t('admin.user_trust_title') . '</h3><p class="admin-muted">' . $t('admin.user_trust_note') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('users')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="user_trust"><input type="hidden" name="page" value="users"><input type="hidden" name="user_id" value="' . $e($editing['id']) . '"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.trust_level') . '</span><input type="number" min="0" max="5" name="trust_level" value="' . $e((int)($editing['trust_level'] ?? 0)) . '"></label><label class="admin-field admin-checkbox"><input type="hidden" name="login_disabled" value="0"><input type="checkbox" name="login_disabled" value="1"' . (!empty($editing['login_disabled']) ? ' checked' : '') . '><span>' . $t('admin.fields.login_disabled') . '</span></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';
    }

    echo '<section class="admin-card"><div class="admin-table-wrap"><table class="admin-table admin-users-table"><thead><tr><th>' . $t('admin.fields.user') . '</th><th>' . $t('admin.fields.role') . '</th><th>' . $t('admin.fields.account_type') . '</th><th>' . $t('admin.fields.trust_level') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.fields.locale') . '</th><th>' . $t('admin.fields.points') . '</th><th>' . $t('admin.table.created') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    foreach ($users as $item) {
        $id = (int) $item['id'];
        $isSelf = $id === $currentUserId;
        $status = (string) $item['status'];
        echo '<tr><td><strong><bdi dir="auto">' . $e($item['name'] ?: $item['email']) . '</bdi></strong><small><bdi dir="ltr">' . $e($item['email']) . '</bdi>' . ($isSelf ? ' · ' . $t('admin.current_user') : '') . '</small></td>';
        echo '<td><span class="admin-badge">' . $t('admin.roles.' . (string) $item['role']) . '</span></td><td><span class="admin-badge">' . $e((string)($item['account_type'] ?? 'member')) . '</span></td><td>' . $e((int)($item['trust_level'] ?? 0)) . (!empty($item['login_disabled']) ? ' · ' . $t('admin.login_disabled_short') : '') . '</td><td><span class="admin-badge admin-status-' . $e($status) . '">' . $t('admin.user_status.' . $status) . '</span></td><td>' . $e($item['preferred_locale']) . '</td><td>' . $e($item['points']) . '</td><td>' . $e($item['created_at']) . '</td><td><div class="admin-actions">';
        if ($isAdmin) {
            echo '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('users', ['edit_user' => $id])) . '">' . $t('admin.actions.edit') . '</a>';
            if (!$isSelf) {
                foreach ([['active', 'activate'], ['suspended', 'suspend'], ['banned', 'ban']] as [$targetStatus, $labelKey]) {
                    if ($status === $targetStatus) continue;
                    echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('users')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="user_status"><input type="hidden" name="page" value="users"><input type="hidden" name="user_id" value="' . $e($id) . '"><input type="hidden" name="status" value="' . $e($targetStatus) . '"><button class="admin-button-small admin-button-secondary" type="submit">' . $t('admin.actions.' . $labelKey) . '</button></form>';
                }
                admin_action_form('user_delete', '', $id, $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger admin-confirm-delete', 'user_id');
            }
        } else {
            echo '<span class="admin-muted">' . $t('admin.read_only') . '</span>';
        }
        echo '</div></td></tr>';
    }
    if ($users === []) echo '<tr><td colspan="9" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';
}

function admin_render_editorial(PDO $pdo, AdminRepository $repository, array $languages, callable $t, callable $e, string $csrfToken, int $actorAdminId): void
{
    $service = new EditorialSeedService($pdo);
    $personas = $service->personas();
    $categories = $repository->categories();
    $result = is_array($_SESSION['editorial_seed_result'] ?? null) ? $_SESSION['editorial_seed_result'] : null;
    unset($_SESSION['editorial_seed_result']);
    $localeOptions = '';
    foreach ($languages as $language) {
        $code = trim((string) ($language['code'] ?? ''));
        if ($code === '' || empty($language['is_enabled'])) continue;
        $localeOptions .= '<option value="' . $e($code) . '">' . $e((string) ($language['native_name'] ?? $language['name'] ?? $code)) . '</option>';
    }
    $personaOptions = '';
    foreach ($personas as $persona) $personaOptions .= '<option value="' . $e($persona['id']) . '">' . $e($persona['name']) . ' · ' . $e($persona['email']) . '</option>';
    $categoryOptions = '<option value="0">—</option>';
    foreach ($categories as $category) $categoryOptions .= '<option value="' . $e($category['id']) . '">' . $e($category['name']) . '</option>';

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.editorial') . '</h2><p class="admin-muted">' . $t('admin.editorial_note') . '</p></div><span class="admin-count">' . $e(count($personas)) . '</span></div></section>';
    if ($result !== null) {
        echo '<section class="admin-card"><div class="admin-card-body"><div class="admin-alert"><strong>' . $t('admin.editorial_import_result') . '</strong><br>' . $e('new=' . (int)($result['new'] ?? 0) . ' duplicate=' . (int)($result['duplicate'] ?? 0) . ' invalid=' . ((int)($result['invalid'] ?? 0) + (int)($result['parser_invalid'] ?? 0)) . ' unknown=' . (int)($result['unknown'] ?? 0)) . '</div>';
        $errors = array_merge((array) ($result['errors'] ?? []), (array) ($result['parser_errors'] ?? []));
        if ($errors !== []) echo '<pre class="admin-code">' . $e(implode("\n", array_slice($errors, 0, 30))) . '</pre>';
        echo '</div></section>';
    }

    echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.editorial_persona_create') . '</h3><p class="admin-muted">' . $t('admin.editorial_persona_note') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('editorial')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="editorial_persona_create"><input type="hidden" name="page" value="editorial"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="name" required maxlength="120"></label><label class="admin-field"><span>' . $t('admin.fields.email') . '</span><input type="email" name="email" required maxlength="191"></label><label class="admin-field"><span>' . $t('admin.fields.locale') . '</span><select name="preferred_locale">' . $localeOptions . '</select></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.create') . '</button></div></form></div></section>';

    echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.editorial_seed_create') . '</h3><p class="admin-muted">' . $t('admin.editorial_seed_note') . '</p></div></div><div class="admin-card-body">';
    if ($personas === []) {
        echo '<div class="admin-alert">' . $t('admin.editorial_requires_persona') . '</div>';
    } else {
        echo '<form method="post" action="' . $e(admin_url('editorial')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="editorial_seed_create"><input type="hidden" name="page" value="editorial"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.editorial_persona') . '</span><select name="persona_id">' . $personaOptions . '</select></label><label class="admin-field"><span>' . $t('admin.editorial_type') . '</span><select name="seed_type"><option value="question">Question</option><option value="answer">Answer</option><option value="topic">Discussion</option></select></label><label class="admin-field"><span>' . $t('admin.fields.locale') . '</span><select name="content_locale">' . $localeOptions . '</select></label><label class="admin-field"><span>' . $t('admin.fields.status') . '</span><select name="status"><option value="published">Published</option><option value="pending">Pending</option></select></label><label class="admin-field"><span>' . $t('admin.fields.category') . '</span><select name="category_id">' . $categoryOptions . '</select></label><label class="admin-field"><span>Question ID (answers only)</span><input type="number" min="1" name="question_id"></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.title') . '</span><input name="title" maxlength="240"></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.body') . '</span><textarea name="body" rows="7" required maxlength="100000"></textarea></label><label class="admin-field admin-field-full"><span>Tag IDs (questions only)</span><input name="tag_ids" dir="ltr" placeholder="1, 4, 8"></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.create') . '</button></div></form>';
    }
    echo '</div></section>';

    echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.editorial_import') . '</h3><p class="admin-muted">' . $t('admin.editorial_import_note') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('editorial')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="page" value="editorial"><div class="admin-form-grid"><label class="admin-field"><span>Format</span><select name="seed_format"><option value="json">JSON</option><option value="csv">CSV</option></select></label><label class="admin-field admin-field-full"><span>Seed data</span><textarea name="seed_payload" rows="14" maxlength="2097152" dir="ltr" required></textarea></label></div><div class="admin-form-actions"><button type="submit" name="admin_action" value="editorial_seed_preview" class="admin-button-secondary">' . $t('admin.editorial_preview') . '</button><button type="submit" name="admin_action" value="editorial_seed_import">' . $t('admin.editorial_import_action') . '</button></div></form></div></section>';
}

function admin_report_target_url(array $report): string
{
    $slug = trim((string) ($report['question_slug'] ?? ''));
    if ($slug === '') return '';
    $url = admin_public_url('q/' . rawurlencode($slug));
    $type = (string) ($report['reportable_type'] ?? '');
    if ($type === 'topic') return admin_public_url('d/' . rawurlencode($slug));
    if ($type === 'answer') {
        $answerId = (int) ($report['target_answer_id'] ?? $report['reportable_id'] ?? 0);
        return $answerId > 0 ? $url . '#answer-' . $answerId : $url;
    }
    if ($type === 'comment') {
        $commentId = (int) ($report['target_comment_id'] ?? $report['reportable_id'] ?? 0);
        if ($commentId <= 0) return $url;
        if (($report['target_commentable_type'] ?? '') === 'topic') {
            $url = admin_public_url('d/' . rawurlencode($slug));
            return $url . '#comment-' . $commentId;
        }
        if ((int) ($report['target_answer_id'] ?? 0) <= 0) {
            $url = admin_public_url('q/' . rawurlencode($slug), ['show_comment' => $commentId]);
        }
        return $url . '#comment-' . $commentId;
    }
    return $url;
}

function admin_render_reports(AdminRepository $repository, callable $t, callable $e, string $csrfToken): void
{
    $statusFilter = trim((string) ($_GET['report_status'] ?? ''));
    if (!in_array($statusFilter, ['open', 'reviewing', 'resolved', 'dismissed'], true)) $statusFilter = '';
    $total = $repository->countRows('reports', $statusFilter !== '' ? $statusFilter : null);
    $reports = admin_pagination($repository->reports($statusFilter !== '' ? $statusFilter : null), $total, $e);
    $reportMap = [];
    foreach ($reports as $row) $reportMap[(int) ($row['id'] ?? 0)] = $row;
    $viewId = max(0, (int) ($_GET['view_report'] ?? 0));
    $viewing = $viewId > 0 && isset($reportMap[$viewId]) ? $reportMap[$viewId] : null;
    $editId = max(0, (int) ($_GET['edit_report'] ?? 0));
    $editing = $editId > 0 && isset($reportMap[$editId]) ? $reportMap[$editId] : null;

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.reports') . '</h2><p class="admin-muted">' . $t('admin.reports_note') . '</p></div><span class="admin-count">' . $e($total) . '</span></div><div class="admin-toolbar"><form method="get" action="' . $e(admin_url('reports')) . '"><input type="hidden" name="page" value="reports"><label>' . $t('admin.fields.status') . '<select name="report_status"><option value="">' . $t('admin.all_statuses') . '</option>';
    foreach (['open', 'reviewing', 'resolved', 'dismissed'] as $option) echo '<option value="' . $e($option) . '"' . ($statusFilter === $option ? ' selected' : '') . '>' . $t('admin.report_status.' . $option) . '</option>';
    echo '</select></label><button type="submit">' . $t('admin.filter') . '</button></form></div></section>';

    if (is_array($viewing)) {
        $targetUrl = admin_report_target_url($viewing);
        echo '<section class="admin-card admin-report-inspector"><div class="admin-card-head"><div><h2>' . $t('admin.report_details') . '</h2><p class="admin-muted">' . $t('admin.report_types.' . (string) $viewing['reportable_type']) . ' #' . $e((string) $viewing['reportable_id']) . '</p></div><a class="admin-link" href="' . $e(admin_url('reports')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body">';
        echo '<div class="admin-report-meta-grid">';
        echo '<div><span>' . $t('admin.fields.reason') . '</span><strong>' . $t('questions.report_reasons.' . (string) $viewing['reason']) . '</strong></div>';
        echo '<div><span>' . $t('admin.reporter') . '</span><strong><bdi dir="auto">' . $e((string) $viewing['reporter_name']) . '</bdi></strong></div>';
        echo '<div><span>' . $t('admin.fields.status') . '</span><strong>' . $t('admin.report_status.' . (string) $viewing['status']) . '</strong></div>';
        echo '<div><span>' . $t('admin.table.created') . '</span><strong>' . $e((string) $viewing['created_at']) . '</strong></div>';
        echo '</div>';
        if (trim((string) ($viewing['notes'] ?? '')) !== '') {
            echo '<div class="admin-report-note"><strong>' . $t('admin.fields.notes') . '</strong><p dir="auto">' . nl2br($e((string) $viewing['notes'])) . '</p></div>';
        }
        if (trim((string) ($viewing['moderator_notes'] ?? '')) !== '') {
            echo '<div class="admin-report-note"><strong>' . $t('admin.fields.moderator_notes') . '</strong><p dir="auto">' . nl2br($e((string) $viewing['moderator_notes'])) . '</p></div>';
        }
        if (trim((string) ($viewing['question_title'] ?? '')) !== '') {
            echo '<div class="admin-report-context"><strong>' . $t('admin.report_question_context') . '</strong><p><bdi dir="auto">' . $e((string) $viewing['question_title']) . '</bdi></p></div>';
        }
        echo '<div class="admin-report-target-preview"><strong>' . $t('admin.report_target_content') . '</strong><div dir="auto">' . nl2br($e((string) ($viewing['target_content'] ?? ''))) . '</div></div>';
        echo '<div class="admin-form-actions">';
        if ($targetUrl !== '') echo '<a class="admin-button" href="' . $e($targetUrl) . '" target="_blank" rel="noopener noreferrer">' . $t('admin.admin_report_open_target') . '</a>';
        echo '<a class="admin-button admin-button-secondary" href="' . $e(admin_url('reports', ['edit_report' => (int) $viewing['id']])) . '">' . $t('admin.actions.review') . '</a>';
        echo '</div></div></section>';
    }

    if ($editing !== null) {
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><div><h2>' . $t('admin.report_edit') . '</h2><p class="admin-muted">' . $t('admin.report_types.' . (string) $editing['reportable_type']) . ' #' . $e($editing['reportable_id']) . '</p></div><a class="admin-link" href="' . $e(admin_url('reports')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body">';
        if (trim((string) ($editing['notes'] ?? '')) !== '') echo '<div class="admin-report-note"><strong>' . $t('admin.fields.notes') . '</strong><p dir="auto">' . nl2br($e((string) $editing['notes'])) . '</p></div>';
        echo '<form method="post" action="' . $e(admin_url('reports')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="report_update"><input type="hidden" name="page" value="reports"><input type="hidden" name="report_id" value="' . $e($editing['id']) . '"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.status') . '</span><select name="status">';
        foreach (['open', 'reviewing', 'resolved', 'dismissed'] as $status) echo '<option value="' . $e($status) . '"' . ($editing['status'] === $status ? ' selected' : '') . '>' . $t('admin.report_status.' . $status) . '</option>';
        echo '</select></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.moderator_notes') . '</span><textarea name="notes" rows="4">' . $e((string) ($editing['moderator_notes'] ?? '')) . '</textarea></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('reports')) . '">' . $t('admin.actions.cancel') . '</a></div></form></div></section>';
    }

    echo '<section class="admin-card"><div class="admin-table-wrap"><table class="admin-table admin-reports-table"><thead><tr><th>' . $t('admin.fields.target') . '</th><th>' . $t('admin.fields.user') . '</th><th>' . $t('admin.fields.reason') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.table.created') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    foreach ($reports as $report) {
        $id = (int) $report['id'];
        $targetUrl = admin_report_target_url($report);
        $preview = trim((string) ($report['target_content'] ?? ''));
        $preview = function_exists('mb_substr') ? mb_substr($preview, 0, 150, 'UTF-8') : substr($preview, 0, 150);
        echo '<tr><td><strong>' . $t('admin.report_types.' . (string) $report['reportable_type']) . ' #' . $e($report['reportable_id']) . '</strong>';
        if (trim((string) ($report['question_title'] ?? '')) !== '') echo '<small><bdi dir="auto">' . $e((string) $report['question_title']) . '</bdi></small>';
        if ($preview !== '') echo '<small class="admin-report-row-preview" dir="auto">' . $e($preview) . '</small>';
        echo '</td><td><bdi dir="auto">' . $e($report['reporter_name']) . '</bdi></td><td>' . $t('questions.report_reasons.' . (string) $report['reason']) . (!empty($report['notes']) ? '<small dir="auto">' . $e($report['notes']) . '</small>' : '') . '</td><td><span class="admin-badge">' . $t('admin.report_status.' . (string) $report['status']) . '</span></td><td>' . $e($report['created_at']) . '</td><td><div class="admin-actions">';
        echo '<a class="admin-button admin-button-small" href="' . $e(admin_url('reports', ['view_report' => $id])) . '">' . $t('admin.actions.review') . '</a>';
        if ($targetUrl !== '') echo '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e($targetUrl) . '" target="_blank" rel="noopener noreferrer">' . $t('admin.admin_report_open_target') . '</a>';
        foreach ([['reviewing', 'review'], ['resolved', 'resolve'], ['dismissed', 'dismiss']] as [$status, $label]) {
            if ((string) $report['status'] === $status) continue;
            echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('reports')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="report_update"><input type="hidden" name="page" value="reports"><input type="hidden" name="report_id" value="' . $e($id) . '"><input type="hidden" name="status" value="' . $e($status) . '"><button class="admin-button-small admin-button-secondary" type="submit">' . $t('admin.actions.' . $label) . '</button></form>';
        }
        echo '</div></td></tr>';
    }
    if ($reports === []) echo '<tr><td colspan="6" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';
}

function admin_render_messages(AdminRepository $repository, callable $t, callable $e, string $csrfToken): void
{
    try {
        $messages = admin_pagination($repository->contactMessages(), $repository->countRows('contact_messages'), $e);
    } catch (Throwable $throwable) {
        (new FileLogger())->error('Admin contact-message query failed.', $throwable);
        echo '<section class="admin-card"><div class="admin-card-body"><div class="admin-alert admin-alert-error">' . $t('admin.migration_013_required') . '</div><code>php bin/migrate.php</code></div></section>';
        return;
    }
    $editId = max(0, (int) ($_GET['edit_message'] ?? 0));
    $editing = null;
    foreach ($messages as $row) if ((int) ($row['id'] ?? 0) === $editId) $editing = $row;

    $messageTotal = $repository->countRows('contact_messages');
    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.messages') . '</h2><p class="admin-muted">' . $t('admin.messages_note') . '</p></div><span class="admin-count">' . $e($messageTotal) . '</span></div></section>';
    if (is_array($editing)) {
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><div><h2>' . $t('admin.message_review') . '</h2><p class="admin-muted"><bdi dir="auto">' . $e($editing['email']) . '</bdi></p></div><a class="admin-link" href="' . $e(admin_url('messages')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body"><div class="admin-message-content"><strong>' . $e($editing['subject'] ?: $t('admin.no_subject')) . '</strong><p dir="auto">' . nl2br($e($editing['message'])) . '</p></div><form method="post" action="' . $e(admin_url('messages')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="contact_message_update"><input type="hidden" name="page" value="messages"><input type="hidden" name="message_id" value="' . $e($editing['id']) . '"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.status') . '</span><select name="status">';
        foreach (['open','reviewing','resolved','closed'] as $status) echo '<option value="' . $status . '"' . (($editing['status'] ?? '') === $status ? ' selected' : '') . '>' . $t('admin.message_status.' . $status) . '</option>';
        echo '</select></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.notes') . '</span><textarea name="notes">' . $e($editing['admin_notes'] ?? '') . '</textarea></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="mailto:' . $e($editing['email']) . '">' . $t('admin.actions.reply_email') . '</a></div></form></div></section>';
    }
    echo '<section class="admin-card"><div class="admin-table-wrap"><table class="admin-table admin-messages-table"><thead><tr><th>' . $t('admin.fields.user') . '</th><th>' . $t('admin.fields.subject') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.table.created') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    foreach ($messages as $row) {
        echo '<tr><td><strong><bdi dir="auto">' . $e($row['name']) . '</bdi></strong><small><bdi dir="ltr">' . $e($row['email']) . '</bdi></small></td><td><strong>' . $e($row['subject'] ?: $t('admin.no_subject')) . '</strong><small dir="auto">' . $e(function_exists('mb_substr') ? mb_substr((string) $row['message'], 0, 120, 'UTF-8') : substr((string) $row['message'], 0, 120)) . '</small></td><td><span class="admin-badge">' . $t('admin.message_status.' . (string) $row['status']) . '</span></td><td>' . $e($row['created_at']) . '</td><td><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('messages', ['edit_message' => (int) $row['id']])) . '">' . $t('admin.actions.review') . '</a></td></tr>';
    }
    if ($messages === []) echo '<tr><td colspan="5" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';
}

function admin_render_notifications(AdminRepository $repository, Translator $translator, callable $t, callable $e, string $csrfToken, int $userId): void
{
    $statusFilter = trim((string) ($_GET['notification_status'] ?? ''));
    if (!in_array($statusFilter, ['read', 'unread'], true)) $statusFilter = '';
    try {
        $unread = $repository->unreadNotificationsCount($userId);
        $total = $repository->countNotificationsForUser($userId, $statusFilter !== '' ? $statusFilter : null);
        $items = admin_pagination($repository->notificationsForUser($userId, $statusFilter !== '' ? $statusFilter : null), $total, $e);
    } catch (Throwable) {
        echo '<section class="admin-card"><div class="admin-card-body"><div class="admin-alert admin-alert-error">' . $t('admin.migration_013_required') . '</div><code>php bin/migrate.php</code></div></section>';
        return;
    }
    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.notifications') . '</h2><p class="admin-muted">' . $t('admin.notifications_note') . '</p></div><span class="admin-count">' . $e($total) . '</span>' . ($unread > 0 ? '<form class="admin-inline-form" method="post" action="' . $e(admin_url('notifications')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="notifications_mark_all_read"><input type="hidden" name="page" value="notifications"><button class="admin-button-small" type="submit">' . $t('admin.actions.mark_all_read') . '</button></form>' : '') . '</div><div class="admin-toolbar"><form method="get" action="' . $e(admin_url('notifications')) . '"><input type="hidden" name="page" value="notifications"><label>' . $t('admin.fields.status') . '<select name="notification_status"><option value="">' . $t('admin.notification_filter_all') . '</option><option value="unread"' . ($statusFilter === 'unread' ? ' selected' : '') . '>' . $t('admin.notification_filter_unread') . '</option><option value="read"' . ($statusFilter === 'read' ? ' selected' : '') . '>' . $t('admin.notification_filter_read') . '</option></select></label><button type="submit">' . $t('admin.filter') . '</button></form></div>';

    echo '<div class="admin-notification-cards">';
    foreach ($items as $item) {
        $read = !empty($item['read_at']);
        $rawType = (string) ($item['type'] ?? '');
        $typeKey = 'admin.notification_types.' . $rawType;
        $typeLabel = $t($typeKey);
        if ($typeLabel === $typeKey) $typeLabel = $rawType;

        $badgeClass = 'admin-badge admin-badge-' . str_replace(['new_', '_'], ['', '-'], $rawType);
        $timeText = quaeriva_relative_time((string) ($item['created_at'] ?? ''), $translator);
        if ($timeText === '') {
            $timeText = (string) ($item['created_at'] ?? '');
        }

        echo '<div class="admin-notification-card' . ($read ? '' : ' admin-notification-card-unread') . '">'
            . '<div class="admin-notification-card-head">'
            . '<div class="admin-notification-card-meta">'
            . '<span class="' . $e($badgeClass) . '">' . $e($typeLabel) . '</span>'
            . '<span class="admin-notification-time">' . $e($timeText) . '</span>'
            . '</div>'
            . '<div class="admin-actions">';

        if (!empty($item['url'])) echo '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e($item['url']) . '">' . $t('admin.actions.view') . '</a>';
        if (!$read) echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('notifications')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="notification_read"><input type="hidden" name="page" value="notifications"><input type="hidden" name="notification_id" value="' . $e($item['id']) . '"><button class="admin-button-small" type="submit">' . $t('admin.actions.mark_read') . '</button></form>';

        echo '</div></div>'
            . '<div class="admin-notification-card-body">'
            . '<strong class="admin-notification-title"><bdi dir="auto">' . $e($item['title']) . '</bdi></strong>'
            . (!empty($item['body']) ? '<p class="admin-notification-text" dir="auto">' . $e($item['body']) . '</p>' : '')
            . '</div></div>';
    }
    if ($items === []) {
        echo '<div class="admin-empty">' . $t('admin.empty') . '</div>';
    }
    echo '</div></section>';
}

/** @param array<string,mixed> $input @param array<string,mixed> $fallback @return array<string,array<string,string>> */
function admin_theme_profile_tokens_from_post(array $input, array $fallback): array
{
    $patch = [];
    $colorKeys = ['bg','surface','surface_soft','text','muted','border','primary','primary_hover','secondary','accent','success','warning','danger'];
    foreach ($colorKeys as $key) {
        $field = 'token_color_' . $key;
        if (array_key_exists($field, $input)) $patch['colors'][$key] = (string) $input[$field];
    }
    foreach (['font_body','font_heading','font_arabic'] as $key) {
        $field = 'token_' . $key;
        if (array_key_exists($field, $input)) $patch['typography'][$key] = (string) $input[$field];
    }
    foreach (['radius_sm','radius_md','radius_lg','radius_xl'] as $key) {
        $field = 'token_' . $key;
        if (array_key_exists($field, $input)) $patch['shape'][$key] = (string) $input[$field];
    }
    $layoutFields = [
        'home_preset'=>'layout_home','question_preset'=>'layout_question','topic_preset'=>'layout_topic',
        'header_preset'=>'layout_header','density'=>'layout_density','sidebar_position'=>'layout_sidebar',
    ];
    foreach ($layoutFields as $key => $field) if (array_key_exists($field, $input)) $patch['layout'][$key] = (string) $input[$field];
    return ThemeTokenService::normalize($patch, $fallback);
}

function admin_render_theme_profiles(PDO $pdo, SettingsRepository $settings, callable $t, callable $e, string $csrfToken): void
{
    try {
        $repository = new ThemeProfileRepository($pdo);
        $profiles = $repository->all();
    } catch (Throwable) {
        echo '<section class="admin-card"><div class="admin-alert admin-alert-warning">' . $t('admin.theme_profiles_migration_required') . '</div></section>';
        return;
    }
    $activeId = (int) $settings->get('theme.profile_id', 0);
    // Migration 037 creates the tables but no row, so a fresh/upgraded install
    // showed an empty list with no "current theme". Adopt the live tokens as
    // the first profile instead of asking the admin to recreate them by hand.
    if ($profiles === []) {
        try {
            $liveTokens = ThemeTokenService::normalize((array) $settings->get('theme.tokens', ThemeTokenService::defaults()));
            $activeId = $repository->create((string) $t('admin.theme_profile_current'), '', $liveTokens);
            $repository->activate($activeId);
            $settings->put('theme.profile_id', $activeId, 'integer');
            $profiles = $repository->all();
        } catch (Throwable) {
            // ponytail: seeding is a convenience; the empty-state row below still renders.
        }
    }
    // Auto-seed flagship profiles if available on disk and not yet in table
    // Each theme ships its profile package inside its own directory, so a theme
    // installed over the filesystem carries its palette with it.
    $flagshipPackages = ['cortex', 'rawy', 'zenith', 'orbit', 'nexus', 'hikaya'];
    $existingNames = array_map(static fn(array $p): string => strtolower((string) ($p['name'] ?? '')), $profiles);
    $seededNew = false;
    foreach ($flagshipPackages as $fSlug) {
        $fPath = app_public_path('themes', $fSlug, $fSlug . '.json');
        if (is_file($fPath) && !in_array($fSlug, $existingNames, true)) {
            try {
                $pkg = ThemeProfilePackageService::import((string) file_get_contents($fPath));
                $repository->create($pkg['name'], $pkg['description'], $pkg['tokens']);
                $seededNew = true;
            } catch (Throwable) {}
        }
    }
    if ($seededNew) {
        try { $profiles = $repository->all(); } catch (Throwable) {}
    }
    $presets = ThemePresetService::all();

    echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.theme_profiles_title') . '</h3><p class="admin-muted">' . $t('admin.theme_profiles_note') . '</p></div></div><div class="admin-card-body">';
    echo '<form method="post" action="' . $e(admin_url('appearance', ['tab'=>'profiles'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="theme_profile_create"><input type="hidden" name="page" value="appearance"><div class="admin-form-grid">';
    echo '<label class="admin-field"><span>' . $t('admin.theme_profile_name') . '</span><input name="profile_name" maxlength="120"></label>';
    echo '<label class="admin-field"><span>' . $t('admin.theme_profile_preset') . '</span><select name="preset_key"><option value="">' . $t('admin.theme_profile_current') . '</option>';
    foreach ($presets as $key=>$preset) echo '<option value="' . $e($key) . '">' . $e($preset['name']) . '</option>';
    echo '</select></label><label class="admin-field admin-field-full"><span>' . $t('admin.theme_profile_description') . '</span><input name="profile_description" maxlength="500"></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.theme_profile_create') . '</button></div></form></div></section>';

    echo '<section class="admin-card"><div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.theme_profile_layout') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    if ($profiles === []) echo '<tr><td colspan="4" class="admin-empty">' . $t('admin.theme_profiles_empty') . '</td></tr>';
    foreach ($profiles as $profile) {
        $id=(int)$profile['id'];$tokens=(array)($profile['tokens']??[]);$layout=(array)($tokens['layout']??[]);$isActive=!empty($profile['is_active'])||$activeId===$id;
        echo '<tr><td><strong>' . $e($profile['name']) . '</strong><small>' . $e($profile['description'] ?? '') . '</small></td><td><code>' . $e(($layout['home_preset']??'community') . ' / ' . ($layout['question_preset']??'classic')) . '</code></td><td><span class="admin-badge ' . ($isActive?'admin-badge-success':'admin-badge-muted') . '">' . ($isActive?$t('admin.enabled'):$t('admin.disabled')) . '</span></td><td><div class="admin-actions">';
        echo '<a class="admin-button admin-button-small admin-button-secondary" target="_blank" rel="noopener" href="' . $e(quaeriva_url('', ['theme_preview'=>$id])) . '">' . $t('admin.theme_profile_preview') . '</a>';
        echo '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('appearance',['tab'=>'profiles','edit_profile'=>$id])) . '">' . $t('admin.actions.edit') . '</a>';
        echo '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('appearance',['tab'=>'profiles','export_profile'=>$id,'_csrf'=>$csrfToken])) . '">' . $t('admin.theme_profile_export') . '</a>';
        if (!$isActive) {
            foreach ([['theme_profile_apply','admin.theme_profile_apply'],['theme_profile_duplicate','admin.theme_profile_duplicate'],['theme_profile_delete','admin.actions.delete']] as [$action,$label]) {
                echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('appearance',['tab'=>'profiles'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="page" value="appearance"><input type="hidden" name="admin_action" value="' . $e($action) . '"><input type="hidden" name="profile_id" value="' . $id . '"><button' . ($action==='theme_profile_delete'?' class="admin-button-danger admin-confirm-delete"':'') . ' type="submit">' . $t($label) . '</button></form>';
            }
        }
        echo '</div></td></tr>';
    }
    echo '</tbody></table></div></section>';

    $editId=max(0,(int)($_GET['edit_profile']??0));
    if($editId>0 && ($profile=$repository->find($editId))!==null){$tokens=(array)$profile['tokens'];$colors=(array)$tokens['colors'];$shape=(array)$tokens['shape'];$layout=(array)$tokens['layout'];$type=(array)$tokens['typography'];
        echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.theme_profile_edit') . ': ' . $e($profile['name']) . '</h3><p class="admin-muted">' . $t('admin.theme_profile_edit_note') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('appearance',['tab'=>'profiles','edit_profile'=>$editId])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="page" value="appearance"><input type="hidden" name="admin_action" value="theme_profile_update"><input type="hidden" name="profile_id" value="' . $editId . '"><div class="admin-form-grid">';
        echo '<label class="admin-field"><span>' . $t('admin.theme_profile_name') . '</span><input name="profile_name" maxlength="120" value="' . $e($profile['name']) . '" required></label><label class="admin-field"><span>' . $t('admin.theme_profile_description') . '</span><input name="profile_description" maxlength="500" value="' . $e($profile['description']??'') . '"></label>';
        // The colour swatch alone hides the value, so show the hex next to it.
        foreach(['bg','surface','surface_soft','text','muted','border','primary','primary_hover','secondary','accent','success','warning','danger'] as $key){$value=(string)($colors[$key]??'#000000');echo '<label class="admin-field admin-color-field"><span>' . $e($key) . '</span><span class="admin-color-row"><input type="color" name="token_color_' . $e($key) . '" value="' . $e($value) . '"><code dir="ltr">' . $e($value) . '</code></span></label>';}
        foreach(['font_body','font_heading','font_arabic'] as $fontKey){echo '<label class="admin-field"><span>' . $e($fontKey) . '</span><select name="token_' . $e($fontKey) . '">';foreach(ThemeAppearanceService::fontStacks() as $font)echo '<option value="' . $e($font) . '"' . (($type[$fontKey]??'')===$font?' selected':'') . '>' . $e($font) . '</option>';echo '</select></label>';}
        foreach(['radius_sm','radius_md','radius_lg','radius_xl'] as $radiusKey) echo '<label class="admin-field"><span>' . $e($radiusKey) . '</span><input name="token_' . $e($radiusKey) . '" dir="ltr" value="' . $e($shape[$radiusKey]??'') . '"></label>';
        $allowed=LayoutPresetService::allowed();$layoutFields=['home_preset'=>['home','layout_home'],'question_preset'=>['question','layout_question'],'topic_preset'=>['topic','layout_topic'],'header_preset'=>['header','layout_header'],'density'=>['density','layout_density'],'sidebar_position'=>['sidebar_position','layout_sidebar']];
        foreach($layoutFields as $key=>[$kind,$field]){echo '<label class="admin-field"><span>' . $e($key) . '</span><select name="' . $e($field) . '">';foreach($allowed[$kind] as $option)echo '<option value="' . $e($option) . '"' . (($layout[$key]??'')===$option?' selected':'') . '>' . $e($option) . '</option>';echo '</select></label>';}
        echo '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';
        $versions=$repository->versions($editId);if($versions!==[]){echo '<section class="admin-card"><div class="admin-card-head"><h3>' . $t('admin.theme_profile_versions') . '</h3></div><div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>#</th><th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.fields.created_at') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';foreach($versions as $version){echo '<tr><td>' . $e($version['version_number']) . '</td><td>' . $e($version['name']) . '</td><td><bdi>' . $e($version['created_at']) . '</bdi></td><td><form method="post" action="' . $e(admin_url('appearance',['tab'=>'profiles','edit_profile'=>$editId])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="page" value="appearance"><input type="hidden" name="admin_action" value="theme_profile_restore"><input type="hidden" name="profile_id" value="' . $editId . '"><input type="hidden" name="version_id" value="' . $e($version['id']) . '"><button type="submit" class="admin-button-secondary">' . $t('admin.theme_profile_restore') . '</button></form></td></tr>';}echo '</tbody></table></div></section>';}
    }

    echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.theme_profile_import') . '</h3><p class="admin-muted">' . $t('admin.theme_profile_import_note') . '</p></div></div><div class="admin-card-body"><form method="post" enctype="multipart/form-data" action="' . $e(admin_url('appearance',['tab'=>'profiles'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="page" value="appearance"><input type="hidden" name="admin_action" value="theme_profile_import"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.appearance_package_file') . '</span><input type="file" name="theme_profile_file" accept="application/json,.json"></label><label class="admin-field admin-field-full"><span>' . $t('admin.appearance_package_json') . '</span><textarea name="theme_profile_json" dir="ltr" rows="6"></textarea></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.theme_profile_import') . '</button></div></form></div></section>';
}

function admin_render_appearance(SettingsRepository $settings, array $site, callable $t, callable $e, string $csrfToken, ?PDO $pdo = null): void
{
    $defaults = ThemeAppearanceService::defaults((array) ($site['theme'] ?? []));
    $theme = [];
    foreach (array_keys($defaults) as $key) {
        $theme[$key] = (string) $settings->get('theme.' . $key, $defaults[$key]);
    }
    $activeTheme = ThemeManager::resolveActiveName((string) $settings->get('theme.active', 'default'));
    $availableThemes = ThemeManager::available(array_values(array_unique(array_values((array) ($site['locale_directions'] ?? [])))));

    // Each tab posts only its own fields; appearance_settings_save keeps every
    // key it did not receive, so saving one tab never resets another.
    $tabs = ['themes', 'branding', 'palette', 'typography', 'layout', 'profiles', 'css', 'package'];
    $tab = (string) ($_GET['tab'] ?? 'themes');
    if (!in_array($tab, $tabs, true)) $tab = 'themes';

    $formOpen = static function (string $action, bool $multipart = false) use ($e, $csrfToken, $tab): string {
        return '<form method="post"' . ($multipart ? ' enctype="multipart/form-data"' : '') . ' action="' . $e(admin_url('appearance', ['tab' => $tab])) . '">'
            . '<input type="hidden" name="_csrf" value="' . $e($csrfToken) . '">'
            . '<input type="hidden" name="admin_action" value="' . $e($action) . '">'
            . '<input type="hidden" name="page" value="appearance">'
            . '<input type="hidden" name="tab" value="' . $e($tab) . '">';
    };
    $saveButton = '<div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form>';

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.appearance_title') . '</h2><p class="admin-muted">' . $t('admin.appearance_note') . '</p></div></div></section>';

    echo '<nav class="admin-tabs" aria-label="' . $t('admin.appearance_tabs_label') . '">';
    foreach ($tabs as $item) {
        echo '<a href="' . $e(admin_url('appearance', ['tab' => $item])) . '"' . ($tab === $item ? ' aria-current="page"' : '') . '>' . $t('admin.appearance_tabs.' . $item) . '</a>';
    }
    echo '</nav>';

    if ($tab === 'themes') {
        // Copy is translated and describes the interaction pattern rather than
        // naming a real product: the layout convention is a UX pattern, the
        // branding belongs to whoever owns it.
        $themeInfoMap = [
            'orbit' => ['title' => 'Orbit', 'colors' => ['#C83200', '#0079D3', '#EAEDF1']],
            'nexus' => ['title' => 'Nexus', 'colors' => ['#B84E00', '#0A95FF', '#FFFFFF']],
            'hikaya' => ['title' => 'Hikaya', 'colors' => ['#842339', '#A35D26', '#FDFBF7']],
            'zenith' => ['title' => 'Zenith', 'colors' => ['#4F46E5', '#0284C7', '#F0F4F8']],
            'cortex' => ['title' => 'Cortex', 'colors' => ['#0066CC', '#3B4252', '#F8F9FA']],
            'rawy' => ['title' => 'Rawy', 'colors' => ['#9E472A', '#2C5E58', '#FAF7F2']],
            'default' => ['title' => 'Default', 'colors' => ['#0E6F5C', '#C99A3B', '#F6F7F9']],
        ];

        $cardsHtml = '<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:1.25rem; margin-block-end:2rem;">';
        foreach ($availableThemes as $available) {
            $name = strtolower((string) ($available['name'] ?? ''));
            if ($name === '') continue;
            $isActive = $activeTheme === $name;
            $info = $themeInfoMap[$name] ?? ['title' => ucfirst($name), 'colors' => ['#0E6F5C', '#C99A3B', '#FFFFFF']];
            $known = isset($themeInfoMap[$name]);
            $archetypeLabel = $known ? $t('admin.theme_archetype.' . $name) : $t('admin.theme_archetype_generic');
            $descriptionLabel = $known ? $t('admin.theme_desc.' . $name) : $t('admin.theme_desc_generic');

            $swatches = '';
            foreach ($info['colors'] as $hex) {
                $swatches .= '<span style="display:inline-block; inline-size:1.25rem; block-size:1.25rem; border-radius:50%; background:' . $e($hex) . '; border:1px solid rgba(0,0,0,0.15);" title="' . $e($hex) . '"></span>';
            }

            $cardsHtml .= '<label class="admin-theme-card" style="display:flex; flex-direction:column; justify-content:space-between; padding:1.25rem; border:2px solid ' . ($isActive ? 'var(--color-primary, #0E6F5C)' : 'var(--line, #e2e8f0)') . '; border-radius:0.75rem; background:' . ($isActive ? 'rgba(14, 111, 92, 0.04)' : '#FFFFFF') . '; cursor:pointer; transition:all 0.2s ease; position:relative;">'
                . '<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-block-end:0.75rem;">'
                . '<div><strong style="font-size:1.125rem; display:block;">' . $e($info['title']) . '</strong><span style="font-size:0.75rem; color:#64748b; font-weight:600;">' . $archetypeLabel . '</span></div>'
                . '<input type="radio" name="theme_active" value="' . $e($name) . '"' . ($isActive ? ' checked' : '') . ' style="accent-color:var(--color-primary, #0E6F5C); transform:scale(1.25);">'
                . '</div>'
                . '<p style="font-size:0.8125rem; color:#475569; line-height:1.5; margin:0 0 1rem 0; flex:1;">' . $descriptionLabel . '</p>'
                . '<div style="display:flex; justify-content:space-between; align-items:center; border-block-start:1px solid rgba(0,0,0,0.06); padding-block-start:0.75rem;">'
                . '<div style="display:flex; gap:0.35rem; align-items:center;">' . $swatches . '</div>'
                . ($isActive ? '<span style="font-size:0.75rem; font-weight:700; color:var(--color-primary, #0E6F5C);">✓ ' . $t('admin.theme_profile_active_badge') . '</span>' : '<span style="font-size:0.75rem; color:#94a3b8;">' . $t('admin.theme_profile_activate') . '</span>')
                . '</div>'
                . '</label>';
        }
        $cardsHtml .= '</div>';

        // Choosing a theme is its own decision, so it gets its own form and its
        // own save. Branding below is about naming the site, not laying it out.
        echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.appearance_tabs.themes') . '</h3><p class="admin-muted">' . $t('admin.appearance_themes_note') . '</p></div></div><div class="admin-card-body">'
            . $formOpen('appearance_settings_save')
            . $cardsHtml
            . $saveButton . '</div></section>';
    }

    if ($tab === 'branding') {
        $brandModes = implode('', array_map(static function (string $mode) use ($theme, $t, $e): string {
            $checked = ($theme['brand_display_mode'] ?? 'logo_text') === $mode ? ' checked' : '';
            return '<label class="admin-brand-mode"><input type="radio" name="brand_display_mode" value="' . $e($mode) . '"' . $checked . '>'
                . '<span><strong>' . $t('admin.brand_mode.' . $mode) . '</strong>'
                . '<small>' . $t('admin.brand_mode_hint.' . $mode) . '</small></span></label>';
        }, ThemeAppearanceService::BRAND_DISPLAY_MODES));

        $logoPreview = $theme['logo_url'] === '' ? '' :
            '<span class="admin-logo-preview"><img src="' . $e($theme['logo_url']) . '" alt="" style="max-block-size:2.5rem;">'
            // The stored file keeps its generated name, so the operator sees the
            // name they actually uploaded rather than a hash.
            . '<code>' . $e($theme['logo_name'] !== '' ? $theme['logo_name'] : basename((string) parse_url($theme['logo_url'], PHP_URL_PATH))) . '</code>'
            . '<label class="admin-inline-check"><input type="checkbox" name="logo_remove" value="1"> ' . $t('admin.appearance_logo_remove') . '</label>'
            . '</span>';

        $faviconPreview = $theme['favicon_url'] === '' ? '' :
            '<span class="admin-logo-preview"><img src="' . $e($theme['favicon_url']) . '" alt="" style="max-block-size:1.5rem;">'
            . '<code>' . $e(basename((string) parse_url($theme['favicon_url'], PHP_URL_PATH))) . '</code></span>';

        // One form, three labelled groups. Splitting the form instead would mean
        // three saves for one visual decision.
        echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.appearance_brand_mark') . '</h3><p class="admin-muted">' . $t('admin.appearance_brand_mark_note') . '</p></div></div><div class="admin-card-body">'
            . $formOpen('appearance_settings_save', true)

            . '<fieldset class="admin-fieldset"><legend>' . $t('admin.appearance_group_identity') . '</legend>'
            . '<div class="admin-form-grid">'
            . '<label class="admin-field"><span>' . $t('admin.appearance_brand_text') . '</span>'
            . '<input name="brand_text" maxlength="64" value="' . $e($theme['brand_text']) . '">'
            . '<small>' . $t('admin.appearance_brand_text_hint') . '</small></label>'
            . '</div>'
            . '<div class="admin-brand-modes">' . $brandModes . '</div>'
            . '</fieldset>'

            . '<fieldset class="admin-fieldset"><legend>' . $t('admin.appearance_group_logo') . '</legend>'
            . '<div class="admin-form-grid">'
            . '<label class="admin-field"><span>' . $t('admin.appearance_logo_url') . '</span>'
            . '<input name="logo_url" dir="ltr" maxlength="500" value="' . $e($theme['logo_url']) . '">'
            . '<small>' . $t('admin.appearance_url_hint') . '</small></label>'
            . '<label class="admin-field"><span>' . $t('admin.appearance_logo_upload') . '</span>'
            . '<input type="file" name="logo_file" accept="image/png,image/jpeg,image/gif,image/webp,image/x-icon">'
            . $logoPreview
            . '<small>' . $t('admin.appearance_upload_hint') . '</small></label>'
            . '<label class="admin-field"><span>' . $t('admin.appearance_favicon_url') . '</span>'
            . '<input name="favicon_url" dir="ltr" maxlength="500" value="' . $e($theme['favicon_url']) . '">'
            . '<small>' . $t('admin.appearance_url_hint') . '</small></label>'
            . '<label class="admin-field"><span>' . $t('admin.appearance_favicon_upload') . '</span>'
            . '<input type="file" name="favicon_file" accept="image/png,image/jpeg,image/gif,image/webp,image/x-icon">'
            . $faviconPreview
            . '<small>' . $t('admin.appearance_upload_hint') . '</small></label>'
            . '</div></fieldset>'

            . '<fieldset class="admin-fieldset"><legend>' . $t('admin.appearance_group_mobile') . '</legend>'
            . '<label class="admin-inline-check admin-field-full"><input type="checkbox" name="mobile_bottom_nav" value="1"'
            . (($theme['mobile_bottom_nav'] ?? '0') === '1' ? ' checked' : '') . '> '
            . $t('admin.appearance_mobile_bottom_nav') . '</label>'
            . '<small class="admin-muted">' . $t('admin.appearance_mobile_bottom_nav_hint') . '</small>'
            . '</fieldset>'

            . $saveButton . '</div></section>';
    }

    if ($tab === 'palette') {
        $colorFields = [
            'primary' => 'admin.appearance_color_primary',
            'primary_dark' => 'admin.appearance_color_primary_dark',
            'accent' => 'admin.appearance_color_accent',
            'surface' => 'admin.appearance_color_surface',
            'surface_soft' => 'admin.appearance_color_surface_soft',
            'ink' => 'admin.appearance_color_ink',
            'ink_muted' => 'admin.appearance_color_ink_muted',
            'line' => 'admin.appearance_color_line',
            'danger' => 'admin.appearance_color_danger',
        ];

        echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.appearance_palette') . '</h3><p class="admin-muted">' . $t('admin.appearance_palette_note') . '</p></div></div><div class="admin-card-body">'
            . $formOpen('appearance_settings_save') . '<div class="admin-form-grid">';
        foreach ($colorFields as $key => $label) {
            echo '<label class="admin-field admin-color-field"><span>' . $t($label) . '</span><span class="admin-color-control">'
                . '<input type="color" name="' . $e($key) . '" value="' . $e($theme[$key]) . '" data-color-sync="' . $e($key) . '">'
                . '<input type="text" dir="ltr" maxlength="7" pattern="#[0-9A-Fa-f]{6}" value="' . $e($theme[$key]) . '" data-color-text="' . $e($key) . '" aria-label="' . $t($label) . '">'
                . '</span></label>';
        }
        echo '</div>' . $saveButton . '</div></section>';
    }

    if ($tab === 'typography') {
        $dimensionFields = [
            'radius_md' => ['label' => 'admin.appearance_radius_md', 'min' => '0.25', 'max' => '1.5'],
            'radius_lg' => ['label' => 'admin.appearance_radius_lg', 'min' => '0.5', 'max' => '2.5'],
            'container_width' => ['label' => 'admin.appearance_container_width', 'min' => '56', 'max' => '100'],
            'narrow_width' => ['label' => 'admin.appearance_narrow_width', 'min' => '32', 'max' => '64'],
            'font_size_base' => ['label' => 'admin.appearance_font_size', 'min' => '0.875', 'max' => '1.25'],
        ];

        echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.appearance_type_layout') . '</h3><p class="admin-muted">' . $t('admin.appearance_type_layout_note') . '</p></div></div><div class="admin-card-body">'
            . $formOpen('appearance_settings_save') . '<div class="admin-form-grid">'
            . '<label class="admin-field"><span>' . $t('admin.appearance_font_stack') . '</span><select name="font_stack">';
        foreach (ThemeAppearanceService::fontStacks() as $fontStack) {
            $fontLabel = $t('admin.font_stacks.' . $fontStack);
            if ($fontLabel === 'admin.font_stacks.' . $fontStack) {
                $fontLabel = $fontStack;
            }
            echo '<option value="' . $e($fontStack) . '"' . ($theme['font_stack'] === $fontStack ? ' selected' : '') . '>' . $e($fontLabel) . '</option>';
        }
        echo '</select></label>';
        foreach ($dimensionFields as $key => $field) {
            echo '<label class="admin-field"><span>' . $t($field['label']) . '</span><input name="' . $e($key) . '" type="number" min="' . $e($field['min']) . '" max="' . $e($field['max']) . '" step="0.05" value="' . $e($theme[$key]) . '"></label>';
        }
        echo '</div>' . $saveButton . '</div></section>';
    }

    if ($tab === 'layout') {
        admin_render_appearance_layout($pdo, $t, $e, $csrfToken);
    }

    if ($tab === 'profiles' && $pdo !== null) {
        admin_render_theme_profiles($pdo, $settings, $t, $e, $csrfToken);
    }

    if ($tab === 'css') {
        echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.appearance_custom_css') . '</h3><p class="admin-muted">' . $t('admin.appearance_custom_css_note') . '</p></div></div><div class="admin-card-body">'
            . $formOpen('appearance_settings_save')
            . '<div class="admin-form-grid"><label class="admin-field admin-field-full"><span>' . $t('admin.appearance_custom_css') . '</span>'
            . '<textarea name="custom_css" dir="ltr" rows="14" spellcheck="false">' . $e($theme['custom_css']) . '</textarea></label></div>'
            . $saveButton . '</div></section>';
    }

    if ($tab === 'package') {
        echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.appearance_package_title') . '</h3><p class="admin-muted">' . $t('admin.appearance_package_note') . '</p></div></div><div class="admin-card-body">'
            . '<div class="admin-actions"><a class="admin-button admin-button-secondary" href="' . $e(admin_url('appearance', ['export_theme' => 1, '_csrf' => $csrfToken])) . '">' . $t('admin.appearance_export') . '</a></div>'
            . '<div class="admin-appearance-import"><div class="admin-section-heading"><h4>' . $t('admin.appearance_import') . '</h4><p class="admin-muted">' . $t('admin.appearance_import_note') . '</p></div>'
            . '<form method="post" enctype="multipart/form-data" action="' . $e(admin_url('appearance', ['tab' => 'package'])) . '">'
            . '<input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="theme_import"><input type="hidden" name="page" value="appearance">'
            . '<div class="admin-form-grid">'
            . '<label class="admin-field"><span>' . $t('admin.appearance_package_file') . '</span><input type="file" name="theme_package" accept="application/json,.json"></label>'
            . '<label class="admin-field admin-field-full"><span>' . $t('admin.appearance_package_json') . '</span><textarea name="theme_package_json" dir="ltr" rows="6"></textarea></label>'
            . '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.appearance_import_action') . '</button></div></form></div>'
            . '<div class="admin-section-heading"><h4>' . $t('admin.appearance_reset') . '</h4><p class="admin-muted">' . $t('admin.appearance_reset_note') . '</p></div>'
            . '<form method="post" action="' . $e(admin_url('appearance', ['tab' => 'package'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="appearance_settings_reset"><input type="hidden" name="page" value="appearance">'
            . '<div class="admin-form-actions"><button class="admin-button-danger admin-confirm-delete" type="submit">' . $t('admin.appearance_reset') . '</button></div></form>'
            . '</div></section>';
    }
}

/**
 * Header, footer and sidebar regions with the widgets each currently holds.
 *
 * The regions are widget sections, but an operator looking for "manage the
 * header" has no reason to know that. This lists every region of the page in
 * reading order with its contents and a direct link to add or edit a widget
 * there, instead of leaving them to work it out from the widgets screen.
 */
function admin_render_appearance_layout(?PDO $pdo, callable $t, callable $e, string $csrfToken): void
{
    $regions = [
        'header_top' => 'admin.appearance_regions.header_top',
        'nav_extra' => 'admin.appearance_regions.nav_extra',
        'homepage_main' => 'admin.appearance_regions.homepage_main',
        'home_sidebar' => 'admin.appearance_regions.home_sidebar',
        'question_sidebar' => 'admin.appearance_regions.question_sidebar',
        'footer_col_1' => 'admin.appearance_regions.footer_col_1',
        'footer_col_2' => 'admin.appearance_regions.footer_col_2',
        'footer_col_3' => 'admin.appearance_regions.footer_col_3',
        'footer_bottom' => 'admin.appearance_regions.footer_bottom',
    ];

    $sections = [];
    $widgets = [];

    if ($pdo instanceof PDO) {
        try {
            $layoutId = (int) $pdo->query("SELECT id FROM layouts WHERE status='published' ORDER BY updated_at DESC,id DESC LIMIT 1")->fetchColumn();
            if ($layoutId > 0) {
                $statement = $pdo->prepare('SELECT id, section_key FROM layout_sections WHERE layout_id = :layout_id');
                $statement->execute(['layout_id' => $layoutId]);
                foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) {
                    $sections[(string) $row['section_key']] = (int) $row['id'];
                }

                $statement = $pdo->prepare(
                    'SELECT w.id, w.type, w.title, w.is_enabled, w.sort_order, ls.section_key
                     FROM widgets w INNER JOIN layout_sections ls ON ls.id = w.layout_section_id
                     WHERE ls.layout_id = :layout_id ORDER BY w.sort_order ASC, w.id ASC'
                );
                $statement->execute(['layout_id' => $layoutId]);
                foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) {
                    $widgets[(string) $row['section_key']][] = $row;
                }
            }
        } catch (Throwable) {
            // Before the sections migration this screen simply shows the
            // regions as empty rather than failing.
        }
    }

    echo '<section class="admin-card"><div class="admin-card-head"><div><h3>' . $t('admin.appearance_regions_title') . '</h3><p class="admin-muted">' . $t('admin.appearance_regions_note') . '</p></div></div></section>';

    echo '<div class="admin-region-grid">';
    foreach ($regions as $key => $label) {
        $sectionId = $sections[$key] ?? 0;
        $items = $widgets[$key] ?? [];

        echo '<section class="admin-card admin-region"><div class="admin-card-head"><div><h4>' . $t($label) . '</h4><p class="admin-muted"><code>' . $e($key) . '</code></p></div>'
            . ($sectionId > 0
                ? '<a class="admin-button admin-button-small" href="' . $e(admin_url('widgets', ['new_widget' => 1, 'section_id' => $sectionId, 'return' => 'appearance_layout'])) . '">' . $t('admin.appearance_region_add') . '</a>'
                : '')
            . '</div><div class="admin-card-body">';

        if ($sectionId === 0) {
            echo '<p class="admin-muted">' . $t('admin.appearance_region_missing') . '</p>';
        } elseif ($items === []) {
            echo '<p class="admin-muted">' . $t('admin.appearance_region_empty') . '</p>';
        } else {
            // Reordering is a form of hidden inputs: dragging rewrites their
            // order, the up/down buttons do the same for keyboard and
            // touch users, and the order is only persisted on submit.
            echo '<form method="post" action="' . $e(admin_url('appearance', ['tab' => 'layout'])) . '" data-widget-reorder>'
                . '<input type="hidden" name="_csrf" value="' . $e($csrfToken) . '">'
                . '<input type="hidden" name="admin_action" value="widget_reorder">'
                . '<input type="hidden" name="page" value="appearance">'
                . '<input type="hidden" name="tab" value="layout">'
                . '<ul class="admin-region-widgets" data-reorder-list>';

            foreach ($items as $item) {
                $widgetId = (int) $item['id'];
                $title = trim((string) ($item['title'] ?? ''));
                $typeLabel = admin_widget_type_label((string) $item['type'], $t, $e);

                echo '<li data-reorder-item draggable="true">'
                    . '<span class="admin-drag-handle" aria-hidden="true">⠿</span>'
                    . '<input type="hidden" name="order[]" value="' . $e($widgetId) . '">'
                    . '<a class="admin-region-widget-name" href="' . $e(admin_url('widgets', ['edit_widget' => $widgetId, 'return' => 'appearance_layout'])) . '">'
                    . '<bdi dir="auto">' . $e($title !== '' ? $title : $typeLabel) . '</bdi></a>'
                    . '<span class="admin-badge">' . $typeLabel . '</span>'
                    . (empty($item['is_enabled']) ? '<span class="admin-badge admin-badge-danger">' . $t('admin.disabled') . '</span>' : '')
                    . '<span class="admin-region-widget-actions">'
                    . '<button type="button" class="admin-button admin-button-small admin-button-secondary" data-move="up" aria-label="' . $t('admin.appearance_move_up') . '">↑</button>'
                    . '<button type="button" class="admin-button admin-button-small admin-button-secondary" data-move="down" aria-label="' . $t('admin.appearance_move_down') . '">↓</button>'
                    . '</span></li>';
            }

            echo '</ul><div class="admin-form-actions"><button type="submit">' . $t('admin.appearance_save_order') . '</button></div></form>';
        }

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

function admin_render_settings(SettingsRepository $settings, array $site, callable $t, callable $e, string $csrfToken): void
{
    $tabs = ['general', 'membership', 'community', 'maintenance', 'mail', 'notifications', 'seo'];
    $tab = (string) ($_GET['tab'] ?? 'general');
    if (!in_array($tab, $tabs, true)) $tab = 'general';

    echo '<nav class="admin-tabs" aria-label="' . $t('admin.settings_tabs_label') . '">';
    foreach ($tabs as $item) {
        echo '<a href="' . $e(admin_url('settings', ['tab' => $item])) . '"' . ($tab === $item ? ' aria-current="page"' : '') . '>' . $t('admin.settings_tabs.' . $item) . '</a>';
    }
    echo '</nav>';

    if ($tab === 'general') {
        $siteName = (string) $settings->get('site.name', $site['name'] ?? 'Quaeriva');
        $siteDescription = (string) $settings->get('site.description', $site['description'] ?? '');
        $siteTimezone = (string) $settings->get('site.timezone', $site['timezone'] ?? 'UTC');
        $timezones = quaeriva_timezone_groups();

        echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.settings_site') . '</h2><p class="admin-muted">' . $t('admin.settings_language_moved') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('settings', ['tab' => 'general'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="site_settings_save"><input type="hidden" name="page" value="settings"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.site_name') . '</span><input name="site_name" required maxlength="120" value="' . $e($siteName) . '"></label><label class="admin-field"><span>' . $t('admin.fields.site_timezone') . '</span><select name="timezone">';
        foreach ($timezones as $group => $list) {
            echo '<optgroup label="' . $e($group) . '">';
            foreach ($list as $tzKey => $tzName) {
                echo '<option value="' . $e($tzKey) . '"' . ($siteTimezone === $tzKey ? ' selected' : '') . '>' . $e($tzName) . '</option>';
            }
            echo '</optgroup>';
        }
        echo '</select><small class="admin-muted">' . $t('admin.fields.site_timezone_hint') . ' (' . $e(date('Y-m-d h:i A')) . ')</small></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.site_description') . '</span><textarea name="site_description" maxlength="300">' . $e($siteDescription) . '</textarea></label><label class="admin-field"><span>' . $t('admin.fields.admin_path') . '</span><input name="admin_path" maxlength="32" pattern="[A-Za-z0-9][A-Za-z0-9_-]*" value="' . $e(quaeriva_admin_path()) . '"><small class="admin-muted">' . $t('admin.fields.admin_path_hint') . '</small></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';
        return;
    }

    if ($tab === 'membership') {
        $registrationEnabled = (bool) $settings->get('membership.registration_enabled', true);
        $verificationRequired = (bool) $settings->get('membership.require_email_verification', false);
        $turnstileEnabled = (bool) $settings->get('security.turnstile_enabled', false);
        $turnstileSiteKey = (string) $settings->get('security.turnstile_site_key', '');
        echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.settings_membership_title') . '</h2><p class="admin-muted">' . $t('admin.settings_membership_note') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('settings', ['tab' => 'membership'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="membership_settings_save"><input type="hidden" name="page" value="settings"><div class="admin-form-grid"><label class="admin-field admin-checkbox"><input type="hidden" name="registration_enabled" value="0"><input name="registration_enabled" type="checkbox" value="1"' . ($registrationEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.registration_enabled') . '</span></label><label class="admin-field admin-checkbox"><input type="hidden" name="require_email_verification" value="0"><input name="require_email_verification" type="checkbox" value="1"' . ($verificationRequired ? ' checked' : '') . '><span>' . $t('admin.fields.require_email_verification') . '</span></label><label class="admin-field admin-checkbox"><input type="hidden" name="turnstile_enabled" value="0"><input name="turnstile_enabled" type="checkbox" value="1"' . ($turnstileEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.turnstile_enabled') . '</span></label><label class="admin-field"><span>' . $t('admin.fields.turnstile_site_key') . '</span><input dir="ltr" name="turnstile_site_key" value="' . $e($turnstileSiteKey) . '"></label><label class="admin-field"><span>' . $t('admin.fields.turnstile_secret_key') . '</span><input dir="ltr" type="password" name="turnstile_secret_key" autocomplete="new-password" placeholder="' . $t('admin.secret_saved') . '"></label><label class="admin-field admin-checkbox"><input type="checkbox" name="clear_turnstile_secret_key" value="1"><span>' . $t('admin.fields.clear_turnstile_secret_key') . '</span></label></div><div class="admin-alert">' . $t('admin.membership_email_warning') . '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';
        return;
    }

    if ($tab === 'community') {
        $commentsEnabled = (bool) $settings->get('community.comments_enabled', true);
        $followsEnabled = (bool) $settings->get('community.follows_enabled', true);
        $duplicatesEnabled = (bool) $settings->get('community.duplicate_suggestions_enabled', true);
        $questionModeration = (string) $settings->get('community.question_moderation', 'all');
        $spamModerateScore = max(10, min(90, (int) $settings->get('community.spam_moderate_score', 35)));
        $spamRejectScore = max($spamModerateScore + 5, min(100, (int) $settings->get('community.spam_reject_score', 80)));
        $quality = [];
        foreach (PostingPolicyService::defaults() as $qualityKey => $qualityDefault) {
            $quality[$qualityKey] = $settings->get('quality.' . $qualityKey, $qualityDefault);
        }
        echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.settings_community_title') . '</h2><p class="admin-muted">' . $t('admin.settings_community_note') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('settings', ['tab' => 'community'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="community_settings_save"><input type="hidden" name="page" value="settings"><div class="admin-form-grid"><label class="admin-field admin-checkbox"><input type="hidden" name="comments_enabled" value="0"><input name="comments_enabled" type="checkbox" value="1"' . ($commentsEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.comments_enabled') . '</span></label><label class="admin-field admin-checkbox"><input type="hidden" name="follows_enabled" value="0"><input name="follows_enabled" type="checkbox" value="1"' . ($followsEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.follows_enabled') . '</span></label><label class="admin-field admin-checkbox"><input type="hidden" name="duplicate_suggestions_enabled" value="0"><input name="duplicate_suggestions_enabled" type="checkbox" value="1"' . ($duplicatesEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.duplicate_suggestions_enabled') . '</span></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.question_moderation') . '</span><select name="question_moderation"><option value="all"' . ($questionModeration === 'all' ? ' selected' : '') . '>' . $t('admin.question_moderation_modes.all') . '</option><option value="new_members"' . ($questionModeration === 'new_members' ? ' selected' : '') . '>' . $t('admin.question_moderation_modes.new_members') . '</option><option value="none"' . ($questionModeration === 'none' ? ' selected' : '') . '>' . $t('admin.question_moderation_modes.none') . '</option></select></label><label class="admin-field"><span>' . $t('admin.fields.spam_moderate_score') . '</span><input type="number" min="10" max="90" name="spam_moderate_score" value="' . $e($spamModerateScore) . '"></label><label class="admin-field"><span>' . $t('admin.fields.spam_reject_score') . '</span><input type="number" min="15" max="100" name="spam_reject_score" value="' . $e($spamRejectScore) . '"></label></div>';
        echo '<hr class="admin-divider"><div class="admin-card-head"><div><h3>' . $t('admin.quality_title') . '</h3><p class="admin-muted">' . $t('admin.quality_note') . '</p></div></div><div class="admin-form-grid">';
        $numberFields = [
            'question_title_min' => 'admin.fields.quality_question_title_min', 'question_title_max' => 'admin.fields.quality_question_title_max',
            'question_body_min' => 'admin.fields.quality_question_body_min', 'question_body_max' => 'admin.fields.quality_question_body_max',
            'answer_min' => 'admin.fields.quality_answer_min', 'answer_max' => 'admin.fields.quality_answer_max',
            'comment_min' => 'admin.fields.quality_comment_min', 'comment_max' => 'admin.fields.quality_comment_max',
            'tags_min' => 'admin.fields.quality_tags_min', 'tags_max' => 'admin.fields.quality_tags_max',
            'question_rate_hour' => 'admin.fields.quality_question_rate_hour', 'answer_rate_hour' => 'admin.fields.quality_answer_rate_hour',
            'comment_rate_hour' => 'admin.fields.quality_comment_rate_hour', 'comment_rate_minute' => 'admin.fields.quality_comment_rate_minute',
            'tag_create_rate_hour' => 'admin.fields.quality_tag_create_rate_hour', 'comment_max_links' => 'admin.fields.quality_comment_max_links',
            'duplicate_content_minutes' => 'admin.fields.quality_duplicate_content_minutes',
        ];
        foreach ($numberFields as $key => $labelKey) {
            echo '<label class="admin-field"><span>' . $t($labelKey) . '</span><input type="number" min="0" max="100000" name="quality_' . $e($key) . '" value="' . $e((string) $quality[$key]) . '"></label>';
        }
        echo '<label class="admin-field admin-field-full"><span>' . $t('admin.fields.quality_banned_words') . '</span><textarea name="quality_banned_words" rows="5" maxlength="20000">' . $e((string) $quality['banned_words']) . '</textarea><small>' . $t('admin.quality_list_hint') . '</small></label>';
        echo '<label class="admin-field admin-field-full"><span>' . $t('admin.fields.quality_banned_domains') . '</span><textarea name="quality_banned_domains" rows="5" maxlength="20000">' . $e((string) $quality['banned_domains']) . '</textarea><small>' . $t('admin.quality_list_hint') . '</small></label>';
        echo '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';
        return;
    }

    if ($tab === 'maintenance') {
        $enabled = (bool) $settings->get('maintenance.enabled', false);
        $message = (string) $settings->get('maintenance.message', '');
        echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.settings_maintenance_title') . '</h2><p class="admin-muted">' . $t('admin.settings_maintenance_note') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('settings', ['tab' => 'maintenance'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="maintenance_settings_save"><input type="hidden" name="page" value="settings"><div class="admin-form-grid"><label class="admin-field admin-checkbox"><input type="hidden" name="maintenance_enabled" value="0"><input name="maintenance_enabled" type="checkbox" value="1"' . ($enabled ? ' checked' : '') . '><span>' . $t('admin.fields.maintenance_enabled') . '</span></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.maintenance_message') . '</span><textarea name="maintenance_message" maxlength="1000">' . $e($message) . '</textarea></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';
        return;
    }

    if ($tab === 'mail') {
        $mailEnabled = (bool) $settings->get('mail.enabled', false);
        $mailTransport = strtolower((string) $settings->get('mail.transport', 'mail'));
        $fromEmail = (string) $settings->get('mail.from_email', '');
        $fromName = trim((string) $settings->get('mail.from_name', $mailSettingsSiteName));
        if ($fromName === '') {
            $fromName = $mailSettingsSiteName;
        }
        $smtpHost = (string) $settings->get('mail.smtp.host', ''); $smtpPort = (int) $settings->get('mail.smtp.port', 587); $smtpEncryption = (string) $settings->get('mail.smtp.encryption', 'starttls'); $smtpAuth = (string) $settings->get('mail.smtp.auth', 'login'); $smtpUsername = (string) $settings->get('mail.smtp.username', '');
        echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.settings_mail_title') . '</h2><p class="admin-muted">' . $t('admin.settings_mail_note') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('settings', ['tab' => 'mail'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="mail_settings_save"><input type="hidden" name="page" value="settings"><div class="admin-form-grid"><label class="admin-field admin-checkbox"><input type="hidden" name="mail_enabled" value="0"><input name="mail_enabled" type="checkbox" value="1"' . ($mailEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.mail_enabled') . '</span></label><label class="admin-field"><span>' . $t('admin.fields.mail_transport') . '</span><select name="mail_transport"><option value="mail"' . ($mailTransport === 'mail' ? ' selected' : '') . '>PHP mail()</option><option value="smtp"' . ($mailTransport === 'smtp' ? ' selected' : '') . '>SMTP</option></select></label><label class="admin-field"><span>' . $t('admin.fields.mail_from_email') . '</span><input type="email" dir="ltr" name="mail_from_email" maxlength="191" value="' . $e($fromEmail) . '"></label><label class="admin-field"><span>' . $t('admin.fields.mail_from_name') . '</span><input name="mail_from_name" maxlength="120" value="' . $e($fromName) . '"></label><label class="admin-field"><span>' . $t('admin.fields.mail_smtp_host') . '</span><input dir="ltr" name="mail_smtp_host" value="' . $e($smtpHost) . '"></label><label class="admin-field"><span>' . $t('admin.fields.mail_smtp_port') . '</span><input dir="ltr" type="number" min="1" max="65535" name="mail_smtp_port" value="' . $e($smtpPort) . '"></label><label class="admin-field"><span>' . $t('admin.fields.mail_smtp_encryption') . '</span><select name="mail_smtp_encryption"><option value="none"' . ($smtpEncryption === 'none' ? ' selected' : '') . '>None</option><option value="starttls"' . ($smtpEncryption === 'starttls' ? ' selected' : '') . '>STARTTLS</option><option value="ssl"' . ($smtpEncryption === 'ssl' ? ' selected' : '') . '>SSL/TLS</option></select></label><label class="admin-field"><span>' . $t('admin.fields.mail_smtp_auth') . '</span><select name="mail_smtp_auth"><option value="login"' . ($smtpAuth === 'login' ? ' selected' : '') . '>LOGIN</option><option value="plain"' . ($smtpAuth === 'plain' ? ' selected' : '') . '>PLAIN</option></select></label><label class="admin-field"><span>' . $t('admin.fields.mail_smtp_username') . '</span><input dir="ltr" name="mail_smtp_username" value="' . $e($smtpUsername) . '"></label><label class="admin-field"><span>' . $t('admin.fields.mail_smtp_password') . '</span><input dir="ltr" type="password" name="mail_smtp_password" autocomplete="new-password" placeholder="' . $t('admin.secret_saved') . '"></label></div><div class="admin-alert">' . $t('admin.mail_php_note') . '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form><form class="admin-delivery-tests" method="post" action="' . $e(admin_url('settings', ['tab' => 'mail'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="notification_test"><input type="hidden" name="page" value="settings"><input type="hidden" name="channel" value="email"><button class="admin-button-small admin-button-secondary" type="submit">' . $t('admin.notification_test_email') . '</button></form></div></section>';
        return;
    }

    if ($tab === 'notifications') {
        $contactEnabled = (bool) $settings->get('contact.enabled', true);
        $databaseEnabled = (bool) $settings->get('notifications.database_enabled', true);
        $emailEnabled = (bool) $settings->get('notifications.email_enabled', false);
        $adminEmail = (string) $settings->get('notifications.admin_email', '');
        $telegramEnabled = (bool) $settings->get('notifications.telegram_enabled', false);
        $hasTelegramToken = trim((string) $settings->get('notifications.telegram_bot_token', '')) !== '';
        $hasTelegramChat = trim((string) $settings->get('notifications.telegram_chat_id', '')) !== '';
        $newQuestion = (bool) $settings->get('notifications.new_question', true);
        $newAnswer = (bool) $settings->get('notifications.new_answer', true);
        $newComment = (bool) $settings->get('notifications.new_comment', true);
        $newReport = (bool) $settings->get('notifications.new_report', true);
        $contactMessage = (bool) $settings->get('notifications.contact_message', true);
        echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.settings_notifications_title') . '</h2><p class="admin-muted">' . $t('admin.settings_notifications_note') . '</p></div></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('settings', ['tab' => 'notifications'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="notification_settings_save"><input type="hidden" name="page" value="settings"><div class="admin-form-grid">';
        echo '<label class="admin-field admin-checkbox"><input type="hidden" name="contact_enabled" value="0"><input name="contact_enabled" type="checkbox" value="1"' . ($contactEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.contact_enabled') . '</span></label>';
        echo '<label class="admin-field admin-checkbox"><input type="hidden" name="database_enabled" value="0"><input name="database_enabled" type="checkbox" value="1"' . ($databaseEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.database_notifications') . '</span></label>';
        echo '<label class="admin-field admin-checkbox"><input type="hidden" name="email_enabled" value="0"><input name="email_enabled" type="checkbox" value="1"' . ($emailEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.email_notifications') . '</span></label><label class="admin-field"><span>' . $t('admin.fields.admin_email') . '</span><input type="email" name="admin_email" value="' . $e($adminEmail) . '"></label>';
        echo '<label class="admin-field admin-checkbox"><input type="hidden" name="telegram_enabled" value="0"><input name="telegram_enabled" type="checkbox" value="1"' . ($telegramEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.telegram_notifications') . '</span></label><label class="admin-field"><span>' . $t('admin.fields.telegram_bot_token') . '</span><input type="password" name="telegram_bot_token" autocomplete="new-password" placeholder="' . ($hasTelegramToken ? $t('admin.secret_saved') : '') . '">' . ($hasTelegramToken ? '<span class="admin-mini-check"><input type="checkbox" name="clear_telegram_bot_token" value="1" aria-label="' . $t('admin.clear_saved_secret') . '"> ' . $t('admin.clear_saved_secret') . '</span>' : '') . '</label><label class="admin-field"><span>' . $t('admin.fields.telegram_chat_id') . '</span><input name="telegram_chat_id" dir="ltr" placeholder="' . ($hasTelegramChat ? $t('admin.secret_saved') : '') . '">' . ($hasTelegramChat ? '<span class="admin-mini-check"><input type="checkbox" name="clear_telegram_chat_id" value="1" aria-label="' . $t('admin.clear_saved_secret') . '"> ' . $t('admin.clear_saved_secret') . '</span>' : '') . '</label>';
        echo '<label class="admin-field admin-checkbox"><input type="hidden" name="notify_new_question" value="0"><input name="notify_new_question" type="checkbox" value="1"' . ($newQuestion ? ' checked' : '') . '><span>' . $t('admin.fields.notify_new_question') . '</span></label><label class="admin-field admin-checkbox"><input type="hidden" name="notify_new_answer" value="0"><input name="notify_new_answer" type="checkbox" value="1"' . ($newAnswer ? ' checked' : '') . '><span>' . $t('admin.fields.notify_new_answer') . '</span></label><label class="admin-field admin-checkbox"><input type="hidden" name="notify_new_comment" value="0"><input name="notify_new_comment" type="checkbox" value="1"' . ($newComment ? ' checked' : '') . '><span>' . $t('admin.fields.notify_new_comment') . '</span></label><label class="admin-field admin-checkbox"><input type="hidden" name="notify_new_report" value="0"><input name="notify_new_report" type="checkbox" value="1"' . ($newReport ? ' checked' : '') . '><span>' . $t('admin.fields.notify_new_report') . '</span></label><label class="admin-field admin-checkbox"><input type="hidden" name="notify_contact_message" value="0"><input name="notify_contact_message" type="checkbox" value="1"' . ($contactMessage ? ' checked' : '') . '><span>' . $t('admin.fields.notify_contact_message') . '</span></label>';
        echo '</div><div class="admin-alert">' . $t('admin.telegram_help') . '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form><div class="admin-delivery-tests"><strong>' . $t('admin.notification_test_channels') . '</strong><div class="admin-actions">';
        foreach (['database', 'email', 'telegram'] as $channel) {
            echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('settings', ['tab' => 'notifications'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="notification_test"><input type="hidden" name="page" value="settings"><input type="hidden" name="channel" value="' . $e($channel) . '"><button class="admin-button-small admin-button-secondary" type="submit">' . $t('admin.notification_test_' . $channel) . '</button></form>';
        }
        echo '</div></div></div></section>';
        return;
    }

    $siteName = (string) $settings->get('site.name', $site['name'] ?? 'Quaeriva');
    $defaultTitle = (string) $settings->get('seo.default_title', $siteName);
    $robotsMode = (string) $settings->get('seo.robots_mode', 'index,follow');
    $sitemapEnabled = (bool) $settings->get('seo.sitemap_enabled', true);
    $ogImage = (string) $settings->get('seo.og_image', '');
    $twitterSite = (string) $settings->get('seo.twitter_site', '');
    $googleVerification = (string) $settings->get('seo.google_verification', '');
    $bingVerification = (string) $settings->get('seo.bing_verification', '');
    $slugStyle = (string) $settings->get('seo.slug_style', 'unicode') === 'ascii' ? 'ascii' : 'unicode';
    $permalinkStructure = (string) $settings->get('seo.question_permalink', 'slug');
    $permalinkStructure = in_array($permalinkStructure, ['slug', 'id-slug', 'id', 'id-dash-slug'], true) ? $permalinkStructure : 'slug';
    $questionPrefix = (string) $settings->get('seo.question_prefix', 'q');
    $questionPrefix = quaeriva_permalink_prefix_is_valid($questionPrefix) ? $questionPrefix : 'q';
    $strictCsp = (bool) $settings->get('security.strict_csp', false);
    $permalinkSample = static function (string $structure) use ($questionPrefix): string {
        $sample = rawurldecode('%D9%85%D8%AB%D8%A7%D9%84');
        return match ($structure) {
            'id' => '/' . $questionPrefix . '/128',
            'id-slug' => '/' . $questionPrefix . '/128/' . $sample,
            'id-dash-slug' => '/' . $questionPrefix . '/128-' . $sample,
            default => '/' . $questionPrefix . '/' . $sample,
        };
    };
    echo '<section class="admin-card"><div class="admin-card-head"><h2>' . $t('admin.settings_seo_title') . '</h2></div><div class="admin-card-body"><p class="admin-muted">' . $t('admin.settings_seo_note') . '</p><form method="post" action="' . $e(admin_url('settings', ['tab' => 'seo'])) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="seo_settings_save"><input type="hidden" name="page" value="settings"><input type="hidden" name="seo_sitemap_enabled" value="0"><input type="hidden" name="security_strict_csp" value="0"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.seo_default_title') . '</span><input name="seo_default_title" maxlength="255" value="' . $e($defaultTitle) . '"></label><label class="admin-field"><span>' . $t('admin.fields.seo_robots_mode') . '</span><select name="seo_robots_mode"><option value="index,follow"' . ($robotsMode === 'index,follow' ? ' selected' : '') . '>index,follow</option><option value="noindex,follow"' . ($robotsMode === 'noindex,follow' ? ' selected' : '') . '>noindex,follow</option><option value="index,nofollow"' . ($robotsMode === 'index,nofollow' ? ' selected' : '') . '>index,nofollow</option><option value="noindex,nofollow"' . ($robotsMode === 'noindex,nofollow' ? ' selected' : '') . '>noindex,nofollow</option></select></label><label class="admin-field"><span>' . $t('admin.fields.seo_og_image') . '</span><input name="seo_og_image" type="url" maxlength="500" value="' . $e($ogImage) . '"></label><label class="admin-field"><span>' . $t('admin.fields.seo_twitter_site') . '</span><input name="seo_twitter_site" maxlength="120" value="' . $e($twitterSite) . '"></label><label class="admin-field"><span>' . $t('admin.fields.seo_google_verification') . '</span><input name="seo_google_verification" maxlength="255" value="' . $e($googleVerification) . '"></label><label class="admin-field"><span>' . $t('admin.fields.seo_bing_verification') . '</span><input name="seo_bing_verification" maxlength="255" value="' . $e($bingVerification) . '"></label><label class="admin-field"><span>' . $t('admin.fields.seo_slug_style') . '</span><select name="seo_slug_style"><option value="unicode"' . ($slugStyle === 'unicode' ? ' selected' : '') . '>' . $t('admin.slug_styles.unicode') . '</option><option value="ascii"' . ($slugStyle === 'ascii' ? ' selected' : '') . '>' . $t('admin.slug_styles.ascii') . '</option></select><small class="admin-muted">' . $t('admin.fields.seo_slug_style_hint') . '</small></label><label class="admin-field"><span>' . $t('admin.fields.seo_question_permalink') . '</span><select name="seo_question_permalink">' . implode('', array_map(static fn (string $structure): string => '<option value="' . $e($structure) . '"' . ($permalinkStructure === $structure ? ' selected' : '') . '>' . $e($permalinkSample($structure)) . '</option>', ['slug', 'id-slug', 'id', 'id-dash-slug'])) . '</select><small class="admin-muted">' . $t('admin.fields.seo_question_permalink_hint') . '</small></label><label class="admin-field"><span>' . $t('admin.fields.seo_question_prefix') . '</span><input name="seo_question_prefix" maxlength="32" pattern="[a-z0-9][a-z0-9_-]*" value="' . $e($questionPrefix) . '"><small class="admin-muted">' . $t('admin.fields.seo_question_prefix_hint') . '</small></label><label class="admin-field admin-checkbox"><input name="security_strict_csp" type="checkbox" value="1"' . ($strictCsp ? ' checked' : '') . '><span>' . $t('admin.fields.strict_csp') . '</span></label><p class="admin-field admin-field-full admin-muted">' . $t('admin.fields.strict_csp_hint') . '</p><label class="admin-field admin-checkbox"><input name="seo_sitemap_enabled" type="checkbox" value="1"' . ($sitemapEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.seo_sitemap_enabled') . '</span></label>'
        . '<p class="admin-field admin-field-full admin-muted">' . $t('admin.settings_sitemap_hint') . ' <a href="' . $e(quaeriva_url('sitemap.xml')) . '" target="_blank" rel="noopener"><code>' . $e(quaeriva_url('sitemap.xml')) . '</code></a></p></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';
}

function admin_render_languages(?LanguageRepository $languageRepository, SettingsRepository $settings, array $site, array $enabledLanguages, callable $t, callable $e, string $csrfToken, bool $isAdmin): void
{
    if (!$languageRepository instanceof LanguageRepository) {
        echo '<section class="admin-card"><div class="admin-card-head"><h2>' . $t('admin.pages.languages') . '</h2></div><div class="admin-card-body"><div class="admin-alert admin-alert-error">' . $t('admin.languages_migration_required') . '</div><code>php bin/migrate.php</code></div></section>';
        return;
    }

    $allLanguages = $languageRepository->all();
    $languageMap = [];
    foreach ($allLanguages as $row) $languageMap[(string) ($row['code'] ?? '')] = $row;
    $publicDefault = (string) $settings->get('site.default_locale', $site['locale'] ?? 'ar');
    $adminDefault = (string) $settings->get('admin.default_locale', $publicDefault);
    $switcherEnabled = (bool) $settings->get('site.language_switcher_enabled', true);
    $browserDetection = (bool) $settings->get('site.browser_locale_detection', false);
    $languageMode = strtolower(trim((string) $settings->get('site.language_mode', count($enabledLanguages) === 1 ? 'single' : 'multilingual')));
    if (!in_array($languageMode, ['single', 'multilingual'], true)) $languageMode = 'multilingual';
    $optionHtml = static function (array $items, string $selected) use ($e): string {
        $html = '';
        foreach ($items as $language) {
            $code = (string) ($language['code'] ?? '');
            if ($code === '') continue;
            $label = (string) ($language['native_name'] ?? $language['name'] ?? $code);
            $html .= '<option value="' . $e($code) . '"' . ($selected === $code ? ' selected' : '') . '>' . $e($label) . ' (' . $e($code) . ')</option>';
        }
        return $html;
    };

    // With one language enabled the switcher, browser detection and the
    // per-question language choice are all meaningless, and the public site
    // collapses onto that language automatically. Saying so here stops the
    // administrator hunting for a switcher that is correctly hidden.
    $enabledCount = 0;
    foreach ($allLanguages as $row) {
        if (!empty($row['is_enabled'])) $enabledCount++;
    }
    if ($languageMode === 'single') {
        echo '<section class="admin-card"><div class="admin-card-body"><div class="admin-alert">' . $t('admin.language_single_mode') . '</div></div></section>';
        $stranded = $languageRepository->taxonomyCountsOutsideLocale($publicDefault);
        if (($stranded['categories'] ?? 0) > 0 || ($stranded['tags'] ?? 0) > 0) {
            echo '<section class="admin-card"><div class="admin-card-body"><div class="admin-alert">' . $t('admin.language_taxonomy_stranded', ['categories' => $stranded['categories'], 'tags' => $stranded['tags'], 'locale' => $publicDefault]) . '</div></div></section>';
        }
    }

    $switcherDisabled = $languageMode === 'single' ? ' disabled aria-describedby="language-mode-note"' : '';
    $modeNote = $languageMode === 'single' ? '<p class="form-hint" id="language-mode-note">' . $t('admin.language_mode_inactive_note') . '</p>' : '';
    echo '<section class="admin-card"><div class="admin-card-head"><h2>' . $t('admin.language_defaults') . '</h2><p class="admin-muted">' . $t('admin.language_defaults_note') . '</p></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('languages')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="language_defaults_save"><input type="hidden" name="page" value="languages"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.public_default_locale') . '</span><select name="public_default_locale">' . $optionHtml($enabledLanguages, $publicDefault) . '</select></label><label class="admin-field"><span>' . $t('admin.fields.admin_default_locale') . '</span><select name="admin_default_locale">' . $optionHtml($enabledLanguages, $adminDefault) . '</select></label><label class="admin-field"><span>' . $t('admin.fields.language_mode') . '</span><select name="language_mode"><option value="single"' . ($languageMode === 'single' ? ' selected' : '') . '>' . $t('admin.fields.language_mode_single') . '</option><option value="multilingual"' . ($languageMode === 'multilingual' ? ' selected' : '') . '>' . $t('admin.fields.language_mode_multilingual') . '</option></select></label><label class="admin-field admin-checkbox"><input name="language_switcher_enabled" type="checkbox" value="1"' . ($switcherEnabled ? ' checked' : '') . $switcherDisabled . '><span>' . $t('admin.fields.language_switcher_enabled') . '</span></label><label class="admin-field admin-checkbox"><input name="browser_locale_detection" type="checkbox" value="1"' . ($browserDetection ? ' checked' : '') . $switcherDisabled . '><span>' . $t('admin.fields.browser_locale_detection') . '</span></label></div>' . $modeNote . '<div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.translation_import_export') . '</h2><p class="admin-muted">' . $t('admin.translation_import_export_note') . '</p></div></div><div class="admin-card-body"><form method="post" enctype="multipart/form-data" action="' . $e(admin_url('languages')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="translation_pack_import"><input type="hidden" name="page" value="languages"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.language_pack_file') . '</span><input type="file" name="language_pack" accept="application/json,.json"></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.language_pack_json') . '</span><textarea name="language_pack_json" rows="5" placeholder="{ &quot;format&quot;: &quot;quaeriva-language-pack&quot;, ... }"></textarea></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.import') . '</button></div></form></div></section>';

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.language_installed') . '</h2><p class="admin-muted">' . $t('admin.languages_table_note') . '</p></div><a class="admin-button" href="' . $e(admin_url('languages', ['new_language' => 1])) . '">' . $t('admin.actions.create') . '</a></div></section>';

    $editCode = strtolower(str_replace('_', '-', trim((string) ($_GET['edit_language'] ?? ''))));
    $newLanguage = isset($_GET['new_language']);
    $editing = $editCode !== '' && isset($languageMap[$editCode]) ? $languageMap[$editCode] : null;
    if ($newLanguage || $editing !== null) {
        $code = (string) ($editing['code'] ?? '');
        $direction = (string) ($editing['direction'] ?? 'ltr');
        $fallback = (string) ($editing['fallback_locale'] ?? '');
        $enabled = $editing === null || !empty($editing['is_enabled']);
        $sort = (int) ($editing['sort_order'] ?? 100);
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><div><h2>' . ($editing !== null ? $t('admin.language_edit') : $t('admin.language_add')) . '</h2></div><a class="admin-link" href="' . $e(admin_url('languages')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('languages')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="language_save"><input type="hidden" name="page" value="languages">';
        if ($editing !== null) echo '<input type="hidden" name="code" value="' . $e($code) . '">';
        echo '<div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.language_code') . '</span><input ' . ($editing !== null ? 'disabled ' : 'name="code" ') . 'required pattern="[A-Za-z]{2,3}([_-][A-Za-z0-9]{2,8})*" maxlength="16" value="' . $e($code) . '"></label><label class="admin-field"><span>' . $t('admin.fields.language_name') . '</span><input name="name" required maxlength="120" value="' . $e($editing['name'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.native_name') . '</span><input name="native_name" required maxlength="120" value="' . $e($editing['native_name'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.direction') . '</span><select name="direction"><option value="ltr"' . ($direction === 'ltr' ? ' selected' : '') . '>LTR</option><option value="rtl"' . ($direction === 'rtl' ? ' selected' : '') . '>RTL</option></select></label><label class="admin-field"><span>' . $t('admin.fields.fallback_locale') . '</span><select name="fallback_locale"><option value="">—</option>' . $optionHtml($allLanguages, $fallback) . '</select></label><label class="admin-field"><span>' . $t('admin.fields.sort') . '</span><input name="sort_order" type="number" value="' . $e($sort) . '"></label><label class="admin-field admin-checkbox"><input name="is_enabled" type="checkbox" value="1"' . ($enabled ? ' checked' : '') . '><span>' . $t('admin.fields.enabled') . '</span></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('languages')) . '">' . $t('admin.actions.cancel') . '</a></div></form></div></section>';
    }

    echo '<section class="admin-card"><div class="admin-table-wrap"><table class="admin-table admin-languages-table"><thead><tr><th>' . $t('admin.fields.language_name') . '</th><th>' . $t('admin.fields.language_code') . '</th><th>' . $t('admin.fields.direction') . '</th><th>' . $t('admin.fields.fallback_locale') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.translation_overrides') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    foreach ($allLanguages as $language) {
        $code = (string) $language['code'];
        $packExists = is_file(app_root_path('resources', 'lang', $code . '.php'));
        $actions = '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('languages', ['edit_language' => $code])) . '">' . $t('admin.actions.edit') . '</a><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('languages', ['edit_locale' => $code])) . '">' . $t('admin.actions.translations') . '</a>';
        $references = $languageRepository->contentReferences($code);
        $fallbackDependents = $languageRepository->fallbackDependents($code);
        $deleteBlockers = [];
        if ($code === $publicDefault || $code === $adminDefault) $deleteBlockers[] = $t('admin.language_delete_reason_default');
        if ($fallbackDependents !== []) $deleteBlockers[] = $t('admin.language_delete_reason_fallback');
        $contentReferenceCount = (int) ($references['categories'] ?? 0) + (int) ($references['tags'] ?? 0) + (int) ($references['questions'] ?? 0);
        if ($contentReferenceCount > 0) $deleteBlockers[] = $t('admin.language_delete_reason_content');
        if (!empty($language['is_enabled']) && count($enabledLanguages) <= 1) $deleteBlockers[] = $t('admin.language_delete_reason_last');
        $canDelete = $deleteBlockers === [];
        if ($isAdmin) {
            $actions .= '<form method="post" action="' . $e(admin_url('languages')) . '" class="admin-inline-form"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="language_toggle"><input type="hidden" name="page" value="languages"><input type="hidden" name="code" value="' . $e($code) . '"><button class="admin-button admin-button-small ' . (!empty($language['is_enabled']) ? 'admin-button-active' : 'admin-button-muted') . '" type="submit" aria-pressed="' . (!empty($language['is_enabled']) ? 'true' : 'false') . '">' . (!empty($language['is_enabled']) ? $t('admin.actions.disable') : $t('admin.actions.enable')) . '</button></form><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('languages', ['download_language_pack' => $code, 'source_locale' => $code === 'en' ? 'ar' : 'en', '_csrf' => $csrfToken])) . '">' . $t('admin.actions.export') . '</a>';
            if ($canDelete) {
                $actions .= '<form method="post" action="' . $e(admin_url('languages')) . '" class="admin-inline-form"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="language_delete"><input type="hidden" name="page" value="languages"><input type="hidden" name="code" value="' . $e($code) . '"><button class="admin-button admin-button-small admin-button-danger admin-confirm-delete" type="submit">' . $t('admin.actions.delete') . '</button></form>';
            } else {
                $actions .= '<span class="admin-language-delete-status"><small class="admin-muted">' . $t('admin.language_delete_blocked') . '</small><small class="admin-muted">' . $e(implode(' · ', $deleteBlockers)) . '</small></span>';
            }
        }
        // The status column sits at the far end of a table that scrolls sideways on
        // a narrow screen, so on its own it told an operator nothing about whether
        // the toggle had worked. The row itself now carries the state.
        echo '<tr class="' . (!empty($language['is_enabled']) ? '' : 'admin-row-disabled') . '"><td><strong>' . $e($language['native_name']) . '</strong><small>' . $e($language['name']) . ' · ' . ($packExists ? $t('admin.language_pack_file') : $t('admin.language_database_only')) . '</small></td><td><code>' . $e($code) . '</code></td><td><span class="admin-badge">' . strtoupper($e($language['direction'])) . '</span></td><td>' . ($language['fallback_locale'] ? '<code>' . $e($language['fallback_locale']) . '</code>' : '<span class="admin-muted">—</span>') . '</td><td><span class="admin-badge ' . (!empty($language['is_enabled']) ? '' : 'admin-badge-danger') . '">' . (!empty($language['is_enabled']) ? $t('admin.enabled') : $t('admin.disabled')) . '</span></td><td>' . $e($language['overrides_count']) . '</td><td><div class="admin-actions">' . $actions . '</div></td></tr>';
    }
    if ($allLanguages === []) echo '<tr><td colspan="7" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';

    $editLocale = strtolower(str_replace('_', '-', trim((string) ($_GET['edit_locale'] ?? ''))));
    if ($editLocale === '' || !$languageRepository->exists($editLocale)) {
        echo '<section class="admin-card"><div class="admin-card-head"><h2>' . $t('admin.translation_editor') . '</h2><p class="admin-muted">' . $t('admin.translation_choose_language') . '</p></div><div class="admin-card-body"><div class="admin-actions">';
        foreach ($allLanguages as $language) echo '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('languages', ['edit_locale' => (string) $language['code']])) . '">' . $e($language['native_name']) . '</a>';
        echo '</div></div></section>';
        return;
    }

    $language = $languageRepository->find($editLocale);
    $fallbackLocale = is_array($language) && trim((string) ($language['fallback_locale'] ?? '')) !== '' ? (string) $language['fallback_locale'] : $publicDefault;
    $baseDictionary = admin_flatten_translation_dictionary(admin_load_language_dictionary($editLocale));
    $fallbackDictionary = admin_flatten_translation_dictionary(admin_load_language_dictionary($fallbackLocale));
    $overrides = $languageRepository->overrides($editLocale);
    $keys = array_values(array_unique(array_merge(array_keys($fallbackDictionary), array_keys($baseDictionary), array_keys($overrides))));
    sort($keys, SORT_STRING);
    $search = trim((string) ($_GET['translation_q'] ?? ''));
    $translationNamespace = trim((string) ($_GET['translation_namespace'] ?? ''));
    $translationStatus = trim((string) ($_GET['translation_status'] ?? ''));
    if (!in_array($translationStatus, ['', 'override', 'fallback', 'missing'], true)) $translationStatus = '';
    $translationNamespaces = [];
    foreach ($keys as $translationKey) {
        $prefix = explode('.', $translationKey, 2)[0];
        if ($prefix !== '') $translationNamespaces[$prefix] = true;
    }
    $translationNamespaces = array_keys($translationNamespaces);
    sort($translationNamespaces, SORT_STRING);
    if ($translationNamespace !== '' && !in_array($translationNamespace, $translationNamespaces, true)) $translationNamespace = '';
    $translationStatusFor = static function (string $key) use ($overrides, $baseDictionary, $fallbackDictionary): string {
        if (array_key_exists($key, $overrides)) return 'override';
        if (!array_key_exists($key, $baseDictionary) && array_key_exists($key, $fallbackDictionary)) return 'fallback';
        if (!array_key_exists($key, $baseDictionary) || trim((string) $baseDictionary[$key]) === '') return 'missing';
        return 'pack';
    };
    $keys = array_values(array_filter($keys, static function (string $key) use ($search, $translationNamespace, $translationStatus, $translationStatusFor, $overrides, $baseDictionary, $fallbackDictionary): bool {
        if ($search !== '' && stripos($key, $search) === false && stripos((string) ($overrides[$key] ?? $baseDictionary[$key] ?? $fallbackDictionary[$key] ?? ''), $search) === false) return false;
        if ($translationNamespace !== '' && explode('.', $key, 2)[0] !== $translationNamespace) return false;
        return $translationStatus === '' || $translationStatusFor($key) === $translationStatus;
    }));
    $translationTotal = count($keys);
    $translationQuery = ['admin_page' => (int) ($_GET['translation_page'] ?? 1), 'per_page' => 100];
    $translationPagination = AdminPagination::fromQuery($translationQuery, $translationTotal, 100);
    $translationPage = $translationPagination->page;
    $translationPages = $translationPagination->pages();
    $keys = $translationPagination->slice($keys);
    $translationReturnQuery = ['edit_locale' => $editLocale, 'translation_q' => $search, 'translation_namespace' => $translationNamespace, 'translation_status' => $translationStatus, 'translation_page' => $translationPage];
    $namespaceOptions = '<option value="">' . $e($t('admin.translation_all_namespaces')) . '</option>';
    foreach ($translationNamespaces as $namespace) {
        $namespaceOptions .= '<option value="' . $e($namespace) . '"' . ($translationNamespace === $namespace ? ' selected' : '') . '>' . $e($namespace) . '</option>';
    }
    $statusOptions = '<option value="">' . $e($t('admin.translation_all_statuses')) . '</option>';
    foreach (['override', 'fallback', 'missing'] as $statusOption) {
        $statusOptions .= '<option value="' . $statusOption . '"' . ($translationStatus === $statusOption ? ' selected' : '') . '>' . $e($t('admin.translation_statuses.' . $statusOption)) . '</option>';
    }

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.translation_editor') . ' — ' . $e($editLocale) . '</h2><p class="admin-muted">' . $t('admin.translation_editor_note') . '</p></div><a class="admin-link" href="' . $e(admin_url('languages')) . '">' . $t('admin.actions.close_editor') . '</a></div><div class="admin-card-body"><form class="admin-inline-form admin-search-form" method="get" action="' . $e(admin_url('languages')) . '"><input type="hidden" name="page" value="languages"><label class="admin-field"><span>' . $t('admin.fields.language') . '</span><select name="edit_locale">' . $optionHtml($allLanguages, $editLocale) . '</select></label><label class="admin-field"><span>' . $t('admin.translation_search') . '</span><input name="translation_q" value="' . $e($search) . '"></label><label class="admin-field"><span>' . $t('admin.translation_namespace') . '</span><select name="translation_namespace">' . $namespaceOptions . '</select></label><label class="admin-field"><span>' . $t('admin.translation_status') . '</span><select name="translation_status">' . $statusOptions . '</select></label><button type="submit" class="admin-button-small">' . $t('common.search') . '</button></form>';
    echo '<div class="admin-pagination-row"><p class="admin-pagination-summary">' . $t('admin.translation_page_summary', ['page' => $translationPage, 'pages' => $translationPages, 'total' => $translationTotal]) . '</p><nav class="admin-pagination" aria-label="' . $t('admin.translation_pagination') . '">';
    if ($translationPage > 1) { $previousQuery = $translationReturnQuery; $previousQuery['translation_page'] = $translationPage - 1; echo '<a class="admin-button admin-button-small" href="' . $e(admin_url('languages', $previousQuery)) . '">' . $t('admin.pagination.previous') . '</a>'; }
    if ($translationPage < $translationPages) { $nextQuery = $translationReturnQuery; $nextQuery['translation_page'] = $translationPage + 1; echo '<a class="admin-button admin-button-small" href="' . $e(admin_url('languages', $nextQuery)) . '">' . $t('admin.pagination.next') . '</a>'; }
    echo '</nav></div>';
    if ($keys === []) echo '<p class="admin-empty">' . $t('admin.empty') . '</p>';
    foreach ($keys as $key) {
        $hasOverride = array_key_exists($key, $overrides);
        $source = $translationStatusFor($key);
        $value = (string) ($overrides[$key] ?? $baseDictionary[$key] ?? $fallbackDictionary[$key] ?? '');
        echo '<article class="admin-translation-row"><div><code>' . $e($key) . '</code><small>' . $e($t('admin.translation_statuses.' . $source)) . '</small></div><form method="post" action="' . $e(admin_url('languages', $translationReturnQuery)) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="translation_override_save"><input type="hidden" name="page" value="languages"><input type="hidden" name="locale" value="' . $e($editLocale) . '"><input type="hidden" name="translation_key" value="' . $e($key) . '"><textarea name="translation_value" rows="2">' . $e($value) . '</textarea><button class="admin-button-small" type="submit">' . $t('admin.actions.save') . '</button></form>';
        if ($hasOverride) echo '<form method="post" action="' . $e(admin_url('languages', $translationReturnQuery)) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="translation_override_delete"><input type="hidden" name="page" value="languages"><input type="hidden" name="locale" value="' . $e($editLocale) . '"><input type="hidden" name="translation_key" value="' . $e($key) . '"><button class="admin-button-small admin-button-danger" type="submit">' . $t('admin.actions.reset') . '</button></form>';
        echo '</article>';
    }
    echo '</div></section>';
}

function admin_load_language_dictionary(string $locale): array
{
    $locale = strtolower(str_replace('_', '-', trim($locale)));
    if ($locale === '' || preg_match('/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/', $locale) !== 1) return [];
    $file = app_root_path('resources', 'lang', $locale . '.php');
    if (!is_file($file)) return [];
    $dictionary = require $file;
    return is_array($dictionary) ? $dictionary : [];
}

function admin_translation_placeholders(string $value): array
{
    preg_match_all('/(?<!:):[A-Za-z][A-Za-z0-9_]*/', $value, $matches);
    $tokens = array_values(array_map('strval', $matches[0] ?? []));
    sort($tokens, SORT_STRING);
    return $tokens;
}

function admin_flatten_translation_dictionary(array $dictionary, string $prefix = ''): array
{
    $flat = [];
    foreach ($dictionary as $key => $value) {
        $path = $prefix === '' ? (string) $key : $prefix . '.' . $key;
        if (is_array($value)) {
            $flat += admin_flatten_translation_dictionary($value, $path);
        } elseif (is_string($value) || is_numeric($value)) {
            $flat[$path] = (string) $value;
        }
    }
    return $flat;
}

function admin_render_ads(SettingsRepository $settings, callable $t, callable $e, string $csrfToken, ?PDO $pdo = null): void
{
    $repository = $pdo instanceof PDO ? new AdCreativeRepository($pdo) : null;
    $creatives = $repository?->all() ?? [];
    $enabled = (bool) $settings->get('ads.enabled', false);

    $editId = max(0, (int) ($_GET['edit_ad'] ?? 0));
    $editing = $editId > 0 ? $repository?->find($editId) : null;
    $creating = isset($_GET['new_ad']);

    // Global switch and the between-answers pacing.
    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.ads_title') . '</h2><p class="admin-muted">' . $t('admin.ads_note') . '</p></div></div><div class="admin-card-body">'
        . '<div class="admin-alert">' . $t('admin.ads_trusted_code_warning') . '</div>'
        . '<form method="post" action="' . $e(admin_url('ads')) . '">'
        . '<input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="ads_settings_save"><input type="hidden" name="page" value="ads">'
        . '<div class="admin-form-grid">'
        . '<label class="admin-field admin-checkbox"><input type="hidden" name="ads_enabled" value="0"><input name="ads_enabled" type="checkbox" value="1"' . ($enabled ? ' checked' : '') . '><span>' . $t('admin.ads_enabled') . '</span></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_interval') . '</span><input name="ads_between_answers_interval" type="number" min="1" max="20" value="' . $e((int) $settings->get('ads.between_answers.interval', 2)) . '"></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_max_insertions') . '</span><input name="ads_between_answers_max_insertions" type="number" min="1" max="10" value="' . $e((int) $settings->get('ads.between_answers.max_insertions', 3)) . '"></label>'
        . '</div>';

    // Rotation is per placement: with it off a placement runs one
    // advertisement, and enabling a second one is refused.
    echo '<div class="admin-section-heading"><h3>' . $t('admin.ads_rotation_title') . '</h3>'
        . '<p class="admin-muted">' . $t('admin.ads_rotation_off') . '</p>'
        . '<p class="admin-muted">' . $t('admin.ads_rotation_on') . '</p></div><div class="admin-form-grid">';
    foreach (AdPlacementService::placements() as $placement) {
        $rotation = (bool) $settings->get('ads.' . $placement . '.rotation', false);
        // Labelled as an action on the placement, not just its name: a bare
        // placement name next to a checkbox reads as "enable this ad", which is
        // not what it does.
        echo '<label class="admin-field admin-checkbox"><input type="hidden" name="ads_' . $e($placement) . '_rotation" value="0"><input name="ads_' . $e($placement) . '_rotation" type="checkbox" value="1"' . ($rotation ? ' checked' : '') . '><span>' . $t('admin.ads_rotation_for', ['placement' => $t('admin.ads_placement_' . $placement)]) . '</span></label>';
    }
    echo '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';

    if ($repository === null) {
        echo '<section class="admin-card"><div class="admin-card-body"><div class="admin-alert admin-alert-error">' . $t('admin.ads_migration_required') . '</div><code>php bin/migrate.php</code></div></section>';
        return;
    }

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.ads_creatives_title') . '</h2><p class="admin-muted">' . $t('admin.ads_creatives_note') . '</p></div><a class="admin-button" href="' . $e(admin_url('ads', ['new_ad' => 1])) . '">' . $t('admin.ads_add') . '</a></div></section>';

    if ($creating || is_array($editing)) {
        admin_render_ad_editor($editing, $t, $e, $csrfToken);
    }

    echo '<section class="admin-card"><div class="admin-table-wrap"><table class="admin-table"><thead><tr>'
        . '<th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.ads_placement') . '</th><th>' . $t('admin.ads_kind') . '</th>'
        . '<th>' . $t('admin.ads_targeting') . '</th><th>' . $t('admin.ads_weight') . '</th>'
        . '<th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';

    $now = date('Y-m-d H:i:s');
    foreach ($creatives as $creative) {
        $id = (int) $creative['id'];
        $starts = (string) ($creative['starts_at'] ?? '');
        $ends = (string) ($creative['ends_at'] ?? '');

        if (empty($creative['is_enabled'])) {
            $status = ['admin.disabled', 'admin-badge-danger'];
        } elseif ($starts !== '' && $starts > $now) {
            $status = ['admin.ads_status_scheduled', ''];
        } elseif ($ends !== '' && $ends < $now) {
            $status = ['admin.ads_status_expired', 'admin-badge-danger'];
        } else {
            $status = ['admin.ads_status_running', ''];
        }

        $devices = trim((string) ($creative['target_devices'] ?? ''));
        $locales = trim((string) ($creative['target_locales'] ?? ''));
        $audience = (string) ($creative['target_audience'] ?? 'any');
        $targeting = [];
        if ($devices !== '') $targeting[] = $devices;
        if ($locales !== '') $targeting[] = $locales;
        if ($audience !== 'any') $targeting[] = $t('admin.menu_audience.' . $audience);

        echo '<tr><td><bdi dir="auto">' . $e((string) $creative['name']) . '</bdi></td>'
            . '<td>' . $t('admin.ads_placement_' . (string) $creative['placement']) . '</td>'
            . '<td><span class="admin-badge">' . $t(trim((string) ($creative['code'] ?? '')) !== '' ? 'admin.ads_kind_code' : 'admin.ads_kind_image') . '</span></td>'
            . '<td>' . ($targeting === [] ? '<span class="admin-muted">' . $t('admin.ads_targeting_all') . '</span>' : '<code>' . $e(implode(' · ', $targeting)) . '</code>') . '</td>'
            . '<td>' . $e((int) ($creative['weight'] ?? 1)) . '</td>'
            . '<td><span class="admin-badge ' . $e($status[1]) . '">' . $t($status[0]) . '</span></td>'
            . '<td><div class="admin-actions">'
            . '<form method="post" action="' . $e(admin_url('ads')) . '" class="admin-inline-form"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="ad_creative_toggle"><input type="hidden" name="page" value="ads"><input type="hidden" name="id" value="' . $e($id) . '"><button class="admin-button admin-button-small ' . (empty($creative['is_enabled']) ? 'admin-button-muted' : 'admin-button-active') . '" type="submit" aria-pressed="' . (empty($creative['is_enabled']) ? 'false' : 'true') . '">' . $t(empty($creative['is_enabled']) ? 'admin.actions.enable' : 'admin.actions.disable') . '</button></form>'
            . '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('ads', ['edit_ad' => $id])) . '">' . $t('admin.actions.edit') . '</a>'
            . '<form method="post" action="' . $e(admin_url('ads')) . '" class="admin-inline-form"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="ad_creative_delete"><input type="hidden" name="page" value="ads"><input type="hidden" name="id" value="' . $e($id) . '"><button class="admin-button admin-button-small admin-button-danger admin-confirm-delete" type="submit">' . $t('admin.actions.delete') . '</button></form>'
            . '</div></td></tr>';
    }

    if ($creatives === []) {
        echo '<tr><td colspan="7" class="admin-empty">' . $t('admin.ads_creatives_empty') . '</td></tr>';
    }

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

/** @param array<string, mixed>|null $editing */
function admin_render_ad_editor(?array $editing, callable $t, callable $e, string $csrfToken): void
{
    $value = static fn (string $key, string $default = ''): string => (string) ($editing[$key] ?? $default);
    $devices = array_filter(explode(',', (string) ($editing['target_devices'] ?? '')));

    $placementOptions = '';
    foreach (AdPlacementService::placements() as $placement) {
        $placementOptions .= '<option value="' . $e($placement) . '"' . ($value('placement') === $placement ? ' selected' : '') . '>' . $t('admin.ads_placement_' . $placement) . '</option>';
    }

    $audienceOptions = '';
    foreach (AdCreativeRepository::AUDIENCES as $audience) {
        $audienceOptions .= '<option value="' . $e($audience) . '"' . ($value('target_audience', 'any') === $audience ? ' selected' : '') . '>' . $t('admin.menu_audience.' . $audience) . '</option>';
    }

    $deviceBoxes = '';
    foreach (AdCreativeRepository::DEVICES as $device) {
        $checked = $devices === [] || in_array($device, $devices, true);
        $deviceBoxes .= '<label class="admin-checkbox"><input type="checkbox" name="target_devices[]" value="' . $e($device) . '"' . ($checked ? ' checked' : '') . '><span>' . $t('admin.ads_show_' . $device) . '</span></label>';
    }

    // datetime-local wants YYYY-MM-DDTHH:MM.
    $dateValue = static function (string $raw): string {
        $raw = trim($raw);
        return $raw === '' ? '' : str_replace(' ', 'T', substr($raw, 0, 16));
    };

    echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><div><h2>' . $t($editing === null ? 'admin.ads_add' : 'admin.ads_edit') . '</h2></div><a class="admin-link" href="' . $e(admin_url('ads')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body">'
        . '<form method="post" action="' . $e(admin_url('ads')) . '">'
        . '<input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="ad_creative_save"><input type="hidden" name="page" value="ads">'
        . ($editing === null ? '' : '<input type="hidden" name="id" value="' . $e((int) $editing['id']) . '">')
        . '<div class="admin-form-grid">'
        . '<label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="name" maxlength="191" required value="' . $e($value('name')) . '"></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_placement') . '</span><select name="placement">' . $placementOptions . '</select></label>'
        . '<label class="admin-field admin-field-full"><span>' . $t('admin.ads_code') . '</span><textarea name="code" dir="ltr" rows="7" maxlength="100000" spellcheck="false">' . $e($value('code')) . '</textarea><small>' . $t('admin.ads_code_hint') . '</small></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_image_url') . '</span><input name="image_url" dir="ltr" maxlength="500" value="' . $e($value('image_url')) . '"></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_link_url') . '</span><input name="link_url" dir="ltr" maxlength="500" value="' . $e($value('link_url')) . '"></label>'
        . '<label class="admin-field admin-field-full"><span>' . $t('admin.ads_alt') . '</span><input name="alt" maxlength="200" value="' . $e($value('alt')) . '"></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_image_width') . '</span><input name="image_width" type="number" min="0" max="2000" value="' . $e((int) ($editing['image_width'] ?? 0)) . '"></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_image_height') . '</span><input name="image_height" type="number" min="0" max="2000" value="' . $e((int) ($editing['image_height'] ?? 0)) . '"></label>'
        . '<div class="admin-field admin-field-full"><span>' . $t('admin.ads_devices') . '</span><div class="admin-actions">' . $deviceBoxes . '</div></div>'
        . '<label class="admin-field"><span>' . $t('admin.ads_locales') . '</span><input name="target_locales" dir="ltr" placeholder="ar,en" value="' . $e($value('target_locales')) . '"></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_audience') . '</span><select name="target_audience">' . $audienceOptions . '</select></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_starts_at') . '</span><input name="starts_at" type="datetime-local" value="' . $e($dateValue($value('starts_at'))) . '"><small>' . $t('admin.ads_starts_at_hint') . '</small></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_ends_at') . '</span><input name="ends_at" type="datetime-local" value="' . $e($dateValue($value('ends_at'))) . '"><small>' . $t('admin.ads_ends_at_hint') . '</small></label>'
        . '<label class="admin-field"><span>' . $t('admin.ads_weight') . '</span><input name="weight" type="number" min="1" max="100" value="' . $e((int) ($editing['weight'] ?? 1)) . '"><small>' . $t('admin.ads_weight_hint') . '</small></label>'
        . '<label class="admin-field"><span>' . $t('admin.fields.sort') . '</span><input name="sort_order" type="number" value="' . $e((int) ($editing['sort_order'] ?? 0)) . '"></label>'
        . '<label class="admin-field admin-checkbox"><input type="hidden" name="is_enabled" value="0"><input name="is_enabled" type="checkbox" value="1"' . ($editing === null || !empty($editing['is_enabled']) ? ' checked' : '') . '><span>' . $t('admin.fields.enabled') . '</span></label>'
        . '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('ads')) . '">' . $t('admin.actions.cancel') . '</a></div></form></div></section>';
}

function admin_render_menus(PDO $pdo, callable $t, callable $e, string $csrfToken): void
{
    $repository = new \App\Theme\MenuRepository($pdo);
    $items = $repository->all('primary');

    $editId = max(0, (int) ($_GET['edit_menu_item'] ?? 0));
    $editing = null;
    foreach ($items as $row) {
        if ((int) ($row['id'] ?? 0) === $editId) $editing = $row;
    }
    $creating = isset($_GET['new_menu_item']);

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.menus') . '</h2><p class="admin-muted">' . $t('admin.menus_note') . '</p></div><a class="admin-button" href="' . $e(admin_url('menus', ['new_menu_item' => 1])) . '">' . $t('admin.actions.create') . '</a></div></section>';

    if ($creating || $editing !== null) {
        $audienceOptions = '';
        foreach (\App\Theme\MenuRepository::AUDIENCES as $audience) {
            $selected = ((string) ($editing['audience'] ?? 'any')) === $audience ? ' selected' : '';
            $audienceOptions .= '<option value="' . $e($audience) . '"' . $selected . '>' . $t('admin.menu_audience.' . $audience) . '</option>';
        }

        // A seeded item carries a translation key instead of a literal label, so
        // it stays translated in every language until an administrator types one.
        $labelValue = (string) ($editing['label'] ?? '');
        $labelHint = $labelValue === '' && ($editing['translation_key'] ?? '') !== ''
            ? '<p class="form-hint admin-muted">' . $t('admin.menu_translated_label') . '</p>'
            : '';

        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><div><h2>' . ($editing !== null ? $t('admin.menu_item_edit') : $t('admin.menu_item_add')) . '</h2></div><a class="admin-link" href="' . $e(admin_url('menus')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body">'
            . '<form method="post" action="' . $e(admin_url('menus')) . '">'
            . '<input type="hidden" name="_csrf" value="' . $e($csrfToken) . '">'
            . '<input type="hidden" name="admin_action" value="menu_item_save">'
            . '<input type="hidden" name="page" value="menus">'
            . ($editing !== null ? '<input type="hidden" name="id" value="' . $e((int) $editing['id']) . '">' : '')
            . '<div class="admin-form-grid">'
            . '<label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="label" maxlength="191" value="' . $e($labelValue) . '"' . ($editing === null ? ' required' : '') . '>' . $labelHint . '</label>'
            . '<label class="admin-field"><span>' . $t('admin.fields.menu_url') . '</span><input name="url" dir="ltr" maxlength="500" required value="' . $e((string) ($editing['url'] ?? '/')) . '"></label>'
            . '<label class="admin-field"><span>' . $t('admin.fields.menu_audience') . '</span><select name="audience">' . $audienceOptions . '</select></label>'
            . '<label class="admin-field"><span>' . $t('admin.fields.menu_locales') . '</span><input name="locales" dir="ltr" maxlength="191" placeholder="ar,en" value="' . $e((string) ($editing['locales'] ?? '')) . '"></label>'
            . '<label class="admin-field"><span>' . $t('admin.fields.sort') . '</span><input name="sort_order" type="number" value="' . $e((int) ($editing['sort_order'] ?? 0)) . '"></label>'
            . '<label class="admin-field admin-checkbox"><input name="is_enabled" type="checkbox" value="1"' . ($editing === null || !empty($editing['is_enabled']) ? ' checked' : '') . '><span>' . $t('admin.fields.enabled') . '</span></label>'
            . '<label class="admin-field admin-checkbox"><input name="opens_new_tab" type="checkbox" value="1"' . (!empty($editing['opens_new_tab']) ? ' checked' : '') . '><span>' . $t('admin.fields.menu_new_tab') . '</span></label>'
            . '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('menus')) . '">' . $t('admin.actions.cancel') . '</a></div></form></div></section>';
    }

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.pages.menus') . '</h2><p class="admin-muted">' . $t('admin.menu_drag_hint') . '</p></div>'
        . ($items !== []
            ? '<form method="post" action="' . $e(admin_url('menus')) . '" id="menu-reorder-form" class="admin-menu-reorder" data-menu-reorder><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="menu_reorder"><input type="hidden" name="page" value="menus"><button class="admin-button" type="submit">' . $t('admin.menu_save_order') . '</button></form>'
            : '')
        . '</div><div class="admin-table-wrap"><table class="admin-table admin-menu-table"><thead><tr>'
        . '<th>' . $t('admin.fields.sort') . '</th><th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.fields.menu_url') . '</th><th>' . $t('admin.fields.menu_audience') . '</th>'
        . '<th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody data-menu-reorder-list>';

    foreach ($items as $row) {
        $id = (int) ($row['id'] ?? 0);
        $label = trim((string) ($row['label'] ?? ''));
        $displayLabel = $label !== ''
            ? $e($label)
            : '<span class="admin-muted">' . $e((string) ($row['translation_key'] ?? '')) . '</span>';

        echo '<tr class="admin-menu-item" data-menu-item data-menu-item-id="' . $e($id) . '" draggable="true">'
            . '<td class="admin-menu-reorder-cell"><input type="hidden" name="order[]" value="' . $e($id) . '" form="menu-reorder-form"><button type="button" class="admin-menu-drag-handle" draggable="true" aria-label="' . $e($t('admin.menu_drag_hint')) . '" title="' . $e($t('admin.menu_drag_hint')) . '">↕</button><div class="admin-menu-move-actions"><button type="button" class="admin-button admin-button-small admin-button-secondary" data-menu-move="up" aria-label="' . $e($t('admin.menu_move_up')) . '">↑</button><button type="button" class="admin-button admin-button-small admin-button-secondary" data-menu-move="down" aria-label="' . $e($t('admin.menu_move_down')) . '">↓</button></div></td>'
            . '<td><bdi dir="auto">' . $displayLabel . '</bdi></td>'
            . '<td><code>' . $e((string) ($row['url'] ?? '')) . '</code></td>'
            . '<td>' . $t('admin.menu_audience.' . (string) ($row['audience'] ?? 'any')) . '</td>'
            . '<td><span class="admin-badge ' . (!empty($row['is_enabled']) ? '' : 'admin-badge-danger') . '">' . (!empty($row['is_enabled']) ? $t('admin.enabled') : $t('admin.disabled')) . '</span></td>'
            . '<td><div class="admin-actions">'
            . '<a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('menus', ['edit_menu_item' => $id])) . '">' . $t('admin.actions.edit') . '</a>'
            . '<form method="post" action="' . $e(admin_url('menus')) . '" class="admin-inline-form"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="menu_item_delete"><input type="hidden" name="page" value="menus"><input type="hidden" name="id" value="' . $e($id) . '"><button class="admin-button admin-button-small admin-button-danger admin-confirm-delete" type="submit">' . $t('admin.actions.delete') . '</button></form>'
            . '</div></td></tr>';
    }

    if ($items === []) {
        echo '<tr><td colspan="6" class="admin-empty">' . $t('admin.menus_empty') . '</td></tr>';
    }

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

    $languageNames = [];
    try {
        $languageRows = $pdo->query('SELECT native_name, name FROM languages WHERE is_enabled = 1 ORDER BY sort_order ASC, id ASC')->fetchAll(PDO::FETCH_ASSOC);
        foreach ($languageRows as $languageRow) {
            $languageName = trim((string) ($languageRow['native_name'] ?? '')) ?: trim((string) ($languageRow['name'] ?? ''));
            if ($languageName !== '') $languageNames[] = $languageName;
        }
    } catch (Throwable) {
        // The managed-menu table can still be displayed while an older schema
        // is being diagnosed; the controls card then uses the catalog label.
    }
    $languageSummary = $languageNames !== []
        ? implode(' · ', array_map($e, $languageNames)) . ' · ' . $t('common.apply_language')
        : $t('common.language');
    echo '<section class="admin-card admin-menu-system-controls"><div class="admin-card-head"><div><h2>' . $t('admin.menu_system_controls_title') . '</h2><p class="admin-muted">' . $t('admin.menu_system_controls_note') . '</p></div></div><dl class="admin-menu-system-controls-list">'
        . '<div class="admin-menu-system-control"><dt>' . $t('auth.account') . '</dt><dd>' . $t('admin.menu_system_control_automatic') . '</dd></div>'
        . '<div class="admin-menu-system-control"><dt>' . $t('common.language') . '</dt><dd>' . $languageSummary . '</dd></div>'
        . '</dl></section>';
}

function admin_render_custom_code(PDO $pdo, callable $t, callable $e, string $csrfToken): void
{
    $positionLabels = [
        'head' => $t('admin.custom_code_position_head'),
        'body_start' => $t('admin.custom_code_position_body_start'),
        'body_end' => $t('admin.custom_code_position_body_end'),
    ];
    $editingId = max(0, (int) ($_GET['edit_custom_code'] ?? 0));
    $editing = null;
    if ($editingId > 0) {
        $statement = $pdo->prepare('SELECT id, name, position, code, is_enabled, sort_order FROM custom_code_snippets WHERE id=:id LIMIT 1');
        $statement->execute(['id' => $editingId]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        $editing = is_array($row) ? $row : null;
    }

    $rows = $pdo->query(
        'SELECT id, name, position, is_enabled, sort_order, updated_at FROM custom_code_snippets ORDER BY sort_order ASC, id ASC'
    )->fetchAll(PDO::FETCH_ASSOC);
    $rows = admin_pagination($rows, count($rows), $e);

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.custom_code_title') . '</h2><p class="admin-muted">' . $t('admin.custom_code_note') . '</p></div><a class="admin-button admin-button-small" href="' . $e(admin_url('custom-code', ['edit_custom_code' => 0])) . '">' . $t('admin.actions.create') . '</a></div><div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.fields.position') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.fields.sort') . '</th><th>' . $t('admin.table.updated') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    foreach ($rows as $row) {
        $id = (int) ($row['id'] ?? 0);
        $position = (string) ($row['position'] ?? '');
        $enabled = !empty($row['is_enabled']);
        echo '<tr><td><strong>' . $e($row['name'] ?? '') . '</strong></td><td><span class="admin-badge">' . ($positionLabels[$position] ?? $e($position)) . '</span></td><td><span class="admin-badge ' . ($enabled ? '' : 'admin-badge-danger') . '">' . ($enabled ? $t('admin.enabled') : $t('admin.disabled')) . '</span></td><td>' . $e($row['sort_order'] ?? 0) . '</td><td><bdi>' . $e($row['updated_at'] ?? '') . '</bdi></td><td><div class="admin-actions"><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('custom-code', ['edit_custom_code' => $id])) . '">' . $t('admin.actions.edit') . '</a>';
        admin_action_form('custom_code_toggle', '', $id, $csrfToken, $enabled ? $t('admin.actions.disable') : $t('admin.actions.enable'), 'admin-button-small admin-button-secondary', 'id');
        admin_action_form('custom_code_delete', '', $id, $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger admin-confirm-delete', 'id');
        echo '</div></td></tr>';
    }
    if ($rows === []) echo '<tr><td colspan="6" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';

    $editingPosition = (string) ($editing['position'] ?? 'head');
    $editingEnabled = $editing === null || !empty($editing['is_enabled']);
    $formQuery = $editingId > 0 ? ['edit_custom_code' => $editingId] : [];
    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t($editingId > 0 ? 'admin.custom_code_edit' : 'admin.custom_code_new') . '</h2></div></div><div class="admin-card-body"><div class="admin-alert">' . $t('admin.ads_trusted_code_warning') . '</div><form method="post" action="' . $e(admin_url('custom-code', $formQuery)) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="custom_code_save"><input type="hidden" name="page" value="custom-code"><input type="hidden" name="id" value="' . $e($editing['id'] ?? 0) . '"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="name" required maxlength="191" value="' . $e($editing['name'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.position') . '</span><select name="position">';
    foreach ($positionLabels as $position => $label) echo '<option value="' . $e($position) . '"' . ($editingPosition === $position ? ' selected' : '') . '>' . $label . '</option>';
    echo '</select></label>';
    // Scoping. Empty values mean "everywhere", so a snippet keeps its previous
    // behaviour until the operator narrows it deliberately.
    $audienceOptions = '';
    foreach (\App\Support\CustomCodeService::AUDIENCES as $audienceOption) {
        $audienceOptions .= '<option value="' . $e($audienceOption) . '"' . (((string) ($editing['audience'] ?? 'any')) === $audienceOption ? ' selected' : '') . '>' . $t('admin.menu_audience.' . $audienceOption) . '</option>';
    }
    echo '<label class="admin-field"><span>' . $t('admin.fields.custom_code_audience') . '</span><select name="audience">' . $audienceOptions . '</select></label>';
    echo '<label class="admin-field"><span>' . $t('admin.fields.custom_code_pages') . '</span><input name="pages" dir="ltr" placeholder="' . $e(implode(',', \App\Support\CustomCodeService::PAGES)) . '" value="' . $e((string) ($editing['pages'] ?? '')) . '"><small>' . $t('admin.custom_code_pages_hint') . '</small></label>';
    echo '<label class="admin-field"><span>' . $t('admin.fields.custom_code_locales') . '</span><input name="locales" dir="ltr" placeholder="ar,en" value="' . $e((string) ($editing['locales'] ?? '')) . '"></label>';
    echo '<label class="admin-field"><span>' . $t('admin.fields.sort') . '</span><input type="number" name="sort_order" value="' . $e($editing['sort_order'] ?? 0) . '"></label><label class="admin-field admin-checkbox"><input type="hidden" name="is_enabled" value="0"><input type="checkbox" name="is_enabled" value="1"' . ($editingEnabled ? ' checked' : '') . '><span>' . $t('admin.fields.enabled') . '</span></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.code') . '</span><textarea class="admin-code" dir="ltr" rows="6" maxlength="100000" name="code">' . $e($editing['code'] ?? '') . '</textarea><small>' . $t('admin.custom_code_code_hint') . '</small></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button>' . ($editingId > 0 ? '<a class="admin-button admin-button-secondary" href="' . $e(admin_url('custom-code')) . '">' . $t('admin.actions.cancel') . '</a>' : '') . '</div></form></div></section>';
}

function admin_audit_action_label(string $action, callable $t): string
{
    $semanticKeys = [
        'language.enabled' => 'admin.audit_semantic_actions.language.enabled',
        'language.disabled' => 'admin.audit_semantic_actions.language.disabled',
        'plugin.enabled' => 'admin.audit_semantic_actions.plugin.enabled',
        'plugin.disabled' => 'admin.audit_semantic_actions.plugin.disabled',
        'theme.changed' => 'admin.audit_semantic_actions.theme.changed',
        'widget.created' => 'admin.audit_semantic_actions.widget.created',
        'widget.updated' => 'admin.audit_semantic_actions.widget.updated',
        'widget.deleted' => 'admin.audit_semantic_actions.widget.deleted',
    ];
    if (isset($semanticKeys[$action])) {
        return $t($semanticKeys[$action]);
    }
    if (str_starts_with($action, 'admin.')) {
        $action = substr($action, 6);
    }
    $keys = [
        'question_moderate' => 'admin.audit_actions.question_moderate', 'question_seo_save' => 'admin.audit_actions.question_seo_save', 'question_delete' => 'admin.audit_actions.question_delete',
        'answer_best' => 'admin.audit_actions.answer_best', 'answer_unset_best' => 'admin.audit_actions.answer_unset_best', 'answer_delete' => 'admin.audit_actions.answer_delete',
        'comment_status' => 'admin.audit_actions.comment_status', 'comment_delete' => 'admin.audit_actions.comment_delete', 'category_save' => 'admin.audit_actions.category_save',
        'category_delete' => 'admin.audit_actions.category_delete', 'tag_save' => 'admin.audit_actions.tag_save', 'tag_delete' => 'admin.audit_actions.tag_delete',
        'tag_merge' => 'admin.audit_actions.tag_merge', 'user_create' => 'admin.audit_actions.user_create', 'user_update' => 'admin.audit_actions.user_update',
        'user_status' => 'admin.audit_actions.user_status', 'user_verify' => 'admin.audit_actions.user_verify', 'user_delete' => 'admin.audit_actions.user_delete',
        'report_update' => 'admin.audit_actions.report_update', 'contact_message_update' => 'admin.audit_actions.contact_message_update', 'notification_read' => 'admin.audit_actions.notification_read',
        'notifications_mark_all_read' => 'admin.audit_actions.notifications_mark_all_read', 'language_save' => 'admin.audit_actions.language_save', 'language_toggle' => 'admin.audit_actions.language_toggle',
        'language_delete' => 'admin.audit_actions.language_delete', 'language_defaults_save' => 'admin.audit_actions.language_defaults_save', 'translation_override_save' => 'admin.audit_actions.translation_override_save',
        'translation_override_delete' => 'admin.audit_actions.translation_override_delete', 'translation_pack_import' => 'admin.audit_actions.translation_pack_import', 'layout_save' => 'admin.audit_actions.layout_save',
        'layout_delete' => 'admin.audit_actions.layout_delete', 'section_save' => 'admin.audit_actions.section_save', 'section_delete' => 'admin.audit_actions.section_delete',
        'widget_save' => 'admin.audit_actions.widget_save', 'widget_toggle' => 'admin.audit_actions.widget_toggle', 'widget_delete' => 'admin.audit_actions.widget_delete', 'ads_settings_save' => 'admin.audit_actions.ads_settings_save',
        'theme_import' => 'admin.appearance_audit_imported', 'appearance_settings_save' => 'admin.appearance_audit_saved', 'appearance_settings_reset' => 'admin.appearance_audit_reset', 'custom_code_save' => 'admin.audit_actions.custom_code_save', 'custom_code_delete' => 'admin.audit_actions.custom_code_delete', 'custom_code_toggle' => 'admin.audit_actions.custom_code_toggle',
    ];
    return $t($keys[$action] ?? 'admin.audit_action_unknown');
}

/** @return array<string,mixed>|null */
function admin_audit_snapshot(mixed $json): ?array
{
    $decoded = json_decode(trim((string) $json), true);
    return is_array($decoded) ? $decoded : null;
}

/** @param array<string,mixed>|null $snapshot */
function admin_audit_snapshot_text(?array $snapshot, callable $t): string
{
    if ($snapshot === null) return $t('admin.audit_none');
    return (string) (json_encode($snapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) ?: $t('admin.audit_none'));
}

/** @param array<string,mixed>|null $before @param array<string,mixed>|null $after */
function admin_audit_resource_summary(array $row, ?array $before, ?array $after): string
{
    $resource = trim((string) ($row['resource_type'] ?? ''));
    $resourceId = (int) ($row['resource_id'] ?? 0);
    $snapshot = $after ?? $before ?? [];
    $identity = match ($resource) {
        'language' => trim((string) ($snapshot['native_name'] ?? $snapshot['name'] ?? $snapshot['code'] ?? '')),
        'plugin' => trim((string) ($snapshot['slug'] ?? '')),
        'appearance' => trim((string) ($snapshot['active'] ?? '')),
        'widget' => trim((string) ($snapshot['title'] ?? $snapshot['type'] ?? '')),
        default => '',
    };
    if ($resource === 'language' && trim((string) ($snapshot['code'] ?? '')) !== '') {
        $code = trim((string) $snapshot['code']);
        $identity = $identity === '' || $identity === $code ? $code : $identity . ' (' . $code . ')';
    }
    if ($identity !== '') $resource .= ' · ' . $identity;
    if ($resourceId > 0) $resource .= ' #' . $resourceId;
    return $resource;
}

function admin_render_audit(AdminRepository $repository, callable $t, callable $e): void
{
    try {
        $logs=$repository->auditLogs();
        $moderation=$repository->moderationActions();
    } catch(Throwable) {
        echo '<section class="admin-card"><div class="admin-card-head"><h2>'.$t('admin.audit_title').'</h2></div><div class="admin-card-body"><div class="admin-alert admin-alert-error">'.$t('admin.audit_migration_required').'</div><code>php bin/migrate.php</code></div></section>';
        return;
    }
    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>'.$t('admin.audit_title').'</h2><p class="admin-muted">'.$t('admin.audit_note').'</p></div><span class="admin-count">'.$e(count($logs)).'</span></div></section>';
    echo '<section class="admin-card"><div class="admin-card-head"><h2>'.$t('admin.audit_recent').'</h2></div><div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>'.$t('admin.table.created').'</th><th>'.$t('admin.fields.user').'</th><th>'.$t('admin.audit_action').'</th><th>'.$t('admin.audit_resource').'</th><th>'.$t('admin.audit_details').'</th></tr></thead><tbody>';
    foreach($logs as $row){
        $before=admin_audit_snapshot($row['before_json']??null);$after=admin_audit_snapshot($row['after_json']??null);
        $resource=admin_audit_resource_summary($row,$before,$after);$action=(string)($row['action']??'');
        $beforeText=admin_audit_snapshot_text($before,$t);$afterText=admin_audit_snapshot_text($after,$t);
        $details='<details class="admin-audit-details"><summary>'.$e($t('admin.audit_show_details')).'</summary><div class="admin-audit-snapshots"><div><strong>'.$e($t('admin.audit_before')).'</strong><pre><code>'.$e($beforeText).'</code></pre></div><div><strong>'.$e($t('admin.audit_after')).'</strong><pre><code>'.$e($afterText).'</code></pre></div></div></details>';
        echo '<tr><td><bdi>'.$e($row['created_at']??'').'</bdi></td><td><bdi dir="auto">'.$e($row['actor_name']??'').'</bdi></td><td><span>'.$e(admin_audit_action_label($action,$t)).'</span></td><td><bdi dir="auto">'.$e($resource).'</bdi></td><td>'.$details.'</td></tr>';
    }
    if($logs===[])echo '<tr><td colspan="5">'.$t('admin.audit_empty').'</td></tr>';
    echo '</tbody></table></div></section>';
    echo '<section class="admin-card"><div class="admin-card-head"><h2>'.$t('admin.moderation_history').'</h2></div><div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>'.$t('admin.table.created').'</th><th>'.$t('admin.fields.user').'</th><th>'.$t('admin.audit_action').'</th><th>'.$t('admin.audit_resource').'</th><th>'.$t('admin.fields.reason').'</th></tr></thead><tbody>';
    foreach($moderation as $row){$resource=(string)($row['resource_type']??'').' #'.(int)($row['resource_id']??0);echo '<tr><td><bdi>'.$e($row['created_at']??'').'</bdi></td><td><bdi dir="auto">'.$e($row['actor_name']??'').'</bdi></td><td><span>'.$e(admin_audit_action_label((string)($row['action']??''),$t)).'</span></td><td><bdi>'.$e($resource).'</bdi></td><td dir="auto">'.$e($row['reason']??'').'</td></tr>';}
    if($moderation===[])echo '<tr><td colspan="5">'.$t('admin.audit_empty').'</td></tr>';
    echo '</tbody></table></div></section>';
}

function admin_render_system(PDO $pdo, array $languages, callable $t, callable $e, string $csrfToken = '', bool $isAdmin = false): void
{
    // Both this page and bin/doctor.php read DiagnosticService, so the two
    // surfaces cannot drift apart about the same installation.
    $diagnostics = (new DiagnosticService($pdo, $_SERVER))->inspect();

    $labels = [
        'version' => 'admin.system_version',
        'root' => 'admin.system_root',
        'php' => 'admin.system_php',
        'pdo_mysql' => 'admin.system_pdo_mysql',
        'json' => 'admin.system_json',
        'mbstring' => 'admin.system_mbstring',
        'curl' => 'admin.system_curl',
        'openssl' => 'admin.system_openssl',
        'server_software' => 'admin.system_server',
        'build_id' => 'admin.system_build_id',
        'built_at' => 'admin.system_build_time',
        'release_manifest' => 'admin.system_release_manifest',
        'base_path' => 'admin.system_base_path',
        'base_source' => 'admin.system_base_source',
        'base_candidates' => 'admin.system_base_candidates',
        'base_ambiguous' => 'admin.system_base_ambiguous',
        'admin_path' => 'admin.system_admin_path',
        'https' => 'admin.system_https',
        'app_env' => 'admin.system_app_env',
        'log_path' => 'admin.system_log_path',
        'log_writable' => 'admin.system_log_writable',
        'session_handler' => 'admin.system_session_handler',
        'session_path' => 'admin.system_session_path',
        'file_permissions' => 'admin.system_file_permissions',
        'database_config_source' => 'admin.system_database_config_source',
        'database' => 'admin.system_database',
        'database_version' => 'admin.system_database_version',
        'migrations_applied' => 'admin.system_migrations',
        'migrations_pending' => 'admin.system_pending_migrations',
        'schema_integrity' => 'admin.system_schema_integrity',
        'layout' => 'admin.system_layout',
    ];
    $badges = [
        DiagnosticService::PASS => 'admin-status-active',
        DiagnosticService::WARN => 'admin-badge-gold',
        DiagnosticService::FAIL => 'admin-badge-danger',
    ];

    $pendingCheck = DiagnosticService::find($diagnostics, 'migrations_pending');
    $appliedCheck = DiagnosticService::find($diagnostics, 'migrations_applied');
    $databaseCheck = DiagnosticService::find($diagnostics, 'database');
    $schemaAvailable = ($databaseCheck['state'] ?? DiagnosticService::FAIL) !== DiagnosticService::FAIL;
    $pending = (array) ($pendingCheck['detail'] ?? []);

    $schemaState = !$schemaAvailable ? 'error' : ($pending === [] ? 'success' : 'warning');
    $schemaBadgeClass = ['success' => 'admin-status-active', 'warning' => 'admin-badge-gold', 'error' => 'admin-badge-danger'][$schemaState];
    $schemaSummary = !$schemaAvailable
        ? $t('admin.system_schema_unavailable')
        : ($pending === [] ? $t('system.database_schema_all_applied') : $t('system.database_migrations_pending', ['count' => count($pending), 'list' => implode(', ', $pending)]));

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('system.database_schema_status') . '</h2><p class="admin-muted">' . $schemaSummary . '</p></div><span class="admin-badge ' . $schemaBadgeClass . '" role="status">' . $t('admin.system_status_' . $schemaState) . '</span>'
        . ($schemaAvailable && $pending !== [] && $isAdmin ? '<form method="post" action="' . $e(admin_path_url()) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="database_upgrade"><button class="admin-button admin-button-small" type="submit">' . $t('system.database_upgrade_action') . '</button></form>' : '')
        . '</div><div class="admin-card-body"><p><strong>' . ($schemaAvailable ? $t('system.database_migrations_applied', ['count' => (int) ($appliedCheck['value'] ?? 0)]) : $t('admin.system_unknown')) . '</strong></p></div></section>';

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.system_health') . '</h2><p class="admin-muted">' . $t('admin.system_health_note') . '</p></div></div><div class="admin-table-wrap"><table class="admin-table admin-system-table"><tbody>';
    foreach ($diagnostics['checks'] as $check) {
        $label = isset($labels[$check['id']]) ? $t($labels[$check['id']]) : $e($check['id']);
        $value = $check['value'] === '' ? $t('admin.none') : $check['value'];
        $valueHtml = $check['state'] === DiagnosticService::INFO
            ? '<code>' . $e($value) . '</code>'
            : '<span class="admin-badge ' . $badges[$check['state']] . '">' . $e($value) . '</span>';
        if ($check['detail'] !== []) {
            $valueHtml .= '<small class="admin-muted">' . $e(implode(' · ', array_slice($check['detail'], 0, 6))) . '</small>';
        }
        echo '<tr><th scope="row">' . $label . '</th><td>' . $valueHtml . '</td></tr>';
    }
    echo '<tr><th scope="row">' . $t('admin.system_admin_form_action') . '</th><td><code>' . $e(admin_path_url()) . '</code></td></tr>';
    echo '<tr><th scope="row">' . $t('admin.system_languages') . '</th><td><code>' . $e(implode(', ', array_map(static fn (array $row): string => (string) ($row['code'] ?? ''), $languages))) . '</code></td></tr>';
    echo '</tbody></table></div></section>';
}

function admin_widget_type_label(string $type, callable $t, callable $e): string
{
    $key = 'admin.widget_types.' . $type;
    $label = $t($key);
    return $label === $key ? $e($type) : $label;
}

function admin_widget_type_hint(string $type, callable $t, callable $e): string
{
    $key = 'admin.widget_type_hints.' . $type;
    $label = $t($key);
    return $label === $key ? '' : $label;
}

function admin_render_widgets(AdminRepository $repository, array $languages, callable $t, callable $e, string $csrfToken): void
{
    $data = $repository->widgets();
    $layouts = $data['layouts'];
    $sections = $data['sections'];
    $widgets = admin_pagination($data['widgets'], $repository->countRows('widgets'), $e);
    $layoutMap = [];
    $sectionMap = [];
    $widgetMap = [];
    foreach ($layouts as $row) $layoutMap[(int) ($row['id'] ?? 0)] = $row;
    foreach ($sections as $row) $sectionMap[(int) ($row['id'] ?? 0)] = $row;
    foreach ($widgets as $row) $widgetMap[(int) ($row['id'] ?? 0)] = $row;

    echo '<details class="admin-card admin-widget-guide"><summary class="admin-card-head" style="cursor:pointer;list-style:none;"><div><h3 style="display:inline-block;margin:0;">' . $t('admin.widget_guide_title') . '</h3><p class="admin-muted" style="margin:4px 0 0;">' . $t('admin.widget_guide_flow') . '</p></div></summary><div class="admin-card-body">'
        . '<div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>' . $t('admin.fields.section_key') . '</th><th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.fields.description') . '</th></tr></thead><tbody>';
    $standardRegions = ['header_top', 'nav_extra', 'homepage_main', 'home_sidebar', 'question_sidebar', 'footer_col_1', 'footer_col_2', 'footer_col_3', 'footer_bottom'];
    foreach ($standardRegions as $rKey) {
        echo '<tr><td><code>' . $e($rKey) . '</code></td><td><strong>' . $t('admin.region_names.' . $rKey) . '</strong></td><td>' . $t('admin.region_descriptions.' . $rKey) . '</td></tr>';
    }
    echo '</tbody></table></div>'
        . '<p class="admin-alert" style="margin-block-start:.75rem;"><strong>' . $t('admin.widget_region_warning') . '</strong></p>'
        . '</div></details>';

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.pages.widgets') . '</h2><p class="admin-muted">' . $t('admin.widgets_live_note') . '</p></div><span class="admin-badge admin-badge-gold">' . $t('admin.live') . '</span></div></section>';

    $editLayoutId = max(0, (int) ($_GET['edit_layout'] ?? 0));
    $newLayout = isset($_GET['new_layout']);
    $editingLayout = $editLayoutId > 0 && isset($layoutMap[$editLayoutId]) ? $layoutMap[$editLayoutId] : null;
    if ($newLayout || $editingLayout !== null) {
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><h2>' . ($editingLayout !== null ? $t('admin.layout_edit') : $t('admin.layout_new')) . '</h2><a class="admin-link" href="' . $e(admin_url('widgets')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('widgets')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="layout_save"><input type="hidden" name="page" value="widgets">';
        if ($editingLayout !== null) echo '<input type="hidden" name="id" value="' . $e($editingLayout['id']) . '">';
        echo '<div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="name" required value="' . $e($editingLayout['name'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.slug') . '</span><input name="slug" value="' . $e($editingLayout['slug'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.status') . '</span><select name="status"><option value="draft"' . (($editingLayout['status'] ?? 'draft') === 'draft' ? ' selected' : '') . '>' . $t('admin.layout_status.draft') . '</option><option value="published"' . (($editingLayout['status'] ?? '') === 'published' ? ' selected' : '') . '>' . $t('admin.layout_status.published') . '</option></select></label><label class="admin-field admin-field-full"><span>' . $t('admin.fields.description') . '</span><textarea name="description">' . $e($editingLayout['description'] ?? '') . '</textarea></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('widgets')) . '">' . $t('admin.actions.cancel') . '</a></div></form></div></section>';
    }

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.layouts') . '</h2><p class="admin-muted">' . $t('admin.layouts_note') . '</p></div><a class="admin-button admin-button-small" href="' . $e(admin_url('widgets', ['new_layout' => 1])) . '">' . $t('admin.actions.create') . '</a></div><div class="admin-table-wrap"><table class="admin-table admin-layouts-table"><thead><tr><th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.fields.slug') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.sections') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    foreach ($layouts as $layout) {
        $id = (int) $layout['id'];
        echo '<tr><td><strong>' . $e($layout['name']) . '</strong><small>' . $e($layout['description'] ?? '') . '</small></td><td><code>' . $e($layout['slug']) . '</code></td><td><span class="admin-badge">' . $t('admin.layout_status.' . (string) $layout['status']) . '</span></td><td>' . $e($layout['sections_count']) . '</td><td><div class="admin-actions"><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('widgets', ['edit_layout' => $id])) . '">' . $t('admin.actions.edit') . '</a><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('widgets', ['new_section' => 1, 'layout_id' => $id])) . '">' . $t('admin.actions.add_section') . '</a>';
        admin_action_form('layout_delete', '', $id, $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger admin-confirm-delete', 'layout_id');
        echo '</div></td></tr>';
    }
    if ($layouts === []) echo '<tr><td colspan="5" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';

    $editSectionId = max(0, (int) ($_GET['edit_section'] ?? 0));
    $newSection = isset($_GET['new_section']);
    $editingSection = $editSectionId > 0 && isset($sectionMap[$editSectionId]) ? $sectionMap[$editSectionId] : null;
    if (($newSection && $layouts !== []) || $editingSection !== null) {
        $selectedLayout = (int) ($editingSection['layout_id'] ?? ($_GET['layout_id'] ?? ($layouts[0]['id'] ?? 0)));
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><h2>' . ($editingSection !== null ? $t('admin.section_edit') : $t('admin.section_new')) . '</h2><a class="admin-link" href="' . $e(admin_url('widgets')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('widgets')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="section_save"><input type="hidden" name="page" value="widgets">';
        if ($editingSection !== null) echo '<input type="hidden" name="id" value="' . $e($editingSection['id']) . '">';
        echo '<div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.layout') . '</span><select name="layout_id">';
        foreach ($layouts as $layout) echo '<option value="' . $e($layout['id']) . '"' . ((int) $layout['id'] === $selectedLayout ? ' selected' : '') . '>' . $e($layout['name']) . '</option>';
        echo '</select></label><label class="admin-field"><span>' . $t('admin.fields.section_key') . '</span><input name="section_key" pattern="[a-z0-9_-]{2,64}" required value="' . $e($editingSection['section_key'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.name') . '</span><input name="name" required value="' . $e($editingSection['name'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.sort') . '</span><input name="sort_order" type="number" value="' . $e($editingSection['sort_order'] ?? 0) . '"></label></div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('widgets')) . '">' . $t('admin.actions.cancel') . '</a></div></form></div></section>';
    }

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.sections') . '</h2><p class="admin-muted">' . $t('admin.sections_note') . '</p></div>' . ($layouts !== [] ? '<a class="admin-button admin-button-small" href="' . $e(admin_url('widgets', ['new_section' => 1])) . '">' . $t('admin.actions.create') . '</a>' : '') . '</div><div class="admin-table-wrap"><table class="admin-table admin-sections-table"><thead><tr><th>' . $t('admin.fields.layout') . '</th><th>' . $t('admin.fields.section') . '</th><th>' . $t('admin.fields.section_key') . '</th><th>' . $t('admin.fields.sort') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    foreach ($sections as $section) {
        $id = (int) $section['id'];
        echo '<tr><td>' . $e($section['layout_name']) . '</td><td><strong>' . $e($section['name']) . '</strong></td><td><code>' . $e($section['section_key']) . '</code></td><td>' . $e($section['sort_order']) . '</td><td><div class="admin-actions"><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('widgets', ['edit_section' => $id])) . '">' . $t('admin.actions.edit') . '</a><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('widgets', ['new_widget' => 1, 'section_id' => $id])) . '">' . $t('admin.actions.add_widget') . '</a>';
        admin_action_form('section_delete', '', $id, $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger admin-confirm-delete', 'section_id');
        echo '</div></td></tr>';
    }
    if ($sections === []) echo '<tr><td colspan="5" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';

    $editWidgetId = max(0, (int) ($_GET['edit_widget'] ?? 0));
    $newWidget = isset($_GET['new_widget']);
    $editingWidget = $editWidgetId > 0 && isset($widgetMap[$editWidgetId]) ? $widgetMap[$editWidgetId] : null;
    if (($newWidget && $sections !== []) || $editingWidget !== null) {
        $selectedSection = (int) ($editingWidget['layout_section_id'] ?? ($_GET['section_id'] ?? ($sections[0]['id'] ?? 0)));
        $selectedType = (string) ($editingWidget['type'] ?? 'html_block');
        $configJson = (string) ($editingWidget['config_json'] ?? '{"content_html":"<p>Safe content</p>"}');
        $enabled = $editingWidget === null || !empty($editingWidget['is_enabled']);
        echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><h2>' . ($editingWidget !== null ? $t('admin.widget_edit') : $t('admin.widget_new')) . '</h2><a class="admin-link" href="' . $e(admin_url('widgets')) . '">' . $t('admin.actions.cancel') . '</a></div><div class="admin-card-body"><form method="post" action="' . $e(admin_url('widgets')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="widget_save"><input type="hidden" name="page" value="widgets">';
        if ($editingWidget !== null) echo '<input type="hidden" name="id" value="' . $e($editingWidget['id']) . '">';
        // Set when the editor was opened from the appearance layout tab, so the
        // save returns to the region the operator was arranging.
        $widgetReturn = (string) ($_GET['return'] ?? '');
        if ($widgetReturn !== '') echo '<input type="hidden" name="return" value="' . $e($widgetReturn) . '">';
        echo '<div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.section') . '</span><select name="layout_section_id">';
        foreach ($sections as $section) echo '<option value="' . $e($section['id']) . '"' . ((int) $section['id'] === $selectedSection ? ' selected' : '') . '>' . $e($section['layout_name'] . ' / ' . $section['name']) . '</option>';
        echo '</select></label><label class="admin-field"><span>' . $t('admin.fields.type') . '</span><select name="type">';
        $widgetTypes = (new WidgetRegistry(new HtmlSanitizer()))->types(true);
        foreach ($widgetTypes as $type) {
            $label = admin_widget_type_label($type, $t, $e);
            $hint = admin_widget_type_hint($type, $t, $e);
            $display = $label . ($hint !== '' ? ' — ' . $hint : '');
            echo '<option value="' . $e($type) . '"' . ($selectedType === $type ? ' selected' : '') . '>' . $e($display) . '</option>';
        }
        echo '</select></label><label class="admin-field"><span>' . $t('admin.fields.title') . '</span><input name="title" value="' . $e($editingWidget['title'] ?? '') . '"></label><label class="admin-field"><span>' . $t('admin.fields.sort') . '</span><input name="sort_order" type="number" value="' . $e($editingWidget['sort_order'] ?? 0) . '"></label><label class="admin-field admin-checkbox"><input type="hidden" name="is_enabled" value="0"><input name="is_enabled" type="checkbox" value="1"' . ($enabled ? ' checked' : '') . '><span>' . $t('admin.fields.enabled') . '</span></label>' . admin_widget_config_fields($selectedType, $configJson, $t, $e, $repository->categories()) . '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button><a class="admin-button admin-button-secondary" href="' . $e(admin_url('widgets')) . '">' . $t('admin.actions.cancel') . '</a></div></form></div></section>';
        if ($editingWidget !== null && $languages !== []) {
            $availableLocales = array_values(array_map(static fn (array $language): string => (string) ($language['code'] ?? ''), $languages));
            $translationLocale = strtolower((string) ($_GET['widget_locale'] ?? ($availableLocales[0] ?? '')));
            if (!in_array($translationLocale, $availableLocales, true)) $translationLocale = (string) ($availableLocales[0] ?? '');
            $translation = null;
            try { $translation = $repository->widgetTranslation((int) $editingWidget['id'], $translationLocale); } catch (Throwable) {}
            $translationContent = json_decode((string) ($translation['content_json'] ?? '{}'), true);
            if (!is_array($translationContent)) $translationContent = [];
            $registry = new WidgetRegistry(new HtmlSanitizer());
            echo '<section class="admin-card admin-record-editor"><div class="admin-card-head"><div><h3>' . $t('admin.widget_translation_title') . '</h3><p class="admin-muted">' . $t('admin.widget_translation_note') . '</p></div></div><div class="admin-card-body">';
            echo '<form method="get" action="' . $e(admin_url('widgets')) . '" class="admin-inline-form"><input type="hidden" name="page" value="widgets"><input type="hidden" name="edit_widget" value="' . $e($editingWidget['id']) . '"><label>' . $t('admin.fields.locale') . '<select name="widget_locale">';
            foreach ($languages as $language) echo '<option value="' . $e($language['code']) . '"' . ((string) $language['code'] === $translationLocale ? ' selected' : '') . '>' . $e($language['native_name']) . '</option>';
            echo '</select></label><button class="admin-button-small" type="submit">' . $t('admin.actions.edit') . '</button></form>';
            echo '<form method="post" action="' . $e(admin_url('widgets')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="widget_translation_save"><input type="hidden" name="page" value="widgets"><input type="hidden" name="return" value="widget_translation"><input type="hidden" name="widget_id" value="' . $e($editingWidget['id']) . '"><input type="hidden" name="locale" value="' . $e($translationLocale) . '"><div class="admin-form-grid"><label class="admin-field"><span>' . $t('admin.fields.title') . '</span><input name="translation_title" value="' . $e($translation['title'] ?? '') . '"></label>';
            foreach ($registry->translatableFields($selectedType) as $field) {
                $value = $translationContent[$field] ?? '';
                if ($field === 'links' && is_array($value)) $value = implode("\n", array_map(static fn (mixed $item): string => is_array($item) ? (string) ($item['label'] ?? '') : (string) $item, $value));
                echo '<label class="admin-field admin-field-full"><span>' . $t('admin.widget_fields.' . $field) . '</span><textarea name="translation_config[' . $e($field) . ']" rows="3">' . $e($value) . '</textarea></label>';
            }
            echo '</div><div class="admin-form-actions"><button type="submit">' . $t('admin.actions.save') . '</button></div></form></div></section>';
        }
    }

    echo '<section class="admin-card"><div class="admin-card-head"><div><h2>' . $t('admin.widgets') . '</h2><p class="admin-muted">' . $t('admin.widgets_note') . '</p></div>' . ($sections !== [] ? '<a class="admin-button admin-button-small" href="' . $e(admin_url('widgets', ['new_widget' => 1])) . '">' . $t('admin.actions.create') . '</a>' : '') . '</div><div class="admin-table-wrap"><table class="admin-table admin-widgets-table"><thead><tr><th>' . $t('admin.fields.title') . '</th><th>' . $t('admin.fields.layout') . '</th><th>' . $t('admin.fields.type') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.fields.sort') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    foreach ($widgets as $widget) {
        $id = (int) $widget['id'];
        $typeLabel = admin_widget_type_label((string) $widget['type'], $t, $e);
        echo '<tr><td><strong>' . $e($widget['title'] ?: $typeLabel) . '</strong><small class="admin-code">' . $e($widget['config_json']) . '</small></td><td>' . $e($widget['layout_name']) . '<small>' . $e($widget['section_key']) . '</small></td><td><span class="admin-badge">' . $typeLabel . '</span></td><td><span class="admin-badge ' . (!empty($widget['is_enabled']) ? '' : 'admin-badge-danger') . '">' . (!empty($widget['is_enabled']) ? $t('admin.enabled') : $t('admin.disabled')) . '</span></td><td>' . $e($widget['sort_order']) . '</td><td><div class="admin-actions"><a class="admin-button admin-button-small admin-button-secondary" href="' . $e(admin_url('widgets', ['edit_widget' => $id])) . '">' . $t('admin.actions.edit') . '</a>';
        admin_action_form('widget_toggle', '', $id, $csrfToken, !empty($widget['is_enabled']) ? $t('admin.actions.disable') : $t('admin.actions.enable'), 'admin-button-small admin-button-secondary', 'widget_id');
        admin_action_form('widget_delete', '', $id, $csrfToken, $t('admin.actions.delete'), 'admin-button-small admin-button-danger admin-confirm-delete', 'widget_id');
        echo '</div></td></tr>';
    }
    if ($widgets === []) echo '<tr><td colspan="6" class="admin-empty">' . $t('admin.empty') . '</td></tr>';
    echo '</tbody></table></div></section>';
}

/**
 * Build the configuration inputs for every widget type.
 *
 * The editor used to present a raw JSON textarea, which required the operator
 * to know the validator's key names and bounds. Fields are generated from
 * WidgetRegistry::fields() instead, and every type's group is rendered at once
 * so switching the type select needs no round trip. Inactive groups are
 * disabled, so only the selected type's values are ever submitted.
 *
 * @param array<int, array<string, mixed>> $categories
 */
function admin_widget_config_fields(string $selectedType, string $configJson, callable $t, callable $e, array $categories = []): string
{
    $registry = new WidgetRegistry(new HtmlSanitizer());
    $current = json_decode($configJson, true);
    if (!is_array($current)) {
        $current = [];
    }

    $html = '';

    foreach ($registry->types(true) as $type) {
        $isActive = $type === $selectedType;
        $fields = $registry->fields($type);
        $group = '';

        foreach ($fields as $field) {
            $name = (string) $field['name'];
            // Values only carry over for the type being edited; another type's
            // group starts from its declared defaults.
            $value = $isActive && array_key_exists($name, $current) ? $current[$name] : ($field['default'] ?? '');
            $inputName = 'config[' . $name . ']';
            $disabled = $isActive ? '' : ' disabled';
            $label = $t('admin.widget_fields.' . $name);

            $group .= match ((string) $field['input']) {
                'html' => '<label class="admin-field admin-field-full"><span>' . $label . '</span>'
                    . '<textarea name="' . $e($inputName) . '" rows="6"' . $disabled . '>' . $e((string) $value) . '</textarea>'
                    . '<small>' . $t('admin.widget_html_help') . '</small></label>',
                'textarea' => '<label class="admin-field admin-field-full"><span>' . $label . '</span>'
                    . '<textarea name="' . $e($inputName) . '" rows="4"' . $disabled . '>' . $e(is_array($value) ? implode("\n", array_map(static fn($item) => is_array($item) ? ($item['label'] ?? '') . '|' . ($item['url'] ?? '') : (string) $item, $value)) : (string) $value) . '</textarea></label>',
                'text' => '<label class="admin-field"><span>' . $label . '</span>'
                    . '<input type="text" name="' . $e($inputName) . '" value="' . $e((string) $value) . '"' . $disabled . '></label>',
                'boolean' => '<label class="admin-field admin-checkbox"><input type="hidden" name="' . $e($inputName) . '" value="0"' . $disabled . '><input type="checkbox" name="' . $e($inputName) . '" value="1"' . (!empty($value) ? ' checked' : '') . $disabled . '><span>' . $label . '</span></label>',
                'number' => '<label class="admin-field"><span>' . $label . '</span>'
                    . '<input type="number" name="' . $e($inputName) . '" value="' . $e((string) $value) . '"'
                    . ' min="' . $e((int) ($field['min'] ?? 1)) . '" max="' . $e((int) ($field['max'] ?? 20)) . '"' . $disabled . '></label>',
                'category' => '<label class="admin-field"><span>' . $label . '</span>'
                    . '<select name="' . $e($inputName) . '"' . $disabled . '>'
                    . '<option value="">' . $t('admin.widget_any_category') . '</option>'
                    . admin_widget_category_options($categories, $value, $e)
                    . '</select></label>',
                // A fixed value still has to reach the server, so it is posted
                // as a hidden input rather than shown as a disabled control.
                'fixed' => '<input type="hidden" name="' . $e($inputName) . '" value="' . $e((string) $value) . '"' . $disabled . '>',
                default => '',
            };
        }

        if ($fields === []) {
            $group = '<p class="admin-muted admin-field-full">' . $t('admin.widget_no_options') . '</p>';
        }

        $html .= '<div class="admin-field-full admin-widget-config" data-widget-config="' . $e($type) . '"'
            . ($isActive ? '' : ' hidden') . '><div class="admin-form-grid">' . $group . '</div></div>';
    }

    return $html;
}

/** @param array<int, array<string, mixed>> $categories */
function admin_widget_category_options(array $categories, mixed $selected, callable $e): string
{
    $options = '';
    $selectedId = (int) $selected;

    foreach ($categories as $category) {
        $id = (int) ($category['id'] ?? 0);
        if ($id <= 0) continue;
        $depth = max(0, (int) ($category['depth'] ?? 0));
        $options .= '<option value="' . $e($id) . '"' . ($selectedId === $id ? ' selected' : '') . '>'
            . $e(str_repeat('— ', $depth) . (string) ($category['name'] ?? '')) . '</option>';
    }

    return $options;
}

function admin_action_form(string $action, string $moderation, int $id, string $csrfToken, string $label, string $class = '', string $idField = 'question_id'): void
{
    $e = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
    echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url()) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="' . $e($action) . '"><input type="hidden" name="page" value="' . $e((string) ($_GET['page'] ?? 'overview')) . '"><input type="hidden" name="' . $e($idField) . '" value="' . $e($id) . '">';
    if ($moderation !== '') echo '<input type="hidden" name="moderation" value="' . $e($moderation) . '">';
    echo '<button class="' . $e($class) . '" type="submit">' . $e($label) . '</button></form>';
}

function admin_render_plugins(SettingsRepository $settings, callable $t, callable $e, string $csrfToken, bool $isAdmin): void
{
    $safeMode = quaeriva_is_safe_mode();
    $pluginsDir = quaeriva_plugins_directory();
    $activePlugins = (array) $settings->get('plugins.active', []);

    echo '<section class="admin-card"><div class="admin-page-toolbar"><div><h2>' . $t('admin.plugins_title') . '</h2><p class="admin-muted">' . $t('admin.plugins_note') . '</p></div></div></section>';

    if ($safeMode) {
        echo '<div class="admin-alert admin-alert-warning" role="alert"><strong>' . $t('admin.plugins_safe_mode_warning') . '</strong></div>';
    }

    $discovered = array_values(quaeriva_load_plugins($activePlugins, false));
    $hookCatalog = CoreHooks::catalog();
    $hookDiagnostics = Hooks::diagnostics();
    echo '<section class="admin-card"><div class="admin-stat-grid admin-stat-grid-compact">'
        . '<div><strong>' . $e(count($hookCatalog)) . '</strong><span>' . $t('admin.plugin_core_hooks') . '</span></div>'
        . '<div><strong>' . $e((int) ($hookDiagnostics['total'] ?? 0)) . '</strong><span>' . $t('admin.plugin_hook_listeners') . '</span></div>'
        . '<div><strong>' . $e(count(array_filter($discovered, static fn (array $plugin): bool => ($plugin['runtime_status'] ?? '') === 'loaded'))) . '</strong><span>' . $t('admin.plugin_loaded_count') . '</span></div>'
        . '</div><p class="admin-muted">' . $t('admin.plugin_hooks_contract_note') . '</p></section>';

    echo '<section class="admin-card"><div class="admin-table-wrap"><table class="admin-table admin-plugins-table"><thead><tr><th>' . $t('admin.fields.name') . '</th><th>' . $t('admin.plugin_version') . '</th><th>' . $t('admin.plugin_author') . '</th><th>' . $t('admin.fields.status') . '</th><th>' . $t('admin.table.actions') . '</th></tr></thead><tbody>';
    if ($discovered === []) {
        echo '<tr><td colspan="5" class="admin-empty">' . $t('admin.no_plugins_found') . '</td></tr>';
    }
    foreach ($discovered as $plugin) {
        $slug = (string) $plugin['slug'];
        $isActive = !empty($plugin['is_active']);
        $isRequestedActive = !empty($plugin['is_requested_active']);
        $isCompatible = !empty($plugin['is_compatible']);
        $runtimeStatus = (string) ($plugin['runtime_status'] ?? 'disabled');
        $statusClass = $runtimeStatus === 'loaded' ? 'admin-badge-success' : (in_array($runtimeStatus, ['load_failed', 'incompatible'], true) ? 'admin-badge-danger' : 'admin-badge-muted');
        $statusLabel = match ($runtimeStatus) {
            'loaded' => $t('admin.enabled'),
            'load_failed' => $t('admin.plugin_load_failed'),
            'safe_mode' => $t('admin.plugin_safe_mode_status'),
            'incompatible' => $t('admin.plugin_incompatible'),
            'not_loaded', 'loading' => $t('admin.plugin_not_loaded'),
            default => $t('admin.disabled'),
        };
        $requiresText = trim((string) ($plugin['requires'] ?? ''));
        $requiresHtml = $requiresText !== '' ? '<small>' . $t('admin.plugin_requires', ['version' => $requiresText]) . '</small>' : '';
        echo '<tr><td><strong>' . $e($plugin['name']) . '</strong><small>' . $e($plugin['description']) . '</small>' . $requiresHtml . '</td><td><code>' . $e($plugin['version']) . '</code></td><td>' . $e($plugin['author']) . '</td><td><span class="admin-badge ' . $statusClass . '">' . $statusLabel . '</span></td><td>';
        if ($isAdmin) {
            echo '<form class="admin-inline-form" method="post" action="' . $e(admin_url('plugins')) . '"><input type="hidden" name="_csrf" value="' . $e($csrfToken) . '"><input type="hidden" name="admin_action" value="plugin_toggle"><input type="hidden" name="page" value="plugins"><input type="hidden" name="plugin" value="' . $e($slug) . '"><input type="hidden" name="enable" value="' . ($isRequestedActive ? '0' : '1') . '"><button class="admin-button-small ' . ($isRequestedActive ? 'admin-button-secondary' : '') . '" type="submit">' . ($isRequestedActive ? $t('admin.actions.disable') : $t('admin.actions.enable')) . '</button></form>';
        }
        echo '</td></tr>';
    }
    echo '</tbody></table></div></section>';
}
