Ctrl K
ComunidadGuías…

templates/login.html

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<!DOCTYPE html>
<html lang="{{ html_lang }}" dir="{{ html_dir }}">
<head>
    <meta name="tm-base-path" content="{{ base_path }}">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% if reset_mode %}{{ _('Set a New Password - Traefik Manager') }}{% elif setup_mode %}{{ _('Set Up Traefik Manager') }}{% elif force_change_mode %}{{ _('Change Password - Traefik Manager') }}{% else %}{{ _('Sign In - Traefik Manager') }}{% endif %}</title>
    <link rel="icon" type="image/png" sizes="192x192" href="{{ base_path }}/static/icons/icon.png">
    <link rel="preload" href="{{ base_path }}/static/vendor/phosphor/Phosphor-Bold.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="preload" href="{{ base_path }}/static/vendor/phosphor/Phosphor-Fill.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="preload" href="{{ base_path }}/static/vendor/phosphor/Phosphor-Light.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="stylesheet" href="{{ base_path }}/static/vendor/fonts/inter.css">
    <link rel="stylesheet" href="{{ base_path }}/static/vendor/fonts/jetbrains-mono.css">
    <link rel="stylesheet" href="{{ base_path }}/static/vendor/phosphor.css">
    <link rel="stylesheet" href="{{ base_path }}/static/css/tailwind.css?v={{ asset_version }}">
    <link rel="stylesheet" href="{{ base_path }}/static/css/app.css?v={{ asset_version }}">
    <script type="application/json" id="i18n-catalog">{{ i18n_catalog | tojson }}</script>
    <script src="{{ base_path }}/static/js/i18n.js?v={{ asset_version }}"></script>
    <script>
        (function() {
            var t = '{{ default_theme }}' || localStorage.getItem('tm-theme') || 'dark';
            var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
            var isDark = t === 'dark' || (t === 'system' && prefersDark);
            document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
            document.documentElement.setAttribute('data-tm-pref', t);
        })();
    </script>
    <style>
        body {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 24px;
        }
        .card-login  { max-width: 380px; width: 100%; padding: 36px 32px; }
        .card-login:hover, .wz-shell:hover { border-color: var(--border); box-shadow: none; }

        .wz-page { width: 100%; max-width: 1080px; display: flex; flex-direction: column; }
        .wz-shell {
            width: 100%; padding: 0; overflow: hidden;
            display: flex; flex-direction: column;
            height: min(760px, calc(100vh - 96px));
        }
        .wz-head {
            display: flex; align-items: center; justify-content: space-between;
            padding: 14px 20px; border-bottom: 1px solid var(--border); flex-shrink: 0;
        }
        .wz-progress { font: 600 10.5px 'JetBrains Mono', monospace; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }

        .wz-nav { width: 200px; padding: 12px 10px; overflow-y: auto; }
        .wz-nav-label {
            font: 600 9.5px 'JetBrains Mono', monospace; letter-spacing: .12em; text-transform: uppercase;
            color: var(--muted); opacity: .75; padding: 0 10px; margin: 14px 0 6px;
        }
        .wz-nav-label:first-child { margin-top: 2px; }
        .wz-nav-btn { gap: 10px; }
        .wz-nav-btn .wiz-step {
            width: 20px; height: 20px; padding: 0; flex-shrink: 0;
            display: inline-flex; align-items: center; justify-content: center;
            font-size: 10px; border-radius: 6px;
        }

        .wz-body { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
        .wz-error { margin: 14px 20px 0; }
        .wz-lede { margin-bottom: 20px; }
        .wz-title { font-size: 19px; font-weight: 700; letter-spacing: -0.015em; color: var(--text); margin-bottom: 6px; }
        .wz-sub   { font-size: 13px; line-height: 1.6; color: var(--muted); max-width: 76ch; }
        .wz-opt {
            font: 600 9px 'JetBrains Mono', monospace; letter-spacing: .1em; text-transform: uppercase;
            color: var(--muted); border: 1px solid var(--border); border-radius: 4px;
            padding: 1px 5px; margin-left: 6px; vertical-align: 1px;
        }
        .wz-note { font: 400 11px/1.55 'JetBrains Mono', monospace; color: var(--muted); margin: 8px 2px 24px; max-width: 84ch; }
        .wz-body .sc-root .sc-set { align-items: start; padding-top: 10px; padding-bottom: 10px; }
        .wz-body .sc-root .sc-set-d { white-space: normal; overflow: visible; max-width: 68ch; }
        .wz-body .sc-root .sc-sec-head { margin-top: 4px; }

        .wz-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 24px; }
        .wz-card { border: 1px solid var(--border); border-radius: 10px; background: var(--card); padding: 13px 14px; }
        .wz-card > i { color: var(--blue); font-size: 16px; display: block; margin-bottom: 8px; }
        .wz-card-n { font: 600 12px/1.35 'JetBrains Mono', monospace; letter-spacing: -.01em; color: var(--text); margin-bottom: 3px; }
        .wz-card-d { font: 400 10.5px/1.55 'JetBrains Mono', monospace; color: var(--muted); }

        .wz-foot {
            display: flex; align-items: center; gap: 8px; flex-shrink: 0;
            padding: 12px 20px; border-top: 1px solid var(--border); background: var(--card);
        }
        .wz-skip-note { font: 400 10.5px 'JetBrains Mono', monospace; color: var(--muted); }

        @media (max-width: 860px) {
            body { padding: 0; }
            .wz-shell { height: 100vh; border-radius: 0; border-left: none; border-right: none; }
            .wz-nav { width: 60px; padding: 12px 6px; }
            .wz-nav-btn { justify-content: center; font-size: 0; gap: 0; }
            .wz-nav-label { display: none; }
            .wz-cards { grid-template-columns: 1fr; }
        }

        .logo { display: flex; align-items: center; gap: 10px; margin-bottom: 28px; }
        .logo-icon { font-size: 28px; color: var(--blue); }
        .logo-text { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; }
        .logo-text span { color: var(--blue); }


        .step-title    { font-size: 15px; font-weight: 700; margin-bottom: 4px; }
        .step-subtitle { font-size: 13px; color: var(--muted); margin-bottom: 20px; line-height: 1.5; }

        .field { margin-bottom: 14px; }
        .field-hint { font-size: 11px; color: var(--muted); margin-top: 4px; line-height: 1.4; }

        .btn-row { display: flex; gap: 8px; margin-top: 20px; }
        .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; filter: none; }
        .btn-inline {
            background: none; border: 1px solid var(--border);
            border-radius: 6px; padding: 6px 10px;
            font-size: 11px; font-weight: 600; color: var(--muted);
            cursor: pointer; font-family: 'Inter', sans-serif;
            transition: all 0.15s; white-space: nowrap;
            display: inline-flex; align-items: center; gap: 4px;
        }
        .btn-inline:hover { border-color: var(--blue); color: var(--blue); }

        .banner {
            border-radius: 8px; padding: 10px 12px; font-size: 12px;
            margin-bottom: 14px;
            display: flex; align-items: flex-start; gap: 8px; line-height: 1.4;
        }
        .banner-error   { background: rgba(248,81,73,0.1); border: 1px solid rgba(248,81,73,0.3); color: var(--red); }
        .banner-success { background: rgba(63,185,80,0.1); border: 1px solid rgba(63,185,80,0.3); color: var(--green); }
        .banner-info    { background: rgba(36,161,222,0.08); border: 1px solid rgba(36,161,222,0.2); color: var(--blue); }

        .api-test-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
        #apiTestResult { font-size: 11px; color: var(--muted); }

        .tab-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
        .tab-toggle-label { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text); }
        .tab-toggle-label i { color: var(--muted); font-size: 14px; }

        .seg { display: flex; gap: 6px; }
        .seg button {
            flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px;
            padding: 8px 10px; font-size: 12px; font-weight: 500;
            border: 1px solid var(--border); border-radius: 8px;
            background: var(--input-bg); color: var(--muted); cursor: pointer;
            transition: border-color .15s, color .15s, background .15s;
        }
        .seg button:hover { border-color: var(--muted); }
        .seg button.on { border-color: var(--blue); color: var(--blue); background: rgba(36,161,222,0.08); }

        .topbar { display: flex; justify-content: flex-end; margin-bottom: 16px; }
        .theme-toggle {
            background: none; border: 1px solid var(--border);
            border-radius: 8px; padding: 6px 10px;
            color: var(--muted); cursor: pointer; font-size: 14px;
            transition: border-color 0.15s, color 0.15s;
        }
        .theme-toggle:hover { border-color: var(--blue); color: var(--blue); }
        .tm-theme-icon { display: none; }
        html[data-tm-pref="dark"] .tm-theme-icon[data-pref="dark"],
        html[data-tm-pref="light"] .tm-theme-icon[data-pref="light"],
        html[data-tm-pref="system"] .tm-theme-icon[data-pref="system"],
        html:not([data-tm-pref]) .tm-theme-icon[data-pref="dark"] { display: inline; }

        .footer { margin-top: 20px; font-size: 12px; color: var(--muted); text-align: center; }
        .footer a { color: var(--muted); text-decoration: none; }
        .footer a:hover { color: var(--text); }

        @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

        details.reset-hint { margin-top: 16px; }
        details.reset-hint summary {
            font-size: 12px; color: var(--muted); cursor: pointer;
            list-style: none; display: flex; align-items: center; gap: 5px;
            user-select: none;
        }
        details.reset-hint summary::-webkit-details-marker { display: none; }
        details.reset-hint summary:hover { color: var(--text); }
        details.reset-hint[open] summary { color: var(--text); }
        .reset-hint-body {
            margin-top: 10px; padding: 12px;
            background: rgba(36,161,222,0.05); border: 1px solid rgba(36,161,222,0.15);
            border-radius: 8px; font-size: 12px; color: var(--muted); line-height: 1.6;
        }
        .reset-hint-body code {
            display: block; margin-top: 8px; padding: 8px 10px;
            background: var(--bg); border: 1px solid var(--border);
            border-radius: 6px; font-family: 'JetBrains Mono', monospace;
            font-size: 11px; color: var(--blue); word-break: break-all;
        }
    
        label.flex {
            text-transform: none;
            letter-spacing: normal;
            font-weight: 500;
            font-size: 13px;
            margin-bottom: 0;
        }
