const zlib = require('zlib'); const CRC_TABLE = (() => { const table = new Int32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1); table[n] = c; } return table; })(); function crc32(buf) { let crc = 0xffffffff; for (let i = 0; i < buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); return (crc ^ 0xffffffff) >>> 0; } function dosDateTime(d = new Date()) { return { time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2), date: ((Math.max(1980, d.getFullYear()) - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(), }; } function createZipWriter() { const parts = []; const central = []; let count = 0; let offset = 0; function buildEntry(nameBuf, method, crc, compressed, plain, dt) { const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(0x0800, 6); local.writeUInt16LE(method, 8); local.writeUInt16LE(dt.time, 10); local.writeUInt16LE(dt.date, 12); local.writeUInt32LE(crc, 14); local.writeUInt32LE(compressed, 18); local.writeUInt32LE(plain, 22); local.writeUInt16LE(nameBuf.length, 26); local.writeUInt16LE(0, 28); const cen = Buffer.alloc(46); cen.writeUInt32LE(0x02014b50, 0); cen.writeUInt16LE(20, 4); cen.writeUInt16LE(20, 6); cen.writeUInt16LE(0x0800, 8); cen.writeUInt16LE(method, 10); cen.writeUInt16LE(dt.time, 12); cen.writeUInt16LE(dt.date, 14); cen.writeUInt32LE(crc, 16); cen.writeUInt32LE(compressed, 20); cen.writeUInt32LE(plain, 24); cen.writeUInt16LE(nameBuf.length, 28); cen.writeUInt16LE(0, 30); cen.writeUInt16LE(0, 32); cen.writeUInt16LE(0, 34); cen.writeUInt16LE(0, 36); cen.writeUInt32LE(0, 38); cen.writeUInt32LE(offset, 42); return { local, cen, nameBuf }; } return { addFile(name, data, d) { const nameBuf = Buffer.from(name, 'utf8'); const dt = dosDateTime(d); const crc = crc32(data); const compressed = zlib.deflateRawSync(data, { level: 9 }); const e = buildEntry(nameBuf, 8, crc, compressed.length, data.length, dt); const chunk = Buffer.concat([e.local, e.nameBuf, compressed]); parts.push(chunk); central.push(Buffer.concat([e.cen, e.nameBuf])); offset += chunk.length; count++; }, addDir(name) { const nameBuf = Buffer.from(String(name).replace(/\/?$/, '/'), 'utf8'); const dt = dosDateTime(); const e = buildEntry(nameBuf, 0, 0, 0, 0, dt); const chunk = Buffer.concat([e.local, e.nameBuf]); parts.push(chunk); central.push(Buffer.concat([e.cen, e.nameBuf])); offset += chunk.length; count++; }, toBuffer() { const centralStart = offset; const centralBuf = Buffer.concat(central); const eocd = Buffer.alloc(22); eocd.writeUInt32LE(0x06054b50, 0); eocd.writeUInt16LE(0, 4); eocd.writeUInt16LE(0, 6); eocd.writeUInt16LE(count, 8); eocd.writeUInt16LE(count, 10); eocd.writeUInt32LE(centralBuf.length, 12); eocd.writeUInt32LE(centralStart, 16); eocd.writeUInt16LE(0, 20); return Buffer.concat([...parts, centralBuf, eocd]); }, }; } const MONTHS_GEN = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря']; const MONTHS_SHORT = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']; function escapeHtml(str) { return String(str === null || str === undefined ? '' : str) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function asDate(iso) { if (!iso) return null; const d = new Date(iso); return Number.isNaN(d.getTime()) ? null : d; } function longDate(iso) { const d = asDate(iso); if (!d) return ''; return d.getDate() + ' ' + MONTHS_GEN[d.getMonth()] + ' ' + d.getFullYear(); } function monthYear(iso) { const d = asDate(iso); if (!d) return ''; return MONTHS_GEN[d.getMonth()] + ' ' + d.getFullYear(); } function rangeLabel(from, to) { const a = asDate(from); const b = asDate(to); if (!a && !b) return ''; if (!a) return 'до ' + monthYear(to); if (!b) return 'с ' + monthYear(from); if (a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()) return monthYear(from); return MONTHS_SHORT[a.getMonth()] + ' ' + a.getFullYear() + ' — ' + MONTHS_SHORT[b.getMonth()] + ' ' + b.getFullYear(); } function bytes(n) { if (!Number.isFinite(n)) return ''; if (n < 1024) return n + ' Б'; if (n < 1024 * 1024) return (n / 1024).toFixed(1).replace(/\.0$/, '') + ' КБ'; return (n / (1024 * 1024)).toFixed(1).replace(/\.0$/, '') + ' МБ'; } function truncate(value, max) { const s = String(value || '').replace(/\s+/g, ' ').trim(); return s.length > max ? s.slice(0, max - 1) + '…' : s; } function firstLine(value, max) { const s = String(value || '').split('\n').map(x => x.trim()).filter(Boolean)[0] || ''; return truncate(s, max); } function plural(n, one, few, many) { const m10 = n % 10; const m100 = n % 100; if (m10 === 1 && m100 !== 11) return one; if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return few; return many; } function initials(name) { const parts = String(name || '').trim().split(/\s+/).filter(Boolean); const a = parts[0] ? parts[0][0] : ''; const b = parts[1] ? parts[1][0] : ''; return (a + b).toUpperCase() || '?'; } function chipInit(name) { const parts = String(name || '').trim().split(/\s+/).filter(Boolean); if (!parts.length) return '—'; return parts[0] + (parts[1] ? ' ' + parts[1][0] + '.' : ''); } function str(value, max) { if (value === null || value === undefined) return null; const s = String(value).trim(); if (!s) return null; return max && s.length > max ? s.slice(0, max) : s; } function num(value) { const n = Number(value); return Number.isFinite(n) ? n : null; } function arr(value) { return Array.isArray(value) ? value : []; } function iconName(value, fallback) { const s = String(value || '').trim().toLowerCase(); return /^[a-z0-9-]{1,32}$/.test(s) ? s : fallback; } function safeHref(value) { const s = String(value || '').trim(); if (!s || s.length > 500) return null; if (/^(https?:\/\/|mailto:|tel:|\/|#)/i.test(s)) return s; if (/^[\w.+-]+@[\w-]+\.[\w.-]{2,}$/.test(s)) return 'mailto:' + s; return null; } const IMG_EXT = new Set(['JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'BMP', 'AVIF', 'SVG', 'ICO', 'JFIF', 'HEIC', 'HEIF']); const VID_EXT = new Set(['MP4', 'WEBM', 'MOV', 'M4V', 'OGV', 'MKV', 'MPEG', 'MPG', '3GP', 'AVI']); const EXT_ICONS = { PDF: 'file-text', DOC: 'file-text', DOCX: 'file-text', TXT: 'file-text', RTF: 'file-text', XLS: 'table-2', XLSX: 'table-2', CSV: 'table-2', PPT: 'presentation', PPTX: 'presentation', ZIP: 'folder-archive', RAR: 'folder-archive', '7Z': 'folder-archive', TAR: 'folder-archive', GZ: 'folder-archive', JS: 'file-code-2', TS: 'file-code-2', HTML: 'file-code-2', CSS: 'file-code-2', JSON: 'file-code-2', PY: 'file-code-2', JAVA: 'file-code-2', CS: 'file-code-2', CPP: 'file-code-2', C: 'file-code-2', PSD: 'image', AI: 'image', MP3: 'music', WAV: 'music', OGG: 'music', FLAC: 'music', FIG: 'figma', SKETCH: 'figma', BLEND: 'box', OBJ: 'box', STL: 'box', FBX: 'box', GLB: 'box', }; function extIcon(ext) { const e = String(ext || '').toUpperCase(); if (IMG_EXT.has(e)) return 'image'; if (VID_EXT.has(e)) return 'video'; return EXT_ICONS[e] || 'file'; } function iconForGroup(title) { const t = String(title || '').toLowerCase(); if (/вёрстк|верстк|css|html|стил|дизайн|ui|ux/.test(t)) return 'layout-panel-left'; if (/javascript|js|программ|код|разработ/.test(t)) return 'braces'; if (/vue|react|framework|компонент/.test(t)) return 'component'; if (/инструмент|git|сборк|figma|vite/.test(t)) return 'wrench'; if (/данн|sql|база|api/.test(t)) return 'database'; if (/робот|python|ии|ai|нейро/.test(t)) return 'bot'; return 'sparkles'; } function buildProfileView(data, opts) { const o = opts || {}; const showEntries = o.includeEntries !== false; const showPhotos = o.includePhotos !== false; const showFiles = o.includeFiles !== false; const showCaptions = o.includeCaptions !== false; const showDates = o.showDates !== false; const student = data.student || {}; const profile = (data.profile && typeof data.profile === 'object' && !Array.isArray(data.profile)) ? data.profile : {}; const entries = arr(data.entries); const dbModules = arr(data.modules); const photos = arr(data.photos); const files = arr(data.files); const name = str(data.name, 150) || str(student.name, 150) || 'Резидент'; const profContacts = arr(profile.contacts); const profAchievements = arr(profile.achievements); const gallery = []; const galleryIdx = new Map(); function galleryAdd(type, src, caption) { if (galleryIdx.has(src)) return galleryIdx.get(src); const idx = gallery.length; gallery.push({ type, src, caption: caption || '' }); galleryIdx.set(src, idx); return idx; } const photoMap = new Map(); const photoCards = []; if (showPhotos) { for (const p of photos) { const src = 'photos/' + p.stored; const g = galleryAdd('image', src, p.caption || ''); const card = { src, g, caption: showCaptions ? (str(p.caption, 160) || '') : '', date: showDates ? longDate(p.createdAt) : '', group: str(p.groupName, 80) || '', entryId: p.entryId, }; photoMap.set(p.stored, card); photoCards.push(card); } } const groupPhotoCards = arr(data.groupPhotos).map(gp => { const src = 'photos/' + gp.stored; const g = galleryAdd('image', src, gp.caption || ''); const when = gp.takenAt || gp.createdAt; return { src, g, caption: showCaptions ? (str(gp.caption, 160) || '') : '', date: showDates && when ? longDate(when) : '', group: str(gp.groupName, 80) || '', }; }); const photoByEntry = new Map(); for (const p of photos) { if (!photoByEntry.has(p.entryId)) photoByEntry.set(p.entryId, []); photoByEntry.get(p.entryId).push(p); } const projectMap = new Map(); const projects = []; if (showFiles) { for (const f of files) { const ext = String(f.ext || '').toUpperCase(); const isImg = IMG_EXT.has(ext); const isVid = VID_EXT.has(ext); const href = 'files/' + f.saved; const card = { href, ext, icon: extIcon(ext), isImg, isVid, g: (isImg || isVid) ? galleryAdd(isVid ? 'video' : 'image', href, f.original) : -1, title: str(f.original, 160) || 'Файл', description: truncate(f.desc, 170) || (bytes(f.size) + (ext ? ' · ' + ext : '')), tags: [ext, str(f.moduleName, 70)].filter(Boolean), date: showDates ? longDate(f.createdAt) : '', size: bytes(f.size), entryId: f.entryId, }; projectMap.set(f.saved, card); projects.push(card); } } const fileByEntry = new Map(); for (const f of files) { if (!fileByEntry.has(f.entryId)) fileByEntry.set(f.entryId, []); fileByEntry.get(f.entryId).push(f); } const tasks = showEntries ? entries.map(e => { const st = e.ai_status; const ok = st === 'done' || st === 'skipped'; const bad = st === 'error'; return { icon: ok ? 'check' : (bad ? 'triangle-alert' : 'loader'), done: ok, statusLabel: ok ? (st === 'skipped' ? 'Принято' : 'Проверено') : (bad ? 'Ошибка проверки' : 'В проверке'), title: firstLine(e.description, 120) || 'Запись занятия', sub: [str(e.module_name, 90) || str(e.group_name, 90), showDates ? longDate(e.created_at) : ''].filter(Boolean).join(' · '), thumbs: (photoByEntry.get(e.id) || []).slice(0, 4).map(p => photoMap.get(p.stored)).filter(Boolean), files: (fileByEntry.get(e.id) || []).slice(0, 5).map(f => projectMap.get(f.saved)).filter(Boolean), }; }) : []; const groupSet = new Set(); if (student.group_name) groupSet.add(student.group_name); for (const e of entries) if (e.group_name) groupSet.add(e.group_name); const groups = [...groupSet]; const firstAt = entries.length ? entries[entries.length - 1].created_at : student.created_at; const days = (() => { const d = asDate(firstAt); if (!d) return 0; return Math.max(1, Math.round((Date.now() - d.getTime()) / 86400000)); })(); const eduMeta = new Map(); for (const m of arr(profile.education)) { const key = str(m && m.module, 200); if (key) eduMeta.set(key.toLowerCase(), m); } const MODULE_ICONS = ['file-code-2', 'braces', 'component', 'route', 'file-type-2', 'figma', 'layout-panel-left', 'wrench']; const education = dbModules.map((m, i) => { const meta = eduMeta.get(String(m.name || '').trim().toLowerCase()) || {}; const lessons = num(m.lessons_count) || 0; const done = num(m.entries_count) || 0; let progress = (meta.progress === null || meta.progress === undefined) ? null : num(meta.progress); if (progress === null && lessons > 0) progress = Math.max(1, Math.min(100, Math.round((done / lessons) * 100))); if (progress !== null) progress = Math.max(0, Math.min(100, Math.round(progress))); const teacher = str(meta.teacher, 150); return { icon: iconName(meta.icon, MODULE_ICONS[i % MODULE_ICONS.length]), code: 'MOD-' + String(m.id).padStart(2, '0'), title: str(m.name, 200) || 'Модуль', teacher: teacher ? 'Преподаватель: ' + teacher : null, period: rangeLabel(m.first_at, m.last_at), lessons: lessons || null, done, progress, grade: str(meta.grade, 90), }; }); const stats = []; if (showEntries) stats.push({ icon: 'clipboard-list', value: entries.length, suffix: plural(entries.length, 'запись', 'записи', 'записей'), label: 'Записей в журнале', hint: 'что делал резидент на занятиях' }); if (showPhotos) stats.push({ icon: 'image', value: photos.length, suffix: plural(photos.length, 'фото', 'фото', 'фото'), label: 'Фотографий', hint: 'снимки с занятий' }); if (showFiles) stats.push({ icon: 'folder-git-2', value: files.length, suffix: plural(files.length, 'работа', 'работы', 'работ'), label: 'Работ в портфолио', hint: 'файлы, прикреплённые к занятиям' }); stats.push({ icon: 'users-round', value: groups.length, suffix: plural(groups.length, 'группа', 'группы', 'групп'), label: 'Групп', hint: groups.join(' · ') || '—' }); stats.push({ icon: 'book-open-check', value: education.length, suffix: plural(education.length, 'тема', 'темы', 'тем'), label: 'Тем программы', hint: 'модули, которые проходил резидент' }); if (days && showEntries) stats.push({ icon: 'calendar-clock', value: days, suffix: plural(days, 'день', 'дня', 'дней'), label: 'В KIBERone', hint: 'от первой записи в журнале' }); for (const s of arr(profile.stats)) { const label = str(s && s.label, 80); const value = (s && s.value !== undefined && s.value !== null) ? String(s.value).trim().slice(0, 20) : ''; if (!label || !value) continue; stats.push({ icon: iconName(s.icon, 'star'), value, raw: value, suffix: str(s.suffix, 20), label, hint: str(s.hint, 120), delta: str(s.delta, 60) }); } const skillGroups = []; if (arr(profile.skills).length) { const map = new Map(); for (const s of arr(profile.skills)) { const title = str(s && s.group, 80) || 'Навыки'; const itemName = str(s && s.name, 120); if (!itemName) continue; const value = (s.value === null || s.value === undefined) ? null : Math.max(0, Math.min(100, Math.round(Number(s.value) || 0))); if (!map.has(title)) map.set(title, []); map.get(title).push({ name: itemName, level: str(s.level, 40), value }); } for (const [title, items] of map) skillGroups.push({ title, icon: iconForGroup(title), items }); } else if (education.length) { skillGroups.push({ title: 'Пройденные темы', icon: 'sparkles', items: education.map(m => ({ name: m.title, level: m.progress === null ? null : (m.progress >= 100 ? 'Изучен' : 'В процессе'), value: m.progress })), }); } const mediaPool = projects.filter(p => p.isImg).map(p => p.src); const experience = arr(profile.experience).map((e, i) => ({ title: str(e && e.title, 160) || 'Кейс', company: str(e && e.company, 160), period: str(e && e.period, 80), date: str(e && e.date, 40) || (showDates ? longDate(e && e.createdAt) : ''), badge: str(e && e.badge, 40) || 'Кейс', description: str(e && e.description, 800), tags: arr(e && e.tags).map(t => str(t, 40)).filter(Boolean), media: mediaPool.length ? mediaPool[i % mediaPool.length] : null, })); const contacts = profContacts.map(c => ({ icon: iconName(c && c.icon, 'link'), label: str(c && c.label, 120), href: safeHref(c && c.href), })).filter(c => c.label); const autoAchievements = []; if (showEntries && entries.length) autoAchievements.push(entries.length + ' ' + plural(entries.length, 'запись', 'записи', 'записей') + ' в журнале занятий'); if (showPhotos && photos.length) autoAchievements.push(photos.length + ' ' + plural(photos.length, 'фотография', 'фотографии', 'фотографий') + ' в архиве занятий'); if (showFiles && files.length) autoAchievements.push(files.length + ' ' + plural(files.length, 'работа', 'работы', 'работ') + ' в портфолио'); if (education.length) autoAchievements.push('Пройдено тем программы: ' + education.length + ' — ' + education.slice(0, 3).map(m => m.title).join(', ')); const achievements = profAchievements.length ? profAchievements.map(a => str(a, 200)).filter(Boolean) : autoAchievements; const facts = []; const city = str(profile.city, 120) || str(student.branch_name, 120); if (city) facts.push({ icon: 'map-pin', text: city }); if (groups.length) facts.push({ icon: 'users-round', text: groups.join(' · ') }); const joined = str(profile.joined, 120) || (student.created_at ? 'В KIBERone с ' + monthYear(student.created_at) : null); if (joined) facts.push({ icon: 'calendar-check', text: joined }); const mentor = str(profile.mentor, 150); if (mentor) facts.push({ icon: 'user-check', text: 'Наставник: ' + mentor }); if (education.length) facts.push({ icon: 'book-open', text: education.length + ' ' + plural(education.length, 'тема', 'темы', 'тем') + ' программы' }); const autoBio = []; if (groups.length) autoBio.push('Резидент группы ' + groups.join(', ') + '.'); if (entries.length) autoBio.push('В журнале ' + entries.length + ' ' + plural(entries.length, 'запись', 'записи', 'записей') + ' о занятиях.'); if (photos.length) autoBio.push('Фотографий: ' + photos.length + '.'); if (files.length) autoBio.push('Работ в портфолио: ' + files.length + '.'); const tagList = arr(profile.tags).map(t => str(t, 40)).filter(Boolean); const heroTags = tagList.length ? tagList : education.slice(0, 6).map(m => m.title); const avatar = showPhotos && photoCards.length ? (photoCards[1] || photoCards[0]) : null; const cover = showPhotos && photoCards.length ? photoCards[0] : null; const mainPhotoCard = showPhotos && groupPhotoCards.length ? groupPhotoCards[0] : null; const profilePhotoCard = showPhotos && photoCards.length ? photoCards[0] : null; const eduYears = education.map(m => m.period).filter(Boolean); const idLabel = student.id ? 'ID S-' + String(student.id).padStart(4, '0') : null; return { name, student, profile, entries, education, projects, photoCards, groupPhotoCards, tasks, gallery, groups, stats, skillGroups, experience, contacts, achievements, facts, heroTags, avatar, cover, idLabel, mainPhotoCard, profilePhotoCard, role: str(profile.role, 200) || (education.length ? 'Резидент программы: ' + education[0].title : null), bio: str(profile.bio, 2000) || (autoBio.join(' ') || null), quote: str(profile.quote, 300), status: str(profile.status, 60) || 'Активно учится', statusNote: str(profile.status_note, 120) || (student.created_at ? 'с ' + monthYear(student.created_at) : ''), periodLabel: eduYears.length ? eduYears[0] + ' — ' + eduYears[eduYears.length - 1] : '', showEntries, showPhotos, showFiles, showCaptions, showDates, days, }; } function reportCss() { return ` :root{--bg:#080c1a;--bg-soft:#0d1424;--surface:rgba(255,255,255,.045);--surface-2:rgba(255,255,255,.03);--surface-hover:rgba(255,255,255,.08);--stroke:rgba(255,255,255,.09);--stroke-strong:rgba(255,255,255,.22);--text:#e9eeff;--muted:#96a3c6;--brand:#6d8cff;--brand-2:#a855f7;--accent:#22d3ee;--ok:#34d399;--warn:#fbbf24;--danger:#fb7185;--topbar-bg:rgba(8,12,26,.78);--radius:22px;--radius-sm:14px;--radius-xs:10px;--gap:18px;--max:1440px;--shadow:0 22px 60px rgba(3,7,22,.5);--shadow-soft:0 10px 30px rgba(3,7,22,.35)} [data-theme="light"]{--bg:#f2f5ff;--bg-soft:#fff;--surface:rgba(255,255,255,.92);--surface-2:rgba(15,23,50,.035);--surface-hover:#eef2ff;--stroke:rgba(20,30,70,.10);--stroke-strong:rgba(20,30,70,.24);--text:#101733;--muted:#5a678b;--brand:#3f5df0;--brand-2:#8b3ff0;--accent:#0891b2;--ok:#059669;--warn:#b45309;--danger:#e11d48;--topbar-bg:rgba(255,255,255,.82);--shadow:0 22px 55px rgba(30,45,95,.14);--shadow-soft:0 10px 26px rgba(30,45,95,.10)} *,*::before,*::after{box-sizing:border-box} *{margin:0;padding:0} html{scroll-behavior:smooth;scroll-padding-top:96px} body{min-height:100vh;font-family:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;font-size:16px;line-height:1.55;color:var(--text);background-color:var(--bg);background-image:radial-gradient(1100px 620px at 12% -8%,rgba(109,140,255,.24),transparent 62%),radial-gradient(900px 520px at 92% -4%,rgba(168,85,247,.20),transparent 58%),radial-gradient(800px 600px at 60% 108%,rgba(34,211,238,.12),transparent 60%);background-attachment:fixed;-webkit-font-smoothing:antialiased} a{color:inherit;text-decoration:none} button{font:inherit;color:inherit;background:none;border:0;cursor:pointer} ul,ol{list-style:none} svg{display:block;flex:none} ::selection{background:rgba(109,140,255,.35)} :focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:6px} ::-webkit-scrollbar{width:10px;height:10px} ::-webkit-scrollbar-track{background:transparent} ::-webkit-scrollbar-thumb{background:var(--stroke-strong);border-radius:999px} ::-webkit-scrollbar-thumb:hover{background:var(--brand)} @media (prefers-reduced-motion:reduce){*{animation-duration:.001ms !important;transition-duration:.001ms !important;scroll-behavior:auto !important}} .wrap{width:100%;max-width:var(--max);margin:0 auto;padding:0 24px} @media (max-width:640px){.wrap{padding:0 14px}} .page{padding-top:clamp(20px,3vw,38px);padding-bottom:90px;display:flex;flex-direction:column;gap:clamp(26px,3.4vw,52px)} .section{display:flex;flex-direction:column;gap:18px;scroll-margin-top:96px;min-width:0} .section__head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap} .section__head>*{min-width:0} .section__title{display:flex;align-items:center;gap:14px;font-size:clamp(1.3rem,2.2vw,1.95rem);font-weight:800;letter-spacing:-.02em;line-height:1.15;flex-wrap:wrap;min-width:0} .section__hint{color:var(--muted);font-size:.95rem;max-width:70ch} .icon-badge{width:46px;height:46px;flex:none;border-radius:var(--radius-sm);display:grid;place-items:center;background:linear-gradient(140deg,rgba(109,140,255,.26),rgba(168,85,247,.22));border:1px solid var(--stroke);color:var(--text);box-shadow:inset 0 1px 0 rgba(255,255,255,.08)} .icon-badge svg{width:22px;height:22px;stroke-width:2} .card{background:var(--surface);border:1px solid var(--stroke);border-radius:var(--radius);box-shadow:var(--shadow);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px)} .topbar{position:sticky;top:0;z-index:60;background:var(--topbar-bg);border-bottom:1px solid var(--stroke);backdrop-filter:blur(20px) saturate(160%);-webkit-backdrop-filter:blur(20px) saturate(160%)} .topbar__inner{max-width:var(--max);margin:0 auto;padding:12px 24px;display:flex;align-items:center;gap:14px} @media (max-width:640px){.topbar__inner{padding:10px 14px}} .brand{display:flex;align-items:center;gap:12px;font-weight:800;letter-spacing:-.02em} .brand__logo{width:44px;height:44px;border-radius:14px;display:grid;place-items:center;background:linear-gradient(135deg,var(--brand),var(--brand-2));color:#fff;box-shadow:0 10px 24px rgba(109,140,255,.35)} .brand__logo svg{width:22px;height:22px;stroke-width:2.2} .brand__text small{display:block;font-size:.72rem;font-weight:600;color:var(--muted);letter-spacing:.06em;text-transform:uppercase} @media (max-width:540px){.brand__text{display:none}} .topbar__nav{display:flex;align-items:center;gap:4px;flex:1;justify-content:center} .nav-link{display:flex;align-items:center;gap:8px;padding:10px 15px;border-radius:999px;font-size:.94rem;font-weight:600;color:var(--muted);white-space:nowrap;transition:background .2s ease,color .2s ease} .nav-link svg{width:17px;height:17px;stroke-width:2} .nav-link:hover{background:var(--surface-hover);color:var(--text)} .nav-link[aria-current="true"]{background:linear-gradient(120deg,rgba(109,140,255,.22),rgba(168,85,247,.18));color:var(--text);box-shadow:inset 0 0 0 1px var(--stroke)} .topbar__actions{display:flex;align-items:center;gap:8px;margin-left:auto} .icon-btn{width:44px;height:44px;border-radius:14px;display:grid;place-items:center;background:var(--surface);border:1px solid var(--stroke);color:var(--text);transition:background .2s ease,transform .2s ease,border-color .2s ease} .icon-btn svg{width:19px;height:19px;stroke-width:2} .icon-btn:hover{background:var(--surface-hover);border-color:var(--stroke-strong);transform:translateY(-2px)} .icon-btn[aria-pressed="true"]{background:linear-gradient(120deg,rgba(109,140,255,.3),rgba(168,85,247,.26));border-color:var(--stroke-strong)} .user-chip{display:flex;align-items:center;gap:10px;padding:6px 14px 6px 6px;border-radius:999px;background:var(--surface);border:1px solid var(--stroke);font-weight:700;font-size:.9rem} .user-chip__ph{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;flex:none;background:linear-gradient(135deg,rgba(109,140,255,.3),rgba(168,85,247,.28));border:1px dashed var(--stroke-strong);color:var(--muted)} .user-chip__ph svg{width:16px;height:16px} @media (max-width:1000px){.user-chip__name{display:none}} .topbar__burger{display:none} @media (max-width:1080px){.topbar__nav{position:absolute;top:100%;left:0;right:0;flex-direction:column;align-items:stretch;padding:10px 16px 18px;gap:4px;display:none;background:var(--bg-soft);border-bottom:1px solid var(--stroke);box-shadow:var(--shadow)}.topbar__nav.is-open{display:flex}.nav-link{padding:14px 16px;border-radius:var(--radius-sm);font-size:1rem}.topbar__burger{display:grid}} .btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;padding:13px 20px;border-radius:var(--radius-sm);font-weight:700;font-size:.95rem;border:1px solid var(--stroke);background:var(--surface);color:var(--text);transition:background .2s ease,transform .2s ease,border-color .2s ease,box-shadow .2s ease} .btn svg{width:18px;height:18px;stroke-width:2} .btn:hover{background:var(--surface-hover);border-color:var(--stroke-strong);transform:translateY(-2px)} .btn--primary{background:linear-gradient(120deg,var(--brand),var(--brand-2));color:#fff;border-color:transparent;box-shadow:0 14px 30px rgba(109,140,255,.32)} .btn--primary:hover{box-shadow:0 18px 40px rgba(109,140,255,.42)} .btn--ghost{background:transparent} .btn--sm{padding:9px 14px;font-size:.85rem;border-radius:var(--radius-xs)} .btn--sm svg{width:16px;height:16px} .btn[disabled]{opacity:.45;cursor:not-allowed;transform:none} .chip{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;border-radius:999px;background:var(--surface-2);border:1px solid var(--stroke);color:var(--muted);font-size:.82rem;font-weight:600;white-space:nowrap} .chip svg{width:14px;height:14px;stroke-width:2.2} .chip--brand{color:var(--text);background:linear-gradient(120deg,rgba(109,140,255,.18),rgba(168,85,247,.14))} .badge{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border-radius:999px;font-size:.74rem;font-weight:800;letter-spacing:.02em;text-transform:uppercase;border:1px solid var(--stroke)} .badge svg{width:13px;height:13px;stroke-width:2.4} .badge--ok{color:var(--ok);background:rgba(52,211,153,.14);border-color:rgba(52,211,153,.36)} .badge--progress{color:var(--warn);background:rgba(251,191,36,.14);border-color:rgba(251,191,36,.36)} .badge--info{color:var(--accent);background:rgba(34,211,238,.14);border-color:rgba(34,211,238,.36)} .badge--muted{color:var(--muted);background:var(--surface-2)} .meta{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:.84rem;font-weight:600;flex-wrap:wrap} .meta svg{width:15px;height:15px;stroke-width:2} .meta__dot{width:4px;height:4px;border-radius:50%;background:var(--stroke-strong);flex:none} .ph{position:relative;display:grid;place-items:center;gap:8px;overflow:hidden;background-color:var(--surface-2);background-image:repeating-linear-gradient(45deg,transparent 0 12px,rgba(127,150,255,.07) 12px 24px),linear-gradient(135deg,rgba(109,140,255,.16),rgba(168,85,247,.14));border:1px dashed var(--stroke-strong);color:var(--muted);text-align:center;padding:14px} .ph__icon{width:clamp(26px,3.4vw,42px);height:clamp(26px,3.4vw,42px);stroke-width:1.6;opacity:.85} .ph__label{font-size:.8rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase} .ph__size{position:absolute;right:12px;bottom:10px;padding:3px 9px;border-radius:999px;font-family:'JetBrains Mono',ui-monospace,monospace;font-size:.7rem;font-weight:500;background:rgba(6,10,24,.55);color:#e4eaff;border:1px solid var(--stroke)} [data-theme="light"] .ph__size{background:rgba(255,255,255,.86);color:#31386a} .ph--square{aspect-ratio:1/1} .ph--16x9{aspect-ratio:16/9} .ph--4x3{aspect-ratio:4/3} .ph--wide{aspect-ratio:21/9} .media{position:relative;display:block;width:100%;overflow:hidden;background:var(--surface-2)} .media img,.media video{width:100%;height:100%;object-fit:cover;display:block} .media--4x3{aspect-ratio:4/3} .media--16x9{aspect-ratio:16/9} .media--square{aspect-ratio:1/1} .media--wide{aspect-ratio:21/9} .media[data-g]{cursor:zoom-in} .hero{padding:clamp(20px,2.6vw,34px);overflow:hidden;position:relative} .hero__cover{border-radius:var(--radius-sm);margin-bottom:clamp(24px,3vw,40px);position:relative;z-index:0} .hero__cover .media{border-radius:var(--radius-sm)} .hero__grid{display:grid;grid-template-columns:minmax(240px,380px) minmax(0,1fr);gap:clamp(18px,2.6vw,34px);align-items:end;position:relative;z-index:1} @media (max-width:900px){.hero__grid{grid-template-columns:1fr;align-items:start}.hero__cover{display:none}} .hero__media{position:relative;margin-top:clamp(-72px,-5vw,-48px);padding-top:clamp(10px,2vw,26px)} .hero__avatar{border-radius:26px;overflow:hidden;box-shadow:var(--shadow)} .hero__status{position:absolute;left:14px;bottom:14px;padding:7px 12px;border-radius:999px;background:rgba(6,10,24,.6);backdrop-filter:blur(8px);border:1px solid var(--stroke);color:#eaf0ff;font-size:.78rem;font-weight:800;display:inline-flex;align-items:center;gap:7px} .hero__pulse{width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 rgba(52,211,153,.7);animation:pulse 2s infinite} @keyframes pulse{70%{box-shadow:0 0 0 12px rgba(52,211,153,0)}100%{box-shadow:0 0 0 0 rgba(52,211,153,0)}} .hero__info{display:flex;flex-direction:column;gap:14px;min-width:0} .hero__eyebrow{display:flex;align-items:center;gap:10px;color:var(--muted);font-size:.82rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;flex-wrap:wrap} .hero__name{font-size:clamp(1.9rem,4.4vw,3.4rem);font-weight:800;letter-spacing:-.035em;line-height:1.05;overflow-wrap:anywhere} .hero__role{font-size:clamp(1rem,1.7vw,1.3rem);font-weight:600;color:var(--muted);max-width:60ch} .hero__tags{display:flex;flex-wrap:wrap;gap:8px} .hero__actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:4px} .hero__facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px;margin-top:8px} @media (max-width:640px){.hero__facts{grid-template-columns:repeat(2,minmax(0,1fr))}} .fact{display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:var(--radius-sm);background:var(--surface-2);border:1px solid var(--stroke);min-width:0} .fact svg{width:18px;height:18px;color:var(--brand);flex:none} .fact span{font-weight:600;font-size:.9rem;color:var(--muted);overflow-wrap:anywhere} .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:var(--gap)} @media (max-width:620px){.stats{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}} .stat{padding:clamp(14px,1.6vw,20px);display:flex;flex-direction:column;gap:10px;min-width:0} .stat__top{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap} .stat__icon{width:46px;height:46px;border-radius:var(--radius-sm);display:grid;place-items:center;flex:none;background:linear-gradient(140deg,rgba(109,140,255,.22),rgba(168,85,247,.18));border:1px solid var(--stroke);color:var(--brand)} @media (max-width:620px){.stat__icon{width:38px;height:38px}} .stat__icon svg{width:21px;height:21px;stroke-width:2} .stat__value{font-size:clamp(1.7rem,3.2vw,2.6rem);font-weight:800;letter-spacing:-.04em;line-height:1} .stat__value small{font-size:.5em;font-weight:700;color:var(--muted);letter-spacing:0} .stat__label{font-size:.92rem;font-weight:700} .stat__hint{font-size:.78rem;color:var(--muted);font-weight:600;overflow-wrap:anywhere} .stat__delta{display:inline-flex;align-items:center;gap:5px;font-size:.78rem;font-weight:800;color:var(--ok)} .stat__delta svg{width:14px;height:14px;stroke-width:2.4} .modules{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:var(--gap)} @media (max-width:720px){.modules{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}} .module{padding:clamp(14px,1.7vw,20px);display:flex;flex-direction:column;gap:12px;min-width:0} .module__head{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap} .module__head>div{min-width:0;flex:1 1 55%} .module__icon{width:46px;height:46px;border-radius:var(--radius-sm);display:grid;place-items:center;flex:none;background:var(--surface-2);border:1px solid var(--stroke);color:var(--accent)} .module__icon svg{width:22px;height:22px;stroke-width:2} @media (max-width:720px){.module__icon{width:38px;height:38px}.module__icon svg{width:18px;height:18px}} .module__title{font-size:1.05rem;font-weight:800;letter-spacing:-.01em;overflow-wrap:anywhere} .module__sub{font-size:.8rem;color:var(--muted);font-weight:600;overflow-wrap:anywhere} .module__code{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:.72rem;color:var(--muted)} .module__foot{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap} .module__grade{font-size:.8rem;font-weight:800;color:var(--ok)} .bar{height:8px;border-radius:999px;background:var(--surface-2);border:1px solid var(--stroke);overflow:hidden} .bar>i{display:block;height:100%;width:0;border-radius:inherit;background:linear-gradient(90deg,var(--brand),var(--accent));transition:width 1.1s cubic-bezier(.2,.8,.2,1)} .bar--warn>i{background:linear-gradient(90deg,var(--warn),var(--danger))} .bar--muted>i{background:linear-gradient(90deg,var(--muted),var(--stroke-strong))} .module__progress{display:flex;align-items:center;gap:10px} .module__progress b{font-size:.8rem;font-weight:800;white-space:nowrap} .rail-wrap{position:relative} .rail{display:flex;gap:var(--gap);overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x mandatory;scroll-behavior:smooth;padding:4px 4px 16px;scrollbar-width:thin} .rail>*{scroll-snap-align:start;min-width:0} .rail--half>*{flex:0 0 calc(50% - var(--gap)/2)} .rail--third>*{flex:0 0 calc((100% - var(--gap) * 2) / 3)} @media (max-width:1100px){.rail--third>*{flex:0 0 calc(50% - var(--gap)/2)}} @media (max-width:720px){.rail{gap:12px;padding-bottom:12px}.rail--half>*,.rail--third>*{flex:0 0 calc(50% - 6px)}} .rail-controls{display:flex;align-items:center;gap:8px} .rail-progress{height:6px;border-radius:999px;background:var(--surface-2);border:1px solid var(--stroke);overflow:hidden;width:min(260px,40vw)} .rail-progress>i{display:block;height:100%;width:20%;border-radius:inherit;background:linear-gradient(90deg,var(--brand),var(--accent));transition:transform .25s ease,width .25s ease} .exp-card{display:flex;flex-direction:column;overflow:hidden} .exp-card__media{position:relative} .exp-card__logo{position:absolute;left:14px;bottom:-22px;width:56px;height:56px;border-radius:16px;display:grid;place-items:center;background:var(--bg-soft);border:1px solid var(--stroke);box-shadow:var(--shadow-soft);color:var(--brand)} .exp-card__logo svg{width:24px;height:24px;stroke-width:2} .exp-card__body{padding:32px clamp(14px,1.6vw,20px) clamp(14px,1.6vw,20px);display:flex;flex-direction:column;gap:10px;flex:1;min-width:0} .exp-card__title{font-size:clamp(1rem,1.5vw,1.2rem);font-weight:800;letter-spacing:-.02em;overflow-wrap:anywhere} .exp-card__company{font-size:.88rem;font-weight:700;color:var(--muted)} .exp-card__text{font-size:.88rem;color:var(--muted);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} .exp-card__foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;flex-wrap:wrap} .prj-card{display:flex;flex-direction:column;overflow:hidden;transition:transform .25s ease,box-shadow .25s ease} .prj-card:hover{transform:translateY(-6px)} .prj-card__body{padding:clamp(14px,1.6vw,18px);display:flex;flex-direction:column;gap:10px;flex:1;min-width:0} .prj-card__title{font-size:clamp(.98rem,1.4vw,1.15rem);font-weight:800;overflow-wrap:anywhere} .prj-card__text{font-size:.85rem;color:var(--muted);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} .prj-card__tags{display:flex;flex-wrap:wrap;gap:6px} .prj-card__foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;flex-wrap:wrap} .prj-card__stats{display:flex;align-items:center;gap:12px;color:var(--muted);font-size:.8rem;font-weight:700} .prj-card__stats span{display:inline-flex;align-items:center;gap:5px} .prj-card__stats svg{width:15px;height:15px} .photos{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px} @media (max-width:620px){.photos{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}} .photo-card{overflow:hidden;display:flex;flex-direction:column;min-width:0} .photo-card figcaption{padding:10px 12px;font-size:.8rem;font-weight:600;color:var(--muted);display:flex;flex-direction:column;gap:2px;min-width:0} .photo-card figcaption b{color:var(--text);font-weight:700;overflow-wrap:anywhere} .skills{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--gap)} @media (max-width:720px){.skills{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}} .skill-group{padding:clamp(14px,1.6vw,20px);display:flex;flex-direction:column;gap:14px;min-width:0} .skill-group__head{display:flex;align-items:center;gap:10px;font-weight:800;font-size:1rem} .skill-group__head svg{width:19px;height:19px;color:var(--brand);flex:none} .skill-group__count{margin-left:auto;font-size:.76rem;font-weight:800;color:var(--muted);background:var(--surface-2);border:1px solid var(--stroke);padding:3px 9px;border-radius:999px;white-space:nowrap} .skill-list{display:flex;flex-direction:column;gap:11px} .skill{display:flex;flex-direction:column;gap:6px;min-width:0} .skill__top{display:flex;align-items:center;justify-content:space-between;gap:8px} .skill__name{font-size:.86rem;font-weight:700;overflow-wrap:anywhere} .skill__val{font-size:.74rem;font-weight:800;color:var(--muted);white-space:nowrap} .tasks{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:12px} @media (max-width:760px){.tasks{grid-template-columns:1fr}} .task{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:14px;align-items:center;padding:14px 16px;transition:background .2s ease,border-color .2s ease} .task:hover{background:var(--surface-hover);border-color:var(--stroke-strong)} .task__check{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;flex:none;background:rgba(52,211,153,.14);border:1px solid rgba(52,211,153,.32);color:var(--ok)} .task__check svg{width:20px;height:20px;stroke-width:2.4} .task__check--progress{background:rgba(251,191,36,.14);border-color:rgba(251,191,36,.32);color:var(--warn)} .task__check--error{background:rgba(251,113,133,.14);border-color:rgba(251,113,133,.32);color:var(--danger)} .task__main{min-width:0;width:100%;display:flex;flex-direction:column;gap:6px} .task__title{font-size:.95rem;font-weight:800;overflow-wrap:anywhere} .task__sub{font-size:.79rem;color:var(--muted);font-weight:600;overflow-wrap:anywhere} .task__right{display:flex;flex-direction:column;align-items:flex-end;gap:6px} .task__extra{grid-column:1 / -1;display:flex;flex-wrap:wrap;gap:6px} .task__extra a{display:inline-flex;align-items:center;gap:5px;font-size:.75rem;font-weight:700;color:var(--muted);border:1px solid var(--stroke);border-radius:999px;padding:3px 9px;background:var(--surface-2)} .task__extra a:hover{color:var(--text);border-color:var(--stroke-strong)} .task__extra a svg{width:12px;height:12px} .task__thumbs{display:flex;gap:6px;flex-wrap:wrap} .task__thumbs img{width:64px;height:64px;object-fit:cover;border-radius:10px;border:1px solid var(--stroke);cursor:zoom-in} @media (max-width:520px){.task{grid-template-columns:auto minmax(0,1fr)}.task__right{grid-column:2;align-items:flex-start}} .about{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:var(--gap)} @media (max-width:960px){.about{grid-template-columns:1fr}} .about__bio{padding:clamp(18px,2vw,26px);display:flex;flex-direction:column;gap:14px} .about__text{font-size:clamp(.95rem,1.2vw,1.05rem);color:var(--muted)} .about__text b{color:var(--text)} .about__quote{margin-top:auto;padding-left:16px;border-left:3px solid var(--brand);font-style:italic;font-weight:600;color:var(--text)} .about__side{display:grid;gap:var(--gap);grid-template-columns:1fr} @media (max-width:960px){.about__side{grid-template-columns:repeat(2,minmax(0,1fr))}} @media (max-width:640px){.about__side{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}} .about__card{padding:clamp(14px,1.6vw,20px);display:flex;flex-direction:column;gap:12px;min-width:0} .about__card h3{font-size:.95rem;font-weight:800;display:flex;align-items:center;gap:9px} .about__card h3 svg{width:18px;height:18px;color:var(--brand);flex:none} .about__photo{border-radius:var(--radius-sm);overflow:hidden} .about__photo img{width:100%;display:block;aspect-ratio:4/3;object-fit:cover} .contacts{display:flex;flex-wrap:wrap;gap:8px} .contact{display:inline-flex;align-items:center;gap:9px;padding:10px 13px;border-radius:var(--radius-xs);background:var(--surface-2);border:1px solid var(--stroke);font-size:.85rem;font-weight:700;transition:background .2s ease,transform .2s ease;overflow-wrap:anywhere} .contact:hover{background:var(--surface-hover);transform:translateY(-2px)} .contact svg{width:16px;height:16px;color:var(--brand);flex:none} .about__list{display:flex;flex-direction:column;gap:10px} .about__list li{display:flex;gap:10px;align-items:flex-start;font-size:.86rem;font-weight:600;color:var(--muted)} .about__list svg{width:17px;height:17px;color:var(--ok);margin-top:2px;flex:none} .empty{display:flex;flex-direction:column;align-items:center;gap:10px;padding:clamp(22px,3vw,38px);text-align:center;color:var(--muted);font-size:.9rem;font-weight:600} .empty svg{width:30px;height:30px;stroke-width:1.8;opacity:.8} .empty b{color:var(--text);font-weight:800;font-size:.98rem} .empty small{font-weight:600;opacity:.85;max-width:56ch} .footer{margin-top:10px;padding:26px 0 0;border-top:1px solid var(--stroke);display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;color:var(--muted);font-size:.85rem;font-weight:600} .footer__links{display:flex;gap:16px;flex-wrap:wrap} .footer__links a:hover{color:var(--text)} .toast{position:fixed;left:50%;bottom:26px;z-index:90;padding:12px 20px;border-radius:999px;background:var(--bg-soft);border:1px solid var(--stroke);box-shadow:var(--shadow);font-weight:700;font-size:.88rem;opacity:0;transform:translate(-50%,140%);transition:transform .3s cubic-bezier(.2,.8,.2,1),opacity .3s ease;pointer-events:none;max-width:88vw;text-align:center} .toast.is-show{transform:translate(-50%,0);opacity:1} .visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap} .reveal{opacity:0;transform:translateY(18px);transition:opacity .6s ease,transform .6s cubic-bezier(.2,.8,.2,1)} .reveal.is-in{opacity:1;transform:none} .lb{position:fixed;inset:0;z-index:100;display:none;align-items:center;justify-content:center;background:rgba(4,7,18,.9);backdrop-filter:blur(6px);padding:24px} .lb.open{display:flex} .lb__media{max-width:min(1200px,92vw);max-height:86vh;display:grid;place-items:center} .lb__media img,.lb__media video{max-width:min(1200px,92vw);max-height:78vh;border-radius:var(--radius-sm);display:block} .lb__cap{position:absolute;left:50%;bottom:26px;transform:translateX(-50%);color:#dfe6ff;font-size:.85rem;font-weight:600;max-width:80vw;text-align:center} .lb__btn{position:absolute;width:48px;height:48px;border-radius:14px;display:grid;place-items:center;background:var(--surface);border:1px solid var(--stroke);color:#eaf0ff} .lb__btn svg{width:20px;height:20px} .lb__btn:hover{background:var(--surface-hover)} .lb__prev{left:18px;top:50%;transform:translateY(-50%)} .lb__next{right:18px;top:50%;transform:translateY(-50%)} .lb__close{right:18px;top:18px} .lb__count{position:absolute;left:18px;top:18px;padding:6px 12px;border-radius:999px;background:var(--surface);border:1px solid var(--stroke);color:#eaf0ff;font-size:.78rem;font-weight:700} @media (max-width:640px){.lb__prev{left:8px}.lb__next{right:8px}.lb__btn{width:40px;height:40px}} html.no-icons i[data-lucide]{display:inline-block;width:1em;height:1em;border-radius:4px;background:currentColor;opacity:.35;flex:none} html.no-icons .ph__icon{width:32px;height:32px} @media print{ :root{--bg:#fff;--bg-soft:#fff;--surface:#fff;--surface-2:#f5f7ff;--text:#101733;--muted:#48506e;--stroke:rgba(20,30,70,.2);--shadow:none;--shadow-soft:none} body{background-image:none !important} .topbar,.hero__actions,.rail-controls,.lb,.toast,.footer__links{display:none !important} .card{box-shadow:none;border-color:rgba(20,30,70,.25)} .rail{flex-wrap:wrap;overflow:visible} .rail--half>*,.rail--third>*{flex:0 0 calc(50% - var(--gap)/2)} .page{gap:22px;padding-bottom:0} .reveal{opacity:1 !important;transform:none !important} .media img{object-fit:contain} } @media (max-width:620px){.badge{font-size:.68rem;padding:4px 8px;gap:4px}.chip{font-size:.76rem;padding:6px 10px}.stat__delta,.stat__hint{font-size:.74rem}.stat__label{font-size:.85rem}.module__title{font-size:.95rem}.module__sub,.exp-card__company{font-size:.76rem}.skill__name{font-size:.8rem}.icon-badge{width:40px;height:40px}.fact span{font-size:.82rem}.btn{padding:11px 16px;font-size:.9rem}.rail-progress{width:min(150px,34vw)}} @media (max-width:400px){.stats{grid-template-columns:1fr}.about__side{grid-template-columns:1fr}} `; } function iconTag(name) { return ''; } function ratioMod(w, h) { if (w === h) return 'square'; const r = w / h; if (Math.abs(r - 16 / 9) < 0.02) return '16x9'; if (Math.abs(r - 4 / 3) < 0.02) return '4x3'; return 'wide'; } function phBlock(label, w, h, icon, style) { return ''; } function mediaBlock(src, mod, alt, g) { return '= 0 ? ' data-g="' + g + '"' : '') + '>' + '' + escapeHtml(alt) + ''; } function videoBlock(src, mod, g) { return '= 0 ? ' data-g="' + g + '"' : '') + '>' + ''; } function sectionHead(icon, title, hint) { return '

' + iconTag(icon) + '' + escapeHtml(title) + '

' + (hint ? '

' + escapeHtml(hint) + '

' : '') + '
'; } function railHead(icon, title, hint, railId) { return '

' + iconTag(icon) + '' + escapeHtml(title) + '

' + (hint ? '

' + escapeHtml(hint) + '

' : '') + '
' + '
' + '' + '
'; } function emptyBlock(icon, title, text) { return '
' + iconTag(icon) + '' + escapeHtml(title) + '' + (text ? '' + escapeHtml(text) + '' : '') + '
'; } function heroHtml(v) { const cover = v.mainPhotoCard ? mediaBlock(v.mainPhotoCard.src, 'wide', 'Обложка профиля', v.mainPhotoCard.g) : (v.cover ? mediaBlock(v.cover.src, 'wide', 'Обложка профиля', v.cover.g) : phBlock('Обложка профиля', 1440, 480, 'wallpaper', 'border-radius:var(--radius-sm)')); const avatar = v.profilePhotoCard ? mediaBlock(v.profilePhotoCard.src, 'square', v.name, v.profilePhotoCard.g) : (v.avatar ? mediaBlock(v.avatar.src, 'square', v.name, v.avatar.g) : phBlock('Фото резидента', 480, 480, 'user-round')); const eyebrow = [v.idLabel, str(v.periodLabel, 60)].filter(Boolean).join(' · '); const actions = []; actions.push(''); const mail = v.contacts.find(c => (c.href || '').toLowerCase().startsWith('mailto:')) || v.contacts.find(c => c.href); if (mail) actions.push('' + iconTag('send') + 'Написать'); if (v.photoCards.length) actions.push('' + iconTag('images') + 'Фотографии'); const facts = v.facts.length ? v.facts.map(f => '' + iconTag(f.icon) + '' + escapeHtml(f.text) + '').join('') : '' + iconTag('info') + 'Данные профиля ещё не заполнены'; const tags = v.heroTags.length ? v.heroTags.map(t => '' + iconTag('tag') + escapeHtml(t) + '').join('') : '' + iconTag('tag') + 'теги не заполнены'; return '
' + '
' + cover + '
' + '
' + '
' + avatar + '
' + '' + escapeHtml(v.status + (v.statusNote ? ' · ' + v.statusNote : '')) + '
' + '
' + (eyebrow ? '' + iconTag('id-card') + escapeHtml(eyebrow) + '' : '') + '

' + escapeHtml(v.name) + '

' + (v.role ? '

' + escapeHtml(v.role) + '

' : '') + (v.bio ? '

' + escapeHtml(v.bio) + '

' : '') + '
' + tags + '
' + '
' + facts + '
' + '
' + actions.join('') + '
' + '
'; } function statsHtml(v) { return v.stats.map(s => { const raw = s.raw !== undefined ? s.raw : String(s.value); return '
' + '
' + iconTag(s.icon) + '' + (s.delta ? '' + iconTag('trending-up') + escapeHtml(s.delta) + '' : '') + '
' + '

' + escapeHtml(String(s.value)) + (s.suffix ? ' ' + escapeHtml(s.suffix) + '' : '') + '

' + '

' + escapeHtml(s.label) + '

' + (s.hint ? '

' + escapeHtml(s.hint) + '

' : '') + '
'; }).join(''); } function skillsHtml(v) { if (!v.skillGroups.length) { return emptyBlock('sparkles', 'Навыки пока не заполнены', 'Список навыков и технологий можно добавить в данных профиля резидента.'); } return v.skillGroups.map(g => '
' + '
' + iconTag(g.icon) + '' + escapeHtml(g.title) + '' + '' + g.items.length + ' ' + plural(g.items.length, 'пункт', 'пункта', 'пунктов') + '
' + '
' + g.items.map(it => { const has = it.value !== null && it.value !== undefined; return '
' + '' + escapeHtml(it.name) + '' + '' + escapeHtml([it.level, has ? it.value + '%' : null].filter(Boolean).join(' · ') || '—') + '' + '
'; }).join('') + '
').join(''); } function projectMedia(p) { if (p.isImg) return mediaBlock(p.href, '16x9', p.title, p.g); if (p.isVid) return videoBlock(p.href, '16x9', p.g); return phBlock(p.ext ? p.ext + ' · файл' : 'Файл проекта', 1280, 720, p.icon); } function projectsHtml(v) { if (!v.projects.length) { return emptyBlock('folder-git-2', 'Работ в портфолио пока нет', 'Файлы появятся здесь, как только резидент прикрепит их к записи занятия.'); } return v.projects.map(p => '
' + projectMedia(p) + '
' + '' + iconTag(p.icon) + escapeHtml(p.ext || 'Файл') + '' + '

' + escapeHtml(p.title) + '

' + (p.description ? '

' + escapeHtml(p.description) + '

' : '') + (p.tags.length ? '
' + p.tags.map(t => '' + escapeHtml(t) + '').join('') + '
' : '') + '
' + (p.date ? '' + iconTag('calendar-days') + escapeHtml(p.date) + '' : '') + (p.size ? '' + iconTag('hard-drive') + escapeHtml(p.size) + '' : '') + '= 0 ? ' data-g="' + p.g + '"' : ' target="_blank" rel="noopener"') + '>' + iconTag(p.g >= 0 ? 'maximize-2' : 'external-link') + (p.g >= 0 ? 'Просмотр' : 'Открыть') + '
' + '
').join(''); } function photosHtml(v) { if (!v.photoCards.length) { return emptyBlock('image', 'Фотографий пока нет', 'Фотографии занятий появятся здесь, как только резидент отправит их вместе с записью.'); } return v.photoCards.map(p => '
' + mediaBlock(p.src, '4x3', v.name + ' — фото', p.g) + (p.caption || p.date || p.group ? '
' + (p.caption ? '' + escapeHtml(p.caption) + '' : '') + (p.date || p.group ? '' + escapeHtml([p.date, p.group].filter(Boolean).join(' · ')) + '' : '') + '
' : '') + '
').join(''); } function groupPhotosHtml(v) { if (!v.groupPhotoCards.length) { return emptyBlock('images', 'Фото группы пока нет', 'Фотографии группы появятся здесь, как только их добавят в галерее группы.'); } return v.groupPhotoCards.map(p => '
' + mediaBlock(p.src, '4x3', (p.group || 'Группа') + ' — фото', p.g) + (p.caption || p.date || p.group ? '
' + (p.caption ? '' + escapeHtml(p.caption) + '' : '') + (p.date || p.group ? '' + escapeHtml([p.date, p.group].filter(Boolean).join(' · ')) + '' : '') + '
' : '') + '
').join(''); } function tasksHtml(v) { if (!v.tasks.length) { return emptyBlock('list-checks', 'Записей о занятиях пока нет', 'Как только резидент отправит запись через форму, она появится в этом разделе.'); } return v.tasks.map(t => { const checkCls = t.done ? '' : (t.icon === 'triangle-alert' ? ' task__check--error' : ' task__check--progress'); const right = ['' + escapeHtml(t.statusLabel) + '']; if (t.thumbs.length) right.push('
' + t.thumbs.map(x => 'фото').join('') + '
'); if (t.files.length) var extra = '
' + t.files.map(f => '= 0 ? ' data-g="' + f.g + '"' : ' target="_blank" rel="noopener"') + '>' + iconTag(f.icon) + escapeHtml(truncate(f.title, 28)) + '').join('') + '
'; return '
' + '' + iconTag(t.icon) + '' + '

' + escapeHtml(t.title) + '

' + (t.sub ? '

' + escapeHtml(t.sub) + '

' : '') + '
' + '
' + right.join('') + '
' + (extra || '') + '
'; }).join(''); } function aboutHtml(v) { const bio = '
' + '

' + iconTag('user-round') + 'Коротко о себе

' + (v.bio ? '

' + escapeHtml(v.bio) + '

' : '

Здесь появится рассказ резидента о себе. Добавьте текст в данных профиля.

') + (v.quote ? '

«' + escapeHtml(v.quote) + '»

' : '') + (v.facts.length ? '
' + v.facts.map(f => '' + iconTag(f.icon) + '' + escapeHtml(f.text) + '').join('') + '
' : '') + '
'; const photoCard = '

' + iconTag('image') + 'Фото автора

' + (v.profilePhotoCard ? '
' + escapeHtml(v.name) + '
' : (v.mainPhotoCard ? '
' + escapeHtml(v.name) + '
' : (v.avatar ? '
' + escapeHtml(v.name) + '
' : phBlock('Фото автора', 800, 600, 'image')))) + '

' + iconTag('info') + ((v.profilePhotoCard || v.mainPhotoCard || v.avatar) ? 'Фотография из архива занятий' : 'Место под реальное фото автора профиля') + '

'; const contactsCard = '

' + iconTag('at-sign') + 'Контакты

' + (v.contacts.length ? '
' + v.contacts.map(c => (c.href ? '' + iconTag(c.icon) + escapeHtml(c.label) + '' : '' + iconTag(c.icon) + escapeHtml(c.label) + '')).join('') + '
' : '

' + iconTag('info') + 'Контакты не заполнены

') + '
'; const achCard = '

' + iconTag('trophy') + 'Достижения

' + (v.achievements.length ? '' : '

' + iconTag('info') + 'Достижения пока не отмечены

') + '
'; return bio + '
' + photoCard + contactsCard + achCard + '
'; } function footerHtml(v, generatedAt) { const links = []; if (v.entries.length) links.push('Занятия'); if (v.photoCards.length) links.push('Фотографии'); if (v.groupPhotoCards.length) links.push('Фото группы'); if (v.projects.length) links.push('Работы'); links.push('Наверх'); return ''; } function navItems(v) { const items = ['' + iconTag('user-round') + 'Профиль']; if (v.photoCards.length) items.push('' + iconTag('images') + 'Фотографии'); if (v.groupPhotoCards.length) items.push('' + iconTag('users-round') + 'Фото группы'); items.push('' + iconTag('book-open') + 'Образование'); if (v.experience.length) items.push('' + iconTag('briefcase') + 'Опыт'); if (v.skillGroups.length) items.push('' + iconTag('sparkles') + 'Навыки'); if (v.projects.length) items.push('' + iconTag('folder-git-2') + 'Работы'); if (v.tasks.length) items.push('' + iconTag('list-checks') + 'Занятия'); items.push('' + iconTag('info') + 'О себе'); return items.join(''); } function educationHtml(v) { if (!v.education.length) { return emptyBlock('book-open', 'Темы программы ещё не заполнены', 'Добавьте темы модулей в админке (раздел «Темы модулей») или отметьте пройденные модули в данных профиля резидента.'); } return v.education.map(m => { const p = m.progress; const hasBar = p !== null; const barCls = hasBar ? (p >= 100 ? '' : ' bar--warn') : ' bar--muted'; const foot = []; foot.push(p === null ? '' + iconTag('circle-dashed') + 'Нет данных' : (p >= 100 ? '' + iconTag('badge-check') + 'Пройден' : '' + iconTag('loader') + 'В процессе')); if (m.grade) foot.push('' + iconTag('award') + ' ' + escapeHtml(m.grade) + ''); const meta = [m.code, m.period, m.lessons ? m.lessons + ' ' + plural(m.lessons, 'занятие', 'занятия', 'занятий') + ' в программе' : null].filter(Boolean).join(' · '); return '
' + '
' + iconTag(m.icon) + '' + '
' + '

' + escapeHtml(m.title) + '

' + (m.teacher ? '

' + escapeHtml(m.teacher) + '

' : '') + '

' + escapeHtml(meta) + '

' + '
' + '
' + '' + (hasBar ? p + '%' : '—') + '
' + '
' + foot.join('') + '' + iconTag('clipboard-list') + escapeHtml(String(m.done)) + ' записей' + '
'; }).join(''); } function experienceHtml(v) { if (!v.experience.length) { return emptyBlock('briefcase', 'Опыт работы пока не заполнен', 'Кейсы, стажировки и командные проекты можно добавить в данных профиля резидента — они появятся в этом архиве.'); } return v.experience.map(e => { const media = e.media ? mediaBlock(e.media, '16x9', e.title, -1) : phBlock('Фото кейса', 1280, 720, 'image'); return '
' + '
' + media + '
' + '
' + '
' + '' + iconTag('briefcase') + escapeHtml(e.badge) + '' + (e.date ? '' + iconTag('calendar-days') + escapeHtml(e.date) + '' : '') + '
' + '

' + escapeHtml(e.title) + '

' + (e.company || e.period ? '

' + escapeHtml([e.company, e.period].filter(Boolean).join(' · ')) + '

' : '') + (e.description ? '

' + escapeHtml(e.description) + '

' : '') + (e.tags.length ? '
' + e.tags.map(t => '' + iconTag('tag') + escapeHtml(t) + '').join('') + '
' : '') + '
' + iconTag('user-check') + escapeHtml(e.company || v.name) + '
' + '
'; }).join(''); } function reportScript() { const a = [ '(function(){', 'function refresh(){ if(window.lucide && typeof window.lucide.createIcons === "function") window.lucide.createIcons(); }', 'var root = document.documentElement;', 'var themeBtn = document.getElementById("themeToggle");', 'try { var saved = localStorage.getItem("whatido-profile-theme"); if(saved) root.dataset.theme = saved; } catch(e){}', 'function paintTheme(){ if(!themeBtn) return; themeBtn.innerHTML = \'\'; refresh(); }', 'paintTheme();', 'if(themeBtn) themeBtn.addEventListener("click", function(){ root.dataset.theme = root.dataset.theme === "light" ? "dark" : "light"; try { localStorage.setItem("whatido-profile-theme", root.dataset.theme); } catch(e){} paintTheme(); });', 'var nav = document.getElementById("mainNav");', 'var burger = document.getElementById("burger");', 'if(burger && nav){', ' burger.addEventListener("click", function(){ var open = nav.classList.toggle("is-open"); burger.setAttribute("aria-expanded", open ? "true" : "false"); burger.innerHTML = \'\'; refresh(); });', ' nav.addEventListener("click", function(e){ if(e.target.closest(".nav-link")) nav.classList.remove("is-open"); });', '}', 'var toast = document.getElementById("toast"); var toastTimer;', 'function showToast(text){ if(!toast) return; toast.textContent = text; toast.classList.add("is-show"); clearTimeout(toastTimer); toastTimer = setTimeout(function(){ toast.classList.remove("is-show"); }, 2600); }', 'document.querySelectorAll("[data-rail]").forEach(function(rail){', ' var id = rail.dataset.rail;', ' var bar = document.querySelector(\'[data-rail-progress="\' + id + \'"]\');', ' function step(){ var card = rail.firstElementChild; return card ? card.getBoundingClientRect().width + 18 : rail.clientWidth * 0.5; }', ' function sync(){ if(!bar) return; var max = rail.scrollWidth - rail.clientWidth; var visible = Math.min(rail.clientWidth / rail.scrollWidth, 1); var ratio = max > 0 ? rail.scrollLeft / max : 0; bar.style.width = Math.max(visible * 100, 10) + "%"; bar.style.transform = "translateX(" + (ratio * (100 / Math.max(visible * 100, 10) - 1) * 100) + "%)"; }', ' rail.addEventListener("scroll", sync, { passive: true });', ' window.addEventListener("resize", sync);', ' sync();', ' var prev = document.querySelector(\'[data-rail-prev="\' + id + \'"]\');', ' var next = document.querySelector(\'[data-rail-next="\' + id + \'"]\');', ' if(prev) prev.addEventListener("click", function(){ rail.scrollBy({ left: -step(), behavior: "smooth" }); });', ' if(next) next.addEventListener("click", function(){ rail.scrollBy({ left: step(), behavior: "smooth" }); });', ' rail.addEventListener("keydown", function(e){ if(e.key === "ArrowRight"){ e.preventDefault(); rail.scrollBy({ left: step(), behavior: "smooth" }); } if(e.key === "ArrowLeft"){ e.preventDefault(); rail.scrollBy({ left: -step(), behavior: "smooth" }); } });', '});', 'var lb = document.getElementById("lb"); var lbMedia = document.getElementById("lbMedia"); var lbCap = document.getElementById("lbCap"); var lbCount = document.getElementById("lbCount");', 'var cur = 0;', 'function renderLb(i){ if(!GALLERY.length || !lbMedia) return; cur = (i + GALLERY.length) % GALLERY.length; var m = GALLERY[cur]; lbMedia.innerHTML = ""; var el; if(m.type === "video"){ el = document.createElement("video"); el.src = m.src; el.controls = true; } else { el = document.createElement("img"); el.src = m.src; el.alt = ""; } lbMedia.appendChild(el); if(lbCap) lbCap.textContent = m.caption || ""; if(lbCount) lbCount.textContent = (cur + 1) + " / " + GALLERY.length; }', 'function openLb(i){ if(!lb) return; renderLb(i); lb.classList.add("open"); }', 'function closeLb(){ if(!lb) return; lb.classList.remove("open"); if(lbMedia) lbMedia.innerHTML = ""; }', 'document.addEventListener("click", function(e){', ' var g = e.target.closest("[data-g]");', ' if(g){ e.preventDefault(); openLb(parseInt(g.getAttribute("data-g"), 10) || 0); return; }', ' if(e.target === lb) closeLb();', ' var act = e.target.closest("[data-action]");', ' if(act){ var a = act.getAttribute("data-action"); if(a === "print") window.print(); if(a === "top") window.scrollTo({ top: 0, behavior: "smooth" }); }', '});', ]; const b = [ 'var lbPrev = document.getElementById("lbPrev"); var lbNext = document.getElementById("lbNext"); var lbClose = document.getElementById("lbClose");', 'if(lbPrev) lbPrev.addEventListener("click", function(e){ e.preventDefault(); e.stopPropagation(); renderLb(cur - 1); });', 'if(lbNext) lbNext.addEventListener("click", function(e){ e.preventDefault(); e.stopPropagation(); renderLb(cur + 1); });', 'if(lbClose) lbClose.addEventListener("click", function(e){ e.preventDefault(); e.stopPropagation(); closeLb(); });', 'document.addEventListener("keydown", function(e){ if(!lb || !lb.classList.contains("open")) return; if(e.key === "Escape") closeLb(); else if(e.key === "ArrowRight") renderLb(cur + 1); else if(e.key === "ArrowLeft") renderLb(cur - 1); });', 'function animateValue(el){ var raw = String(el.dataset.count); var target = parseFloat(raw.replace(",", ".")); if(isNaN(target)) return; var decimals = raw.indexOf(",") >= 0 ? 1 : 0; var suffix = el.querySelector("small") ? el.querySelector("small").outerHTML : ""; var start = performance.now(); function tick(now){ var p = Math.min((now - start) / 1100, 1); var eased = 1 - Math.pow(1 - p, 3); el.innerHTML = (target * eased).toFixed(decimals).replace(".", ",") + " " + suffix; if(p < 1) requestAnimationFrame(tick); } requestAnimationFrame(tick); }', 'setTimeout(function(){ document.querySelectorAll("[data-progress]").forEach(function(el){ el.style.width = el.dataset.progress + "%"; }); }, 120);', 'var targets = Array.prototype.slice.call(document.querySelectorAll(".reveal"));', 'function reveal(el){ el.classList.add("is-in"); el.querySelectorAll(".bar > i[data-progress]").forEach(function(x){ x.style.width = x.dataset.progress + "%"; }); var counter = el.querySelector("[data-count]"); if(counter) animateValue(counter); }', 'if(typeof IntersectionObserver !== "function"){ targets.forEach(reveal); }', 'else {', ' targets.filter(function(el){ return el.getBoundingClientRect().top < window.innerHeight * 0.92; }).forEach(reveal);', ' var io = new IntersectionObserver(function(entries){ entries.forEach(function(en){ if(!en.isIntersecting) return; reveal(en.target); io.unobserve(en.target); }); }, { rootMargin: "0px 0px -5% 0px", threshold: 0.05 });', ' targets.filter(function(el){ return !el.classList.contains("is-in"); }).forEach(function(el){ io.observe(el); });', ' var spy = new IntersectionObserver(function(entries){ entries.forEach(function(en){ if(!en.isIntersecting) return; document.querySelectorAll(".nav-link").forEach(function(l){ l.setAttribute("aria-current", String(l.dataset.spy === en.target.id)); }); }); }, { rootMargin: "-45% 0px -50% 0px" });', ' document.querySelectorAll("[data-spy]").forEach(function(link){ var sec = document.getElementById(link.dataset.spy); if(sec) spy.observe(sec); });', '}', 'var year = document.getElementById("year"); if(year) year.textContent = new Date().getFullYear();', 'var CDN = ["https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js", "https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"];', 'function loadIcons(i){ if(i >= CDN.length){ document.documentElement.classList.add("no-icons"); return; } var s = document.createElement("script"); s.src = CDN[i]; s.async = true; s.onload = refresh; s.onerror = function(){ loadIcons(i + 1); }; document.head.appendChild(s); }', 'loadIcons(0);', '})();', ]; return a.concat(b).join('\n'); } function renderStudentReport(data, opts) { const v = buildProfileView(data, opts); const generatedAt = data.generatedAt || new Date(); const periodChip = data.period ? '' + iconTag('calendar-range') + escapeHtml(data.period) + '' : ''; const sections = []; sections.push('
' + heroHtml(v) + '
'); sections.push('
' + sectionHead('activity', 'Информация об авторе', 'Показатели резидента по данным журнала занятий и архива работ.') + (periodChip ? '
' + periodChip + '
' : '') + '
' + statsHtml(v) + '
'); sections.push('
' + sectionHead('images', 'Фотографии', 'Снимки из записей занятий. Нажмите на фотографию, чтобы открыть её крупнее.') + '
' + photosHtml(v) + '
'); sections.push('
' + sectionHead('users-round', 'Фотографии группы', 'Снимки из хроники группы резидента. Нажмите на фотографию, чтобы открыть её крупнее.') + '
' + groupPhotosHtml(v) + '
'); sections.push('
' + sectionHead('book-open', 'Образование', 'Темы программы, которые проходил резидент, и прогресс по ним.') + '
' + educationHtml(v) + '
'); sections.push('
' + railHead('briefcase', 'Опыт работы', 'Стажировки, фриланс и командные проекты — ленту можно прокручивать вбок.', 'experienceRail') + '
' + experienceHtml(v) + '
'); sections.push('
' + sectionHead('sparkles', 'Навыки', 'Темы и технологии, которые резидент уже проходил.') + '
' + skillsHtml(v) + '
'); sections.push('
' + railHead('folder-git-2', 'Работы', 'Файлы, прикреплённые резидентом к записям занятий.', 'projectsRail') + '
' + projectsHtml(v) + '
'); sections.push('
' + sectionHead('list-checks', 'Занятия', 'Записи журнала: что резидент делал, фотографии и файлы занятий.') + '
' + tasksHtml(v) + '
'); sections.push('
' + sectionHead('info', 'Немного о себе', 'Рассказ резидента, контакты и достижения.') + '
' + aboutHtml(v) + '
'); const topbar = '
' + '' + 'KIBERoneПрофиль резидента' + '' + '
' + '' + '' + '
' + iconTag('user-round') + '' + '' + escapeHtml(chipInit(v.name)) + '
' + '
'; const lightbox = '
' + '' + '' + '' + '
'; const galleryJson = JSON.stringify(v.gallery).replace(/'; return '\n\n\n' + '\n\n' + '' + escapeHtml(v.name) + ' — профиль резидента\n' + '\n' + '\n' + '\n' + '\n\n\n' + topbar + '\n
\n' + sections.join('\n') + '\n' + footerHtml(v, generatedAt) + '\n
\n' + '
\n' + lightbox + '\n' + script + '\n\n'; } module.exports = { createZipWriter, renderStudentReport };