<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login & Register</title>
<link rel="stylesheet" href="styles.css" />
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div class="bg-grid"></div>
<main class="container">
<!-- ========== LOGIN CARD ========== -->
<div id="login-card" class="card card--login active">
<div class="card__accent"></div>
<div class="card__body">
<div class="card__badge">US_UI_001</div>
<h1 class="card__title">Login<span class="blink">_</span></h1>
<p class="card__subtitle">Welcome back. Enter your credentials.</p>
<form id="login-form" novalidate>
<div class="input-group">
<label for="login-username">Username</label>
<input type="text" id="login-username" placeholder="your_username" autocomplete="username" />
<span class="error-msg" id="login-username-err"></span>
</div>
<div class="input-group">
<label for="login-password">Password</label>
<div class="password-wrapper">
<input type="password" id="login-password" placeholder="••••••••" autocomplete="current-password" />
<button type="button" class="toggle-pw" data-target="login-password" aria-label="Toggle password visibility">👁</button>
</div>
<span class="error-msg" id="login-password-err"></span>
</div>
<button type="submit" class="btn btn--primary">Log In →</button>
</form>
<p class="switch-text">
Not registered?
<button type="button" class="link-btn" id="go-register">Create Account</button>
</p>
</div>
</div>
<!-- ========== REGISTER CARD ========== -->
<div id="register-card" class="card card--register">
<div class="card__accent card__accent--green"></div>
<div class="card__body">
<div class="card__badge badge--green">NEW USER</div>
<h1 class="card__title">Register<span class="blink">_</span></h1>
<p class="card__subtitle">Create your account in seconds.</p>
<form id="register-form" novalidate>
<div class="input-group">
<label for="reg-username">Username</label>
<input type="text" id="reg-username" placeholder="pick_a_username" autocomplete="username" />
<span class="error-msg" id="reg-username-err"></span>
</div>
<div class="input-group">
<label for="reg-email">Email</label>
<input type="email" id="reg-email" placeholder="[email protected]" autocomplete="email" />
<span class="error-msg" id="reg-email-err"></span>
</div>
<div class="input-group">
<label for="reg-password">Password</label>
<div class="password-wrapper">
<input type="password" id="reg-password" placeholder="••••••••" autocomplete="new-password" />
<button type="button" class="toggle-pw" data-target="reg-password" aria-label="Toggle password visibility">👁</button>
</div>
<span class="error-msg" id="reg-password-err"></span>
</div>
<div class="input-group">
<label for="reg-confirm">Confirm Password</label>
<div class="password-wrapper">
<input type="password" id="reg-confirm" placeholder="••••••••" autocomplete="new-password" />
<button type="button" class="toggle-pw" data-target="reg-confirm" aria-label="Toggle password visibility">👁</button>
</div>
<span class="error-msg" id="reg-confirm-err"></span>
</div>
<button type="submit" class="btn btn--green">Register →</button>
</form>
<p class="switch-text">
Already have an account?
<button type="button" class="link-btn" id="go-login">Log In</button>
</p>
</div>
</div>
<!-- ========== SUCCESS TOAST ========== -->
<div id="toast" class="toast" aria-live="polite"></div>
</main>
<script src="script.js"></script>
</body>
</html>
/* ===== RESET & BASE ===== */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--black: #1a1a1a;
--white: #fffdf7;
--yellow: #facc15;
--pink: #f472b6;
--blue: #3b82f6;
--green: #22c55e;
--red: #ef4444;
--orange: #fb923c;
--purple: #a855f7;
--border: 3px solid var(--black);
--shadow: 6px 6px 0 var(--black);
--shadow-sm: 4px 4px 0 var(--black);
--shadow-lg: 8px 8px 0 var(--black);
--radius: 0px;
--font-mono: "Space Mono", monospace;
--font-sans: "Space Grotesk", system-ui, sans-serif;
}
html {
font-size: 16px;
scroll-behavior: smooth;
}
body {
font-family: var(--font-sans);
color: var(--black);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
/* ===== ANIMATED GRADIENT BACKGROUND ===== */
.bg-gradient {
position: fixed;
inset: 0;
background: linear-gradient(
135deg,
#0f172a 0%,
#1e3a5f 20%,
#1e40af 40%,
#7c3aed 60%,
#db2777 80%,
#f97316 100%
);
background-size: 400% 400%;
animation: gradientShift 12s ease infinite;
z-index: 0;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* ===== GRID OVERLAY ===== */
.bg-grid {
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 1;
}
/* ===== NOISE TEXTURE ===== */
.bg-noise {
position: fixed;
inset: 0;
opacity: 0.35;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 2;
}
/* ===== FLOATING DECORATIONS ===== */
.floater {
position: fixed;
font-size: 2.5rem;
opacity: 0.15;
z-index: 2;
pointer-events: none;
animation: floatUp 18s linear infinite;
}
.floater--1 { left: 8%; top: 20%; animation-duration: 20s; font-size: 3.5rem; opacity: 0.2; }
.floater--2 { left: 85%; top: 15%; animation-duration: 15s; animation-delay: -3s; }
.floater--3 { left: 75%; top: 70%; animation-duration: 22s; animation-delay: -7s; }
.floater--4 { left: 12%; top: 75%; animation-duration: 17s; animation-delay: -5s; font-size: 3rem; }
.floater--5 { left: 50%; top: 40%; animation-duration: 25s; animation-delay: -10s; }
@keyframes floatUp {
0% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-30px) rotate(5deg); }
50% { transform: translateY(-10px) rotate(-3deg); }
75% { transform: translateY(-40px) rotate(4deg); }
100% { transform: translateY(0) rotate(0deg); }
}
/* ===== SCROLLING TRAIN TRACK ===== */
.track-strip {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
overflow: hidden;
z-index: 3;
background: rgba(0, 0, 0, 0.5);
border-top: 3px solid rgba(255, 255, 255, 0.15);
backdrop-filter: blur(4px);
padding: 0.4rem 0;
}
.track-scroll {
display: flex;
white-space: nowrap;
animation: scrollTrack 12s linear infinite;
font-family: var(--font-mono);
font-size: 1rem;
color: rgba(250, 204, 21, 0.6);
letter-spacing: 2px;
}
.track-scroll span {
flex-shrink: 0;
padding-right: 1rem;
}
@keyframes scrollTrack {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
/* ===== CONTAINER ===== */
.container {
position: relative;
z-index: 10;
width: 100%;
max-width: 480px;
padding: 1.5rem;
}
/* ===== BRAND ===== */
.brand {
display: flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
margin-bottom: 1.25rem;
animation: popIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.brand__icon {
font-size: 2rem;
}
.brand__name {
font-family: var(--font-mono);
font-size: 1.6rem;
font-weight: 700;
color: var(--white);
text-shadow: 3px 3px 0 var(--black);
letter-spacing: 0.05em;
}
/* ===== CARD ===== */
.card {
display: none;
border: var(--border);
box-shadow: var(--shadow-lg);
background: var(--white);
position: relative;
overflow: hidden;
animation: popIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.card.active {
display: block;
}
@keyframes popIn {
from {
opacity: 0;
transform: translateY(24px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* accent strip */
.card__accent {
height: 10px;
background: repeating-linear-gradient(
90deg,
var(--yellow) 0px,
var(--yellow) 20px,
var(--black) 20px,
var(--black) 24px
);
border-bottom: var(--border);
}
.card__accent--green {
background: repeating-linear-gradient(
90deg,
var(--green) 0px,
var(--green) 20px,
var(--black) 20px,
var(--black) 24px
);
}
.card__body {
padding: 2rem 2rem 1.75rem;
}
/* badge row */
.card__header {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.card__badge {
display: inline-block;
font-family: var(--font-mono);
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
background: var(--yellow);
border: 2px solid var(--black);
box-shadow: var(--shadow-sm);
padding: 0.2rem 0.65rem;
}
.badge--green {
background: var(--green);
}
.badge--outline {
background: transparent;
color: var(--black);
}
/* title */
.card__title {
font-family: var(--font-mono);
font-size: 2.4rem;
font-weight: 700;
line-height: 1.1;
margin-bottom: 0.3rem;
}
.blink {
animation: blink 1s step-end infinite;
color: var(--blue);
}
@keyframes blink {
50% { opacity: 0; }
}
.card__subtitle {
font-size: 0.95rem;
color: #666;
margin-bottom: 1.75rem;
}
/* ===== INPUT GROUP ===== */
.input-group {
margin-bottom: 1.15rem;
}
.input-group label {
display: flex;
align-items: center;
gap: 0.35rem;
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 0.4rem;
}
.label-icon {
font-size: 0.85rem;
}
.input-group input {
width: 100%;
padding: 0.8rem 1rem;
font-family: var(--font-sans);
font-size: 1rem;
border: var(--border);
background: #fafaf8;
box-shadow: var(--shadow-sm);
outline: none;
transition: box-shadow 0.2s, transform 0.2s, background 0.2s;
}
.input-group input::placeholder {
color: #b0b0a8;
}
.input-group input:focus {
box-shadow: 8px 8px 0 var(--blue);
transform: translate(-2px, -2px);
background: var(--white);
}
.input-group input.invalid {
box-shadow: 6px 6px 0 var(--red);
border-color: var(--red);
background: #fff5f5;
}
.input-group input.valid {
box-shadow: 6px 6px 0 var(--green);
border-color: var(--green);
background: #f0fdf4;
}
/* password wrapper */
.password-wrapper {
position: relative;
}
.password-wrapper input {
padding-right: 3.2rem;
}
.toggle-pw {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 3rem;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
border-left: var(--border);
cursor: pointer;
font-size: 1rem;
transition: background 0.15s;
}
.toggle-pw:hover {
background: var(--yellow);
}
/* error message */
.error-msg {
display: block;
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 700;
color: var(--red);
min-height: 1rem;
margin-top: 0.3rem;
}
/* ===== BUTTONS ===== */
.btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
padding: 0.9rem 1.5rem;
font-family: var(--font-mono);
font-size: 1.05rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
border: var(--border);
box-shadow: var(--shadow);
cursor: pointer;
transition: transform 0.12s, box-shadow 0.12s;
margin-top: 0.75rem;
}
.btn:hover {
transform: translate(-2px, -2px);
box-shadow: 8px 8px 0 var(--black);
}
.btn:active {
transform: translate(4px, 4px);
box-shadow: 0 0 0 var(--black);
}
.btn__arrow {
font-size: 1.3rem;
transition: transform 0.2s;
}
.btn:hover .btn__arrow {
transform: translateX(4px);
}
.btn--primary {
background: var(--yellow);
color: var(--black);
}
.btn--green {
background: var(--green);
color: var(--black);
}
/* divider */
.divider {
display: flex;
align-items: center;
gap: 1rem;
margin: 1.25rem 0;
}
.divider::before,
.divider::after {
content: "";
flex: 1;
height: 2px;
background: repeating-linear-gradient(
90deg,
var(--black) 0px,
var(--black) 4px,
transparent 4px,
transparent 8px
);
}
.divider span {
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 700;
color: #999;
letter-spacing: 0.1em;
}
/* switch link */
.switch-text {
text-align: center;
font-size: 0.9rem;
color: #666;
}
.link-btn {
background: none;
border: none;
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.88rem;
color: var(--blue);
cursor: pointer;
text-decoration: none;
border-bottom: 2px solid var(--blue);
padding-bottom: 1px;
transition: color 0.15s, border-color 0.15s;
}
.link-btn:hover {
color: var(--purple);
border-color: var(--purple);
}
/* ===== TOAST ===== */
.toast {
position: fixed;
bottom: 3rem;
left: 50%;
transform: translateX(-50%) translateY(120px);
padding: 0.85rem 1.5rem;
font-family: var(--font-mono);
font-size: 0.82rem;
font-weight: 700;
border: var(--border);
box-shadow: var(--shadow);
background: var(--green);
color: var(--black);
white-space: nowrap;
z-index: 100;
opacity: 0;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ===== RESPONSIVE ===== */
@media (max-width: 520px) {
.container {
padding: 1rem;
}
.card__body {
padding: 1.5rem 1.25rem;
}
.card__title {
font-size: 1.9rem;
}
.brand__name {
font-size: 1.3rem;
}
.floater {
display: none;
}
}
// ===== DOM REFERENCES =====
const loginCard = document.getElementById("login-card");
const registerCard = document.getElementById("register-card");
const loginForm = document.getElementById("login-form");
const registerForm = document.getElementById("register-form");
const toast = document.getElementById("toast");
// ===== PAGE SWITCHING =====
document.getElementById("go-register").addEventListener("click", () => {
loginCard.classList.remove("active");
registerCard.classList.add("active");
clearForm(loginForm);
});
document.getElementById("go-login").addEventListener("click", () => {
registerCard.classList.remove("active");
loginCard.classList.add("active");
clearForm(registerForm);
});
// ===== PASSWORD TOGGLE =====
document.querySelectorAll(".toggle-pw").forEach((btn) => {
btn.addEventListener("click", () => {
const input = document.getElementById(btn.dataset.target);
const isPassword = input.type === "password";
input.type = isPassword ? "text" : "password";
btn.textContent = isPassword ? "🙈" : "👁";
});
});
// ===== VALIDATION HELPERS =====
function setError(id, msg) {
const el = document.getElementById(id);
const input =
el.previousElementSibling?.querySelector?.("input") ||
el.previousElementSibling;
el.textContent = msg;
if (input) {
input.classList.remove("valid");
input.classList.add("invalid");
}
}
function setValid(id) {
const el = document.getElementById(id);
const input =
el.previousElementSibling?.querySelector?.("input") ||
el.previousElementSibling;
el.textContent = "";
if (input) {
input.classList.remove("invalid");
input.classList.add("valid");
}
}
function clearForm(form) {
form.reset();
form.querySelectorAll(".error-msg").forEach((e) => (e.textContent = ""));
form
.querySelectorAll("input")
.forEach((i) => i.classList.remove("invalid", "valid"));
}
function showToast(message) {
toast.textContent = message;
toast.classList.add("show");
setTimeout(() => toast.classList.remove("show"), 3000);
}
// ===== REAL-TIME VALIDATION =====
function attachLiveValidation(inputId, errId, validator) {
const input = document.getElementById(inputId);
input.addEventListener("input", () => {
const msg = validator(input.value.trim());
msg ? setError(errId, msg) : setValid(errId);
});
}
// Login fields
attachLiveValidation("login-username", "login-username-err", (v) =>
v === "" ? "Username is required." : null
);
attachLiveValidation("login-password", "login-password-err", (v) =>
v === "" ? "Password is required." : null
);
// Register fields
attachLiveValidation("reg-username", "reg-username-err", (v) => {
if (v === "") return "Username is required.";
if (v.length < 3) return "Min 3 characters.";
if (!/^[a-zA-Z0-9_]+$/.test(v)) return "Only letters, numbers, underscores.";
return null;
});
attachLiveValidation("reg-email", "reg-email-err", (v) => {
if (v === "") return "Email is required.";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) return "Enter a valid email.";
return null;
});
attachLiveValidation("reg-password", "reg-password-err", (v) => {
if (v === "") return "Password is required.";
if (v.length < 6) return "Min 6 characters.";
return null;
});
attachLiveValidation("reg-confirm", "reg-confirm-err", (v) => {
if (v === "") return "Please confirm your password.";
if (v !== document.getElementById("reg-password").value)
return "Passwords do not match.";
return null;
});
// ===== LOGIN SUBMIT =====
loginForm.addEventListener("submit", (e) => {
e.preventDefault();
let isValid = true;
const username = document.getElementById("login-username").value.trim();
const password = document.getElementById("login-password").value.trim();
if (username === "") {
setError("login-username-err", "Username is required.");
isValid = false;
} else {
setValid("login-username-err");
}
if (password === "") {
setError("login-password-err", "Password is required.");
isValid = false;
} else {
setValid("login-password-err");
}
if (!isValid) return;
const btn = loginForm.querySelector(".btn");
const btnText = btn.querySelector(".btn__text") || btn;
btnText.textContent = "Boarding…";
btn.disabled = true;
setTimeout(() => {
showToast("✓ Welcome aboard! Login successful.");
btnText.textContent = "Log In";
btn.disabled = false;
clearForm(loginForm);
}, 1200);
});
// ===== REGISTER SUBMIT =====
registerForm.addEventListener("submit", (e) => {
e.preventDefault();
let isValid = true;
const username = document.getElementById("reg-username").value.trim();
const email = document.getElementById("reg-email").value.trim();
const password = document.getElementById("reg-password").value;
const confirm = document.getElementById("reg-confirm").value;
if (username === "") {
setError("reg-username-err", "Username is required.");
isValid = false;
} else if (username.length < 3) {
setError("reg-username-err", "Min 3 characters.");
isValid = false;
} else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
setError("reg-username-err", "Only letters, numbers, underscores.");
isValid = false;
} else {
setValid("reg-username-err");
}
if (email === "") {
setError("reg-email-err", "Email is required.");
isValid = false;
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setError("reg-email-err", "Enter a valid email.");
isValid = false;
} else {
setValid("reg-email-err");
}
if (password === "") {
setError("reg-password-err", "Password is required.");
isValid = false;
} else if (password.length < 6) {
setError("reg-password-err", "Min 6 characters.");
isValid = false;
} else {
setValid("reg-password-err");
}
if (confirm === "") {
setError("reg-confirm-err", "Please confirm your password.");
isValid = false;
} else if (confirm !== password) {
setError("reg-confirm-err", "Passwords do not match.");
isValid = false;
} else {
setValid("reg-confirm-err");
}
if (!isValid) return;
const btn = registerForm.querySelector(".btn");
const btnText = btn.querySelector(".btn__text") || btn;
btnText.textContent = "Issuing ticket…";
btn.disabled = true;
setTimeout(() => {
showToast("🎫 Ticket issued! Redirecting to login…");
btnText.textContent = "Register";
btn.disabled = false;
clearForm(registerForm);
setTimeout(() => {
registerCard.classList.remove("active");
loginCard.classList.add("active");
}, 1500);
}, 1200);
});
6 views