</style>
</head>
<body>
{% import 'sections/lang_menu.html' as langmenu with context %}
{% if not setup_mode or reset_mode %}{{ langmenu.corner(html_lang) }}{% endif %}

{% if force_change_mode %}

<div style="width:100%;max-width:380px">
    <div class="card card-login">
        <div class="logo">
            <img src="{{ base_path }}/static/icons/icon-48x48.png" alt="{{ pgettext('label', 'logo') }}" class="logo-icon" style="width:28px;height:28px">
            <span class="logo-text">Traefik <span>Manager</span></span>
        </div>
        <div class="step-title">{{ _('Set your permanent password') }}</div>
        <p class="step-subtitle">{{ _("You're using a temporary password. Set a permanent one to continue.") }}</p>
        {% if error %}
        <div class="banner banner-error">
            <i class="ph-bold ph-warning-circle" style="flex-shrink:0;margin-top:1px"></i>
            {{ error }}
        </div>
        {% endif %}
        <form method="POST" action="{{ url_for('force_change_password') }}">
            <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
            <div class="field">
                <label for="new_password">{{ _('New password') }}</label>
                <input type="password" id="new_password" name="new_password"
                       placeholder="{{ _('At least 8 characters') }}" autofocus autocomplete="new-password"
                       oninput="checkForcePwMatch()" class="input-field">
            </div>
            <div class="field">
                <label for="confirm_password">{{ _('Confirm password') }}</label>
                <input type="password" id="confirm_password" name="confirm_password"
                       placeholder="{{ _('Repeat your password') }}" autocomplete="new-password"
                       oninput="checkForcePwMatch()" class="input-field">
                <p class="field-hint" id="forcePwHint" style="display:none"></p>
            </div>
            <div class="btn-row">
                <button type="submit" id="forceSubmitBtn" class="btn-primary" disabled>
                    <i class="ph-bold ph-check"></i> {{ _('Set password') }}
                </button>
            </div>
        </form>
    </div>
    <div class="footer">
        <a href="https://github.com/traefik/traefik" target="_blank">Traefik</a>
        {{ pgettext('label', '·') }}
        <a href="https://github.com/chr0nzz/traefik-manager" target="_blank">Traefik Manager</a>
    </div>
</div>

{% elif otp_mode %}

<div style="width:100%;max-width:380px">
    <div class="card card-login">
        <div class="logo">
            <img src="{{ base_path }}/static/icons/icon-48x48.png" alt="{{ pgettext('label', 'logo') }}" class="logo-icon" style="width:28px;height:28px">
            <span class="logo-text">Traefik <span>Manager</span></span>
        </div>
        <div class="step-title">{{ _('Two-factor authentication') }}</div>
        <p class="step-subtitle">{{ _('Enter the 6-digit code from your authenticator app.') }}</p>
        {% if error %}
        <div class="banner banner-error">
            <i class="ph-bold ph-warning-circle" style="flex-shrink:0;margin-top:1px"></i>
            {{ error }}
        </div>
        {% endif %}
        <form method="POST" action="{{ url_for('login_otp') }}">
            <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
            <div class="field">
                <label for="code">{{ _('Authenticator code') }}</label>
                <input type="text" id="code" name="code"
                       placeholder="000000" autofocus autocomplete="one-time-code"
                       inputmode="numeric" maxlength="6"
                       style="font-family:'JetBrains Mono',monospace;letter-spacing:.2em;font-size:20px;text-align:center" class="input-field">
            </div>
            <div class="btn-row">
                <button type="submit" class="btn-primary">
                    <i class="ph-bold ph-check"></i> {{ pgettext('button', 'Verify') }}
                </button>
            </div>
        </form>
        <details class="reset-hint">
            <summary><i class="ph-bold ph-lock-key" style="font-size:11px"></i> {{ _('Reset 2FA') }}</summary>
            <div class="reset-hint-body">
                {{ _('Run this command to disable 2FA: %(docker_exec_it)s %(podman_exec_it)s %(flask_reset_password)s', docker_exec_it=tag('code', 'docker exec -it traefik-manager flask reset-password --disable-otp'), podman_exec_it=tag('code', 'podman exec -it traefik-manager flask reset-password --disable-otp'), flask_reset_password=tag('code', 'flask reset-password --disable-otp')) }}
            </div>
        </details>
    </div>
    <div class="footer">
        <a href="{{ url_for('login') }}">{{ _('← Back to sign in') }}</a>
    </div>
</div>

{% elif not setup_mode %}

<div style="width:100%;max-width:380px">
    <div class="card card-login">
        <div class="logo">
            <img src="{{ base_path }}/static/icons/icon-48x48.png" alt="{{ pgettext('label', 'logo') }}" class="logo-icon" style="width:28px;height:28px">
            <span class="logo-text">Traefik <span>Manager</span></span>
        </div>
        <div class="step-title">{{ _('Sign in') }}</div>
        {% for category, message in get_flashed_messages(with_categories=true) %}
        <div class="banner banner-{{ 'error' if category == 'error' else 'info' }}">
            <i class="ph-bold {{ 'ph-warning-circle' if category == 'error' else 'ph-info' }}" style="flex-shrink:0;margin-top:1px"></i>
            {{ message }}
        </div>
        {% endfor %}
        {% set show_local = local_auth_enabled | default(true) %}
        <p class="step-subtitle">{% if show_local %}{{ _('Enter your password to access Traefik Manager.') }}{% else %}{{ _('Sign in with your identity provider.') }}{% endif %}</p>
        {% if temp_password_hint and show_local %}
        <div class="banner banner-info">
            <i class="ph-bold ph-info" style="flex-shrink:0;margin-top:1px"></i>
            {{ _('A temporary password was generated at startup - check your container logs.') }}
        </div>
        {% endif %}
        {% if error %}
        <div class="banner banner-error">
            <i class="ph-bold ph-warning-circle" style="flex-shrink:0;margin-top:1px"></i>
            {{ error }}
        </div>
        {% endif %}
        {% if show_local %}
        <form method="POST" action="{{ url_for('login') }}">
            <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
            <input type="hidden" name="next" value="{{ next }}">
            <div class="field">
                <label for="password">{{ pgettext('setting', 'Password') }}</label>
                <input type="password" id="password" name="password"
                       placeholder="{{ _('Your password') }}" autofocus autocomplete="current-password" class="input-field">
            </div>
            <label class="flex items-center gap-2 text-sm mb-3" style="color:var(--muted);cursor:pointer">
                <input type="checkbox" name="remember" value="on" checked style="accent-color:var(--blue)">
                {{ _('Remember me for 7 days') }}
            </label>
            <div class="btn-row">
                <button type="submit" class="btn-primary">
                    <i class="ph-bold ph-sign-in"></i> {{ _('Sign in') }}
                </button>
            </div>
        </form>
        {% endif %}
        {% if oidc_enabled %}
        {% if show_local %}
        <div style="display:flex;align-items:center;gap:8px;margin:16px 0 12px;">
            <div style="flex:1;height:1px;background:var(--border)"></div>
            <span style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">{{ pgettext('label', 'or') }}</span>
            <div style="flex:1;height:1px;background:var(--border)"></div>
        </div>
        {% endif %}
        <a href="{{ url_for('oidc_login') }}" class="btn-secondary" style="display:flex;align-items:center;justify-content:center;gap:8px;text-decoration:none;border-radius:8px;padding:10px 0;font-size:14px;font-weight:600;{% if not show_local %}margin-top:8px;{% endif %}">
            <i class="ph-bold ph-key"></i> {{ _('Sign in with %(oidc_display_name)s', oidc_display_name=oidc_display_name) }}
        </a>
        {% endif %}
        {% if show_local %}
        <details class="reset-hint">
            <summary><i class="ph-bold ph-lock-key" style="font-size:11px"></i> {{ _('Reset password') }}</summary>
            <div class="reset-hint-body">
                {{ _('Run this command and enter a new password when asked: %(docker_exec_it)s %(podman_exec_it)s %(flask_reset_password)s The password is not printed back. Log in with it straight away.', docker_exec_it=tag('code', 'docker exec -it traefik-manager flask reset-password --prompt'), podman_exec_it=tag('code', 'podman exec -it traefik-manager flask reset-password --prompt'), flask_reset_password=tag('code', 'flask reset-password --prompt')) }}
            </div>
        </details>
        {% endif %}
    </div>
    <div class="footer">
        <a href="https://github.com/traefik/traefik" target="_blank">Traefik</a>
        {{ pgettext('label', '·') }}
        <a href="https://github.com/chr0nzz/traefik-manager" target="_blank">Traefik Manager</a>
    </div>
