This commit is contained in:
2026-08-10 12:01:03 +07:00
commit 265b1202ad
95 changed files with 27718 additions and 0 deletions

921
.ref/account.html Normal file
View File

@@ -0,0 +1,921 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MaleniaVPN · Account</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&family=Tenor+Sans&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, [data-cursor] { cursor: none; }
}
/* Layout */
.shell {
max-width: 1080px;
margin: 0 auto;
padding: 64px 32px 96px;
}
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 32px;
border-bottom: 1px solid var(--hairline);
}
.logo {
font-family: var(--font-display);
font-size: 22px;
font-weight: 500;
letter-spacing: 0.35em;
text-transform: uppercase;
color: var(--fg);
}
.logo .e { color: var(--accent); }
.nav {
display: flex;
gap: 28px;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
.nav a {
color: inherit;
text-decoration: none;
position: relative;
padding: 4px 0;
transition: color 0.3s;
}
.nav a:hover { color: var(--fg); }
.nav a.is-active::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: -2px;
height: 1px;
background: var(--accent);
}
/* Hero / identity */
.hero {
padding: 56px 0 48px;
text-align: center;
border-bottom: 1px solid var(--hairline);
}
.kicker {
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 20px;
}
.hero-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 44px;
line-height: 1.15;
letter-spacing: -0.01em;
color: var(--fg);
}
.hero-sub {
margin-top: 14px;
font-size: 14px;
color: var(--muted);
letter-spacing: 0.02em;
}
/* Profile stat strip — graphics-led */
.profile {
display: grid;
grid-template-columns: repeat(4, 1fr);
border-bottom: 1px solid var(--hairline);
}
.stat {
padding: 40px 24px;
border-right: 1px solid var(--hairline);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
min-height: 168px;
justify-content: center;
}
.stat:last-child { border-right: none; }
.stat-icon {
width: 34px; height: 34px;
color: var(--muted);
display: flex;
align-items: center;
justify-content: center;
transition: color 0.3s;
}
.stat:hover .stat-icon { color: var(--accent); }
.stat-label {
font-size: 10px;
letter-spacing: 0.26em;
text-transform: uppercase;
color: var(--muted);
}
.stat-value {
font-family: var(--font-display);
font-weight: 500;
font-size: 26px;
line-height: 1.1;
color: var(--fg);
letter-spacing: -0.01em;
word-break: break-word;
}
.stat-value.is-mono { font-family: var(--font-body); font-size: 15px; letter-spacing: 0.04em; }
.stat-value.is-empty {
color: var(--muted);
opacity: 0.45;
font-family: var(--font-body);
font-size: 13px;
font-style: italic;
letter-spacing: 0.02em;
}
.stat-value .accent { color: var(--accent); }
/* Subscription access link */
.subscription-link {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 24px;
padding: 20px 24px;
border-bottom: 1px solid var(--hairline);
background: color-mix(in srgb, var(--surface) 86%, var(--accent) 14%);
}
.subscription-emblem {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--accent);
border-radius: 50%;
color: var(--fg);
}
.subscription-emblem svg { width: 18px; height: 18px; }
.subscription-copy { min-width: 0; }
.subscription-label {
margin-bottom: 4px;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
}
.subscription-url {
display: block;
overflow: hidden;
color: var(--fg);
font-size: 13px;
letter-spacing: 0.02em;
text-overflow: ellipsis;
white-space: nowrap;
}
.subscription-url strong {
color: var(--fg);
font-weight: 400;
}
.copy-link {
appearance: none;
min-height: 44px;
padding: 0 16px;
border: 1px solid var(--fg);
border-radius: var(--radius);
background: var(--fg);
color: var(--bg);
font-family: var(--font-body);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
transition: background 0.25s, color 0.25s, border-color 0.25s, transform 0.15s;
}
.copy-link:hover { background: var(--accent); color: var(--fg); border-color: var(--accent); }
.copy-link:active { transform: translateY(1px); }
.copy-link:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.35);
}
/* Devices section */
.devices-head {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 48px 0 24px;
}
.devices-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 28px;
letter-spacing: -0.01em;
}
.devices-count {
font-size: 11px;
letter-spacing: 0.26em;
text-transform: uppercase;
color: var(--muted);
}
.devices-count strong {
color: var(--fg);
font-weight: 400;
font-family: var(--font-display);
font-size: 16px;
margin-right: 4px;
letter-spacing: 0;
}
.device-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1px;
background: var(--hairline);
border: 1px solid var(--hairline);
border-radius: var(--radius);
overflow: hidden;
}
.device {
background: var(--surface);
padding: 28px 28px 24px;
display: flex;
align-items: flex-start;
gap: 22px;
position: relative;
transition: background 0.3s;
}
.device:hover { background: color-mix(in srgb, var(--surface) 80%, var(--fg) 4%); }
.device-glyph {
flex: 0 0 auto;
width: 48px; height: 48px;
border: 1px solid var(--hairline-strong);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
color: var(--fg);
background: var(--bg);
}
.device-glyph svg { width: 22px; height: 22px; }
.device-meta {
flex: 1 1 auto;
min-width: 0;
}
.device-model {
font-family: var(--font-display);
font-weight: 500;
font-size: 20px;
letter-spacing: -0.01em;
line-height: 1.2;
color: var(--fg);
margin-bottom: 6px;
}
.device-tags {
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
.device-tags .tag-client {
color: var(--fg);
}
.device-tags .sep {
width: 1px;
background: var(--hairline-strong);
align-self: stretch;
}
.device-delete {
flex: 0 0 auto;
appearance: none;
background: transparent;
border: 1px solid var(--hairline-strong);
color: var(--muted);
width: 36px; height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.25s, color 0.25s, background 0.25s, transform 0.25s;
}
.device-delete:hover {
border-color: var(--fg);
color: var(--fg);
background: var(--bg);
}
.device-delete:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.35);
border-color: var(--accent);
}
.device-delete svg { width: 14px; height: 14px; }
/* Empty devices state */
.devices-empty {
border: 1px solid var(--hairline);
border-radius: var(--radius);
background: var(--surface);
padding: 72px 24px;
text-align: center;
}
.devices-empty-glyph {
width: 44px; height: 44px;
margin: 0 auto 20px;
color: var(--muted);
opacity: 0.55;
}
.devices-empty-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 22px;
color: var(--fg);
margin-bottom: 8px;
}
.devices-empty-sub {
font-size: 13px;
color: var(--muted);
letter-spacing: 0.02em;
}
/* Footer rule */
.foot {
margin-top: 64px;
padding-top: 28px;
border-top: 1px solid var(--hairline);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
}
/* Verification modal */
.modal-veil {
position: fixed;
inset: 0;
background: rgba(26, 22, 16, 0.32);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
padding: 24px;
}
.modal-veil.is-open {
opacity: 1;
pointer-events: auto;
}
.modal {
background: var(--bg);
border: 1px solid var(--hairline-strong);
border-radius: var(--radius);
max-width: 420px;
width: 100%;
padding: 36px 32px 28px;
text-align: center;
transform: translateY(8px);
transition: transform 0.3s cubic-bezier(.2,.7,.2,1);
box-shadow: 0 24px 60px -20px rgba(26, 22, 16, 0.35);
}
.modal-veil.is-open .modal { transform: translateY(0); }
.modal-glyph {
width: 52px; height: 52px;
margin: 0 auto 20px;
border: 1px solid var(--accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent);
}
.modal-glyph svg { width: 22px; height: 22px; }
.modal-kicker {
font-size: 10px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 10px;
}
.modal-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 24px;
line-height: 1.25;
color: var(--fg);
margin-bottom: 12px;
}
.modal-body {
font-size: 14px;
color: var(--muted);
line-height: 1.55;
margin-bottom: 26px;
}
.modal-body strong {
color: var(--fg);
font-weight: 400;
font-family: var(--font-display);
font-size: 16px;
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: center;
}
.btn {
appearance: none;
font-family: var(--font-body);
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
padding: 13px 22px;
border-radius: var(--radius);
border: 1px solid var(--hairline-strong);
background: transparent;
color: var(--fg);
transition: background 0.25s, color 0.25s, border-color 0.25s, transform 0.15s;
min-height: 44px;
}
.btn:hover { background: var(--surface); }
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.35);
border-color: var(--accent);
}
.btn-primary {
background: var(--fg);
color: var(--bg);
border-color: var(--fg);
}
.btn-primary:hover {
background: var(--accent);
color: var(--fg);
border-color: var(--accent);
}
.btn-danger:hover {
background: var(--fg);
color: var(--bg);
border-color: var(--fg);
}
.btn:active { transform: translateY(1px); }
/* Fade-up */
.reveal {
opacity: 0;
transform: translateY(14px);
transition: opacity 0.7s cubic-bezier(.2,.7,.2,1), transform 0.7s cubic-bezier(.2,.7,.2,1);
}
.reveal.is-in { opacity: 1; transform: translateY(0); }
/* Toast */
.toast {
position: fixed;
bottom: 32px;
left: 50%;
transform: translate(-50%, 16px);
background: var(--fg);
color: var(--bg);
padding: 13px 22px;
border-radius: var(--radius);
font-size: 12px;
letter-spacing: 0.16em;
text-transform: uppercase;
z-index: 10001;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s, transform 0.3s;
box-shadow: 0 12px 30px -10px rgba(26, 22, 16, 0.4);
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 860px) {
.profile { grid-template-columns: repeat(2, 1fr); }
.stat:nth-child(2) { border-right: none; }
.stat:nth-child(1), .stat:nth-child(2) { border-bottom: 1px solid var(--hairline); }
.device-grid { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
.shell { padding: 40px 20px 64px; }
.topbar { padding-bottom: 24px; }
.nav { gap: 18px; }
.nav a { font-size: 10px; }
.hero { padding: 40px 0 36px; }
.hero-title { font-size: 34px; }
.profile { grid-template-columns: 1fr; }
.stat { border-right: none; border-bottom: 1px solid var(--hairline); min-height: 132px; padding: 32px 20px; }
.stat:last-child { border-bottom: none; }
.subscription-link { grid-template-columns: auto minmax(0, 1fr); gap: 16px; padding: 20px; }
.subscription-url { font-size: 12px; }
.copy-link { grid-column: 1 / -1; width: 100%; }
.devices-head { padding: 40px 0 20px; }
.device { padding: 22px 20px; gap: 16px; }
.device-model { font-size: 18px; }
.device-delete { width: 34px; height: 34px; }
.foot { flex-direction: column; gap: 10px; text-align: center; }
.modal { padding: 28px 24px 24px; }
.modal-title { font-size: 21px; }
}
</style>
</head>
<body>
<div class="cursor-ring" aria-hidden="true"></div>
<div class="cursor-dot" aria-hidden="true"></div>
<main class="shell">
<!-- Topbar -->
<header class="topbar" data-od-id="topbar">
<div class="logo" data-od-id="brand">Mal<span class="e">e</span>nia</div>
<nav class="nav" data-od-id="primary-nav">
<a href="#" class="is-active">Account</a>
<a href="#">Devices</a>
<a href="#">Billing</a>
<a href="#">Support</a>
</nav>
</header>
<!-- Hero identity -->
<section class="hero reveal" data-od-id="account-hero">
<div class="kicker">Your account</div>
<h1 class="hero-title">Eleanor Ashworth</h1>
<p class="hero-sub">Member since March 2023 · Plan: Solitude</p>
</section>
<!-- Profile stat strip -->
<section class="profile reveal" data-od-id="profile-stats" aria-label="Account summary">
<!-- Username -->
<div class="stat" data-od-id="stat-username">
<div class="stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" width="100%" height="100%">
<circle cx="12" cy="8" r="4"/>
<path d="M4 20c0-3.6 3.6-6 8-6s8 2.4 8 6"/>
</svg>
</div>
<div class="stat-label">Username</div>
<div class="stat-value">e.ashworth</div>
</div>
<!-- Telegram ID -->
<div class="stat" data-od-id="stat-telegram">
<div class="stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" width="100%" height="100%">
<path d="M21 4L3 11l5 2 2 6 3-4 4 3 4-14z"/>
<path d="M8 13l8-6"/>
</svg>
</div>
<div class="stat-label">Telegram ID</div>
<div class="stat-value is-mono">@eashworth</div>
</div>
<!-- Device count -->
<div class="stat" data-od-id="stat-device-count">
<div class="stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" width="100%" height="100%">
<rect x="3" y="4" width="18" height="12" rx="1.5"/>
<path d="M9 20h6M12 16v4"/>
</svg>
</div>
<div class="stat-label">Devices linked</div>
<div class="stat-value"><span id="device-count">4</span> <span class="accent">/</span> <span style="font-family:var(--font-body); font-size:14px; color:var(--muted); letter-spacing:0.04em">of 6</span></div>
</div>
<!-- Subscription expiry -->
<div class="stat" data-od-id="stat-expiry">
<div class="stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" width="100%" height="100%">
<rect x="3" y="5" width="18" height="16" rx="1.5"/>
<path d="M3 9h18M8 3v4M16 3v4"/>
</svg>
</div>
<div class="stat-label">Renews</div>
<div class="stat-value">14 Mar 2026</div>
</div>
</section>
<section class="subscription-link reveal" data-od-id="subscription-link" aria-label="Subscription link">
<div class="subscription-emblem" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.1.1l2-2a5 5 0 0 0-7.1-7.1l-1.1 1.1"/>
<path d="M14 11a5 5 0 0 0-7.1-.1l-2 2A5 5 0 0 0 12 20l1.1-1.1"/>
</svg>
</div>
<div class="subscription-copy">
<div class="subscription-label">Subscription link</div>
<span class="subscription-url" id="subscription-url">https://malenia.network/sub/<strong>mle_7f2a9c</strong></span>
</div>
<button class="copy-link" id="copy-subscription-link" data-od-id="copy-subscription-link" data-cursor type="button">Copy link</button>
</section>
<!-- Devices -->
<section data-od-id="devices-section">
<div class="devices-head reveal">
<h2 class="devices-title">Connected devices</h2>
<div class="devices-count" data-od-id="devices-count"><strong id="devices-count-num">4</strong>active</div>
</div>
<div class="device-grid reveal" data-od-id="device-grid" id="device-grid">
<!-- devices injected by JS -->
</div>
</section>
<!-- Footer -->
<footer class="foot" data-od-id="footer">
<span>MaleniaVPN · Solitude plan</span>
<span>Sign out</span>
</footer>
</main>
<!-- Verification modal -->
<div class="modal-veil" id="modal-veil" data-od-id="delete-verify-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal" role="document">
<div class="modal-glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/>
</svg>
</div>
<div class="modal-kicker">Confirm removal</div>
<h3 class="modal-title" id="modal-title">Remove this device?</h3>
<p class="modal-body">Disconnecting <strong id="modal-device-name">—</strong> will end its VPN session immediately. You can reconnect it later from the device.</p>
<div class="modal-actions">
<button class="btn" id="modal-cancel" data-od-id="modal-cancel">Keep it</button>
<button class="btn btn-danger" id="modal-confirm" data-od-id="modal-confirm">Remove</button>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
(function() {
// ----- Device data -----
const devices = [
{ id: 'd1', os: 'macos', model: 'MacBook Pro 14"', client: 'Malenia 2.4', joined: 'Joined 2d ago' },
{ id: 'd2', os: 'ios', model: 'iPhone 15 Pro', client: 'Malenia 2.4', joined: 'Joined 11d ago' },
{ id: 'd3', os: 'android', model: 'Pixel 8', client: 'OpenVPN', joined: 'Joined 28d ago' },
{ id: 'd4', os: 'windows', model: 'Surface Laptop 5', client: 'Malenia 2.4', joined: 'Joined 3mo ago' }
];
// ----- OS glyphs (monoline SVG) -----
const glyphs = {
macos: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="13" rx="1.5"/><path d="M1 20h22M9 20l1-3h4l1 3"/></svg>',
ios: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="2.5" width="10" height="19" rx="2"/><path d="M11 5.5h2"/></svg>',
android: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 14a7 7 0 0 1 14 0v5H5z"/><path d="M5 14L3 12M19 14l2-2M8 9l-1.5-2M16 9l1.5-2M9 8.5h6"/></svg>',
windows: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5l8-1v8H3zM12 4l9-1v10h-9zM3 13h8v6l-8-1zM12 13h9v8l-9-1z"/></svg>',
linux: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="13" rx="6" ry="7"/><path d="M9 9.5l1 1M15 9.5l-1 1M10 17h4"/></svg>'
};
const osLabels = { macos: 'macOS', ios: 'iOS', android: 'Android', windows: 'Windows', linux: 'Linux' };
// ----- Render -----
const grid = document.getElementById('device-grid');
function renderDevices() {
grid.innerHTML = '';
if (devices.length === 0) {
const empty = document.createElement('div');
empty.className = 'devices-empty';
empty.setAttribute('data-od-id', 'devices-empty');
empty.innerHTML = `
<div class="devices-empty-glyph">${glyphs.macos}</div>
<div class="devices-empty-title">No devices linked</div>
<div class="devices-empty-sub">Install MaleniaVPN on a device to connect it to your account.</div>
`;
grid.appendChild(empty);
} else {
devices.forEach((d, i) => {
const card = document.createElement('article');
card.className = 'device reveal';
card.setAttribute('data-od-id', 'device-' + d.id);
card.innerHTML = `
<div class="device-glyph" aria-hidden="true">${glyphs[d.os] || glyphs.linux}</div>
<div class="device-meta">
<h3 class="device-model">${d.model}</h3>
<div class="device-tags">
<span>${osLabels[d.os] || d.os}</span>
<span class="sep"></span>
<span class="tag-client">${d.client}</span>
<span class="sep"></span>
<span>${d.joined}</span>
</div>
</div>
<button class="device-delete" data-od-id="device-delete-${d.id}" data-cursor aria-label="Remove ${d.model}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>
</button>
`;
grid.appendChild(card);
requestAnimationFrame(() => {
setTimeout(() => card.classList.add('is-in'), 40 * i);
});
});
}
document.getElementById('device-count').textContent = devices.length;
document.getElementById('devices-count-num').textContent = devices.length;
bindDeleteButtons();
}
// ----- Delete flow -----
const veil = document.getElementById('modal-veil');
const modalName = document.getElementById('modal-device-name');
const cancelBtn = document.getElementById('modal-cancel');
const confirmBtn = document.getElementById('modal-confirm');
const toast = document.getElementById('toast');
let pendingId = null;
function bindDeleteButtons() {
grid.querySelectorAll('.device-delete').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-od-id').replace('device-delete-', '');
const d = devices.find(x => x.id === id);
if (!d) return;
pendingId = id;
modalName.textContent = d.model;
openModal();
});
});
}
function openModal() {
veil.classList.add('is-open');
veil.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
setTimeout(() => cancelBtn.focus(), 60);
}
function closeModal() {
veil.classList.remove('is-open');
veil.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
pendingId = null;
}
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('is-show');
setTimeout(() => toast.classList.remove('is-show'), 2200);
}
// ----- Subscription link -----
const subscriptionUrl = document.getElementById('subscription-url');
const copySubscriptionLink = document.getElementById('copy-subscription-link');
copySubscriptionLink.addEventListener('click', async () => {
const link = subscriptionUrl.textContent.trim();
try {
await navigator.clipboard.writeText(link);
showToast('Subscription link copied');
} catch (error) {
const fallback = document.createElement('textarea');
fallback.value = link;
fallback.setAttribute('readonly', '');
fallback.style.position = 'fixed';
fallback.style.opacity = '0';
document.body.appendChild(fallback);
fallback.select();
document.execCommand('copy');
fallback.remove();
showToast('Subscription link copied');
}
});
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', () => {
if (!pendingId) return;
const idx = devices.findIndex(x => x.id === pendingId);
if (idx > -1) {
const removed = devices.splice(idx, 1)[0];
renderDevices();
showToast(removed.model + ' disconnected');
}
closeModal();
});
veil.addEventListener('click', (e) => { if (e.target === veil) closeModal(); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && veil.classList.contains('is-open')) closeModal();
});
// ----- 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 * 80) + '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, [data-cursor]');
ring.classList.toggle('is-active', !!t);
});
} else {
dot.style.display = 'none'; ring.style.display = 'none';
}
// ----- Init -----
renderDevices();
})();
</script>
</body>
</html>

