style: match login page to public website design language

- Dark navy gradient background (matches public site's MarketingBackground feel)
- Glassmorphism card: rounded-[28px] border-white/30 bg-white/92 backdrop-blur-xl
  shadow-[0_28px_60px_-34px_rgba(2,6,23,0.88)] — identical to public site card
- Input style: rounded-xl border-[#cfd4e3] focus:border-[#fd6216] focus:ring-[#ffd8c3]
- Label style: text-xs font-semibold uppercase tracking-[0.11em] text-[#4b546f]
- Field spacing: space-y-3.5 with mb-2 labels — matches public site exactly
- Password show/hide toggle (eye icon) matching public site
- Radial glow accent blobs behind the background
- Left brand panel kept as frosted glass over the dark bg (lg+ only)
- Navy submit button (intentional admin deviation from public orange)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-24 16:01:07 +01:00
parent eefdbc4561
commit 801bf293a0

View file

@ -11,23 +11,22 @@ function pickChallengeId(payload: any): string {
if (direct) return direct;
const nested = String(payload?.data?.challengeId || '').trim();
if (nested) return nested;
const snake = String(payload?.challenge_id || '').trim();
if (snake) return snake;
return '';
return String(payload?.challenge_id || '').trim();
}
function pickMaskedEmail(payload: any, fallback: string): string {
const direct = String(payload?.maskedEmail || '').trim();
if (direct) return direct;
const nested = String(payload?.data?.maskedEmail || '').trim();
if (nested) return nested;
return fallback;
return nested || fallback;
}
/* Matches public website input exactly */
const inputCls =
'h-11 w-full rounded-lg border border-gray-200 bg-gray-50 px-3.5 text-sm text-gray-900 outline-none transition placeholder:text-gray-400 focus:border-[#0a1d37] focus:bg-white focus:ring-2 focus:ring-[#0a1d37]/10';
'h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-4 text-sm text-[#101228] outline-none transition placeholder:text-[#9ba3bc] focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]';
const labelCls = 'mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-500';
const labelCls =
'mb-2 block text-xs font-semibold uppercase tracking-[0.11em] text-[#4b546f]';
export default function LoginPage() {
const navigate = useNavigate();
@ -35,6 +34,7 @@ export default function LoginPage() {
const [resetStep, setResetStep] = createSignal<ResetStep>('request');
const [email, setEmail] = createSignal('');
const [password, setPassword] = createSignal('');
const [showPassword, setShowPassword] = createSignal(false);
const [resetCode, setResetCode] = createSignal('');
const [newPassword, setNewPassword] = createSignal('');
const [confirmPassword, setConfirmPassword] = createSignal('');
@ -44,357 +44,313 @@ export default function LoginPage() {
const [error, setError] = createSignal('');
const [info, setInfo] = createSignal('');
const canSubmitResetRequest = createMemo(
() =>
email().trim().length > 0 &&
newPassword().trim().length > 0 &&
confirmPassword().trim().length > 0,
);
const canSubmitResetVerify = createMemo(
() => challengeId().trim().length > 0 && resetCode().trim().length === 6,
);
const canSubmitLoginCredentials = createMemo(
() => email().trim().length > 0 && password().trim().length > 0,
);
const canSubmitLogin = createMemo(() => email().trim().length > 0 && password().trim().length > 0);
const canSubmitResetRequest = createMemo(() => email().trim().length > 0 && newPassword().trim().length > 0 && confirmPassword().trim().length > 0);
const canSubmitResetVerify = createMemo(() => challengeId().trim().length > 0 && resetCode().trim().length === 6);
const clearMessages = () => { setError(''); setInfo(''); };
const resetPasswordFlow = () => {
setResetStep('request');
setResetCode('');
setChallengeId('');
setMaskedEmail('');
setNewPassword('');
setConfirmPassword('');
};
const switchMode = (nextMode: AuthMode) => {
const switchMode = (next: AuthMode) => {
clearMessages();
setMode(nextMode);
if (nextMode === 'login') resetPasswordFlow();
setMode(next);
if (next === 'login') {
setResetStep('request');
setResetCode(''); setChallengeId(''); setMaskedEmail('');
setNewPassword(''); setConfirmPassword('');
}
};
onMount(() => {
if (hasAdminSession()) navigate('/admin', { replace: true });
});
onMount(() => { if (hasAdminSession()) navigate('/admin', { replace: true }); });
const completeAdminLogin = () => {
setAdminSession();
const params = new URLSearchParams(window.location.search);
const from = params.get('from');
const from = new URLSearchParams(window.location.search).get('from');
navigate(from && from.startsWith('/admin') ? from : '/admin', { replace: true });
};
const directSignIn = async () => {
clearMessages();
if (!canSubmitLoginCredentials()) { setError('Email and password are required.'); return; }
if (!canSubmitLogin()) { setError('Email and password are required.'); return; }
setIsSubmitting(true);
try {
const loginPayload = { email: email().trim().toLowerCase(), password: password(), loginTarget: 'admin' };
const attempts = ['/api/gateway/users/auth/internal/login', '/api/gateway/auth/internal/login'];
let payload: any = {};
let status = 500;
let success = false;
for (const url of attempts) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'x-portal-target': 'admin' },
credentials: 'include',
body: JSON.stringify(loginPayload),
});
status = response.status;
payload = await response.json().catch(() => ({}));
if (response.ok) { success = true; break; }
const body = JSON.stringify({ email: email().trim().toLowerCase(), password: password(), loginTarget: 'admin' });
const headers = { 'Content-Type': 'application/json', Accept: 'application/json', 'x-portal-target': 'admin' };
let payload: any = {}; let status = 500; let success = false;
for (const url of ['/api/gateway/users/auth/internal/login', '/api/gateway/auth/internal/login']) {
const r = await fetch(url, { method: 'POST', headers, credentials: 'include', body });
status = r.status; payload = await r.json().catch(() => ({}));
if (r.ok) { success = true; break; }
}
if (!success) {
const fallback = status === 502 ? 'Auth service unavailable (502). Please retry in 12 minutes.' : 'Sign in failed.';
throw new Error(pickManagementLoginError(payload) || fallback);
}
if (isExternalIdentity(payload)) throw new Error('External users cannot use this portal.');
const accessToken = String(payload?.access_token || payload?.accessToken || '').trim();
if (accessToken) sessionStorage.setItem('nxtgauge_admin_access_token', accessToken);
const token = String(payload?.access_token || payload?.accessToken || '').trim();
if (token) sessionStorage.setItem('nxtgauge_admin_access_token', token);
completeAdminLogin();
} catch (e: any) {
setError(String(e?.message || 'Sign in failed.'));
} finally {
setIsSubmitting(false);
}
} catch (e: any) { setError(String(e?.message || 'Sign in failed.')); }
finally { setIsSubmitting(false); }
};
const requestResetCode = async () => {
clearMessages();
if (!canSubmitResetRequest()) { setError('Email, new password, and confirm password are required.'); return; }
if (!canSubmitResetRequest()) { setError('All fields are required.'); return; }
if (newPassword() !== confirmPassword()) { setError('Passwords do not match.'); return; }
const trimmedEmail = email().trim().toLowerCase();
const resolvedPassword = newPassword().trim();
const requestPayload = { email: trimmedEmail, newPassword: resolvedPassword, new_password: resolvedPassword, password: resolvedPassword };
const attempts = [
{ url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify(requestPayload) },
{ url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify(requestPayload) },
{ url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: requestPayload }) },
{ url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: requestPayload }) },
];
const pw = newPassword().trim();
const reqBody = { email: trimmedEmail, newPassword: pw, new_password: pw, password: pw };
setIsSubmitting(true);
try {
let payload: any = {};
let status = 500;
for (const attempt of attempts) {
const response = await fetch(attempt.url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: attempt.body });
status = response.status;
payload = await response.json().catch(() => ({}));
if (response.ok) break;
let payload: any = {}; let status = 500;
for (const { url, body } of [
{ url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify(reqBody) },
{ url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify(reqBody) },
{ url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: reqBody }) },
{ url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: reqBody }) },
]) {
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body });
status = r.status; payload = await r.json().catch(() => ({}));
if (r.ok) break;
}
const nextChallengeId = pickChallengeId(payload);
if (!nextChallengeId) {
const fallback = status === 502 ? 'Verification service unavailable (502). Please retry in 12 minutes.' : 'Failed to send reset code.';
const cId = pickChallengeId(payload);
if (!cId) {
const fallback = status === 502 ? 'Service unavailable (502). Please retry.' : 'Failed to send reset code.';
throw new Error(String(payload?.message || payload?.error || fallback).trim());
}
setChallengeId(nextChallengeId);
setChallengeId(cId);
setMaskedEmail(pickMaskedEmail(payload, trimmedEmail));
setResetStep('verify');
const debugCode = String(payload?.debugCode || payload?.data?.debugCode || '').trim();
setInfo(debugCode ? `Reset code sent. [DEV: ${debugCode}]` : 'Reset code sent to your email.');
} catch (e: any) {
setError(String(e?.message || 'Failed to send reset code.'));
} finally {
setIsSubmitting(false);
}
const dev = String(payload?.debugCode || payload?.data?.debugCode || '').trim();
setInfo(dev ? `Code sent. [DEV: ${dev}]` : 'Reset code sent to your email.');
} catch (e: any) { setError(String(e?.message || 'Failed to send reset code.')); }
finally { setIsSubmitting(false); }
};
const verifyResetCode = async () => {
clearMessages();
if (!canSubmitResetVerify()) { setError('A valid 6-digit code is required.'); return; }
if (newPassword().trim() !== confirmPassword().trim()) { setError('Passwords do not match.'); return; }
const resolvedPassword = newPassword().trim();
const resolvedChallengeId = challengeId().trim();
const resolvedCode = resetCode().trim();
const verifyPayload = {
challengeId: resolvedChallengeId, challenge_id: resolvedChallengeId,
code: resolvedCode, otp: resolvedCode, verificationCode: resolvedCode, verification_code: resolvedCode,
newPassword: resolvedPassword, new_password: resolvedPassword, password: resolvedPassword,
};
const attempts = ['/api/gateway/users/auth/internal/forgot-password/verify-code', '/api/gateway/auth/internal/forgot-password/verify-code'];
const pw = newPassword().trim();
const cId = challengeId().trim();
const code = resetCode().trim();
const body = JSON.stringify({ challengeId: cId, challenge_id: cId, code, otp: code, verificationCode: code, verification_code: code, newPassword: pw, new_password: pw, password: pw });
setIsSubmitting(true);
try {
let payload: any = {};
let status = 500;
let success = false;
for (const url of attempts) {
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(verifyPayload) });
status = response.status;
payload = await response.json().catch(() => ({}));
if (response.ok) { success = true; break; }
let payload: any = {}; let status = 500; let success = false;
for (const url of ['/api/gateway/users/auth/internal/forgot-password/verify-code', '/api/gateway/auth/internal/forgot-password/verify-code']) {
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body });
status = r.status; payload = await r.json().catch(() => ({}));
if (r.ok) { success = true; break; }
}
if (!success) {
const fallback = status === 502 ? 'Verification service unavailable (502). Please retry in 12 minutes.' : 'Password reset failed.';
const fallback = status === 502 ? 'Service unavailable (502). Please retry.' : 'Password reset failed.';
throw new Error(String(payload?.message || payload?.error || fallback).trim());
}
setPassword('');
switchMode('login');
setPassword(''); switchMode('login');
setInfo('Password reset successful. Please sign in with your new password.');
} catch (e: any) {
setError(String(e?.message || 'Password reset failed.'));
} finally {
setIsSubmitting(false);
}
} catch (e: any) { setError(String(e?.message || 'Password reset failed.')); }
finally { setIsSubmitting(false); }
};
return (
<div class="flex min-h-screen w-full overflow-hidden">
{/* ── Left brand panel ── */}
<div class="relative hidden flex-col justify-between overflow-hidden bg-[#0a1d37] p-10 lg:flex lg:w-[44%] xl:w-[42%]">
{/* Subtle radial glow */}
<div class="pointer-events-none absolute -top-32 -left-32 h-96 w-96 rounded-full bg-[#fd6216]/10 blur-3xl" />
<div class="pointer-events-none absolute bottom-0 right-0 h-72 w-72 rounded-full bg-[#fd6216]/5 blur-2xl" />
{/* Logo */}
<div class="relative z-10">
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-10 w-auto brightness-0 invert" />
</div>
{/* Headline */}
<div class="relative z-10 space-y-6">
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-[11px] font-bold uppercase tracking-widest text-orange-300">
<span class="h-1.5 w-1.5 rounded-full bg-orange-400" />
Internal Admin Portal
</span>
<h1 class="text-4xl font-extrabold leading-tight text-white xl:text-[44px]">
Manage everything<br />from one place.
</h1>
<p class="text-[15px] leading-relaxed text-slate-400">
Roles, approvals, users, dashboards all controlled from a single secure control panel.
</p>
{/* Feature list */}
<ul class="space-y-3 text-sm text-slate-300">
{(['Role & permission management', 'Approval workflow control', 'User & company oversight', 'Dashboard configuration'] as const).map((item) => (
<li class="flex items-center gap-3">
<span class="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-[#fd6216]/20 text-[#fd6216]">
<svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4l2.5 2.5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
{item}
</li>
))}
</ul>
</div>
{/* Bottom tag */}
<div class="relative z-10 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-[13px] text-slate-400">
🔒 Secured with internal access policies. Authorised personnel only.
</div>
<main
class="relative min-h-screen overflow-x-clip"
style="background: linear-gradient(135deg, #0a1d37 0%, #0d2347 40%, #0a1d37 70%, #081628 100%)"
>
{/* Radial glow accents matching public site MarketingBackground feel */}
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="absolute -top-40 -left-40 h-[500px] w-[500px] rounded-full bg-[#fd6216]/8 blur-[100px]" />
<div class="absolute top-1/2 right-0 h-[400px] w-[400px] -translate-y-1/2 rounded-full bg-[#fd6216]/5 blur-[80px]" />
<div class="absolute bottom-0 left-1/3 h-[300px] w-[300px] rounded-full bg-white/3 blur-[60px]" />
</div>
{/* ── Right form panel ── */}
<div class="flex flex-1 items-center justify-center bg-[#f5f6fa] px-4 py-12 sm:px-8">
<div class="w-full max-w-[420px]">
<div class="relative z-10 mx-auto grid min-h-screen w-full max-w-[1260px] items-center gap-6 px-4 py-8 sm:px-6 lg:grid-cols-[1.02fr_0.98fr] lg:py-10">
{/* Mobile logo */}
<div class="mb-8 flex justify-center lg:hidden">
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-9 w-auto" />
{/* ── Left brand panel (hidden on mobile) ── */}
<section class="relative hidden min-h-[620px] overflow-hidden rounded-[28px] border border-white/15 bg-white/5 p-10 text-white backdrop-blur-sm lg:flex lg:flex-col lg:justify-between">
{/* Inner glow */}
<div class="pointer-events-none absolute -top-24 -right-24 h-64 w-64 rounded-full bg-[#fd6216]/12 blur-3xl" />
{/* Logo */}
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-10 w-auto brightness-0 invert" />
{/* Main copy */}
<div class="space-y-5">
<p class="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-[11px] font-bold uppercase tracking-widest text-orange-300">
<span class="h-1.5 w-1.5 rounded-full bg-orange-400" />
Internal Admin Portal
</p>
<h1 class="text-[40px] font-extrabold leading-tight text-white xl:text-[46px]">
Welcome back<br />to Nxtgauge.
</h1>
<p class="text-[15px] leading-relaxed text-white/60">
Sign in to manage operations, roles, and approval workflows from one secure control panel.
</p>
<ul class="space-y-3 text-sm text-white/70">
{(['Role & permission management', 'Approval workflow control', 'User & company oversight', 'Dashboard configuration'] as const).map(item => (
<li class="flex items-center gap-3">
<span class="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-[#fd6216]/20 text-[#fd6216]">
<svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4l2.5 2.5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
{item}
</li>
))}
</ul>
</div>
{/* Card */}
<div class="rounded-2xl border border-gray-200 bg-white p-8 shadow-sm">
{/* Bottom tag */}
<p class="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-[13px] text-white/50">
🔒 Secured with internal access policies. Authorised personnel only.
</p>
</section>
<div class="mb-7">
<h2 class="text-2xl font-bold text-gray-900">
{mode() === 'login' ? 'Sign in' : 'Reset password'}
</h2>
<p class="mt-1 text-sm text-gray-500">
{mode() === 'login' ? 'Internal team access only.' : 'Enter your email and set a new password.'}
</p>
{/* ── Right form card — matches public website card exactly ── */}
<section class="rounded-[28px] border border-white/30 bg-white/92 p-5 text-[#101228] shadow-[0_28px_60px_-34px_rgba(2,6,23,0.88)] backdrop-blur-xl sm:p-6">
{/* Logo (always visible) */}
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-8 w-auto" />
<div class="mt-4">
<h2 class="text-3xl font-extrabold text-[#101228]">
{mode() === 'login' ? 'Sign In' : 'Reset Password'}
</h2>
<p class="mt-1.5 text-sm text-[#535e7a]">
{mode() === 'login' ? 'Internal team access only.' : 'Use your internal email to reset access.'}
</p>
</div>
<div class="mt-5 space-y-3.5">
{/* Email */}
<div>
<label class={labelCls}>Email</label>
<input
type="email"
value={email()}
onInput={(e) => { setEmail(e.currentTarget.value); clearMessages(); }}
placeholder="Enter your email"
class={inputCls}
autocomplete="email"
/>
</div>
<div class="space-y-4">
{/* Email */}
{/* Login mode */}
<Show when={mode() === 'login'}>
<div>
<label class={labelCls}>Email</label>
<input
type="email"
value={email()}
onInput={(e) => { setEmail(e.currentTarget.value); clearMessages(); }}
placeholder="you@nxtgauge.com"
class={inputCls}
autocomplete="email"
/>
</div>
{/* Login: Password */}
<Show when={mode() === 'login'}>
<div>
<div class="mb-1.5 flex items-center justify-between">
<label class={labelCls} style="margin-bottom:0">Password</label>
<button type="button" class="text-xs font-semibold text-[#fd6216] hover:underline" onClick={() => switchMode('reset')}>
Forgot password?
</button>
</div>
<div class="mb-2 flex items-center justify-between">
<label class={labelCls} style="margin-bottom:0">Password</label>
<button type="button" class="text-xs font-semibold text-[#fd6216] underline" onClick={() => switchMode('reset')}>
Forgot?
</button>
</div>
<div class="relative">
<input
type="password"
type={showPassword() ? 'text' : 'password'}
value={password()}
onInput={(e) => { setPassword(e.currentTarget.value); clearMessages(); }}
placeholder="••••••••"
class={inputCls}
placeholder="Enter your password"
class={`${inputCls} pr-11`}
autocomplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(v => !v)}
class="absolute inset-y-0 right-0 flex items-center px-3 text-[#5b6480] transition hover:text-[#1b2440]"
>
<Show when={showPassword()} fallback={
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z" /><circle cx="12" cy="12" r="3" />
</svg>
}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 3l18 18M10.58 10.58a2 2 0 002.83 2.83" />
<path stroke-linecap="round" stroke-linejoin="round" d="M9.88 5.09A10.96 10.96 0 0112 4.91c5.52 0 10 4.09 10 7.09 0 1.2-.72 2.53-1.95 3.72M6.1 6.1C3.54 7.58 2 9.79 2 12c0 3 4.48 7.09 10 7.09 1.72 0 3.36-.4 4.84-1.12" />
</svg>
</Show>
</button>
</div>
</Show>
</div>
</Show>
{/* Reset: New + Confirm + Code */}
<Show when={mode() === 'reset'}>
{/* Reset mode */}
<Show when={mode() === 'reset'}>
<div>
<label class={labelCls}>New Password</label>
<input type="password" value={newPassword()} onInput={(e) => { setNewPassword(e.currentTarget.value); clearMessages(); }} placeholder="Minimum 8 characters" class={inputCls} />
</div>
<div>
<label class={labelCls}>Confirm Password</label>
<input type="password" value={confirmPassword()} onInput={(e) => { setConfirmPassword(e.currentTarget.value); clearMessages(); }} placeholder="Repeat new password" class={inputCls} />
</div>
<Show when={resetStep() === 'verify'}>
<div>
<label class={labelCls}>New password</label>
<label class={labelCls}>Verification Code</label>
<input
type="password"
value={newPassword()}
onInput={(e) => { setNewPassword(e.currentTarget.value); clearMessages(); }}
placeholder="Minimum 8 characters"
class={inputCls}
type="text" inputMode="numeric" maxLength={6}
value={resetCode()}
onInput={(e) => { setResetCode(e.currentTarget.value.replace(/\D/g, '').slice(0, 6)); clearMessages(); }}
placeholder="000000"
class={`${inputCls} text-center text-lg tracking-[0.3em]`}
/>
<p class="mt-1.5 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs text-emerald-800">
Code sent to <span class="font-semibold">{maskedEmail() || email()}</span>
</p>
</div>
<div>
<label class={labelCls}>Confirm password</label>
<input
type="password"
value={confirmPassword()}
onInput={(e) => { setConfirmPassword(e.currentTarget.value); clearMessages(); }}
placeholder="Repeat new password"
class={inputCls}
/>
</div>
<Show when={resetStep() === 'verify'}>
<div>
<label class={labelCls}>Verification code</label>
<input
type="text"
inputMode="numeric"
maxLength={6}
value={resetCode()}
onInput={(e) => { setResetCode(e.currentTarget.value.replace(/\D/g, '').slice(0, 6)); clearMessages(); }}
placeholder="000000"
class={`${inputCls} text-center text-lg tracking-[0.3em]`}
/>
<p class="mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs text-emerald-700">
Code sent to <span class="font-semibold">{maskedEmail() || email()}</span>
</p>
</div>
</Show>
</Show>
</Show>
{/* Error / Info */}
<Show when={error()}>
<p class="rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-600">{error()}</p>
</Show>
<Show when={info()}>
<p class="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2.5 text-sm text-emerald-700">{info()}</p>
</Show>
{/* Error / Info */}
<Show when={error()}>
<p class="text-sm font-medium text-red-600">{error()}</p>
</Show>
<Show when={info()}>
<p class="rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{info()}</p>
</Show>
{/* Submit button */}
<Show when={mode() === 'login'}>
{/* Sign In button */}
<Show when={mode() === 'login'}>
<button
type="button"
class="h-11 w-full rounded-xl bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-60"
disabled={isSubmitting()}
onClick={directSignIn}
>
{isSubmitting() ? 'Signing in…' : 'Sign In'}
</button>
<p class="text-xs text-[#6a7390]">Secure login with internal access policies.</p>
</Show>
{/* Reset buttons */}
<Show when={mode() === 'reset'}>
<Show when={resetStep() === 'request'} fallback={
<button
type="button"
class="mt-1 h-11 w-full rounded-lg bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitting()}
onClick={directSignIn}
class="h-11 w-full rounded-xl bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-60"
disabled={isSubmitting() || !canSubmitResetVerify()}
onClick={verifyResetCode}
>
{isSubmitting() ? 'Signing in…' : 'Sign in'}
{isSubmitting() ? 'Resetting…' : 'Verify & Reset Password'}
</button>
}>
<button
type="button"
class="h-11 w-full rounded-xl bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-60"
disabled={isSubmitting() || !canSubmitResetRequest()}
onClick={requestResetCode}
>
{isSubmitting() ? 'Sending Code…' : 'Send Reset Code'}
</button>
</Show>
<button type="button" class="w-full text-center text-sm font-semibold text-[#fd6216] underline" onClick={() => switchMode('login')}>
Back to sign in
</button>
</Show>
<Show when={mode() === 'reset'}>
<Show when={resetStep() === 'request'} fallback={
<button
type="button"
class="mt-1 h-11 w-full rounded-lg bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitting() || !canSubmitResetVerify()}
onClick={verifyResetCode}
>
{isSubmitting() ? 'Resetting…' : 'Verify & reset password'}
</button>
}>
<button
type="button"
class="mt-1 h-11 w-full rounded-lg bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitting() || !canSubmitResetRequest()}
onClick={requestResetCode}
>
{isSubmitting() ? 'Sending code…' : 'Send reset code'}
</button>
</Show>
<button type="button" class="w-full text-center text-sm font-medium text-gray-500 hover:text-gray-700" onClick={() => switchMode('login')}>
Back to sign in
</button>
</Show>
</div>
</div>
</section>
<p class="mt-6 text-center text-xs text-gray-400">
© {new Date().getFullYear()} Nxtgauge. Internal use only.
</p>
</div>
</div>
</div>
</main>
);
}