</div>

{% else %}

{% if reset_mode %}
<div style="width:100%;max-width:380px">
    <div class="card card-login">
        <div class="logo">
            <img src="{{ base_path }}/static/icons/icon-48x48.png" alt="{{ pgettext('label', 'logo') }}" class="logo-icon" style="width:28px;height:28px">
            <span class="logo-text">Traefik <span>Manager</span></span>
        </div>
        <div class="step-title">{{ _('Set a new password') }}</div>
        <p class="step-subtitle">{{ _('Your password was reset. Choose a new one to get back in - nothing else about your configuration has changed.') }}</p>

        {% if error %}
        <div class="banner banner-error">
            <i class="ph-bold ph-warning-circle" style="flex-shrink:0;margin-top:1px"></i>
            <span>{{ error }}</span>
        </div>
        {% endif %}

        <form method="POST" action="{{ url_for('setup') }}">
            <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
            <div class="field">
                <label for="password">{{ _('New password') }}</label>
                <input type="password" id="password" name="password" class="input-field"
                       placeholder="{{ _('At least 8 characters') }}" autocomplete="new-password" required autofocus>
            </div>
            <div class="field">
                <label for="confirm">{{ _('Confirm password') }}</label>
                <input type="password" id="confirm" name="confirm" class="input-field"
                       placeholder="{{ _('Repeat your password') }}" autocomplete="new-password" required>
            </div>
            {% if otp_required %}
            <div class="field">
                <label for="code">{{ _('Authenticator code') }}</label>
                <input type="text" id="code" name="code" class="input-field"
                       placeholder="000000" autocomplete="one-time-code" inputmode="numeric" maxlength="6" required
                       style="font-family:'JetBrains Mono',monospace;letter-spacing:.2em;font-size:20px;text-align:center">
            </div>
            {% endif %}
            <button type="submit" class="btn-primary" style="width:100%;margin-top:4px">
                <i class="ph-bold ph-check"></i> {{ _('Set password') }}
            </button>
        </form>
    </div>
    <div class="footer">
        <a href="https://github.com/traefik/traefik" target="_blank">Traefik</a>
        {{ pgettext('label', '·') }}
        <a href="https://github.com/chr0nzz/traefik-manager" target="_blank">Traefik Manager</a>
    </div>
