Premium polish: counters anim, data flow, live clock, gradient h1, 3D tilt, FAQ spring

This commit is contained in:
neo committed 2026-06-25 19:37:57 +05:00
1 parent 45f20c192f
commit 94d125a503
1 file changed
+262 -50
+262 -50
View File
@@ -84,6 +84,7 @@ body {
--mono: 'IBM Plex Mono', 'SF Mono', monospace;
--grid: 8px;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* ── BACKGROUND — grid + ambient gradient ── */
@@ -264,8 +265,10 @@ section { padding: 120px 0; position: relative; z-index: 1; }
h1 {
font-size: clamp(3rem, 6vw, 5rem); font-weight: 700;
letter-spacing: -0.04em; line-height: 0.95; margin-bottom: 24px;
background: linear-gradient(135deg, var(--text) 45%, var(--accent) 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
h1 .accent { color: var(--accent); }
.hero-sub {
font-size: 1.125rem; color: var(--text-sec);
@@ -298,13 +301,25 @@ h1 .accent { color: var(--accent); }
position: relative;
animation: dashFloat 6s ease-in-out infinite;
box-shadow: var(--dash-shadow);
transition: background 0.5s var(--ease-out), box-shadow 0.5s var(--ease-out);
transition: background 0.5s var(--ease-out), box-shadow 0.5s var(--ease-out), transform 0.1s;
transform-style: preserve-3d;
perspective: 1000px;
}
@keyframes dashFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
/* ── 3D TILT (Feature #7) ── */
.hero-right.tilt-active {
animation: none;
transition: transform 0.15s var(--ease-out), box-shadow 0.3s var(--ease-out);
box-shadow: 0 20px 60px rgba(34,197,94,0.08), 0 0 1px var(--border);
}
.hero-right.tilt-active .dash-glow {
opacity: calc(var(--dash-glow-opacity) * 1.8);
}
.dash-glow {
position: absolute; top: -40%; left: -20%; width: 140%; height: 140%;
background: radial-gradient(circle, var(--accent), transparent 50%);
@@ -337,6 +352,13 @@ h1 .accent { color: var(--accent); }
50% { opacity:0.3; box-shadow: 0 0 0 4px transparent; }
}
/* ── LIVE CLOCK (Feature #5) ── */
.dash-clock {
font-size: 0.625rem; color: var(--text-dim); font-family: 'IBM Plex Mono', 'SF Mono', monospace;
letter-spacing: 0.05em;
}
.dash-clock .clock-time { color: var(--text-sec); }
.dash-body { display: grid; position: relative; z-index: 1; }
.dash-big {
@@ -351,12 +373,13 @@ h1 .accent { color: var(--accent); }
.dash-big-value {
font-size: clamp(3rem, 5vw, 5rem); font-weight: 700;
font-family: 'IBM Plex Mono', 'SF Mono', monospace;
line-height: 1;
line-height: 1; display: flex; align-items: center; justify-content: flex-end; gap: 12px;
}
.dash-big-value .accent { color: var(--accent); }
.dash-big-value .count-up { display: inline-block; }
.dash-big-trend {
display: inline-flex; align-items: center; gap: 6px;
margin-left: 16px; font-size: 0.875rem; color: var(--accent);
font-size: 0.875rem; color: var(--accent);
font-weight: 600;
}
.trend-icon { width: 14px; height: 14px; }
@@ -427,6 +450,64 @@ h1 .accent { color: var(--accent); }
transform: scaleX(1);
}
/* ── DATA FLOW (Feature #3) ── */
.data-flow {
border-bottom: 1px solid var(--border);
padding: 64px 0;
overflow: hidden;
position: relative;
}
.flow-inner {
display: flex; align-items: center; justify-content: center; gap: 0;
max-width: 900px; margin: 0 auto;
}
.flow-node {
display: flex; flex-direction: column; align-items: center; gap: 12px;
padding: 20px; position: relative; z-index: 1;
}
.flow-icon {
width: 56px; height: 56px; border-radius: 12px;
border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
background: var(--surface);
font-family: 'IBM Plex Mono', 'SF Mono', monospace;
font-size: 0.6875rem; font-weight: 700; color: var(--accent);
transition: all 0.4s var(--ease-out);
}
.flow-node:hover .flow-icon {
border-color: var(--accent);
box-shadow: 0 0 0 4px var(--accent-dim);
transform: translateY(-2px);
}
.flow-icon svg { width: 24px; height: 24px; stroke: var(--text-sec); fill: none; stroke-width: 1.5; }
.flow-node:hover .flow-icon svg { stroke: var(--accent); }
.flow-name {
font-size: 0.75rem; color: var(--text-sec); text-align: center;
font-family: 'IBM Plex Mono', 'SF Mono', monospace;
}
.flow-arrow {
display: flex; align-items: center; padding: 0 8px;
opacity: 0.3;
}
.flow-arrow svg { width: 40px; height: 12px; stroke: var(--accent); fill: none; stroke-width: 2; }
.flow-arrow .arrow-dash {
stroke-dasharray: 6 4;
animation: flowDash 1s linear infinite;
}
@keyframes flowDash {
to { stroke-dashoffset: -10; }
}
.flow-pulse {
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent);
animation: flowPulse 2s ease-in-out infinite;
box-shadow: 0 0 8px var(--accent-glow);
}
@keyframes flowPulse {
0%, 100% { transform: scale(1); opacity: 0.4; }
50% { transform: scale(1.5); opacity: 1; }
}
/* ── MANIFESTO ── */
.manifesto { padding: 160px 0; border-bottom: 1px solid var(--border); }
.manifesto-inner {
@@ -621,26 +702,40 @@ h1 .accent { color: var(--accent); }
font-style: italic;
}
/* ── FAQ ── */
/* ── FAQ — Enhanced (Feature #8) ── */
.faq-list { max-width: 800px; margin: 0 auto; }
.faq-item { border-bottom: 1px solid var(--border); }
.faq-q {
display: flex; align-items: center; justify-content: space-between;
padding: 24px 0; cursor: pointer; font-size: 1rem;
font-weight: 600; letter-spacing: -0.01em; transition: color 0.2s;
user-select: none;
}
.faq-q:hover { color: var(--accent); }
.faq-q::after {
content: '+'; font-family: 'IBM Plex Mono', 'SF Mono', monospace; font-size: 1.25rem;
color: var(--text-dim); transition: transform 0.3s var(--ease-out);
flex-shrink: 0; margin-left: 16px;
content: '+'; font-family: 'IBM Plex Mono', 'SF Mono', monospace; font-size: 1.5rem;
color: var(--text-dim); transition: all 0.4s var(--ease-spring);
flex-shrink: 0; margin-left: 16px; width: 24px; height: 24px;
display: flex; align-items: center; justify-content: center;
border-radius: 4px; border: 1px solid var(--border);
}
.faq-item.open .faq-q::after {
transform: rotate(45deg);
color: var(--accent);
border-color: var(--accent);
}
.faq-item.open .faq-q::after { transform: rotate(45deg); }
.faq-a { max-height: 0; overflow: hidden; transition: max-height 0.5s var(--ease-out); }
.faq-item.open .faq-a { max-height: 400px; }
.faq-item.open .faq-a { max-height: 500px; }
.faq-a-inner {
padding: 0 0 24px; font-size: 0.875rem; color: var(--text-sec);
line-height: 1.7;
opacity: 0;
transform: translateY(-8px);
transition: opacity 0.4s var(--ease-out) 0.1s, transform 0.4s var(--ease-out) 0.1s;
}
.faq-item.open .faq-a-inner {
opacity: 1;
transform: translateY(0);
}
/* ── FOOTER ── */
@@ -711,6 +806,8 @@ h1 .accent { color: var(--accent); }
.step:nth-child(2) { border-right: none; }
.step:nth-child(n+3) { border-top: 1px solid var(--border); }
.footer-inner { grid-template-columns: 1fr 1fr; }
.flow-inner { flex-wrap: wrap; gap: 16px; }
.flow-arrow { transform: rotate(90deg); }
}
@media (max-width: 640px) {
.wrap { padding: 0 16px; }
@@ -728,6 +825,7 @@ h1 .accent { color: var(--accent); }
.nav { display: none; }
.price-card { padding: 40px 24px; }
.price-amount { font-size: 3rem; }
.flow-arrow { display: none; }
}
</style>
</head>
@@ -768,7 +866,7 @@ h1 .accent { color: var(--accent); }
<div class="hero-inner">
<div class="hero-left" id="heroLeft">
<div class="hero-label">ДАННЫЕ. БЕЗ ШУМА.</div>
<h1>Вы теряете<br>деньги, <span class="accent">не зная</span><br>почему</h1>
<h1>Вы теряете<br>деньги, <span style="-webkit-text-fill-color: var(--accent); color: var(--accent);">не зная</span><br>почему</h1>
<p class="hero-sub">
Один дашборд с 10 ключевыми метриками вашего бизнеса.
Обновляется автоматически из 1С, CRM и банков.
@@ -810,13 +908,16 @@ h1 .accent { color: var(--accent); }
</svg>
ROI24 / DASHBOARD
</span>
<div style="display:flex;align-items:center;gap:12px;">
<span class="dash-clock" id="dashClock"><span class="clock-time">--:--:--</span></span>
<span class="dash-status"><span class="dash-dot"></span> LIVE</span>
</div>
</div>
<div class="dash-body">
<div class="dash-big">
<div class="dash-big-label">Выручка за 30 дней</div>
<div class="dash-big-value" id="bigValue">
12.4М <span class="accent">₽</span>
<span class="count-up">12.4</span><span class="accent">М ₽</span>
<span class="dash-big-trend">
<svg class="trend-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M8 12V4M8 4l-3 3M8 4l3 3"/>
@@ -827,28 +928,28 @@ h1 .accent { color: var(--accent); }
</div>
<div class="dash-cell">
<div class="dash-cell-label">Маржа</div>
<div class="dash-cell-value" style="color: var(--accent);" id="marginValue">23.4%</div>
<div class="dash-cell-value" style="color: var(--accent);" id="marginValue"><span class="count-up">23.4</span>%</div>
<div class="dash-cell-sub" id="marginNote">↑ 2.1% к прошлому месяцу</div>
</div>
<div class="dash-cell">
<div class="dash-cell-label">Касса</div>
<div class="dash-cell-value" id="cashValue">4.8М ₽</div>
<div class="dash-cell-value" id="cashValue"><span class="count-up">4.8</span>М ₽</div>
<div class="dash-cell-sub" id="cashNote">Дебиторка: 1.2М</div>
</div>
<div class="dash-tri" id="dashTri">
<div class="dash-mini">
<div class="dash-mini-label">Конверсия</div>
<div class="dash-mini-value" style="color: var(--blue);" id="mini1Val">2.8%</div>
<div class="dash-mini-value" style="color: var(--blue);" id="mini1Val"><span class="count-up">2.8</span>%</div>
<div class="dash-mini-bar"><div class="dash-mini-fill" id="mini1Bar" style="width:0%; background: var(--blue);"></div></div>
</div>
<div class="dash-mini">
<div class="dash-mini-label">CAC</div>
<div class="dash-mini-value" style="color: var(--amber);" id="mini2Val">890₽</div>
<div class="dash-mini-value" style="color: var(--amber);" id="mini2Val"><span class="count-up">890</span>₽</div>
<div class="dash-mini-bar"><div class="dash-mini-fill" id="mini2Bar" style="width:0%; background: var(--amber);"></div></div>
</div>
<div class="dash-mini">
<div class="dash-mini-label">ROI рекл.</div>
<div class="dash-mini-value" style="color: var(--accent);" id="mini3Val">91%</div>
<div class="dash-mini-value" style="color: var(--accent);" id="mini3Val"><span class="count-up">91</span>%</div>
<div class="dash-mini-bar"><div class="dash-mini-fill" id="mini3Bar" style="width:0%; background: var(--accent);"></div></div>
</div>
</div>
@@ -863,6 +964,47 @@ h1 .accent { color: var(--accent); }
</div>
</section>
<!-- ── DATA FLOW ── -->
<section class="data-flow">
<div class="wrap">
<div class="flow-inner reveal">
<div class="flow-node">
<div class="flow-icon">
<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z" stroke="none" fill="currentColor" opacity="0.1"/><path d="M4 4h16v16H4z" rx="2"/><path d="M8 4v16M16 4v16M4 12h16"/></svg>
</div>
<span class="flow-name">1С / CRM</span>
</div>
<div class="flow-arrow">
<svg viewBox="0 0 40 12"><path class="arrow-dash" d="M2 6h32M32 2l4 4-4 4"/></svg>
</div>
<div class="flow-node">
<div class="flow-icon">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" fill="none"/><path d="M3 9h18M9 3v18"/></svg>
</div>
<span class="flow-name">Агрегатор</span>
</div>
<div class="flow-arrow">
<svg viewBox="0 0 40 12"><path class="arrow-dash" d="M2 6h32M32 2l4 4-4 4"/></svg>
</div>
<div class="flow-node">
<div class="flow-icon">
<svg viewBox="0 0 24 24"><path d="M3 15l5-5 4 4 4-6 5 5" stroke="none" fill="var(--accent)" opacity="0.3"/><path d="M3 15l5-5 4 4 4-6 5 5" stroke="var(--accent)" stroke-width="1.5" fill="none"/></svg>
</div>
<span class="flow-name">Реклама</span>
</div>
<div class="flow-arrow">
<svg viewBox="0 0 40 12"><path class="arrow-dash" d="M2 6h32M32 2l4 4-4 4"/></svg>
</div>
<div class="flow-node">
<div class="flow-icon">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="3" stroke="var(--accent)" fill="none" stroke-width="1.5"/><path d="M8 12h8M12 8v8"/></svg>
</div>
<span class="flow-name" style="color: var(--accent); font-weight: 600;">ROI24</span>
</div>
</div>
</div>
</section>
<!-- ── MANIFESTO ── -->
<section class="manifesto">
<div class="manifesto-inner">
@@ -1128,7 +1270,6 @@ h1 .accent { color: var(--accent); }
const html = document.documentElement;
const themeToggle = document.getElementById('themeToggle');
// Check saved preference
const savedTheme = localStorage.getItem('roi24-theme');
if (savedTheme) {
html.setAttribute('data-theme', savedTheme);
@@ -1154,7 +1295,7 @@ window.addEventListener('load', () => {
setTimeout(() => {
document.querySelectorAll('.dash-mini-fill').forEach(bar => {
const target = bar.style.width || '0';
const target = bar.dataset.targetWidth || bar.style.width;
bar.style.width = '0';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
@@ -1163,8 +1304,68 @@ window.addEventListener('load', () => {
});
});
}, 800);
// ── Feature #2: Counter Animation ──
animateCounters();
});
/* ── Feature #2: Animated Counters ── */
function animateCounters() {
document.querySelectorAll('.count-up').forEach(el => {
if (el.dataset.animated) return;
el.dataset.animated = 'true';
const text = el.textContent;
const match = text.match(/^(\d+\.?\d*)/);
if (!match) return;
const target = parseFloat(match[1]);
const prefix = text.substring(0, text.indexOf(match[1]));
const suffix = text.substring(text.indexOf(match[1]) + match[1].length);
const duration = 1500;
const startTime = performance.now();
const startVal = 0;
function easeOutQuart(t) { return 1 - Math.pow(1 - t, 4); }
function update(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easeOutQuart(progress);
const currentVal = startVal + (target - startVal) * easedProgress;
let display;
if (text.includes('.')) {
display = prefix + currentVal.toFixed(1) + suffix;
} else {
display = prefix + Math.round(currentVal) + suffix;
}
el.textContent = display;
if (progress < 1) {
requestAnimationFrame(update);
} else {
el.textContent = text;
}
}
requestAnimationFrame(update);
});
}
/* ── Feature #5: Live Clock ── */
function updateClock() {
const now = new Date();
const timeStr = now.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const clockEl = document.getElementById('dashClock');
if (clockEl) {
clockEl.querySelector('.clock-time').textContent = timeStr;
}
}
setInterval(updateClock, 1000);
updateClock();
/* ── SCROLL REVEAL ── */
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
@@ -1180,34 +1381,37 @@ document.querySelectorAll('.reveal').forEach(el => revealObserver.observe(el));
/* ── DASHBOARD SEGMENT TOGGLE ── */
const SEGMENTS = {
ecom: {
revenue: '12.4М', revenueTrend: '+12%',
margin: '23.4%', marginNote: '↑ 2.1% к прошлому месяцу',
cash: '4.8М ₽', cashNote: 'Дебиторка: 1.2М',
revenue: '12.4', revenueTrend: '+12%',
margin: '23.4', marginNote: '↑ 2.1% к прошлому месяцу',
cash: '4.8', cashNote: 'Дебиторка: 1.2М',
mini: [
{ label: 'Конверсия', value: '2.8%', pct: 28, color: 'var(--blue)' },
{ label: 'CAC', value: '890₽', pct: 45, color: 'var(--amber)' },
{ label: 'ROI рекл.', value: '91%', pct: 91, color: 'var(--accent)' }
]
{ label: 'Конверсия', value: '2.8', pct: 28, color: 'var(--blue)' },
{ label: 'CAC', value: '890', pct: 45, color: 'var(--amber)' },
{ label: 'ROI рекл.', value: '91', pct: 91, color: 'var(--accent)' }
],
suffixes: ['М ₽', '%', 'М ₽', '%', '', '%']
},
services: {
revenue: '3.8М', revenueTrend: '+8%',
margin: '31.2%', marginNote: '↑ 1.4%',
cash: '2.1М ₽', cashNote: 'Пайплайн: 8.5М',
revenue: '3.8', revenueTrend: '+8%',
margin: '31.2', marginNote: '↑ 1.4%',
cash: '2.1', cashNote: 'Пайплайн: 8.5М',
mini: [
{ label: 'Загрузка', value: '78%', pct: 78, color: 'var(--blue)' },
{ label: 'Марж. проект', value: '34%', pct: 68, color: 'var(--amber)' },
{ label: 'LTV', value: '185К', pct: 82, color: 'var(--accent)' }
]
{ label: 'Загрузка', value: '78', pct: 78, color: 'var(--blue)' },
{ label: 'Марж. проект', value: '34', pct: 68, color: 'var(--amber)' },
{ label: 'LTV', value: '185', pct: 82, color: 'var(--accent)' }
],
suffixes: ['М ₽', '%', 'М ₽', '%', '', 'К']
},
production: {
revenue: '28.7М', revenueTrend: '+5%',
margin: '18.7%', marginNote: '↓ 0.8%',
cash: '6.2М ₽', cashNote: 'OEE: 72%',
revenue: '28.7', revenueTrend: '+5%',
margin: '18.7', marginNote: '↓ 0.8%',
cash: '6.2', cashNote: 'OEE: 72%',
mini: [
{ label: 'План/факт', value: '94%', pct: 94, color: 'var(--blue)' },
{ label: 'OEE', value: '72%', pct: 72, color: 'var(--amber)' },
{ label: 'Качество', value: '99.1%', pct: 99, color: 'var(--accent)' }
]
{ label: 'План/факт', value: '94', pct: 94, color: 'var(--blue)' },
{ label: 'OEE', value: '72', pct: 72, color: 'var(--amber)' },
{ label: 'Качество', value: '99.1', pct: 99, color: 'var(--accent)' }
],
suffixes: ['М ₽', '%', 'М ₽', '%', '', '%']
}
};
@@ -1227,9 +1431,10 @@ toggleBtns.forEach(btn => {
bigValue.style.transition = 'opacity 0.3s, transform 0.3s';
setTimeout(() => {
bigValue.innerHTML = `${seg.revenue} <span class="accent">₽</span><span class="dash-big-trend"><svg class="trend-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 12V4M8 4l-3 3M8 4l3 3"/></svg>${seg.revenueTrend}</span>`;
bigValue.innerHTML = `<span class="count-up">${seg.revenue}</span><span class="accent">${seg.suffixes[0]}</span><span class="dash-big-trend"><svg class="trend-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 12V4M8 4l-3 3M8 4l3 3"/></svg>${seg.revenueTrend}</span>`;
bigValue.style.opacity = '1';
bigValue.style.transform = 'translateY(0)';
animateCounters();
}, 300);
const marginValue = document.getElementById('marginValue');
@@ -1243,11 +1448,12 @@ toggleBtns.forEach(btn => {
});
setTimeout(() => {
marginValue.textContent = seg.margin;
marginValue.innerHTML = `<span class="count-up">${seg.margin}</span>%`;
marginNote.textContent = seg.marginNote;
cashValue.textContent = seg.cash;
cashValue.innerHTML = `<span class="count-up">${seg.cash}</span>${seg.suffixes[2]}`;
cashNote.textContent = seg.cashNote;
[marginValue, marginNote, cashValue, cashNote].forEach(el => { el.style.opacity = '1'; });
animateCounters();
}, 280);
seg.mini.forEach((m, i) => {
@@ -1257,7 +1463,7 @@ toggleBtns.forEach(btn => {
miniVal.style.transform = 'translateY(-4px)';
setTimeout(() => {
miniVal.textContent = m.value;
miniVal.innerHTML = `<span class="count-up">${m.value}</span>${seg.suffixes[4 + i] || ''}`;
miniVal.style.color = m.color;
miniVal.style.opacity = '1';
miniVal.style.transform = 'translateY(0)';
@@ -1269,7 +1475,7 @@ toggleBtns.forEach(btn => {
});
});
/* ── FAQ ACCORDION ── */
/* ── Feature #8: Enhanced FAQ Accordion ── */
document.querySelectorAll('.faq-q').forEach(q => {
q.addEventListener('click', () => {
const item = q.parentElement;
@@ -1279,23 +1485,29 @@ document.querySelectorAll('.faq-q').forEach(q => {
});
});
/* ── HERO PARALLAX ── */
/* ── Feature #7: 3D Tilt on Dashboard ── */
const hero = document.getElementById('hero');
const heroRight = document.getElementById('heroRight');
hero.addEventListener('mousemove', (e) => {
heroRight.classList.add('tilt-active');
const rect = hero.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
heroRight.style.transform = `translateY(${-6 + y * -4}px) perspective(800px) rotateY(${x * 1.5}deg) rotateX(${y * 1.5}deg)`;
heroRight.style.transform = `perspective(1200px) rotateX(${y * -8}deg) rotateY(${x * 8}deg) translateY(0)`;
heroRight.style.boxShadow = `${x * -30}px ${y * -30 + 20}px 80px rgba(34,197,94,0.06), 0 0 1px var(--border)`;
});
hero.addEventListener('mouseleave', () => {
heroRight.style.transform = 'translateY(0) perspective(800px) rotateY(0) rotateX(0)';
heroRight.style.transition = 'transform 0.6s var(--ease-out)';
heroRight.classList.remove('tilt-active');
heroRight.style.transform = '';
heroRight.style.boxShadow = '';
setTimeout(() => {
heroRight.style.transition = 'transform 0.1s, opacity 0.8s var(--ease-out) 0.3s, box-shadow 0.5s var(--ease-out), background 0.5s var(--ease-out)';
heroRight.style.transition = 'transform 0.6s var(--ease-out), box-shadow 0.6s var(--ease-out)';
setTimeout(() => {
heroRight.style.transition = '';
}, 600);
}, 100);
});
</script>