A login form is usually the very first thing a user interacts with when they open your website or app, so it needs to make a strong first impression. If it looks messy, outdated, or confusing, people lose trust instantly. But if it feels clean, smooth, and modern, users feel comfortable and confident right away. That’s exactly why learning to build a good-looking login form is such an important skill for every front-end developer and web design beginner.
In this tutorial, we’ll create a beautiful, modern, and fully responsive login form using only HTML, CSS, and a little bit of JavaScript. No Bootstrap, no external libraries, and no complicated setup — just pure front-end code that you can understand line by line. This makes it perfect for beginners who want to practice real projects, as well as developers who need a ready-made design they can quickly drop into their own work.
We’ll start by building the basic structure of the form with clean, semantic HTML. Then we’ll bring it to life using modern CSS — soft gradients, smooth shadows, rounded corners, proper spacing, and subtle hover effects that make the design feel premium. After that, we’ll add a few interactive touches with JavaScript, like a show/hide password toggle and basic input handling, so the form actually feels alive instead of static.
You’ll also learn some useful design tips along the way, such as how to keep your layout balanced, how to make the form responsive for both mobile and desktop screens, and how to customize the colors and style to match your own brand or project. Everything is explained in a simple, easy-to-follow way, so even if you’re just starting out, you won’t feel lost.
By the end of this tutorial, you’ll have a stunning login form that not only looks professional but is also lightweight, fast, and easy to reuse in any of your future projects. The complete source code is provided below, so you can either follow the full step-by-step guide or grab the code directly and start using it right away.
So let’s open up your code editor and start building this beautiful login form together — only here on Coding Stellix.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aurora — Sign in & Sign up | Coding_Stellix</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Jost:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
/* ═══════════ CSS | Coding_Stellix ═══════════ */
:root{
--bg:#070c18;
--card:rgba(255,255,255,.045);
--card-2:rgba(255,255,255,.07);
--line:rgba(255,255,255,.10);
--txt:#eaf0ff;
--muted:#8b95b5;
--cyan:#34d1e0;
--indigo:#6f7dff;
--lime:#5ce6a8;
--rose:#ff6183;
--amber:#ffc46b;
--slot:38px; /* ← ek hi icon slot size, sab fields me same */
}
*{margin:0;padding:0;box-sizing:border-box;}
body{
min-height:100vh;display:grid;place-items:center;padding:24px;
background:var(--bg);font-family:'Jost',system-ui,sans-serif;color:var(--txt);
position:relative;overflow-x:hidden;
}
/* ── aurora background ── */
.aurora{position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none;}
.aurora i{position:absolute;border-radius:50%;filter:blur(80px);opacity:.5;display:block;}
.aurora i:nth-child(1){width:520px;height:520px;background:var(--indigo);
top:-160px;left:-110px;animation:drift1 18s ease-in-out infinite;}
.aurora i:nth-child(2){width:460px;height:460px;background:var(--cyan);
bottom:-150px;right:-90px;animation:drift2 21s ease-in-out infinite;}
.aurora i:nth-child(3){width:340px;height:340px;background:var(--lime);opacity:.22;
top:45%;left:52%;animation:drift3 26s ease-in-out infinite;}
@keyframes drift1{50%{transform:translate(70px,50px) scale(1.12);}}
@keyframes drift2{50%{transform:translate(-60px,-45px) scale(1.15);}}
@keyframes drift3{50%{transform:translate(-70px,60px) scale(.88);}}
.aurora::after{content:"";position:absolute;inset:0;
background-image:
linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px);
background-size:44px 44px;
mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%);}
/* ═══════════ CARD ═══════════ */
.card{
position:relative;z-index:1;width:100%;max-width:410px;
background:var(--card);
backdrop-filter:blur(26px);-webkit-backdrop-filter:blur(26px);
border:1px solid var(--line);border-radius:24px;
padding:34px 34px 28px;
box-shadow:0 40px 90px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.09);
animation:rise .7s cubic-bezier(.2,.8,.2,1) both;
}
@keyframes rise{from{opacity:0;transform:translateY(22px) scale(.98);}}
.card::before{content:"";position:absolute;top:0;left:26px;right:26px;height:1px;
background:linear-gradient(90deg,transparent,var(--cyan),var(--indigo),transparent);opacity:.7;}
.logo{
width:46px;height:46px;border-radius:14px;display:grid;place-items:center;
background:linear-gradient(135deg,var(--cyan),var(--indigo));
box-shadow:0 12px 30px rgba(111,125,255,.45);margin-bottom:16px;
}
.logo svg{width:21px;height:21px;stroke:#fff;stroke-width:2.2;fill:none;}
h1{font-size:24px;font-weight:900;letter-spacing:-.6px;line-height:1.15;}
h1 em{font-style:normal;background:linear-gradient(120deg,var(--cyan),var(--indigo));
-webkit-background-clip:text;background-clip:text;color:transparent;}
.sub{color:var(--muted);font-size:13px;font-weight:500;margin:6px 0 20px;}
/* ═══════════ TABS (Sign in / Sign up) ═══════════ */
.tabs{
position:relative;display:grid;grid-template-columns:1fr 1fr;gap:4px;
background:rgba(255,255,255,.04);border:1px solid var(--line);
border-radius:13px;padding:4px;margin-bottom:22px;
}
.tabs::before{ /* sliding pill */
content:"";position:absolute;top:4px;bottom:4px;left:4px;
width:calc(50% - 6px);border-radius:10px;
background:linear-gradient(135deg,var(--cyan),var(--indigo));
box-shadow:0 8px 22px rgba(111,125,255,.4);
transition:transform .35s cubic-bezier(.4,1.3,.5,1);
}
.tabs[data-m="up"]::before{transform:translateX(calc(100% + 4px));}
.tab{
position:relative;z-index:1;font-family:'Jost',sans-serif;
background:transparent;border:none;cursor:pointer;
padding:10px 8px;border-radius:10px;
font-size:12.5px;font-weight:800;letter-spacing:.7px;
color:var(--muted);transition:color .3s;
}
.tab[aria-selected="true"]{color:#fff;}
.tab:hover{color:var(--txt);}
/* ── social ── */
.social{display:flex;gap:9px;margin-bottom:18px;}
.social button{
flex:1;font-family:'Jost',sans-serif;cursor:pointer;
display:flex;align-items:center;justify-content:center;gap:7px;
background:var(--card-2);border:1px solid var(--line);color:var(--txt);
font-size:12px;font-weight:700;padding:11px 8px;border-radius:11px;
transition:transform .22s,border-color .22s,background .22s;
}
.social button:hover{transform:translateY(-2px);border-color:rgba(255,255,255,.28);
background:rgba(255,255,255,.1);}
.social svg{width:15px;height:15px;flex-shrink:0;}
.divider{display:flex;align-items:center;gap:12px;margin-bottom:18px;
color:var(--muted);font-size:10.5px;font-weight:700;letter-spacing:1.4px;text-transform:uppercase;}
.divider::before,.divider::after{content:"";flex:1;height:1px;background:var(--line);}
/* ═══════════ FIELDS — icons perfectly aligned ═══════════ */
.field{position:relative;margin-bottom:14px;}
.field.gone{display:none;}
.field input{
width:100%;font-family:'Jost',sans-serif;
background:rgba(255,255,255,.04);
border:1.5px solid var(--line);border-radius:13px;
/* right padding = slot + gap → text kabhi icon ke neeche nahi jayega */
padding:19px calc(var(--slot) + 10px) 9px 15px;
font-size:14px;font-weight:600;color:var(--txt);
outline:none;line-height:1.3;
transition:border-color .25s,box-shadow .25s,background .25s;
}
.field input::placeholder{color:transparent;}
.field label{
position:absolute;left:15px;top:15px;
color:var(--muted);font-size:14px;font-weight:500;pointer-events:none;
transition:top .2s ease,font-size .2s ease,color .2s ease;
}
.field input:focus{border-color:var(--indigo);background:rgba(255,255,255,.06);
box-shadow:0 0 0 4px rgba(111,125,255,.14);}
.field input:focus + label,
.field input:not(:placeholder-shown) + label{
top:7px;font-size:10px;font-weight:800;letter-spacing:.8px;text-transform:uppercase;}
.field input:focus + label{color:var(--cyan);}
/* ── ONE slot rule: static icon AND eye button dono identical ── */
.field .slot{
position:absolute;right:8px;
top:50%;transform:translateY(-50%); /* hamesha vertically center */
width:var(--slot);height:var(--slot);
display:flex;align-items:center;justify-content:center;
border:none;background:transparent;padding:0;margin:0;
color:var(--muted);border-radius:10px;
transition:color .2s,background .2s;
line-height:0; /* SVG baseline shift kill */
}
.field button.slot{cursor:pointer;}
.field button.slot:hover{color:var(--txt);background:rgba(255,255,255,.07);}
.field .slot svg{
width:17px;height:17px;display:block; /* block = koi inline gap nahi */
stroke:currentColor;stroke-width:2;fill:none;
}
/* validation */
.field.ok input{border-color:rgba(92,230,168,.55);}
.field.ok .slot:not(button){color:var(--lime);}
.field.bad input{border-color:rgba(255,97,131,.6);box-shadow:0 0 0 4px rgba(255,97,131,.1);}
.field.bad .slot:not(button){color:var(--rose);}
.field.bad{animation:shake .35s ease;}
@keyframes shake{25%{transform:translateX(-5px);}75%{transform:translateX(5px);}}
.msg{font-size:11px;font-weight:600;color:var(--rose);margin:6px 2px 0;
height:0;opacity:0;overflow:hidden;transition:height .2s,opacity .2s;}
.field.bad .msg{height:15px;opacity:1;}
/* ── password strength ── */
.strength{display:flex;gap:5px;height:0;opacity:0;margin:-6px 2px 12px;
transition:height .25s,opacity .25s,margin .25s;}
.strength.show{height:4px;opacity:1;margin:2px 2px 14px;}
.strength i{flex:1;height:4px;border-radius:99px;background:rgba(255,255,255,.1);
transition:background .3s;}
.strength[data-s="1"] i:nth-child(-n+1){background:var(--rose);}
.strength[data-s="2"] i:nth-child(-n+2){background:var(--amber);}
.strength[data-s="3"] i:nth-child(-n+3){background:var(--cyan);}
.strength[data-s="4"] i{background:var(--lime);}
/* ── row ── */
.row{display:flex;align-items:center;justify-content:space-between;gap:10px;
margin-bottom:18px;flex-wrap:wrap;}
.row.gone{display:none;}
.check{display:flex;align-items:center;gap:8px;cursor:pointer;
font-size:12.5px;font-weight:600;color:var(--muted);user-select:none;line-height:1.4;}
.check input{display:none;}
.box{width:17px;height:17px;border-radius:6px;flex-shrink:0;
border:1.5px solid var(--line);background:rgba(255,255,255,.04);
display:grid;place-items:center;transition:.22s;}
.box svg{width:10px;height:10px;stroke:#fff;stroke-width:3;fill:none;
opacity:0;transform:scale(.5);transition:.22s;display:block;}
.check input:checked + .box{background:linear-gradient(135deg,var(--cyan),var(--indigo));
border-color:transparent;}
.check input:checked + .box svg{opacity:1;transform:scale(1);}
.check:hover{color:var(--txt);}
.check a{color:var(--cyan);text-decoration:none;font-weight:700;}
.forgot{font-size:12.5px;font-weight:700;color:var(--cyan);text-decoration:none;transition:color .2s;}
.forgot:hover{color:var(--indigo);}
/* ── submit ── */
.submit{
width:100%;font-family:'Jost',sans-serif;cursor:pointer;border:none;
display:flex;align-items:center;justify-content:center;gap:9px;
padding:15px;border-radius:13px;
font-size:13px;font-weight:900;letter-spacing:1.3px;text-transform:uppercase;color:#fff;
background:linear-gradient(135deg,var(--cyan),var(--indigo));
box-shadow:0 14px 34px rgba(111,125,255,.4);
position:relative;overflow:hidden;transition:transform .25s,box-shadow .25s;
}
.submit::before{content:"";position:absolute;inset:0;left:-100%;
background:linear-gradient(120deg,transparent,rgba(255,255,255,.4),transparent);
transition:left .65s;}
.submit:hover{transform:translateY(-3px);box-shadow:0 20px 46px rgba(111,125,255,.55);}
.submit:hover::before{left:100%;}
.submit:active{transform:translateY(-1px);}
.submit svg{width:15px;height:15px;stroke:#fff;stroke-width:2.4;fill:none;display:block;}
.submit.loading{pointer-events:none;}
.submit.loading span,.submit.loading svg{opacity:0;}
.submit.loading::after{content:"";position:absolute;width:19px;height:19px;border-radius:50%;
border:2.5px solid rgba(255,255,255,.3);border-top-color:#fff;animation:spin .65s linear infinite;}
@keyframes spin{to{transform:rotate(360deg);}}
.submit.done{background:linear-gradient(135deg,var(--lime),#3fd0a0);
box-shadow:0 14px 34px rgba(92,230,168,.4);}
.foot{text-align:center;margin-top:18px;font-size:12.5px;color:var(--muted);font-weight:600;}
.foot button{background:none;border:none;cursor:pointer;font-family:'Jost',sans-serif;
color:var(--cyan);font-size:12.5px;font-weight:800;transition:color .2s;padding:0;}
.foot button:hover{color:var(--indigo);}
/* fade when switching mode */
.pane{animation:fade .35s ease both;}
@keyframes fade{from{opacity:0;transform:translateY(8px);}}
/* ═══════════ RESPONSIVE ═══════════ */
@media(max-width:480px){
body{padding:16px;align-items:flex-start;padding-top:28px;}
.card{padding:28px 20px 22px;border-radius:20px;max-width:100%;}
h1{font-size:21px;}
.sub{font-size:12.5px;margin-bottom:16px;}
.tabs{margin-bottom:18px;}
.tab{font-size:11.5px;padding:9px 6px;}
.social button{font-size:11px;padding:10px 6px;}
.social button b{display:none;}
.field input{font-size:13.5px;}
:root{--slot:34px;}
.field .slot svg{width:16px;height:16px;}
}
@media(max-width:340px){
.card{padding:24px 16px 18px;}
.row{flex-direction:column;align-items:flex-start;gap:11px;}
}
@media(min-width:1200px){
.card{max-width:430px;padding:40px 40px 32px;}
}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;}}
:focus-visible{outline:2px solid var(--cyan);outline-offset:2px;border-radius:8px;}
</style>
</head>
<body>
<div class="aurora"><i></i><i></i><i></i></div>
<div class="card">
<div class="logo">
<svg viewBox="0 0 24 24"><path d="M12 2l3 6 6 1-4.5 4.3L18 20l-6-3.2L6 20l1.5-6.7L3 9l6-1 3-6z"/></svg>
</div>
<h1 id="title">Welcome <em>back</em></h1>
<p class="sub" id="subtitle">Sign in to pick up where you left off.</p>
<!-- ── TABS ── -->
<div class="tabs" id="tabs" data-m="in" role="tablist">
<button class="tab" id="tabIn" role="tab" aria-selected="true">Sign in</button>
<button class="tab" id="tabUp" role="tab" aria-selected="false">Sign up</button>
</div>
<div class="social">
<button type="button">
<svg viewBox="0 0 24 24"><path fill="#4285F4" d="M23.5 12.3c0-.8-.1-1.6-.2-2.3H12v4.5h6.5a5.6 5.6 0 0 1-2.4 3.6v3h3.9c2.3-2.1 3.5-5.2 3.5-8.8z"/><path fill="#34A853" d="M12 24c3.2 0 5.9-1.1 7.9-2.9l-3.9-3a7.2 7.2 0 0 1-10.7-3.8H1.3v3.1A12 12 0 0 0 12 24z"/><path fill="#FBBC05" d="M5.3 14.3a7.1 7.1 0 0 1 0-4.6V6.6H1.3a12 12 0 0 0 0 10.8l4-3.1z"/><path fill="#EA4335" d="M12 4.8c1.8 0 3.4.6 4.6 1.8l3.5-3.5A12 12 0 0 0 1.3 6.6l4 3.1A7.2 7.2 0 0 1 12 4.8z"/></svg>
<b>Google</b>
</button>
<button type="button">
<svg viewBox="0 0 24 24" fill="#eaf0ff"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2.2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1.1-.7.1-.7.1-.7 1.2.1 1.9 1.2 1.9 1.2 1.1 1.9 2.9 1.3 3.6 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-5.9 0-1.3.5-2.4 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.3 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3z"/></svg>
<b>GitHub</b>
</button>
</div>
<div class="divider" id="divider">or with email</div>
<form id="form" novalidate>
<!-- NAME — sirf Sign up me -->
<div class="field gone" id="fName">
<input type="text" id="name" placeholder=" " autocomplete="name">
<label for="name">Full name</label>
<span class="slot">
<svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</span>
<p class="msg">Please enter your name</p>
</div>
<!-- EMAIL -->
<div class="field" id="fEmail">
<input type="email" id="email" placeholder=" " autocomplete="email">
<label for="email">Email address</label>
<span class="slot">
<svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="3"/><path d="M2 7l10 6 10-6"/></svg>
</span>
<p class="msg">Enter a valid email address</p>
</div>
<!-- PASSWORD -->
<div class="field" id="fPass">
<input type="password" id="pass" placeholder=" " autocomplete="current-password">
<label for="pass">Password</label>
<button type="button" class="slot" id="eye" aria-label="Show password">
<svg id="eyeIcon" viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<p class="msg">Password must be at least 8 characters</p>
</div>
<div class="strength" id="strength" data-s="0"><i></i><i></i><i></i><i></i></div>
<!-- CONFIRM — sirf Sign up me -->
<div class="field gone" id="fConf">
<input type="password" id="conf" placeholder=" " autocomplete="new-password">
<label for="conf">Confirm password</label>
<button type="button" class="slot" id="eye2" aria-label="Show password">
<svg id="eyeIcon2" viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<p class="msg">Passwords don't match</p>
</div>
<!-- ROW: sign in -->
<div class="row" id="rowIn">
<label class="check">
<input type="checkbox" checked>
<span class="box"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></span>
Remember me
</label>
<a href="#" class="forgot">Forgot password?</a>
</div>
<!-- ROW: sign up -->
<div class="row gone" id="rowUp">
<label class="check">
<input type="checkbox" id="terms">
<span class="box"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></span>
I agree to the <a href="#">Terms</a>
</label>
</div>
<button type="submit" class="submit" id="submit">
<span id="btnText">Sign in</span>
<svg viewBox="0 0 24 24" id="arrow"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</form>
<p class="foot" id="foot">
<span id="footText">New here?</span>
<button type="button" id="footBtn">Create an account</button>
</p>
</div>
<!-- JAVASCRIPT | Coding_Stellix -->
<script>
(function(){
const $ = id => document.getElementById(id);
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
const form=$('form'), name=$('name'), email=$('email'), pass=$('pass'), conf=$('conf');
const fName=$('fName'), fEmail=$('fEmail'), fPass=$('fPass'), fConf=$('fConf');
const strength=$('strength'), submit=$('submit'), tabs=$('tabs');
let mode = 'in'; /* 'in' | 'up' */
/* ═══ MODE SWITCH ═══ */
function setMode(m){
if(m === mode) return;
mode = m;
tabs.dataset.m = m;
$('tabIn').setAttribute('aria-selected', String(m === 'in'));
$('tabUp').setAttribute('aria-selected', String(m === 'up'));
const up = m === 'up';
fName.classList.toggle('gone', !up);
fConf.classList.toggle('gone', !up);
$('rowIn').classList.toggle('gone', up);
$('rowUp').classList.toggle('gone', !up);
$('title').innerHTML = up ? 'Create your <em>account</em>' : 'Welcome <em>back</em>';
$('subtitle').textContent = up
? 'Takes less than a minute to get started.'
: 'Sign in to pick up where you left off.';
$('divider').textContent = up ? 'or sign up with email' : 'or with email';
$('btnText').textContent = up ? 'Create account' : 'Sign in';
$('footText').textContent = up ? 'Already have an account?' : 'New here?';
$('footBtn').textContent = up ? 'Sign in' : 'Create an account';
pass.setAttribute('autocomplete', up ? 'new-password' : 'current-password');
/* clear states */
[fName,fEmail,fPass,fConf].forEach(f => f.className = f.className.replace(/ (ok|bad)/g,''));
form.querySelectorAll('input').forEach(i => { if(i.type !== 'checkbox') i.value = ''; });
strength.classList.remove('show');
strength.dataset.s = 0;
/* fade the card body */
form.classList.remove('pane'); void form.offsetWidth; form.classList.add('pane');
}
$('tabIn').addEventListener('click', () => setMode('in'));
$('tabUp').addEventListener('click', () => setMode('up'));
$('footBtn').addEventListener('click', () => setMode(mode === 'in' ? 'up' : 'in'));
/* ═══ SHOW / HIDE PASSWORD ═══ */
const EYE_OPEN = '<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/>';
const EYE_SHUT = '<path d="M17.9 17.9A10.6 10.6 0 0 1 12 19c-7 0-11-7-11-7a19 19 0 0 1 5.1-5.9m3.3-1.6A10.6 10.6 0 0 1 12 5c7 0 11 7 11 7a19 19 0 0 1-2.2 3.2M9.9 4.2 1 21"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/>';
function toggleEye(btn, icon, input){
btn.addEventListener('click', () => {
const show = input.type === 'password';
input.type = show ? 'text' : 'password';
icon.innerHTML = show ? EYE_SHUT : EYE_OPEN;
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
input.focus();
});
}
toggleEye($('eye'), $('eyeIcon'), pass);
toggleEye($('eye2'), $('eyeIcon2'), conf);
/* ═══ STRENGTH ═══ */
function score(v){
let s = 0;
if(v.length >= 8) s++;
if(/[A-Z]/.test(v) && /[a-z]/.test(v)) s++;
if(/\d/.test(v)) s++;
if(/[^A-Za-z0-9]/.test(v) && v.length >= 10) s++;
return s;
}
pass.addEventListener('input', () => {
strength.classList.toggle('show', pass.value.length > 0);
strength.dataset.s = score(pass.value);
if(fPass.classList.contains('bad') && pass.value.length >= 8) fPass.className = 'field ok';
});
/* ═══ LIVE VALIDATION ═══ */
email.addEventListener('input', () => {
if(!email.value){ fEmail.className = 'field'; return; }
fEmail.className = 'field ' + (EMAIL_RE.test(email.value) ? 'ok' : '');
});
email.addEventListener('blur', () => {
if(email.value && !EMAIL_RE.test(email.value)) fEmail.className = 'field bad';
});
pass.addEventListener('blur', () => {
if(pass.value && pass.value.length < 8) fPass.className = 'field bad';
else if(pass.value) fPass.className = 'field ok';
});
name.addEventListener('input', () => {
fName.className = 'field' + (name.value.trim().length >= 2 ? ' ok' : '');
});
conf.addEventListener('input', () => {
if(!conf.value){ fConf.className = 'field'; return; }
fConf.className = 'field ' + (conf.value === pass.value ? 'ok' : '');
});
conf.addEventListener('blur', () => {
if(conf.value && conf.value !== pass.value) fConf.className = 'field bad';
});
function fail(f){
f.className = 'field';
void f.offsetWidth; /* reflow → shake replay */
f.className = 'field bad';
}
/* ═══ SUBMIT ═══ */
form.addEventListener('submit', e => {
e.preventDefault();
const up = mode === 'up';
let bad = null;
if(up && name.value.trim().length < 2){ fail(fName); bad = bad || name; }
if(!EMAIL_RE.test(email.value)){ fail(fEmail); bad = bad || email; }
if(pass.value.length < 8){ fail(fPass); bad = bad || pass; }
if(up && conf.value !== pass.value){ fail(fConf); bad = bad || conf; }
if(up && !$('terms').checked){
const box = $('terms').nextElementSibling;
box.style.borderColor = 'var(--rose)';
setTimeout(() => { box.style.borderColor = ''; }, 1200);
bad = bad || $('terms');
}
if(bad){ bad.focus(); return; }
submit.classList.add('loading');
setTimeout(() => {
submit.classList.remove('loading');
submit.classList.add('done');
$('btnText').textContent = up ? 'Account created' : 'Signed in';
$('arrow').innerHTML = '<path d="M20 6L9 17l-5-5"/>';
setTimeout(() => {
submit.classList.remove('done');
$('btnText').textContent = up ? 'Create account' : 'Sign in';
$('arrow').innerHTML = '<path d="M5 12h14M12 5l7 7-7 7"/>';
}, 2200);
}, 1400);
});
})();
</script>
</body>
</html>