</div>
{% else %}
<div class="wz-page">
    <div class="card wz-shell">

        <div class="wz-head">
            <div class="logo" style="margin:0">
                <img src="{{ base_path }}/static/icons/icon-48x48.png" alt="{{ pgettext('label', 'logo') }}" class="logo-icon" style="width:26px;height:26px">
                <span class="logo-text">Traefik <span>Manager</span></span>
            </div>
            <div class="flex items-center gap-3">
                <span class="wz-progress" id="wzProgress"></span>
                <button class="theme-toggle" onclick="cycleTheme()" title="{{ _('Toggle theme') }}">
                    <i class="ph-bold ph-moon tm-theme-icon" data-pref="dark"></i>
                    <i class="ph-bold ph-sun tm-theme-icon" data-pref="light"></i>
                    <i class="ph-bold ph-desktop tm-theme-icon" data-pref="system"></i>
                </button>
            </div>
        </div>

        <div class="modal-inner">

            <div class="modal-sidebar wz-nav">
                <div class="wz-nav-label">{{ pgettext('label', 'Start') }}</div>
                <button type="button" class="modal-sidebar-btn wz-nav-btn active" id="wzb-0" onclick="goTo(0)">
                    {{ _('%(n1)s Welcome', n1=tag('span', '1', class_='wiz-step', id_='wzn-0')) }}
                </button>

                <div class="wz-nav-label">{{ pgettext('label', 'Connect') }}</div>
                <button type="button" class="modal-sidebar-btn wz-nav-btn" id="wzb-1" onclick="goTo(1)">
                    <span class="wiz-step" id="wzn-1">2</span> Traefik
                </button>
                <button type="button" class="modal-sidebar-btn wz-nav-btn" id="wzb-2" onclick="goTo(2)">
                    {{ _('%(n3)s Self route', n3=tag('span', '3', class_='wiz-step', id_='wzn-2')) }}
                </button>

                <div class="wz-nav-label">{{ pgettext('label', 'Optional') }}</div>
                <button type="button" class="modal-sidebar-btn wz-nav-btn" id="wzb-3" onclick="goTo(3)">
                    {{ _('%(n4)s Monitoring', n4=tag('span', '4', class_='wiz-step', id_='wzn-3')) }}
                </button>
                <button type="button" class="modal-sidebar-btn wz-nav-btn" id="wzb-4" onclick="goTo(4)">
                    <span class="wiz-step" id="wzn-4">5</span> CrowdSec
                </button>
                <button type="button" class="modal-sidebar-btn wz-nav-btn" id="wzb-5" onclick="goTo(5)">
                    {{ _('%(n6)s Git backup', n6=tag('span', '6', class_='wiz-step', id_='wzn-5')) }}
                </button>
                <button type="button" class="modal-sidebar-btn wz-nav-btn" id="wzb-6" onclick="goTo(6)">
                    {{ _('%(n7)s Notifications', n7=tag('span', '7', class_='wiz-step', id_='wzn-6')) }}
                </button>

                {% if not temp_password_mode %}
                <div class="wz-nav-label">{{ pgettext('label', 'Finish') }}</div>
                <button type="button" class="modal-sidebar-btn wz-nav-btn" id="wzb-7" onclick="goTo(7)">
                    {{ _('%(n8)s Password', n8=tag('span', '8', class_='wiz-step', id_='wzn-7')) }}
                </button>
                {% endif %}
            </div>

            <div class="wz-body">

                <div id="globalError" class="banner banner-error wz-error" style="display:{{ 'flex' if error else 'none' }}">
                    <i class="ph-bold ph-warning-circle" style="flex-shrink:0;margin-top:1px"></i>
                    <span id="globalErrorMsg">{{ error or '' }}</span>
                </div>

                <div class="modal-panel sc-root active" id="panel-0">
                    <div class="wz-lede">
                        <h1 class="wz-title">{{ _('Welcome to Traefik Manager') }}</h1>
                        <p class="wz-sub">{{ _('A web interface for the routes, middlewares and services Traefik already serves. This setup takes about a minute: point it at your Traefik API, pick the views you want, and set a password. Everything here can be changed later in Settings.') }}</p>
                    </div>

                    <div class="wz-lang">
                        <div class="sc-sec-head"><i class="ph-bold ph-translate sc-sec-icon" style="color:var(--blue)"></i><span class="sc-sec-label">{{ _('Language') }}</span><span class="sc-sec-rule"></span></div>
                        {{ langmenu.field(url_language) }}
                    </div>

                    <div class="wz-cards">
                        <div class="wz-card">
                            <i class="ph-bold ph-layout"></i>
                            <div class="wz-card-n">{{ _('Routes, middlewares, services') }}</div>
                            <div class="wz-card-d">{{ _('Create, edit and disable them through a form or raw YAML, written straight to your dynamic config file.') }}</div>
                        </div>
                        <div class="wz-card">
                            <i class="ph-bold ph-activity"></i>
                            <div class="wz-card-n">{{ _('Live from the Traefik API') }}</div>
                            <div class="wz-card-d">{{ _('Router, service and middleware health read from Traefik itself, not from a cached copy.') }}</div>
                        </div>
                        <div class="wz-card">
                            <i class="ph-bold ph-shield-check"></i>
                            <div class="wz-card-n">{{ _('Certificates and access logs') }}</div>
                            <div class="wz-card-d">{{ _('Expiry dates from your ACME store, and the access log read as analytics rather than a wall of text.') }}</div>
                        </div>
                        <div class="wz-card">
                            <i class="ph-bold ph-lock-key"></i>
                            <div class="wz-card-n">{{ _('Yours alone') }}</div>
                            <div class="wz-card-d">{{ _('Nothing leaves the container. No account, no telemetry, no outbound calls you did not configure.') }}</div>
                        </div>
                    </div>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-palette sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ pgettext('heading', 'Appearance') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Default theme') }}</div>
                        <div class="sc-set-d">{{ _('What Traefik Manager looks like for anyone who has not chosen for themselves. Each person can still override it from the toolbar.') }}</div>
                        <div class="seg">
                            <button type="button" id="th-dark"   onclick="setSetupTheme('dark')"><i class="ph-bold ph-moon"></i> {{ pgettext('button', 'Dark') }}</button>
                            <button type="button" id="th-light"  onclick="setSetupTheme('light')"><i class="ph-bold ph-sun"></i> {{ pgettext('button', 'Light') }}</button>
                            <button type="button" id="th-system" onclick="setSetupTheme('system')"><i class="ph-bold ph-desktop"></i> {{ pgettext('button', 'System') }}</button>
                        </div>
                    </div>
                </div>

                <div class="modal-panel sc-root" id="panel-1">
                    <div class="wz-lede">
                        <h1 class="wz-title">{{ _('Connect to Traefik') }}</h1>
                        <p class="wz-sub">{{ _('Traefik Manager reads everything through the Traefik API. This is the one step it cannot work without.') }}</p>
                    </div>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-plugs-connected sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">Traefik API</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">API URL</div>
                        <div class="sc-set-d">{{ _('Where this container reaches the Traefik API. On the same Docker network that is usually %(http_traefik_8080)s - the service name, not %(localhost)s. Requires %(api)s in your static config.', http_traefik_8080=tag('code', 'http://traefik:8080', class_='font-mono'), localhost=tag('code', 'localhost', class_='font-mono'), api=tag('code', 'api: {}', class_='font-mono')) }}</div>
                        <input type="text" id="s_apiurl" placeholder="http://traefik:8080"
                               value="{{ defaults.traefik_api_url }}" class="input-field">
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('API credentials %(optional)s', optional=tag('span', _('optional'), class_='wz-opt')) }}</div>
                        <div class="sc-set-d">{{ _('Only if your Traefik dashboard sits behind basic auth. Leave both blank when the API is reachable without credentials.') }}</div>
                        <div class="flex gap-2">
                            <input type="text" id="s_apiuser" placeholder="{{ pgettext('placeholder', 'Username') }}" autocomplete="off" class="input-field" style="flex:1">
                            <input type="password" id="s_apipassword" placeholder="{{ pgettext('placeholder', 'Password') }}" autocomplete="off" class="input-field" style="flex:1">
                        </div>
                        <div class="api-test-row">
                            <button type="button" class="btn-inline" onclick="testApi()">
                                <i class="ph-bold ph-plugs-connected"></i> {{ _('Test connection') }}
                            </button>
                            <span id="apiTestResult"></span>
                        </div>
                    </div>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-globe sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ _('Routing defaults') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ pgettext('setting', 'Domains') }}</div>
                        <div class="sc-set-d">{{ _("A convenience list for the route form's host dropdown. It does not change your Traefik config, your TLS, or your routing - domains found in existing routes are picked up automatically, so this is just seeding.") }}</div>
                        <div class="sc-set-d">{{ _('Separate domains with commas, spaces or new lines.') }}</div>
                        <input type="text" id="s_domains" placeholder="example.com, example.net"
                               value="{{ defaults.domains | join(', ') }}" class="input-field">
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Certificate resolver') }}</div>
                        <div class="sc-set-d">{{ _('Resolver names from your %(traefik_yml)s, comma-separated. The first becomes the per-route default. If you manage certificates outside ACME, enter %(none)s.', traefik_yml=tag('code', 'traefik.yml', class_='font-mono'), none=tag('code', 'none', class_='font-mono')) }}</div>
                        <input type="text" id="s_resolver" placeholder="letsencrypt, cloudflare"
                               value="{{ defaults.cert_resolver }}" class="input-field">
                    </div>
                </div>

                <div class="modal-panel sc-root" id="panel-2">
                    <div class="wz-lede">
                        <h1 class="wz-title">{{ _('Route to Traefik Manager itself') }}</h1>
                        <p class="wz-sub">{{ _('Give this interface its own hostname through Traefik, so you can reach it over TLS instead of a published port. Writes a %(traefik_manager_self)s into your config directory. Skip it if you already route to this container yourself.', traefik_manager_self=tag('code', 'traefik-manager-self.yml', class_='font-mono')) }}</p>
                    </div>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-signpost sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ _('Self route') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ pgettext('setting', 'Hostname') }}</div>
                        <div class="sc-set-d">{% if detected_self_domain %}{{ _('Detected from your existing configuration. Clear it to skip this step.') }}{% else %}{{ _('The hostname you want to open Traefik Manager on. Leave blank to skip this step entirely.') }}{% endif %}</div>
                        <input type="text" id="s_self_domain" placeholder="manager.example.com" value="{{ detected_self_domain or '' }}" class="input-field">
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Internal service URL') }}</div>
                        <div class="sc-set-d">{{ _('How Traefik reaches this container from inside the network. Normally the compose service name and port.') }}</div>
                        <input type="text" id="s_self_service" placeholder="http://traefik-manager:5000" value="{{ detected_self_svc or 'http://traefik-manager:5000' }}" class="input-field">
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Entry point') }}</div>
                        <div class="sc-set-d">{{ _('The Traefik entry point to attach the router to, as named in your static config - commonly %(websecure)s or %(https)s.', websecure=tag('code', 'websecure', class_='font-mono'), https=tag('code', 'https', class_='font-mono')) }}</div>
                        <input type="text" id="s_self_entrypoint" placeholder="websecure" value="{{ detected_self_entry_point or 'websecure' }}" class="input-field">
                    </div>
                </div>

                <div class="modal-panel sc-root" id="panel-3">
                    <div class="wz-lede">
                        <h1 class="wz-title">{{ _('Choose your views') }}</h1>
                        <p class="wz-sub">{{ _('Each of these adds a tab to the navigation. Turn on what you use - an unused tab is just noise. All of them can be toggled later under Settings, so nothing here is a commitment.') }}</p>
                    </div>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-chart-donut sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ pgettext('heading', 'Visualizations') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="tab-grid">
                        <div class="tab-toggle-row" onclick="toggleSetupTab('dashboard')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-squares-four"></i> {{ pgettext('label', 'Dashboard') }}</span>
                            <div class="toggle-switch" id="sw-dashboard"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('routemap')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-graph"></i> {{ _('Route Map') }}</span>
                            <div class="toggle-switch" id="sw-routemap"><div class="toggle-knob"></div></div>
                        </div>
                    </div>
                    <p class="wz-note">{{ _('An overview of every router grouped by entry point, and a graph of how requests reach each service.') }}</p>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-path sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ _('Provider tabs') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <p class="wz-note" id="wzDetectedTabs" style="display:none">{{ _('Switched on for every provider Traefik is reporting right now. Turn any of them off if you would rather not see it.') }}</p>
                    <div class="tab-grid">
                        <div class="tab-toggle-row" onclick="toggleSetupTab('docker')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-cube"></i> Docker</span>
                            <div class="toggle-switch" id="sw-docker"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('kubernetes')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-circles-three"></i> Kubernetes</span>
                            <div class="toggle-switch" id="sw-kubernetes"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('swarm')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-graph"></i> Swarm</span>
                            <div class="toggle-switch" id="sw-swarm"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('nomad')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-circles-four"></i> Nomad</span>
                            <div class="toggle-switch" id="sw-nomad"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('ecs')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-cloud"></i> ECS</span>
                            <div class="toggle-switch" id="sw-ecs"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('consulcatalog')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-address-book"></i> Consul Catalog</span>
                            <div class="toggle-switch" id="sw-consulcatalog"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('redis')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-database"></i> Redis</span>
                            <div class="toggle-switch" id="sw-redis"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('etcd')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-database"></i> etcd</span>
                            <div class="toggle-switch" id="sw-etcd"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('consul')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-database"></i> Consul KV</span>
                            <div class="toggle-switch" id="sw-consul"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('zookeeper')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-database"></i> ZooKeeper</span>
                            <div class="toggle-switch" id="sw-zookeeper"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('http_provider')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-link"></i> {{ _('HTTP Provider') }}</span>
                            <div class="toggle-switch" id="sw-http_provider"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('file_external')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-file-text"></i> {{ _('File (external)') }}</span>
                            <div class="toggle-switch" id="sw-file_external"><div class="toggle-knob"></div></div>
                        </div>
                    </div>
                    <p class="wz-note">{{ _('Read-only views of routes Traefik discovers from other providers. Turn on the ones you actually run - these routes live in Docker labels or a KV store, so Traefik Manager shows them but cannot edit them.') }}</p>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-heartbeat sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ pgettext('heading', 'System') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="tab-grid">
                        <div class="tab-toggle-row" onclick="toggleSetupTab('certs')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-shield-check"></i> {{ pgettext('label', 'Certificates') }}</span>
                            <div class="toggle-switch" id="sw-certs"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('plugins')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-puzzle-piece"></i> {{ pgettext('label', 'Plugins') }}</span>
                            <div class="toggle-switch" id="sw-plugins"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupTab('logs')">
                            <span class="tab-toggle-label"><i class="ph-bold ph-terminal"></i> {{ pgettext('label', 'Logs') }}</span>
                            <div class="toggle-switch" id="sw-logs"><div class="toggle-knob"></div></div>
                        </div>
                        <div class="tab-toggle-row" onclick="toggleSetupGeoip()">
                            <span class="tab-toggle-label"><i class="ph-bold ph-flag"></i> {{ _('GeoIP lookup') }}</span>
                            <div class="toggle-switch" id="sw-geoip"><div class="toggle-knob"></div></div>
                        </div>
                    </div>
                    <p class="wz-note">{{ _('Certificates, Plugins and Logs each read a file that has to be mounted into this container. If it is not mounted yet, the view tells you the exact volume line to add. GeoIP resolves client addresses to countries in Logs and CrowdSec, downloading a free database on first use.') }}</p>
                </div>

                <div class="modal-panel sc-root" id="panel-4">
                    <div class="wz-lede">
                        <h1 class="wz-title">CrowdSec</h1>
                        <p class="wz-sub">{{ _('If you run CrowdSec, Traefik Manager can read its local API and show who is probing this host, from which networks, with which scenarios, and what is already blocked. Leave the URL blank to skip.') }}</p>
                    </div>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-shield-checkered sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ _('Local API') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">LAPI URL</div>
                        <div class="sc-set-d">{{ _('Where the CrowdSec local API listens. On the same Docker network that is usually %(http_crowdsec_8080)s.', http_crowdsec_8080=tag('code', 'http://crowdsec:8080', class_='font-mono')) }}</div>
                        <input type="url" id="s_cs_url" placeholder="http://crowdsec:8080" class="input-field">
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Bouncer API key') }}</div>
                        <div class="sc-set-d">{{ _('Reads active decisions - the bans in force. Generate one with %(cscli_bouncers_add)s.', cscli_bouncers_add=tag('code', 'cscli bouncers add traefik-manager', class_='font-mono')) }}</div>
                        <input type="password" id="s_cs_key" placeholder="cscli bouncers add traefik-manager" class="input-field">
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Machine login %(optional)s', optional=tag('span', _('optional'), class_='wz-opt')) }}</div>
                        <div class="sc-set-d">{{ _('Reads alerts, which is where scenarios, targeted paths and attacking networks come from. This complements the bouncer key rather than replacing it: with only the bouncer key you see bans but not the attacks behind them.') }}</div>
                        <div class="flex gap-2">
                            <input type="text" id="s_cs_machine_id" placeholder="{{ _('Machine ID') }}" class="input-field" style="flex:1">
                            <input type="password" id="s_cs_machine_pw" placeholder="{{ pgettext('placeholder', 'Password') }}" class="input-field" style="flex:1">
                        </div>
                        <div class="api-test-row">
                            <button type="button" class="btn-inline" onclick="testCrowdSec()">
                                <i class="ph-bold ph-plugs-connected"></i> {{ _('Test connection') }}
                            </button>
                            <span id="csTestResult"></span>
                        </div>
                    </div>
                </div>

                <div class="modal-panel sc-root" id="panel-5">
                    <div class="wz-lede">
                        <h1 class="wz-title">{{ _('Git backup') }}</h1>
                        <p class="wz-sub">{{ _('Commit every configuration change to a git repository, so you get history, diffs and a one-click restore to any earlier commit. Local file backups happen regardless - this adds an off-box copy. Leave the URL blank to skip.') }}</p>
                    </div>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-git-branch sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ pgettext('heading', 'Repository') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Repository URL') }}</div>
                        <div class="sc-set-d">{{ _('Any HTTPS git remote: GitHub, GitLab, Gitea, Forgejo, or self-hosted. Use a private repository - your dynamic config is in it.') }}</div>
                        <input type="url" id="s_git_repo" placeholder="https://github.com/you/traefik-configs" class="input-field">
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Branch and username') }}</div>
                        <div class="sc-set-d">{{ _('The branch to commit to, and the account the token belongs to.') }}</div>
                        <div class="flex gap-2">
                            <input type="text" id="s_git_branch" value="main" placeholder="main" class="input-field" style="flex:1">
                            <input type="text" id="s_git_user" placeholder="{{ pgettext('placeholder', 'Username') }}" class="input-field" style="flex:1">
                        </div>
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Access token') }}</div>
                        <div class="sc-set-d">{{ _('A personal access token with write access to that one repository. Stored encrypted, and never shown again after setup.') }}</div>
                        <input type="password" id="s_git_token" placeholder="ghp_..." class="input-field">
                        <div class="api-test-row">
                            <button type="button" class="btn-inline" onclick="testGitRepo()">
                                <i class="ph-bold ph-git-branch"></i> {{ _('Test repository') }}
                            </button>
                            <span id="gitTestResult"></span>
                        </div>
                    </div>
                    <div class="sc-panel">
                        <div class="sc-set">
                            <div class="sc-set-l">
                                <div class="sc-set-n">{{ _('Push automatically') }}</div>
                                <div class="sc-set-d">{{ _('Commit and push on every configuration change. With this off, you push by hand from the Backups pane.') }}</div>
                            </div>
                            <div class="sc-set-v">
                                <div class="toggle-switch on" id="sw-gitauto" onclick="toggleGitAuto()"><div class="toggle-knob"></div></div>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="modal-panel sc-root" id="panel-6">
                    <div class="wz-lede">
                        <h1 class="wz-title">{{ pgettext('heading', 'Notifications') }}</h1>
                        <p class="wz-sub">{{ _('Send a message when a route is saved or deleted, a backup runs, or a restore completes. This sets up one destination that receives every event; add more, and choose which events reach each one, in Settings.') }}</p>
                    </div>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-bell sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ pgettext('heading', 'Destination') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ pgettext('setting', 'Type') }}</div>
                        <div class="sc-set-d">{{ _('Payload format for the target service. Generic sends a plain JSON body you can shape yourself downstream.') }}</div>
                        <select id="s_notify_kind" class="input-field" onchange="onSetupNotifyKind()">
                            <option value="discord">Discord</option>
                            <option value="slack">Slack</option>
                            <option value="ntfy">{{ _('ntfy.sh / self-hosted ntfy') }}</option>
                            <option value="generic">{{ _('Generic JSON') }}</option>
                            <option value="gotify">Gotify</option>
                            <option value="pushover">Pushover</option>
                            <option value="pushbullet">Pushbullet</option>
                            <option value="telegram">Telegram</option>
                        </select>
                    </div>
                    <div class="sc-fld" id="s_notify_fld_url">
                        <div class="sc-set-n" id="s_notify_url_label">{{ _('Webhook URL') }}</div>
                        <div class="sc-set-d" id="s_notify_url_desc">{{ _('Where notifications are delivered.') }}</div>
                        <input type="url" id="s_notify_url" class="input-field">
                    </div>
                    <div class="sc-fld" id="s_notify_fld_token" style="display:none">
                        <div class="sc-set-n" id="s_notify_token_label">{{ _('App Token') }}</div>
                        <div class="sc-set-d" id="s_notify_token_desc">{{ _('Stored encrypted.') }}</div>
                        <input type="password" id="s_notify_token" class="input-field">
                    </div>
                    <div class="sc-fld" id="s_notify_fld_token2" style="display:none">
                        <div class="sc-set-n" id="s_notify_token2_label">{{ _('User Key') }}</div>
                        <div class="sc-set-d" id="s_notify_token2_desc">{{ _('Stored encrypted.') }}</div>
                        <input type="password" id="s_notify_token2" class="input-field">
                        <p class="field-hint" id="s_notify_token2_hint" style="display:none"></p>
                    </div>
                    <div class="sc-fld" id="s_notify_fld_auth" style="display:none">
                        <div class="sc-set-n">{{ _('Basic auth %(optional)s', optional=tag('span', _('optional'), class_='wz-opt')) }}</div>
                        <div class="sc-set-d">{{ _('Credentials for endpoints that require authentication.') }}</div>
                        <div class="flex gap-2">
                            <input type="text" id="s_notify_user" placeholder="{{ pgettext('placeholder', 'Username') }}" class="input-field" style="flex:1">
                            <input type="password" id="s_notify_pw" placeholder="{{ pgettext('placeholder', 'Password') }}" class="input-field" style="flex:1">
                        </div>
                    </div>
                </div>

                {% if not temp_password_mode %}
                <div class="modal-panel sc-root" id="panel-7">
                    <div class="wz-lede">
                        <h1 class="wz-title">{{ _('Set your password') }}</h1>
                        <p class="wz-sub">{{ _('This is the last step. The password protects the whole interface and is stored as a bcrypt hash, never in plain text. You can change it, add two-factor, or switch to an identity provider later in Settings.') }}</p>
                    </div>

                    <div class="sc-sec-head">
                        <i class="ph-bold ph-lock-key sc-sec-icon" style="color:var(--blue)"></i>
                        <span class="sc-sec-label">{{ pgettext('heading', 'Password') }}</span>
                        <span class="sc-sec-rule"></span>
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ pgettext('setting', 'Password') }}</div>
                        <div class="sc-set-d">{{ _('At least 8 characters. A long passphrase beats a short complicated one.') }}</div>
                        <input type="password" id="s_password" placeholder="{{ _('At least 8 characters') }}"
                               autocomplete="new-password" oninput="checkPwMatch()" class="input-field">
                    </div>
                    <div class="sc-fld">
                        <div class="sc-set-n">{{ _('Confirm password') }}</div>
                        <div class="sc-set-d">{{ _('Type it again so a typo does not lock you out.') }}</div>
                        <input type="password" id="s_confirm" placeholder="{{ _('Repeat your password') }}"
                               autocomplete="new-password" oninput="checkPwMatch()" class="input-field">
                        <p class="field-hint" id="pwMatchHint" style="display:none"></p>
                    </div>
                </div>
                {% endif %}

                <div class="wz-foot">
                    <button type="button" class="btn-secondary" id="wzBack" onclick="goTo(currentStep - 1)">
                        <i class="ph-bold ph-arrow-left"></i> {{ pgettext('button', 'Back') }}
                    </button>
                    <div class="flex items-center gap-2" style="margin-left:auto">
                        <span class="wz-skip-note" id="wzSkipNote"></span>
                        <button type="button" class="btn-primary" id="wzNext" onclick="nextStep()">
                            {{ pgettext('button', 'Continue') }} <i class="ph-bold ph-arrow-right"></i>
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <form method="POST" action="{{ url_for('setup') }}" id="setupForm" style="display:none">
        <input type="hidden" name="csrf_token"        value="{{ csrf_token }}">
        <input type="hidden" name="domains"           id="f_domains">
        <input type="hidden" name="cert_resolver"     id="f_resolver">
        <input type="hidden" name="traefik_api_url"      id="f_apiurl">
        <input type="hidden" name="traefik_api_user"     id="f_apiuser">
        <input type="hidden" name="traefik_api_password" id="f_apipassword">
        <input type="hidden" name="visible_tabs"         id="f_visible_tabs">
        <input type="hidden" name="password"          id="f_password">
        <input type="hidden" name="confirm"           id="f_confirm">
        <input type="hidden" name="self_route_domain"      id="f_self_domain">
        <input type="hidden" name="self_route_service"     id="f_self_service">
        <input type="hidden" name="self_route_entry_point" id="f_self_entrypoint">
        <input type="hidden" name="crowdsec_lapi_url"         id="f_cs_url">
        <input type="hidden" name="crowdsec_api_key"          id="f_cs_key">
        <input type="hidden" name="crowdsec_machine_id"       id="f_cs_machine_id">
        <input type="hidden" name="crowdsec_machine_password" id="f_cs_machine_pw">
        <input type="hidden" name="git_backup_repo"      id="f_git_repo">
        <input type="hidden" name="git_backup_branch"    id="f_git_branch">
        <input type="hidden" name="git_backup_username"  id="f_git_user">
        <input type="hidden" name="git_backup_token"     id="f_git_token">
        <input type="hidden" name="git_backup_auto_push" id="f_git_autopush">
        <input type="hidden" name="notify_kind"     id="f_notify_kind">
        <input type="hidden" name="notify_url"      id="f_notify_url">
        <input type="hidden" name="notify_token"    id="f_notify_token">
        <input type="hidden" name="notify_token2"   id="f_notify_token2">
        <input type="hidden" name="notify_username" id="f_notify_user">
        <input type="hidden" name="notify_password" id="f_notify_pw">
        <input type="hidden" name="geoip_enabled" id="f_geoip">
        <input type="hidden" name="default_theme" id="f_default_theme">
        <input type="hidden" name="default_language" value="{{ url_language }}">
    </form>

    <div class="footer">
        <a href="https://github.com/traefik/traefik" target="_blank">Traefik</a>
        {{ pgettext('label', '·') }}
        <a href="https://github.com/chr0nzz/traefik-manager" target="_blank">Traefik Manager</a>
    </div>
