This repository has been archived on 2026-11-14. You can view files and clone it, but cannot push or open issues or pull requests.
anelk/index.html
2026-05-19 12:12:29 +05:00

894 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Anelk — AI Education Analytics</title>
<style>
:root {
--bg: #f6f7fb;
--bg-2: #eef0f7;
--ink: #0b1020;
--ink-2: #3b4256;
--muted: #7a8194;
--line: rgba(11, 16, 32, 0.08);
--card: rgba(255, 255, 255, 0.65);
--card-strong: rgba(255, 255, 255, 0.85);
--grid-line: rgba(11,16,32,.06);
--hover-tint: rgba(11,16,32,.05);
--accent: #2563ff;
--accent-2: #6f7bff;
--good: #16a36a;
--warn: #d39a1c;
--bad: #dc3855;
--crit: #a31736;
--shadow: 0 30px 80px rgba(20, 28, 60, 0.10), 0 8px 24px rgba(20, 28, 60, 0.06);
--bg-gradient-1: #dfe6ff;
--bg-gradient-2: #ffe2f0;
--bg-gradient-3: #d8f3ff;
--btn-primary-bg: #0b1020;
--btn-primary-ink: #ffffff;
--feed-bg: rgba(255,255,255,.55);
--radius: 22px;
--radius-sm: 14px;
}
html[data-theme="dark"] {
--bg: #0a0d18;
--bg-2: #0f1322;
--ink: #f2f4f9;
--ink-2: #b8bdce;
--muted: #7d8499;
--line: rgba(255,255,255,0.08);
--card: rgba(22, 27, 42, 0.55);
--card-strong: rgba(22, 27, 42, 0.78);
--grid-line: rgba(255,255,255,.06);
--hover-tint: rgba(255,255,255,.05);
--accent: #7aa2ff;
--accent-2: #9aa8ff;
--good: #4cd29a;
--warn: #f0c060;
--bad: #ff6b85;
--crit: #ff3a63;
--shadow: 0 30px 80px rgba(0, 0, 0, 0.45), 0 8px 24px rgba(0, 0, 0, 0.25);
--bg-gradient-1: #1a2350;
--bg-gradient-2: #401a30;
--bg-gradient-3: #0e2a3a;
--btn-primary-bg: #f2f4f9;
--btn-primary-ink: #0a0d18;
--feed-bg: rgba(22,27,42,.5);
}
@media (prefers-color-scheme: dark) {
html:not([data-theme]) {
--bg: #0a0d18;
--bg-2: #0f1322;
--ink: #f2f4f9;
--ink-2: #b8bdce;
--muted: #7d8499;
--line: rgba(255,255,255,0.08);
--card: rgba(22, 27, 42, 0.55);
--card-strong: rgba(22, 27, 42, 0.78);
--grid-line: rgba(255,255,255,.06);
--hover-tint: rgba(255,255,255,.05);
--accent: #7aa2ff;
--accent-2: #9aa8ff;
--good: #4cd29a;
--warn: #f0c060;
--bad: #ff6b85;
--crit: #ff3a63;
--shadow: 0 30px 80px rgba(0, 0, 0, 0.45), 0 8px 24px rgba(0, 0, 0, 0.25);
--bg-gradient-1: #1a2350;
--bg-gradient-2: #401a30;
--bg-gradient-3: #0e2a3a;
--btn-primary-bg: #f2f4f9;
--btn-primary-ink: #0a0d18;
--feed-bg: rgba(22,27,42,.5);
}
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background:
radial-gradient(1200px 800px at 85% -10%, var(--bg-gradient-1) 0%, transparent 60%),
radial-gradient(900px 700px at -10% 10%, var(--bg-gradient-2) 0%, transparent 55%),
radial-gradient(900px 700px at 50% 120%, var(--bg-gradient-3) 0%, transparent 60%),
var(--bg);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter",
"Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-feature-settings: "ss01", "cv11";
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
letter-spacing: -0.01em;
line-height: 1.5;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }
.glass {
background: var(--card);
backdrop-filter: saturate(180%) blur(28px);
-webkit-backdrop-filter: saturate(180%) blur(28px);
border: 1px solid var(--line);
box-shadow: var(--shadow);
}
/* NAV */
.nav {
position: sticky;
top: 16px;
z-index: 50;
margin: 16px auto 0;
width: min(1180px, calc(100% - 32px));
border-radius: 999px;
padding: 8px;
}
.nav-inner {
display: flex;
align-items: center;
gap: 24px;
padding: 0 12px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 600;
letter-spacing: -0.02em;
padding: 8px 12px;
}
.logo-dot {
width: 22px; height: 22px; border-radius: 8px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
box-shadow: 0 6px 20px rgba(37, 99, 255, .35);
}
.nav nav {
display: flex; gap: 6px; margin-left: auto;
}
.nav nav a {
padding: 8px 14px;
border-radius: 999px;
color: var(--ink-2);
font-size: 14.5px;
}
.nav nav a:hover { background: var(--hover-tint); color: var(--ink); }
.icon-btn {
width: 40px; padding: 0;
background: transparent;
border-color: var(--line);
color: var(--ink);
font-size: 16px;
}
.icon-btn:hover { background: var(--hover-tint); }
.btn {
display: inline-flex; align-items: center; justify-content: center;
height: 40px; padding: 0 18px;
border-radius: 999px;
font-weight: 500;
font-size: 14.5px;
border: 1px solid transparent;
transition: transform .15s ease, background .2s ease, box-shadow .2s ease;
white-space: nowrap;
}
.btn-primary {
background: var(--btn-primary-bg);
color: var(--btn-primary-ink);
box-shadow: 0 10px 24px rgba(11,16,32,.25);
}
.btn-primary:hover { transform: translateY(-1px); }
.btn-ghost {
background: transparent;
color: var(--ink);
border-color: var(--line);
}
.btn-ghost:hover { background: var(--hover-tint); }
/* HERO */
.hero {
max-width: 1180px;
margin: 80px auto 40px;
padding: 0 24px;
}
.hero-grid {
display: grid;
grid-template-columns: 1.15fr 1fr;
gap: 48px;
align-items: center;
}
.eyebrow {
display: inline-block;
font-size: 12.5px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent);
background: rgba(37,99,255,.08);
padding: 6px 12px;
border-radius: 999px;
margin-bottom: 22px;
}
h1 {
font-size: clamp(40px, 5.2vw, 68px);
line-height: 1.04;
letter-spacing: -0.035em;
font-weight: 600;
margin: 0 0 22px;
}
.lede {
font-size: 18px;
color: var(--ink-2);
max-width: 56ch;
margin: 0 0 28px;
}
.hero-cta { display: flex; gap: 12px; flex-wrap: wrap; }
.hero-stats {
display: flex; gap: 36px; margin-top: 40px;
}
.hero-stats div { display: flex; flex-direction: column; }
.hero-stats b {
font-size: 28px; font-weight: 600; letter-spacing: -0.02em;
}
.hero-stats span { color: var(--muted); font-size: 13.5px; }
.hero-card {
border-radius: var(--radius);
padding: 26px;
}
.float { animation: float 8s ease-in-out infinite; }
@keyframes float {
0%,100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.card-head {
display: flex; justify-content: space-between; align-items: flex-start;
gap: 16px; margin-bottom: 16px;
}
.card-title { font-weight: 600; font-size: 18px; letter-spacing: -0.01em; }
.muted { color: var(--muted); font-size: 13.5px; }
.muted.small { font-size: 12.5px; }
.pill {
font-size: 12px; padding: 5px 10px; border-radius: 999px;
background: var(--grid-line); color: var(--ink-2);
}
.pill-warn { background: rgba(211,154,28,.14); color: #8a6404; }
.pill-soft { background: rgba(37,99,255,.10); color: var(--accent); }
.ring-wrap {
position: relative; display: grid; place-items: center;
width: 100%; padding: 12px 0 8px;
}
.ring { width: 180px; height: 180px; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: var(--line); stroke-width: 10; }
.ring-fg {
fill: none;
stroke: url(#g) var(--bad);
stroke: var(--bad);
stroke-width: 10;
stroke-linecap: round;
stroke-dasharray: 314;
stroke-dashoffset: 100;
transition: stroke-dashoffset 1.2s ease;
}
.ring-label {
position: absolute; text-align: center;
}
.ring-label b { font-size: 32px; letter-spacing: -0.02em; display: block; }
.ring-label span { color: var(--muted); font-size: 13px; }
.mini-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 14px 24px;
margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--line);
}
.mini-grid b { font-size: 18px; letter-spacing: -0.01em; }
.mini-grid div { display: flex; flex-direction: column; gap: 2px; }
.up { color: var(--good); font-size: 13.5px; font-weight: 500; }
.down { color: var(--bad); font-size: 13.5px; font-weight: 500; }
/* SECTIONS */
.section {
max-width: 1180px;
margin: 100px auto;
padding: 0 24px;
}
.section-head { max-width: 820px; margin-bottom: 40px; }
.section-head h2 {
font-size: clamp(30px, 3.6vw, 46px);
line-height: 1.08;
letter-spacing: -0.03em;
font-weight: 600;
margin: 0 0 16px;
}
/* DASHBOARD */
.dash-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.card {
border-radius: var(--radius);
padding: 22px;
}
.card.span-2 { grid-column: span 2; }
.card-row {
display: flex; justify-content: space-between; align-items: center;
gap: 16px; margin-bottom: 16px;
}
.card-row h3 {
margin: 4px 0 0; font-size: 20px; font-weight: 600; letter-spacing: -0.02em;
}
.kpi {
display: flex; align-items: baseline; gap: 10px; margin: 6px 0 10px;
}
.kpi b { font-size: 36px; font-weight: 600; letter-spacing: -0.02em; }
.seg {
display: inline-flex; padding: 4px;
background: var(--hover-tint);
border-radius: 999px;
}
.seg-btn {
border: 0; background: transparent;
padding: 7px 14px; border-radius: 999px;
font-size: 13px; color: var(--ink-2);
}
.seg-btn.is-active {
background: var(--card-strong); color: var(--ink);
box-shadow: 0 4px 12px rgba(0,0,0,.08);
}
.chart { width: 100%; height: 260px; display: block; }
.spark { width: 100%; height: 80px; display: block; margin: 6px 0 8px; }
.feed { list-style: none; padding: 0; margin: 0; display: grid; gap: 10px; }
.feed li {
display: grid;
grid-template-columns: 12px 1fr auto;
gap: 14px;
align-items: center;
padding: 12px 14px;
border-radius: var(--radius-sm);
background: var(--feed-bg);
border: 1px solid var(--line);
}
.feed .ldot {
width: 10px; height: 10px; border-radius: 50%;
background: var(--accent);
}
.feed .ldot.warn { background: var(--warn); }
.feed .ldot.bad { background: var(--bad); }
.feed .ldot.good { background: var(--good); }
.feed time { color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums; }
/* PROBLEMS */
.problem-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.problem { display: flex; flex-direction: column; gap: 10px; }
.prob-icon {
width: 44px; height: 44px; border-radius: 14px;
background: linear-gradient(135deg, rgba(37,99,255,.12), rgba(111,123,255,.10));
display: grid; place-items: center;
font-size: 22px; color: var(--accent);
margin-bottom: 6px;
}
.problem h3 { margin: 0; font-size: 19px; font-weight: 600; letter-spacing: -0.01em; }
.problem p { color: var(--ink-2); margin: 0 0 8px; font-size: 15px; }
.bar { height: 8px; background: var(--grid-line); border-radius: 999px; overflow: hidden; }
.bar span {
display: block; height: 100%;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
border-radius: 999px;
}
/* REVEAL ANIMATION */
.reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity .8s cubic-bezier(.2,.7,.2,1), transform .8s cubic-bezier(.2,.7,.2,1);
will-change: opacity, transform;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.reveal { opacity: 1; transform: none; transition: none; }
.float { animation: none; }
}
/* FOOTER */
footer { padding: 28px 0 48px; }
.foot {
max-width: 1180px; margin: 0 auto; padding: 0 24px;
display: flex; justify-content: space-between; align-items: center;
color: var(--ink-2); font-size: 13.5px;
}
/* CHARTS SVG */
.chart .grid { stroke: var(--grid-line); stroke-width: 1; }
.chart .axis { fill: var(--muted); font-size: 11px; font-family: inherit; }
.chart .area { fill: url(#areaG); opacity: .9; }
.chart .line { fill: none; stroke: var(--accent); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.chart .point { fill: var(--card-strong); stroke: var(--accent); stroke-width: 2; }
.chart .bar { fill: url(#barG); rx: 6; }
.spark .sline { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linecap: round; }
.spark .sarea { fill: rgba(37,99,255,.12); }
/* RESPONSIVE */
@media (max-width: 980px) {
.hero-grid { grid-template-columns: 1fr; gap: 36px; }
.dash-grid { grid-template-columns: 1fr 1fr; }
.card.span-2 { grid-column: span 2; }
.problem-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 640px) {
.nav { top: 8px; }
.nav nav { display: none; }
.hero { margin-top: 48px; }
.hero-stats { gap: 22px; }
.dash-grid { grid-template-columns: 1fr; }
.card.span-2 { grid-column: auto; }
.problem-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header class="nav glass">
<div class="nav-inner">
<a class="brand" href="#top">
<span class="logo-dot"></span>
Anelk
</a>
<nav>
<a href="#dashboard">Dashboard</a>
<a href="#problems">Аналитика</a>
</nav>
<button class="btn btn-ghost icon-btn" id="themeToggle" aria-label="Сменить тему">
<span id="themeIcon">☾</span>
</button>
<a class="btn btn-primary" href="#dashboard">Открыть демо</a>
</div>
</header>
<main id="top">
<!-- HERO -->
<section class="hero">
<div class="hero-grid">
<div class="hero-copy">
<span class="eyebrow">AI для образования · 2026</span>
<h1>Видеть проблемы в&nbsp;школах до того, как они станут кризисом.</h1>
<p class="lede">
Anelk — платформа аналитики для школ и&nbsp;министерств. Машинное обучение
прогнозирует риск отчисления, падение вовлечённости и&nbsp;точки давления
в&nbsp;регионах Казахстана за&nbsp;недели до того, как они проявятся в&nbsp;отчётах.
</p>
<div class="hero-cta">
<a class="btn btn-primary" href="#dashboard">Посмотреть dashboard</a>
<a class="btn btn-ghost" href="#problems">Как это работает</a>
</div>
<div class="hero-stats">
<div><b>17</b><span>регионов</span></div>
<div><b>7 312</b><span>школ</span></div>
<div><b>94%</b><span>точность модели</span></div>
</div>
</div>
<div class="hero-card glass float">
<div class="card-head">
<div>
<div class="muted">Школа · Алматы №48</div>
<div class="card-title">Прогноз на&nbsp;30&nbsp;дней</div>
</div>
<div class="pill pill-warn">Внимание</div>
</div>
<div class="ring-wrap">
<svg viewBox="0 0 120 120" class="ring">
<circle cx="60" cy="60" r="50" class="ring-bg"></circle>
<circle cx="60" cy="60" r="50" class="ring-fg" id="heroRing"></circle>
</svg>
<div class="ring-label">
<b id="heroRingValue">68%</b>
<span>риск отчисления</span>
</div>
</div>
<div class="mini-grid">
<div><span class="muted">Посещаемость</span><b>82%</b></div>
<div><span class="muted">Вовлечённость</span><b>61%</b></div>
<div><span class="muted">Успеваемость</span><b>3.8</b></div>
<div><span class="muted">Тренд</span><b class="down">−4.2%</b></div>
</div>
</div>
</div>
</section>
<!-- DASHBOARD -->
<section id="dashboard" class="section">
<div class="section-head">
<span class="eyebrow">Dashboard</span>
<h2>Одна панель для&nbsp;всей системы образования.</h2>
<p class="lede">
Каждая школа, район и&nbsp;регион — в&nbsp;одном пространстве. Метрики обновляются
ежедневно, аномалии подсвечиваются автоматически.
</p>
</div>
<div class="dash-grid">
<article class="card glass span-2">
<header class="card-row">
<div>
<div class="muted">Риск отчисления · по&nbsp;стране</div>
<h3>12 462 учащихся в&nbsp;зоне риска</h3>
</div>
<div class="seg">
<button class="seg-btn is-active" data-range="30">30д</button>
<button class="seg-btn" data-range="90">90д</button>
<button class="seg-btn" data-range="365">Год</button>
</div>
</header>
<svg id="riskChart" class="chart" viewBox="0 0 800 260" preserveAspectRatio="none"></svg>
</article>
<article class="card glass">
<div class="muted">Средняя посещаемость</div>
<div class="kpi">
<b>87.4%</b>
<span class="up">+1.2%</span>
</div>
<svg id="attendanceSpark" class="spark" viewBox="0 0 300 80" preserveAspectRatio="none"></svg>
<div class="muted small">vs. прошлая неделя</div>
</article>
<article class="card glass">
<div class="muted">Вовлечённость</div>
<div class="kpi">
<b>72.1</b>
<span class="down">−2.4</span>
</div>
<svg id="engagementSpark" class="spark" viewBox="0 0 300 80" preserveAspectRatio="none"></svg>
<div class="muted small">индекс из&nbsp;100</div>
</article>
<article class="card glass">
<div class="muted">Средний балл</div>
<div class="kpi">
<b>3.94</b>
<span class="up">+0.05</span>
</div>
<svg id="gradeSpark" class="spark" viewBox="0 0 300 80" preserveAspectRatio="none"></svg>
<div class="muted small">по&nbsp;5-балльной шкале</div>
</article>
<article class="card glass span-2">
<header class="card-row">
<div>
<div class="muted">Распределение по&nbsp;классам</div>
<h3>Где концентрируются проблемы</h3>
</div>
</header>
<svg id="gradeBars" class="chart" viewBox="0 0 800 220" preserveAspectRatio="none"></svg>
</article>
<article class="card glass span-2">
<header class="card-row">
<div>
<div class="muted">Сигналы AI · последние 24ч</div>
<h3>Что заметила модель</h3>
</div>
<span class="pill pill-soft">live</span>
</header>
<ul class="feed" id="feed"></ul>
</article>
</div>
</section>
<!-- PROBLEMS -->
<section id="problems" class="section">
<div class="section-head">
<span class="eyebrow">Аналитика проблем</span>
<h2>Четыре сигнала, которые предсказывают всё остальное.</h2>
</div>
<div class="problem-grid">
<article class="card glass problem">
<div class="prob-icon">○</div>
<h3>Пропуски занятий</h3>
<p>Anelk видит закономерности прогулов раньше классного руководителя: дни недели, предметы, кластеры учеников.</p>
<div class="bar"><span style="width:78%"></span></div>
<div class="muted small">78% школ имеют скрытые паттерны прогулов</div>
</article>
<article class="card glass problem">
<div class="prob-icon">◐</div>
<h3>Низкая вовлечённость</h3>
<p>Сигналы LMS, активность на&nbsp;уроке и&nbsp;скорость выполнения домашних — индекс вовлечённости в&nbsp;реальном времени.</p>
<div class="bar"><span style="width:64%"></span></div>
<div class="muted small">64% учеников группы риска теряют интерес до спада оценок</div>
</article>
<article class="card glass problem">
<div class="prob-icon">◑</div>
<h3>Снижение успеваемости</h3>
<p>Тренды по&nbsp;предметам и&nbsp;когортам. Модель отделяет шум от устойчивого падения.</p>
<div class="bar"><span style="width:53%"></span></div>
<div class="muted small">−0.4 балла — критический порог для вмешательства</div>
</article>
<article class="card glass problem">
<div class="prob-icon">●</div>
<h3>Проблемные регионы</h3>
<p>Корреляции по&nbsp;инфраструктуре, кадрам и&nbsp;демографии. Помогает министерству распределять ресурсы.</p>
<div class="bar"><span style="width:41%"></span></div>
<div class="muted small">41% бюджетных мер можно перенаправить точнее</div>
</article>
</div>
</section>
</main>
<footer>
<div class="foot">
<span>© 2026 Anelk</span>
<span class="muted">AI Education Analytics · сделано в&nbsp;Казахстане</span>
</div>
</footer>
<script>
// Anelk — landing demo data + SVG charts. No external deps.
// ---------- utils ----------
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const rand = (seed) => {
// small seeded PRNG for stable demo data
let s = seed | 0;
return () => {
s = (s * 1664525 + 1013904223) | 0;
return ((s >>> 0) % 100000) / 100000;
};
};
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
// ---------- hero ring ----------
(function heroRing() {
const ring = $("#heroRing");
const val = 0.68;
const C = 2 * Math.PI * 50;
ring.setAttribute("stroke-dasharray", C.toFixed(2));
ring.setAttribute("stroke-dashoffset", (C * (1 - val)).toFixed(2));
ring.style.stroke =
val > 0.75 ? "var(--crit)" : val > 0.5 ? "var(--bad)" : val > 0.3 ? "var(--warn)" : "var(--good)";
})();
// ---------- risk chart ----------
function buildLineChart(svg, points, opts = {}) {
const W = 800, H = 260, P = { t: 20, r: 20, b: 32, l: 40 };
const xs = points.map((_, i) => i);
const ys = points;
const xMin = 0, xMax = xs.length - 1;
const yMin = Math.min(...ys) - 2;
const yMax = Math.max(...ys) + 2;
const x = (i) => P.l + ((i - xMin) / (xMax - xMin)) * (W - P.l - P.r);
const y = (v) => P.t + (1 - (v - yMin) / (yMax - yMin)) * (H - P.t - P.b);
const gridLines = [];
for (let i = 0; i < 5; i++) {
const gy = P.t + (i * (H - P.t - P.b)) / 4;
const yv = yMax - (i * (yMax - yMin)) / 4;
gridLines.push(`<line class="grid" x1="${P.l}" y1="${gy}" x2="${W - P.r}" y2="${gy}"/>`);
gridLines.push(`<text class="axis" x="${P.l - 8}" y="${gy + 4}" text-anchor="end">${Math.round(yv)}</text>`);
}
const xticks = opts.labels || [];
const xAxis = xticks.map((lbl, i) => {
const px = x((i / (xticks.length - 1)) * (xs.length - 1));
return `<text class="axis" x="${px}" y="${H - 10}" text-anchor="middle">${lbl}</text>`;
}).join("");
const linePath = points.map((v, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
const areaPath = `${linePath} L${x(xMax).toFixed(1)},${H - P.b} L${x(0).toFixed(1)},${H - P.b} Z`;
const pts = points
.map((v, i) => (i % Math.ceil(points.length / 8) === 0 ? `<circle class="point" cx="${x(i)}" cy="${y(v)}" r="3.5"/>` : ""))
.join("");
svg.innerHTML = `
<defs>
<linearGradient id="areaG" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#2563ff" stop-opacity=".28"/>
<stop offset="100%" stop-color="#2563ff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="barG" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#2563ff"/>
<stop offset="100%" stop-color="#6f7bff"/>
</linearGradient>
</defs>
${gridLines.join("")}
${xAxis}
<path class="area" d="${areaPath}"/>
<path class="line" d="${linePath}"/>
${pts}
`;
}
function genRisk(days, seed) {
const r = rand(seed);
const out = [];
let v = 9 + r() * 4;
for (let i = 0; i < days; i++) {
v += (r() - 0.45) * 1.2;
// weekly cycle
v += Math.sin((i / 7) * Math.PI) * 0.4;
v = clamp(v, 4, 22);
out.push(+v.toFixed(2));
}
// express as thousands of students
return out.map((x) => +(x * 1000 + 0).toFixed(0)).map((x) => x / 100);
}
function labelsForRange(days) {
if (days <= 30) return ["−30д", "−20д", "−10д", "сегодня"];
if (days <= 90) return ["−90д", "−60д", "−30д", "сегодня"];
return ["янв", "апр", "июл", "окт", "сегодня"];
}
const riskSvg = $("#riskChart");
function renderRisk(days) {
const data = genRisk(days, days);
buildLineChart(riskSvg, data, { labels: labelsForRange(days) });
}
renderRisk(30);
$$(".seg-btn").forEach((b) => {
b.addEventListener("click", () => {
$$(".seg-btn").forEach((x) => x.classList.remove("is-active"));
b.classList.add("is-active");
renderRisk(+b.dataset.range);
});
});
// ---------- sparklines ----------
function buildSpark(svg, points, opts = {}) {
const W = 300, H = 80, P = 4;
const yMin = Math.min(...points);
const yMax = Math.max(...points);
const x = (i) => P + (i / (points.length - 1)) * (W - 2 * P);
const y = (v) => P + (1 - (v - yMin) / (yMax - yMin || 1)) * (H - 2 * P);
const d = points.map((v, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
const area = `${d} L${x(points.length - 1).toFixed(1)},${H - P} L${x(0).toFixed(1)},${H - P} Z`;
const stroke = opts.color || "var(--accent)";
svg.innerHTML = `
<path class="sarea" d="${area}" style="fill:${opts.fill || "rgba(37,99,255,.12)"}"/>
<path class="sline" d="${d}" style="stroke:${stroke}"/>
`;
}
function spark(n, seed, drift = 0) {
const r = rand(seed);
const out = [];
let v = 50;
for (let i = 0; i < n; i++) {
v += (r() - 0.5) * 6 + drift;
out.push(v);
}
return out;
}
buildSpark($("#attendanceSpark"), spark(28, 11, 0.1), { color: "var(--good)", fill: "rgba(22,163,106,.14)" });
buildSpark($("#engagementSpark"), spark(28, 22, -0.15), { color: "var(--bad)", fill: "rgba(220,56,85,.12)" });
buildSpark($("#gradeSpark"), spark(28, 33, 0.04), { color: "var(--accent)" });
// ---------- grade bars ----------
(function bars() {
const svg = $("#gradeBars");
const W = 800, H = 220, P = { t: 16, r: 16, b: 36, l: 36 };
const grades = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"];
const r = rand(7);
const vals = grades.map((_, i) => 8 + r() * 18 + (i > 6 ? 10 : 0));
const max = Math.max(...vals) * 1.15;
const bw = (W - P.l - P.r) / grades.length - 14;
const bars = vals
.map((v, i) => {
const x = P.l + i * ((W - P.l - P.r) / grades.length) + 7;
const h = (v / max) * (H - P.t - P.b);
const y = H - P.b - h;
return `<rect class="bar" x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${h.toFixed(1)}" rx="6"/>`;
})
.join("");
const labels = grades
.map((g, i) => {
const x = P.l + i * ((W - P.l - P.r) / grades.length) + bw / 2 + 7;
return `<text class="axis" x="${x}" y="${H - 14}" text-anchor="middle">${g} класс</text>`;
})
.join("");
const gridLines = [];
for (let i = 0; i < 4; i++) {
const gy = P.t + (i * (H - P.t - P.b)) / 3;
const yv = Math.round(max - (i * max) / 3);
gridLines.push(`<line class="grid" x1="${P.l}" y1="${gy}" x2="${W - P.r}" y2="${gy}"/>`);
gridLines.push(`<text class="axis" x="${P.l - 8}" y="${gy + 4}" text-anchor="end">${yv}%</text>`);
}
svg.innerHTML = `
<defs>
<linearGradient id="barG" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#2563ff"/>
<stop offset="100%" stop-color="#6f7bff"/>
</linearGradient>
</defs>
${gridLines.join("")}
${bars}
${labels}
`;
})();
// ---------- AI feed ----------
const FEED = [
{ t: "2 мин назад", c: "warn", text: "Школа №48, Алматы — рост прогулов по математике на 18% за неделю." },
{ t: "14 мин", c: "bad", text: "Кызылординская обл. — индекс вовлечённости упал ниже 60 в 12 школах." },
{ t: "1 ч", c: "good", text: "Шымкент — вмешательство сработало: посещаемость +4.6% за 14 дней." },
{ t: "2 ч", c: "warn", text: "Северный Казахстан — 287 учеников переведены в группу высокого риска." },
{ t: "4 ч", c: "bad", text: "Туркестанская обл. — снижение среднего балла по физике −0.42." },
{ t: "вчера", c: "", text: "Обновлены прогнозы по 7 312 школам. Модель v0.9.1." },
];
$("#feed").innerHTML = FEED.map(
(f) => `<li><span class="ldot ${f.c}"></span><span>${f.text}</span><time>${f.t}</time></li>`
).join("");
// ---------- theme toggle ----------
(function theme() {
const root = document.documentElement;
const btn = $("#themeToggle");
const icon = $("#themeIcon");
if (!btn || !icon) return;
const safeGet = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
const safeSet = (k, v) => { try { localStorage.setItem(k, v); } catch { /* private mode */ } };
const systemDark = typeof window.matchMedia === "function"
&& window.matchMedia("(prefers-color-scheme: dark)").matches;
const saved = safeGet("anelk-theme");
apply(saved || (systemDark ? "dark" : "light"));
function apply(mode) {
root.setAttribute("data-theme", mode);
icon.textContent = mode === "dark" ? "☀" : "☾";
}
btn.addEventListener("click", () => {
const next = root.getAttribute("data-theme") === "dark" ? "light" : "dark";
apply(next);
safeSet("anelk-theme", next);
});
})();
// ---------- scroll reveal ----------
(function reveal() {
const targets = $$(".section, .hero-copy, .hero-card");
targets.forEach((el) => el.classList.add("reveal"));
if (!("IntersectionObserver" in window)) {
targets.forEach((el) => el.classList.add("is-visible"));
return;
}
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add("is-visible");
io.unobserve(e.target);
}
});
},
{ threshold: 0.12, rootMargin: "0px 0px -10% 0px" }
);
targets.forEach((el) => io.observe(el));
})();
</script>
</body>
</html>