Files
malenia-frontend/.ref/login.html
2026-08-10 12:01:03 +07:00

561 lines
15 KiB
HTML

<!doctype html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MaleniaVPN · Sign In</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=Cormorant+Garamond:wght@400;500;600&amp;family=Tenor+Sans&amp;display=swap" rel="stylesheet">
<style>
:root {
--bg: #faf7f2;
--surface: #f5f0e8;
--fg: #1a1610;
--muted: #3d3428;
--accent: #c9a84c;
--font-display: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
--font-body: 'Tenor Sans', 'Helvetica Neue', Arial, sans-serif;
--radius: 8px;
--baseline: 8px;
--hairline: rgba(26, 22, 16, 0.10);
--hairline-strong: rgba(26, 22, 16, 0.18);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
/* Grain overlay */
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9998;
opacity: 0.035;
mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* Custom cursor */
.cursor-dot, .cursor-ring {
position: fixed;
top: 0; left: 0;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: opacity 0.2s;
}
.cursor-dot {
width: 5px; height: 5px;
background: var(--fg);
border-radius: 50%;
}
.cursor-ring {
width: 30px; height: 30px;
border: 1px solid var(--muted);
border-radius: 50%;
opacity: 0.4;
transition: width 0.25s, height 0.25s, border-color 0.25s, opacity 0.25s, transform 0.08s linear;
}
.cursor-ring.is-active {
width: 52px; height: 52px;
border-color: var(--accent);
opacity: 0.9;
}
@media (hover: none), (pointer: coarse) {
.cursor-dot, .cursor-ring { display: none; }
* { cursor: auto !important; }
}
@media (hover: hover) and (pointer: fine) {
body, button, a, input, [data-cursor] { cursor: none; }
}
/* Shell */
.shell {
max-width: 460px;
margin: 0 auto;
padding: 56px 32px 48px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Topbar — logo + kicker anchored to the top, together */
.topbar {
text-align: center;
}
.topbar .kicker {
margin-bottom: 0;
}
.logo {
font-family: var(--font-display);
font-size: 22px;
font-weight: 500;
letter-spacing: 0.35em;
text-transform: uppercase;
color: var(--fg);
text-decoration: none;
display: inline-block;
}
.logo .e { color: var(--accent); }
/* Emblem — geometric mark above the form */
.emblem {
display: flex;
justify-content: center;
margin-bottom: 28px;
color: var(--accent);
}
.emblem svg { width: 44px; height: 44px; display: block; }
/* Card — centered in remaining space */
.card {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: center;
}
.kicker {
text-align: center;
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 18px;
}
.title {
font-family: var(--font-display);
font-weight: 500;
font-size: 36px;
line-height: 1.2;
letter-spacing: -0.01em;
text-align: center;
color: var(--fg);
margin-bottom: 8px;
}
.sub {
text-align: center;
font-size: 14px;
color: var(--muted);
letter-spacing: 0.02em;
margin-bottom: 40px;
}
/* Form */
.form {
display: flex;
flex-direction: column;
gap: 22px;
}
.field {
position: relative;
}
.field-label {
display: block;
font-size: 10px;
letter-spacing: 0.26em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 10px;
}
.field-input {
width: 100%;
background: transparent;
border: none;
border-bottom: 1px solid var(--hairline-strong);
padding: 8px 36px 12px 0;
font-family: var(--font-body);
font-size: 16px;
color: var(--fg);
letter-spacing: 0.02em;
transition: border-color 0.3s;
outline: none;
}
.field-input::placeholder {
color: var(--muted);
opacity: 0.45;
font-style: italic;
}
.field-input:focus {
border-color: var(--fg);
}
.field-input:focus + .field-line {
transform: scaleX(1);
}
.field-line {
position: absolute;
left: 0; right: 0; bottom: 0;
height: 1px;
background: var(--accent);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
pointer-events: none;
}
.field-icon {
position: absolute;
right: 0;
bottom: 12px;
color: var(--muted);
opacity: 0.5;
display: flex;
align-items: center;
transition: opacity 0.25s, color 0.25s;
background: none;
border: none;
padding: 0;
}
.field-icon svg { width: 18px; height: 18px; display: block; }
.field-icon:hover { opacity: 0.9; color: var(--fg); }
.field-icon:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 4px;
border-radius: 4px;
}
/* Row: remember + forgot */
.row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4px;
font-size: 13px;
}
.check {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--muted);
cursor: pointer;
user-select: none;
}
.check input {
appearance: none;
-webkit-appearance: none;
width: 16px; height: 16px;
border: 1px solid var(--hairline-strong);
border-radius: 3px;
background: transparent;
position: relative;
transition: border-color 0.2s, background 0.2s;
cursor: pointer;
flex-shrink: 0;
}
.check input:checked {
background: var(--fg);
border-color: var(--fg);
}
.check input:checked::after {
content: "";
position: absolute;
left: 4px; top: 1px;
width: 5px; height: 9px;
border: solid var(--bg);
border-width: 0 1.5px 1.5px 0;
transform: rotate(45deg);
}
.check input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.link {
color: var(--muted);
text-decoration: none;
letter-spacing: 0.02em;
border-bottom: 1px solid transparent;
transition: color 0.25s, border-color 0.25s;
}
.link:hover { color: var(--fg); border-bottom-color: var(--hairline-strong); }
.link:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 4px;
border-radius: 2px;
}
/* Primary action */
.btn {
font-family: var(--font-body);
font-size: 13px;
letter-spacing: 0.18em;
text-transform: uppercase;
padding: 16px 28px;
border-radius: var(--radius);
border: 1px solid var(--fg);
background: var(--fg);
color: var(--bg);
cursor: pointer;
transition: background 0.25s, color 0.25s, border-color 0.25s, transform 0.08s;
margin-top: 14px;
}
.btn:hover {
background: var(--muted);
border-color: var(--muted);
}
.btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.btn:active { transform: translateY(1px); }
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* Divider */
.divider {
display: flex;
align-items: center;
gap: 14px;
margin: 32px 0 22px;
color: var(--muted);
font-size: 10px;
letter-spacing: 0.28em;
text-transform: uppercase;
}
.divider::before,
.divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--hairline);
}
/* Secondary — telegram */
.btn-ghost {
width: 100%;
font-family: var(--font-body);
font-size: 13px;
letter-spacing: 0.16em;
text-transform: uppercase;
padding: 15px 28px;
border-radius: var(--radius);
border: 1px solid var(--hairline-strong);
background: transparent;
color: var(--fg);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: border-color 0.25s, background 0.25s, transform 0.08s;
}
.btn-ghost:hover {
border-color: var(--fg);
background: var(--surface);
}
.btn-ghost:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.btn-ghost:active { transform: translateY(1px); }
.btn-ghost svg { width: 16px; height: 16px; }
/* Sign-up hint */
.signup {
text-align: center;
margin-top: 36px;
font-size: 13px;
color: var(--muted);
letter-spacing: 0.02em;
}
.signup .link { color: var(--fg); }
/* Error / status */
.status {
text-align: center;
font-size: 12px;
letter-spacing: 0.04em;
color: var(--muted);
margin-top: 16px;
opacity: 0;
transition: opacity 0.25s;
}
.status:empty { display: none; }
.status.is-error { color: var(--fg); opacity: 1; }
.status.is-info { opacity: 1; }
/* Footer */
.foot {
text-align: center;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
opacity: 0.6;
padding-top: 40px;
}
/* Reveal animation */
.reveal {
opacity: 0;
transform: translateY(14px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.reveal.is-in {
opacity: 1;
transform: none;
}
/* Responsive */
@media (max-width: 480px) {
.shell { padding: 40px 22px 36px; }
.title { font-size: 30px; }
}
</style>
</head>
<body>
<div class="cursor-dot"></div>
<div class="cursor-ring"></div>
<div class="shell">
<header class="topbar reveal" data-od-id="topbar">
<a href="malenia-vpn-account.html" class="logo" data-od-id="logo" aria-label="MaleniaVPN home">
Mal<span class="e">e</span>nia
</a>
<div class="kicker">Private access</div>
</header>
<main class="card">
<h1 class="title reveal" data-od-id="login-title">Sign in</h1>
<p class="sub reveal">Enter your credentials to resume your session.</p>
<form class="form reveal" data-od-id="login-form" novalidate="">
<div class="field" data-od-id="field-username">
<label class="field-label" for="username">Username</label>
<input class="field-input" id="username" name="username" type="text" autocomplete="username" placeholder="your handle" spellcheck="false" required="">
<span class="field-line"></span>
</div>
<div class="field" data-od-id="field-password">
<label class="field-label" for="password">Password</label>
<input class="field-input" id="password" name="password" type="password" autocomplete="current-password" placeholder="••••••••" required="">
<span class="field-line"></span>
<button type="button" class="field-icon" id="toggle-pass" data-od-id="toggle-password" aria-label="Show password" aria-pressed="false">
<svg id="eye-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
<div class="row" data-od-id="login-options">
<label class="check">
<input type="checkbox" id="remember" name="remember">
<span>Keep me signed in</span>
</label>
<a href="#" class="link" data-od-id="forgot-password">Forgot password?</a>
</div>
<button type="submit" class="btn" data-od-id="btn-signin">Sign in</button>
<div class="status" id="status" role="status" aria-live="polite"></div>
</form>
<p class="signup reveal" data-od-id="signup-hint">
No account yet? <a href="#" class="link" data-od-id="signup-link">Create one</a>
</p>
</main>
<footer class="foot reveal" data-od-id="footer">
Protected by MaleniaVPN
</footer>
</div>
<script>
(function () {
// ----- Password visibility toggle -----
const toggle = document.getElementById('toggle-pass');
const pass = document.getElementById('password');
const eye = document.getElementById('eye-icon');
toggle.addEventListener('click', () => {
const show = pass.type === 'password';
pass.type = show ? 'text' : 'password';
toggle.setAttribute('aria-pressed', String(show));
toggle.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
eye.innerHTML = show
? '<path d="M3 3l18 18M10.5 10.7a3 3 0 0 0 4 4M9.4 5.2A10.6 10.6 0 0 1 12 5c6.5 0 10 7 10 7a17.3 17.3 0 0 1-3.2 4.1M6.1 6.2A17.3 17.3 0 0 0 2 12s3.5 7 10 7c1.4 0 2.7-.3 3.8-.7"/>'
: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" /><circle cx="12" cy="12" r="3" />';
});
// ----- Form submit (demo) -----
const form = document.querySelector('.form');
const status = document.getElementById('status');
const btn = form.querySelector('.btn');
form.addEventListener('submit', (e) => {
e.preventDefault();
const u = document.getElementById('username').value.trim();
const p = document.getElementById('password').value;
status.classList.remove('is-error', 'is-info');
if (!u || !p) {
status.textContent = 'Username and password are required.';
status.classList.add('is-error');
return;
}
status.textContent = 'Verifying…';
status.classList.add('is-info');
btn.disabled = true;
setTimeout(() => {
btn.disabled = false;
status.classList.remove('is-info');
status.textContent = 'Unable to reach server. Check your connection.';
status.classList.add('is-error');
}, 1400);
});
// ----- Reveal observer -----
const io = new IntersectionObserver((entries) => {
entries.forEach((en) => {
if (en.isIntersecting) {
en.target.classList.add('is-in');
io.unobserve(en.target);
}
});
}, { threshold: 0.12 });
document.querySelectorAll('.reveal').forEach((el, i) => {
el.style.transitionDelay = (i * 70) + 'ms';
io.observe(el);
});
// ----- Custom cursor -----
const dot = document.querySelector('.cursor-dot');
const ring = document.querySelector('.cursor-ring');
let mx = 0, my = 0, rx = 0, ry = 0;
const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
if (fine) {
window.addEventListener('mousemove', (e) => {
mx = e.clientX; my = e.clientY;
dot.style.left = mx + 'px'; dot.style.top = my + 'px';
});
function tick() {
rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18;
ring.style.left = rx + 'px'; ring.style.top = ry + 'px';
requestAnimationFrame(tick);
}
tick();
document.addEventListener('mouseover', (e) => {
const t = e.target.closest('a, button, input, [data-cursor]');
ring.classList.toggle('is-active', !!t);
});
} else {
dot.style.display = 'none'; ring.style.display = 'none';
}
})();
</script>
</body></html>