</div>
{% endif %}
{% endif %}

<script>

const THEME_KEY = 'tm-theme';
function applyTheme(t) {
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const isDark = t === 'dark' || (t === 'system' && prefersDark);
    document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
    document.documentElement.setAttribute('data-tm-pref', t);
}
function cycleTheme() {
    const cur  = document.documentElement.getAttribute('data-tm-pref') || 'dark';
    const next = cur === 'dark' ? 'light' : cur === 'light' ? 'system' : 'dark';
    localStorage.setItem(THEME_KEY, next);
    applyTheme(next);
}
applyTheme('{{ default_theme }}' || localStorage.getItem(THEME_KEY) || 'dark');

function _passwordError(pw, label) {
    label = label || t('Password');
    if (pw.length < 8) return t('{label} must be at least 8 characters.', { label });
    if (new TextEncoder().encode(pw).length > 72) {
        return t('{label} must be 72 bytes or fewer, which is the bcrypt limit. Accented and non-Latin characters take more than one byte each.', { label });
    }
    return null;
}

{% if setup_mode and not reset_mode %}

const TEMP_PW_MODE = {{ 'true' if temp_password_mode else 'false' }};
let currentStep = 0;
const setupTabs = { dashboard: false, routemap: false, docker: false, certs: false, plugins: false, logs: false };
const DETECTED_TABS = {{ detected_tabs | tojson }};
DETECTED_TABS.forEach(t => { setupTabs[t] = true; });