1212
.ref/calculator.html Normal file

File diff suppressed because it is too large Load Diff

467
.ref/landing.html Normal file
View File

@@ -0,0 +1,467 @@
<html lang="ru"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Malenia — Свобода в сети</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:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;family=Tenor+Sans&amp;display=swap" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<style>
:root {
--gold: #C9A84C;
--gold-light: #E8CA7A;
--gold-dim: #8B6914;
--cream: #FAF7F2;
--warm-white: #F5F0E8;
--ink: #1A1610;
--ink-soft: #3D3428;
--ink-muted: #7A6E5F;
--border: rgba(201,168,76,0.25);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { cursor: none; }
body {
font-family: 'Tenor Sans', sans-serif;
background: var(--cream);
color: var(--ink);
min-height: 100vh;
overflow-x: hidden;
cursor: none;
}
/* ── Custom cursor ─────────────────────────────────── */
#cursor-dot,
#cursor-ring {
position: fixed;
top: 0; left: 0;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: opacity .3s;
}
#cursor-dot {
width: 6px; height: 6px;
background: var(--gold);
border-radius: 50%;
}
#cursor-ring {
width: 32px; height: 32px;
border: 1px solid var(--gold);
border-radius: 50%;
transition: width .25s cubic-bezier(.4,0,.2,1),
height .25s cubic-bezier(.4,0,.2,1),
opacity .3s;
}
body.hovering #cursor-ring {
width: 48px; height: 48px;
border-color: var(--gold-light);
}
/* ── Noise overlay ─────────────────────────────────── */
body::before {
content: '';
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
opacity: .035;
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)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 180px;
}
/* ── Layout ────────────────────────────────────────── */
.page {
position: relative;
z-index: 1;
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
}
/* ── Header ────────────────────────────────────────── */
header {
padding: 2.5rem 4rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
}
.logo {
font-family: 'Cormorant Garamond', serif;
font-weight: 300;
font-size: 1.5rem;
letter-spacing: .35em;
text-transform: uppercase;
color: var(--ink);
text-decoration: none;
}
.logo span { color: var(--gold); }
.header-rule {
flex: 1;
height: 1px;
background: var(--border);
margin: 0 2rem;
}
.header-tag {
font-size: .65rem;
letter-spacing: .25em;
text-transform: uppercase;
color: var(--ink-muted);
}
/* ── Hero ──────────────────────────────────────────── */
.hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 6rem 2rem;
gap: 3.5rem;
}
.emblem {
position: relative;
width: 90px;
height: 90px;
opacity: 0;
animation: fadeUp .9s .1s forwards;
}
.emblem svg { width: 100%; height: 100%; }
.quote-block {
max-width: 640px;
opacity: 0;
animation: fadeUp .9s .25s forwards;
}
.quote-text {
font-family: 'Cormorant Garamond', serif;
font-size: clamp(2rem, 5vw, 3.4rem);
font-weight: 300;
line-height: 1.25;
color: var(--ink);
letter-spacing: .01em;
}
.quote-text em {
font-style: italic;
color: var(--gold-dim);
}
.quote-author {
margin-top: 1.25rem;
font-size: .7rem;
letter-spacing: .3em;
text-transform: uppercase;
color: var(--ink-muted);
}
/* ── Divider ───────────────────────────────────────── */
.divider {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
max-width: 480px;
opacity: 0;
animation: fadeUp .9s .4s forwards;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.divider-diamond {
width: 6px; height: 6px;
background: var(--gold);
transform: rotate(45deg);
flex-shrink: 0;
}
/* ── Buttons ───────────────────────────────────────── */
.cta-group {
display: flex;
gap: 1.25rem;
flex-wrap: wrap;
justify-content: center;
opacity: 0;
animation: fadeUp .9s .55s forwards;
}
.btn {
position: relative;
display: inline-flex;
align-items: center;
gap: .75rem;
padding: .85rem 2rem;
font-family: 'Tenor Sans', sans-serif;
font-size: .78rem;
letter-spacing: .18em;
text-transform: uppercase;
text-decoration: none;
border-radius: 0;
cursor: none;
transition: color .25s, background .25s, border-color .25s;
overflow: hidden;
}
.btn::after {
content: '';
position: absolute;
inset: 0;
background: var(--gold);
transform: scaleX(0);
transform-origin: left;
transition: transform .35s cubic-bezier(.4,0,.2,1);
z-index: 0;
}
.btn:hover::after { transform: scaleX(1); }
.btn > * { position: relative; z-index: 1; }
.btn-primary {
background: var(--ink);
color: var(--cream);
border: 1px solid var(--ink);
}
.btn-primary:hover { color: var(--ink); border-color: var(--gold); }
.btn-secondary {
background: transparent;
color: var(--ink);
border: 1px solid var(--gold);
}
.btn-secondary::after { background: var(--ink); }
.btn-secondary:hover { color: var(--cream); }
.btn-icon {
width: 18px; height: 18px;
fill: currentColor;
flex-shrink: 0;
}
/* ── Footer ────────────────────────────────────────── */
footer {
padding: 2rem 4rem;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.footer-copy {
font-size: .65rem;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--ink-muted);
}
.footer-dot {
width: 4px; height: 4px;
background: var(--gold);
border-radius: 50%;
}
/* ── Animations ────────────────────────────────────── */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Responsive ────────────────────────────────────── */
@media (max-width: 640px) {
header { padding: 1.75rem 1.5rem; }
.header-rule { display: none; }
.header-tag { display: none; }
.hero { padding: 4rem 1.5rem; gap: 2.5rem; }
footer { padding: 1.5rem; flex-direction: column; align-items: flex-start; }
.btn { padding: .8rem 1.5rem; font-size: .72rem; }
.cta-group { flex-direction: column; align-items: stretch; }
.btn { justify-content: center; }
}
</style>
</head>
<body>
<!-- Cursor -->
<div id="cursor-dot" style="left: 233px; top: 539px;"></div>
<div id="cursor-ring" style="left: 233px; top: 539px;"></div>
<div class="page">
<!-- Header -->
<header>
<a class="logo" href="#">Mal<span>e</span>nia</a>
<div class="header-rule"></div>
<span class="header-tag">Свобода. Практика. Простота.</span>
</header>
<!-- Hero -->
<main class="hero">
<!-- Emblem -->
<div class="emblem" aria-hidden="true">
<svg viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- outer ring -->
<circle cx="45" cy="45" r="42" stroke="#C9A84C" stroke-width=".8" stroke-dasharray="3 4"></circle>
<!-- inner ring -->
<circle cx="45" cy="45" r="32" stroke="#C9A84C" stroke-width=".5" opacity=".5"></circle>
<!-- diamond -->
<path d="M45 18 L72 45 L45 72 L18 45 Z" stroke="#C9A84C" stroke-width=".9" fill="none"></path>
<!-- center dot -->
<circle cx="45" cy="45" r="3.5" fill="#C9A84C"></circle>
<!-- cardinal marks -->
<line x1="45" y1="3" x2="45" y2="12" stroke="#C9A84C" stroke-width=".8"></line>
<line x1="45" y1="78" x2="45" y2="87" stroke="#C9A84C" stroke-width=".8"></line>
<line x1="3" y1="45" x2="12" y2="45" stroke="#C9A84C" stroke-width=".8"></line>
<line x1="78" y1="45" x2="87" y2="45" stroke="#C9A84C" stroke-width=".8"></line>
</svg>
</div>
<!-- Quote -->
<div class="quote-block">
<p class="quote-text" id="quote-text">Верните себе<br><em>свободный интернет.</em></p>
<p class="quote-author">malenia.space</p>
</div>
<!-- Divider -->
<div class="divider" aria-hidden="true">
<div class="divider-diamond"></div>
</div>
<!-- CTA Buttons -->
<div class="cta-group">
<a class="btn btn-primary" href="https://t.me/maleniavpn" target="_blank" rel="noopener">
<!-- Telegram icon -->
<svg class="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.265 2.428a1.523 1.523 0 0 0-1.547-.232L1.91 9.9a1.523 1.523 0 0 0 .089 2.844l4.498 1.498 1.815 5.784a.761.761 0 0 0 1.295.3l2.617-2.818 4.898 3.674a1.523 1.523 0 0 0 2.389-.98l3.047-16.762a1.523 1.523 0 0 0-.293-1.012zM9.94 13.88l-.98 3.267-.98-3.143L18.5 6.04z"></path>
</svg>
<span>Канал</span>
</a>
<a class="btn btn-secondary" href="https://t.me/maleniasupportbot" target="_blank" rel="noopener">
<!-- Bot icon -->
<svg class="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2a2 2 0 0 1 2 2 2 2 0 0 1-1 1.732V7h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h3V5.732A2 2 0 0 1 10 4a2 2 0 0 1 2-2zm-3 9a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm6 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"></path>
</svg>
<span>Поддержка</span>
</a>
</div>
</main>
<!-- Footer -->
<footer>
<span class="footer-copy">© 2026 Malenia</span>
<div class="footer-dot"></div>
<span class="footer-copy">malenia.space</span>
</footer>
</div>
<script>
/* ── Quotes ─────────────────────────────────────────── */
const quotes = [
"Верните себе\nсвободный интернет.",
"Свобода должна\nбыть доступной.",
"Интернет без границ —\nне привилегия, а норма.",
"Ваш трафик.\nВаши правила.",
"Тишина в сети —\nэто тоже выбор.",
"Открытый мир\nначинается здесь.",
"Без цензуры.\nБез компромиссов.",
"Простота —\nлучшая форма свободы.",
"Один клик\nмежду вами и миром.",
"Malenia —\nсвобода в форме привычки.",
"Будьте везде.\nОставайтесь невидимым.",
"Приватность — это право,\nа не роскошь.",
"Интернет таким,\nкаким он задумывался.",
"Ваши данные —\nтолько ваше дело.",
"Закрытых дверей\nбольше не существует.",
"Любой сайт.\nИз любой точки мира.",
"Оставьте след в истории,\nа не в браузере.",
"Malenia —\nключ к открытой сети.",
"Фильтры придумали они.\nРешение выбрали вы.",
"Безопасность, которую\nвы не замечаете.",
"Сотрите цифровые границы.\nОстаньтесь собой.",
"Скорость без компромиссов.\nСеть без преград.",
"Включили и забыли.\nВы в безопасности.",
"Блокировки иллюзорны.\nСвобода реальна.",
"Никакой слежки.\nТолько вы и интернет.",
"Доступ к информации —\nваше базовое право.",
"Malenia.\nИнтернет снова ваш.",
"Контроль над сетью\nв ваших руках.",
"Невидимый щит\nдля вашей свободы.",
"Весь мир на экране.\nБез лишних вопросов.",
"Никогда не поздно\nвыбрать свободу.",
"Привычка к запретам —\nповод проснуться.",
"Откройте глаза.\nСтен больше нет.",
"Вспомните, как выглядит\nнастоящая сеть.",
"Запреты стали нормой.\nПора это менять.",
"Свобода начинается\nс вашего решения.",
"Выход из спящего режима.\nСделайте первый шаг.",
"Лучший момент для\nсвободы — сейчас.",
"Иллюзия границ рушится\nв один клик.",
"Хватит мириться с рамками.\nМир шире, чем кажется.",
"Не ждите свободы.\nВключите её.",
"Цифровая амнезия окончена.\nВерните своё.",
"Осознанность начинается\nтам, где кончается цензура.",
"Первый шаг к свободе —\nперестать верить в стены.",
"Malenia.\nВаше цифровое пробуждение."
];
function renderQuote(raw) {
// italic the second line if two lines exist
const lines = raw.split('\n');
if (lines.length === 2) {
return `${lines[0]}<br><em>${lines[1]}</em>`;
}
return raw;
}
const el = document.getElementById('quote-text');
const chosen = quotes[Math.floor(Math.random() * quotes.length)];
el.innerHTML = renderQuote(chosen);
/* ── Custom cursor ──────────────────────────────────── */
const dot = document.getElementById('cursor-dot');
const ring = document.getElementById('cursor-ring');
let mx = -100, my = -100, rx = -100, ry = -100;
document.addEventListener('mousemove', e => { mx = e.clientX; my = e.clientY; });
(function loop() {
rx += (mx - rx) * .12;
ry += (my - ry) * .12;
dot.style.left = mx + 'px';
dot.style.top = my + 'px';
ring.style.left = rx + 'px';
ring.style.top = ry + 'px';
requestAnimationFrame(loop);
})();
document.querySelectorAll('a, button').forEach(el => {
el.addEventListener('mouseenter', () => document.body.classList.add('hovering'));
el.addEventListener('mouseleave', () => document.body.classList.remove('hovering'));
});
document.addEventListener('mouseleave', () => {
dot.style.opacity = '0';
ring.style.opacity = '0';
});
document.addEventListener('mouseenter', () => {
dot.style.opacity = '1';
ring.style.opacity = '1';
});
</script>
</body></html>

561
.ref/login.html Normal file
View File

@@ -0,0 +1,561 @@
<!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>