1212 lines
35 KiB
HTML
1212 lines
35 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 · Compose your plan</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;700&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 */
|
||
.hero {
|
||
padding: 64px 0 40px;
|
||
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: 22px;
|
||
}
|
||
.hero-title {
|
||
font-family: var(--font-display);
|
||
font-weight: 500;
|
||
font-size: 52px;
|
||
line-height: 1.12;
|
||
letter-spacing: -0.02em;
|
||
color: var(--fg);
|
||
}
|
||
.hero-sub {
|
||
margin: 18px auto 0;
|
||
font-size: 14px;
|
||
color: var(--muted);
|
||
letter-spacing: 0.02em;
|
||
max-width: 46ch;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
/* Calculator block */
|
||
.calc {
|
||
padding: 72px 0 64px;
|
||
}
|
||
.calc-stage {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 56px;
|
||
align-items: center;
|
||
}
|
||
.calc-readout {
|
||
text-align: center;
|
||
}
|
||
.readout-label {
|
||
font-size: 11px;
|
||
letter-spacing: 0.28em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
margin-bottom: 18px;
|
||
}
|
||
.readout-value {
|
||
font-family: var(--font-display);
|
||
font-weight: 500;
|
||
font-size: 96px;
|
||
line-height: 1;
|
||
letter-spacing: -0.03em;
|
||
color: var(--fg);
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 4px;
|
||
}
|
||
.readout-value .unit {
|
||
font-family: var(--font-body);
|
||
font-size: 16px;
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
margin-left: 14px;
|
||
align-self: center;
|
||
}
|
||
.readout-note {
|
||
margin-top: 16px;
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
/* Slider — the hero accent */
|
||
.slider-wrap {
|
||
padding: 0 8px;
|
||
user-select: none;
|
||
}
|
||
.slider-axis {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: baseline;
|
||
font-size: 11px;
|
||
letter-spacing: 0.22em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
margin-bottom: 22px;
|
||
}
|
||
.slider-axis .axis-now {
|
||
color: var(--fg);
|
||
font-family: var(--font-display);
|
||
font-size: 18px;
|
||
letter-spacing: 0;
|
||
}
|
||
.slider-track-frame {
|
||
position: relative;
|
||
height: 88px;
|
||
padding: 0 4px;
|
||
}
|
||
.slider-track {
|
||
position: absolute;
|
||
left: 4px; right: 4px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
height: 1px;
|
||
background: var(--hairline-strong);
|
||
}
|
||
.slider-fill {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
height: 100%;
|
||
background: var(--fg);
|
||
transition: width 0.12s cubic-bezier(.2,.7,.2,1);
|
||
}
|
||
.slider-ticks {
|
||
position: absolute;
|
||
left: 4px; right: 4px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
pointer-events: none;
|
||
}
|
||
.tick {
|
||
width: 1px;
|
||
height: 8px;
|
||
background: var(--hairline-strong);
|
||
transform: translateY(-50%);
|
||
position: relative;
|
||
}
|
||
.tick.is-major {
|
||
height: 14px;
|
||
background: var(--hairline-strong);
|
||
}
|
||
.tick-label {
|
||
position: absolute;
|
||
top: 14px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-size: 10px;
|
||
letter-spacing: 0.16em;
|
||
color: var(--muted);
|
||
opacity: 0.6;
|
||
font-family: var(--font-body);
|
||
}
|
||
.tick.is-major .tick-label { opacity: 0.9; }
|
||
.tick.is-active .tick-label {
|
||
color: var(--fg);
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Rhombus pointer */
|
||
.pointer {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
width: 26px;
|
||
height: 26px;
|
||
transform: translate(-50%, -50%) rotate(45deg);
|
||
background: var(--accent);
|
||
border: 1px solid var(--accent);
|
||
box-shadow: 0 6px 16px -4px rgba(201, 168, 76, 0.55), 0 0 0 6px rgba(201, 168, 76, 0.10);
|
||
transition: left 0.12s cubic-bezier(.2,.7,.2,1), box-shadow 0.25s, background 0.25s, border-color 0.25s;
|
||
z-index: 3;
|
||
}
|
||
.pointer::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 5px;
|
||
border: 1px solid rgba(250, 247, 242, 0.7);
|
||
}
|
||
.slider-wrap.is-dragging .pointer {
|
||
box-shadow: 0 10px 24px -4px rgba(201, 168, 76, 0.7), 0 0 0 10px rgba(201, 168, 76, 0.14);
|
||
background: var(--fg);
|
||
border-color: var(--fg);
|
||
}
|
||
.slider-wrap.is-dragging .pointer::before {
|
||
border-color: rgba(250, 247, 242, 0.85);
|
||
}
|
||
/* Pointer halo ring (decorative, pulses on drag) */
|
||
.pointer-halo {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
width: 46px;
|
||
height: 46px;
|
||
transform: translate(-50%, -50%);
|
||
border: 1px solid var(--accent);
|
||
border-radius: 50%;
|
||
opacity: 0;
|
||
transition: left 0.12s cubic-bezier(.2,.7,.2,1), opacity 0.3s;
|
||
pointer-events: none;
|
||
z-index: 2;
|
||
}
|
||
.slider-wrap.is-dragging .pointer-halo { opacity: 0.5; }
|
||
|
||
.slider-hit {
|
||
position: absolute;
|
||
left: 0; right: 0;
|
||
top: 0; bottom: 0;
|
||
touch-action: none;
|
||
}
|
||
|
||
.slider-caption {
|
||
margin-top: 18px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 11px;
|
||
letter-spacing: 0.22em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
}
|
||
.slider-caption .hint {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.slider-caption .hint svg { width: 14px; height: 14px; opacity: 0.7; }
|
||
|
||
/* Duration selector */
|
||
.duration {
|
||
margin-top: 56px;
|
||
}
|
||
.duration-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: baseline;
|
||
margin-bottom: 18px;
|
||
padding: 0 4px;
|
||
}
|
||
.duration-label {
|
||
font-size: 11px;
|
||
letter-spacing: 0.24em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
}
|
||
.duration-cap {
|
||
font-size: 11px;
|
||
letter-spacing: 0.22em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
}
|
||
.duration-cap .save { color: var(--accent); font-family: var(--font-display); font-size: 14px; letter-spacing: 0; margin-left: 6px; }
|
||
.duration-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 1px;
|
||
background: var(--hairline);
|
||
border: 1px solid var(--hairline);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
.dur {
|
||
appearance: none;
|
||
font: inherit;
|
||
text-align: center;
|
||
padding: 22px 10px 18px;
|
||
background: var(--surface);
|
||
border: none;
|
||
color: var(--fg);
|
||
cursor: pointer;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 5px;
|
||
position: relative;
|
||
transition: background 0.25s, color 0.25s;
|
||
-webkit-tap-highlight-color: transparent;
|
||
min-height: 44px;
|
||
}
|
||
.dur:focus-visible {
|
||
outline: none;
|
||
box-shadow: inset 0 0 0 2px var(--accent);
|
||
z-index: 1;
|
||
}
|
||
.dur:hover { background: color-mix(in srgb, var(--surface) 84%, var(--fg) 4%); }
|
||
.dur.is-active {
|
||
background: var(--fg);
|
||
color: var(--bg);
|
||
}
|
||
.dur-num {
|
||
font-family: var(--font-display);
|
||
font-weight: 500;
|
||
font-size: 30px;
|
||
letter-spacing: -0.02em;
|
||
line-height: 1;
|
||
}
|
||
.dur-unit {
|
||
font-size: 10px;
|
||
letter-spacing: 0.22em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
}
|
||
.dur.is-active .dur-unit { color: rgba(250, 247, 242, 0.62); }
|
||
.dur-tag {
|
||
font-size: 10px;
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
min-height: 12px;
|
||
margin-top: 2px;
|
||
}
|
||
.dur.is-active .dur-tag { color: var(--accent); }
|
||
|
||
/* Breakdown */
|
||
.breakdown {
|
||
margin-top: 64px;
|
||
border-top: 1px solid var(--hairline);
|
||
}
|
||
.breakdown-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
align-items: baseline;
|
||
gap: 24px;
|
||
padding: 22px 4px;
|
||
border-bottom: 1px solid var(--hairline);
|
||
}
|
||
.breakdown-row:last-child { border-bottom: none; }
|
||
.breakdown-key {
|
||
font-size: 11px;
|
||
letter-spacing: 0.24em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
}
|
||
.breakdown-key em {
|
||
font-style: normal;
|
||
color: var(--fg);
|
||
font-family: var(--font-display);
|
||
font-size: 14px;
|
||
letter-spacing: 0;
|
||
margin-left: 8px;
|
||
}
|
||
.breakdown-val {
|
||
font-family: var(--font-display);
|
||
font-weight: 500;
|
||
font-size: 22px;
|
||
letter-spacing: -0.01em;
|
||
color: var(--fg);
|
||
}
|
||
.breakdown-val .muted {
|
||
font-family: var(--font-body);
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
letter-spacing: 0.04em;
|
||
margin-left: 6px;
|
||
}
|
||
.breakdown-total {
|
||
padding: 26px 4px 24px;
|
||
}
|
||
.breakdown-total .breakdown-key {
|
||
color: var(--fg);
|
||
font-size: 12px;
|
||
letter-spacing: 0.28em;
|
||
}
|
||
.breakdown-total .breakdown-val {
|
||
font-size: 38px;
|
||
}
|
||
.breakdown-total .breakdown-val .per {
|
||
font-family: var(--font-body);
|
||
font-size: 13px;
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
margin-left: 10px;
|
||
}
|
||
.breakdown-billed {
|
||
padding: 18px 4px 22px;
|
||
}
|
||
.breakdown-billed .breakdown-key { color: var(--muted); }
|
||
.breakdown-billed .breakdown-val {
|
||
font-size: 18px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* Included strip */
|
||
.included {
|
||
margin-top: 56px;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 1px;
|
||
background: var(--hairline);
|
||
border: 1px solid var(--hairline);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
.incl-cell {
|
||
background: var(--surface);
|
||
padding: 28px 24px 26px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
align-items: flex-start;
|
||
transition: background 0.3s;
|
||
}
|
||
.incl-cell:hover { background: color-mix(in srgb, var(--surface) 80%, var(--fg) 4%); }
|
||
.incl-glyph {
|
||
width: 32px; height: 32px;
|
||
color: var(--muted);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.incl-cell:hover .incl-glyph { color: var(--accent); }
|
||
.incl-glyph svg { width: 100%; height: 100%; }
|
||
.incl-title {
|
||
font-family: var(--font-display);
|
||
font-weight: 500;
|
||
font-size: 19px;
|
||
letter-spacing: -0.01em;
|
||
color: var(--fg);
|
||
}
|
||
.incl-sub {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
letter-spacing: 0.02em;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
/* Addons */
|
||
.addons {
|
||
margin-top: 64px;
|
||
}
|
||
.addons-head {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
padding: 26px 4px 18px;
|
||
border-top: 1px solid var(--hairline);
|
||
}
|
||
.addons-title {
|
||
font-family: var(--font-display);
|
||
font-weight: 500;
|
||
font-size: 24px;
|
||
letter-spacing: -0.01em;
|
||
color: var(--fg);
|
||
}
|
||
.addons-cap {
|
||
font-size: 11px;
|
||
letter-spacing: 0.24em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
}
|
||
.addon-list {
|
||
display: grid;
|
||
gap: 1px;
|
||
background: var(--hairline);
|
||
border: 1px solid var(--hairline);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
.addon-row {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto;
|
||
align-items: center;
|
||
gap: 20px;
|
||
width: 100%;
|
||
padding: 22px 24px;
|
||
background: var(--surface);
|
||
border: none;
|
||
font: inherit;
|
||
text-align: left;
|
||
color: var(--fg);
|
||
cursor: pointer;
|
||
transition: background 0.3s;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
.addon-row:focus-visible {
|
||
outline: none;
|
||
box-shadow: inset 0 0 0 2px var(--accent);
|
||
}
|
||
.addon-row:hover { background: color-mix(in srgb, var(--surface) 82%, var(--fg) 4%); }
|
||
.addon-row.is-selected { background: color-mix(in srgb, var(--surface) 78%, var(--accent) 10%); }
|
||
.addon-toggle {
|
||
width: 18px; height: 18px;
|
||
border: 1px solid var(--hairline-strong);
|
||
border-radius: 3px;
|
||
transform: rotate(45deg);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: background 0.25s, border-color 0.25s;
|
||
position: relative;
|
||
}
|
||
.addon-row.is-selected .addon-toggle {
|
||
background: var(--accent);
|
||
border-color: var(--accent);
|
||
}
|
||
.addon-toggle::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 3px;
|
||
border: 1px solid rgba(250, 247, 242, 0.75);
|
||
opacity: 0;
|
||
}
|
||
.addon-row.is-selected .addon-toggle::after { opacity: 1; }
|
||
.addon-info { display: flex; flex-direction: column; gap: 4px; }
|
||
.addon-name {
|
||
font-family: var(--font-display);
|
||
font-weight: 500;
|
||
font-size: 18px;
|
||
letter-spacing: -0.01em;
|
||
color: var(--fg);
|
||
}
|
||
.addon-desc {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
letter-spacing: 0.02em;
|
||
line-height: 1.5;
|
||
}
|
||
.addon-price {
|
||
font-family: var(--font-display);
|
||
font-weight: 500;
|
||
font-size: 18px;
|
||
letter-spacing: -0.01em;
|
||
color: var(--fg);
|
||
white-space: nowrap;
|
||
}
|
||
.addon-price .plus { color: var(--muted); font-family: var(--font-body); font-size: 12px; margin-right: 6px; letter-spacing: 0.04em; }
|
||
|
||
/* CTA */
|
||
.cta {
|
||
margin-top: 56px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 16px;
|
||
text-align: center;
|
||
}
|
||
.btn {
|
||
appearance: none;
|
||
font-family: var(--font-body);
|
||
font-size: 12px;
|
||
letter-spacing: 0.22em;
|
||
text-transform: uppercase;
|
||
padding: 16px 38px;
|
||
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-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:focus-visible {
|
||
outline: none;
|
||
box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.35);
|
||
border-color: var(--accent);
|
||
}
|
||
.btn:active { transform: translateY(1px); }
|
||
.cta-note {
|
||
font-size: 11px;
|
||
letter-spacing: 0.22em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
|
||
/* Reveal */
|
||
.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); }
|
||
|
||
/* Responsive */
|
||
@media (max-width: 860px) {
|
||
.included { grid-template-columns: 1fr; }
|
||
.readout-value { font-size: 76px; }
|
||
.hero-title { font-size: 42px; }
|
||
}
|
||
@media (max-width: 560px) {
|
||
.shell { padding: 40px 20px 64px; }
|
||
.topbar { padding-bottom: 24px; }
|
||
.nav { gap: 18px; }
|
||
.nav a { font-size: 10px; }
|
||
.hero { padding: 44px 0 32px; }
|
||
.hero-title { font-size: 34px; }
|
||
.hero-sub { font-size: 13px; }
|
||
.calc { padding: 48px 0 40px; }
|
||
.readout-value { font-size: 60px; }
|
||
.readout-value .unit { font-size: 13px; margin-left: 10px; }
|
||
.breakdown-total .breakdown-val { font-size: 30px; }
|
||
.duration-grid { grid-template-columns: repeat(2, 1fr); }
|
||
.dur-num { font-size: 26px; }
|
||
.foot { flex-direction: column; gap: 10px; text-align: center; }
|
||
.tick-label { font-size: 9px; }
|
||
.cta .btn { width: 100%; }
|
||
.addon-row { grid-template-columns: auto 1fr auto; gap: 14px; padding: 18px 18px; }
|
||
.addon-name { font-size: 16px; }
|
||
@media (max-width: 400px) {
|
||
.addon-row { grid-template-columns: auto 1fr; }
|
||
.addon-price { grid-column: 2; }
|
||
}
|
||
}
|
||
</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="#">Account</a>
|
||
<a href="#" class="is-active">Plan</a>
|
||
<a href="#">Devices</a>
|
||
<a href="#">Support</a>
|
||
</nav>
|
||
</header>
|
||
|
||
<!-- Hero -->
|
||
<section class="hero reveal" data-od-id="calc-hero">
|
||
<div class="kicker">Compose your plan</div>
|
||
<h1 class="hero-title">A circle, sized to you.</h1>
|
||
<p class="hero-sub">Choose how many devices share your freedom. The price scales quietly — one rate per seat, no tiers, no surcharges.</p>
|
||
</section>
|
||
|
||
<!-- Calculator -->
|
||
<section class="calc" data-od-id="calculator">
|
||
<div class="calc-stage reveal">
|
||
<!-- Live readout -->
|
||
<div class="calc-readout" data-od-id="calc-readout">
|
||
<div class="readout-label">Estimated price · monthly</div>
|
||
<div class="readout-value">
|
||
<span id="price-value">$18</span>
|
||
<span class="unit">USD / mo</span>
|
||
</div>
|
||
<div class="readout-note" id="readout-note">For 6 devices · $3.00 per seat</div>
|
||
</div>
|
||
|
||
<!-- Slider -->
|
||
<div class="slider-wrap" id="slider-wrap" data-od-id="device-slider" data-cursor="">
|
||
<div class="slider-axis">
|
||
<span>Devices</span>
|
||
<span><span class="axis-now" id="axis-now">6</span> seats</span>
|
||
</div>
|
||
<div class="slider-track-frame" id="track-frame">
|
||
<div class="slider-track">
|
||
<div class="slider-fill" id="slider-fill"></div>
|
||
</div>
|
||
<div class="slider-ticks" id="slider-ticks" aria-hidden="true"></div>
|
||
<div class="pointer-halo" id="pointer-halo" aria-hidden="true"></div>
|
||
<div class="pointer" id="pointer" role="slider" tabindex="0" aria-label="Number of devices" aria-valuemin="3" aria-valuemax="12" aria-valuenow="6" aria-valuetext="6 devices"></div>
|
||
<div class="slider-hit" id="slider-hit"></div>
|
||
</div>
|
||
<div class="slider-caption">
|
||
<span class="hint">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 12h8M12 8l-4 4 4 4M12 8l4 4-4 4"></path></svg>
|
||
Drag the rhombus · arrow keys to fine-tune
|
||
</span>
|
||
<span>Range 3 – 12</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Duration -->
|
||
<div class="duration reveal" data-od-id="duration-selector">
|
||
<div class="duration-head">
|
||
<span class="duration-label">Commitment</span>
|
||
<span class="duration-cap" id="duration-cap">Billed monthly</span>
|
||
</div>
|
||
<div class="duration-grid" id="duration-grid" role="radiogroup" aria-label="Subscription duration"></div>
|
||
</div>
|
||
|
||
<!-- Breakdown -->
|
||
<div class="breakdown reveal" data-od-id="price-breakdown">
|
||
<div class="breakdown-row">
|
||
<div class="breakdown-key">Devices <em id="bd-devices">6</em></div>
|
||
<div class="breakdown-val"><span id="bd-per-seat">$3.00</span><span class="muted">/ seat</span></div>
|
||
</div>
|
||
<div class="breakdown-row" id="bd-addons-row" hidden="">
|
||
<div class="breakdown-key">Add-ons <em id="bd-addons-count">0</em></div>
|
||
<div class="breakdown-val"><span id="bd-addons-sum">$0</span><span class="muted">/ month</span></div>
|
||
</div>
|
||
<div class="breakdown-row" id="bd-commit-row" hidden="">
|
||
<div class="breakdown-key">Commitment <em id="bd-months">1</em></div>
|
||
<div class="breakdown-val"><span id="bd-discount">$0</span><span class="muted">saved / month</span></div>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Addons -->
|
||
<div class="addons reveal" data-od-id="addons" id="addons-section">
|
||
<div class="addons-head">
|
||
<div class="addons-title">Refine your circle</div>
|
||
<div class="addons-cap">Optional add-ons · placeholder</div>
|
||
</div>
|
||
<div class="addon-list" id="addon-list"></div>
|
||
</div>
|
||
|
||
<!-- CTA -->
|
||
<div class="cta reveal" data-od-id="plan-cta">
|
||
<button class="btn btn-primary" data-od-id="confirm-plan">Confirm plan</button>
|
||
<div class="cta-note">Plan adjusts at next renewal · No lock-in</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Footer -->
|
||
<footer class="foot" data-od-id="footer">
|
||
<span>MaleniaVPN · Solitude plan</span>
|
||
<span>Estimate only</span>
|
||
</footer>
|
||
</main>
|
||
|
||
<script>
|
||
(function() {
|
||
// ----- Slider -----
|
||
const MIN = 3, MAX = 12;
|
||
const wrap = document.getElementById('slider-wrap');
|
||
const frame = document.getElementById('track-frame');
|
||
const hit = document.getElementById('slider-hit');
|
||
const fill = document.getElementById('slider-fill');
|
||
const pointer = document.getElementById('pointer');
|
||
const halo = document.getElementById('pointer-halo');
|
||
const ticksHost = document.getElementById('slider-ticks');
|
||
const axisNow = document.getElementById('axis-now');
|
||
const priceValue = document.getElementById('price-value');
|
||
const readoutNote = document.getElementById('readout-note');
|
||
const bdDevices = document.getElementById('bd-devices');
|
||
const bdPerSeat = document.getElementById('bd-per-seat');
|
||
const bdTotal = document.getElementById('bd-total');
|
||
|
||
let value = 6;
|
||
let dragging = false;
|
||
|
||
// ----- Duration -----
|
||
// Placeholder pricing model — frontend will replace this; design only.
|
||
// Longer commitments earn a quiet discount on the base subscription.
|
||
const DURATIONS = [
|
||
{ months: 1, mult: 1.00, tag: 'Standard' },
|
||
{ months: 3, mult: 0.95, tag: 'Save 5%' },
|
||
{ months: 6, mult: 0.90, tag: 'Save 10%' },
|
||
{ months: 12, mult: 0.80, tag: 'Save 20%' }
|
||
];
|
||
let duration = DURATIONS[0];
|
||
|
||
const durationGrid = document.getElementById('duration-grid');
|
||
const durationCap = document.getElementById('duration-cap');
|
||
|
||
function buildDurations() {
|
||
durationGrid.innerHTML = '';
|
||
DURATIONS.forEach((d) => {
|
||
const b = document.createElement('button');
|
||
b.type = 'button';
|
||
b.className = 'dur' + (d === duration ? ' is-active' : '');
|
||
b.dataset.months = d.months;
|
||
b.setAttribute('role', 'radio');
|
||
b.setAttribute('aria-checked', d === duration ? 'true' : 'false');
|
||
b.dataset.odId = 'duration-' + d.months;
|
||
const num = document.createElement('span');
|
||
num.className = 'dur-num';
|
||
num.textContent = d.months;
|
||
const unit = document.createElement('span');
|
||
unit.className = 'dur-unit';
|
||
unit.textContent = d.months === 1 ? 'month' : 'months';
|
||
const tag = document.createElement('span');
|
||
tag.className = 'dur-tag';
|
||
tag.textContent = d.tag;
|
||
b.append(num, unit, tag);
|
||
b.addEventListener('click', () => {
|
||
duration = d;
|
||
syncDurations();
|
||
render();
|
||
});
|
||
durationGrid.appendChild(b);
|
||
});
|
||
}
|
||
function syncDurations() {
|
||
durationGrid.querySelectorAll('.dur').forEach((b) => {
|
||
const m = Number(b.dataset.months);
|
||
const d = DURATIONS.find(x => x.months === m);
|
||
const on = d === duration;
|
||
b.classList.toggle('is-active', on);
|
||
b.setAttribute('aria-checked', on ? 'true' : 'false');
|
||
});
|
||
}
|
||
|
||
// Visual pricing model — frontend will replace this; design only.
|
||
function priceFor(n) {
|
||
// gentle declining per-seat rate as the circle grows
|
||
const perSeat = 3.00 - (n - 3) * 0.12;
|
||
return { perSeat, total: perSeat * n };
|
||
}
|
||
function fmt(v) {
|
||
return (Math.round(v * 100) / 100).toFixed(2).replace(/\.00$/, '');
|
||
}
|
||
function money(v) {
|
||
const s = (Math.round(v * 100) / 100).toFixed(2);
|
||
return '$' + s.replace(/\.00$/, '');
|
||
}
|
||
|
||
// Build ticks
|
||
function buildTicks() {
|
||
ticksHost.innerHTML = '';
|
||
for (let i = MIN; i <= MAX; i++) {
|
||
const t = document.createElement('div');
|
||
t.className = 'tick' + ((i === MIN || i === MAX || i === Math.round((MIN+MAX)/2)) ? ' is-major' : '');
|
||
t.dataset.i = i;
|
||
const lab = document.createElement('span');
|
||
lab.className = 'tick-label';
|
||
lab.textContent = i;
|
||
t.appendChild(lab);
|
||
ticksHost.appendChild(t);
|
||
}
|
||
}
|
||
|
||
function valueToPct(v) { return (v - MIN) / (MAX - MIN); }
|
||
function pctToValue(p) {
|
||
let pClamped = Math.max(0, Math.min(1, p));
|
||
return Math.round(MIN + pClamped * (MAX - MIN));
|
||
}
|
||
|
||
function render() {
|
||
const pct = valueToPct(value);
|
||
const trackInnerW = Math.max(0, frame.clientWidth - 8);
|
||
const x = 4 + pct * trackInnerW;
|
||
pointer.style.left = x + 'px';
|
||
halo.style.left = x + 'px';
|
||
fill.style.width = (x - 4) + 'px';
|
||
axisNow.textContent = value;
|
||
|
||
// ticks active state
|
||
ticksHost.querySelectorAll('.tick').forEach(t => {
|
||
t.classList.toggle('is-active', Number(t.dataset.i) === value);
|
||
});
|
||
|
||
// pricing readout
|
||
const { perSeat, total } = priceFor(value);
|
||
const addons = addonsTotal();
|
||
const mult = duration.mult;
|
||
const discountedPerSeat = perSeat * mult;
|
||
const discountedBase = total * mult;
|
||
const savingsMonthly = total - discountedBase;
|
||
const grand = discountedBase + addons;
|
||
const periodTotal = grand * duration.months;
|
||
priceValue.textContent = money(grand);
|
||
readoutNote.textContent = 'For ' + value + ' devices · ' + money(discountedPerSeat) + ' per seat · ' + duration.months + (duration.months === 1 ? ' month' : ' months');
|
||
bdDevices.textContent = value;
|
||
bdPerSeat.textContent = money(discountedPerSeat);
|
||
|
||
const count = selectedAddons.size;
|
||
if (count > 0) {
|
||
bdAddonsRow.hidden = false;
|
||
bdAddonsCount.textContent = count;
|
||
bdAddonsSum.textContent = money(addons);
|
||
} else {
|
||
bdAddonsRow.hidden = true;
|
||
}
|
||
|
||
if (duration.months > 1) {
|
||
bdCommitRow.hidden = false;
|
||
bdMonths.textContent = duration.months;
|
||
bdDiscount.textContent = money(savingsMonthly);
|
||
bdPeriodRow.hidden = false;
|
||
bdPeriod.textContent = money(periodTotal);
|
||
bdPeriodLabel.textContent = '· ' + duration.months + ' months';
|
||
durationCap.innerHTML = 'Billed every ' + duration.months + ' months <span class="save">' + duration.tag + '</span>';
|
||
} else {
|
||
bdCommitRow.hidden = true;
|
||
bdPeriodRow.hidden = true;
|
||
durationCap.textContent = 'Billed monthly';
|
||
}
|
||
bdTotal.textContent = money(grand);
|
||
|
||
pointer.setAttribute('aria-valuenow', value);
|
||
pointer.setAttribute('aria-valuetext', value + ' devices');
|
||
}
|
||
|
||
function valueFromClientX(clientX) {
|
||
const rect = frame.getBoundingClientRect();
|
||
const innerLeft = rect.left + 4;
|
||
const innerW = rect.width - 8;
|
||
const p = (clientX - innerLeft) / innerW;
|
||
return pctToValue(p);
|
||
}
|
||
|
||
function setCursor(clientX, clientY) {
|
||
if (!fine) return;
|
||
mx = clientX; my = clientY;
|
||
dot.style.left = mx + 'px'; dot.style.top = my + 'px';
|
||
}
|
||
function startDrag(clientX, clientY) {
|
||
dragging = true;
|
||
wrap.classList.add('is-dragging');
|
||
setCursor(clientX, clientY);
|
||
value = valueFromClientX(clientX);
|
||
render();
|
||
}
|
||
function moveDrag(clientX, clientY) {
|
||
if (!dragging) return;
|
||
setCursor(clientX, clientY);
|
||
value = valueFromClientX(clientX);
|
||
render();
|
||
}
|
||
function endDrag() {
|
||
if (!dragging) return;
|
||
dragging = false;
|
||
wrap.classList.remove('is-dragging');
|
||
}
|
||
|
||
// Pointer events
|
||
hit.addEventListener('pointerdown', (e) => {
|
||
e.preventDefault();
|
||
hit.setPointerCapture(e.pointerId);
|
||
startDrag(e.clientX, e.clientY);
|
||
});
|
||
hit.addEventListener('pointermove', (e) => moveDrag(e.clientX, e.clientY));
|
||
hit.addEventListener('pointerup', (e) => { endDrag(); try { hit.releasePointerCapture(e.pointerId); } catch(_){} });
|
||
hit.addEventListener('pointercancel', endDrag);
|
||
|
||
// Click anywhere on track frame jumps
|
||
frame.addEventListener('click', (e) => {
|
||
if (dragging) return;
|
||
if (e.target.closest('.pointer')) return;
|
||
value = valueFromClientX(e.clientX);
|
||
render();
|
||
});
|
||
|
||
// Keyboard on the rhombus
|
||
pointer.addEventListener('keydown', (e) => {
|
||
let handled = true;
|
||
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') value = Math.min(MAX, value + (e.shiftKey ? 2 : 1));
|
||
else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') value = Math.max(MIN, value - (e.shiftKey ? 2 : 1));
|
||
else if (e.key === 'Home') value = MIN;
|
||
else if (e.key === 'End') value = MAX;
|
||
else handled = false;
|
||
if (handled) { e.preventDefault(); render(); }
|
||
});
|
||
pointer.addEventListener('focus', () => wrap.classList.add('is-dragging'));
|
||
pointer.addEventListener('blur', () => wrap.classList.remove('is-dragging'));
|
||
|
||
// Re-render on resize (positions are pixel-based)
|
||
window.addEventListener('resize', render);
|
||
|
||
buildTicks();
|
||
buildDurations();
|
||
// initial render after layout
|
||
requestAnimationFrame(render);
|
||
|
||
// ----- Addons -----
|
||
// Placeholder dataset — replace with backend response.
|
||
// Addon shape: { id, name, description, price }
|
||
const ADDONS = [
|
||
{ id: 'dedicated-ip', name: 'Dedicated IP', description: 'A private exit address reserved for your circle.', price: 4 },
|
||
{ id: 'multi-hop', name: 'Multi-Hop', description: 'Cascade through two regions for layered solitude.', price: 3 },
|
||
{ id: 'tracker-block', name: 'Tracker Block', description: 'Quiet blocking of ads and telemetry at the network edge.', price: 2 }
|
||
];
|
||
const selectedAddons = new Set();
|
||
|
||
const addonList = document.getElementById('addon-list');
|
||
const addonSection = document.getElementById('addons-section');
|
||
const bdAddonsRow = document.getElementById('bd-addons-row');
|
||
const bdAddonsCount = document.getElementById('bd-addons-count');
|
||
const bdAddonsSum = document.getElementById('bd-addons-sum');
|
||
const bdCommitRow = document.getElementById('bd-commit-row');
|
||
const bdMonths = document.getElementById('bd-months');
|
||
const bdDiscount = document.getElementById('bd-discount');
|
||
const bdPeriodRow = document.getElementById('bd-period-row');
|
||
const bdPeriod = document.getElementById('bd-period');
|
||
const bdPeriodLabel = document.getElementById('bd-period-label');
|
||
|
||
function renderAddons() {
|
||
// If the backend returns no addons, hide the entire section and bail.
|
||
if (!ADDONS || ADDONS.length === 0) {
|
||
if (addonSection) addonSection.hidden = true;
|
||
addonList.innerHTML = '';
|
||
selectedAddons.clear();
|
||
return;
|
||
}
|
||
if (addonSection) addonSection.hidden = false;
|
||
addonList.innerHTML = '';
|
||
ADDONS.forEach((a) => {
|
||
const row = document.createElement('button');
|
||
row.type = 'button';
|
||
row.className = 'addon-row';
|
||
row.dataset.odId = 'addon-' + a.id;
|
||
row.setAttribute('aria-pressed', 'false');
|
||
row.innerHTML =
|
||
'<span class="addon-toggle" aria-hidden="true"></span>' +
|
||
'<span class="addon-info">' +
|
||
'<span class="addon-name"></span>' +
|
||
'<span class="addon-desc"></span>' +
|
||
'</span>' +
|
||
'<span class="addon-price"><span class="plus">+</span><span class="amt"></span>$</span>';
|
||
row.querySelector('.addon-name').textContent = a.name;
|
||
row.querySelector('.addon-desc').textContent = a.description;
|
||
row.querySelector('.amt').textContent = a.price;
|
||
row.addEventListener('click', () => {
|
||
if (selectedAddons.has(a.id)) selectedAddons.delete(a.id);
|
||
else selectedAddons.add(a.id);
|
||
syncAddonRow(row, a.id);
|
||
render();
|
||
});
|
||
addonList.appendChild(row);
|
||
syncAddonRow(row, a.id);
|
||
});
|
||
}
|
||
function syncAddonRow(row, id) {
|
||
const on = selectedAddons.has(id);
|
||
row.classList.toggle('is-selected', on);
|
||
row.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||
}
|
||
function addonsTotal() {
|
||
return ADDONS.filter(a => selectedAddons.has(a.id)).reduce((s, a) => s + a.price, 0);
|
||
}
|
||
|
||
renderAddons();
|
||
|
||
// ----- 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 * 90) + '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';
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
|
||
</body></html> |