function _paintSetupTabs() {
    Object.keys(setupTabs).forEach(t => {
        const el = document.getElementById('sw-' + t);
        if (el) el.classList.toggle('on', !!setupTabs[t]);
    });
    const note = document.getElementById('wzDetectedTabs');
    if (note && DETECTED_TABS.length) note.style.display = '';
}
const MAX_STEP   = TEMP_PW_MODE ? 6 : 7;

const STEP_NOTES = [
    '',
    '',
    t('Leave the hostname blank to skip'),
    t('Every tab can be changed later'),
    t('Leave the URL blank to skip'),
    t('Leave the URL blank to skip'),
    t('Leave the fields blank to skip'),
    '',
];

function goTo(step) {
    if (step < 0 || step > MAX_STEP) return;
    hideError();
    document.getElementById('panel-' + currentStep).classList.remove('active');
    currentStep = step;
    const panel = document.getElementById('panel-' + step);
    panel.classList.add('active');
    panel.scrollTop = 0;

    for (let i = 0; i <= MAX_STEP; i++) {
        const btn = document.getElementById('wzb-' + i);
        const num = document.getElementById('wzn-' + i);
        if (!btn) continue;
        btn.classList.toggle('active', i === step);
        if (!num) continue;
        num.classList.toggle('on', i === step);
        num.classList.toggle('done', i < step);
        num.innerHTML = i < step
            ? '<i class="ph-bold ph-check" style="font-size:9px"></i>'
            : String(i + 1);
    }

    document.getElementById('wzProgress').textContent =
        t('Step {n} of {total}', { n: step + 1, total: MAX_STEP + 1 });

    const back = document.getElementById('wzBack');
    const next = document.getElementById('wzNext');
    const note = document.getElementById('wzSkipNote');
    back.style.visibility = step === 0 ? 'hidden' : 'visible';
    note.textContent = STEP_NOTES[step] || '';

    if (step === MAX_STEP) {
        next.id = 'wzNext';
        next.innerHTML = `<i class="ph-bold ph-check"></i> ${thc('button', 'Finish setup')}`;
        next.disabled = !TEMP_PW_MODE && !_pwOk();
    } else {
        next.innerHTML = (step === 0 ? thc('button', 'Get started') : thc('button', 'Continue'))
            + ' <i class="ph-bold ph-arrow-right"></i>';
        next.disabled = false;
    }

    const first = panel.querySelector('input:not([type=hidden]), select');
    if (first && step !== 0) setTimeout(() => first.focus(), 80);
}

