ReVoltVPN website

The landing page for ReVoltVPN, live at userevolt.app.

Vanilla HTML, CSS and JavaScript: no framework, no build step, no
dependencies. Every claim on the page is checked against the app it
describes, which is the reason this is public.

GPL-3.0, same as the app.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
esefandClaude Opus 5 committed 2026-10-18 15:00:56 +03:00
commit 9abf266444
31 files changed
+2602

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
/* ═══════════════════════════════════════════════════════════════════════════
background.css — Background image + canvas overlay layers
═══════════════════════════════════════════════════════════════════════════ */
.bg-layer {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.bg-image {
position: absolute;
inset: 0;
background-image: url('../images/background.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* Darken overlay — matches Flutter ColorFilter.mode(overlay, darken) */
.bg-image::after {
content: '';
position: absolute;
inset: 0;
background: rgba(13, 17, 23, 0.67);
}
.bg-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
+62
View File
@@ -0,0 +1,62 @@
/* ═══════════════════════════════════════════════════════════════════════════
base.css — Reset, variables, typography
═══════════════════════════════════════════════════════════════════════════ */
:root {
--color-accent: #FFD600;
--color-accent-dim: rgba(255, 214, 0, 0.12);
--color-bg-deep: #0D1117;
--color-text: #FFFFFF;
--color-text-dim: rgba(255, 255, 255, 0.54);
--color-text-muted: #A0AEC0;
--color-glass-bg: rgba(255, 255, 255, 0.08);
--color-glass-border: rgba(255, 255, 255, 0.12);
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace;
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-2xl: 3rem;
--radius-card: 16px;
--radius-btn: 12px;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
body {
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
color: var(--color-text);
background-color: var(--color-bg-deep);
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
h1,h2,h3 { line-height: 1.2; }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
+36
View File
@@ -0,0 +1,36 @@
/* ═══════════════════════════════════════════════════════════════════════════
components.css — Buttons
═══════════════════════════════════════════════════════════════════════════ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-weight: 700;
font-family: var(--font-body);
border: none;
border-radius: var(--radius-btn);
cursor: pointer;
text-align: center;
white-space: nowrap;
text-decoration: none;
}
.btn--md { min-height: 44px; padding: 0 20px; font-size: 1rem; }
.btn--outline {
color: var(--color-accent);
background-color: transparent;
border: 1.5px solid var(--color-accent);
font-family: var(--font-mono);
font-size: 0.8125rem;
font-weight: 400;
text-transform: none;
letter-spacing: 0;
}
.btn--outline:hover { text-decoration: underline; }
.btn--outline:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
+71
View File
@@ -0,0 +1,71 @@
/* ═══════════════════════════════════════════════════════════════════════════
layout.css — Shell layout, tab content area, responsive breakpoints
═══════════════════════════════════════════════════════════════════════════ */
/* ── Body is the tab viewport, no page scroll ───────────────────────────── */
html, body {
height: 100%;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
}
/* ── Tab content area (fills remaining space below nav) ─────────────────── */
.tab-content {
position: relative;
z-index: 1;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
/* ── Tab pane (injected by tabs.js) ─────────────────────────────────────── */
.tab-pane {
display: none;
padding: 1.5rem 1rem;
}
.tab-pane.active {
display: block;
}
/* Home tab specifically fills the viewport */
.tab-pane--fill {
display: none;
padding: 0;
height: 100%;
}
.tab-pane--fill.active {
display: flex;
flex-direction: column;
}
/* ── Container (shared across all tabs) ─────────────────────────────────── */
.container {
width: 100%;
max-width: 720px;
margin-inline: auto;
}
/* ── Responsive ─────────────────────────────────────────────────────────── */
@media (min-width: 576px) {
.tab-pane { padding: 2rem 1.5rem; }
}
@media (min-width: 768px) {
.container { max-width: 704px; }
.tab-pane { padding: 2rem 2rem; }
}
@media (min-width: 992px) {
.container { max-width: 800px; }
.tab-pane { padding: 2.5rem 2rem; }
}
@media (min-width: 1200px) {
.container { max-width: 880px; }
}
+151
View File
@@ -0,0 +1,151 @@
/* ═══════════════════════════════════════════════════════════════════════════
nav.css — Top navigation bar with tabs (matching scraped2 style)
═══════════════════════════════════════════════════════════════════════════ */
.nav {
position: relative;
z-index: 100;
flex-shrink: 0;
background: rgba(13, 17, 23, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.nav__inner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
min-height: 56px;
max-width: 880px;
margin-inline: auto;
}
/* Brand / logo */
.nav__brand {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
font-size: 1.15rem;
color: var(--color-text);
flex-shrink: 0;
cursor: pointer;
margin-right: 2rem;
}
.nav__logo {
display: block;
flex-shrink: 0;
}
/* Tab buttons */
.nav__tabs {
display: flex;
align-items: center;
gap: 0;
}
.nav__tab {
display: inline-block;
background: none;
border: none;
text-decoration: none;
color: var(--color-text-dim);
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
border-radius: 6px;
white-space: nowrap;
padding: 0.5rem 0.625rem;
min-height: 40px;
display: inline-flex;
align-items: center;
}
.nav__tab.active {
color: var(--color-accent);
background: var(--color-accent-dim);
}
/* ── Hamburger button ──────────────────────────────────────────────────── */
.nav__hamburger {
display: none;
flex-direction: column;
justify-content: center;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 8px;
width: 40px;
height: 40px;
flex-shrink: 0;
}
.nav__hamburger span {
display: block;
width: 22px;
height: 2px;
background: var(--color-text);
border-radius: 1px;
transition: transform 0.25s, opacity 0.25s;
}
/* Animate to X when open */
.nav--open .nav__hamburger span:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.nav--open .nav__hamburger span:nth-child(2) {
opacity: 0;
}
.nav--open .nav__hamburger span:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
/* ── Mobile: hamburger menu ────────────────────────────────────────────── */
@media (max-width: 575px) {
.nav__hamburger {
display: flex;
}
.nav__tabs {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
background: rgba(13, 17, 23, 0.95);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding: 0.5rem 0;
z-index: 200;
}
.nav--open .nav__tabs {
display: flex;
}
.nav__tabs .nav__tab {
width: 100%;
text-align: left;
padding: 0.75rem 1.5rem;
font-size: 0.9375rem;
border-radius: 0;
min-height: 44px;
}
.nav__tabs .nav__tab.active {
background: var(--color-accent-dim);
}
}
/* ── Responsive ─────────────────────────────────────────────────────────── */
@media (min-width: 576px) {
.nav__tab { font-size: 0.875rem; padding: 0.5rem 1rem; }
.nav__brand { font-size: 1.25rem; }
}
+49
View File
@@ -0,0 +1,49 @@
/* ═══════════════════════════════════════════════════════════════════════════
contact.css — Contact tab
═══════════════════════════════════════════════════════════════════════════ */
.contact-tab {
max-width: 560px;
margin-inline: auto;
text-align: center;
padding-top: var(--space-xl);
padding-bottom: var(--space-xl);
}
.contact-tab h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: var(--space-2xl);
}
.contact-item {
margin-bottom: var(--space-2xl);
}
@media (max-width: 575px) {
.contact-item {
margin-bottom: var(--space-xl);
}
}
.contact-item h3 {
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
font-size: 1.0625rem;
font-weight: 700;
margin-bottom: var(--space-sm);
}
.contact-icon {
flex-shrink: 0;
color: var(--color-text);
}
.contact-item p {
font-size: 0.9375rem;
color: var(--color-text-dim);
line-height: 1.7;
margin-bottom: var(--space-md);
}
+33
View File
@@ -0,0 +1,33 @@
/* ═══════════════════════════════════════════════════════════════════════════
download.css — Download tab (Google Play + GitHub Releases)
═══════════════════════════════════════════════════════════════════════════ */
.download-tab {
text-align: center;
padding: var(--space-xl);
margin-bottom: var(--space-lg);
max-width: 480px;
}
.download-tab h1 {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: var(--space-md);
}
.download-tab h1 .icon {
flex-shrink: 0;
}
.download-tab p {
color: var(--color-text-dim);
font-size: 0.9375rem;
line-height: 1.7;
max-width: 520px;
margin-inline: auto;
margin-bottom: var(--space-xl);
}
+62
View File
@@ -0,0 +1,62 @@
/* ═══════════════════════════════════════════════════════════════════════════
faq.css — FAQ tab
═══════════════════════════════════════════════════════════════════════════ */
.faq-tab {
max-width: 680px;
margin-inline: auto;
padding-top: var(--space-xl);
padding-bottom: var(--space-xl);
}
.faq-tab h1 {
font-size: 1.5rem;
font-weight: 700;
text-align: center;
margin-bottom: var(--space-xl);
}
/* Each question */
.faq-item {
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding: var(--space-md) 0;
}
.faq-item summary {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-weight: 600;
font-size: 1rem;
color: var(--color-text);
list-style: none;
}
.faq-item summary::-webkit-details-marker {
display: none;
}
.faq-item summary::after {
content: '+';
font-size: 1.25rem;
font-weight: 300;
color: var(--color-text-dim);
transition: transform 0.2s;
}
.faq-item[open] summary::after {
content: '\2212';
}
.faq-item .answer {
padding-top: var(--space-md);
font-size: 0.9375rem;
color: var(--color-text-dim);
line-height: 1.7;
}
/* Longer answers run to two paragraphs; without this they butt together. */
.faq-item .answer p + p {
margin-top: var(--space-sm);
}
+62
View File
@@ -0,0 +1,62 @@
/* ═══════════════════════════════════════════════════════════════════════════
features.css — Features tab
═══════════════════════════════════════════════════════════════════════════ */
.features-tab {
padding-top: var(--space-xl);
padding-bottom: var(--space-xl);
}
.features-tab h1 {
font-size: 1.5rem;
font-weight: 700;
text-align: center;
margin-bottom: var(--space-xs);
}
.features-tab .lead {
text-align: center;
color: var(--color-text-dim);
max-width: 480px;
margin-inline: auto;
margin-bottom: var(--space-xl);
}
.features-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-lg);
}
.feature-card {
background: var(--color-glass-bg);
border: 1px solid var(--color-glass-border);
border-radius: var(--radius-card);
padding: var(--space-lg);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.feature-card h3 {
font-size: 1.0625rem;
font-weight: 700;
margin-bottom: var(--space-sm);
}
.feature-card p {
font-size: 0.9375rem;
color: var(--color-text-dim);
line-height: 1.6;
}
@media (min-width: 768px) {
.features-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 992px) {
.features-grid {
grid-template-columns: repeat(3, 1fr);
}
}
+62
View File
@@ -0,0 +1,62 @@
/* ═══════════════════════════════════════════════════════════════════════════
github.css — GitHub tab
═══════════════════════════════════════════════════════════════════════════ */
.github-tab {
text-align: center;
padding: var(--space-xl);
max-width: 480px;
}
.github-tab h1 {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: var(--space-md);
}
.github-tab h1 .icon {
flex-shrink: 0;
}
.github-tab p {
color: var(--color-text-dim);
font-size: 0.9375rem;
line-height: 1.7;
max-width: 520px;
margin-inline: auto;
margin-bottom: var(--space-xl);
}
.github-tab .btn {
font-family: var(--font-mono);
font-size: 0.8125rem;
font-weight: 400;
text-transform: none;
letter-spacing: 0;
}
.github-tab .meta {
display: flex;
justify-content: center;
gap: var(--space-xl);
margin-top: var(--space-xl);
font-size: 0.75rem;
color: var(--color-text-muted);
font-family: var(--font-mono);
}
/* The Gitea mirror sits in .meta so it reads as a footnote, not a second CTA. */
.github-tab .meta a {
color: var(--color-text-muted);
text-decoration: underline;
text-underline-offset: 3px;
}
.github-tab .meta a:hover,
.github-tab .meta a:focus-visible {
color: var(--color-accent);
}
+44
View File
@@ -0,0 +1,44 @@
/* ═══════════════════════════════════════════════════════════════════════════
home.css — Landing / home tab (full-viewport hero, no buttons)
═══════════════════════════════════════════════════════════════════════════ */
.home-hero {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
padding: 2rem 1rem;
max-width: 860px;
margin-inline: auto;
}
.home-hero h1 {
font-size: 2rem;
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.5px;
margin-bottom: var(--space-lg);
}
.home-hero .subhead {
font-size: 1.0625rem;
color: var(--color-text-dim);
line-height: 1.8;
}
@media (min-width: 576px) {
.home-hero h1 { font-size: 2.5rem; }
.home-hero { padding: 2rem 1.5rem; }
}
@media (min-width: 768px) {
.home-hero h1 { font-size: 3rem; }
.home-hero .subhead { font-size: 1.125rem; }
.home-hero { padding: 2rem; }
}
@media (min-width: 992px) {
.home-hero h1 { font-size: 3.5rem; }
.home-hero { padding: 2.5rem 2rem; }
}
+38
View File
@@ -0,0 +1,38 @@
/* ═══════════════════════════════════════════════════════════════════════════
legal.css — Shared styles for Terms of Service and Privacy Policy tabs
═══════════════════════════════════════════════════════════════════════════ */
.legal-tab {
max-width: 680px;
margin-inline: auto;
}
.legal-tab h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: var(--space-lg);
}
.legal-tab h2 {
font-size: 1rem;
font-weight: 700;
margin-top: var(--space-lg);
margin-bottom: var(--space-xs);
}
.legal-tab p,
.legal-tab li {
color: var(--color-text-dim);
font-size: 0.875rem;
line-height: 1.6;
}
.legal-tab ul {
list-style: disc;
padding-left: 1.25rem;
margin-bottom: var(--space-sm);
}
.legal-tab li {
margin-bottom: 0.125rem;
}
+43
View File
@@ -0,0 +1,43 @@
/* ═══════════════════════════════════════════════════════════════════════════
why.css — "What most VPNs get wrong" tab
═══════════════════════════════════════════════════════════════════════════ */
.why-tab {
max-width: 720px;
margin-inline: auto;
padding-top: var(--space-xl);
padding-bottom: var(--space-xl);
}
.why-tab h1 {
font-size: 1.5rem;
font-weight: 700;
text-align: center;
margin-bottom: var(--space-sm);
}
.why-tab .opener {
text-align: center;
color: var(--color-text-dim);
font-size: 1.0625rem;
line-height: 1.7;
max-width: 560px;
margin-inline: auto;
margin-bottom: var(--space-2xl);
}
.why-item {
margin-bottom: var(--space-xl);
}
.why-item h3 {
font-size: 1.0625rem;
font-weight: 700;
margin-bottom: var(--space-xs);
}
.why-item p {
font-size: 0.9375rem;
color: var(--color-text-dim);
line-height: 1.7;
}
+11
View File
@@ -0,0 +1,11 @@
# Images
| File | Used for |
|---|---|
| `brand.png` | nav logo, 32x32 |
| `playstore.png` | Google Play icon on the Download tab, 32x32 |
| `background.png` | full-screen page background |
| `favicon-16x16.png`, `favicon-32x32.png`, `apple-touch-icon.png` | favicons |
These are ReVoltVPN brand assets. The GPL-3.0 grant in `LICENSE` covers the
site's code, not the name, logo or brand images.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+199
View File
@@ -0,0 +1,199 @@
/* ═══════════════════════════════════════════════════════════════════════════
Lightning Effect — canvas-based, matches ReVoltVPN Flutter LightningEffect
Random jagged yellow bolts with glow, 2-10 sec intervals, double-strike
═══════════════════════════════════════════════════════════════════════════ */
(function () {
const canvas = document.getElementById('bg-lightning');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
/* ── Configuration (mirrors LightningEffect defaults) ────────────────── */
const MIN_INTERVAL_SEC = 2;
const MAX_INTERVAL_SEC = 10;
const DOUBLE_CHANCE = 0.2;
/* ── State ──────────────────────────────────────────────────────────── */
let width = 0;
let height = 0;
let bolt = null; // current LightningBolt or null
let opacity = 0; // current opacity (driven by easing)
let startTime = 0; // performance.now() at strike start
const DURATION_MS = 400; // total animation duration
let animId = null;
let timerId = null;
/* ── LightningBolt model ──────────────────────────────────────────────── */
class LightningBolt {
/**
* @param {Array<{x:number, y:number}>} mainPath — zigzag from top to bottom
* @param {Array<Array<{x:number, y:number}>>} branches — fork paths
*/
constructor(mainPath, branches = []) {
this.mainPath = mainPath;
this.branches = branches;
}
/** Generate a random bolt — matches Flutter LightningBolt.random(). */
static random() {
const points = [];
// Start: top, slight horizontal offset
let x = 0.5 + (Math.random() - 0.5) * 0.15;
points.push({ x, y: 0.0 });
// Zigzag down — 6-10 segments
const segments = 6 + Math.floor(Math.random() * 5);
for (let i = 1; i < segments; i++) {
const y = i / segments;
x += (Math.random() - 0.5) * 0.25;
x = clamp(x, 0.05, 0.95);
points.push({ x, y });
}
// End: bottom
points.push({ x: clamp(x, 0.1, 0.9), y: 1.0 });
// Branches — 0-2 forks
const branches = [];
const branchCount = Math.floor(Math.random() * 3);
for (let b = 0; b < branchCount; b++) {
const branch = [];
const startIdx = 1 + Math.floor(Math.random() * (points.length - 2));
const start = points[startIdx];
branch.push({ ...start });
let bx = start.x;
let by = start.y;
const branchLen = 2 + Math.floor(Math.random() * 3);
for (let j = 0; j < branchLen; j++) {
by += 0.05 + Math.random() * 0.12;
bx += (Math.random() - 0.5) * 0.3;
bx = clamp(bx, 0.02, 0.98);
by = clamp(by, 0.0, 1.0);
branch.push({ x: bx, y: by });
}
branches.push(branch);
}
return new LightningBolt(points, branches);
}
}
/* ── Easing: sharp flash curve (fast on, fast off, no slow fade) ────── */
function flashOpacity(t) {
if (t < 0.08) return t / 0.08; // fast ramp 0→1 in 8% of time
return 1.0 - ((t - 0.08) / 0.92); // linear decay over rest of duration
}
/* ── Schedule next strike ────────────────────────────────────────────── */
function scheduleNext() {
const secs = MIN_INTERVAL_SEC + Math.floor(Math.random() * (MAX_INTERVAL_SEC - MIN_INTERVAL_SEC + 1));
timerId = setTimeout(strike, secs * 1000);
}
/* ── Strike ──────────────────────────────────────────────────────────── */
function strike() {
bolt = LightningBolt.random();
startTime = performance.now();
// Double strike?
if (Math.random() < DOUBLE_CHANCE) {
setTimeout(() => {
bolt = LightningBolt.random();
startTime = performance.now();
}, 120);
}
scheduleNext();
}
/* ── Resize handler ──────────────────────────────────────────────────── */
function resize() {
const dpr = window.devicePixelRatio || 1;
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
/* ── Draw a single path (main or branch) ─────────────────────────────── */
function drawPath(pts, px, pn) {
if (pts.length < 2) return;
ctx.beginPath();
ctx.moveTo(pts[0].x * width, pts[0].y * height);
for (let i = 1; i < pts.length; i++) {
ctx.lineTo(pts[i].x * width, pts[i].y * height);
}
ctx.stroke();
}
/* ── Render frame ────────────────────────────────────────────────────── */
function render() {
if (!bolt) { opacity = 0; return; }
const elapsed = performance.now() - startTime;
const t = Math.min(elapsed / DURATION_MS, 1.0);
opacity = flashOpacity(t);
if (opacity <= 0.02) { bolt = null; opacity = 0; return; }
}
/* ── Animation loop ──────────────────────────────────────────────────── */
function tick() {
animId = requestAnimationFrame(tick);
render();
ctx.clearRect(0, 0, width, height);
if (!bolt || opacity <= 0.02) return;
const baseAlpha = Math.round(opacity * 255);
const glowAlpha = Math.round(opacity * 0.35 * 255);
// Glow layer — wide, blurred
ctx.save();
ctx.strokeStyle = `rgba(255, 214, 0, ${(glowAlpha / 255).toFixed(3)})`;
ctx.lineWidth = 18;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.shadowColor = `rgba(255, 214, 0, ${(glowAlpha / 255).toFixed(3)})`;
ctx.shadowBlur = 8;
drawPath(bolt.mainPath);
for (const b of bolt.branches) drawPath(b);
ctx.restore();
// Core — medium
ctx.strokeStyle = `rgba(255, 214, 0, ${(baseAlpha / 255).toFixed(3)})`;
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
drawPath(bolt.mainPath);
for (const b of bolt.branches) drawPath(b);
// Hot center — white, only when bright
if (opacity > 0.6) {
const hotAlpha = Math.round(opacity * 180);
ctx.strokeStyle = `rgba(255, 255, 255, ${(hotAlpha / 255).toFixed(3)})`;
ctx.lineWidth = 2;
drawPath(bolt.mainPath);
for (const b of bolt.branches) drawPath(b);
}
}
/* ── Helpers ──────────────────────────────────────────────────────────── */
function clamp(v, min, max) { return v < min ? min : v > max ? max : v; }
/* ── Bootstrap ────────────────────────────────────────────────────────── */
resize();
window.addEventListener('resize', resize);
animId = requestAnimationFrame(tick);
scheduleNext();
})();
+104
View File
@@ -0,0 +1,104 @@
/* ═══════════════════════════════════════════════════════════════════════════
Rain Effect — canvas-based, matches ReVoltVPN Flutter RainEffect
~150 drops, white/light with slight leftward slant, semi-transparent
Recycles drops to the top when they fall off-screen.
═══════════════════════════════════════════════════════════════════════════ */
(function () {
const canvas = document.getElementById('bg-rain');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
/* ── Configuration (mirrors RainEffect defaults) ────────────────────── */
const DROP_COUNT = 150;
const COLOR = 'rgba(232, 234, 246, {opacity})'; // matches #E8EAF6
const MIN_LENGTH = 12;
const MAX_LENGTH = 44;
const WIND_DRIFT = 4; // slight leftward slant in px
const STROKE_W = 1.5;
/* ── State ──────────────────────────────────────────────────────────── */
const drops = [];
let width = 0;
let height = 0;
let lastTime = 0;
let animId = null;
/* ── Drop model ─────────────────────────────────────────────────────── */
class Drop {
constructor() { this.spawn(); }
/** Reset all properties to a new random drop. */
spawn() {
this.x = Math.random(); // 0–1 normalized
this.y = Math.random(); // 0–1 normalized
this.speed = 0.30 + Math.random() * 0.55; // matches Flutter range
this.length = MIN_LENGTH + Math.random() * (MAX_LENGTH - MIN_LENGTH);
this.opacity = 0.05 + Math.random() * 0.12; // matches Flutter range
}
}
/* ── Initialize drops ────────────────────────────────────────────────── */
for (let i = 0; i < DROP_COUNT; i++) {
drops.push(new Drop());
}
/* ── Resize handler ──────────────────────────────────────────────────── */
function resize() {
const dpr = window.devicePixelRatio || 1;
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
/* ── Animation loop ──────────────────────────────────────────────────── */
function tick(now) {
animId = requestAnimationFrame(tick);
/* Delta time in seconds, clamped */
let dt = 0.016; // ~60fps fallback
if (lastTime !== 0) {
dt = (now - lastTime) / 1000;
if (dt > 0.1) dt = 0.1;
}
lastTime = now;
/* Update drops */
for (const d of drops) {
d.y += d.speed * dt;
if (d.y > 1.08) {
d.spawn();
d.y = -0.06 - Math.random() * 0.08; // start slightly above viewport
}
}
/* Draw */
ctx.clearRect(0, 0, width, height);
ctx.lineCap = 'round';
ctx.lineWidth = STROKE_W;
for (const d of drops) {
const sx = d.x * width;
const sy = d.y * height;
const ex = sx - WIND_DRIFT; // slight leftward slant
const ey = sy + d.length;
ctx.strokeStyle = COLOR.replace('{opacity}', d.opacity.toFixed(3));
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(ex, ey);
ctx.stroke();
}
}
/* ── Bootstrap ───────────────────────────────────────────────────────── */
resize();
window.addEventListener('resize', resize);
animId = requestAnimationFrame(tick);
})();
+104
View File
@@ -0,0 +1,104 @@
/* ═══════════════════════════════════════════════════════════════════════════
tabs.js — Tab system
- Toggles .active on .tab-pane sections already in the HTML
- Hash routing, back/forward
═══════════════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
var TABS = ['home', 'features', 'why', 'faq', 'contact', 'download', 'github', 'terms', 'privacy'];
var contentEl = document.getElementById('tab-content');
var navLinks = document.querySelectorAll('.nav__tab');
function switchTo(name) {
contentEl.querySelectorAll('.tab-pane').forEach(function (p) { p.classList.remove('active'); });
var target = document.getElementById('tab-' + name);
if (target) target.classList.add('active');
navLinks.forEach(function (link) {
link.classList.toggle('active', link.getAttribute('href') === '#' + name);
});
contentEl.scrollTop = 0;
if (window.location.hash !== '#' + name) history.pushState(null, '', '#' + name);
var titles = {
home: 'ReVoltVPN', features: 'Features — ReVoltVPN', why: 'Why ReVolt — ReVoltVPN',
faq: 'FAQ — ReVoltVPN', contact: 'Contact — ReVoltVPN', download: 'Download — ReVoltVPN', github: 'Source — ReVoltVPN',
terms: 'Terms of Service — ReVoltVPN', privacy: 'Privacy Policy — ReVoltVPN'
};
if (titles[name]) document.title = titles[name];
}
navLinks.forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
var name = this.getAttribute('href').replace('#', '');
if (TABS.indexOf(name) !== -1) {
switchTo(name);
closeMenu();
}
});
});
/* ── Hamburger menu ──────────────────────────────────────────────────── */
var navEl = document.getElementById('nav');
var hamburger = document.querySelector('.nav__hamburger');
function openMenu() {
navEl.classList.add('nav--open');
hamburger.setAttribute('aria-expanded', 'true');
}
function closeMenu() {
navEl.classList.remove('nav--open');
hamburger.setAttribute('aria-expanded', 'false');
}
if (hamburger) {
hamburger.addEventListener('click', function () {
navEl.classList.contains('nav--open') ? closeMenu() : openMenu();
});
/* Close when clicking outside */
document.addEventListener('click', function (e) {
if (!navEl.contains(e.target)) closeMenu();
});
/* Close on Escape */
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeMenu();
});
}
window.addEventListener('popstate', function () {
var hash = window.location.hash.replace('#', '');
switchTo(TABS.indexOf(hash) !== -1 ? hash : 'home');
});
var el = document.getElementById('home-headline');
if (el) {
var headlines = [
'A VPN that does not ask who you are.',
'Privacy is not a subscription.',
'Your ISP is not your friend.',
'Encryption should not require an account.',
'The internet, without the surveillance.',
'You are not the product.',
'A VPN that minds its own business.',
'Your data belongs to you. Not your ISP.',
'Open source. Closed mouths.',
'Browse like nobody is watching. Because nobody is.',
'The VPN that forgets you immediately.',
'Free speech deserves free tools.',
'You decide how you pay. Not your data.',
'Privacy first. Payment optional.'
];
el.textContent = headlines[Math.floor(Math.random() * headlines.length)];
}
var hash = window.location.hash.replace('#', '');
switchTo(TABS.indexOf(hash) !== -1 ? hash : 'home');
})();