- New animated homepage at / with 3D card tilt, particle canvas, floating ring decorations, and gradient title shimmer - Personal finance pages move to /dashboard (base.html shows only personal nav + a subtle Business link) - Business/org inner pages use base_org.html with a purple theme, org breadcrumb, Year/Team dropdowns, and a ← Hub back link - org home/teams/members/invite/events/requests/ledger/analysis/report all switched to renderOrg() + base_org.html - Route strings updated to org-home, org-teams, org-events, etc. so active nav highlighting works correctly in the business shell Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
735 lines
24 KiB
HTML
735 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Finance Hub</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html { scroll-behavior: smooth; }
|
|
|
|
:root {
|
|
--bg: #050810;
|
|
--text: #e8f4f2;
|
|
--text2: #8ab4b0;
|
|
--text3: #3d6e6a;
|
|
--teal: #00c9b8;
|
|
--teal2: #33d9ca;
|
|
--teal-glow:rgba(0,201,184,0.25);
|
|
--purple: #7c3aed;
|
|
--purple2: #a855f7;
|
|
--purple-glow:rgba(124,58,237,0.25);
|
|
--border: rgba(255,255,255,0.06);
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
/* ── Canvas background ─────────────────────────────────────────── */
|
|
#bg-canvas {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ── Gradient orbs ─────────────────────────────────────────────── */
|
|
.orb {
|
|
position: fixed;
|
|
border-radius: 50%;
|
|
filter: blur(100px);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
animation: orb-drift 12s ease-in-out infinite alternate;
|
|
}
|
|
.orb-1 {
|
|
width: 600px; height: 600px;
|
|
background: radial-gradient(circle, rgba(0,201,184,0.12) 0%, transparent 70%);
|
|
top: -200px; left: -100px;
|
|
animation-delay: 0s;
|
|
}
|
|
.orb-2 {
|
|
width: 800px; height: 800px;
|
|
background: radial-gradient(circle, rgba(124,58,237,0.1) 0%, transparent 70%);
|
|
bottom: -300px; right: -200px;
|
|
animation-delay: -4s;
|
|
}
|
|
.orb-3 {
|
|
width: 400px; height: 400px;
|
|
background: radial-gradient(circle, rgba(168,85,247,0.08) 0%, transparent 70%);
|
|
top: 40%; left: 40%;
|
|
animation-delay: -8s;
|
|
}
|
|
@keyframes orb-drift {
|
|
from { transform: translate(0, 0) scale(1); }
|
|
to { transform: translate(40px, 30px) scale(1.08); }
|
|
}
|
|
|
|
/* ── Page wrapper ──────────────────────────────────────────────── */
|
|
.page {
|
|
position: relative;
|
|
z-index: 1;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 0 24px 80px;
|
|
}
|
|
|
|
/* ── Nav bar ───────────────────────────────────────────────────── */
|
|
.topbar {
|
|
width: 100%;
|
|
max-width: 1200px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 24px 0;
|
|
}
|
|
.topbar-logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
letter-spacing: -0.3px;
|
|
}
|
|
.topbar-logo-icon {
|
|
width: 36px; height: 36px;
|
|
background: linear-gradient(135deg, var(--teal), var(--purple2));
|
|
border-radius: 10px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 18px;
|
|
box-shadow: 0 0 20px rgba(0,201,184,0.3), 0 0 40px rgba(124,58,237,0.2);
|
|
animation: logo-pulse 3s ease-in-out infinite;
|
|
}
|
|
@keyframes logo-pulse {
|
|
0%,100% { box-shadow: 0 0 20px rgba(0,201,184,0.3), 0 0 40px rgba(124,58,237,0.2); }
|
|
50% { box-shadow: 0 0 30px rgba(0,201,184,0.5), 0 0 60px rgba(124,58,237,0.35); }
|
|
}
|
|
.topbar-email {
|
|
font-size: 13px;
|
|
color: var(--text3);
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
/* ── Hero ──────────────────────────────────────────────────────── */
|
|
.hero {
|
|
text-align: center;
|
|
padding: 80px 0 60px;
|
|
max-width: 700px;
|
|
animation: fadeUp 0.8s ease-out both;
|
|
}
|
|
.hero-badge {
|
|
display: inline-block;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.15em;
|
|
text-transform: uppercase;
|
|
color: var(--teal);
|
|
background: rgba(0,201,184,0.08);
|
|
border: 1px solid rgba(0,201,184,0.2);
|
|
padding: 5px 14px;
|
|
border-radius: 100px;
|
|
margin-bottom: 24px;
|
|
animation: badge-glow 2.5s ease-in-out infinite;
|
|
}
|
|
@keyframes badge-glow {
|
|
0%,100% { box-shadow: 0 0 0 rgba(0,201,184,0); }
|
|
50% { box-shadow: 0 0 16px rgba(0,201,184,0.3); }
|
|
}
|
|
.hero-title {
|
|
font-size: clamp(48px, 8vw, 80px);
|
|
font-weight: 800;
|
|
letter-spacing: -2px;
|
|
line-height: 1.0;
|
|
margin-bottom: 20px;
|
|
background: linear-gradient(135deg, #fff 0%, var(--teal) 45%, var(--purple2) 80%, #fff 100%);
|
|
background-size: 200% auto;
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
animation: title-shimmer 6s linear infinite;
|
|
}
|
|
@keyframes title-shimmer {
|
|
0% { background-position: 0% center; }
|
|
100% { background-position: 200% center; }
|
|
}
|
|
.hero-sub {
|
|
font-size: 18px;
|
|
color: var(--text2);
|
|
line-height: 1.6;
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* ── Stats row ─────────────────────────────────────────────────── */
|
|
.stats-row {
|
|
display: flex;
|
|
gap: 48px;
|
|
justify-content: center;
|
|
padding: 0 0 60px;
|
|
animation: fadeUp 0.8s 0.15s ease-out both;
|
|
}
|
|
.stat {
|
|
text-align: center;
|
|
}
|
|
.stat-value {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.5px;
|
|
color: var(--text);
|
|
}
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: var(--text3);
|
|
font-weight: 500;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* ── Products grid ─────────────────────────────────────────────── */
|
|
.products {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 24px;
|
|
width: 100%;
|
|
max-width: 900px;
|
|
animation: fadeUp 0.8s 0.25s ease-out both;
|
|
perspective: 1200px;
|
|
}
|
|
|
|
/* ── Product card ──────────────────────────────────────────────── */
|
|
.product-card {
|
|
position: relative;
|
|
background: rgba(13,18,28,0.7);
|
|
border: 1px solid var(--border);
|
|
border-radius: 24px;
|
|
padding: 36px 32px 32px;
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
transition: transform 0.08s ease-out, box-shadow 0.3s ease;
|
|
transform-style: preserve-3d;
|
|
will-change: transform;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
.product-card:hover {
|
|
box-shadow: 0 30px 80px rgba(0,0,0,0.6);
|
|
}
|
|
|
|
/* animated corner glow */
|
|
.product-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: -1px;
|
|
border-radius: 24px;
|
|
padding: 1px;
|
|
background: var(--card-gradient, linear-gradient(135deg, transparent, transparent));
|
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
-webkit-mask-composite: xor;
|
|
mask-composite: exclude;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
.product-card:hover::before { opacity: 1; }
|
|
|
|
/* inner glow blob */
|
|
.card-glow {
|
|
position: absolute;
|
|
width: 200px; height: 200px;
|
|
border-radius: 50%;
|
|
filter: blur(60px);
|
|
opacity: 0;
|
|
transition: opacity 0.4s ease;
|
|
pointer-events: none;
|
|
top: -60px; left: -60px;
|
|
}
|
|
.product-card:hover .card-glow { opacity: 1; }
|
|
|
|
/* teal card */
|
|
.card-personal {
|
|
--card-gradient: linear-gradient(135deg, var(--teal), transparent, var(--teal2));
|
|
animation: float-a 6s ease-in-out infinite;
|
|
}
|
|
.card-personal .card-glow { background: radial-gradient(circle, var(--teal-glow), transparent); }
|
|
.card-personal::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0; right: 0;
|
|
width: 120px; height: 120px;
|
|
background: radial-gradient(circle, rgba(0,201,184,0.08), transparent);
|
|
border-radius: 50%;
|
|
transform: translate(30%, 30%);
|
|
}
|
|
|
|
/* purple card */
|
|
.card-business {
|
|
--card-gradient: linear-gradient(135deg, var(--purple), transparent, var(--purple2));
|
|
animation: float-b 6s ease-in-out infinite;
|
|
}
|
|
.card-business .card-glow { background: radial-gradient(circle, var(--purple-glow), transparent); }
|
|
.card-business::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0; right: 0;
|
|
width: 120px; height: 120px;
|
|
background: radial-gradient(circle, rgba(124,58,237,0.08), transparent);
|
|
border-radius: 50%;
|
|
transform: translate(30%, 30%);
|
|
}
|
|
|
|
@keyframes float-a {
|
|
0%,100% { transform: translateY(0px); }
|
|
50% { transform: translateY(-10px); }
|
|
}
|
|
@keyframes float-b {
|
|
0%,100% { transform: translateY(-6px); }
|
|
50% { transform: translateY(6px); }
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
.card-icon {
|
|
width: 52px; height: 52px;
|
|
border-radius: 14px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 24px;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
}
|
|
.card-personal .card-icon {
|
|
background: rgba(0,201,184,0.12);
|
|
border: 1px solid rgba(0,201,184,0.2);
|
|
box-shadow: 0 0 20px rgba(0,201,184,0.15);
|
|
}
|
|
.card-business .card-icon {
|
|
background: rgba(124,58,237,0.12);
|
|
border: 1px solid rgba(124,58,237,0.2);
|
|
box-shadow: 0 0 20px rgba(124,58,237,0.15);
|
|
}
|
|
.card-title {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.4px;
|
|
color: var(--text);
|
|
}
|
|
.card-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
.card-personal .card-label { color: var(--teal); }
|
|
.card-business .card-label { color: var(--purple2); }
|
|
|
|
.card-desc {
|
|
font-size: 14px;
|
|
color: var(--text2);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* feature list */
|
|
.feature-list {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
flex: 1;
|
|
}
|
|
.feature-list li {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 13px;
|
|
color: var(--text2);
|
|
}
|
|
.feature-list li::before {
|
|
content: '';
|
|
width: 5px; height: 5px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.card-personal .feature-list li::before { background: var(--teal); }
|
|
.card-business .feature-list li::before { background: var(--purple2); }
|
|
|
|
/* CTA button */
|
|
.card-cta {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 24px;
|
|
border-radius: 12px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
transition: all 0.2s ease;
|
|
margin-top: 4px;
|
|
align-self: flex-start;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.card-cta::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(255,255,255,0.08);
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
.card-cta:hover::before { opacity: 1; }
|
|
.card-personal .card-cta {
|
|
background: linear-gradient(135deg, var(--teal), var(--teal2));
|
|
color: #050810;
|
|
box-shadow: 0 4px 20px rgba(0,201,184,0.35);
|
|
}
|
|
.card-personal .card-cta:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 30px rgba(0,201,184,0.5);
|
|
}
|
|
.card-business .card-cta {
|
|
background: linear-gradient(135deg, var(--purple), var(--purple2));
|
|
color: #fff;
|
|
box-shadow: 0 4px 20px rgba(124,58,237,0.35);
|
|
}
|
|
.card-business .card-cta:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 30px rgba(124,58,237,0.5);
|
|
}
|
|
.cta-arrow {
|
|
transition: transform 0.2s ease;
|
|
}
|
|
.card-cta:hover .cta-arrow { transform: translateX(4px); }
|
|
|
|
/* ── Divider ───────────────────────────────────────────────────── */
|
|
.divider {
|
|
width: 100%;
|
|
max-width: 900px;
|
|
height: 1px;
|
|
background: linear-gradient(90deg, transparent, var(--border), transparent);
|
|
margin: 64px 0 48px;
|
|
}
|
|
|
|
/* ── Features row ──────────────────────────────────────────────── */
|
|
.features {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 16px;
|
|
width: 100%;
|
|
max-width: 900px;
|
|
animation: fadeUp 0.8s 0.35s ease-out both;
|
|
}
|
|
.feature-chip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 16px 18px;
|
|
background: rgba(255,255,255,0.025);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
font-size: 13px;
|
|
color: var(--text2);
|
|
transition: all 0.2s ease;
|
|
}
|
|
.feature-chip:hover {
|
|
background: rgba(255,255,255,0.04);
|
|
border-color: rgba(255,255,255,0.1);
|
|
color: var(--text);
|
|
}
|
|
.feature-chip-icon {
|
|
font-size: 18px;
|
|
}
|
|
|
|
/* ── Footer ────────────────────────────────────────────────────── */
|
|
.footer {
|
|
margin-top: 80px;
|
|
font-size: 12px;
|
|
color: var(--text3);
|
|
text-align: center;
|
|
}
|
|
|
|
/* ── Animations ────────────────────────────────────────────────── */
|
|
@keyframes fadeUp {
|
|
from { opacity: 0; transform: translateY(24px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* ── Responsive ────────────────────────────────────────────────── */
|
|
@media (max-width: 640px) {
|
|
.products { grid-template-columns: 1fr; }
|
|
.features { grid-template-columns: 1fr 1fr; }
|
|
.stats-row { gap: 24px; }
|
|
.hero { padding: 50px 0 40px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Particle canvas background -->
|
|
<canvas id="bg-canvas"></canvas>
|
|
|
|
<!-- Gradient orbs -->
|
|
<div class="orb orb-1"></div>
|
|
<div class="orb orb-2"></div>
|
|
<div class="orb orb-3"></div>
|
|
|
|
<div class="page">
|
|
|
|
<!-- Top bar -->
|
|
<div class="topbar">
|
|
<a href="/" class="topbar-logo">
|
|
<div class="topbar-logo-icon">₣</div>
|
|
Finance Hub
|
|
</a>
|
|
{{if .Email}}<span class="topbar-email">{{.Email}}</span>{{end}}
|
|
</div>
|
|
|
|
<!-- Hero -->
|
|
<section class="hero">
|
|
<div class="hero-badge">Your Financial Universe</div>
|
|
<h1 class="hero-title">Finance Hub</h1>
|
|
<p class="hero-sub">One platform for managing personal wealth and business finances — beautifully unified.</p>
|
|
</section>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-row">
|
|
<div class="stat">
|
|
<div class="stat-value">2</div>
|
|
<div class="stat-label">Products</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">∞</div>
|
|
<div class="stat-label">Organisations</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">100%</div>
|
|
<div class="stat-label">Self-hosted</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Product cards -->
|
|
<div class="products" id="products">
|
|
|
|
<a href="/dashboard" class="product-card card-personal" id="card-personal">
|
|
<div class="card-glow"></div>
|
|
<div class="card-header">
|
|
<div class="card-icon">🏦</div>
|
|
<div>
|
|
<div class="card-label">Personal</div>
|
|
<div class="card-title">My Finance</div>
|
|
</div>
|
|
</div>
|
|
<p class="card-desc">Track spending, grow wealth, plan your future with full visibility into your personal finances.</p>
|
|
<ul class="feature-list">
|
|
<li>Dashboard & spending analytics</li>
|
|
<li>Transactions & auto-import</li>
|
|
<li>Investment portfolio</li>
|
|
<li>Financial goals</li>
|
|
<li>Net worth & projections</li>
|
|
<li>Tax reports</li>
|
|
</ul>
|
|
<span class="card-cta">Open Personal <span class="cta-arrow">→</span></span>
|
|
</a>
|
|
|
|
<a href="/orgs" class="product-card card-business" id="card-business">
|
|
<div class="card-glow"></div>
|
|
<div class="card-header">
|
|
<div class="card-icon">🏢</div>
|
|
<div>
|
|
<div class="card-label">Business</div>
|
|
<div class="card-title">Organisations</div>
|
|
</div>
|
|
</div>
|
|
<p class="card-desc">Manage organisations, plan events, control budgets, and keep your teams aligned financially.</p>
|
|
<ul class="feature-list">
|
|
<li>Multi-org & teams</li>
|
|
<li>Events & budget planning</li>
|
|
<li>Purchase requests & approvals</li>
|
|
<li>Ledger & bank imports</li>
|
|
<li>Variance analysis</li>
|
|
<li>End-of-year reports</li>
|
|
</ul>
|
|
<span class="card-cta">Open Business <span class="cta-arrow">→</span></span>
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<!-- Feature chips -->
|
|
<div class="features">
|
|
<div class="feature-chip"><span class="feature-chip-icon">🔒</span> Self-hosted & private</div>
|
|
<div class="feature-chip"><span class="feature-chip-icon">🌙</span> Dark & light themes</div>
|
|
<div class="feature-chip"><span class="feature-chip-icon">📊</span> Real-time analytics</div>
|
|
<div class="feature-chip"><span class="feature-chip-icon">🏦</span> Multi-bank support</div>
|
|
<div class="feature-chip"><span class="feature-chip-icon">👥</span> Role-based access</div>
|
|
<div class="feature-chip"><span class="feature-chip-icon">📅</span> Fiscal year lifecycle</div>
|
|
</div>
|
|
|
|
<div class="footer">Finance Hub · Self-hosted homelab</div>
|
|
</div>
|
|
|
|
<script>
|
|
/* ── Particle canvas ─────────────────────────────────────────── */
|
|
(function() {
|
|
const canvas = document.getElementById('bg-canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
let W, H, pts;
|
|
|
|
function resize() {
|
|
W = canvas.width = window.innerWidth;
|
|
H = canvas.height = window.innerHeight;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', () => { resize(); });
|
|
|
|
const N = 60;
|
|
function initPts() {
|
|
pts = Array.from({ length: N }, () => ({
|
|
x: Math.random() * W,
|
|
y: Math.random() * H,
|
|
vx: (Math.random() - 0.5) * 0.35,
|
|
vy: (Math.random() - 0.5) * 0.35,
|
|
r: Math.random() * 1.8 + 0.6,
|
|
}));
|
|
}
|
|
initPts();
|
|
|
|
let mouse = { x: W / 2, y: H / 2 };
|
|
window.addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
|
|
|
|
function draw() {
|
|
ctx.clearRect(0, 0, W, H);
|
|
|
|
for (let p of pts) {
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
if (p.x < 0 || p.x > W) p.vx *= -1;
|
|
if (p.y < 0 || p.y > H) p.vy *= -1;
|
|
}
|
|
|
|
const LINK = 140;
|
|
for (let i = 0; i < pts.length; i++) {
|
|
for (let j = i + 1; j < pts.length; j++) {
|
|
const dx = pts[i].x - pts[j].x;
|
|
const dy = pts[i].y - pts[j].y;
|
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
if (d < LINK) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(pts[i].x, pts[i].y);
|
|
ctx.lineTo(pts[j].x, pts[j].y);
|
|
ctx.strokeStyle = `rgba(0,201,184,${0.06 * (1 - d / LINK)})`;
|
|
ctx.lineWidth = 0.8;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
// mouse proximity glow
|
|
const mdx = pts[i].x - mouse.x;
|
|
const mdy = pts[i].y - mouse.y;
|
|
const md = Math.sqrt(mdx * mdx + mdy * mdy);
|
|
if (md < 160) {
|
|
ctx.beginPath();
|
|
ctx.arc(pts[i].x, pts[i].y, pts[i].r * 2.5, 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(124,58,237,${0.7 * (1 - md / 160)})`;
|
|
ctx.fill();
|
|
}
|
|
ctx.beginPath();
|
|
ctx.arc(pts[i].x, pts[i].y, pts[i].r, 0, Math.PI * 2);
|
|
ctx.fillStyle = 'rgba(0,201,184,0.35)';
|
|
ctx.fill();
|
|
}
|
|
requestAnimationFrame(draw);
|
|
}
|
|
draw();
|
|
})();
|
|
|
|
/* ── 3D card tilt on mousemove ───────────────────────────────── */
|
|
(function() {
|
|
const cards = document.querySelectorAll('.product-card');
|
|
const MAX_TILT = 12;
|
|
|
|
cards.forEach(card => {
|
|
card.addEventListener('mousemove', e => {
|
|
const rect = card.getBoundingClientRect();
|
|
const cx = rect.left + rect.width / 2;
|
|
const cy = rect.top + rect.height / 2;
|
|
const dx = (e.clientX - cx) / (rect.width / 2);
|
|
const dy = (e.clientY - cy) / (rect.height / 2);
|
|
const rx = -dy * MAX_TILT;
|
|
const ry = dx * MAX_TILT;
|
|
card.style.transform = `perspective(900px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(8px)`;
|
|
});
|
|
card.addEventListener('mouseleave', () => {
|
|
card.style.transform = '';
|
|
card.style.transition = 'transform 0.5s ease, box-shadow 0.3s ease';
|
|
setTimeout(() => card.style.transition = '', 500);
|
|
});
|
|
card.addEventListener('mouseenter', () => {
|
|
card.style.transition = 'transform 0.08s ease-out, box-shadow 0.3s ease';
|
|
});
|
|
});
|
|
})();
|
|
|
|
/* ── 3D rotating ring decoration ────────────────────────────── */
|
|
(function() {
|
|
// subtle CSS 3D ring animation injected dynamically
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes ring-spin {
|
|
from { transform: rotateX(70deg) rotateZ(0deg); }
|
|
to { transform: rotateX(70deg) rotateZ(360deg); }
|
|
}
|
|
.ring {
|
|
position: fixed;
|
|
width: 400px; height: 400px;
|
|
border-radius: 50%;
|
|
border: 1px solid rgba(0,201,184,0.08);
|
|
top: 50%; left: 50%;
|
|
margin: -200px 0 0 -200px;
|
|
animation: ring-spin 20s linear infinite;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
.ring:nth-child(2) {
|
|
width: 600px; height: 600px;
|
|
margin: -300px 0 0 -300px;
|
|
border-color: rgba(124,58,237,0.05);
|
|
animation-duration: 32s;
|
|
animation-direction: reverse;
|
|
}
|
|
.ring:nth-child(3) {
|
|
width: 240px; height: 240px;
|
|
margin: -120px 0 0 -120px;
|
|
border-color: rgba(168,85,247,0.1);
|
|
animation-duration: 14s;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
const container = document.createElement('div');
|
|
container.style.cssText = 'position:fixed;inset:0;z-index:0;pointer-events:none;perspective:800px;';
|
|
for (let i = 0; i < 3; i++) {
|
|
const r = document.createElement('div');
|
|
r.className = 'ring';
|
|
container.appendChild(r);
|
|
}
|
|
document.body.appendChild(container);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|