Files
Public relations & Virtual technology 29def95df8 v1.4.3
Add SOCKS5 support
Add AdGuard Home integration
Add node status and latency tracking
Add drag-and-drop server management
Upgrade Xray from v1.8 to v26.3.27 with backward compatibility
Fix Telemt dependency installation
Extend Telegram bot to support all protocols
Improve server cleanup process
Enhance API structure (system routes moved to a separate group)
Add JWT token generation for external integrations
2026-05-04 16:01:06 +03:00

1863 lines
41 KiB
CSS

/* ========================================
AmneziaVPN Web Panel — Design System
======================================== */
/* Google Font */
/* @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); */
/* ===== CSS Variables / Tokens ===== */
:root {
/* Color palette — deep dark with neon violet accent */
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-card: #1a1a26;
--bg-card-hover: #22223a;
--bg-input: #16162240;
--bg-modal: rgba(10, 10, 15, 0.92);
--border-color: rgba(255, 255, 255, 0.06);
--border-hover: rgba(255, 255, 255, 0.12);
--border-focus: rgba(138, 92, 246, 0.5);
--text-primary: #f0f0f5;
--text-secondary: #8b8ba0;
--text-muted: #5a5a70;
--text-accent: #a78bfa;
--accent: #8b5cf6;
--accent-light: #a78bfa;
--accent-dark: #7c3aed;
--accent-glow: rgba(139, 92, 246, 0.25);
--accent-gradient: linear-gradient(135deg, #7c3aed, #2d2f71);
--success: #22c55e;
--success-bg: rgba(34, 197, 94, 0.12);
--success-border: rgba(34, 197, 94, 0.25);
--danger: #ef4444;
--danger-bg: rgba(239, 68, 68, 0.12);
--danger-border: rgba(239, 68, 68, 0.25);
--warning: #f59e0b;
--warning-bg: rgba(245, 158, 11, 0.12);
--info: #3b82f6;
--info-bg: rgba(59, 130, 246, 0.12);
/* Spacing */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--space-2xl: 48px;
--space-3xl: 64px;
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
--shadow-glow: 0 0 30px rgba(139, 92, 246, 0.15);
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.04);
/* Transitions */
--transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
/* Font */
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* ===== Light Theme ===== */
[data-theme="light"] {
--bg-primary: #f5f5f9;
--bg-secondary: #eeeef3;
--bg-card: #ffffff;
--bg-card-hover: #f8f8fc;
--bg-input: rgba(0, 0, 0, 0.03);
--bg-modal: rgba(255, 255, 255, 0.92);
--border-color: rgba(0, 0, 0, 0.08);
--border-hover: rgba(0, 0, 0, 0.15);
--border-focus: rgba(138, 92, 246, 0.4);
--text-primary: #1a1a2e;
--text-secondary: #5a5a78;
--text-muted: #9090a8;
--text-accent: #7c3aed;
--accent: #7c3aed;
--accent-light: #8b5cf6;
--accent-dark: #6d28d9;
--accent-glow: rgba(124, 58, 237, 0.15);
--accent-gradient: linear-gradient(135deg, #7c3aed, #2d2f71);
--success: #16a34a;
--success-bg: rgba(22, 163, 74, 0.08);
--success-border: rgba(22, 163, 74, 0.2);
--danger: #dc2626;
--danger-bg: rgba(220, 38, 38, 0.06);
--danger-border: rgba(220, 38, 38, 0.15);
--warning: #d97706;
--warning-bg: rgba(217, 119, 6, 0.08);
--info: #2563eb;
--info-bg: rgba(37, 99, 235, 0.06);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1);
--shadow-glow: 0 0 20px rgba(124, 58, 237, 0.08);
--shadow-card: 0 2px 12px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.03);
}
[data-theme="light"] body::before {
background:
radial-gradient(ellipse 600px 400px at 20% 10%, rgba(124, 58, 237, 0.04), transparent),
radial-gradient(ellipse 500px 500px at 80% 80%, rgba(99, 102, 241, 0.03), transparent),
radial-gradient(ellipse 800px 300px at 50% 50%, rgba(139, 92, 246, 0.02), transparent);
}
[data-theme="light"] ::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.2);
}
[data-theme="light"] .logo-text {
background: linear-gradient(135deg, #1a1a2e, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
[data-theme="light"] .theme-toggle {
background: rgba(0, 0, 0, 0.04);
}
[data-theme="light"] .theme-toggle:hover {
background: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .header-nav {
background: #ffffff;
}
/* ===== Theme Toggle ===== */
.theme-toggle {
width: 36px;
height: 36px;
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
background: rgba(255, 255, 255, 0.04);
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
transition: all var(--transition-base);
flex-shrink: 0;
}
.theme-toggle:hover {
color: var(--text-primary);
border-color: var(--border-hover);
background: rgba(255, 255, 255, 0.08);
transform: rotate(15deg);
}
/* ===== Reset & Base ===== */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font-family);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
}
/* Animated mesh gradient background */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(ellipse 600px 400px at 20% 10%, rgba(99, 102, 241, 0.08), transparent),
radial-gradient(ellipse 500px 500px at 80% 80%, rgba(139, 92, 246, 0.06), transparent),
radial-gradient(ellipse 800px 300px at 50% 50%, rgba(79, 70, 229, 0.04), transparent);
pointer-events: none;
z-index: 0;
}
a {
color: var(--accent-light);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--accent);
}
::selection {
background: rgba(139, 92, 246, 0.3);
color: #fff;
}
/* ===== Scrollbar ===== */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(139, 92, 246, 0.3);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(139, 92, 246, 0.5);
}
/* ===== Layout ===== */
.app-container {
position: relative;
z-index: 1;
max-width: 1100px;
margin: 0 auto;
padding: var(--space-xl) var(--space-lg);
}
/* ===== Header ===== */
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-lg) 0;
margin-bottom: var(--space-xl);
border-bottom: 1px solid var(--border-color);
}
.app-logo {
display: flex;
align-items: center;
gap: var(--space-md);
}
.logo-icon {
width: 44px;
height: 44px;
background: var(--accent-gradient);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
box-shadow: var(--shadow-glow);
}
.logo-text {
font-size: 1.35rem;
font-weight: 700;
letter-spacing: -0.02em;
background: linear-gradient(135deg, #f0f0f5, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo-subtitle {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 400;
letter-spacing: 0.05em;
text-transform: uppercase;
}
/* ===== Buttons ===== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
padding: 10px 20px;
border: none;
border-radius: var(--radius-md);
font-family: var(--font-family);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-base);
position: relative;
overflow: hidden;
white-space: nowrap;
}
.btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), transparent);
opacity: 0;
transition: opacity var(--transition-fast);
}
.btn:hover::before {
opacity: 1;
}
.btn-primary {
background: var(--accent-gradient);
color: #fff;
box-shadow: 0 2px 12px rgba(139, 92, 246, 0.3);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-secondary {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
border-color: var(--border-hover);
background: var(--bg-card-hover);
}
.btn-danger {
background: var(--danger-bg);
color: var(--danger);
border: 1px solid var(--danger-border);
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.2);
}
.btn-success {
background: var(--success-bg);
color: var(--success);
border: 1px solid var(--success-border);
}
.btn-sm {
padding: 6px 14px;
font-size: 0.8rem;
border-radius: var(--radius-sm);
}
.btn-icon {
width: 36px;
height: 36px;
padding: 0;
border-radius: var(--radius-sm);
font-size: 1.1rem;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.btn .spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
/* ===== Cards ===== */
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--space-lg);
box-shadow: var(--shadow-card);
transition: all var(--transition-base);
}
.card-hover:hover {
border-color: var(--border-hover);
box-shadow: var(--shadow-card), var(--shadow-glow);
transform: translateY(-2px);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-lg);
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
}
.card-subtitle {
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: var(--space-xs);
}
/* ===== Section ===== */
.section-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: var(--space-lg);
display: flex;
align-items: center;
gap: var(--space-md);
}
.section-title .icon {
font-size: 1.3rem;
}
/* ===== Server Card ===== */
.server-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--space-lg);
margin-bottom: var(--space-xl);
}
.server-card {
position: relative;
cursor: grab;
}
.server-card:active {
cursor: grabbing;
}
/* While being dragged: dim the card so the user sees their selection. */
.server-card.dragging {
opacity: 0.45;
cursor: grabbing;
}
/* Drop target hint: highlighted border + slight lift. */
.server-card.drag-over {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent), 0 8px 20px rgba(139, 92, 246, 0.25);
transform: translateY(-2px);
}
/* Ping indicator dot to the left of the server name. */
.ping-dot {
display: inline-block;
width: 9px;
height: 9px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
background: #888;
transition: background-color 0.25s ease, box-shadow 0.25s ease;
flex-shrink: 0;
}
.ping-dot.ping-pending {
background: #94a3b8;
animation: ping-pulse 1.2s ease-in-out infinite;
}
.ping-dot.ping-ok {
background: var(--success);
box-shadow: 0 0 6px rgba(34, 197, 94, 0.55);
}
.ping-dot.ping-fail {
background: var(--danger, #ef4444);
}
.ping-ms {
font-size: 0.72rem;
margin-left: 6px;
color: var(--text-muted);
font-family: 'SF Mono', 'Fira Code', monospace;
}
@keyframes ping-pulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
.server-icon {
width: 48px;
height: 48px;
background: var(--accent-gradient);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
flex-shrink: 0;
box-shadow: 0 2px 12px rgba(139, 92, 246, 0.2);
}
.server-card .server-meta {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-md);
}
.server-card .server-name {
font-size: 1.05rem;
font-weight: 600;
}
.server-card .server-host {
font-size: 0.8rem;
color: var(--text-muted);
font-family: 'SF Mono', 'Fira Code', monospace;
}
.server-card .server-protocols {
display: flex;
gap: var(--space-sm);
flex-wrap: wrap;
margin-top: var(--space-sm);
}
.server-card .server-actions {
display: flex;
gap: var(--space-sm);
margin-top: var(--space-md);
padding-top: var(--space-md);
border-top: 1px solid var(--border-color);
}
/* ===== Badge ===== */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: var(--radius-full);
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.badge-success {
background: var(--success-bg);
color: var(--success);
border: 1px solid var(--success-border);
}
.badge-danger {
background: var(--danger-bg);
color: var(--danger);
border: 1px solid var(--danger-border);
}
.badge-info {
background: var(--info-bg);
color: var(--info);
border: 1px solid rgba(59, 130, 246, 0.25);
}
.badge-warn {
background: var(--warning-bg);
color: var(--warning);
border: 1px solid rgba(245, 158, 11, 0.25);
}
.badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
/* ===== Form Elements ===== */
.form-group {
margin-bottom: var(--space-md);
}
.form-label {
display: block;
font-size: 0.82rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: var(--space-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 11px 16px;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-family: var(--font-family);
font-size: 0.9rem;
transition: all var(--transition-fast);
outline: none;
}
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
border-color: var(--border-focus);
box-shadow: 0 0 0 3px var(--accent-glow);
background: var(--bg-secondary);
}
.form-input::placeholder {
color: var(--text-muted);
}
.form-textarea {
min-height: 100px;
resize: vertical;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.82rem;
}
.form-select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238b8ba0' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
padding-right: 36px;
cursor: pointer;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-md);
}
.form-hint {
font-size: 0.75rem;
color: var(--text-muted);
margin-top: var(--space-xs);
}
/* ===== Modal ===== */
.modal-backdrop {
position: fixed;
inset: 0;
background: var(--bg-modal);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: all var(--transition-base);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.modal-backdrop.active {
opacity: 1;
visibility: visible;
}
.modal {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-xl);
width: 90%;
max-width: 520px;
max-height: 90vh;
overflow-y: auto;
padding: var(--space-xl);
box-shadow: var(--shadow-lg);
transform: translateY(20px) scale(0.97);
transition: transform var(--transition-base);
}
.modal-backdrop.active .modal {
transform: translateY(0) scale(1);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-lg);
}
.modal-title {
font-size: 1.15rem;
font-weight: 700;
}
.modal-close {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
font-size: 1.3rem;
transition: all var(--transition-fast);
}
.modal-close:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
.modal-footer {
display: flex;
gap: var(--space-sm);
justify-content: flex-end;
margin-top: var(--space-lg);
padding-top: var(--space-lg);
border-top: 1px solid var(--border-color);
}
/* ===== Protocol Card ===== */
.protocol-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--space-lg);
margin-bottom: var(--space-xl);
}
.protocol-card {
position: relative;
overflow: hidden;
}
.protocol-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--accent-gradient);
opacity: 0;
transition: opacity var(--transition-base);
}
.protocol-card:hover::before {
opacity: 1;
}
.protocol-icon {
width: 56px;
height: 56px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.6rem;
margin-bottom: var(--space-md);
}
.protocol-awg .protocol-icon {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(99, 102, 241, 0.1));
color: var(--accent-light);
}
.protocol-legacy .protocol-icon {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(99, 102, 241, 0.1));
color: var(--info);
}
.protocol-xray .protocol-icon {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(245, 158, 11, 0.1));
color: var(--warning);
}
.protocol-telemt .protocol-icon {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(16, 185, 129, 0.1));
color: #38bdf8;
}
.protocol-dns .protocol-icon {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.1));
color: var(--success);
}
.protocol-wireguard .protocol-icon {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(52, 211, 153, 0.1));
color: #10b981;
}
.protocol-adguard .protocol-icon {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(132, 204, 22, 0.1));
color: #65a30d;
}
.protocol-socks5 .protocol-icon {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.2), rgba(217, 70, 239, 0.1));
color: #c084fc;
}
/* ============================================================
Promo blocks — locked teasers for not-yet-shipped features.
They live inside .protocol-cards (a CSS grid) and span every
column via `grid-column: 1 / -1`, so they always stretch the
full width without hard-coding the column count.
============================================================ */
.promo-block {
grid-column: 1 / -1;
position: relative;
overflow: hidden;
padding: 22px 28px;
border-radius: var(--radius-lg);
color: #fff;
cursor: not-allowed;
isolation: isolate; /* keep blend modes / blurred orbs from leaking out */
border: 1px solid rgba(255, 255, 255, 0.12);
transition: transform 0.35s ease, box-shadow 0.35s ease;
user-select: none;
}
.promo-block:hover {
transform: translateY(-2px);
}
/* Slow shimmer that sweeps diagonally across the block — pure CSS, no JS. */
.promo-block::after {
content: "";
position: absolute;
inset: -50% -10%;
background: linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, 0.18) 50%, transparent 65%);
transform: translateX(-100%);
animation: promo-shimmer 6s ease-in-out infinite;
pointer-events: none;
z-index: 1;
}
@keyframes promo-shimmer {
0%, 20% { transform: translateX(-100%); }
50%, 70% { transform: translateX(100%); }
100% { transform: translateX(100%); }
}
/* Floating blurred orbs that drift in the background. */
.promo-orbs {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.promo-orb {
position: absolute;
border-radius: 50%;
filter: blur(48px);
opacity: 0.55;
animation: promo-orb-drift 14s ease-in-out infinite;
}
.promo-orb:nth-child(1) { top: -40px; left: 8%; width: 220px; height: 220px; animation-delay: 0s; }
.promo-orb:nth-child(2) { bottom: -60px; left: 38%; width: 180px; height: 180px; animation-delay: -5s; }
.promo-orb:nth-child(3) { top: 10%; right: 8%; width: 200px; height: 200px; animation-delay: -9s; }
@keyframes promo-orb-drift {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(20px, -16px) scale(1.12); }
66% { transform: translate(-12px, 14px) scale(0.92); }
}
.promo-lock-badge {
position: absolute;
top: 14px;
right: 16px;
z-index: 3;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.18);
color: #fff;
}
.promo-content {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.promo-icon {
flex-shrink: 0;
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.9rem;
background: rgba(255, 255, 255, 0.12);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18), 0 8px 24px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
animation: promo-icon-float 4.5s ease-in-out infinite;
}
@keyframes promo-icon-float {
0%, 100% { transform: translateY(0) rotate(0); }
50% { transform: translateY(-3px) rotate(2deg); }
}
.promo-text {
flex: 1;
min-width: 0;
}
.promo-title {
font-size: 1.35rem;
font-weight: 700;
letter-spacing: -0.01em;
margin-bottom: 2px;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
}
.promo-subtitle {
font-size: 0.88rem;
color: rgba(255, 255, 255, 0.85);
line-height: 1.4;
max-width: 70ch;
}
.promo-cta {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 9px 16px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
text-decoration: none;
color: #fff;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.28);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: background 0.2s ease, transform 0.2s ease;
cursor: pointer;
pointer-events: auto; /* block-level cursor:not-allowed shouldn't kill the CTA link */
}
.promo-cta:hover {
background: rgba(255, 255, 255, 0.32);
transform: translateY(-1px);
}
/* ----- Theme: AIVPN (purple/cyan, neural feel) ----- */
.promo-aivpn {
background:
radial-gradient(circle at 20% 0%, rgba(99, 102, 241, 0.6), transparent 60%),
radial-gradient(circle at 80% 100%, rgba(236, 72, 153, 0.5), transparent 55%),
linear-gradient(135deg, #312e81 0%, #6d28d9 50%, #be185d 100%);
box-shadow: 0 16px 40px -10px rgba(109, 40, 217, 0.5);
}
.promo-aivpn .promo-orb:nth-child(1) { background: #06b6d4; }
.promo-aivpn .promo-orb:nth-child(2) { background: #d946ef; }
.promo-aivpn .promo-orb:nth-child(3) { background: #6366f1; }
.promo-aivpn .promo-icon {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.5), rgba(236, 72, 153, 0.4));
text-shadow: 0 0 16px rgba(168, 85, 247, 0.6);
}
/* ----- Theme: Reverse Proxy (amber/red, shield/firewall feel) ----- */
.promo-revproxy {
background:
radial-gradient(circle at 100% 0%, rgba(251, 146, 60, 0.55), transparent 55%),
radial-gradient(circle at 0% 100%, rgba(244, 63, 94, 0.45), transparent 55%),
linear-gradient(135deg, #7c2d12 0%, #b91c1c 45%, #9d174d 100%);
box-shadow: 0 16px 40px -10px rgba(185, 28, 28, 0.5);
}
.promo-revproxy .promo-orb:nth-child(1) { background: #fb923c; }
.promo-revproxy .promo-orb:nth-child(2) { background: #f43f5e; }
.promo-revproxy .promo-orb:nth-child(3) { background: #f59e0b; }
.promo-revproxy .promo-icon {
background: linear-gradient(135deg, rgba(251, 146, 60, 0.5), rgba(244, 63, 94, 0.4));
text-shadow: 0 0 16px rgba(251, 146, 60, 0.55);
}
/* Faint isometric grid overlay — gives the reverse-proxy block a "network" feel.
Uses a tiled SVG-less approach via repeating linear gradients to keep CSS-only. */
.promo-grid-bg {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
opacity: 0.18;
background-image:
linear-gradient(rgba(255, 255, 255, 0.5) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.5) 1px, transparent 1px);
background-size: 28px 28px;
mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
-webkit-mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
animation: promo-grid-pan 20s linear infinite;
}
@keyframes promo-grid-pan {
from { background-position: 0 0; }
to { background-position: 56px 56px; }
}
/* ----- Responsive: stack content on narrow screens ----- */
@media (max-width: 720px) {
.promo-content { gap: 14px; }
.promo-cta { width: 100%; justify-content: center; margin-left: 0; }
.promo-subtitle { font-size: 0.82rem; }
}
/* Respect users who turn off motion. */
@media (prefers-reduced-motion: reduce) {
.promo-block::after,
.promo-orb,
.promo-icon,
.promo-grid-bg {
animation: none !important;
}
}
.protocol-name {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: var(--space-xs);
}
.protocol-desc {
font-size: 0.82rem;
color: var(--text-secondary);
margin-bottom: var(--space-md);
line-height: 1.5;
}
.protocol-status {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-md);
}
.protocol-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-sm);
margin-bottom: var(--space-md);
}
.protocol-info-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.protocol-info-label {
font-size: 0.72rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.protocol-info-value {
font-size: 0.88rem;
font-weight: 500;
font-family: 'SF Mono', 'Fira Code', monospace;
}
/* ===== Clients Table ===== */
.clients-section {
margin-top: var(--space-xl);
}
.clients-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-lg);
}
.clients-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.client-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-md) var(--space-lg);
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.client-item:hover {
border-color: var(--border-hover);
background: var(--bg-card-hover);
}
.client-info {
display: flex;
align-items: center;
gap: var(--space-md);
flex: 1;
min-width: 0;
}
.client-avatar {
width: 40px;
height: 40px;
border-radius: var(--radius-sm);
background: var(--accent-gradient);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 700;
color: #fff;
flex-shrink: 0;
}
.client-name {
font-weight: 600;
font-size: 0.92rem;
}
.client-meta {
font-size: 0.78rem;
color: var(--text-muted);
display: flex;
gap: var(--space-md);
margin-top: 2px;
flex-wrap: wrap;
align-items: center;
}
.client-actions {
display: flex;
align-items: center;
gap: var(--space-sm);
flex-shrink: 0;
margin-left: var(--space-md);
flex-wrap: wrap;
max-width: 140px;
}
/* ===== Config Display ===== */
.config-display {
position: relative;
margin-top: var(--space-md);
}
.config-text {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-md);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.78rem;
line-height: 1.7;
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
}
.config-actions {
display: flex;
gap: var(--space-sm);
margin-top: var(--space-sm);
}
/* ===== Toast Notifications ===== */
.toast-container {
position: fixed;
top: var(--space-lg);
right: var(--space-lg);
z-index: 2000;
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.toast {
padding: var(--space-md) var(--space-lg);
border-radius: var(--radius-md);
font-size: 0.85rem;
font-weight: 500;
box-shadow: var(--shadow-lg);
animation: toast-in 0.3s ease-out;
display: flex;
align-items: center;
gap: var(--space-sm);
max-width: 400px;
}
.toast-success {
background: #0d2818;
border: 1px solid var(--success-border);
color: var(--success);
}
.toast-error {
background: #2d1212;
border: 1px solid var(--danger-border);
color: var(--danger);
}
.toast-info {
background: #0d1a2d;
border: 1px solid rgba(59, 130, 246, 0.25);
color: var(--info);
}
@keyframes toast-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* ===== Progress / Loading ===== */
.progress-bar {
height: 4px;
background: var(--bg-secondary);
border-radius: var(--radius-full);
overflow: hidden;
margin: var(--space-md) 0;
}
.progress-fill {
height: 100%;
background: var(--accent-gradient);
border-radius: var(--radius-full);
transition: width 0.5s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3));
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-40px);
}
100% {
transform: translateX(40px);
}
}
.loading-overlay {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--space-md);
padding: var(--space-3xl) 0;
color: var(--text-muted);
}
.loading-spinner {
width: 36px;
height: 36px;
border: 3px solid var(--border-color);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ===== Log Output ===== */
.log-output {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-md);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.78rem;
line-height: 1.8;
max-height: 250px;
overflow-y: auto;
color: var(--text-muted);
}
.log-line {
padding: 2px 0;
}
.log-line.success {
color: var(--success);
}
.log-line.error {
color: var(--danger);
}
.log-line.info {
color: var(--text-secondary);
}
/* ===== Empty State ===== */
.empty-state {
text-align: center;
padding: var(--space-3xl) var(--space-xl);
color: var(--text-muted);
}
.empty-state .empty-icon {
font-size: 3.5rem;
margin-bottom: var(--space-lg);
opacity: 0.5;
}
.empty-state .empty-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: var(--space-sm);
}
.empty-state .empty-desc {
font-size: 0.88rem;
max-width: 360px;
margin: 0 auto var(--space-lg);
}
/* ===== Tabs ===== */
.tabs {
display: flex;
gap: 2px;
background: var(--bg-secondary);
border-radius: var(--radius-md);
padding: 3px;
margin-bottom: var(--space-lg);
}
.tab {
flex: 1;
padding: 10px 16px;
border: none;
background: transparent;
color: var(--text-muted);
font-family: var(--font-family);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
text-align: center;
}
.tab:hover {
color: var(--text-secondary);
}
.tab.active {
background: var(--bg-card);
color: var(--text-primary);
box-shadow: var(--shadow-sm);
}
/* ===== Back Link ===== */
.back-link {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
color: var(--text-muted);
font-size: 0.85rem;
margin-bottom: var(--space-lg);
transition: color var(--transition-fast);
}
.back-link:hover {
color: var(--text-primary);
}
/* ===== Divider ===== */
.divider {
height: 1px;
background: var(--border-color);
margin: var(--space-lg) 0;
}
/* ===== Utility ===== */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.gap-sm {
gap: var(--space-sm);
}
.gap-md {
gap: var(--space-md);
}
.mt-md {
margin-top: var(--space-md);
}
.mt-lg {
margin-top: var(--space-lg);
}
.text-muted {
color: var(--text-muted);
}
.text-sm {
font-size: 0.82rem;
}
.font-mono {
font-family: 'SF Mono', 'Fira Code', monospace;
}
.hidden {
display: none !important;
}
/* ===== Responsive ===== */
@media (max-width: 768px) {
.app-container {
padding: var(--space-md) var(--space-md);
}
.server-grid {
grid-template-columns: 1fr;
}
.protocol-cards {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.modal {
width: 95%;
padding: var(--space-lg);
}
.app-header {
flex-wrap: wrap;
gap: var(--space-md);
}
}
@media (max-width: 480px) {
.protocol-info {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
}
/* ===== Server Stats (inline pills) ===== */
.stat-pill {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 3px 8px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
font-size: 0.68rem;
font-weight: 500;
font-family: 'SF Mono', 'Fira Code', monospace;
color: var(--text-secondary);
white-space: nowrap;
cursor: default;
transition: all var(--transition-fast);
}
.stat-pill:hover {
border-color: var(--border-hover);
color: var(--text-primary);
}
/* ===== Config Tabs ===== */
.config-tabs {
display: flex;
gap: 2px;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
padding: 3px;
margin-bottom: var(--space-md);
}
.config-tab {
flex: 1;
padding: 8px 12px;
border: none;
background: transparent;
color: var(--text-muted);
font-family: var(--font-family);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
border-radius: 6px;
transition: all var(--transition-fast);
text-align: center;
white-space: nowrap;
}
.config-tab:hover {
color: var(--text-secondary);
}
.config-tab.active {
background: var(--bg-card);
color: var(--text-primary);
box-shadow: var(--shadow-sm);
}
.config-panel {
display: none;
}
.config-panel.active {
display: block;
}
/* ===== QR Code ===== */
.qr-container {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-md);
padding: var(--space-lg);
}
.qr-container canvas {
border-radius: var(--radius-md);
box-shadow: 0 0 30px rgba(139, 92, 246, 0.15);
}
.qr-hint {
font-size: 0.8rem;
color: var(--text-muted);
text-align: center;
}
/* ===== VPN Link ===== */
.vpn-link-box {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-md);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.72rem;
line-height: 1.6;
color: var(--accent-light);
word-break: break-all;
max-height: 200px;
overflow-y: auto;
user-select: all;
}
/* ===== Traffic Badge ===== */
.traffic-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: var(--radius-full);
font-size: 0.7rem;
font-weight: 500;
font-family: 'SF Mono', 'Fira Code', monospace;
}
.traffic-down {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.traffic-up {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* ===== Header Navigation ===== */
.header-nav {
display: flex;
align-items: center;
gap: 4px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
padding: 5px;
box-shadow: var(--shadow-sm);
transition: all var(--transition-base);
}
.nav-link {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 20px;
border-radius: var(--radius-full);
font-size: 0.85rem;
font-weight: 500;
color: var(--text-secondary);
transition: all var(--transition-base);
text-decoration: none;
white-space: nowrap;
position: relative;
letter-spacing: -0.01em;
}
.nav-link:hover {
color: var(--text-primary);
background: rgba(139, 92, 246, 0.08);
}
.nav-link.active {
color: #fff;
background: var(--accent-gradient);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
font-weight: 600;
}
.nav-user {
display: flex;
align-items: center;
gap: var(--space-md);
padding-left: var(--space-lg);
border-left: 1px solid var(--border-color);
margin-left: var(--space-sm);
transition: border-color var(--transition-base);
}
/* Global Transition for Theme Switching */
body,
.card,
.app-header,
.header-nav,
.nav-link,
.btn,
.form-input,
.form-select,
.modal,
.modal-backdrop,
.nav-user {
transition: background-color var(--transition-slow),
border-color var(--transition-slow),
color var(--transition-slow),
box-shadow var(--transition-slow),
transform var(--transition-base);
}
.nav-username {
font-size: 0.82rem;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: -0.01em;
}
.nav-user .badge {
padding: 2px 10px;
font-size: 0.65rem;
letter-spacing: 0.05em;
font-weight: 700;
}
.nav-user .btn {
padding: 6px 14px;
font-size: 0.8rem;
font-weight: 600;
border-radius: var(--radius-sm);
}
@media (max-width: 900px) {
.header-nav {
gap: 2px;
padding: 4px;
}
.nav-link {
padding: 6px 12px;
font-size: 0.8rem;
}
}
@media (max-width: 768px) {
.app-header {
flex-direction: column;
gap: var(--space-md);
align-items: stretch;
}
.header-nav {
border-radius: var(--radius-md);
justify-content: center;
}
.nav-user {
border-left: none;
margin-left: 0;
padding-left: 0;
padding-top: var(--space-sm);
border-top: 1px solid var(--border-color);
justify-content: center;
}
}
/* ===== Badges ===== */
.badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.badge-success {
background: var(--success-bg);
color: var(--success);
border: 1px solid var(--success-border);
}
.badge-warn {
background: var(--warning-bg);
color: var(--warning);
border: 1px solid rgba(245, 158, 11, 0.25);
}
.badge-danger {
background: var(--danger-bg);
color: var(--danger);
border: 1px solid var(--danger-border);
}
.badge-info {
background: var(--info-bg);
color: var(--info);
border: 1px solid rgba(59, 130, 246, 0.25);
}
.badge-secondary {
background: rgba(255, 255, 255, 0.06);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
/* Additional layout helpers */
.hidden {
display: none !important;
}
.gap-sm {
gap: var(--space-sm);
}
.gap-md {
gap: var(--space-md);
}