<?php
/**
 * Flexia — first-run web installer (disabled after install.lock exists).
 */

declare(strict_types=1);

if (!defined('GRID_ROOT')) {
    define('GRID_ROOT', dirname(__DIR__));
}
if (!defined('ANTARA_ROOT')) {
    define('ANTARA_ROOT', GRID_ROOT);
}

@chdir(GRID_ROOT);

require_once GRID_ROOT . '/includes/bootstrap.php';
require_once GRID_ROOT . '/includes/install_public.php';
require_once GRID_ROOT . '/includes/install_pwa_apply.php';
require_once GRID_ROOT . '/includes/install_checks.php';
require_once GRID_ROOT . '/includes/install_wizard_state.php';
require_once GRID_ROOT . '/includes/install_geo.php';
require_once GRID_ROOT . '/core/auth/bootstrap_auth.php';
require_once GRID_ROOT . '/core/auth/register_user.php';
require_once GRID_ROOT . '/core/security/csrf.php';

global $settingsFile, $settingsBackupFile, $defaultSettings;

if (antara_install_is_locked()) {
    if (!headers_sent()) {
        http_response_code(404);
        header('Content-Type: text/html; charset=utf-8');
        header('X-Robots-Tag: noindex, nofollow');
    }
    $homeUrl = htmlspecialchars(antara_public_app_base(), ENT_QUOTES, 'UTF-8');
    echo '<!DOCTYPE html><html lang="es"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Instalación completada — Flexia</title><style>
        *{box-sizing:border-box}
        html,body{height:100%;margin:0}
        body{
            font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
            color:#e8f0ff;
            background:#060a12;
            min-height:100%;
            display:flex;
            align-items:center;
            justify-content:center;
            padding:1.5rem;
            overflow-x:hidden;
        }
        .bg{
            position:fixed;inset:0;pointer-events:none;z-index:0;
            background:
                radial-gradient(ellipse 80% 55% at 50% -20%,rgba(14,165,233,.22),transparent 55%),
                radial-gradient(ellipse 60% 40% at 100% 0%,rgba(125,211,252,.12),transparent 50%),
                radial-gradient(ellipse 50% 35% at 0% 100%,rgba(56,189,248,.08),transparent 45%),
                linear-gradient(165deg,#0a0f1a 0%,#0c1220 45%,#0f172a 100%);
        }
        .grain{
            position:fixed;inset:0;pointer-events:none;z-index:0;opacity:.04;
            background-image:url("data:image/svg+xml,%3Csvg viewBox=\'0 0 256 256\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'n\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.9\' numOctaves=\'4\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23n)\'/%3E%3C/svg%3E");
        }
        .shell{position:relative;z-index:1;width:100%;max-width:26rem;animation:up .7s cubic-bezier(.22,1,.36,1) both}
        @keyframes up{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}
        .card{
            background:linear-gradient(145deg,rgba(17,24,39,.92),rgba(15,23,42,.88));
            border:1px solid rgba(148,163,184,.12);
            border-radius:20px;
            padding:2rem 1.75rem 1.75rem;
            box-shadow:
                0 0 0 1px rgba(125,211,252,.06) inset,
                0 24px 48px -12px rgba(0,0,0,.45),
                0 0 80px -20px rgba(14,165,233,.15);
            text-align:center;
        }
        .badge{
            width:4.25rem;height:4.25rem;margin:0 auto 1.35rem;
            border-radius:50%;
            background:linear-gradient(135deg,rgba(14,165,233,.25),rgba(56,189,248,.08));
            border:1px solid rgba(125,211,252,.35);
            display:flex;align-items:center;justify-content:center;
            box-shadow:0 0 32px -4px rgba(14,165,233,.35),0 0 0 1px rgba(255,255,255,.04) inset;
        }
        .badge svg{width:2.1rem;height:2.1rem;color:#7dd3fc;filter:drop-shadow(0 0 8px rgba(125,211,252,.4))}
        h1{font-size:1.35rem;font-weight:700;margin:0 0 .5rem;letter-spacing:-.02em;color:#f8fafc}
        .lead{color:#94a3b8;font-size:.95rem;line-height:1.55;margin:0 0 1.5rem}
        .lead strong{color:#cbd5e1;font-weight:600}
        .btn{
            display:inline-flex;align-items:center;justify-content:center;gap:.5rem;
            width:100%;padding:.75rem 1.25rem;
            background:linear-gradient(180deg,#22d3ee,#0ea5e9);
            color:#042f2e;font-weight:700;font-size:.95rem;text-decoration:none;border-radius:12px;
            border:0;cursor:pointer;
            box-shadow:0 4px 14px -2px rgba(14,165,233,.45),0 0 0 1px rgba(255,255,255,.12) inset;
            transition:transform .15s ease,box-shadow .15s ease,filter .15s ease;
        }
        .btn:hover{filter:brightness(1.06);transform:translateY(-1px);box-shadow:0 8px 24px -4px rgba(14,165,233,.5),0 0 0 1px rgba(255,255,255,.15) inset}
        .btn:active{transform:translateY(0)}
        .btn svg{width:1.1rem;height:1.1rem}
        .foot{margin-top:1.25rem;font-size:.8rem;color:#64748b}
    </style></head><body><div class="bg" aria-hidden="true"></div><div class="grain" aria-hidden="true"></div><div class="shell"><div class="card">';
    echo '<div class="badge" role="img" aria-label="Completado"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg></div>';
    echo '<h1>Instalación completada</h1>';
    echo '<p class="lead">El instalador <strong>ya no está disponible</strong> en esta pantalla: la configuración inicial ya se realizó en este servidor.</p>';
    echo '<a class="btn" href="' . $homeUrl . '"><span>Ir al inicio</span><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>';
    echo '<p class="foot">Flexia</p></div></div></body></html>';
    exit;
}

if (AntaraStore::tryConfigRead() === null) {
    require_once GRID_ROOT . '/core/auth/vault_config_recovery.php';
    antara_vault_config_recovery_respond_and_exit();
}

$step = isset($_GET['step']) ? (int)$_GET['step'] : 1;
if ($step < 1 || $step > 3) {
    $step = 1;
}

$installSettingsSnapshot = grid_read_settings_for_api($settingsFile, $settingsBackupFile, $defaultSettings);
$installWizardDraft = antara_install_wizard_read_draft();
$installWizardHasProgress = antara_install_wizard_has_progress($installSettingsSnapshot);

if (strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET' && isset($_GET['fresh']) && (string)$_GET['fresh'] === '1') {
    antara_install_begin_fresh_wizard(false);
    $installSettingsSnapshot = grid_read_settings_for_api($settingsFile, $settingsBackupFile, $defaultSettings);
    $installWizardDraft = [];
    $installWizardHasProgress = false;
    if (!headers_sent()) {
        header('Location: ' . antara_install_base_url() . '?step=1&cleared=1', true, 302);
    }
    exit;
}

$installExternalAuxRecommendation = null;
if (strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET' && $step <= 1 && !$installWizardHasProgress) {
    $installExternalAuxRecommendation = antara_install_reset_workspace_json_for_new_install();
    $installSettingsSnapshot = grid_read_settings_for_api($settingsFile, $settingsBackupFile, $defaultSettings);
}

antara_install_wizard_sync_session_from_draft($installSettingsSnapshot);
antara_install_wizard_keepalive();
$installFormatCountCaption = antara_install_format_count_caption();

if (strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') {
    $allowedStep = antara_install_wizard_max_step();
    if (!empty($_SESSION['install_wizard_company_ok'])) {
        $allowedStep = max($allowedStep, 2);
    }
    if (!empty($_SESSION['install_pending_finish'])) {
        $pendCheck = $_SESSION['install_pending_finish'];
        $modeCheck = (string)($pendCheck['access_mode'] ?? '');
        if ($modeCheck === 'public' || ($modeCheck === 'first_user' && (string)($pendCheck['password'] ?? '') !== '')) {
            $allowedStep = max($allowedStep, 3);
        }
    }
    if ($installWizardHasProgress && $step > $allowedStep) {
        if (!headers_sent()) {
            header('Location: ' . antara_install_base_url() . '?step=' . $allowedStep, true, 302);
        }
        exit;
    }
}

$checks = antara_install_environment_checks();
$blocking = antara_install_checks_have_blocking_errors($checks);
$csrfToken = AntaraCsrf::token();
$errors = [];
$notices = [];
if (!empty($_SESSION['install_flash_notices']) && is_array($_SESSION['install_flash_notices'])) {
    $notices = $_SESSION['install_flash_notices'];
    unset($_SESSION['install_flash_notices']);
}

$installPreview = null;
if ($step === 1 || $step === 2 || $step === 3) {
    $installPreview = $installSettingsSnapshot;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $body = $_POST;
    $csrfIn = is_string($body['csrf'] ?? null) ? (string)$body['csrf'] : '';
    $action = (string)($body['install_action'] ?? '');
    $csrfCheck = antara_install_wizard_verify_csrf($csrfIn, $action, $installSettingsSnapshot);
    $csrfToken = $csrfCheck['csrf'];
    if (!$csrfCheck['valid']) {
        if (antara_install_wizard_has_progress($installSettingsSnapshot) && $action !== '') {
            $_SESSION['install_flash_notices'] = [
                'Se renovó la protección del formulario. Tus datos guardados se conservan: vuelve a pulsar Continuar.',
            ];
            $recoverStep = max(1, min(3, $step));
            header('Location: ' . antara_install_base_url() . '?step=' . $recoverStep, true, 302);
            exit;
        }
        $errors[] = 'No se pudo enviar el formulario. Recarga la página e inténtalo de nuevo.';
    }

    if ($errors === [] && $action === 'wizard_autosave') {
        $wStep = (int) ($body['wizard_step'] ?? 1);
        $settingsForAutosave = grid_read_settings_for_api($settingsFile, $settingsBackupFile, $defaultSettings);
        $autosaveOk = antara_install_wizard_save_autosave_from_body($body, $wStep, $settingsForAutosave);
        if (!headers_sent()) {
            header('Content-Type: application/json; charset=utf-8');
            header('Cache-Control: no-store');
        }
        echo json_encode([
            'ok' => $autosaveOk,
            'step' => max(1, min(3, $wStep)),
            'max_step' => antara_install_wizard_max_step(),
            'csrf' => AntaraCsrf::token(),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    if ($errors === [] && $action === 'wizard_fix') {
        $fid = trim((string)($body['wizard_fix_id'] ?? ''));
        if ($fid !== 'htaccess' && $fid !== 'user_ini') {
            $errors[] = 'Acción de corrección no válida.';
        } else {
            $r = antara_install_apply_wizard_fix($fid);
            if ($r['ok']) {
                $_SESSION['install_flash_notices'] = [$r['message']];
                header('Location: ' . antara_install_base_url() . '?step=1', true, 302);
                exit;
            }
            $errors[] = $r['message'];
        }
    }

    if ($errors === [] && $action === 'company') {
        if ($blocking) {
            $errors[] = 'Corrige los errores del entorno antes de continuar.';
        } else {
            $kind = (string)($body['deployment_kind'] ?? 'personal');
            if (!in_array($kind, ['personal', 'company'], true)) {
                $errors[] = 'Indica si el uso es personal o de empresa.';
            }

            $cur = grid_read_settings_for_api($settingsFile, $settingsBackupFile, $defaultSettings);

            if ($errors === [] && $kind === 'personal') {
                $display = trim((string)($body['personalDisplayName'] ?? ''));
                if ($display === '') {
                    $errors[] = 'Indica un nombre para mostrar (cómo identificar tu espacio y contacto).';
                } elseif (!preg_match('/^.{1,200}$/u', $display)) {
                    $errors[] = 'El nombre para mostrar es demasiado largo.';
                }
            }

            if ($errors === [] && $kind === 'company') {
                $companyName = trim((string)($body['companyName'] ?? ''));
                $companyAddress = trim((string)($body['companyAddress'] ?? ''));
                $companyEmail = trim((string)($body['companyEmail'] ?? ''));
                $companyPhone = trim((string)($body['companyPhone'] ?? ''));
                if ($companyName === '') {
                    $errors[] = 'Indica el nombre de la empresa.';
                }
                if ($companyEmail === '') {
                    $errors[] = 'Indica un correo de contacto de la empresa.';
                } elseif (!filter_var($companyEmail, FILTER_VALIDATE_EMAIL)) {
                    $errors[] = 'El correo de contacto no es válido.';
                }
                if ($companyPhone === '') {
                    $errors[] = 'Indica un teléfono de contacto de la empresa.';
                }
                $existingBranches = is_array($cur['branches'] ?? null) ? $cur['branches'] : [];
                $additionalBranches = antara_install_parse_branches_post($body, $existingBranches);
                $companyLocForCheck = antara_install_merge_location_into_address(
                    antara_install_parse_location_json((string) ($body['company_location_json'] ?? ''))
                )['location'];
                foreach ($additionalBranches as $bi => $br) {
                    $bLabel = (string) ($br['name'] ?? ('Sucursal ' . ($bi + 1)));
                    $brLoc = isset($br['location']) && is_array($br['location'])
                        ? antara_install_normalize_location($br['location'])
                        : antara_install_empty_location();
                    if (antara_install_locations_match($companyLocForCheck, $brLoc)) {
                        $errors[] = 'La dirección de «' . $bLabel . '» debe ser distinta a la de la empresa matriz.';
                    }
                    if ((string) ($br['email'] ?? '') === '') {
                        $errors[] = 'Indica el correo de contacto de «' . $bLabel . '».';
                    } elseif (!filter_var((string) $br['email'], FILTER_VALIDATE_EMAIL)) {
                        $errors[] = 'El correo de «' . $bLabel . '» no es válido.';
                    }
                    if ((string) ($br['phone'] ?? '') === '') {
                        $errors[] = 'Indica el teléfono de «' . $bLabel . '».';
                    }
                }
                $errors = array_merge($errors, antara_install_validate_branch_users_post($body, $additionalBranches));
                $accessModeCompany = (string) ($body['access_mode'] ?? 'first_user');
                if (!in_array($accessModeCompany, ['public', 'first_user'], true)) {
                    $errors[] = 'Selecciona un modo de acceso al escritorio.';
                } elseif ($accessModeCompany === 'first_user') {
                    $adminEmail = trim((string) ($body['email'] ?? ''));
                    $adminUsername = trim((string) ($body['username'] ?? ''));
                    $adminPhone = trim((string) ($body['phone'] ?? ''));
                    $adminPassword = (string) ($body['password'] ?? '');
                    $adminPasswordConfirm = (string) ($body['password_confirm'] ?? '');
                    if ($adminEmail === '') {
                        $errors[] = 'Indica el correo de la cuenta de administrador.';
                    } elseif (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
                        $errors[] = 'El correo de la cuenta de administrador no es válido.';
                    }
                    if ($adminUsername === '') {
                        $errors[] = 'Indica el nombre completo de la cuenta de administrador.';
                    }
                    if ($adminPhone === '') {
                        $errors[] = 'Indica el teléfono de la cuenta de administrador.';
                    }
                    if (strlen($adminPassword) < 8) {
                        $errors[] = 'La contraseña del administrador debe tener al menos 8 caracteres.';
                    }
                    if ($adminPassword !== $adminPasswordConfirm) {
                        $errors[] = 'Las contraseñas del administrador no coinciden.';
                    }
                    if ($adminPassword !== '' && !preg_match('/[A-Za-z]/', $adminPassword)) {
                        $errors[] = 'La contraseña del administrador debe incluir al menos una letra.';
                    }
                    if ($adminPassword !== '' && !preg_match('/[0-9]/', $adminPassword)) {
                        $errors[] = 'La contraseña del administrador debe incluir al menos un número.';
                    }
                }
            }

            if ($errors === []) {
                $policyRaw = (string)($body['branch_access_policy'] ?? 'explicit_grant');
                $policy = $policyRaw === 'open_managed' ? 'open_managed' : 'explicit_grant';
                if ($kind === 'personal') {
                    $policy = 'explicit_grant';
                }
                if ($kind === 'personal') {
                    $display = trim((string)($body['personalDisplayName'] ?? ''));
                    try {
                        $bid = 'br_' . bin2hex(random_bytes(10));
                    } catch (\Throwable $e) {
                        $bid = 'br_' . bin2hex((string)openssl_random_pseudo_bytes(10));
                    }
                    $homeName = $display !== '' ? $display : 'Principal';
                    $personalLocMerged = antara_install_merge_location_into_address(
                        antara_install_parse_location_json((string) ($body['personal_location_json'] ?? ''))
                    );
                    $personalAddress = trim((string) ($body['personalAddress'] ?? ''));
                    if ($personalAddress === '') {
                        $personalAddress = $personalLocMerged['address'];
                    }
                    $cur['deploymentKind'] = 'personal';
                    $cur['personalDisplayName'] = $display;
                    $cur['companyName'] = $display !== '' ? $display : 'Uso personal';
                    $cur['companyAddress'] = $personalAddress;
                    $cur['companyLocation'] = $personalLocMerged['location'];
                    $cur['companyEmail'] = '';
                    $cur['companyPhone'] = '';
                    $cur['branches'] = [
                        [
                            'id' => $bid,
                            'name' => $homeName,
                            'code' => 'PERSONAL',
                            'address' => $personalAddress,
                            'location' => $personalLocMerged['location'],
                        ],
                    ];
                    $cur['defaultBranchId'] = $bid;
                    $cur['branchAccessDefaults'] = [
                        'crossBranchAccessPolicy' => $policy,
                        'userDirectoryScope' => 'branch',
                    ];
                } else {
                    $companyName = trim((string)($body['companyName'] ?? ''));
                    $companyLocMerged = antara_install_merge_location_into_address(
                        antara_install_parse_location_json((string) ($body['company_location_json'] ?? ''))
                    );
                    $companyAddress = trim((string)($body['companyAddress'] ?? ''));
                    if ($companyAddress === '') {
                        $companyAddress = $companyLocMerged['address'];
                    }
                    $companyEmail = trim((string)($body['companyEmail'] ?? ''));
                    $companyPhone = trim((string)($body['companyPhone'] ?? ''));
                    $existingBranches = is_array($cur['branches'] ?? null) ? $cur['branches'] : [];
                    $additionalBranches = antara_install_parse_branches_post($body, $existingBranches);
                    $matrixBranch = antara_install_build_matrix_branch(
                        $companyName,
                        trim((string) ($body['matrix_branch_id'] ?? '')),
                        $existingBranches
                    );
                    $branches = antara_install_merge_matrix_and_branches($matrixBranch, $additionalBranches, $existingBranches);
                    $finalize = antara_install_finalize_company_branches($branches, [
                        'companyAddress' => $companyAddress,
                        'companyEmail' => $companyEmail,
                        'companyPhone' => $companyPhone,
                        'companyLocation' => $companyLocMerged['location'],
                    ]);
                    if ($finalize['errors'] !== []) {
                        $errors = array_merge($errors, $finalize['errors']);
                    }
                    $branches = $finalize['branches'];
                    $cur['deploymentKind'] = 'company';
                    $cur['personalDisplayName'] = '';
                    $cur['companyName'] = $companyName;
                    $cur['companyAddress'] = $companyAddress;
                    $cur['companyLocation'] = $companyLocMerged['location'];
                    $cur['companyEmail'] = $companyEmail;
                    $cur['companyPhone'] = $companyPhone;
                    $cur['branches'] = $branches;
                    $cur['defaultBranchId'] = (string)($branches[0]['id'] ?? '');
                    $cur['branchAccessDefaults'] = [
                        'crossBranchAccessPolicy' => $policy,
                        'userDirectoryScope' => 'branch',
                    ];
                }

                if ($errors === [] && !grid_persist_settings($settingsFile, $settingsBackupFile, $cur)) {
                    $errors[] = 'No se pudo guardar la configuración. Comprueba en tu hosting que la carpeta de la aplicación permita escritura al servidor (permisos o asistente del panel).';
                }
            }

            if ($errors === [] && $kind === 'personal') {
                require_once GRID_ROOT . '/includes/install_company_logo.php';
                $logoResult = antara_install_company_logo_apply_upload();
                if ($logoResult['errors'] !== []) {
                    $errors = array_merge($errors, $logoResult['errors']);
                } elseif ($logoResult['saved']) {
                    $_SESSION['install_profile_image_saved'] = true;
                } else {
                    unset($_SESSION['install_profile_image_saved']);
                }
            }
            if ($errors === [] && $kind === 'company') {
                require_once GRID_ROOT . '/includes/install_branch_logos.php';
                $primaryRel = antara_install_primary_company_logo_rel();
                if ($primaryRel !== '') {
                    $_SESSION['install_profile_image_saved'] = true;
                } else {
                    unset($_SESSION['install_profile_image_saved']);
                }
            }

            if ($errors === []) {
                $_SESSION['install_wizard_company_ok'] = 1;
                $curSaved = grid_read_settings_for_api($settingsFile, $settingsBackupFile, $defaultSettings);
                antara_install_wizard_save_step1_from_settings($curSaved, false);
                antara_install_wizard_save_step2_from_body($body, false);
                if ($kind === 'company') {
                    $accessModeCompany = (string) ($body['access_mode'] ?? 'first_user');
                    if ($accessModeCompany === 'public') {
                        $_SESSION['install_pending_finish'] = ['access_mode' => 'public'];
                        unset($_SESSION['install_pending_branch_users']);
                    } else {
                        antara_install_wizard_apply_session_from_step2_body($body, $curSaved);
                    }
                    $savedBranches = is_array($curSaved['branches'] ?? null) ? $curSaved['branches'] : [];
                    $_SESSION['install_pending_branch_users'] = antara_install_parse_branch_users_post($body, $savedBranches);
                }
                header('Location: ' . antara_install_base_url() . '?step=2', true, 302);
                exit;
            }
            $failPatch = antara_install_wizard_patch_step1_from_body($body);
            $failPatch['max_step'] = max(1, antara_install_wizard_max_step());
            $failPatch['open_profile_pane'] = true;
            antara_install_wizard_merge_draft($failPatch);
        }
    }

    if ($errors === [] && $action === 'continue_to_pwa') {
        if (empty($_SESSION['install_wizard_company_ok'])) {
            $errors[] = 'Completa primero los datos de empresa (paso 1).';
        } else {
            $mode = (string)($body['access_mode'] ?? '');
            if ($mode === 'public') {
                $_SESSION['install_pending_finish'] = [
                    'access_mode' => 'public',
                ];
                antara_install_wizard_save_step2_from_body($body, true);
                header('Location: ' . antara_install_base_url() . '?step=3', true, 302);
                exit;
            }
            if ($mode === 'first_user') {
                $sFin = grid_read_settings_for_api($settingsFile, $settingsBackupFile, $defaultSettings);
                $defaultBranchId = (string)($sFin['defaultBranchId'] ?? '');
                $email = trim((string)($body['email'] ?? ''));
                $username = trim((string)($body['username'] ?? ''));
                $phone = trim((string)($body['phone'] ?? ''));
                $password = (string)($body['password'] ?? '');
                $passwordConfirm = (string)($body['password_confirm'] ?? '');
                if ($email === '') {
                    $errors[] = 'Indica el correo electrónico.';
                }
                if ($username === '') {
                    $errors[] = 'Indica el nombre completo.';
                }
                if ($phone === '') {
                    $errors[] = 'Indica el teléfono.';
                }
                if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
                    $errors[] = 'El correo no es válido.';
                }
                if (strlen($password) < 8) {
                    $errors[] = 'La contraseña debe tener al menos 8 caracteres.';
                }
                if ($password !== $passwordConfirm) {
                    $errors[] = 'Las contraseñas no coinciden.';
                }
                if ($password !== '' && !preg_match('/[A-Za-z]/', $password)) {
                    $errors[] = 'La contraseña debe incluir al menos una letra.';
                }
                if ($password !== '' && !preg_match('/[0-9]/', $password)) {
                    $errors[] = 'La contraseña debe incluir al menos un número.';
                }
                if ($errors === []) {
                    require_once GRID_ROOT . '/includes/install_first_admin_profile.php';
                    $faProf = antara_install_first_admin_profile_apply_upload();
                    if ($faProf['errors'] !== []) {
                        $errors = array_merge($errors, $faProf['errors']);
                    }
                }
                if ($errors === []) {
                    $_SESSION['install_pending_finish'] = [
                        'access_mode' => 'first_user',
                        'email' => $email,
                        'username' => $username,
                        'phone' => $phone,
                        'password' => $password,
                        'branchId' => $defaultBranchId,
                    ];
                    antara_install_wizard_save_step2_from_body($body, true);
                    header('Location: ' . antara_install_base_url() . '?step=3', true, 302);
                    exit;
                }
            } else {
                $errors[] = 'Selecciona un modo de acceso.';
            }
            if ($errors !== []) {
                antara_install_wizard_save_step2_from_body($body, false);
            }
        }
    }

    if ($errors === [] && $action === 'finish') {
        if (empty($_SESSION['install_wizard_company_ok']) || empty($_SESSION['install_pending_finish'])) {
            $errors[] = 'Completa los pasos anteriores del asistente.';
        } else {
            if (!empty($body['skip_pwa'])) {
                $scopePair = antara_install_pwa_default_scope_and_start();
                $basePwa = is_array($defaultSettings['pwa'] ?? null) ? $defaultSettings['pwa'] : [];
                $pwaResult = ['errors' => [], 'pwa' => array_merge($basePwa, [
                    'enabled' => false,
                    'startUrl' => $scopePair['startUrl'],
                    'scope' => $scopePair['scope'],
                ])];
            } else {
                $pwaResult = antara_install_pwa_apply_from_post($body, $_FILES);
            }
            if ($pwaResult['errors'] !== []) {
                $errors = array_merge($errors, $pwaResult['errors']);
            }

            if ($errors === []) {
                $pend = $_SESSION['install_pending_finish'];
                $mode = (string)($pend['access_mode'] ?? '');
                $installedUserIds = [];
                $user = null;
                if ($mode === 'public') {
                    AntaraStore::configWrite([
                        'login_required' => false,
                        'public_desktop_without_users' => true,
                        'allow_user_registration' => true,
                    ]);
                    AntaraLogger::log('install.complete', ['mode' => 'public_desktop']);
                } elseif ($mode === 'first_user') {
                    $regBody = [
                        'email' => $pend['email'] ?? '',
                        'username' => $pend['username'] ?? '',
                        'phone' => $pend['phone'] ?? '',
                        'password' => $pend['password'] ?? '',
                        'branchId' => (string)($pend['branchId'] ?? ''),
                    ];
                    try {
                        AntaraStore::resetUsersAndCredentials();
                    } catch (\Throwable $e) {
                        AntaraLogger::log('install.reset_auth_failed', ['msg' => $e->getMessage()]);
                        $errors[] = 'No se pudo preparar el almacén de usuarios para una instalación limpia. Revisa permisos y el cifrado de datos.';
                    }
                    if ($errors === []) {
                        try {
                            $user = antara_auth_register_user($regBody, true);
                        } catch (RuntimeException $e) {
                            $errors[] = $e->getMessage();
                            $user = null;
                        }
                    } else {
                        $user = null;
                    }
                    if ($errors === [] && $user) {
                        AntaraStore::upsertUser(['role' => AntaraStore::ROLE_SUPER], $user['id']);
                        $user = AntaraStore::findUserById($user['id']) ?? $user;
                        require_once GRID_ROOT . '/includes/install_company_logo.php';
                        require_once GRID_ROOT . '/includes/install_first_admin_profile.php';
                        require_once GRID_ROOT . '/core/auth/profile_image_ops.php';
                        $firstAdminAbs = antara_install_first_admin_profile_resolved_path();
                        $logoAbs = $firstAdminAbs !== null ? $firstAdminAbs : antara_install_company_logo_resolved_path();
                        if ($logoAbs !== null && is_string($logoAbs) && $logoAbs !== '') {
                            $base = basename($logoAbs);
                            if ($base !== '' && $base !== '.' && $base !== '..'
                                && strpos($base, '..') === false) {
                                $rel = '/logos_sinapsys/' . $base;
                                $userPatch = ['id' => $user['id'], 'profileImageRel' => $rel];
                                $faFrame = antara_install_first_admin_profile_media_frame();
                                $kind = antara_user_profile_media_kind($rel);
                                if ($faFrame !== null && in_array($kind, ['gif', 'video'], true)) {
                                    $userPatch['profileMediaFrame'] = $faFrame;
                                }
                                AntaraStore::upsertUser($userPatch, $user['id']);
                                $user = AntaraStore::findUserById($user['id']) ?? $user;
                            }
                        }
                        AntaraStore::configWrite([
                            'login_required' => true,
                            'public_desktop_without_users' => false,
                            'allow_user_registration' => true,
                        ]);
                        AntaraSession::login($user);
                        AntaraCsrf::rotate();
                        AntaraLogger::log('install.complete', ['mode' => 'first_user', 'user' => $user['id']]);
                        require_once GRID_ROOT . '/includes/flexia_disk.php';
                        grid_flexia_disk_ensure_admin_explorer_link((string) $user['id'], AntaraStore::ROLE_SUPER);
                        $installedUserIds[] = (string) $user['id'];
                        $branchUsers = is_array($_SESSION['install_pending_branch_users'] ?? null)
                            ? $_SESSION['install_pending_branch_users']
                            : [];
                        foreach ($branchUsers as $bu) {
                            if (!is_array($bu)) {
                                continue;
                            }
                            try {
                                $buUser = antara_auth_register_user([
                                    'email' => $bu['email'] ?? '',
                                    'username' => $bu['username'] ?? '',
                                    'phone' => $bu['phone'] ?? '',
                                    'password' => $bu['password'] ?? '',
                                    'branchId' => (string) ($bu['branchId'] ?? ''),
                                ], true);
                                $buRole = (string) ($bu['role'] ?? AntaraStore::ROLE_ADMIN);
                                if ($buRole !== AntaraStore::ROLE_USER && $buRole !== 'usuario') {
                                    $buRole = AntaraStore::ROLE_ADMIN;
                                } else {
                                    $buRole = AntaraStore::ROLE_USER;
                                }
                                AntaraStore::upsertUser(['role' => $buRole], $buUser['id']);
                                $installedUserIds[] = (string) $buUser['id'];
                            } catch (RuntimeException $e) {
                                $errors[] = $e->getMessage();
                                break;
                            }
                        }
                    }
                } else {
                    $errors[] = 'Sesión de instalación inválida. Vuelve al paso 2.';
                }
            }

            if ($errors === []) {
                $cur = grid_read_settings_for_api($settingsFile, $settingsBackupFile, $defaultSettings);
                $cur['pwa'] = array_merge(is_array($cur['pwa'] ?? null) ? $cur['pwa'] : [], $pwaResult['pwa']);

                require_once GRID_ROOT . '/includes/user_settings_helpers.php';
                if ($installedUserIds !== []) {
                    grid_install_bootstrap_user_profiles($installedUserIds, $cur);
                }

                $cur = grid_workspace_settings_only($cur);
                if (!grid_persist_settings($settingsFile, $settingsBackupFile, $cur)) {
                    $errors[] = 'No se pudo guardar la configuración PWA.';
                }
            }

            if ($errors !== [] && $action === 'finish') {
                antara_install_wizard_save_step3_from_body($body);
            }

            if ($errors === [] && antara_install_write_lock()) {
                antara_install_wizard_clear_draft();
                unset(
                    $_SESSION['install_wizard_company_ok'],
                    $_SESSION['install_pending_finish'],
                    $_SESSION['install_pending_branch_users'],
                    $_SESSION['install_profile_image_saved']
                );
                $go = antara_public_app_base();
                header('Location: ' . $go, true, 302);
                exit;
            }
            if ($errors === []) {
                $errors[] = 'No se pudo finalizar: el servidor no pudo registrar que la instalación terminó. Revisa permisos de escritura en la carpeta de datos de la aplicación o pide ayuda a tu proveedor de hosting.';
            }
        }
    }
}

$secWiz = antara_install_security_wizard_data();
$installWizardDraft = antara_install_wizard_read_draft();
$installFormPost = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : null;
$wizOpenProfile = ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['install_action'] ?? '') === 'company')
        || ($errors !== [] && in_array((string)($_POST['install_action'] ?? ''), ['company', 'continue_to_pwa'], true))
        || !empty($_SESSION['install_wizard_company_ok'])
        || !empty($installWizardDraft['open_profile_pane'])
        || (isset($_GET['profile']) && (string)$_GET['profile'] === '1');

if ($step === 3) {
    $pendStep3 = $_SESSION['install_pending_finish'] ?? null;
    $needsPassword = is_array($pendStep3)
        && (string)($pendStep3['access_mode'] ?? '') === 'first_user'
        && (string)($pendStep3['password'] ?? '') === '';
    if (empty($_SESSION['install_pending_finish']) || $needsPassword) {
        $draftStep3 = antara_install_wizard_read_draft();
        $draftMode3 = (string) ($draftStep3['access_mode'] ?? '');
        if ($draftMode3 === 'public' && antara_install_wizard_max_step() >= 3) {
            $_SESSION['install_pending_finish'] = ['access_mode' => 'public'];
        } else {
            header('Location: ' . antara_install_base_url() . '?step=2', true, 302);
            exit;
        }
    }
}

if ($step === 2 && empty($_SESSION['install_wizard_company_ok'])) {
    if (antara_install_wizard_settings_indicate_step1($installSettingsSnapshot)) {
        $_SESSION['install_wizard_company_ok'] = 1;
    } else {
        header('Location: ' . antara_install_base_url(), true, 302);
        exit;
    }
}

$installWizardMaxStep = antara_install_wizard_max_step();
$installWizardBaseUrl = antara_install_base_url();

$postRef = is_array($installFormPost) ? $installFormPost : [];
$wizSettingsForForm = is_array($installPreview) ? $installPreview : [];
$wizBranchRows = antara_install_wizard_branch_rows($installWizardDraft, $wizSettingsForForm, $installFormPost);
$fvMatrixBranchId = (string) ($wizSettingsForForm['defaultBranchId'] ?? '');
if ($fvMatrixBranchId === '' && isset($wizSettingsForForm['branches'][0]['id'])) {
    $fvMatrixBranchId = (string) $wizSettingsForForm['branches'][0]['id'];
}
$fvDeploymentKind = (string) antara_install_wizard_field(
    'deployment_kind',
    isset($postRef['deployment_kind']) ? $postRef['deployment_kind'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    'personal'
);
$fvPersonalDisplay = (string) antara_install_wizard_field(
    'personalDisplayName',
    isset($postRef['personalDisplayName']) ? $postRef['personalDisplayName'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    ''
);
$fvCompanyName = (string) antara_install_wizard_field(
    'companyName',
    isset($postRef['companyName']) ? $postRef['companyName'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    ''
);
$fvCompanyAddress = (string) antara_install_wizard_field(
    'companyAddress',
    isset($postRef['companyAddress']) ? $postRef['companyAddress'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    ''
);
$fvCompanyLocationJson = (string) antara_install_wizard_field(
    'company_location_json',
    isset($postRef['company_location_json']) ? $postRef['company_location_json'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    ''
);
$fvCompanyLocation = antara_install_parse_location_json($fvCompanyLocationJson);
if ($fvCompanyAddress !== '' && antara_install_location_is_empty($fvCompanyLocation)) {
    $fvCompanyLocation['street'] = $fvCompanyAddress;
}
$fvPersonalLocationJson = (string) antara_install_wizard_field(
    'personal_location_json',
    isset($postRef['personal_location_json']) ? $postRef['personal_location_json'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    ''
);
$fvPersonalAddress = (string) antara_install_wizard_field(
    'personalAddress',
    isset($postRef['personalAddress']) ? $postRef['personalAddress'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    ''
);
$fvPersonalLocation = antara_install_parse_location_json($fvPersonalLocationJson);
if ($fvPersonalAddress !== '' && antara_install_location_is_empty($fvPersonalLocation)) {
    $fvPersonalLocation['street'] = $fvPersonalAddress;
}
$installGeoDataUrl = antara_public_app_base() . 'assets/data/install-geo-latam.json';
$fvCompanyEmail = (string) antara_install_wizard_field(
    'companyEmail',
    isset($postRef['companyEmail']) ? $postRef['companyEmail'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    ''
);
$fvCompanyPhone = (string) antara_install_wizard_field(
    'companyPhone',
    isset($postRef['companyPhone']) ? $postRef['companyPhone'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    ''
);
$fvBranchPolicy = (string) antara_install_wizard_field(
    'branch_access_policy',
    isset($postRef['branch_access_policy']) ? $postRef['branch_access_policy'] : null,
    $installWizardDraft,
    $wizSettingsForForm,
    'explicit_grant'
);
$step2Pending = is_array($_SESSION['install_pending_finish'] ?? null) ? $_SESSION['install_pending_finish'] : [];
$fvAccessMode = 'first_user';
if (isset($postRef['access_mode']) && is_string($postRef['access_mode'])) {
    $fvAccessMode = (string) $postRef['access_mode'];
} elseif (isset($installWizardDraft['access_mode']) && (string) $installWizardDraft['access_mode'] !== '') {
    $fvAccessMode = (string) $installWizardDraft['access_mode'];
} elseif (isset($step2Pending['access_mode']) && (string) $step2Pending['access_mode'] !== '') {
    $fvAccessMode = (string) $step2Pending['access_mode'];
}
$fvAdminUsername = trim((string) ($postRef['username'] ?? $installWizardDraft['username'] ?? $step2Pending['username'] ?? ''));
$fvAdminEmail = trim((string) ($postRef['email'] ?? $installWizardDraft['email'] ?? $step2Pending['email'] ?? ''));
$fvAdminPhone = trim((string) ($postRef['phone'] ?? $installWizardDraft['phone'] ?? $step2Pending['phone'] ?? ''));
$wizExistingLogoUrl = '';
$wizExistingLogoFrame = null;
$wizExistingLogoKind = null;
require_once GRID_ROOT . '/includes/install_company_logo.php';
require_once GRID_ROOT . '/includes/client_logo_ops.php';
$wizLogoAbs = antara_install_company_logo_resolved_path();
if ($wizLogoAbs !== null && is_file($wizLogoAbs)) {
    $wizExistingLogoUrl = antara_public_app_base() . 'logos_sinapsys/' . rawurlencode(basename($wizLogoAbs));
    $wizRel = 'logos_sinapsys/' . basename($wizLogoAbs);
    $wizExistingLogoKind = antara_user_profile_media_kind('/' . $wizRel);
}
$__wizSettings = grid_read_settings_for_api($settingsFile, $settingsBackupFile, $defaultSettings);
if (isset($__wizSettings['clientLogoMediaFrame']) && is_array($__wizSettings['clientLogoMediaFrame'])) {
    $wizExistingLogoFrame = antara_user_profile_media_frame_normalize($__wizSettings['clientLogoMediaFrame']);
}
$installCanVisitStep3 = false;
if (!empty($_SESSION['install_pending_finish']) && is_array($_SESSION['install_pending_finish'])) {
    $pendVisit = $_SESSION['install_pending_finish'];
    $modeVisit = (string) ($pendVisit['access_mode'] ?? '');
    $installCanVisitStep3 = $modeVisit === 'public'
        || ($modeVisit === 'first_user' && (string) ($pendVisit['password'] ?? '') !== '');
}
if (!$installCanVisitStep3
    && (string) ($installWizardDraft['access_mode'] ?? '') === 'public'
    && antara_install_wizard_max_step() >= 3) {
    $installCanVisitStep3 = true;
}

$esc = static function (string $s): string {
    return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
};

$vendorPublicBase = rtrim(antara_public_app_base(), '/');

?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Instalación — Flexia</title>
    <?php
    require_once dirname(__DIR__) . '/includes/flexia_instance.php';
    ?>
    <script>
    window.FLEXIA_INSTANCE_GUARD = <?php echo json_encode([
        'apiUrl' => antara_public_app_base() . 'index.php?action=flexiaInstance',
        'poll' => false,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP); ?>;
    </script>
    <?php antara_flexia_emit_instance_script(); ?>
    <script src="<?php echo $esc(antara_public_app_base() . 'assets/js/flexia_instance_guard.js'); ?>"></script>
    <?php if ($step === 1 || $step === 2) { ?>
    <link rel="stylesheet" href="<?php echo $esc(antara_vendor_asset_url(ANTARA_VENDOR_INTL_TEL_CSS, $vendorPublicBase)); ?>">
    <link rel="stylesheet" href="<?php echo $esc(antara_vendor_asset_url(ANTARA_VENDOR_CROPPER_CSS, $vendorPublicBase)); ?>">
    <link rel="stylesheet" href="<?php echo $esc(antara_public_app_base() . 'assets/auth/image_editor.css'); ?>">
    <?php } elseif ($step === 3) { ?>
    <link rel="stylesheet" href="<?php echo $esc(antara_vendor_asset_url(ANTARA_VENDOR_CROPPER_CSS, $vendorPublicBase)); ?>">
    <?php } ?>
    <link rel="stylesheet" href="<?php echo $esc(antara_public_app_base() . 'assets/css/app-dialog.css'); ?>">
    <style>
        :root {
            --app-dialog-bg: rgba(17, 24, 39, 0.96);
            --app-dialog-text: #e8f0ff;
            --app-dialog-muted: #94a3b8;
            --app-dialog-border: rgba(148, 163, 184, 0.14);
            --app-dialog-input-bg: rgba(0, 0, 0, 0.35);
            --app-dialog-input-border: #334155;
            --app-dialog-btn-secondary-bg: rgba(15, 23, 42, 0.65);
            --app-dialog-btn-border: #334155;
            --app-dialog-focus: #38bdf8;
            --app-dialog-accent-1: #0ea5e9;
            --app-dialog-accent-2: #22d3ee;
            --app-dialog-accent-3: #7dd3fc;
            --app-dialog-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(125, 211, 252, 0.08) inset;
            --app-dialog-backdrop: rgba(0, 0, 0, 0.55);
        }
        .grid-app-dialog-panel {
            background: linear-gradient(145deg, rgba(17, 24, 39, 0.96), rgba(15, 23, 42, 0.92)) !important;
        }
        body {
            font-family: system-ui, Segoe UI, sans-serif;
            color: #e8f0ff;
            margin: 0;
            line-height: 1.5;
            position: relative;
            min-height: 100vh;
            overflow-x: hidden;
            background: #050a14;
        }
        .install-bg-layer {
            position: fixed;
            inset: 0;
            z-index: 0;
            pointer-events: none;
            overflow: hidden;
        }
        .install-bg-gradient {
            position: absolute;
            inset: 0;
            background: linear-gradient(118deg, #04101f, #0a1a32, #132447, #0c1e38, #111827);
            background-size: 340% 340%;
            animation: installBgMove 24s ease-in-out infinite;
        }
        @keyframes installBgMove {
            0%, 100% { background-position: 0% 45%; }
            50% { background-position: 100% 55%; }
        }
        .install-bg-orb {
            position: absolute;
            border-radius: 50%;
            filter: blur(76px);
            opacity: .42;
            mix-blend-mode: screen;
        }
        .install-bg-orb--a {
            width: min(56vw, 440px);
            height: min(56vw, 440px);
            left: -14%;
            top: 6%;
            background: radial-gradient(circle at 32% 28%, rgba(14, 165, 233, 0.58), transparent 72%);
            animation: installOrbA 17s ease-in-out infinite;
        }
        .install-bg-orb--b {
            width: min(50vw, 400px);
            height: min(50vw, 400px);
            right: -10%;
            bottom: 2%;
            background: radial-gradient(circle at 55% 42%, rgba(99, 102, 241, 0.52), transparent 70%);
            animation: installOrbB 19s ease-in-out infinite;
        }
        .install-bg-orb--c {
            width: min(36vw, 300px);
            height: min(36vw, 300px);
            left: 38%;
            top: 55%;
            background: radial-gradient(circle at 50% 50%, rgba(45, 212, 191, 0.28), transparent 65%);
            animation: installOrbC 21s ease-in-out infinite;
        }
        @keyframes installOrbA {
            0%, 100% { transform: translate(0, 0) scale(1); }
            45% { transform: translate(10%, 14%) scale(1.1); }
            72% { transform: translate(-5%, 8%) scale(0.94); }
        }
        @keyframes installOrbB {
            0%, 100% { transform: translate(0, 0) scale(1); }
            48% { transform: translate(-12%, -10%) scale(1.08); }
            78% { transform: translate(6%, -5%) scale(0.9); }
        }
        @keyframes installOrbC {
            0%, 100% { transform: translate(0, 0) scale(1); opacity: .42; }
            50% { transform: translate(-8%, -12%) scale(1.15); opacity: .55; }
        }
        .install-bg-grid {
            position: absolute;
            inset: -50%;
            opacity: .055;
            background-image:
                linear-gradient(rgba(125, 211, 252, 0.35) 1px, transparent 1px),
                linear-gradient(90deg, rgba(125, 211, 252, 0.2) 1px, transparent 1px);
            background-size: 52px 52px;
            animation: installGridDrift 42s linear infinite;
        }
        @keyframes installGridDrift {
            from { transform: translate(0, 0) rotate(0deg); }
            to { transform: translate(52px, 52px) rotate(0.5deg); }
        }
        .wrap { position: relative; z-index: 1; max-width: 38rem; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
        h1 { font-size: 1.35rem; color: #7dd3fc; margin: 0 0 .25rem; }
        .sub { color: #94a3b8; font-size: .9rem; margin-bottom: 1.25rem; }
        .card { background: #111827; border: 1px solid #1e293b; border-radius: 12px; padding: 1.1rem 1.2rem; margin-bottom: 1rem; }
        label { display: block; font-size: .85rem; color: #cbd5e1; margin: .65rem 0 .2rem; }
        input[type=text], input[type=email], input[type=password], input[type=tel], textarea {
            width: 100%; max-width: 100%; box-sizing: border-box; padding: .45rem .55rem; border-radius: 8px; border: 1px solid #334155; background: #0f172a; color: #f8fafc;
        }
        input[type="color"] {
            width: 100%;
            max-width: 10rem;
            height: 2.35rem;
            padding: .2rem .35rem;
            border-radius: 8px;
            border: 1px solid #334155;
            background: #0f172a;
            cursor: pointer;
        }
        .iti-install-wrap { width: 100%; max-width: 100%; }
        .iti__country-list {
            background: #0f172a !important;
            border: 1px solid #334155 !important;
            color: #e2e8f0;
            box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
        }
        .iti__country-list .iti__country.iti__highlight { background: #1e293b !important; }
        .iti__selected-flag {
            background: #0f172a !important;
            border-right: 1px solid #334155 !important;
        }
        .iti__arrow { border-top-color: #94a3b8 !important; }
        .iti__search-input {
            background: #111827 !important;
            border: 1px solid #334155 !important;
            color: #f8fafc !important;
        }
        textarea { min-height: 4rem; resize: vertical; }
        .pwd-meter { margin-top: .35rem; font-size: .78rem; color: #94a3b8; }
        .pwd-meter-bar { height: 6px; border-radius: 999px; background: #1e293b; overflow: hidden; margin-top: .25rem; border: 1px solid #334155; }
        .pwd-meter-bar > span { display: block; height: 100%; border-radius: 999px; width: 0%; transition: width .2s ease, background .2s ease; }
        .pwd-meter--0 > span { background: #64748b; width: 15%; }
        .pwd-meter--1 > span { background: #f87171; width: 35%; }
        .pwd-meter--2 > span { background: #fbbf24; width: 55%; }
        .pwd-meter--3 > span { background: #4ade80; width: 80%; }
        .pwd-meter--4 > span { background: #22d3ee; width: 100%; }
        .checks { list-style: none; padding: 0; margin: 0; }
        .checks li { padding: .35rem 0; border-bottom: 1px solid #1e293b; font-size: .88rem; }
        .ok { color: #4ade80; }
        .warn { color: #fbbf24; }
        .err { color: #f87171; }
        .errbox { background: #450a0a; border: 1px solid #7f1d1d; color: #fecaca; padding: .75rem 1rem; border-radius: 10px; margin-bottom: 1rem; }
        .okbox { background: #052e1a; border: 1px solid #166534; color: #bbf7d0; padding: .75rem 1rem; border-radius: 10px; margin-bottom: 1rem; }
        .btn { display: inline-block; margin-top: .75rem; padding: .55rem 1.1rem; background: #0ea5e9; color: #fff; border: 0; border-radius: 8px; font-weight: 600; cursor: pointer; }
        .btn:disabled { opacity: .45; cursor: not-allowed; }
        .muted { color: #64748b; font-size: .85rem; }
        fieldset { border: 1px solid #334155; border-radius: 10px; margin: 1rem 0; padding: .75rem 1rem; }
        legend { padding: 0 .35rem; color: #94a3b8; font-size: .9rem; }
        .logo-box {
            display: flex;
            flex-direction: column;
            gap: .75rem;
            align-items: stretch;
        }
        .logo-box > .logo-selected-preview {
            align-self: center;
            width: min(220px, 100%);
        }
        .logo-box input[type=file].install-file-input,
        .logo-box input.install-file-input[type=file] {
            width: 100%;
            max-width: 100%;
            padding: .5rem .6rem;
            border-radius: 10px;
            border: 1px dashed #475569;
            background: rgba(15, 23, 42, 0.6);
            color: #cbd5e1;
            cursor: pointer;
            transition: border-color .2s ease, background .2s ease, box-shadow .2s ease;
        }
        .logo-box input[type=file].install-file-input:hover,
        .logo-box input.install-file-input[type=file]:hover {
            border-color: #7dd3fc;
            background: rgba(30, 41, 59, 0.75);
            box-shadow: 0 0 0 1px rgba(125, 211, 252, 0.12);
        }
        .logo-box input[type=file].install-file-input::file-selector-button,
        .logo-box input.install-file-input[type=file]::file-selector-button {
            margin-right: 12px;
            padding: .4rem .85rem;
            border-radius: 8px;
            border: 0;
            background: linear-gradient(180deg, #22d3ee, #0ea5e9);
            color: #042f2e;
            font-weight: 600;
            cursor: pointer;
        }
        .install-geo-map-circle {
            width: 200px;
            height: 200px;
            border: 1px solid #334155;
            border-radius: 50%;
            overflow: hidden;
            margin: .65rem auto .85rem;
            background: #0f172a;
            display: block;
        }
        .install-location-block [data-geo-status] {
            flex: 1 1 100%;
            min-height: 0;
        }
        .install-location-block [data-geo-status]:empty {
            display: none;
        }
        .install-geo-map-circle[hidden] { display: none !important; }
        .install-geo-map-circle iframe {
            width: 100%;
            height: 100%;
            border: 0;
            display: block;
        }
        .install-location-block select,
        .install-location-block input[type=text] {
            width: 100%;
            max-width: 100%;
            box-sizing: border-box;
        }
        .pwa-multislot-wrap .logo-box.is-invalid input[type=file].install-file-input {
            border-color: #f87171;
            border-style: solid;
            box-shadow: 0 0 0 1px rgba(248, 113, 113, 0.2);
        }
        .pwa-multislot-wrap { margin-top: .35rem; }
        .pwa-master-pack-label { display: block; font-size: .85rem; color: #cbd5e1; margin: .65rem 0 .2rem; }
        .pwa-mosaic-wrap { margin-top: 1rem; }
        .pwa-mosaic-hint { margin: 0 0 .5rem; font-size: .82rem; }
        .pwa-mosaic {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(6.75rem, 1fr));
            gap: .65rem;
        }
        .pwa-mosaic-tile {
            margin: 0;
            padding: .4rem;
            border-radius: 10px;
            border: 1px solid #334155;
            background: #0f172a;
            cursor: pointer;
            text-align: left;
            color: #e2e8f0;
            transition: border-color .15s ease, box-shadow .15s ease;
        }
        .pwa-mosaic-tile:hover, .pwa-mosaic-tile:focus-visible {
            border-color: #7dd3fc;
            outline: none;
            box-shadow: 0 0 0 1px rgba(125, 211, 252, 0.2);
        }
        .pwa-mosaic-tile img {
            display: block;
            width: 100%;
            height: 4.5rem;
            object-fit: contain;
            border-radius: 6px;
            background: repeating-conic-gradient(#1e293b 0% 25%, #0f172a 0% 50%) 50% / 12px 12px;
            border: 1px solid #475569;
        }
        .pwa-mosaic-tile--splash img { height: 5rem; }
        .pwa-mosaic-tile kbd {
            display: block;
            margin-top: .3rem;
            font-size: .62rem;
            color: #94a3b8;
            font-weight: 600;
            font-family: inherit;
        }
        .pwa-modal-slot-file {
            position: absolute;
            width: 0.01px;
            height: 0.01px;
            opacity: 0;
            overflow: hidden;
            z-index: -1;
        }
        .pwa-crop-dialog-uploads {
            display: flex;
            flex-wrap: wrap;
            gap: .5rem;
            align-items: center;
            margin-bottom: .75rem;
            padding-bottom: .75rem;
            border-bottom: 1px solid #334155;
        }
        .pwa-crop-dialog-uploads .btn { margin-top: 0; }
        .pwa-slot-file-error {
            margin: .35rem 0 0;
            font-size: .82rem;
            color: #f87171;
        }
        .pwa-slot-file-error[hidden] { display: none !important; }
        .logo-selected-preview {
            display: none;
            margin: 0;
            padding: 12px;
            border-radius: 12px;
            border: 1px solid #334155;
            background: repeating-conic-gradient(#1e293b 0% 25%, #0f172a 0% 50%) 50% / 14px 14px;
            max-width: 220px;
            box-sizing: border-box;
            text-align: center;
            animation: logoPreviewPop .45s cubic-bezier(.22, 1, .36, 1) both;
        }
        .logo-selected-preview__caption {
            display: block;
            margin-bottom: 8px;
            font-size: .78rem;
            text-align: center;
        }
        .logo-selected-preview.is-on {
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .logo-selected-preview img,
        .logo-selected-preview video,
        .logo-selected-preview [data-antara-profile-img],
        .logo-selected-preview [data-antara-profile-video] {
            display: block;
            width: 120px;
            height: 120px;
            object-fit: cover;
            border-radius: 50%;
            border: 1px solid #475569;
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
            margin: 0 auto;
        }
        .install-logo-media-wrap {
            position: relative;
            width: 120px;
            height: 120px;
            margin: 0 auto;
            overflow: hidden;
            border-radius: 50%;
        }
        .install-logo-media-wrap [data-antara-profile-img],
        .install-logo-media-wrap [data-antara-profile-video] {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            max-width: none;
            margin: 0;
            border: none;
            box-shadow: none;
        }
        .install-logo-media-wrap [data-antara-profile-ph] {
            position: absolute;
            inset: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: 700;
            color: #94a3b8;
            background: rgba(15, 23, 42, 0.6);
        }
        .install-logo-media-wrap [hidden] { display: none !important; }
        @keyframes logoPreviewPop {
            from { opacity: 0; transform: scale(0.94) translateY(6px); }
            to { opacity: 1; transform: scale(1) translateY(0); }
        }
        .logo-editor-dialog {
            max-width: min(560px, 96vw);
            width: calc(100% - 2rem);
            margin: auto;
            border: 1px solid #334155;
            border-radius: 16px;
            background: linear-gradient(165deg, #111827, #0c1424);
            color: #e2e8f0;
            padding: 0;
            box-shadow: 0 28px 90px rgba(0, 0, 0, 0.65), 0 0 0 1px rgba(125, 211, 252, 0.08) inset;
        }
        .logo-editor-dialog::backdrop {
            background: rgba(4, 8, 14, 0.78);
            backdrop-filter: blur(5px);
        }
        .logo-editor-inner { padding: 1.15rem 1.25rem 1.25rem; }
        .logo-editor-title { margin: 0 0 .35rem; font-size: 1.05rem; color: #f8fafc; letter-spacing: -0.02em; }
        .logo-modal-mode {
            display: flex;
            flex-wrap: wrap;
            gap: 10px 1.25rem;
            margin: .85rem 0 1rem;
            padding: .65rem .75rem;
            border-radius: 10px;
            border: 1px solid #334155;
            background: rgba(15, 23, 42, 0.65);
        }
        .logo-modal-mode label { display: inline-flex; align-items: center; gap: .45rem; margin: 0; cursor: pointer; font-size: .86rem; }
        .logo-modal-preview-block[hidden] { display: none !important; }
        .logo-modal-preview-block { margin-top: .25rem; }
        .logo-modal-preview-block > .muted { font-size: .76rem; margin-bottom: .4rem; display: block; }
        .logo-modal-preview-wrap {
            border-radius: 12px;
            border: 1px solid #475569;
            background: repeating-conic-gradient(#1e293b 0% 25%, #0f172a 0% 50%) 50% / 14px 14px;
            padding: 12px;
            min-height: 120px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .logo-modal-preview-wrap img {
            max-width: 100%;
            max-height: min(38vh, 280px);
            width: auto;
            height: auto;
            object-fit: contain;
            border-radius: 8px;
            box-shadow: 0 10px 28px rgba(0, 0, 0, 0.4);
        }
        .logo-modal-crop-wrap { margin-top: 1rem; }
        .logo-modal-crop-actions { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-top: .65rem; }
        .logo-modal-crop-actions .btn { margin-top: 0; }
        .logo-modal-crop-wrap[hidden] { display: none !important; }
        .logo-modal-crop-stage {
            max-height: min(44vh, 340px);
            margin: 0 0 .75rem;
            border-radius: 10px;
            overflow: hidden;
            border: 1px solid #475569;
            background: repeating-conic-gradient(#1e293b 0% 25%, #0f172a 0% 50%) 50% / 14px 14px;
        }
        .logo-modal-crop-stage img { display: block; max-width: 100%; }
        .logo-editor-actions {
            display: flex;
            flex-wrap: wrap;
            justify-content: flex-end;
            gap: .5rem;
            margin-top: 1.15rem;
            padding-top: 1rem;
            border-top: 1px solid #334155;
        }
        .logo-editor-actions .btn { margin-top: 0; }
        .cropper-modal { background: rgba(6, 10, 18, 0.55) !important; }
        .cropper-view-box { outline: 2px solid rgba(125, 211, 252, 0.95) !important; outline-offset: -1px; }
        .cropper-line, .cropper-point { background: rgba(14, 165, 233, 0.45) !important; }
        .pwa-crop-dialog {
            max-width: min(560px, 96vw); width: calc(100% - 2rem); margin: auto;
            border: 1px solid #334155; border-radius: 16px;
            background: linear-gradient(165deg, #111827, #0c1424); color: #e2e8f0; padding: 0;
            box-shadow: 0 28px 90px rgba(0, 0, 0, 0.65);
        }
        .pwa-crop-dialog::backdrop { background: rgba(4, 8, 14, 0.78); backdrop-filter: blur(5px); }
        .pwa-crop-inner { padding: 1rem 1.1rem 1.1rem; }
        .pwa-crop-title { margin: 0 0 .5rem; font-size: 1rem; color: #f8fafc; }
        .pwa-crop-stage {
            max-height: min(44vh, 340px); border-radius: 10px; overflow: hidden;
            border: 1px solid #475569;
            background: repeating-conic-gradient(#1e293b 0% 25%, #0f172a 0% 50%) 50% / 14px 14px;
        }
        .pwa-crop-stage img { display: block; max-width: 100%; }
        .pwa-crop-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: .85rem; padding-top: .85rem; border-top: 1px solid #334155; }
        .pwa-crop-actions .btn { margin-top: 0; }
        .install-mode fieldset label { display: block; margin: .4rem 0; cursor: pointer; }
        .branch-row { border: 1px solid #334155; border-radius: 8px; padding: .75rem; margin-bottom: .65rem; background: #0f172a; }
        .branch-row label { margin-top: .35rem; }
        h2.install-h2 { font-size: 1rem; color: #94a3b8; margin: 1rem 0 .5rem; letter-spacing: .04em; }
        .wiz-steps { display: flex; align-items: center; gap: .5rem; margin-bottom: .35rem; flex-wrap: wrap; }
        .wiz-dot { width: 2rem; height: 2rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: .85rem; font-weight: 700; background: #1e293b; border: 1px solid #334155; color: #94a3b8; }
        .wiz-dot.is-on { background: linear-gradient(135deg, #0ea5e9, #6366f1); border-color: #7dd3fc; color: #0c1220; }
        a.wiz-dot.wiz-step-link { text-decoration: none; cursor: pointer; }
        a.wiz-dot.wiz-step-link:hover { filter: brightness(1.08); }
        .branch-row { border: 1px solid rgba(148,163,184,.2); border-radius: 12px; padding: 1rem 1rem .85rem; margin-bottom: .85rem; background: rgba(15,23,42,.35); }
        .branch-check { display: flex; align-items: flex-start; gap: .5rem; font-size: .88rem; cursor: pointer; }
        .branch-check input { margin-top: .2rem; flex-shrink: 0; }
        .branch-logo-top { margin-bottom: .85rem; }
        .branch-logo-top .branch-logo-fields { margin-top: 0; }
        .branch-logo-top .branch-logo-upload-label { display: block; margin: 0 0 .35rem; font-size: .85rem; color: #cbd5e1; }
        .branch-policy-fieldset label.branch-policy-option {
            display: flex; align-items: flex-start; gap: .55rem;
            margin: .55rem 0; padding: .55rem .65rem;
            border: 1px solid #334155; border-radius: 10px;
            background: rgba(15, 23, 42, .45); cursor: pointer;
        }
        .branch-policy-fieldset label.branch-policy-option:has(input:checked) {
            border-color: #38bdf8; background: rgba(14, 165, 233, .08);
        }
        .branch-policy-fieldset label.branch-policy-option input { margin-top: .25rem; flex-shrink: 0; }
        .branch-policy-fieldset .branch-policy-example { display: block; margin-top: .35rem; font-size: .78rem; line-height: 1.45; }
        .branch-user-fieldset { margin-top: .85rem; border: 1px solid #334155; border-radius: 10px; padding: .65rem .75rem; }
        .branch-user-fieldset legend { font-size: .82rem; color: #94a3b8; }
        .branch-user-fields[hidden] { display: none !important; }
        .company-access-fieldset label { display: block; margin: .4rem 0; cursor: pointer; }
        #companyFirstUserFields[hidden] { display: none !important; }
        #panelCompany .iti-install-wrap { width: 100%; }
        .install-confirm-dialog {
            max-width: min(420px, 92vw); width: calc(100% - 2rem); margin: auto;
            border: 1px solid #334155; border-radius: 14px;
            background: linear-gradient(165deg, #111827, #0c1424); color: #e2e8f0; padding: 0;
            box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6);
        }
        .install-confirm-dialog::backdrop { background: rgba(4, 8, 14, 0.78); backdrop-filter: blur(4px); }
        .install-confirm-inner { padding: 1.1rem 1.15rem 1rem; }
        .install-confirm-title { margin: 0 0 .45rem; font-size: 1rem; color: #f8fafc; }
        .install-confirm-text { margin: 0; font-size: .9rem; color: #94a3b8; line-height: 1.5; }
        .install-confirm-actions {
            display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem;
            margin-top: 1rem; padding-top: .85rem; border-top: 1px solid #334155;
        }
        .install-confirm-actions .btn { margin-top: 0; }
        .wiz-line { flex: 1; height: 2px; background: #334155; min-width: 1.5rem; }
        .wiz-steps-caption { font-size: .78rem; color: #94a3b8; margin: 0 0 1rem; }
        .sec-host { font-size: .8rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: .75rem 1rem; margin-bottom: 1rem; line-height: 1.65; }
        .sec-host strong { color: #cbd5e1; }
        .sec-host code { color: #7dd3fc; font-size: .72rem; word-break: break-all; }
        .sec-score-row { display: flex; flex-wrap: wrap; align-items: stretch; gap: 1.25rem; margin: 1rem 0; }
        .sec-score-box { text-align: center; padding: .75rem 1rem; border-radius: 12px; border: 1px solid #334155; background: linear-gradient(165deg, #0f172a, #111827); min-width: 6.5rem; }
        .sec-score-num { font-size: 2rem; font-weight: 800; color: #7dd3fc; line-height: 1.1; }
        .sec-score-label { font-size: .68rem; color: #64748b; text-transform: uppercase; letter-spacing: .12em; margin-top: .2rem; }
        .sec-bar { height: 8px; border-radius: 999px; background: #1e293b; overflow: hidden; margin-top: .75rem; border: 1px solid #334155; }
        .sec-bar > span { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, #0ea5e9, #34d399); transition: width .4s ease; }
        .sec-tier { flex: 1; min-width: 14rem; }
        .sec-tier-badge { display: inline-block; padding: .35rem .75rem; border-radius: 999px; font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; margin-bottom: .5rem; }
        .tier-high { background: rgba(52, 211, 153, .2); color: #6ee7b7; border: 1px solid rgba(52, 211, 153, .45); }
        .tier-medium { background: rgba(251, 191, 36, .15); color: #fcd34d; border: 1px solid rgba(251, 191, 36, .4); }
        .tier-low { background: rgba(248, 113, 113, .18); color: #fecaca; border: 1px solid rgba(248, 113, 113, .45); }
        .sec-list { margin: 0; padding: 0; list-style: none; max-height: min(52vh, 28rem); overflow-y: auto; }
        .sec-item { border: 1px solid #1e293b; border-radius: 8px; margin-bottom: .5rem; overflow: hidden; background: #0f172a; }
        .sec-item summary { cursor: pointer; padding: .6rem .85rem; font-size: .82rem; list-style: none; display: flex; align-items: flex-start; gap: .5rem; }
        .sec-item summary::-webkit-details-marker { display: none; }
        .sec-item summary::before { content: '▸'; color: #64748b; transition: transform .2s; flex-shrink: 0; font-size: .75rem; }
        .sec-item[open] summary::before { transform: rotate(90deg); color: #7dd3fc; }
        .sec-item .msg { flex: 1; color: #e2e8f0; }
        .sec-item .rec { padding: 0 .85rem .75rem 2rem; font-size: .78rem; color: #94a3b8; line-height: 1.55; border-top: 1px solid #1e293b; margin: 0; background: rgba(0, 0, 0, .2); }
        .sec-item .btn-fix { margin: .35rem 0 .65rem 2rem; font-size: .78rem; padding: .4rem .85rem; }
        .sec-mark { flex-shrink: 0; width: 1.5rem; text-align: center; font-weight: 800; font-size: .85rem; }
        .sec-mark.ok { color: #4ade80; }
        .sec-mark.warn { color: #fbbf24; }
        .sec-mark.err { color: #f87171; }
        .btn-ghost { background: transparent; border: 1px solid #334155; color: #94a3b8; }
        .btn-ghost:hover { border-color: #7dd3fc; color: #e2e8f0; }
        .wiz-actions { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-top: 1rem; }
        .wiz-profile-head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: .5rem; margin-bottom: .75rem; }
        .sec-external-discrete { margin: 0 0 1rem; border: 1px solid #1e293b; border-radius: 10px; background: rgba(15, 23, 42, .45); }
        .sec-external-discrete > summary { cursor: pointer; list-style: none; padding: .5rem .85rem; font-size: .8rem; color: #64748b; display: flex; align-items: center; justify-content: space-between; gap: .5rem; user-select: none; }
        .sec-external-discrete > summary::-webkit-details-marker { display: none; }
        .sec-external-discrete > summary::after { content: '▼'; font-size: .6rem; opacity: .5; flex-shrink: 0; }
        .sec-external-discrete[open] > summary { color: #94a3b8; }
        .sec-external-discrete[open] > summary::after { content: '▲'; }
        .sec-external-discrete .inner { padding: 0 .85rem .85rem; border-top: 1px solid #1e293b; }
        .sec-external-discrete .inner p { margin: .45rem 0; font-size: .76rem; color: #64748b; line-height: 1.55; }
        .sec-external-discrete .inner .btn { margin-top: .25rem; font-size: .76rem; padding: .38rem .75rem; }
    </style>
</head>
<body>
<script src="<?php echo $esc(antara_public_app_base() . 'assets/js/app-dialog.js'); ?>"></script>
<script>
(function () {
    window.installAlert = function (message) {
        if (window.appDialog && typeof window.appDialog.alert === 'function') {
            return window.appDialog.alert(message);
        }
        return new Promise(function (resolve) {
            try { window.alert(message); } catch (e) {}
            resolve();
        });
    };
})();
</script>
<div class="install-bg-layer" aria-hidden="true">
    <div class="install-bg-gradient"></div>
    <div class="install-bg-orb install-bg-orb--a"></div>
    <div class="install-bg-orb install-bg-orb--b"></div>
    <div class="install-bg-orb install-bg-orb--c"></div>
    <div class="install-bg-grid"></div>
</div>
<div class="wrap">
    <h1>Instalador Flexia</h1>
    <p class="sub">Paso <?php echo (int)$step; ?> de 3 — <?php echo $esc($installFormatCountCaption); ?></p>

    <?php if ($errors !== []) { ?>
        <div class="errbox"><?php foreach ($errors as $er) { echo '<p>' . $esc((string)$er) . '</p>'; } ?></div>
    <?php } ?>
    <?php if ($notices !== []) { ?>
        <div class="okbox"><?php foreach ($notices as $n) { echo '<p>' . $esc((string)$n) . '</p>'; } ?></div>
    <?php } ?>

    <?php if ($step === 1) {
        $h = $secWiz['host'] ?? [];
        $scoreW = (int)($secWiz['score'] ?? 0);
        if ($scoreW < 0) {
            $scoreW = 0;
        }
        if ($scoreW > 100) {
            $scoreW = 100;
        }
        $tierKey = (string)($secWiz['tier'] ?? 'medium');
        $tierCls = 'tier-medium';
        if ($tierKey === 'high') {
            $tierCls = 'tier-high';
        } elseif ($tierKey === 'low') {
            $tierCls = 'tier-low';
        }
        $wizProfOpen = $wizOpenProfile;
        $wizSecStyle = $wizProfOpen ? 'display:none' : '';
        $wizProfStyle = $wizProfOpen ? '' : 'display:none';
        ?>
        <p class="wiz-steps-caption">1 · Entorno y perfil · 2 · Acceso inicial · 3 · PWA</p>
        <nav class="wiz-steps" aria-label="Pasos del instalador">
            <span class="wiz-dot is-on" id="wizDot1" aria-current="step">1</span>
            <span class="wiz-line"></span>
            <?php if (!empty($_SESSION['install_wizard_company_ok'])) { ?>
            <a class="wiz-dot wiz-step-link" id="wizDot2" href="<?php echo $esc($installWizardBaseUrl . '?step=2'); ?>" title="Paso 2">2</a>
            <?php } else { ?>
            <span class="wiz-dot" id="wizDot2">2</span>
            <?php } ?>
            <span class="wiz-line"></span>
            <?php if ($installCanVisitStep3) { ?>
            <a class="wiz-dot wiz-step-link" id="wizDot3" href="<?php echo $esc($installWizardBaseUrl . '?step=3'); ?>" title="Paso 3">3</a>
            <?php } else { ?>
            <span class="wiz-dot" id="wizDot3">3</span>
            <?php } ?>
        </nav>

        <div class="card" id="wizPaneSecurity" style="<?php echo $esc($wizSecStyle); ?>">
            <strong>Diagnóstico de seguridad y entorno</strong>
            <p class="muted" style="margin:.5rem 0 .75rem">Verás si la conexión con el servidor va <strong>protegida</strong> (cifrado), desde <strong>qué sitio web</strong> estás instalando y si la <strong>versión de PHP</strong> del alojamiento es adecuada para Flexia. La lista de abajo detalla cada punto: en verde está bien, en amarillo conviene mejorar y en rojo debes actuar antes de seguir. Para decisiones de seguridad exigentes, suma la opinión de quien administre tu servidor o contrato de hosting.</p>
            <p class="muted" style="margin:0 0 .75rem">Flexia <strong>prepara solo lo necesario</strong> en el servidor para que puedas guardar datos, archivos y cuentas con normalidad (carpetas de trabajo, protecciones básicas y cifrado cuando aplica). En la misma visita a este paso intenta <strong>sola</strong>, cuando el hosting lo permite, una carpeta técnica fuera del área pública del sitio; no tienes que pulsar nada. Si <strong>vuelves a abrir este primer paso desde cero</strong>, el escritorio vuelve a un estado inicial y esa preferencia se recalcula automáticamente; solo verás un aviso discreto si el servidor no puede aplicarla.</p>
            <?php if ($installExternalAuxRecommendation !== null && $installExternalAuxRecommendation !== '') { ?>
            <details class="sec-external-discrete" open>
                <summary>Aviso — ubicación fuera del sitio público</summary>
                <div class="inner">
                    <p><?php echo $esc($installExternalAuxRecommendation); ?></p>
                </div>
            </details>
            <?php } ?>
            <div class="sec-host">
                <strong>Resumen de esta instalación</strong><br>
                <?php
                $httpsLike = ((string)($h['scheme'] ?? '')) === 'https';
                ?>
                <strong>Conexión:</strong> <?php echo $httpsLike ? 'cifrada (recomendado para contraseñas y datos).' : 'sin cifrar; en producción conviene usar el candado del navegador (HTTPS).'; ?><br>
                <strong>Sitio:</strong> <?php echo $esc((string)($h['host'] ?? '—')); ?><br>
                <?php
                $phpVer = (string)($h['php'] ?? '');
                $phpOk = $phpVer !== '' && version_compare($phpVer, '8.0.0', '>=');
                ?>
                <strong>PHP del servidor:</strong> <?php echo $phpVer !== '' ? $esc($phpVer) . ' — ' : ''; ?><?php echo $phpOk ? 'adecuado para esta versión de Flexia.' : ($phpVer !== '' ? 'por debajo de lo recomendado; pide en tu panel de hosting una versión más reciente (8 o superior).' : 'no se pudo leer la versión desde aquí.'); ?>
            </div>
            <div class="sec-score-row">
                <div class="sec-score-box">
                    <div class="sec-score-num"><?php echo $scoreW; ?></div>
                    <div class="sec-score-label">Resumen en números (0 a 100)</div>
                    <div class="sec-bar"><span style="width:<?php echo $scoreW; ?>%"></span></div>
                </div>
                <div class="sec-tier">
                    <span class="sec-tier-badge <?php echo $esc($tierCls); ?>"><?php echo $esc((string)($secWiz['tier_label'] ?? '')); ?></span>
                    <p class="muted" style="margin:0;font-size:.82rem"><?php
                        if ($tierKey === 'high') {
                            echo 'En conjunto, las comprobaciones salen bien. Aun así, revisa las advertencias en amarillo: son mejoras sencillas para cuando pases a uso real con clientes o datos importantes.';
                        } elseif ($tierKey === 'low') {
                            echo 'Hay puntos graves o varios avisos serios. Conviene resolverlos con tu proveedor de hosting o quien administre el servidor antes de confiar en esta instalación con datos sensibles.';
                        } else {
                            echo 'Puedes seguir, pero conviene atender las advertencias antes de dar por cerrada la puesta en marcha en un entorno serio (empresa o datos personales de terceros).';
                        }
                    ?></p>
                </div>
            </div>
            <p class="muted" style="margin:0 0 .5rem;font-size:.82rem"><strong>Lista de comprobación</strong> — toca cada fila para entender qué significa y qué puedes hacer al respecto.</p>
            <ul class="sec-list">
                <?php
                $wizardUserIniShown = false;
                foreach ($secWiz['items'] ?? [] as $it) {
                    $lv = (string)($it['level'] ?? 'ok');
                    $okIt = !empty($it['ok']);
                    if ($lv === 'warn' && !$okIt) {
                        $mark = '!';
                        $mcls = 'warn';
                    } elseif (($lv === 'err' || $lv === 'error') && !$okIt) {
                        $mark = '✕';
                        $mcls = 'err';
                    } else {
                        $mark = '✓';
                        $mcls = 'ok';
                    }
                    $msg = (string)($it['message'] ?? '');
                    $rec = trim((string)($it['recommendation'] ?? ''));
                    if ($rec === '' || $rec === '—' || $rec === '-') {
                        $rec = $okIt
                            ? 'Todo correcto en este punto. Cuando tu hosting ofrezca actualizaciones de seguridad, conviene aplicarlas con calma.'
                            : 'Pide orientación a tu proveedor de hosting o a quien administre el servidor, usando el mensaje de arriba como referencia.';
                    }
                    ?>
                <li class="sec-item">
                    <details>
                        <summary><span class="sec-mark <?php echo $esc($mcls); ?>"><?php echo $esc($mark); ?></span><span class="msg"><?php echo $esc($msg); ?></span></summary>
                        <p class="rec"><?php echo $esc($rec); ?></p>
                        <?php
                        $fa = (string)($it['fix_action'] ?? '');
                        if ($fa === 'user_ini' && $wizardUserIniShown) {
                            $fa = '';
                        }
                        if ($fa === 'user_ini') {
                            $wizardUserIniShown = true;
                        }
                        if ($fa !== '') {
                            $fixLabel = $fa === 'htaccess'
                                ? 'Aplicar protecciones desde el instalador'
                                : 'Aplicar ajustes PHP sugeridos (.user.ini)';
                            ?>
                        <p style="margin:0;padding:0 .85rem .65rem 2rem;">
                            <button type="submit" class="btn btn-fix" form="installWizardFixForm" name="wizard_fix_id" value="<?php echo $esc($fa); ?>"><?php echo $esc($fixLabel); ?></button>
                        </p>
                        <?php } ?>
                    </details>
                </li>
                <?php } ?>
            </ul>
            <?php if ($blocking) { ?>
                <p class="muted" style="margin-top:.75rem">Hay puntos marcados en <span class="err">rojo</span>: deben resolverse antes de seguir; el botón «Continuar al perfil» permanece desactivado hasta entonces.</p>
            <?php } ?>
            <form method="post" id="installWizardFixForm" action="">
                <input type="hidden" name="csrf" value="<?php echo $esc($csrfToken); ?>">
                <input type="hidden" name="install_action" value="wizard_fix">
            </form>
            <div class="wiz-actions">
                <button type="button" class="btn" id="wizBtnNext" <?php echo $blocking ? 'disabled' : ''; ?>>Continuar al perfil</button>
                <button type="button" class="btn btn-ghost" onclick="location.reload()">Ejecutar diagnóstico de nuevo</button>
            </div>
        </div>

        <div class="card" id="wizPaneProfile" style="<?php echo $esc($wizProfStyle); ?>">
            <div class="wiz-profile-head">
                <strong>Perfil de uso y organización</strong>
                <button type="button" class="btn btn-ghost" id="wizBtnBack">← Volver al diagnóstico</button>
            </div>
            <form method="post" enctype="multipart/form-data" id="installCompanyForm">
            <input type="hidden" name="csrf" value="<?php echo $esc($csrfToken); ?>">
            <input type="hidden" name="install_action" value="company">
            <p class="muted" style="margin:.5rem 0 .75rem">Indica si Flexia es para <strong>ti solo</strong> o para una <strong>empresa u organización</strong>. En empresa defines <strong>sucursales</strong> (oficinas o equipos): cada una agrupa a sus usuarios de forma clara y más adelante podrás afinar <strong>quién puede ver a quién</strong> entre sucursales desde el panel de administración.</p>

            <div class="install-mode">
            <fieldset>
                <legend>¿Cómo vas a usar Flexia?</legend>
                <label><input type="radio" name="deployment_kind" value="personal" required <?php echo $fvDeploymentKind === 'personal' ? 'checked' : ''; ?>> Uso personal — un solo espacio de trabajo (una «casa» principal para tus usuarios)</label>
                <label><input type="radio" name="deployment_kind" value="company" <?php echo $fvDeploymentKind === 'company' ? 'checked' : ''; ?>> Empresa u organización — varias sedes o equipos, con reglas claras entre ellos</label>
            </fieldset>

            <div id="panelPersonal">
                <h2 class="install-h2">Personal</h2>
                <div id="installLogoPersonal" class="install-profile-image-block">
                <h3 class="install-h3" style="margin:0 0 .5rem;font-size:.92rem">Imagen perfil</h3>
                <div class="logo-box">
                    <div class="logo-selected-preview" id="personalLogoPreviewWrap" aria-live="polite">
                        <span class="muted logo-selected-preview__caption">Vista previa (confirmada)</span>
                        <div class="install-logo-media-wrap" data-install-media-wrap>
                            <img data-antara-profile-img alt="" hidden />
                            <span data-antara-profile-ph aria-hidden="true">?</span>
                        </div>
                    </div>
                    <input type="file" id="personalProfileLogo" name="companyLogo" class="install-file-input" accept="image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm">
                    <input type="hidden" name="company_logo_source" id="personal_logo_source" value="original">
                    <input type="hidden" name="company_logo_media_frame" id="personal_logo_media_frame" value="">
                </div>
                </div>
                <label for="personalDisplayName">Nombre para mostrar *</label>
                <input type="text" id="personalDisplayName" name="personalDisplayName" maxlength="200" required placeholder="Ej. Ana o «Mi escritorio»" value="<?php echo $esc($fvPersonalDisplay); ?>">
                <label>Tu ubicación</label>
                <?php
                echo antara_install_render_location_block($fvPersonalLocation, [
                    'mode' => 'personal',
                    'esc' => $esc,
                    'readonly' => false,
                ]);
                ?>
            </div>

            <div id="panelCompany" style="display:none">
                <input type="hidden" name="matrix_branch_id" id="matrixBranchId" value="<?php echo $esc($fvMatrixBranchId); ?>">
                <h2 class="install-h2">Empresa</h2>
                <h3 class="install-h3" style="margin:0 0 .5rem;font-size:.92rem">Imagen / logo de la empresa</h3>
                <label for="companyLogo">Logo de la empresa (matriz)</label>
                <div class="logo-box" id="companyLogoBox">
                    <div class="logo-selected-preview" id="logoSelectedPreviewWrap" aria-live="polite">
                        <span class="muted logo-selected-preview__caption">Vista previa (confirmada)</span>
                        <div class="install-logo-media-wrap" data-install-media-wrap>
                            <img data-antara-profile-img alt="" hidden />
                            <span data-antara-profile-ph aria-hidden="true">?</span>
                        </div>
                    </div>
                    <input type="file" id="companyLogo" name="companyLogo" class="install-file-input" accept="image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm">
                    <input type="hidden" name="company_logo_source" id="company_logo_source" value="original">
                    <input type="hidden" name="company_logo_media_frame" id="company_logo_media_frame" value="">
                </div>
                <label for="companyName">Nombre de la empresa *</label>
                <input type="text" id="companyName" name="companyName" maxlength="200" placeholder="Ej. Distribuidora Norte S.A." value="<?php echo $esc($fvCompanyName); ?>">
                <label>Dirección</label>
                <?php
                echo antara_install_render_location_block($fvCompanyLocation, [
                    'mode' => 'company',
                    'esc' => $esc,
                    'readonly' => false,
                ]);
                ?>
                <label for="companyEmail">Correo de contacto *</label>
                <input type="email" id="companyEmail" name="companyEmail" maxlength="200" required placeholder="contacto@empresa.com" value="<?php echo $esc($fvCompanyEmail); ?>">
                <label for="companyPhone">Teléfono *</label>
                <input type="tel" id="companyPhone" name="companyPhone" maxlength="40" required autocomplete="tel" class="install-phone-input" placeholder="+52…" value="<?php echo $esc($fvCompanyPhone); ?>">

                <h2 class="install-h2">Cuenta de administrador principal</h2>
                <p class="muted" style="margin:0 0 .65rem;font-size:.85rem">Además del contacto de la empresa, define la <strong>primera cuenta de acceso</strong> (super administrador). Puedes usar los mismos correo y teléfono de contacto o datos distintos.</p>
                <fieldset class="company-access-fieldset">
                    <legend>Acceso al escritorio</legend>
                    <label><input type="radio" name="access_mode" value="first_user" <?php echo $fvAccessMode !== 'public' ? 'checked' : ''; ?>> Crear la primera cuenta de super administrador y pedir inicio de sesión</label>
                    <label style="margin-top:.45rem;display:block"><input type="radio" name="access_mode" value="public" <?php echo $fvAccessMode === 'public' ? 'checked' : ''; ?>> Dejar el escritorio abierto sin cuentas todavía</label>
                </fieldset>
                <div id="companyFirstUserFields">
                    <label for="companyAdminUsername">Nombre completo *</label>
                    <input type="text" id="companyAdminUsername" name="username" maxlength="120" placeholder="Ej. nombre y apellidos" autocomplete="name" value="<?php echo $esc($fvAdminUsername); ?>">
                    <label for="companyAdminEmail">Correo de la cuenta *</label>
                    <input type="email" id="companyAdminEmail" name="email" maxlength="200" placeholder="admin@empresa.com" inputmode="email" autocomplete="email" value="<?php echo $esc($fvAdminEmail); ?>">
                    <label for="companyAdminPhone">Teléfono de la cuenta *</label>
                    <input type="tel" id="companyAdminPhone" name="phone" maxlength="40" class="install-phone-input" placeholder="+52…" value="<?php echo $esc($fvAdminPhone); ?>">
                    <label for="companyAdminPassword">Contraseña *</label>
                    <input type="password" id="companyAdminPassword" name="password" minlength="8" autocomplete="new-password" placeholder="Mínimo 8 caracteres; letra y número">
                    <label for="companyAdminPasswordConfirm">Confirmar contraseña *</label>
                    <input type="password" id="companyAdminPasswordConfirm" name="password_confirm" minlength="8" autocomplete="new-password" placeholder="Repite la contraseña">
                    <div class="pwd-meter" id="companyPwdStrengthWrap" aria-live="polite">
                        <span id="companyPwdStrengthLabel">Seguridad de la contraseña: —</span>
                        <div class="pwd-meter-bar pwd-meter--0" id="companyPwdStrengthBar"><span></span></div>
                    </div>
                </div>

                <h2 class="install-h2">Sucursales adicionales</h2>
                <p class="muted" style="margin:0 0 .65rem">La <strong>matriz</strong> es la empresa (arriba). Aquí defines otras sedes o equipos con su propia dirección, correo y teléfono. Puedes reutilizar el logo de la empresa en cada sucursal.</p>
                <div id="branchRows">
                    <?php
                    require_once GRID_ROOT . '/includes/install_branch_form.php';
                    $branchAppBase = antara_public_app_base();
                    foreach ($wizBranchRows as $bri => $brRow) {
                        echo antara_install_render_branch_row((int) $bri, is_array($brRow) ? $brRow : [], $esc, $branchAppBase);
                    }
                    ?>
                </div>
                <p><button type="button" class="btn" id="addBranchBtn" style="margin-top:.35rem">+ Añadir sucursal</button></p>
                <template id="branchRowTemplate"><?php
                    echo antara_install_render_branch_row(0, [
                        'id' => '',
                        'name' => '',
                        'address' => '',
                        'email' => '',
                        'phone' => '',
                        'usePrimaryLogo' => true,
                        'logoRel' => '',
                        'createUser' => false,
                        'userUsername' => '',
                        'userEmail' => '',
                        'userPhone' => '',
                        'userRole' => 'admin',
                    ], $esc, $branchAppBase);
                ?></template>

                <fieldset class="branch-policy-fieldset" style="margin-top:1rem">
                    <legend>Política entre sucursales</legend>
                    <p class="muted" style="margin:0 0 .65rem;font-size:.82rem">Más adelante los administradores podrán ajustar permisos persona a persona. Aquí solo eliges el <strong>comportamiento por defecto</strong> al arrancar.</p>
                    <label class="branch-policy-option"><input type="radio" name="branch_access_policy" value="explicit_grant" <?php echo $fvBranchPolicy !== 'open_managed' ? 'checked' : ''; ?>>
                        <span><strong>Restringido (recomendado)</strong> — acceso a otra sucursal solo con permiso explícito.<br>
                        <span class="muted branch-policy-example">Ejemplo: un vendedor de <em>Guadalajara</em> no ve clientes ni pedidos de <em>Monterrey</em> hasta que un administrador le conceda acceso a esa sede.</span></span>
                    </label>
                    <label class="branch-policy-option"><input type="radio" name="branch_access_policy" value="open_managed" <?php echo $fvBranchPolicy === 'open_managed' ? 'checked' : ''; ?>>
                        <span><strong>Flexible</strong> — misma empresa; los administradores gestionan permisos entre sucursales con más libertad.<br>
                        <span class="muted branch-policy-example">Ejemplo: el responsable de operaciones puede dar a contabilidad visibilidad sobre varias sedes sin pedir permiso caso por caso para cada usuario.</span></span>
                    </label>
                </fieldset>
            </div>
            </div>

            <p><button type="submit" class="btn" <?php echo $blocking ? 'disabled' : ''; ?>>Continuar</button></p>
            </form>

            <dialog class="install-confirm-dialog" id="installBranchRemoveDialog" aria-labelledby="installBranchRemoveTitle">
                <div class="install-confirm-inner">
                    <h3 class="install-confirm-title" id="installBranchRemoveTitle">¿Quitar esta sucursal?</h3>
                    <p class="install-confirm-text" id="installBranchRemoveText">Se eliminarán los datos de esta sucursal del formulario. Esta acción no se puede deshacer hasta que guardes de nuevo el paso.</p>
                    <div class="install-confirm-actions">
                        <button type="button" class="btn btn-ghost" id="installBranchRemoveCancel">Cancelar</button>
                        <button type="button" class="btn" id="installBranchRemoveConfirm">Sí, quitar</button>
                    </div>
                </div>
            </dialog>

            <?php require GRID_ROOT . '/admin/_image_editor_dialog.php'; ?>
        <script src="<?php echo $esc(antara_public_app_base() . 'assets/js/install_geo.js'); ?>"></script>
        <script src="<?php echo $esc(antara_vendor_asset_url(ANTARA_VENDOR_INTL_TEL_JS, $vendorPublicBase)); ?>"></script>
        <script src="<?php echo $esc(antara_public_app_base() . 'assets/js/install_phone.js'); ?>"></script>
        <script src="<?php echo $esc(antara_public_app_base() . 'assets/auth/avatar_square_crop.js'); ?>"></script>
        <script src="<?php echo $esc(antara_public_app_base() . 'assets/auth/profile_image_sync.js'); ?>"></script>
        <script src="<?php echo $esc(antara_public_app_base() . 'assets/js/install_image_picker.js'); ?>"></script>
        <script src="<?php echo $esc(antara_vendor_asset_url(ANTARA_VENDOR_CROPPER_JS, $vendorPublicBase)); ?>"></script>
        <script>
        (function(){
            var INSTALL_GEO_URL = <?php echo json_encode($installGeoDataUrl, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
            var WIZ_OPEN_PROFILE = <?php echo $wizProfOpen ? 'true' : 'false'; ?>;
            var WIZ_EXISTING_LOGO = <?php echo json_encode($wizExistingLogoUrl, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
            var WIZ_EXISTING_LOGO_FRAME = <?php echo json_encode($wizExistingLogoFrame, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
            var WIZ_EXISTING_LOGO_KIND = <?php echo json_encode($wizExistingLogoKind, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
            var paneS = document.getElementById('wizPaneSecurity');
            var paneP = document.getElementById('wizPaneProfile');
            var dot1 = document.getElementById('wizDot1');
            var dot2 = document.getElementById('wizDot2');
            var btnNext = document.getElementById('wizBtnNext');
            var btnBack = document.getElementById('wizBtnBack');
            function scrollWizardToTop() {
                window.scrollTo(0, 0);
            }
            function showProfile(on) {
                if (paneS) paneS.style.display = on ? 'none' : '';
                if (paneP) paneP.style.display = on ? '' : 'none';
                if (dot1) dot1.classList.toggle('is-on', !on);
                if (dot2) dot2.classList.toggle('is-on', !!on);
                scrollWizardToTop();
            }
            if (btnNext) btnNext.addEventListener('click', function() { showProfile(true); });
            if (btnBack) btnBack.addEventListener('click', function() { showProfile(false); });
            if (WIZ_OPEN_PROFILE) showProfile(true);
            var form = document.getElementById('installCompanyForm');
            if (!form) return;
            var deploymentRadios = form.querySelectorAll('input[name="deployment_kind"]');
            var panelP = document.getElementById('panelPersonal');
            var panelC = document.getElementById('panelCompany');
            var companyNameInput = document.getElementById('companyName');
            var personalDisplayInput = document.getElementById('personalDisplayName');
            var companyEmailInput = document.getElementById('companyEmail');
            var companyPhoneInput = document.getElementById('companyPhone');
            var branchRows = document.getElementById('branchRows');
            var addBranchBtn = document.getElementById('addBranchBtn');
            var logoPersonalWrap = document.getElementById('installLogoPersonal');
            var branchRowTpl = document.getElementById('branchRowTemplate');

            function isCompany() {
                var r = form.querySelector('input[name="deployment_kind"][value="company"]');
                return r && r.checked;
            }
            function syncPanels() {
                var comp = isCompany();
                if (panelP) panelP.style.display = comp ? 'none' : 'block';
                if (panelC) panelC.style.display = comp ? 'block' : 'none';
                if (companyNameInput) {
                    if (comp) companyNameInput.setAttribute('required', 'required');
                    else companyNameInput.removeAttribute('required');
                }
                if (personalDisplayInput) {
                    if (comp) personalDisplayInput.removeAttribute('required');
                    else personalDisplayInput.setAttribute('required', 'required');
                }
                if (companyEmailInput) {
                    if (comp) companyEmailInput.setAttribute('required', 'required');
                    else companyEmailInput.removeAttribute('required');
                }
                if (companyPhoneInput) {
                    if (comp) companyPhoneInput.setAttribute('required', 'required');
                    else companyPhoneInput.removeAttribute('required');
                }
                function setDisabled(root, dis) {
                    if (!root) return;
                    root.querySelectorAll('input,select,textarea,button').forEach(function(x) {
                        if (x.name === 'csrf' || x.name === 'install_action') return;
                        if (dis) x.setAttribute('disabled', 'disabled');
                        else x.removeAttribute('disabled');
                    });
                }
                setDisabled(panelP, comp);
                setDisabled(panelC, !comp);
                setDisabled(logoPersonalWrap, comp);
                if (branchRows) {
                    branchRows.querySelectorAll('input[name="branch_name[]"]').forEach(function(inp) {
                        if (comp) inp.setAttribute('required', 'required');
                        else inp.removeAttribute('required');
                    });
                }
                if (logoPersonalWrap) logoPersonalWrap.style.display = comp ? 'none' : 'block';
                if (typeof bindLogoEls === 'function') bindLogoEls();
                initVisiblePhoneInputs(comp ? panelC : null);
            }
            function initVisiblePhoneInputs(scope) {
                if (!window.InstallPhone) return;
                var root = scope || form;
                if (!root) return;
                root.querySelectorAll('input[type="tel"]').forEach(function(el) {
                    if (el.disabled) return;
                    if (el.closest('#panelCompany') && !isCompany()) return;
                    window.InstallPhone.init(el);
                });
            }
            deploymentRadios.forEach(function(r) { r.addEventListener('change', syncPanels); });
            syncPanels();
            <?php if (($_POST['deployment_kind'] ?? '') === 'company') { ?>
            (function() {
                var c = form.querySelector('input[name="deployment_kind"][value="company"]');
                if (c) c.checked = true;
                syncPanels();
            })();
            <?php } ?>
            <?php if (($_POST['branch_access_policy'] ?? '') === 'open_managed') { ?>
            (function() {
                var o = form.querySelector('input[name="branch_access_policy"][value="open_managed"]');
                if (o) o.checked = true;
            })();
            <?php } ?>

            function reindexBranchRows() {
                if (!branchRows) return;
                var rows = branchRows.querySelectorAll('.branch-row');
                rows.forEach(function(row, idx) {
                    row.setAttribute('data-branch-index', String(idx));
                    row.setAttribute('data-branch-primary', '0');
                    var title = row.querySelector('.install-h3');
                    if (title) title.textContent = 'Sucursal ' + (idx + 1);
                    var logoHidden = row.querySelector('.branch-use-primary-logo-hidden');
                    if (logoHidden) logoHidden.name = 'branch_use_primary_logo[' + idx + ']';
                    var userHidden = row.querySelector('.branch-user-create-hidden');
                    if (userHidden) userHidden.name = 'branch_user_create[' + idx + ']';
                });
            }
            function initInstallLocationBlocks(root) {
                if (!window.InstallGeo || !INSTALL_GEO_URL) return;
                var scope = root || form || document;
                scope.querySelectorAll('[data-install-location-block]').forEach(function(block) {
                    if (block.getAttribute('data-geo-inited') === '1') return;
                    var jsonEl = block.querySelector('[data-geo-json]');
                    var initial = {};
                    if (jsonEl && jsonEl.value) {
                        try { initial = JSON.parse(jsonEl.value); } catch (e1) { initial = {}; }
                    }
                    InstallGeo.initLocationBlock(block, {
                        dataUrl: INSTALL_GEO_URL,
                        initial: initial,
                        readonly: block.getAttribute('data-location-readonly') === '1'
                    });
                });
            }
            function syncAllLocationHidden() {
                (form || document).querySelectorAll('[data-install-location-block]').forEach(function(block) {
                    if (typeof block._installGeoSync === 'function') block._installGeoSync();
                });
            }
            if (form) {
                form.addEventListener('submit', function() { syncAllLocationHidden(); });
            }
            initInstallLocationBlocks(form);

            var logoBindings = [];
            var companyFirstUserWrap = document.getElementById('companyFirstUserFields');
            var companyPwdEl = document.getElementById('companyAdminPassword');
            var companyPwd2El = document.getElementById('companyAdminPasswordConfirm');
            var companyPwdLabel = document.getElementById('companyPwdStrengthLabel');
            var companyPwdBar = document.getElementById('companyPwdStrengthBar');

            function pwdScore(pw) {
                var s = 0;
                if (!pw || pw.length < 8) return 0;
                s = 1;
                if (pw.length >= 10) s++;
                if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++;
                if (/\d/.test(pw)) s++;
                if (/[^A-Za-z0-9]/.test(pw)) s++;
                return Math.min(4, s);
            }
            function pwdStrengthText(lv) {
                if (lv <= 0) return 'Seguridad: muy débil';
                if (lv === 1) return 'Seguridad: débil';
                if (lv === 2) return 'Seguridad: aceptable';
                if (lv === 3) return 'Seguridad: buena';
                return 'Seguridad: fuerte';
            }
            function updateCompanyPwdMeter() {
                if (!companyPwdEl || !companyPwdLabel || !companyPwdBar) return;
                var sc = pwdScore(companyPwdEl.value || '');
                companyPwdLabel.textContent = pwdStrengthText(sc);
                companyPwdBar.className = 'pwd-meter-bar pwd-meter--' + sc;
            }
            function syncCompanyFirstUserPanel() {
                if (!companyFirstUserWrap) return;
                var pub = form.querySelector('input[name="access_mode"][value="public"]');
                var isPublic = pub && pub.checked;
                companyFirstUserWrap.hidden = !!isPublic;
                companyFirstUserWrap.querySelectorAll('input,select').forEach(function(el) {
                    if (isPublic) el.removeAttribute('required');
                    else if (el.name === 'username' || el.name === 'email' || el.name === 'phone' || el.name === 'password' || el.name === 'password_confirm') {
                        el.setAttribute('required', 'required');
                    }
                });
            }
            form.querySelectorAll('input[name="access_mode"]').forEach(function(r) {
                r.addEventListener('change', syncCompanyFirstUserPanel);
            });
            if (companyPwdEl) {
                companyPwdEl.addEventListener('input', updateCompanyPwdMeter);
                updateCompanyPwdMeter();
            }
            syncCompanyFirstUserPanel();

            function initBranchRow(row) {
                if (!row) return;
                initInstallLocationBlocks(row);
                var logoCb = row.querySelector('.branch-use-primary-logo');
                var logoHidden = row.querySelector('.branch-use-primary-logo-hidden');
                var logoFields = row.querySelector('.branch-logo-fields');
                var userCb = row.querySelector('.branch-create-user');
                var userHidden = row.querySelector('.branch-user-create-hidden');
                var userFields = row.querySelector('[data-branch-user-fields]');
                function syncBranchLogo() {
                    var on = logoCb && logoCb.checked;
                    if (logoHidden) logoHidden.value = on ? '1' : '0';
                    if (logoFields) {
                        logoFields.style.display = on ? 'none' : 'block';
                    }
                }
                function syncBranchUser() {
                    var on = userCb && userCb.checked;
                    if (userHidden) userHidden.value = on ? '1' : '0';
                    if (userFields) userFields.hidden = !on;
                    if (!userFields) return;
                    userFields.querySelectorAll('input,select').forEach(function(el) {
                        if (on) el.setAttribute('required', 'required');
                        else el.removeAttribute('required');
                    });
                }
                if (logoCb) {
                    logoCb.addEventListener('change', syncBranchLogo);
                    syncBranchLogo();
                }
                if (userCb) {
                    userCb.addEventListener('change', syncBranchUser);
                    syncBranchUser();
                }
                initVisiblePhoneInputs(row);
                var bf = row.querySelector('.branch-logo-file');
                if (bf && window.InstallImagePicker && !bf.getAttribute('data-install-image-bound')) {
                    bf.setAttribute('data-install-image-bound', '1');
                    var wrap = row.querySelector('.branch-logo-preview');
                    var src = row.querySelector('.branch-logo-source');
                    var mf = row.querySelector('.branch-logo-media-frame');
                    var img = row.querySelector('.branch-logo-preview-img');
                    var existing = img && img.getAttribute('src') ? img.getAttribute('src') : '';
                    var lb = window.InstallImagePicker.bind({
                        fileInput: bf,
                        sourceHidden: src,
                        mediaFrameHidden: mf,
                        previewWrap: wrap,
                        previewImg: img,
                        dialogTitle: 'Logo de sucursal',
                        existingUrl: existing,
                        previewInitial: 'S'
                    });
                    if (lb) logoBindings.push(lb);
                }
            }
            if (branchRows) {
                branchRows.querySelectorAll('.branch-row').forEach(initBranchRow);
                reindexBranchRows();
            }
            if (addBranchBtn && branchRows) {
                addBranchBtn.addEventListener('click', function() {
                    var node;
                    if (branchRowTpl && branchRowTpl.content) {
                        node = branchRowTpl.content.firstElementChild.cloneNode(true);
                    } else {
                        return;
                    }
                    node.querySelectorAll('input,textarea,select').forEach(function(i) {
                        if (i.type === 'checkbox' && i.classList.contains('branch-use-primary-logo')) {
                            i.checked = true;
                        } else if (i.type === 'checkbox' && i.classList.contains('branch-create-user')) {
                            i.checked = false;
                        } else if (i.type !== 'hidden') {
                            i.value = '';
                        }
                    });
                    var lh = node.querySelector('.branch-use-primary-logo-hidden');
                    if (lh) lh.value = '1';
                    var uh = node.querySelector('.branch-user-create-hidden');
                    if (uh) uh.value = '0';
                    var uf = node.querySelector('[data-branch-user-fields]');
                    if (uf) uf.hidden = true;
                    var prev = node.querySelector('.branch-logo-preview');
                    if (prev) { prev.classList.remove('is-on'); var im = prev.querySelector('img'); if (im) im.removeAttribute('src'); }
                    branchRows.appendChild(node);
                    initBranchRow(node);
                    reindexBranchRows();
                });
            }
            var branchRemoveDialog = document.getElementById('installBranchRemoveDialog');
            var branchRemoveText = document.getElementById('installBranchRemoveText');
            var branchRemoveCancel = document.getElementById('installBranchRemoveCancel');
            var branchRemoveConfirm = document.getElementById('installBranchRemoveConfirm');
            var pendingBranchRemoveRow = null;
            function openBranchRemoveConfirm(row) {
                if (!row || !branchRemoveDialog) return;
                pendingBranchRemoveRow = row;
                var titleEl = row.querySelector('.install-h3');
                var branchLabel = titleEl ? titleEl.textContent.trim() : 'esta sucursal';
                if (branchRemoveText) {
                    branchRemoveText.textContent = '¿Deseas quitar «' + branchLabel + '»? Se eliminarán sus datos del formulario.';
                }
                if (typeof branchRemoveDialog.showModal === 'function') {
                    branchRemoveDialog.showModal();
                }
            }
            function closeBranchRemoveConfirm() {
                pendingBranchRemoveRow = null;
                if (branchRemoveDialog && typeof branchRemoveDialog.close === 'function') {
                    branchRemoveDialog.close();
                }
            }
            if (branchRemoveCancel) {
                branchRemoveCancel.addEventListener('click', closeBranchRemoveConfirm);
            }
            if (branchRemoveConfirm) {
                branchRemoveConfirm.addEventListener('click', function() {
                    if (pendingBranchRemoveRow) {
                        pendingBranchRemoveRow.remove();
                        reindexBranchRows();
                    }
                    closeBranchRemoveConfirm();
                    if (window.InstallWizardPersist) {
                        window.InstallWizardPersist.scheduleSave(true);
                    }
                });
            }
            if (branchRemoveDialog) {
                branchRemoveDialog.addEventListener('cancel', function(e) {
                    e.preventDefault();
                    closeBranchRemoveConfirm();
                });
                branchRemoveDialog.addEventListener('click', function(e) {
                    if (e.target === branchRemoveDialog) closeBranchRemoveConfirm();
                });
            }
            if (branchRows) {
                branchRows.addEventListener('click', function(e) {
                    var t = e.target.closest('.btn-remove-branch');
                    if (!t) return;
                    e.preventDefault();
                    var row = t.closest('.branch-row');
                    if (row) openBranchRemoveConfirm(row);
                });
            }

            form.addEventListener('submit', function() {
                if (branchRows) {
                    branchRows.querySelectorAll('.branch-row').forEach(function(row) {
                        row.querySelectorAll('input,textarea').forEach(function(el) { el.disabled = false; });
                    });
                }
                if (window.InstallPhone) window.InstallPhone.syncAll();
            });

            function bindLogoEls() {}
            if (window.InstallImagePicker) {
                window.InstallImagePicker.ensureDialog();
                var lbCompany = window.InstallImagePicker.bind({
                    fileInput: document.getElementById('companyLogo'),
                    sourceHidden: document.getElementById('company_logo_source'),
                    mediaFrameHidden: document.getElementById('company_logo_media_frame'),
                    previewWrap: document.getElementById('logoSelectedPreviewWrap'),
                    dialogTitle: 'Logo de la empresa',
                    existingUrl: WIZ_EXISTING_LOGO || '',
                    existingFrame: WIZ_EXISTING_LOGO_FRAME || null,
                    existingKind: WIZ_EXISTING_LOGO_KIND || null,
                    previewInitial: 'E'
                });
                var lbPersonal = window.InstallImagePicker.bind({
                    fileInput: document.getElementById('personalProfileLogo'),
                    sourceHidden: document.getElementById('personal_logo_source'),
                    mediaFrameHidden: document.getElementById('personal_logo_media_frame'),
                    previewWrap: document.getElementById('personalLogoPreviewWrap'),
                    dialogTitle: 'Imagen perfil',
                    existingUrl: WIZ_EXISTING_LOGO || '',
                    existingFrame: WIZ_EXISTING_LOGO_FRAME || null,
                    existingKind: WIZ_EXISTING_LOGO_KIND || null,
                    previewInitial: 'P'
                });
                if (lbCompany) logoBindings.push(lbCompany);
                if (lbPersonal) logoBindings.push(lbPersonal);
            }
            form.addEventListener('submit', async function(ev) {
                syncCompanyFirstUserPanel();
                if (window.InstallPhone) window.InstallPhone.syncAll();
                if (!window.InstallImagePicker) return;
                var comp = isCompany();
                for (var i = 0; i < logoBindings.length; i++) {
                    var b = logoBindings[i];
                    if (!b || !b.fileInput) continue;
                    var isCo = b.fileInput.id === 'companyLogo';
                    var isBranch = b.fileInput.classList && b.fileInput.classList.contains('branch-logo-file');
                    if (isBranch && !comp) continue;
                    if (comp && !isCo && !isBranch) continue;
                    if (!comp && isCo) continue;
                    if (!b.fileInput.files || !b.fileInput.files[0]) continue;
                    if (!window.InstallImagePicker.validateSubmit(b)) {
                        ev.preventDefault();
                        await installAlert('La imagen o vídeo no está confirmado. Vuelve a elegir el archivo y completa el cuadro de diálogo.');
                        return;
                    }
                }
                if (comp) {
                    var pubEl = form.querySelector('input[name="access_mode"][value="public"]');
                    if (!pubEl || !pubEl.checked) {
                        var em = (document.getElementById('companyAdminEmail') || {}).value || '';
                        var un = (document.getElementById('companyAdminUsername') || {}).value || '';
                        var ph = (document.getElementById('companyAdminPhone') || {}).value || '';
                        if (!String(em).trim() || !String(un).trim() || !String(ph).trim()) {
                            ev.preventDefault();
                            await installAlert('Completa los datos de la cuenta de administrador principal.');
                            return;
                        }
                        if (companyPwdEl && companyPwd2El) {
                            var a = companyPwdEl.value || '';
                            var b2 = companyPwd2El.value || '';
                            if (a !== b2 || a.length < 8 || !/[A-Za-z]/.test(a) || !/[0-9]/.test(a)) {
                                ev.preventDefault();
                                await installAlert('Revisa la contraseña del administrador principal (mínimo 8 caracteres, letra y número, y deben coincidir).');
                                return;
                            }
                        }
                    }
                }
            });
        })();
        </script>
        </div>
    <?php } elseif ($step === 2) { ?>
        <p class="wiz-steps-caption">1 · Entorno y perfil · 2 · Acceso inicial · 3 · PWA</p>
        <nav class="wiz-steps" aria-label="Pasos del instalador">
            <a class="wiz-dot wiz-step-link" href="<?php echo $esc($installWizardBaseUrl . '?step=1&profile=1'); ?>" title="Paso 1">1</a>
            <span class="wiz-line"></span>
            <span class="wiz-dot is-on" aria-current="step">2</span>
            <span class="wiz-line"></span>
            <?php if ($installCanVisitStep3) { ?>
            <a class="wiz-dot wiz-step-link" href="<?php echo $esc($installWizardBaseUrl . '?step=3'); ?>" title="Paso 3">3</a>
            <?php } else { ?>
            <span class="wiz-dot">3</span>
            <?php } ?>
        </nav>
        <?php if (is_array($installPreview)) { ?>
        <div class="card">
            <strong>Resumen de organización</strong>
            <p class="muted" style="margin:.5rem 0 0">Tipo: <strong><?php echo $esc(($installPreview['deploymentKind'] ?? '') === 'company' ? 'Empresa' : 'Personal'); ?></strong>
            <?php if (($installPreview['deploymentKind'] ?? '') === 'company' && !empty($installPreview['companyName'])) { ?>
                — <?php echo $esc((string)$installPreview['companyName']); ?>
            <?php } elseif (($installPreview['deploymentKind'] ?? '') === 'personal' && !empty($installPreview['personalDisplayName'])) { ?>
                — <?php echo $esc((string)$installPreview['personalDisplayName']); ?>
            <?php } ?>
            </p>
            <p class="muted" style="margin:.35rem 0 0">Sucursales definidas: <?php echo (int)count($installPreview['branches'] ?? []); ?>.
            <?php
            $pol = (string)(($installPreview['branchAccessDefaults'] ?? [])['crossBranchAccessPolicy'] ?? 'explicit_grant');
            echo $pol === 'open_managed'
                ? 'Entre sucursales: <strong>más flexible</strong> (los administradores gestionan el alcance con más libertad).'
                : 'Entre sucursales: <strong>más cerrado</strong> (solo con permiso explícito se cruza información entre sedes).';
            ?>
            </p>
            <ul class="checks" style="margin-top:.5rem">
                <?php foreach (($installPreview['branches'] ?? []) as $br) {
                    if (!is_array($br)) {
                        continue;
                    }
                    echo '<li class="ok">' . $esc((string)($br['name'] ?? '—'));
                    if (!empty($br['code'])) {
                        echo ' <span class="muted">(' . $esc((string)$br['code']) . ')</span>';
                    }
                    echo '</li>';
                } ?>
            </ul>
        </div>
        <?php } ?>
        <form method="post" enctype="multipart/form-data" class="card" id="installStep2Form">
            <input type="hidden" name="csrf" value="<?php echo $esc(AntaraCsrf::token()); ?>">
            <input type="hidden" name="install_action" value="continue_to_pwa">
            <strong>Quién puede entrar al principio</strong>
            <fieldset>
                <legend>Elige una opción</legend>
                <label><input type="radio" name="access_mode" value="first_user" <?php echo $fvAccessMode === 'first_user' ? 'checked' : ''; ?>> Crear la primera cuenta de super administrador (rol <code>super_admin</code>) y pedir inicio de sesión para abrir el escritorio</label>
                <label style="margin-top:.5rem"><input type="radio" name="access_mode" value="public" <?php echo $fvAccessMode === 'public' ? 'checked' : ''; ?>> Dejar el escritorio abierto sin cuentas todavía (cualquiera con el enlace puede verlo; luego podrás cerrarlo)</label>
            </fieldset>
            <div id="firstUserFields">
                <label for="firstAdminProfileImage">Imagen</label>
                <div class="logo-box">
                    <div class="logo-selected-preview" id="firstAdminPreviewWrap" aria-live="polite">
                        <span class="muted logo-selected-preview__caption">Vista previa (confirmada)</span>
                        <div class="install-logo-media-wrap" data-install-media-wrap>
                            <img data-antara-profile-img alt="" hidden />
                            <span data-antara-profile-ph aria-hidden="true">A</span>
                        </div>
                    </div>
                    <input type="file" id="firstAdminProfileImage" name="firstAdminProfileImage" class="install-file-input" accept="image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm">
                    <input type="hidden" name="first_admin_logo_source" id="first_admin_logo_source" value="original">
                    <input type="hidden" name="first_admin_media_frame" id="first_admin_media_frame" value="">
                </div>
                <label for="username">Nombre completo *</label>
                <input type="text" id="username" name="username" maxlength="120" required placeholder="Ej. nombre y apellidos" autocomplete="name" autocapitalize="words" spellcheck="false" value="<?php echo $esc($fvAdminUsername); ?>">
                <label for="email">Correo *</label>
                <input type="email" id="email" name="email" maxlength="200" required placeholder="admin@tu-dominio.com" inputmode="email" autocomplete="email" value="<?php echo $esc($fvAdminEmail); ?>">
                <label for="phone">Teléfono *</label>
                <input type="tel" id="phone" name="phone" maxlength="40" required autocomplete="tel" placeholder="+52… o +1… (la bandera se ajusta al prefijo)" value="<?php echo $esc($fvAdminPhone); ?>">
                <label for="password">Contraseña *</label>
                <input type="password" id="password" name="password" minlength="8" required autocomplete="new-password" placeholder="Mínimo 8 caracteres; letra y número">
                <label for="password_confirm">Confirmar contraseña *</label>
                <input type="password" id="password_confirm" name="password_confirm" minlength="8" required autocomplete="new-password" placeholder="Repite la contraseña">
                <div class="pwd-meter" id="pwdStrengthWrap" aria-live="polite">
                    <span id="pwdStrengthLabel">Seguridad de la contraseña: —</span>
                    <div class="pwd-meter-bar pwd-meter--0" id="pwdStrengthBar"><span></span></div>
                </div>
            </div>
            <p style="display:flex;flex-wrap:wrap;gap:.5rem;align-items:center">
                <a class="btn btn-ghost" href="<?php echo $esc($installWizardBaseUrl . '?step=1&profile=1'); ?>">← Volver a organización</a>
                <button type="submit" class="btn">Continuar a configuración PWA</button>
            </p>
        </form>
        <?php require GRID_ROOT . '/admin/_image_editor_dialog.php'; ?>
        <script src="<?php echo $esc(antara_vendor_asset_url(ANTARA_VENDOR_INTL_TEL_JS, $vendorPublicBase)); ?>"></script>
        <script src="<?php echo $esc(antara_public_app_base() . 'assets/js/install_phone.js'); ?>"></script>
        <script src="<?php echo $esc(antara_public_app_base() . 'assets/auth/avatar_square_crop.js'); ?>"></script>
        <script src="<?php echo $esc(antara_public_app_base() . 'assets/auth/profile_image_sync.js'); ?>"></script>
        <script src="<?php echo $esc(antara_public_app_base() . 'assets/js/install_image_picker.js'); ?>"></script>
        <script src="<?php echo $esc(antara_vendor_asset_url(ANTARA_VENDOR_CROPPER_JS, $vendorPublicBase)); ?>"></script>
        <script>
        (function() {
            var WIZ_EXISTING_FA = <?php
                $faUrl = '';
                require_once GRID_ROOT . '/includes/install_first_admin_profile.php';
                $faAbs = antara_install_first_admin_profile_resolved_path();
                if ($faAbs !== null) {
                    $faUrl = antara_public_app_base() . 'logos_sinapsys/' . rawurlencode(basename($faAbs));
                }
                echo json_encode($faUrl, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            ?>;
            var WIZ_EXISTING_FA_FRAME = <?php
                $faFrame = isset($_SESSION['install_first_admin_media_frame']) ? $_SESSION['install_first_admin_media_frame'] : null;
                echo json_encode($faFrame, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            ?>;
            var phoneEl = document.getElementById('phone');
            var finishForm = document.getElementById('installStep2Form');
            var emailEl = document.getElementById('email');
            var usernameEl = document.getElementById('username');
            var pwdEl = document.getElementById('password');
            var pwd2El = document.getElementById('password_confirm');
            var pwdLabel = document.getElementById('pwdStrengthLabel');
            var pwdBar = document.getElementById('pwdStrengthBar');
            if (!finishForm) return;
            function pwdScore(pw) {
                var s = 0;
                if (!pw || pw.length < 8) return 0;
                s = 1;
                if (pw.length >= 10) s++;
                if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++;
                if (/\d/.test(pw)) s++;
                if (/[^A-Za-z0-9]/.test(pw)) s++;
                return Math.min(4, s);
            }
            function pwdStrengthText(lv) {
                if (lv <= 0) return 'Seguridad: muy débil';
                if (lv === 1) return 'Seguridad: débil';
                if (lv === 2) return 'Seguridad: aceptable';
                if (lv === 3) return 'Seguridad: buena';
                return 'Seguridad: fuerte';
            }
            function updatePwdMeter() {
                if (!pwdEl || !pwdLabel || !pwdBar) return;
                var sc = pwdScore(pwdEl.value || '');
                pwdLabel.textContent = pwdStrengthText(sc);
                pwdBar.className = 'pwd-meter-bar pwd-meter--' + sc;
            }
            if (pwdEl) {
                pwdEl.addEventListener('input', function() {
                    pwdEl.setCustomValidity('');
                    updatePwdMeter();
                });
                pwdEl.addEventListener('change', updatePwdMeter);
                updatePwdMeter();
            }
            if (pwd2El) {
                pwd2El.addEventListener('input', function() { pwd2El.setCustomValidity(''); });
            }
            if (window.InstallPhone && phoneEl) {
                window.InstallPhone.init(phoneEl);
            }
            finishForm.addEventListener('submit', async function(ev) {
                var pubEl = document.querySelector('input[name="access_mode"][value="public"]');
                var pub = pubEl && pubEl.checked;
                if (window.InstallPhone) window.InstallPhone.syncAll();
                if (!pub && emailEl && usernameEl && phoneEl) {
                    var em = (emailEl.value || '').trim();
                    var un = (usernameEl.value || '').trim();
                    var ph = (phoneEl.value || '').trim();
                    if (!em || !un || !ph) {
                        ev.preventDefault();
                        await installAlert('Completa correo, nombre completo y teléfono para la primera cuenta.');
                        return;
                    }
                }
                if (!pub && pwdEl && pwd2El) {
                    var a = pwdEl.value || '';
                    var b = pwd2El.value || '';
                    pwdEl.setCustomValidity('');
                    pwd2El.setCustomValidity('');
                    if (a !== b) {
                        ev.preventDefault();
                        pwd2El.setCustomValidity('Las contraseñas deben coincidir.');
                        pwd2El.reportValidity();
                        return;
                    }
                    if (a.length < 8 || !/[A-Za-z]/.test(a) || !/[0-9]/.test(a)) {
                        ev.preventDefault();
                        pwdEl.setCustomValidity('Mínimo 8 caracteres, con al menos una letra y un número.');
                        pwdEl.reportValidity();
                        return;
                    }
                }
            });
        })();
        </script>
        <script>
        (function() {
            var form = document.getElementById('installStep2Form');
            var adminBinding = null;
            if (window.InstallImagePicker) {
                window.InstallImagePicker.ensureDialog();
                adminBinding = window.InstallImagePicker.bind({
                    fileInput: document.getElementById('firstAdminProfileImage'),
                    sourceHidden: document.getElementById('first_admin_logo_source'),
                    mediaFrameHidden: document.getElementById('first_admin_media_frame'),
                    previewWrap: document.getElementById('firstAdminPreviewWrap'),
                    dialogTitle: 'Foto de perfil del administrador',
                    existingUrl: typeof WIZ_EXISTING_FA !== 'undefined' ? WIZ_EXISTING_FA : '',
                    existingFrame: typeof WIZ_EXISTING_FA_FRAME !== 'undefined' ? WIZ_EXISTING_FA_FRAME : null,
                    previewInitial: 'A'
                });
            }
            if (form) {
                form.addEventListener('submit', async function(ev) {
                    var pubEl = document.querySelector('input[name="access_mode"][value="public"]');
                    if (pubEl && pubEl.checked) return;
                    if (!adminBinding || !window.InstallImagePicker) return;
                    if (!adminBinding.fileInput || !adminBinding.fileInput.files || !adminBinding.fileInput.files[0]) return;
                    if (!window.InstallImagePicker.validateSubmit(adminBinding)) {
                        ev.preventDefault();
                        await installAlert('La imagen o vídeo no está confirmado. Vuelve a elegir el archivo y completa el cuadro de diálogo.');
                    }
                });
            }
        })();
        </script>
        <script>
        (function(){
            var modes = document.querySelectorAll('input[name="access_mode"]');
            var box = document.getElementById('firstUserFields');
            if (!box) return;
            function sync(){
                var pubEl = document.querySelector('input[name="access_mode"][value="public"]');
                var pub = pubEl && pubEl.checked;
                box.style.display = pub ? 'none' : 'block';
                box.querySelectorAll('input').forEach(function(i){
                    if (i.type === 'file' || i.type === 'hidden') return;
                    if (pub) {
                        i.removeAttribute('required');
                    } else {
                        i.setAttribute('required', 'required');
                    }
                });
            }
            modes.forEach(function(m){ m.addEventListener('change', sync); });
            sync();
        })();
        </script>
    <?php } elseif ($step === 3) {
        require_once GRID_ROOT . '/includes/install_company_logo.php';
        $pwa = is_array($installPreview['pwa'] ?? null) ? $installPreview['pwa'] : [];
        if (($installPreview['deploymentKind'] ?? '') === 'company') {
            $dn = trim((string)($installPreview['companyName'] ?? ''));
            $pwaDefName = $dn !== '' ? $dn : (trim((string)($pwa['name'] ?? '')) ?: 'Flexia');
        } else {
            $pwaDefName = trim((string)($pwa['name'] ?? '')) ?: 'Flexia';
        }
        $pwaProfileLogoPath = antara_install_company_logo_resolved_path();
        $savedProfileImageThisInstall = !empty($_SESSION['install_profile_image_saved']);
        $hasProfileLogo = $savedProfileImageThisInstall
            && $pwaProfileLogoPath !== null
            && is_file($pwaProfileLogoPath);
        $pwaPathDefaults = antara_install_pwa_default_scope_and_start();
        $isPwaPost = ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['install_action'] ?? '') === 'finish');
        $pf = static function (string $field, string $fallback) use ($isPwaPost, $installWizardDraft, $pwa): string {
            return antara_install_wizard_pwa_field($field, $isPwaPost, $installWizardDraft, $pwa, $fallback);
        };
        $pName = $pf('pwa_name', $pwaDefName);
        $pShort = $pf('pwa_short_name', trim((string)($pwa['shortName'] ?? $pwaDefName)) ?: $pwaDefName);
        $pDesc = $pf('pwa_description', (string)($pwa['description'] ?? ''));
        $pLang = strtolower(trim((string) $pf('pwa_lang', (string) ($pwa['lang'] ?? 'es'))));
        if ($pLang === '') {
            $pLang = 'es';
        }
        $installPwaLangOptions = [
            'es' => 'Español (es)',
            'en' => 'English (en)',
            'en-us' => 'English — Estados Unidos (en-US)',
            'en-gb' => 'English — Reino Unido (en-GB)',
            'pt' => 'Português (pt)',
            'pt-br' => 'Português — Brasil (pt-BR)',
            'fr' => 'Français (fr)',
            'de' => 'Deutsch (de)',
            'it' => 'Italiano (it)',
            'ca' => 'Català (ca)',
            'gl' => 'Galego (gl)',
            'eu' => 'Euskara (eu)',
        ];
        $pTheme = $pf('pwa_theme_color', (string)($pwa['themeColor'] ?? '#0ea5e9'));
        $pBg = $pf('pwa_background_color', (string)($pwa['backgroundColor'] ?? '#0f172a'));
        $hex7 = static function (string $h, string $def): string {
            $h = trim($h);
            if (preg_match('/^#[0-9a-f]{6}$/i', $h)) {
                return strtolower($h);
            }
            if (preg_match('/^#([0-9a-f])([0-9a-f])([0-9a-f])$/i', $h, $m)) {
                return strtolower('#' . $m[1] . $m[1] . $m[2] . $m[2] . $m[3] . $m[3]);
            }

            return $def;
        };
        $pTheme7 = $hex7($pTheme, '#0ea5e9');
        $pBg7 = $hex7($pBg, '#0f172a');
        $pDisplay = $pf('pwa_display', (string)($pwa['display'] ?? 'standalone'));
        $pOrient = $pf('pwa_orientation', (string)($pwa['orientation'] ?? 'any'));
        $pStart = $pf('pwa_start_url', (string)($pwa['startUrl'] ?? $pwaPathDefaults['startUrl']));
        $pScope = $pf('pwa_scope', (string)($pwa['scope'] ?? $pwaPathDefaults['scope']));
        if ($isPwaPost) {
            $pEn = !empty($_POST['pwa_enabled']);
        } elseif (isset($installWizardDraft['pwa_enabled'])) {
            $pEn = (string) $installWizardDraft['pwa_enabled'] === '1';
        } else {
            $pEn = !empty($pwa['enabled']);
        }
        $h192 = $pf('pwa_icon192_rel', (string)($pwa['icon192Rel'] ?? ''));
        $h512 = $pf('pwa_icon512_rel', (string)($pwa['icon512Rel'] ?? ''));
        $hMask = $pf('pwa_maskable_rel', (string)($pwa['iconMaskable512Rel'] ?? ''));
        $hApple = $pf('pwa_apple_rel', (string)($pwa['appleTouch180Rel'] ?? ''));
        $hSplash = $pf('pwa_splash_rel', (string)($pwa['splashLaunchRel'] ?? ''));
        $pwaRulesForJs = antara_install_pwa_pack_rules_for_view();
        $pCropJson = $pf('pwa_crop_json', '');
        $pwaRulesJsonAttr = $esc(json_encode($pwaRulesForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        $pIconModeRaw = $hasProfileLogo ? $pf('pwa_icon_mode', 'reuse_profile') : 'upload';
        $pIconReuseSelected = $hasProfileLogo && $pIconModeRaw === 'reuse_profile';
        ?>
        <p class="wiz-steps-caption">1 · Entorno y perfil · 2 · Acceso inicial · 3 · PWA</p>
        <nav class="wiz-steps" aria-label="Pasos del instalador">
            <a class="wiz-dot wiz-step-link" href="<?php echo $esc($installWizardBaseUrl . '?step=1&profile=1'); ?>" title="Paso 1">1</a>
            <span class="wiz-line"></span>
            <a class="wiz-dot wiz-step-link" href="<?php echo $esc($installWizardBaseUrl . '?step=2'); ?>" title="Paso 2">2</a>
            <span class="wiz-line"></span>
            <span class="wiz-dot is-on" aria-current="step">3</span>
        </nav>
        <div class="card">
            <strong>Paso 3 — Aplicación instalable (PWA)</strong>
            <p class="muted" style="margin:.5rem 0 .75rem">Este paso es <strong>opcional</strong>. Puedes configurar la PWA ahora o <strong>omitirla</strong> y finalizar; siempre podrás ajustarla más adelante. Si activas la PWA, el alcance (scope) y la URL de inicio se toman de la ruta donde está instalado el sistema.</p>
        </div>
        <form method="post" enctype="multipart/form-data" class="card" id="installPwaForm" data-pwa-has-profile-logo="<?php echo $hasProfileLogo ? '1' : '0'; ?>" data-pwa-pack-rules="<?php echo $pwaRulesJsonAttr; ?>">
            <input type="hidden" name="csrf" value="<?php echo $esc(AntaraCsrf::token()); ?>">
            <input type="hidden" name="install_action" value="finish">
            <input type="hidden" name="pwa_start_url" value="<?php echo $esc($pStart); ?>">
            <input type="hidden" name="pwa_scope" value="<?php echo $esc($pScope); ?>">
            <input type="hidden" name="pwa_icon192_rel" value="<?php echo $esc($h192); ?>">
            <input type="hidden" name="pwa_icon512_rel" value="<?php echo $esc($h512); ?>">
            <input type="hidden" name="pwa_maskable_rel" value="<?php echo $esc($hMask); ?>">
            <input type="hidden" name="pwa_apple_rel" value="<?php echo $esc($hApple); ?>">
            <input type="hidden" name="pwa_splash_rel" value="<?php echo $esc($hSplash); ?>">
            <input type="hidden" name="pwa_crop_json" id="pwa_crop_json" value="<?php echo $esc($pCropJson); ?>">

            <p class="muted" style="margin:0 0 .5rem;font-size:.82rem">Ruta pública detectada — <strong>inicio</strong>: <code><?php echo $esc($pStart); ?></code> · <strong>scope</strong>: <code><?php echo $esc($pScope); ?></code></p>

            <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;margin-top:.5rem">
                <input type="checkbox" name="pwa_enabled" value="1" id="pwa_enabled" <?php echo $pEn ? 'checked' : ''; ?>>
                Activar PWA (manifest e iconos en el navegador)
            </label>

            <label for="pwa_name">Nombre de la aplicación</label>
            <input type="text" id="pwa_name" name="pwa_name" maxlength="120" value="<?php echo $esc($pName); ?>">

            <label for="pwa_short_name">Nombre corto (barra de tareas / icono)</label>
            <input type="text" id="pwa_short_name" name="pwa_short_name" maxlength="48" value="<?php echo $esc($pShort); ?>">

            <label for="pwa_description">Descripción</label>
            <textarea id="pwa_description" name="pwa_description" maxlength="400" rows="2" placeholder="Texto breve para el sistema al instalar"><?php echo $esc($pDesc); ?></textarea>

            <label for="pwa_lang">Idioma del manifest (ISO)</label>
            <select id="pwa_lang" name="pwa_lang">
                <?php
                if (!isset($installPwaLangOptions[$pLang])) {
                    $installPwaLangOptions[$pLang] = $pLang;
                }
                foreach ($installPwaLangOptions as $langCode => $langLabel) {
                    $sel = $pLang === $langCode ? ' selected' : '';
                    echo '<option value="' . $esc($langCode) . '"' . $sel . '>' . $esc($langLabel) . '</option>';
                }
                ?>
            </select>

            <div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin-top:.5rem">
                <div>
                    <label for="pwa_theme_color">Color de tema</label>
                    <input type="color" id="pwa_theme_color" value="<?php echo $esc($pTheme7); ?>">
                    <input type="hidden" id="pwa_theme_color_hex" name="pwa_theme_color" value="<?php echo $esc($pTheme); ?>">
                </div>
                <div>
                    <label for="pwa_background_color">Color de fondo al arrancar</label>
                    <input type="color" id="pwa_background_color" value="<?php echo $esc($pBg7); ?>">
                    <input type="hidden" id="pwa_background_color_hex" name="pwa_background_color" value="<?php echo $esc($pBg); ?>">
                </div>
            </div>

            <label for="pwa_display">Modo de visualización</label>
            <select id="pwa_display" name="pwa_display">
                <option value="standalone" <?php echo $pDisplay === 'standalone' ? 'selected' : ''; ?>>standalone (recomendado, como app)</option>
                <option value="fullscreen" <?php echo $pDisplay === 'fullscreen' ? 'selected' : ''; ?>>fullscreen</option>
                <option value="minimal-ui" <?php echo $pDisplay === 'minimal-ui' ? 'selected' : ''; ?>>minimal-ui</option>
                <option value="browser" <?php echo $pDisplay === 'browser' ? 'selected' : ''; ?>>browser (solo pestaña)</option>
            </select>

            <label for="pwa_orientation">Orientación preferida</label>
            <select id="pwa_orientation" name="pwa_orientation">
                <option value="any" <?php echo $pOrient === 'any' ? 'selected' : ''; ?>>Cualquiera</option>
                <option value="portrait" <?php echo $pOrient === 'portrait' ? 'selected' : ''; ?>>Vertical</option>
                <option value="landscape" <?php echo $pOrient === 'landscape' ? 'selected' : ''; ?>>Horizontal</option>
                <option value="natural" <?php echo $pOrient === 'natural' ? 'selected' : ''; ?>>Natural del dispositivo</option>
            </select>

            <h2 class="install-h2">Icono e imagen de arranque</h2>
            <?php if ($hasProfileLogo) { ?>
            <fieldset class="pwa-icon-source-fieldset" style="margin-top:.35rem;border:1px solid #334155;border-radius:10px;padding:.75rem 1rem">
                <legend style="color:#94a3b8;font-size:.85rem">Fuente de imagen para iconos PWA</legend>
                <label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;margin:.35rem 0">
                    <input type="radio" name="pwa_icon_mode" value="reuse_profile" id="pwa_icon_mode_reuse" <?php echo $pIconReuseSelected ? 'checked' : ''; ?>>
                    <span>Usar la imagen de perfil ya cargada</span>
                </label>
                <label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;margin:.35rem 0">
                    <input type="radio" name="pwa_icon_mode" value="upload" id="pwa_icon_mode_upload" <?php echo !$pIconReuseSelected ? 'checked' : ''; ?>>
                    <span>Elegir otra imagen (subir archivo)</span>
                </label>
            </fieldset>
            <p class="muted" id="pwa_reuse_notice" style="margin-top:.5rem;font-size:.82rem;display:<?php echo $pIconReuseSelected ? 'block' : 'none'; ?>">Se usará la imagen guardada como perfil; no hace falta subir archivo. El servidor generará los iconos al finalizar.</p>
            <?php } else { ?>
            <input type="hidden" name="pwa_icon_mode" value="upload" id="pwa_icon_mode_hidden">
            <?php } ?>
            <div id="pwaMultislotWrap" class="pwa-multislot-wrap" style="display:<?php echo ($hasProfileLogo && $pIconReuseSelected) ? 'none' : 'block'; ?>">
            <p class="muted" style="margin:0 0 .5rem;font-size:.82rem">Elige <strong>una imagen maestra</strong>: se generan recortes sugeridos para cada variante. Pulsa una miniatura para <strong>recortar</strong> o <strong>subir otra imagen</strong> solo para esa variante (en el mismo cuadro de diálogo).</p>
            <label for="pwa_icon_pack" class="pwa-master-pack-label">Imagen maestra (PNG, JPEG o WebP)</label>
            <div class="logo-box" id="pwaMasterLogoBox">
                <div class="logo-selected-preview" id="pwa_master_selected_wrap" aria-live="polite">
                    <span class="muted logo-selected-preview__caption">Vista previa (imagen maestra)</span>
                    <img src="" alt="" id="pwa_master_selected_img" />
                </div>
                <input type="file" id="pwa_icon_pack" name="pwa_icon_pack" class="install-file-input" accept="image/png,image/jpeg,image/webp">
                <p id="pwa_pack_error" class="pwa-slot-file-error" role="alert" hidden></p>
            </div>
            <div id="pwaMosaicWrap" class="pwa-mosaic-wrap" hidden>
                <p class="pwa-mosaic-hint muted">Pulsa una variante para abrir el recorte. Dentro del diálogo puedes elegir otra imagen solo para esa pieza o volver a la maestra.</p>
                <div class="pwa-mosaic" id="pwaMosaicGrid" role="list"></div>
            </div>
            <?php foreach ($pwaRulesForJs['slots'] as $slotKey => $slotMeta) {
                $slotField = antara_install_pwa_pack_multislot_field($slotKey);
                ?>
            <input type="file" class="pwa-slot-override-input" id="pwa_slot_override_<?php echo $esc($slotKey); ?>" name="<?php echo $esc($slotField); ?>" accept="image/png,image/jpeg,image/webp" hidden data-slot="<?php echo $esc($slotKey); ?>" tabindex="-1" aria-hidden="true">
            <?php } ?>
            </div>

            <p style="margin-top:1rem;display:flex;flex-wrap:wrap;gap:.5rem;align-items:center">
                <a class="btn btn-ghost" href="<?php echo $esc($installWizardBaseUrl . '?step=2'); ?>">← Volver a acceso inicial</a>
                <button type="submit" class="btn" id="pwaFinishBtn">Finalizar instalación</button>
                <button type="submit" class="btn btn-ghost" name="skip_pwa" value="1" formnovalidate>Omitir PWA y finalizar</button>
            </p>
        </form>
        <dialog id="pwaCropDialog" class="pwa-crop-dialog">
            <div class="pwa-crop-inner">
                <h3 class="pwa-crop-title" id="pwaCropTitle">Recorte</h3>
                <div class="pwa-crop-dialog-uploads">
                    <input type="file" id="pwaModalSlotFile" class="pwa-modal-slot-file" accept="image/png,image/jpeg,image/webp" tabindex="-1" aria-hidden="true">
                    <button type="button" class="btn btn-ghost" id="pwaModalPickSlotFile">Otra imagen para esta variante</button>
                    <button type="button" class="btn btn-ghost" id="pwaCropUseMaster" hidden>Volver a imagen maestra</button>
                </div>
                <div class="pwa-crop-stage"><img id="pwaCropImg" src="" alt=""></div>
                <div class="pwa-crop-actions">
                    <button type="button" class="btn btn-ghost" id="pwaCropCancel">Cancelar</button>
                    <button type="button" class="btn" id="pwaCropApply">Aplicar recorte</button>
                </div>
            </div>
        </dialog>
        <script src="<?php echo $esc(antara_vendor_asset_url(ANTARA_VENDOR_CROPPER_JS, $vendorPublicBase)); ?>"></script>
        <script>
        (function() {
            var en = document.getElementById('pwa_enabled');
            var nm = document.getElementById('pwa_name');
            var c1 = document.getElementById('pwa_theme_color');
            var c2 = document.getElementById('pwa_background_color');
            var h1 = document.getElementById('pwa_theme_color_hex');
            var h2 = document.getElementById('pwa_background_color_hex');
            function syncPwaNameRequired() {
                if (!nm || !en) return;
                if (en.checked) nm.setAttribute('required', 'required');
                else nm.removeAttribute('required');
            }
            if (en) {
                en.addEventListener('change', syncPwaNameRequired);
                syncPwaNameRequired();
            }
            function bindColorPicker(picker, hidden) {
                if (!picker || !hidden) return;
                picker.addEventListener('input', function() {
                    hidden.value = picker.value;
                });
                picker.addEventListener('change', function() {
                    hidden.value = picker.value;
                });
                if (hidden.value && /^#[0-9a-f]{3,6}$/i.test(hidden.value)) {
                    var x = hidden.value;
                    if (x.length === 4) {
                        x = '#' + x[1] + x[1] + x[2] + x[2] + x[3] + x[3];
                    }
                    picker.value = x.length === 7 ? x : picker.value;
                }
            }
            bindColorPicker(c1, h1);
            bindColorPicker(c2, h2);
        })();
        (function() {
            var form = document.getElementById('installPwaForm');
            if (!form) return;
            var rulesJson = form.getAttribute('data-pwa-pack-rules');
            if (!rulesJson) return;
            var rules;
            try {
                rules = JSON.parse(rulesJson);
            } catch (e1) {
                return;
            }
            var hidJson = document.getElementById('pwa_crop_json');
            var multislotWrap = document.getElementById('pwaMultislotWrap');
            var masterFileEl = document.getElementById('pwa_icon_pack');
            var masterBox = document.getElementById('pwaMasterLogoBox');
            var masterPrevWrap = document.getElementById('pwa_master_selected_wrap');
            var masterPrevImg = document.getElementById('pwa_master_selected_img');
            var packErrEl = document.getElementById('pwa_pack_error');
            var mosaicWrap = document.getElementById('pwaMosaicWrap');
            var mosaicGrid = document.getElementById('pwaMosaicGrid');
            var dlg = document.getElementById('pwaCropDialog');
            var cropTitle = document.getElementById('pwaCropTitle');
            var cropImg = document.getElementById('pwaCropImg');
            var btnApply = document.getElementById('pwaCropApply');
            var btnCancel = document.getElementById('pwaCropCancel');
            var modalSlotFile = document.getElementById('pwaModalSlotFile');
            var btnModalPick = document.getElementById('pwaModalPickSlotFile');
            var btnUseMaster = document.getElementById('pwaCropUseMaster');
            if (!hidJson || !multislotWrap || !masterFileEl || !dlg || !cropTitle || !cropImg || typeof window.Cropper !== 'function') return;

            var SLOT_ORDER = ['icon192', 'icon512', 'maskable512', 'apple180', 'splash'];
            var masterUrl = '';
            var masterImg = null;
            var slots = {};
            var slotOverrides = {};
            var cropper = null;
            var activeSlot = '';
            var cropDialogSnapshot = null;
            var dialogCloseMode = null;

            function defaultSquareRect(w, h) {
                var side = Math.min(w, h);
                return { x: Math.floor((w - side) / 2), y: Math.floor((h - side) / 2), width: side, height: side };
            }
            function defaultSplashRect16_9(w, h) {
                if (w < 1 || h < 1) return { x: 0, y: 0, width: 1, height: 1 };
                var rw = w;
                var rh = Math.round(rw * 9 / 16);
                if (rh > h) {
                    rh = h;
                    rw = Math.round(rh * 16 / 9);
                    if (rw > w) {
                        rw = w;
                        rh = Math.round(rw * 9 / 16);
                    }
                }
                rw = Math.max(1, rw);
                rh = Math.max(1, rh);
                return { x: Math.floor((w - rw) / 2), y: Math.floor((h - rh) / 2), width: rw, height: rh };
            }
            function defaultRectForSlot(key, nw, nh) {
                return key === 'splash' ? defaultSplashRect16_9(nw, nh) : defaultSquareRect(nw, nh);
            }
            function hasMaster() {
                return !!(masterImg && masterImg.naturalWidth);
            }
            function getEffImg(slot) {
                var o = slotOverrides[slot];
                if (o && o.img && o.img.naturalWidth) return o.img;
                return masterImg;
            }
            function getEffUrl(slot) {
                var o = slotOverrides[slot];
                if (o && o.url) return o.url;
                return masterUrl;
            }
            function rectFromMasterDefaults(slot) {
                if (!hasMaster()) return null;
                return defaultRectForSlot(slot, masterImg.naturalWidth, masterImg.naturalHeight);
            }
            function destroyCropper() {
                if (cropper) {
                    try { cropper.destroy(); } catch (e3) {}
                    cropper = null;
                }
            }
            function syncHidden() {
                hidJson.value = JSON.stringify(slots);
            }
            function clearPackError() {
                if (masterFileEl) {
                    masterFileEl.removeAttribute('aria-invalid');
                    masterFileEl.removeAttribute('aria-describedby');
                }
                if (packErrEl) {
                    packErrEl.textContent = '';
                    packErrEl.setAttribute('hidden', 'hidden');
                }
                if (masterBox) masterBox.classList.remove('is-invalid');
            }
            function setPackError(msg) {
                if (!masterFileEl || !packErrEl) return;
                packErrEl.textContent = msg;
                packErrEl.removeAttribute('hidden');
                masterFileEl.setAttribute('aria-invalid', 'true');
                masterFileEl.setAttribute('aria-describedby', 'pwa_pack_error');
                if (masterBox) masterBox.classList.add('is-invalid');
            }
            function clearOverrideInputEl(slot) {
                var el = document.getElementById('pwa_slot_override_' + slot);
                if (!el) return;
                try {
                    var dt = new DataTransfer();
                    el.files = dt.files;
                } catch (ex) {}
            }
            function setOverrideInputEl(slot, file) {
                var el = document.getElementById('pwa_slot_override_' + slot);
                if (!el || !file) return;
                try {
                    var dt = new DataTransfer();
                    dt.items.add(file);
                    el.files = dt.files;
                } catch (ex) {}
            }
            function clearOverrideSlot(slot) {
                var o = slotOverrides[slot];
                if (o && o.url) {
                    try { URL.revokeObjectURL(o.url); } catch (eR) {}
                }
                delete slotOverrides[slot];
                clearOverrideInputEl(slot);
            }
            function resetAllPackState() {
                if (masterUrl) {
                    try { URL.revokeObjectURL(masterUrl); } catch (eM) {}
                }
                masterUrl = '';
                masterImg = null;
                SLOT_ORDER.forEach(function(k) {
                    clearOverrideSlot(k);
                });
                slots = {};
                hidJson.value = '';
                if (masterPrevWrap) masterPrevWrap.classList.remove('is-on');
                if (masterPrevImg) masterPrevImg.removeAttribute('src');
                if (mosaicWrap) mosaicWrap.hidden = true;
                clearPackError();
                refreshMosaicThumbs();
            }
            function validateFile(f) {
                if (!f) return 'No hay archivo.';
                if (f.size > rules.maxBytes) return 'La imagen supera ' + rules.maxMb + ' MB.';
                var t = (f.type || '').toLowerCase();
                if (rules.accept.indexOf(t) === -1) return 'Usa PNG, JPEG o WebP.';
                return '';
            }
            function mosaicThumbDataUrl(key) {
                var r = slots[key];
                var im = getEffImg(key);
                if (!r || !im || !im.naturalWidth) return '';
                var tw = key === 'splash' ? 120 : 72;
                var th = key === 'splash' ? Math.round(tw * 9 / 16) : 72;
                var c = document.createElement('canvas');
                c.width = tw;
                c.height = th;
                var ctx = c.getContext('2d');
                if (!ctx) return '';
                ctx.drawImage(im, r.x, r.y, r.width, r.height, 0, 0, tw, th);
                return c.toDataURL('image/png');
            }
            function refreshMosaicThumbs() {
                if (!mosaicGrid) return;
                SLOT_ORDER.forEach(function(key) {
                    var btn = mosaicGrid.querySelector('.pwa-mosaic-tile[data-slot="' + key + '"]');
                    if (!btn) return;
                    var im = btn.querySelector('img');
                    if (im) im.src = mosaicThumbDataUrl(key) || '';
                });
            }
            function buildMosaic() {
                if (!mosaicGrid) return;
                mosaicGrid.innerHTML = '';
                SLOT_ORDER.forEach(function(key) {
                    var meta = rules.slots[key];
                    var btn = document.createElement('button');
                    btn.type = 'button';
                    btn.className = 'pwa-mosaic-tile' + (key === 'splash' ? ' pwa-mosaic-tile--splash' : '');
                    btn.setAttribute('data-slot', key);
                    btn.setAttribute('role', 'listitem');
                    btn.setAttribute('aria-label', 'Editar ' + meta.label);
                    var im = document.createElement('img');
                    im.alt = '';
                    var lab = document.createElement('kbd');
                    lab.textContent = meta.label;
                    btn.appendChild(im);
                    btn.appendChild(lab);
                    mosaicGrid.appendChild(btn);
                });
                mosaicGrid.addEventListener('click', function(ev) {
                    var t = ev.target && ev.target.closest ? ev.target.closest('.pwa-mosaic-tile') : null;
                    if (!t) return;
                    var sl = t.getAttribute('data-slot');
                    if (sl) openCrop(sl);
                });
            }
            function updateUseMasterBtn() {
                if (!btnUseMaster || !activeSlot) return;
                btnUseMaster.hidden = !slotOverrides[activeSlot];
            }
            function bootCropperForSlot(slot) {
                var ar = slot === 'splash' ? (16 / 9) : 1;
                destroyCropper();
                var inst = new window.Cropper(cropImg, {
                    aspectRatio: ar,
                    viewMode: 1,
                    autoCropArea: 0.92,
                    responsive: true,
                    ready: function() {
                        if (slots[slot]) {
                            inst.setData({
                                x: slots[slot].x,
                                y: slots[slot].y,
                                width: slots[slot].width,
                                height: slots[slot].height
                            });
                        }
                    }
                });
                cropper = inst;
            }
            function startCropperWhenReady(slot) {
                function go() {
                    bootCropperForSlot(slot);
                    updateUseMasterBtn();
                }
                if (cropImg.complete && cropImg.naturalWidth) {
                    go();
                } else {
                    cropImg.onload = function() {
                        cropImg.onload = null;
                        go();
                    };
                }
            }
            function openCrop(slot) {
                if (!hasMaster() || !slots[slot]) return;
                activeSlot = slot;
                cropDialogSnapshot = {
                    x: slots[slot].x,
                    y: slots[slot].y,
                    width: slots[slot].width,
                    height: slots[slot].height
                };
                dialogCloseMode = 'pending';
                var meta = rules.slots[slot];
                cropTitle.textContent = meta.label + ' — recorte o imagen propia';
                destroyCropper();
                cropImg.removeAttribute('src');
                cropImg.src = getEffUrl(slot);
                if (modalSlotFile) modalSlotFile.value = '';
                updateUseMasterBtn();
                if (typeof dlg.showModal === 'function') dlg.showModal();
                startCropperWhenReady(slot);
            }
            function closeCrop() {
                destroyCropper();
                cropImg.removeAttribute('src');
                if (dlg.open) dlg.close();
            }
            var modeReuse = document.getElementById('pwa_icon_mode_reuse');
            var modeUpload = document.getElementById('pwa_icon_mode_upload');
            var reuseNotice = document.getElementById('pwa_reuse_notice');
            function wantsReuseProfile() {
                return !!(modeReuse && modeReuse.checked);
            }
            function syncPwaIconModeUi() {
                var reuse = wantsReuseProfile();
                if (masterFileEl) masterFileEl.disabled = !!reuse;
                if (multislotWrap) multislotWrap.style.display = reuse ? 'none' : 'block';
                if (reuseNotice) reuseNotice.style.display = reuse ? 'block' : 'none';
                if (reuse) {
                    resetAllPackState();
                    if (masterFileEl) masterFileEl.value = '';
                } else {
                    clearPackError();
                }
            }
            if (modeReuse && modeUpload) {
                modeReuse.addEventListener('change', syncPwaIconModeUi);
                modeUpload.addEventListener('change', syncPwaIconModeUi);
            }
            btnCancel.addEventListener('click', function() {
                dialogCloseMode = 'discard';
                if (dlg.open) dlg.close();
            });
            btnApply.addEventListener('click', function() {
                if (!cropper || !activeSlot) return;
                var d = cropper.getData(true);
                slots[activeSlot] = {
                    x: Math.round(d.x),
                    y: Math.round(d.y),
                    width: Math.round(d.width),
                    height: Math.round(d.height)
                };
                dialogCloseMode = 'commit';
                syncHidden();
                refreshMosaicThumbs();
                closeCrop();
            });
            dlg.addEventListener('cancel', function() {
                dialogCloseMode = 'discard';
            });
            dlg.addEventListener('close', function() {
                if (dialogCloseMode !== 'commit' && activeSlot && cropDialogSnapshot) {
                    slots[activeSlot] = {
                        x: cropDialogSnapshot.x,
                        y: cropDialogSnapshot.y,
                        width: cropDialogSnapshot.width,
                        height: cropDialogSnapshot.height
                    };
                    syncHidden();
                    refreshMosaicThumbs();
                } else if (dialogCloseMode === 'commit' && activeSlot) {
                    syncHidden();
                }
                dialogCloseMode = null;
                cropDialogSnapshot = null;
                activeSlot = '';
                destroyCropper();
                cropImg.removeAttribute('src');
                if (modalSlotFile) modalSlotFile.value = '';
            });
            if (btnModalPick && modalSlotFile) {
                btnModalPick.addEventListener('click', function() {
                    modalSlotFile.click();
                });
            }
            if (modalSlotFile) {
                modalSlotFile.addEventListener('change', async function() {
                    if (!activeSlot) return;
                    var f = modalSlotFile.files && modalSlotFile.files[0];
                    if (!f) return;
                    var err = validateFile(f);
                    if (err) {
                        await installAlert(err);
                        modalSlotFile.value = '';
                        return;
                    }
                    var url = URL.createObjectURL(f);
                    clearOverrideSlot(activeSlot);
                    var im = new Image();
                    slotOverrides[activeSlot] = { url: url, img: im, file: f };
                    im.onload = async function() {
                        im.onload = null;
                        var nw = im.naturalWidth;
                        var nh = im.naturalHeight;
                        if (nw * nh > rules.maxPixels) {
                            await installAlert('La imagen tiene demasiados píxeles para el instalador.');
                            clearOverrideSlot(activeSlot);
                            modalSlotFile.value = '';
                            cropImg.src = getEffUrl(activeSlot);
                            startCropperWhenReady(activeSlot);
                            return;
                        }
                        slots[activeSlot] = defaultRectForSlot(activeSlot, nw, nh);
                        setOverrideInputEl(activeSlot, f);
                        syncHidden();
                        destroyCropper();
                        cropImg.removeAttribute('src');
                        cropImg.src = url;
                        startCropperWhenReady(activeSlot);
                        updateUseMasterBtn();
                        refreshMosaicThumbs();
                    };
                    im.onerror = async function() {
                        await installAlert('No se pudo leer la imagen.');
                        clearOverrideSlot(activeSlot);
                        modalSlotFile.value = '';
                    };
                    im.src = url;
                });
            }
            if (btnUseMaster) {
                btnUseMaster.addEventListener('click', function() {
                    if (!activeSlot || !hasMaster()) return;
                    clearOverrideSlot(activeSlot);
                    if (modalSlotFile) modalSlotFile.value = '';
                    var rd = rectFromMasterDefaults(activeSlot);
                    if (rd) slots[activeSlot] = rd;
                    syncHidden();
                    destroyCropper();
                    cropImg.removeAttribute('src');
                    cropImg.src = masterUrl;
                    startCropperWhenReady(activeSlot);
                    updateUseMasterBtn();
                    refreshMosaicThumbs();
                });
            }
            if (masterFileEl) {
                masterFileEl.addEventListener('change', function() {
                    resetAllPackState();
                    var f = masterFileEl.files && masterFileEl.files[0];
                    if (!f) return;
                    var err = validateFile(f);
                    if (err) {
                        setPackError(err);
                        masterFileEl.value = '';
                        return;
                    }
                    var url = URL.createObjectURL(f);
                    masterUrl = url;
                    var im = new Image();
                    masterImg = im;
                    im.onload = function() {
                        im.onload = null;
                        var nw = im.naturalWidth;
                        var nh = im.naturalHeight;
                        if (nw * nh > rules.maxPixels) {
                            setPackError('La imagen tiene demasiados píxeles para el instalador.');
                            resetAllPackState();
                            masterFileEl.value = '';
                            return;
                        }
                        SLOT_ORDER.forEach(function(k) {
                            slots[k] = defaultRectForSlot(k, nw, nh);
                        });
                        syncHidden();
                        if (masterPrevImg) masterPrevImg.src = masterUrl;
                        if (masterPrevWrap) masterPrevWrap.classList.add('is-on');
                        if (mosaicWrap) mosaicWrap.hidden = false;
                        clearPackError();
                        refreshMosaicThumbs();
                    };
                    im.onerror = function() {
                        setPackError('No se pudo leer la imagen.');
                        resetAllPackState();
                        masterFileEl.value = '';
                    };
                    im.src = url;
                });
            }
            buildMosaic();
            syncPwaIconModeUi();
            form.addEventListener('submit', function() {
                syncHidden();
            });
        })();
        </script>
    <?php } ?>
<?php if ($step >= 1 && $step <= 3) { ?>
<script>window.scrollTo(0, 0);</script>
<?php
    $installPersistFormId = $step === 1 ? 'installCompanyForm' : ($step === 2 ? 'installStep2Form' : 'installPwaForm');
    ?>
<script>
window.INSTALL_WIZARD_PERSIST = <?php echo json_encode([
    'step' => $step,
    'formId' => $installPersistFormId,
    'autosaveUrl' => antara_install_base_url(),
    'csrf' => $csrfToken,
    'draftUpdatedAt' => (string) ($installWizardDraft['updated_at'] ?? ''),
    'clearLocal' => isset($_GET['cleared']) && (string) $_GET['cleared'] === '1',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
</script>
<script src="<?php echo $esc(antara_public_app_base() . 'assets/js/install_wizard_persist.js'); ?>"></script>
<?php } ?>
</body>
</html>