function nextStep() {
    if (currentStep === 1) { validateStep1(); return; }
    if (currentStep === NOTIFY_STEP) {
        const problem = _notifyProblem();
        if (problem) { showError(problem); return; }
    }
    if (currentStep === MAX_STEP) { submitSetup(); return; }
    goTo(currentStep + 1);
}

function _pwOk() {
    const pw = document.getElementById('s_password');
    const cf = document.getElementById('s_confirm');
    return !!pw && pw.value.length >= 8 && pw.value === cf.value;
}

function showError(msg) {
    const el = document.getElementById('globalError');
    document.getElementById('globalErrorMsg').textContent = msg;
    el.style.display = 'flex';
}
function hideError() {
    document.getElementById('globalError').style.display = 'none';
}

function validateStep1() {
    hideError();
    const domains = document.getElementById('s_domains').value.trim();
    const apiUrl  = document.getElementById('s_apiurl').value.trim();
    if (!domains) { showError(t('Enter at least one domain.')); return; }
    if (!apiUrl)  { showError(t('Enter the Traefik API URL.')); return; }
    if (!apiUrl.startsWith('http://') && !apiUrl.startsWith('https://')) {
        showError(t('Traefik API URL must start with http:// or https://'));
        return;
    }
    goTo(2);
}

async function testApi() {
    const url    = document.getElementById('s_apiurl').value.trim();
    const user   = document.getElementById('s_apiuser').value.trim();
    const pw     = document.getElementById('s_apipassword').value;
    const result = document.getElementById('apiTestResult');
    if (!url) { result.textContent = t('Enter a URL first.'); result.style.color = 'var(--muted)'; return; }
    result.textContent = tc('label', 'Testing…');
    result.style.color = 'var(--muted)';
    try {
        const res = await fetch('{{ url_for("api_setup_test_connection") }}', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ url, user, password: pw })
        });
        const d = await res.json();
        if (d.ok) {
            result.textContent = t('✓ Connected - Traefik v{version}', { version: d.version });
            result.style.color = 'var(--green)';
        } else {
            result.textContent = '✗ ' + (d.error || t('Connection failed'));
            result.style.color = 'var(--red)';
        }
    } catch(e) {
        result.textContent = '✗ ' + t('Connection failed');
        result.style.color = 'var(--red)';
    }
}

function toggleSetupTab(tab) {
    setupTabs[tab] = !setupTabs[tab];
    document.getElementById('sw-' + tab).classList.toggle('on', setupTabs[tab]);
}

let setupGeoip = false;
function toggleSetupGeoip() {
    setupGeoip = !setupGeoip;
    document.getElementById('sw-geoip').classList.toggle('on', setupGeoip);
}

let setupGitAuto = true;
function toggleGitAuto() {
    setupGitAuto = !setupGitAuto;
    document.getElementById('sw-gitauto').classList.toggle('on', setupGitAuto);
}

let setupTheme = localStorage.getItem(THEME_KEY) || 'dark';
function setSetupTheme(pref) {
    setupTheme = pref;
    ['dark', 'light', 'system'].forEach(p =>
        document.getElementById('th-' + p).classList.toggle('on', p === pref));
    localStorage.setItem(THEME_KEY, pref);
    applyTheme(pref);
}
setSetupTheme(setupTheme);

function checkPwMatch() {
    const pw      = document.getElementById('s_password').value;
    const confirm = document.getElementById('s_confirm').value;
    const hint    = document.getElementById('pwMatchHint');
    const btn     = document.getElementById('wzNext');
    if (!pw && !confirm) { hint.style.display = 'none'; btn.disabled = true; return; }
    const pwErr = _passwordError(pw);
    if (pw.length > 0 && pwErr) {
        hint.textContent = pwErr;
        hint.style.color = 'var(--red)'; hint.style.display = 'block';
        btn.disabled = true; return;
    }
    if (confirm && pw !== confirm) {
        hint.textContent = t('Passwords do not match.');
        hint.style.color = 'var(--red)'; hint.style.display = 'block';
        btn.disabled = true; return;
    }
    if (!pwErr && pw === confirm) {
        hint.textContent = t('✓ Passwords match');
        hint.style.color = 'var(--green)'; hint.style.display = 'block';
        btn.disabled = false; return;
    }
    hint.style.display = 'none'; btn.disabled = true;
}

async function testCrowdSec() {
    const out = document.getElementById('csTestResult');
    const url = (document.getElementById('s_cs_url').value || '').trim();
    const key = (document.getElementById('s_cs_key').value || '').trim();
    if (!url) { out.textContent = t('Enter a LAPI URL first'); out.style.color = 'var(--muted)'; return; }
    out.textContent = tc('label', 'Testing…'); out.style.color = 'var(--muted)';
    try {
        const res = await fetch('{{ url_for("setup_test_crowdsec") }}', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' },
            body: JSON.stringify({ url, key }),
        });
        const data = await res.json();
        out.textContent = data.ok ? t('Reachable') : (data.error || t('Failed'));
        out.style.color = data.ok ? 'var(--green)' : 'var(--red)';
    } catch (e) { out.textContent = t('Request failed'); out.style.color = 'var(--red)'; }
}

async function testGitRepo() {
    const out = document.getElementById('gitTestResult');
    const repo = (document.getElementById('s_git_repo').value || '').trim();
    if (!repo) { out.textContent = t('Enter a repository URL first'); out.style.color = 'var(--muted)'; return; }
    out.textContent = tc('label', 'Testing…'); out.style.color = 'var(--muted)';
    try {
        const res = await fetch('{{ url_for("setup_test_git") }}', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' },
            body: JSON.stringify({
                repo_url: repo,
                username: (document.getElementById('s_git_user').value || '').trim(),
                token: (document.getElementById('s_git_token').value || '').trim(),
            }),
        });
        const data = await res.json();
        out.textContent = data.ok ? t('Reachable') : (data.error || t('Failed'));
        out.style.color = data.ok ? 'var(--green)' : 'var(--red)';
    } catch (e) { out.textContent = t('Request failed'); out.style.color = 'var(--red)'; }
}

const NOTIFY_STEP = 6;

