922 lines
29 KiB
HTML
922 lines
29 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 · 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>
|