Files
Learn_System/frontend/collection.html
Maxim Dolgolyov 5381679c68 chore: консолидация незакоммиченной работы (биохимия + System Health + lab/textbooks)
Зафиксирована накопленная незакоммиченная работа рабочего дерева, КРОМЕ файлов
учебника «Химия 7» (migration 046, chemistry_7_*.html, chem7_svg.js, тест —
оставлены незакоммиченными по запросу).

Включает: модуль биохимии (ядро BIO, 3D VSEPR, химдвижок, баланс, challenges,
пути из БД), System Health Level 1 (вердикт/мониторинг), а также frontend-
страницы и lab/textbooks-правки параллельной сессии.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:12:55 +03:00

506 lines
23 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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.0" />
<title>Коллекция — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/ls.css" />
<style>
.sb-content { padding: 0; }
.col-wrap {
min-height: 100vh;
padding: 28px 24px 60px;
max-width: 1100px; margin: 0 auto;
}
/* Header */
.col-header { display:flex; align-items:center; gap:14px; margin-bottom:20px; }
.col-icon {
width:52px; height:52px; border-radius:14px; flex-shrink:0;
background: linear-gradient(135deg,rgba(249,199,79,.2),rgba(155,93,229,.15));
border:1.5px solid rgba(255,255,255,.1);
display:flex; align-items:center; justify-content:center;
}
.col-icon svg { width:26px; height:26px; stroke:#F9C74F; stroke-width:1.8; fill:none; }
.col-title { font-family:'Unbounded',sans-serif; font-size:1.3rem; font-weight:800; }
.col-sub { font-size:.82rem; color:var(--text-2); margin-top:2px; }
/* Summary bar */
.col-summary {
display:flex; gap:10px; flex-wrap:wrap; margin-bottom:20px;
}
.col-sum-card {
background:var(--surface); border:1.5px solid rgba(255,255,255,.08);
border-radius:14px; padding:12px 18px;
display:flex; align-items:center; gap:10px;
min-width:110px;
}
.col-sum-icon { width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.col-sum-icon svg { width:16px; height:16px; stroke:currentColor; fill:none; stroke-width:2; }
.col-sum-val { font-family:'Unbounded',sans-serif; font-size:1rem; font-weight:800; }
.col-sum-label { font-size:.72rem; color:var(--text-2); margin-top:1px; }
/* Tier legend */
.tier-legend { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px; align-items:center; }
.tier-dot {
display:inline-flex; align-items:center; gap:5px;
font-size:.75rem; font-weight:600; color:var(--text-2); cursor:pointer;
padding:4px 10px; border-radius:99px; border:1.5px solid transparent; transition:all .15s;
}
.tier-dot.active { color:var(--text); }
.tier-dot-circle { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
/* Filter */
.col-filter { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:18px; align-items:center; }
.col-pill {
padding:5px 14px; border-radius:99px; border:1.5px solid rgba(255,255,255,.12);
font-size:.78rem; font-weight:600; cursor:pointer; transition:all .15s;
background:transparent; color:var(--text-2);
}
.col-pill:hover { border-color:rgba(249,199,79,.4); color:#F9C74F; }
.col-pill.active { background:rgba(249,199,79,.12); border-color:#F9C74F; color:#F9C74F; }
/* Search */
.col-search-wrap { position:relative; margin-left:auto; }
.col-search {
padding:6px 12px 6px 34px; border-radius:99px;
border:1.5px solid rgba(255,255,255,.12);
background:rgba(255,255,255,.05); color:var(--text);
font-family:'Manrope',sans-serif; font-size:.82rem; outline:none; width:200px;
transition:border-color .2s;
}
.col-search:focus { border-color:rgba(249,199,79,.4); }
.col-search-icon {
position:absolute; left:10px; top:50%; transform:translateY(-50%);
color:var(--text-2); pointer-events:none;
}
.col-search-icon svg { width:14px; height:14px; stroke:currentColor; fill:none; stroke-width:2; }
/* Progress bar */
.col-progress-bar {
background:var(--surface); border:1.5px solid rgba(255,255,255,.08);
border-radius:14px; padding:14px 18px; margin-bottom:20px;
display:flex; align-items:center; gap:16px;
}
.col-prog-label { font-size:.82rem; color:var(--text-2); flex-shrink:0; }
.col-prog-bar { flex:1; height:8px; border-radius:99px; background:rgba(255,255,255,.08); overflow:hidden; }
.col-prog-fill { height:100%; border-radius:99px; background:linear-gradient(90deg,#9B5DE5,#F9C74F); transition:width .8s ease; }
.col-prog-pct { font-family:'Unbounded',sans-serif; font-size:.88rem; font-weight:800; color:#F9C74F; flex-shrink:0; }
/* Cards grid */
.col-grid {
display:grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap:12px;
}
/* Card */
.col-card {
position:relative; cursor:pointer;
perspective:800px;
height:200px;
}
.col-card-inner {
position:absolute; inset:0;
transition:transform .5s cubic-bezier(.4,0,.2,1);
transform-style:preserve-3d;
}
.col-card:hover .col-card-inner:not(.flipped) { transform:rotateY(10deg) scale(1.03); }
.col-card-inner.flipped { transform:rotateY(180deg); }
.col-card-front, .col-card-back {
position:absolute; inset:0;
border-radius:16px; padding:14px 12px;
backface-visibility:hidden;
display:flex; flex-direction:column; align-items:center;
justify-content:center; gap:6px;
}
.col-card-back { transform:rotateY(180deg); }
/* Tier styles */
.tier-locked .col-card-front {
background:rgba(255,255,255,.04); border:1.5px solid rgba(255,255,255,.08);
filter:grayscale(1);
}
.tier-bronze .col-card-front {
background:linear-gradient(145deg,rgba(180,80,20,.3),rgba(120,50,10,.2));
border:1.5px solid rgba(205,127,50,.35);
box-shadow:0 0 20px rgba(205,127,50,.15);
}
.tier-silver .col-card-front {
background:linear-gradient(145deg,rgba(160,160,170,.25),rgba(100,100,110,.2));
border:1.5px solid rgba(192,192,192,.4);
box-shadow:0 0 20px rgba(192,192,192,.15);
}
.tier-gold .col-card-front {
background:linear-gradient(145deg,rgba(249,199,79,.2),rgba(200,150,30,.15));
border:1.5px solid rgba(249,199,79,.45);
box-shadow:0 0 24px rgba(249,199,79,.2);
}
.tier-platinum .col-card-front {
background:linear-gradient(145deg,rgba(6,214,224,.2),rgba(155,93,229,.2));
border:1.5px solid rgba(6,214,224,.45);
box-shadow:0 0 28px rgba(6,214,224,.2);
}
/* Platinum shimmer */
.tier-platinum .col-card-front::after {
content:''; position:absolute; inset:0; border-radius:16px;
background:linear-gradient(135deg,transparent 30%,rgba(255,255,255,.06) 50%,transparent 70%);
animation:shimmer 2.5s linear infinite;
background-size:200% 200%;
}
@keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
.col-card-tier-badge {
position:absolute; top:8px; right:8px;
font-size:.6rem; font-weight:800; padding:2px 7px;
border-radius:99px; text-transform:uppercase; letter-spacing:.05em;
}
.tier-locked .col-card-tier-badge { background:rgba(255,255,255,.08); color:var(--text-2); }
.tier-bronze .col-card-tier-badge { background:rgba(205,127,50,.3); color:#CD7F32; }
.tier-silver .col-card-tier-badge { background:rgba(192,192,192,.3); color:#C0C0C0; }
.tier-gold .col-card-tier-badge { background:rgba(249,199,79,.25); color:#F9C74F; }
.tier-platinum .col-card-tier-badge { background:rgba(6,214,224,.25); color:#06D6E0; }
.col-card-icon { font-size:2rem; line-height:1; }
.tier-locked .col-card-icon { opacity:.3; }
.col-card-name {
font-family:'Unbounded',sans-serif; font-size:.7rem; font-weight:800;
text-align:center; line-height:1.3; word-break:break-word;
}
.tier-locked .col-card-name { color:var(--text-2); }
.col-card-subj {
font-size:.68rem; color:var(--text-2); text-align:center;
}
/* Stars rating */
.col-card-stars { display:flex; gap:2px; }
.col-card-stars svg { width:12px; height:12px; }
/* Back face */
.col-card-back {
background:var(--surface); border:1.5px solid rgba(255,255,255,.12);
}
.col-card-back-title {
font-family:'Unbounded',sans-serif; font-size:.72rem; font-weight:800;
text-align:center; margin-bottom:6px;
}
.col-card-back-stat { font-size:.72rem; color:var(--text-2); text-align:center; }
.col-card-back-pct {
font-family:'Unbounded',sans-serif; font-size:1.3rem; font-weight:800;
color:#F9C74F;
}
.col-card-back-bar { width:80%; height:5px; border-radius:99px; background:rgba(255,255,255,.1); overflow:hidden; }
.col-card-back-fill { height:100%; border-radius:99px; }
/* Lock icon */
.col-lock-icon {
width:36px; height:36px; border-radius:50%;
background:rgba(255,255,255,.06); display:flex; align-items:center; justify-content:center;
}
.col-lock-icon svg { width:18px; height:18px; stroke:var(--text-2); fill:none; stroke-width:2; }
/* Empty state */
.col-empty {
grid-column:1/-1;
text-align:center; padding:60px; color:var(--text-2);
font-size:.88rem;
}
@media (max-width:768px) {
.col-grid { grid-template-columns:repeat(auto-fill, minmax(130px, 1fr)); }
.col-search { width:150px; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="app-sidebar"></aside>
<div class="notif-drop" id="notif-drop"></div>
<div class="sb-content">
<div class="col-wrap">
<div class="col-header">
<div class="col-icon">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
</div>
<div>
<div class="col-title">Коллекция карточек</div>
<div class="col-sub">Открывай карточки, прокачивая знания по темам</div>
</div>
</div>
<!-- Summary -->
<div class="col-summary" id="col-summary">
<div class="col-sum-card">
<div class="col-sum-icon" style="background:rgba(249,199,79,.12);color:#F9C74F">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-total"></div>
<div class="col-sum-label">Всего тем</div>
</div>
</div>
<div class="col-sum-card">
<div class="col-sum-icon" style="background:rgba(56,217,90,.12);color:#38D95A">
<svg viewBox="0 0 24 24"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-unlocked"></div>
<div class="col-sum-label">Открыто</div>
</div>
</div>
<div class="col-sum-card" style="cursor:pointer" onclick="setTier('platinum')">
<div class="col-sum-icon" style="background:rgba(6,214,224,.12);color:#06D6E0">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-plat" style="color:#06D6E0"></div>
<div class="col-sum-label">Платина</div>
</div>
</div>
<div class="col-sum-card" style="cursor:pointer" onclick="setTier('gold')">
<div class="col-sum-icon" style="background:rgba(249,199,79,.12);color:#F9C74F">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-gold" style="color:#F9C74F"></div>
<div class="col-sum-label">Золото</div>
</div>
</div>
<div class="col-sum-card" style="cursor:pointer" onclick="setTier('silver')">
<div class="col-sum-icon" style="background:rgba(192,192,192,.15);color:#C0C0C0">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-silver" style="color:#C0C0C0"></div>
<div class="col-sum-label">Серебро</div>
</div>
</div>
<div class="col-sum-card" style="cursor:pointer" onclick="setTier('bronze')">
<div class="col-sum-icon" style="background:rgba(205,127,50,.15);color:#CD7F32">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-bronze" style="color:#CD7F32"></div>
<div class="col-sum-label">Бронза</div>
</div>
</div>
</div>
<!-- Progress -->
<div class="col-progress-bar">
<div class="col-prog-label">Прогресс коллекции</div>
<div class="col-prog-bar"><div class="col-prog-fill" id="col-prog-fill" style="width:0%"></div></div>
<div class="col-prog-pct" id="col-prog-pct">0%</div>
</div>
<!-- Filters -->
<div class="col-filter">
<button class="col-pill active" data-slug="" onclick="setSubject(this,'')">Все</button>
<button class="col-pill" data-slug="bio" onclick="setSubject(this,'bio')">Биология</button>
<button class="col-pill" data-slug="chem" onclick="setSubject(this,'chem')">Химия</button>
<button class="col-pill" data-slug="math" onclick="setSubject(this,'math')">Математика</button>
<button class="col-pill" data-slug="phys" onclick="setSubject(this,'phys')">Физика</button>
<div class="col-search-wrap" style="margin-left:auto">
<div class="col-search-icon"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></div>
<input class="col-search" id="col-search" placeholder="Поиск темы…" oninput="renderCards()" />
</div>
</div>
<!-- Tier filter -->
<div class="tier-legend" id="tier-legend">
<div class="tier-dot active" data-tier="" onclick="setTier('')">
<div class="tier-dot-circle" style="background:var(--text-2)"></div>Все
</div>
<div class="tier-dot" data-tier="platinum" onclick="setTier('platinum')">
<div class="tier-dot-circle" style="background:#06D6E0"></div>Платина
</div>
<div class="tier-dot" data-tier="gold" onclick="setTier('gold')">
<div class="tier-dot-circle" style="background:#F9C74F"></div>Золото
</div>
<div class="tier-dot" data-tier="silver" onclick="setTier('silver')">
<div class="tier-dot-circle" style="background:#C0C0C0"></div>Серебро
</div>
<div class="tier-dot" data-tier="bronze" onclick="setTier('bronze')">
<div class="tier-dot-circle" style="background:#CD7F32"></div>Бронза
</div>
<div class="tier-dot" data-tier="locked" onclick="setTier('locked')">
<div class="tier-dot-circle" style="background:rgba(255,255,255,.2)"></div>Закрыто
</div>
</div>
<!-- Cards -->
<div class="col-grid" id="col-grid">
<div class="col-empty">
<div style="font-size:2rem;margin-bottom:8px"><svg class="ic" viewBox="0 0 24 24"><path d="M5 22h14M5 2h14M17 22v-4.17a2 2 0 0 0-.59-1.41L12 12l-4.41 4.42A2 2 0 0 0 7 17.83V22M7 2v4.17a2 2 0 0 0 .59 1.41L12 12l4.41-4.42A2 2 0 0 0 17 6.17V2"/></svg></div>
Загружаем коллекцию…
</div>
</div>
</div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<script>
(async () => {
if (!LS.requireAuth()) return;
const user = LS.getUser();
LS.applyRoleSidebar(user);
if (user) {
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
document.getElementById('nav-user').textContent = user.name || '—';
LS.showBoardIfAllowed();
}
LS.sidebar?.init();
lucide.createIcons();
const feats = await LS.loadFeatures();
if (feats.collection === false) { window.location.replace('/403'); return; }
LS.hideDisabledFeatures?.();
await loadCollection();
})();
let _cards = [];
let _filterSubject = '';
let _filterTier = '';
/* ── Subject icons ── */
const SUBJ_ICONS = { bio:'<svg class="ic" viewBox="0 0 24 24"><path d="M2 15c6.667-6 13.333 0 20-6"/><path d="M9 22c1.798-2 2.518-4 2.807-6"/><path d="M15 2c-1.798 2-2.518 4-2.807 6"/><path d="m17 6-2.5-2.5M14 8 13 7M7 18l2.5 2.5M3.5 14.5l.5.5M20 9l.5.5M6.5 12.5l1 1M16.5 10.5l1 1M10 16l1.5 1.5"/></svg>', chem:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg>', math:'<svg class="ic" viewBox="0 0 24 24"><path d="m15 5 6.3 6.3a2.4 2.4 0 0 1 0 3.4L17 19"/><path d="M9.586 5.586A2 2 0 0 0 8.172 5H3a1 1 0 0 0-1 1v5.172a2 2 0 0 0 .586 1.414L8.29 18.29a2.426 2.426 0 0 0 3.42 0l3.58-3.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="6.5" cy="9.5" r=".5" fill="currentColor"/></svg>', phys:'<svg class="ic" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>' };
/* ── Star SVG ── */
function starSVG(filled, color) {
return `<svg viewBox="0 0 24 24" fill="${filled ? color : 'none'}" stroke="${color}" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>`;
}
const TIER_COLOR = { platinum:'#06D6E0', gold:'#F9C74F', silver:'#C0C0C0', bronze:'#CD7F32', locked:'rgba(255,255,255,.2)' };
const TIER_LABEL = { platinum:'Платина', gold:'Золото', silver:'Серебро', bronze:'Бронза', locked:'Закрыто' };
const TIER_STARS = { platinum:5, gold:4, silver:3, bronze:1, locked:0 };
/* ── Load ── */
async function loadCollection() {
const data = await LS.api('/api/collection').catch(() => null);
if (!data) return;
_cards = data.cards;
document.getElementById('sum-total').textContent = data.totalTopics;
document.getElementById('sum-unlocked').textContent = data.unlockedTopics;
document.getElementById('sum-plat').textContent = data.platinumCount;
document.getElementById('sum-gold').textContent = data.goldCount;
document.getElementById('sum-silver').textContent = data.silverCount;
document.getElementById('sum-bronze').textContent = data.bronzeCount;
const pct = data.totalTopics > 0 ? Math.round(data.unlockedTopics / data.totalTopics * 100) : 0;
document.getElementById('col-prog-fill').style.width = pct + '%';
document.getElementById('col-prog-pct').textContent = pct + '%';
renderCards();
}
/* ── Filters ── */
function setSubject(el, slug) {
document.querySelectorAll('.col-pill').forEach(p => p.classList.remove('active'));
el.classList.add('active');
_filterSubject = slug;
renderCards();
}
function setTier(tier) {
document.querySelectorAll('.tier-dot').forEach(d => d.classList.remove('active'));
const el = document.querySelector(`.tier-dot[data-tier="${tier}"]`);
if (el) el.classList.add('active');
_filterTier = tier;
renderCards();
}
/* ── Render ── */
function renderCards() {
const q = document.getElementById('col-search').value.trim().toLowerCase();
let cards = _cards;
if (_filterSubject) cards = cards.filter(c => c.subjectSlug === _filterSubject);
if (_filterTier) cards = cards.filter(c => c.tier === _filterTier);
if (q) cards = cards.filter(c => c.topicName.toLowerCase().includes(q) || c.subjectName.toLowerCase().includes(q));
const grid = document.getElementById('col-grid');
if (!cards.length) {
grid.innerHTML = `<div class="col-empty"><div style="font-size:2rem;margin-bottom:8px"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></div>Ничего не найдено</div>`;
return;
}
// Sort: unlocked first, then by tier weight
const TIER_WEIGHT = { platinum:4, gold:3, silver:2, bronze:1, locked:0 };
cards = [...cards].sort((a,b) => (TIER_WEIGHT[b.tier]||0) - (TIER_WEIGHT[a.tier]||0));
grid.innerHTML = cards.map(card => buildCard(card)).join('');
}
function buildCard(c) {
const tierColor = TIER_COLOR[c.tier] || 'rgba(255,255,255,.2)';
const tierLbl = TIER_LABEL[c.tier] || '';
const stars = TIER_STARS[c.tier] || 0;
const icon = SUBJ_ICONS[c.subjectSlug] || '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>';
const starsHtml = [1,2,3,4,5].map(i =>
starSVG(i <= stars, tierColor)
).join('');
const frontContent = c.tier === 'locked'
? `<div class="col-lock-icon"><svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="col-card-name">${escHtml(c.topicName)}</div>
<div class="col-card-subj">${escHtml(c.subjectName)}</div>`
: `<div class="col-card-icon">${icon}</div>
<div class="col-card-name">${escHtml(c.topicName)}</div>
<div class="col-card-subj">${escHtml(c.subjectName)}</div>
<div class="col-card-stars">${starsHtml}</div>`;
const backContent = c.tier === 'locked'
? `<div class="col-card-back-title">${escHtml(c.topicName)}</div>
<div class="col-card-back-stat">Нет правильных ответов</div>
<div class="col-card-back-stat" style="margin-top:6px">Проходи тесты,<br>чтобы открыть карточку</div>`
: `<div class="col-card-back-title">${escHtml(c.topicName)}</div>
<div class="col-card-back-pct">${c.masteryPct}%</div>
<div class="col-card-back-stat">мастерство</div>
<div class="col-card-back-bar" style="margin-top:6px">
<div class="col-card-back-fill" style="width:${c.masteryPct}%;background:${tierColor}"></div>
</div>
<div class="col-card-back-stat" style="margin-top:6px">${c.correctCount}/${c.totalAttempts} попыток</div>`;
return `<div class="col-card tier-${c.tier}" onclick="flipCard(this)">
<div class="col-card-inner">
<div class="col-card-front">
<span class="col-card-tier-badge">${tierLbl}</span>
${frontContent}
</div>
<div class="col-card-back">${backContent}</div>
</div>
</div>`;
}
function flipCard(el) {
el.querySelector('.col-card-inner').classList.toggle('flipped');
}
function escHtml(s) {
return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
LS.notif?.init();
</script>
<script src="/js/notifications.js"></script>
<script src="/js/search.js"></script>
<script src="/js/mobile.js"></script>
</body>
</html>