const SETUP_NOTIFY_KINDS = {
    discord:    { fields: { url:    { label: t('Webhook URL'),  desc: t('Where notifications are delivered.'), ph: 'https://discord.com/api/webhooks/...' } } },
    slack:      { fields: { url:    { label: t('Webhook URL'),  desc: t('Incoming webhook created in your Slack workspace.'), ph: 'https://hooks.slack.com/services/...' } } },
    ntfy:       { auth: true,
                  fields: { url:    { label: 'URL',          desc: t('Full topic URL on ntfy.sh or your own server.'), ph: 'https://ntfy.sh/my-topic' } } },
    generic:    { auth: true,
                  fields: { url:    { label: 'URL',          desc: t('Receives a JSON body you can shape downstream.'), ph: 'https://example.com/hooks/traefik' } } },
    gotify:     { fields: { url:    { label: t('Server URL'),   desc: t('Base URL of your Gotify server.'), ph: 'https://gotify.example.com' },
                            token:  { label: t('App Token'),    desc: t('Application token from Gotify. Stored encrypted.'), secret: true } } },
    pushover:   { fields: { token:  { label: t('App Token'),    desc: t('Application token from your Pushover app. Stored encrypted.'), secret: true },
                            token2: { label: t('User Key'),     desc: t('Your Pushover user or group key. Stored encrypted.'), secret: true } } },
    pushbullet: { fields: { token:  { label: t('Access Token'), desc: t('Access token from your Pushbullet account. Stored encrypted.'), secret: true } } },
    telegram:   { fields: { token:  { label: t('Bot Token'),    desc: t('Token issued by BotFather. Stored encrypted.'), secret: true },
                            token2: { label: t('Chat ID'),      desc: t('Target chat, group or channel to post into.'), ph: '-1001234567890',
                                      hint: t('Send your bot a message, open {url} and copy chat.id.', { url: 'https://api.telegram.org/bot<token>/getUpdates' }) } } },
};

function onSetupNotifyKind() {
    const spec   = SETUP_NOTIFY_KINDS[document.getElementById('s_notify_kind').value] || {};
    const fields = spec.fields || {};
    ['url', 'token', 'token2'].forEach(key => {
        const meta = fields[key];
        document.getElementById('s_notify_fld_' + key).style.display = meta ? '' : 'none';
        if (!meta) return;
        document.getElementById('s_notify_' + key + '_label').textContent = meta.label;
        document.getElementById('s_notify_' + key + '_desc').textContent  = meta.desc;
        const input = document.getElementById('s_notify_' + key);
        input.placeholder = meta.ph || '';
        input.type = meta.secret ? 'password' : (key === 'url' ? 'url' : 'text');
    });
    document.getElementById('s_notify_fld_auth').style.display = spec.auth ? '' : 'none';
    const hint = document.getElementById('s_notify_token2_hint');
    hint.textContent = (fields.token2 || {}).hint || '';
    hint.style.display = hint.textContent ? 'block' : 'none';
}
onSetupNotifyKind();

function _notifyValues() {
    const kind = document.getElementById('s_notify_kind').value;
    const spec = SETUP_NOTIFY_KINDS[kind] || {};
    const val  = (id) => (document.getElementById(id).value || '').trim();
    const uses = (key) => !!(spec.fields || {})[key];
    return {
        kind:     kind,
        spec:     spec,
        url:      uses('url')    ? val('s_notify_url')    : '',
        token:    uses('token')  ? val('s_notify_token')  : '',
        token2:   uses('token2') ? val('s_notify_token2') : '',
        username: spec.auth ? val('s_notify_user') : '',
        password: spec.auth ? val('s_notify_pw')   : '',
    };
}

function _notifyProblem() {
    const v = _notifyValues();
    if (!['url', 'token', 'token2', 'username', 'password'].some(k => v[k])) return '';
    if (!v.spec.fields) return t('Choose a notification destination.');
    const missing = Object.keys(v.spec.fields).filter(k => !v[k]).map(k => v.spec.fields[k].label);
    if (missing.length === 1) return t('Notifications need {field}, or clear the step to skip it.', { field: missing[0] });
    if (missing.length === 2) return t('Notifications need {first} and {second}, or clear the step to skip it.', { first: missing[0], second: missing[1] });
    return '';
}

function submitSetup() {
    hideError();

    const notifyProblem = _notifyProblem();
    if (notifyProblem) { goTo(NOTIFY_STEP); showError(notifyProblem); return; }

    document.getElementById('f_domains').value      = document.getElementById('s_domains').value.trim();
    document.getElementById('f_resolver').value     = document.getElementById('s_resolver').value.trim();
    document.getElementById('f_apiurl').value       = document.getElementById('s_apiurl').value.trim();
    document.getElementById('f_apiuser').value      = document.getElementById('s_apiuser').value.trim();
    document.getElementById('f_apipassword').value  = document.getElementById('s_apipassword').value;
    document.getElementById('f_visible_tabs').value = JSON.stringify(setupTabs);
    document.getElementById('f_self_domain').value      = (document.getElementById('s_self_domain')?.value      || '').trim();
    document.getElementById('f_self_service').value     = (document.getElementById('s_self_service')?.value     || '').trim();
    document.getElementById('f_self_entrypoint').value  = (document.getElementById('s_self_entrypoint')?.value  || '').trim() || 'websecure';

    const v = (id) => (document.getElementById(id)?.value || '').trim();
    document.getElementById('f_cs_url').value         = v('s_cs_url');
    document.getElementById('f_cs_key').value         = v('s_cs_key');
    document.getElementById('f_cs_machine_id').value  = v('s_cs_machine_id');
    document.getElementById('f_cs_machine_pw').value  = v('s_cs_machine_pw');
    document.getElementById('f_git_repo').value       = v('s_git_repo');
    document.getElementById('f_git_branch').value     = v('s_git_branch') || 'main';
    document.getElementById('f_git_user').value       = v('s_git_user');
    document.getElementById('f_git_token').value      = v('s_git_token');
    document.getElementById('f_git_autopush').value   = setupGitAuto ? 'on' : '';
    const notify = _notifyValues();
    document.getElementById('f_notify_kind').value    = notify.kind;
    document.getElementById('f_notify_url').value     = notify.url;
    document.getElementById('f_notify_token').value   = notify.token;
    document.getElementById('f_notify_token2').value  = notify.token2;
    document.getElementById('f_notify_user').value    = notify.username;
    document.getElementById('f_notify_pw').value      = notify.password;
    document.getElementById('f_geoip').value          = setupGeoip ? 'on' : '';
    document.getElementById('f_default_theme').value  = setupTheme;

    if (!TEMP_PW_MODE) {
        const pw      = document.getElementById('s_password').value;
        const confirm = document.getElementById('s_confirm').value;
        const pwErr = _passwordError(pw);
        if (pwErr)          { showError(pwErr); return; }
        if (pw !== confirm) { showError(t('Passwords do not match.')); return; }
        document.getElementById('f_password').value = pw;
        document.getElementById('f_confirm').value  = confirm;
        const btn = document.getElementById('wzNext');
        btn.disabled  = true;
        btn.innerHTML = `<i class="ph-bold ph-circle-notch" style="animation:spin 0.8s linear infinite"></i> ${thc('label', 'Saving…')}`;
    }

    document.getElementById('setupForm').submit();
}

_paintSetupTabs();

{% endif %}

{% if force_change_mode %}
function checkForcePwMatch() {
    const pw      = document.getElementById('new_password').value;
    const confirm = document.getElementById('confirm_password').value;
    const hint    = document.getElementById('forcePwHint');
    const btn     = document.getElementById('forceSubmitBtn');
    if (!pw && !confirm) { hint.style.display = 'none'; btn.disabled = true; return; }
    const pwErr = _passwordError(pw);
    if (pw.length > 0 && pwErr) {
        hint.textContent = pwErr;
        hint.style.color = 'var(--red)'; hint.style.display = 'block';
        btn.disabled = true; return;
    }
    if (confirm && pw !== confirm) {
        hint.textContent = t('Passwords do not match.');
        hint.style.color = 'var(--red)'; hint.style.display = 'block';
        btn.disabled = true; return;
    }
    if (!pwErr && pw === confirm) {
        hint.textContent = t('✓ Passwords match');
        hint.style.color = 'var(--green)'; hint.style.display = 'block';
        btn.disabled = false; return;
    }
    hint.style.display = 'none'; btn.disabled = true;
}
{% endif %}
</script>
{{ langmenu.script() }}
</body>
</html>