Files
Learn_System/frontend/textbooks/geometry_9_hub.html
T
Maxim Dolgolyov 660e7e2747 feat(gamification): Phase 1 — full kill-switch + textbook XP wrapping
Until now the 'gamification' feature flag did nothing: it had no row in
app_settings, the admin couldn't toggle it, awardXP/awardCoins ignored
it, and the CSS only hid three dashboard widgets — XP bars in textbooks
stayed visible regardless.

Phase 1 closes every hole.

Backend (source of truth):
  • migration 029 seeds feature_gamification_enabled=1
  • new isGamificationEnabled() helper in gamification/_shared.js with a
    30s cache + invalidateGamificationCache() for instant admin toggles
  • awardXP / awardCoins / updateStreak / unlockAchievement /
    checkAchievements all bail out when the flag is off
  • /api/gamification/* and /api/shop/* (user routes) return 404 when
    disabled; admin routes remain open so the switch itself is reachable
  • adminController.updateFeatures gains 'gamification' in the allow-list
    and invalidates the cache on flip

Frontend:
  • LS.isGamificationEnabled() (synchronous, populated by loadFeatures)
    so xp.js + applyCosmetics can bail without a round-trip
  • xp.js load/add/flush become no-ops when the flag is off
  • applyCosmetics skips the round-trip when off
  • CSS .no-gamification rule expanded to cover .hero-xp-badge, .po-xp,
    .xp-card, .xp-bar, #frames-section, and a universal [data-gamified]
    hook for future blocks

Textbooks (Variant 2 of the plan):
  • backend/scripts/wrap_textbook_xp.py — idempotent script that adds
    data-gamified to 167 XP tags across 63 textbook files (chapters +
    hubs, all subjects/grades). Single CSS rule now hides everything.

Verified end-to-end: with the flag off, awardXP/awardCoins write nothing;
flipping back restores normal behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:43:24 +03:00

949 lines
47 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 http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Геометрия 9 класс — учебник</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<style>
:root{
--bg:#fff1f2; --card:#fff;
--text:#1a0a14; --muted:#6b5560;
--border:#ffe4e6;
--pri:#e11d48; --pri-d:#9f1239;
--pri-soft:#ffe4e6;
--ch1:#d97706; --ch1-d:#b45309;
--ch2:#059669; --ch2-d:#047857;
--ch3:#7c3aed; --ch3-d:#6d28d9;
--ch4:#0891b2; --ch4-d:#0e7490;
--sh:0 4px 16px rgba(225,29,72,.10);
--sh-h:0 12px 36px rgba(225,29,72,.18);
}
html.dark{
--bg:#1a0a14; --card:#291218;
--text:#ffe4e6; --muted:#c0a0a8;
--border:#5a2f3a;
--pri-soft:rgba(225,29,72,.16);
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
/* HEADER */
.hdr{position:relative;background:linear-gradient(110deg,#9f1239 0%,#e11d48 55%,#fb7185 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(255,228,230,.15)}
.hdr::before{content:'ГЕОМЕТРИЯ';position:absolute;right:-14px;top:-18%;font-family:'Outfit',sans-serif;font-size:clamp(5rem,16vw,13rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,228,230,.10);line-height:1;pointer-events:none;user-select:none}
.hdr-inner{position:relative;z-index:1;max-width:1100px;margin:0 auto;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.hdr-back{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(255,255,255,.14);border-radius:9px;color:#fff;text-decoration:none;font-size:.85rem;font-weight:600;transition:background .15s}
.hdr-back:hover{background:rgba(255,255,255,.24)}
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.85rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.85;margin-top:4px}
.hdr-side{margin-left:auto;display:flex;gap:8px}
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.14);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
/* OVERALL PROGRESS */
.prog-overall{background:linear-gradient(135deg,var(--pri-soft),rgba(251,113,133,.10));border:1px solid var(--border);border-radius:14px;padding:14px 18px;margin-bottom:28px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.po-icon{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#e11d48,#9f1239);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:900;font-style:italic}
.po-text{flex:1;min-width:160px}
.po-label{font-size:.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
.po-bar{height:8px;background:rgba(225,29,72,.12);border-radius:5px;overflow:hidden;margin-top:6px}
.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#fb7185);border-radius:5px;transition:width .5s}
.po-xp{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,#f59e0b,var(--pri));color:#fff;border-radius:99px;font-size:.8rem;font-weight:800;font-family:'Unbounded',sans-serif;letter-spacing:.02em;box-shadow:0 4px 12px rgba(225,29,72,.22)}
/* CHAPTER GRID */
.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
@media(min-width:600px){.ch-grid{grid-template-columns:1fr 1fr}}
@media(min-width:1000px){.ch-grid{grid-template-columns:repeat(4,1fr)}}
.ch-card{background:var(--card);border:1.5px solid var(--border);border-radius:18px;overflow:hidden;display:flex;flex-direction:column;transition:transform .2s,box-shadow .2s,border-color .2s;cursor:pointer;text-decoration:none;color:inherit}
.ch-card:hover{transform:translateY(-4px);box-shadow:var(--sh-h)}
.ch-cover{padding:22px 22px 18px;color:#fff;position:relative;overflow:hidden}
.ch-cover-wm{position:absolute;right:-8px;top:-22px;font-size:6rem;font-weight:900;font-family:'Outfit',sans-serif;line-height:1;color:rgba(255,255,255,.18);pointer-events:none}
.ch-num{display:inline-block;padding:4px 10px;background:rgba(255,255,255,.22);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;position:relative;z-index:1}
.ch-title{font-family:'Outfit',sans-serif;font-size:1.1rem;font-weight:800;letter-spacing:-.01em;position:relative;z-index:1;line-height:1.3}
.ch-range{font-size:.84rem;opacity:.88;margin-top:4px;position:relative;z-index:1;font-weight:500}
.ch-cover.ch1{background:linear-gradient(135deg,#92400e,#d97706 60%,#fbbf24)}
.ch-cover.ch2{background:linear-gradient(135deg,#064e3b,#059669 60%,#34d399)}
.ch-cover.ch3{background:linear-gradient(135deg,#3b0764,#7c3aed 60%,#a78bfa)}
.ch-cover.ch4{background:linear-gradient(135deg,#164e63,#0891b2 60%,#22d3ee)}
.ch-body{padding:16px 20px 18px;display:flex;flex-direction:column;flex:1}
.ch-desc{font-size:.88rem;color:var(--text);opacity:.82;flex:1;margin-bottom:12px;line-height:1.55}
.ch-prog{margin-bottom:12px}
.ch-prog-label{display:flex;justify-content:space-between;font-size:.74rem;color:var(--muted);font-weight:600;margin-bottom:4px}
.ch-prog-bar{height:6px;background:rgba(0,0,0,.07);border-radius:4px;overflow:hidden}
.ch-prog-fill{height:100%;border-radius:4px;transition:width .5s}
.ch-card.ch1-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch1),var(--ch1-d))}
.ch-card.ch2-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch2),var(--ch2-d))}
.ch-card.ch3-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch3),var(--ch3-d))}
.ch-card.ch4-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch4),var(--ch4-d))}
.ch-action{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-radius:11px;font-weight:700;font-size:.9rem;color:#fff;transition:filter .15s}
.ch-action:hover{filter:brightness(1.08)}
.ch-card.ch1-card .ch-action{background:linear-gradient(135deg,var(--ch1),#fbbf24)}
.ch-card.ch2-card .ch-action{background:linear-gradient(135deg,var(--ch2),#34d399)}
.ch-card.ch3-card .ch-action{background:linear-gradient(135deg,var(--ch3),#a78bfa)}
.ch-card.ch4-card .ch-action{background:linear-gradient(135deg,var(--ch4),#22d3ee)}
/* ACHIEVEMENT STRIP */
.ach-strip{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:18px 22px;margin-bottom:28px;display:flex;align-items:center;gap:16px;transition:border-color .4s,box-shadow .4s}
.ach-strip.lit{border-color:#f59e0b;box-shadow:0 0 0 3px rgba(245,158,11,.18)}
.ach-icon{width:52px;height:52px;border-radius:14px;background:rgba(0,0,0,.06);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .4s}
.ach-strip.lit .ach-icon{background:linear-gradient(135deg,#fbbf24,#f59e0b)}
.ach-icon svg{width:28px;height:28px;stroke:var(--muted);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ach-strip.lit .ach-icon svg{stroke:#fff}
.ach-text{flex:1}
.ach-title{font-weight:800;font-size:1.02rem;color:var(--text)}
.ach-sub{font-size:.85rem;color:var(--muted);margin-top:2px}
.ach-strip.lit .ach-title{color:#92400e}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
/* COURSE FINAL */
.final-wrap{margin:0 0 28px;background:var(--card);border:1.5px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:var(--sh)}
.final-head{padding:18px 22px;background:linear-gradient(135deg,#9f1239 0%,#e11d48 55%,#fb7185 100%);color:#fff;cursor:pointer;display:flex;align-items:center;gap:14px;user-select:none;transition:filter .15s}
.final-head:hover{filter:brightness(1.06)}
.final-head-icon{width:46px;height:46px;border-radius:12px;background:rgba(255,255,255,.18);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.final-head-icon svg{width:26px;height:26px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.final-head-text{flex:1;min-width:0}
.final-head-tag{display:inline-block;padding:3px 9px;background:rgba(255,255,255,.22);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px}
.final-head-title{font-family:'Outfit',sans-serif;font-size:1.18rem;font-weight:800;letter-spacing:-.01em;line-height:1.25}
.final-head-sub{font-size:.84rem;opacity:.88;margin-top:2px}
.final-chevron{flex-shrink:0;transition:transform .25s}
.final-chevron svg{width:24px;height:24px;stroke:#fff;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.final-wrap.open .final-chevron{transform:rotate(180deg)}
.final-body{display:none;padding:22px}
.final-wrap.open .final-body{display:block}
.fin-section-title{font-family:'Outfit',sans-serif;font-size:1.18rem;font-weight:800;color:var(--text);margin:8px 0 14px;letter-spacing:-.005em;display:flex;align-items:center;gap:9px}
.fin-section-title svg{width:20px;height:20px;stroke:var(--pri);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* PLACEHOLDER */
.fin-placeholder{padding:32px 22px;background:linear-gradient(135deg,var(--pri-soft),rgba(251,113,133,.08));border:1.5px dashed var(--pri);border-radius:14px;text-align:center;color:var(--text)}
.fin-placeholder .ph-title{font-family:'Outfit',sans-serif;font-size:1.1rem;font-weight:800;color:var(--pri-d);margin-bottom:8px}
.fin-placeholder .ph-sub{color:var(--muted);font-size:.92rem}
.boss-overall-bar{background:linear-gradient(135deg,rgba(225,29,72,.08),rgba(251,113,133,.06));border:1px solid var(--border);border-radius:12px;padding:13px 16px;margin:16px 0 18px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.boss-overall-bar .lab{font-weight:700;font-size:.95rem;color:var(--text);min-width:200px}
.boss-overall-bar .bar{flex:1;min-width:160px;height:9px;background:rgba(225,29,72,.12);border-radius:5px;overflow:hidden}
.boss-overall-bar .fill{height:100%;background:linear-gradient(90deg,#e11d48,#fb7185,#f59e0b);transition:width .5s;border-radius:5px}
/* CHEAT SHEET */
.cheat-grid{display:grid;grid-template-columns:1fr;gap:14px;margin-bottom:28px}
@media(min-width:680px){.cheat-grid{grid-template-columns:1fr 1fr}}
.cheat-card{border:1.5px solid var(--border);border-radius:13px;padding:14px 16px;background:var(--card);position:relative;overflow:hidden}
.cheat-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px}
.cheat-card.c1::before{background:linear-gradient(180deg,var(--ch1),var(--ch1-d))}
.cheat-card.c2::before{background:linear-gradient(180deg,var(--ch2),var(--ch2-d))}
.cheat-card.c3::before{background:linear-gradient(180deg,var(--ch3),var(--ch3-d))}
.cheat-card.c4::before{background:linear-gradient(180deg,var(--ch4),var(--ch4-d))}
.cheat-head{display:flex;align-items:center;gap:9px;margin-bottom:9px;padding-left:6px}
.cheat-badge{font-size:.7rem;font-weight:800;padding:2px 8px;border-radius:99px;color:#fff;letter-spacing:.05em;text-transform:uppercase}
.cheat-card.c1 .cheat-badge{background:var(--ch1)}
.cheat-card.c2 .cheat-badge{background:var(--ch2)}
.cheat-card.c3 .cheat-badge{background:var(--ch3)}
.cheat-card.c4 .cheat-badge{background:var(--ch4)}
.cheat-title{font-weight:800;color:var(--text);font-size:.98rem}
.cheat-list{list-style:none;padding-left:6px;margin:0}
.cheat-list li{padding:6px 0;border-bottom:1px dashed var(--border);font-size:.92rem;line-height:1.5;color:var(--text)}
.cheat-list li:last-child{border-bottom:0}
/* BOSS CARDS */
.boss-card{background:var(--card);border:2px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;transition:border-color .35s,box-shadow .35s,transform .2s}
.boss-card.solved{border-color:#10b981;box-shadow:0 0 0 3px rgba(16,185,129,.18)}
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}
.boss-tag{font-size:.7rem;font-weight:800;padding:3px 9px;border-radius:99px;background:rgba(225,29,72,.12);color:var(--pri-d);letter-spacing:.04em;text-transform:uppercase}
html.dark .boss-tag{color:#fecdd3}
.boss-title{font-family:'Outfit',sans-serif;font-weight:800;color:var(--text);font-size:1.02rem;flex:1;min-width:0}
.boss-q{padding:12px 14px;background:rgba(225,29,72,.06);border-radius:10px;font-size:.96rem;line-height:1.55;margin-bottom:10px;color:var(--text)}
.boss-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
.boss-input{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'JetBrains Mono',monospace;width:130px;text-align:center;font-size:.95rem;transition:border-color .15s}
.boss-input:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
.boss-btn{padding:8px 16px;border-radius:9px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:700;font-size:.88rem;cursor:pointer;font-family:inherit;transition:background .15s,border-color .15s,transform .1s}
.boss-btn:hover{background:var(--pri-soft);border-color:var(--pri)}
.boss-btn:active{transform:scale(.96)}
.boss-btn.primary{background:linear-gradient(135deg,var(--pri),#fb7185);color:#fff;border-color:transparent}
.boss-btn.primary:hover{filter:brightness(1.08)}
.boss-fb{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none;line-height:1.45}
.boss-fb.ok{display:block;background:#d1fae5;color:#065f46;border-left:4px solid #10b981}
.boss-fb.fail{display:block;background:#fee2e2;color:#7f1d1d;border-left:4px solid #dc2626}
html.dark .boss-fb.ok{background:rgba(16,185,129,.18);color:#a7f3d0}
html.dark .boss-fb.fail{background:rgba(220,38,38,.18);color:#fecaca}
.boss-hint-txt{margin-top:8px;padding:9px 13px;background:rgba(245,158,11,.12);border-left:3px solid #f59e0b;border-radius:6px;font-size:.86rem;color:var(--text);display:none;line-height:1.5}
.boss-hint-txt.show{display:block}
/* FINAL CTA */
.final-cta{margin-top:24px;padding:18px 20px;border-radius:14px;background:linear-gradient(135deg,#fef3c7,#fde68a);border:1.5px solid #fbbf24;display:none;align-items:center;gap:14px;flex-wrap:wrap}
.final-cta.show{display:flex}
html.dark .final-cta{background:linear-gradient(135deg,rgba(245,158,11,.18),rgba(217,119,6,.15));border-color:#d97706}
.final-cta-icon{width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg,#fbbf24,#f59e0b);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.final-cta-icon svg{width:28px;height:28px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.final-cta-txt{flex:1;min-width:180px}
.final-cta-title{font-weight:800;color:#92400e;font-size:1.05rem;font-family:'Outfit',sans-serif}
html.dark .final-cta-title{color:#fde68a}
.final-cta-sub{font-size:.86rem;color:#78350f;margin-top:2px}
html.dark .final-cta-sub{color:#fcd34d}
.final-cta-btn{padding:10px 18px;border-radius:10px;background:linear-gradient(135deg,var(--pri),#fb7185);color:#fff;text-decoration:none;font-weight:800;font-size:.9rem;display:inline-flex;align-items:center;gap:7px;transition:filter .15s}
.final-cta-btn:hover{filter:brightness(1.1)}
.final-cta-btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* === GEOM9 POLISH (hub) === */
@keyframes chCardIn{from{opacity:0;transform:translateY(10px) scale(.985)}to{opacity:1;transform:none}}
.ch-card{animation:chCardIn .4s cubic-bezier(.16,1,.3,1) backwards}
.ch-card:nth-child(1){animation-delay:.04s}
.ch-card:nth-child(2){animation-delay:.10s}
.ch-card:nth-child(3){animation-delay:.16s}
.ch-card:nth-child(4){animation-delay:.22s}
@keyframes bossCardIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.final-wrap.open #fin-bosses-container .boss-card{animation:bossCardIn .35s cubic-bezier(.16,1,.3,1) backwards}
.final-wrap.open #fin-bosses-container .boss-card:nth-child(1){animation-delay:.06s}
.final-wrap.open #fin-bosses-container .boss-card:nth-child(2){animation-delay:.12s}
.final-wrap.open #fin-bosses-container .boss-card:nth-child(3){animation-delay:.18s}
.final-wrap.open #fin-bosses-container .boss-card:nth-child(4){animation-delay:.24s}
.final-wrap.open #fin-bosses-container .boss-card:nth-child(5){animation-delay:.30s}
.final-wrap.open #fin-bosses-container .boss-card:nth-child(6){animation-delay:.36s}
.final-wrap.open #fin-bosses-container .boss-card:nth-child(7){animation-delay:.42s}
.po-fill,.ch-prog-fill,.boss-overall-bar .fill{transition:width .6s cubic-bezier(.16,1,.3,1)!important}
.boss-btn,.ch-action,.final-cta-btn{position:relative;overflow:hidden}
.boss-btn::after,.ch-action::after,.final-cta-btn::after{content:'';position:absolute;inset:0;background:radial-gradient(circle at center,rgba(255,255,255,.45) 0%,transparent 60%);opacity:0;transition:opacity .25s;pointer-events:none}
.boss-btn:hover::after,.ch-action:hover::after,.final-cta-btn:hover::after{opacity:1}
.cheat-list li .katex{transition:color .2s}
.cheat-list li .katex:hover{color:var(--pri-d);cursor:help}
.boss-card.glow-once{box-shadow:0 0 24px rgba(16,185,129,.6),0 0 0 2px rgba(16,185,129,.45)!important}
@keyframes bossPulseH{0%{box-shadow:0 0 0 0 rgba(16,185,129,.55)}70%{box-shadow:0 0 0 14px rgba(16,185,129,0)}100%{box-shadow:0 0 0 0 rgba(16,185,129,0)}}
.boss-card.pulse-once{animation:bossPulseH .85s ease-out}
.fin-boss-bar-bump b{transition:transform .2s,color .2s;display:inline-block}
.fin-boss-bar-bump b.bump{transform:scale(1.18);color:#10b981}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbooks" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К каталогу
</a>
</div>
<div>
<h1>Геометрия — 9 класс</h1>
<div class="hdr-sub">Полный курс: прямоугольный треугольник, окружности, теоремы синусов и косинусов, правильные многоугольники</div>
</div>
<div class="hdr-side">
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
<svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
<span id="theme-lab">Тёмная</span>
</button>
</div>
</div>
</header>
<main>
<section class="prog-overall">
<div class="po-icon">g</div>
<div class="po-text">
<div class="po-label">Общий прогресс по курсу</div>
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
</div>
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
</section>
<div class="ch-grid">
<a href="/textbook/geometry-9-ch1" class="ch-card ch1-card" id="ch-1">
<div class="ch-cover ch1">
<div class="ch-cover-wm">&#9651;</div>
<div class="ch-num">Глава 1</div>
<div class="ch-title">Соотношения в прямоугольном треугольнике</div>
<div class="ch-range">&sect;1&ndash;&sect;6 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Тригонометрия острого и тупого угла, решение прямоугольного треугольника, формулы площади и среднего геометрического.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-1">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-1" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-1">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/geometry-9-ch2" class="ch-card ch2-card" id="ch-2">
<div class="ch-cover ch2">
<div class="ch-cover-wm">&#9675;</div>
<div class="ch-num">Глава 2</div>
<div class="ch-title">Вписанные и описанные окружности</div>
<div class="ch-range">&sect;7&ndash;&sect;9 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Описанная и вписанная окружности треугольника, окружности прямоугольного треугольника, вписанные и описанные четырёхугольники.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-2">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-2" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-2">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/geometry-9-ch3" class="ch-card ch3-card" id="ch-3">
<div class="ch-cover ch3">
<div class="ch-cover-wm">&ang;</div>
<div class="ch-num">Глава 3</div>
<div class="ch-title">Теоремы синусов и косинусов</div>
<div class="ch-range">&sect;10&ndash;&sect;12 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Теоремы синусов и косинусов, формула Герона, решение произвольных треугольников.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-3">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-3" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-3">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/geometry-9-ch4" class="ch-card ch4-card" id="ch-4">
<div class="ch-cover ch4">
<div class="ch-cover-wm">&#11042;</div>
<div class="ch-num">Глава 4</div>
<div class="ch-title">Правильные многоугольники</div>
<div class="ch-range">&sect;13&ndash;&sect;16 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Внутренние углы и радиусы правильных многоугольников, треугольник/квадрат/шестиугольник, длина окружности и площадь круга.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-4">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-4" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-4">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
</div>
<section class="final-wrap" id="course-final">
<div class="final-head" id="final-head" tabindex="0" role="button" aria-expanded="false" aria-controls="final-body">
<div class="final-head-icon">
<svg viewBox="0 0 24 24"><path d="M7 4h10v6a5 5 0 0 1-10 0V4z"/><path d="M5 4h2v2H5a2 2 0 0 1 0-4M19 4h-2v2h2a2 2 0 0 0 0-4M9 20h6M12 15v5"/></svg>
</div>
<div class="final-head-text">
<div class="final-head-tag">Финал курса</div>
<div class="final-head-title">Босс-проверка по всему курсу</div>
<div class="final-head-sub">Итоговая шпаргалка и 7 интегрированных боссов. Победи всех — получи «Магистр геометрии 9» и +50 XP.</div>
</div>
<div class="final-chevron"><svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></div>
</div>
<div class="final-body" id="final-body">
<div class="fin-section-title">
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
Шпаргалка курса
</div>
<div class="cheat-grid">
<div class="cheat-card c1">
<div class="cheat-head">
<span class="cheat-badge">Гл. 1</span>
<span class="cheat-title">Прямоугольный треугольник</span>
</div>
<ul class="cheat-list">
<li>$\sin A = \dfrac{a}{c}$, $\cos A = \dfrac{b}{c}$, $\tan A = \dfrac{a}{b}$</li>
<li>$\sin^2\alpha + \cos^2\alpha = 1$</li>
<li>Тупой угол: $\sin(180°-\alpha) = \sin\alpha$, $\cos(180°-\alpha) = -\cos\alpha$</li>
<li>$S_\triangle = \dfrac{1}{2}ab\sin C$</li>
<li>Высота к гипотенузе: $h^2 = a_1 b_1$</li>
</ul>
</div>
<div class="cheat-card c2">
<div class="cheat-head">
<span class="cheat-badge">Гл. 2</span>
<span class="cheat-title">Вписанные и описанные окружности</span>
</div>
<ul class="cheat-list">
<li>$S = pr$ (вписанная), $R = \dfrac{abc}{4S}$ (описанная)</li>
<li>Прямоуг. треуг.: $R = \dfrac{c}{2}$, $r = \dfrac{a+b-c}{2}$</li>
<li>Вписанный 4-уг: $\alpha + \gamma = 180°$</li>
<li>Описанный 4-уг: $a + c = b + d$</li>
</ul>
</div>
<div class="cheat-card c3">
<div class="cheat-head">
<span class="cheat-badge">Гл. 3</span>
<span class="cheat-title">Теоремы синусов и косинусов</span>
</div>
<ul class="cheat-list">
<li>Синусов: $\dfrac{a}{\sin A} = \dfrac{b}{\sin B} = \dfrac{c}{\sin C} = 2R$</li>
<li>Косинусов: $a^2 = b^2 + c^2 - 2bc\cos A$</li>
<li>Герон: $S = \sqrt{p(p-a)(p-b)(p-c)}$, $p = \dfrac{a+b+c}{2}$</li>
</ul>
</div>
<div class="cheat-card c4">
<div class="cheat-head">
<span class="cheat-badge">Гл. 4</span>
<span class="cheat-title">Правильные многоугольники</span>
</div>
<ul class="cheat-list">
<li>Внутр. угол: $\beta = \dfrac{180°(n-2)}{n}$</li>
<li>Сторона: $a_n = 2R\sin\dfrac{180°}{n}$</li>
<li>$a_3 = R\sqrt{3}$, $a_4 = R\sqrt{2}$, $a_6 = R$</li>
<li>Окружность: $C = 2\pi R$, $S_{\text{круга}} = \pi R^2$</li>
</ul>
</div>
</div>
<div class="fin-section-title">
<svg viewBox="0 0 24 24"><path d="M14.5 3.5l-5 5L4 4l1.5 6L3 12l5 1 1 5 2.5-2.5 6 1.5-4.5-5.5 5-5"/></svg>
7 интегрированных боссов
</div>
<div class="boss-overall-bar">
<div class="lab" id="fin-boss-lab">Боссов побеждено: 0 / 7</div>
<div class="bar"><div class="fill" id="fin-boss-fill" style="width:0%"></div></div>
</div>
<div id="fin-bosses-container"></div>
<div class="final-cta" id="final-cta">
<div class="final-cta-icon">
<svg viewBox="0 0 24 24"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg>
</div>
<div class="final-cta-txt">
<div class="final-cta-title">Курс Геометрия 9 пройден!</div>
<div class="final-cta-sub">Вы прошли всю итоговую проверку курса. +50 XP, ачивка «Магистр геометрии 9» получена.</div>
</div>
<a href="/textbooks" class="final-cta-btn">
К каталогу учебников
<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</a>
</div>
</div>
</section>
<div class="ach-strip" id="ach-strip">
<div class="ach-icon">
<svg viewBox="0 0 24 24">
<path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/>
</svg>
</div>
<div class="ach-text">
<div class="ach-title">Магистр геометрии 9</div>
<div class="ach-sub" id="ach-sub">Прочитайте все 16 параграфов четырёх глав, чтобы получить достижение</div>
</div>
</div>
</main>
<footer class="foot">
Интерактивный учебник «Геометрия — 9 класс» · LearnSpace
</footer>
<script>
'use strict';
/* THEME */
(function(){
var saved = localStorage.getItem('geometry9_theme') || localStorage.getItem('theme') || 'light';
if (saved === 'dark') document.documentElement.classList.add('dark');
var lab = document.getElementById('theme-lab');
if (lab) lab.textContent = saved === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', function(){
document.documentElement.classList.toggle('dark');
var dark = document.documentElement.classList.contains('dark');
localStorage.setItem('geometry9_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
/* PROGRESS */
var TOTAL = 16;
var CH_PARA = {
'geometry-9-ch1': 6,
'geometry-9-ch2': 3,
'geometry-9-ch3': 3,
'geometry-9-ch4': 4,
};
var CH_IDX = {
'geometry-9-ch1': 1,
'geometry-9-ch2': 2,
'geometry-9-ch3': 3,
'geometry-9-ch4': 4,
};
var FIN_ACH_KEY = 'geometry9_course_master';
var FIN_BOSS_KEY = 'geometry9_course_bosses';
function setChProg(idx, readCount, total) {
var pct = total ? Math.round(readCount * 100 / total) : 0;
var labelEl = document.getElementById('prog-' + idx);
var fillEl = document.getElementById('fill-' + idx);
var btnEl = document.getElementById('btn-' + idx);
if (labelEl) labelEl.textContent = pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
if (btnEl) {
if (readCount > 0 && readCount < total) btnEl.textContent = 'Продолжить';
else if (readCount >= total) btnEl.textContent = 'Открыть снова';
else btnEl.textContent = 'Открыть главу';
}
return pct;
}
function renderProgress(children) {
var totalRead = 0;
for (var i = 0; i < children.length; i++) {
var ch = children[i];
var idx = CH_IDX[ch.slug];
if (!idx) continue;
var read = ch.progress ? ch.progress.read.length : 0;
var total = ch.para_count || CH_PARA[ch.slug] || 1;
totalRead += read;
setChProg(idx, read, total);
}
var pct = Math.round(totalRead * 100 / TOTAL);
var overallEl = document.getElementById('overall-text');
var fillEl = document.getElementById('overall-fill');
if (overallEl) overallEl.textContent = totalRead + ' из ' + TOTAL + ' параграфов · ' + pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
var xpBadge = document.getElementById('hero-xp-badge');
var xp = parseInt(localStorage.getItem('geometry9_xp') || '0', 10) || 0;
if (xpBadge && xp > 0) {
xpBadge.style.display = '';
xpBadge.textContent = xp + ' XP';
}
var mastered = localStorage.getItem(FIN_ACH_KEY) === '1';
if (totalRead >= TOTAL || mastered) {
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) {
if (mastered) sub.textContent = 'Выполнено! Вы — Магистр геометрии 9.';
else sub.textContent = 'Выполнено! Вы прочитали весь курс геометрии 9 класса.';
}
}
}
/* COURSE FINAL — bosses */
var FIN_BOSSES = [
{
n: 1,
title: 'Прямой угол + Окружность',
tag: 'Гл. 1 + Гл. 2',
q: 'В прямоугольном треугольнике катеты $5$ и $12$. Найдите <b>сумму</b> радиусов описанной и вписанной окружностей.',
hint: 'Гипотенуза $c = \\sqrt{25 + 144} = 13$. Для прямоугольного: $R = c/2$, $r = (a + b - c)/2$.',
ans: 8.5,
tol: 0.05,
step: '0.1'
},
{
n: 2,
title: 'Синус через окружность',
tag: 'Гл. 3 + Гл. 2',
q: 'В треугольнике сторона $a = 6$, противолежащий угол $A = 30°$. Найдите радиус описанной окружности $R$.',
hint: 'Теорема синусов: $\\dfrac{a}{\\sin A} = 2R$, значит $R = \\dfrac{a}{2\\sin A}$. $\\sin 30° = 0{,}5$.',
ans: 6,
tol: 0.01,
step: '1'
},
{
n: 3,
title: 'Тригонометрия + Площадь',
tag: 'Гл. 1 + Гл. 3',
q: 'В треугольнике стороны $4$ и $7$, угол между ними $90°$. Найдите площадь.',
hint: '$S = \\dfrac{1}{2}ab\\sin C$. $\\sin 90° = 1$.',
ans: 14,
tol: 0.01,
step: '1'
},
{
n: 4,
title: 'Правильный 6-уг. + Окружность',
tag: 'Гл. 4 + Гл. 2',
q: 'В правильный шестиугольник вписана окружность радиуса $\\sqrt{3}$. Найдите его сторону.',
hint: 'Для $n=6$: $a = R$ (сторона равна радиусу описанной), $r = \\dfrac{R\\sqrt{3}}{2}$. Из $r = \\sqrt{3}$ найдите $R$.',
ans: 2,
tol: 0.01,
step: '1'
},
{
n: 5,
title: 'Косинусы + Площадь',
tag: 'Гл. 3',
q: 'В треугольнике стороны $3$ и $4$, угол между ними $60°$. Найдите <b>сумму</b> площади и третьей стороны (округлите до 0{,}1).',
hint: '$S = \\dfrac{1}{2}\\cdot 3\\cdot 4\\cdot \\sin 60° = 3\\sqrt{3}$. По теореме косинусов: $a^2 = 9 + 16 - 24\\cdot \\cos 60° = 13$, $a = \\sqrt{13}$. Сложите.',
ans: 8.8,
tol: 0.1,
step: '0.1'
},
{
n: 6,
title: 'Многоугольник + Длина',
tag: 'Гл. 4',
q: 'Найдите длину окружности, описанной около правильного шестиугольника со стороной $3$. Возьмите $\\pi = 3{,}14$.',
hint: 'Для $n=6$ сторона равна радиусу: $R = a = 3$. Тогда $C = 2\\pi R$.',
ans: 18.84,
tol: 0.05,
step: '0.01'
},
{
n: 7,
title: 'Магистр Геометрии',
tag: 'синтез всего курса',
q: 'В прямоугольном треугольнике катеты $6$ и $8$. Из прямого угла опущена высота к гипотенузе. Найдите <b>произведение</b> длины этой высоты и радиуса вписанной окружности.',
hint: 'Гипотенуза $c = 10$. Высота $h = \\dfrac{ab}{c} = \\dfrac{48}{10}$. Вписанная: $r = \\dfrac{a+b-c}{2} = 2$.',
ans: 9.6,
tol: 0.05,
step: '0.1'
}
];
function loadFinBossState(){
try { return JSON.parse(localStorage.getItem(FIN_BOSS_KEY) || '{}') || {}; }
catch(e) { return {}; }
}
function saveFinBossState(s){
try { localStorage.setItem(FIN_BOSS_KEY, JSON.stringify(s)); } catch(e){}
}
function finRenderKatex(root){
if (typeof window.renderMathInElement !== 'function') return;
try {
window.renderMathInElement(root, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false}
],
throwOnError: false
});
} catch(e){}
}
function updateFinBossBar(state){
var won = 0;
for (var k in state) if (state[k]) won++;
var lab = document.getElementById('fin-boss-lab');
var fill = document.getElementById('fin-boss-fill');
if (lab) lab.textContent = 'Боссов побеждено: ' + won + ' / ' + FIN_BOSSES.length;
if (fill) fill.style.width = Math.round(won * 100 / FIN_BOSSES.length) + '%';
return won;
}
function maybeUnlockMaster(state){
if (localStorage.getItem(FIN_ACH_KEY) === '1') return;
var won = 0;
for (var k in state) if (state[k]) won++;
if (won < FIN_BOSSES.length) return;
localStorage.setItem(FIN_ACH_KEY, '1');
/* +50 XP */
var xp = parseInt(localStorage.getItem('geometry9_xp') || '0', 10) || 0;
localStorage.setItem('geometry9_xp', String(xp + 50));
try {
if (window.LS && typeof window.LS.addXp === 'function') {
window.LS.addXp(50, 'geometry9-master');
} else if (typeof window.addXp === 'function') {
window.addXp(50, 'geometry9-master');
}
} catch(e){}
/* confetti */
try { if (typeof window.confetti === 'function') window.confetti({particleCount: 160, spread: 90, origin: {y: .6}}); } catch(e){}
/* light up ach-strip */
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) sub.textContent = 'Выполнено! Вы — Магистр геометрии 9.';
/* show CTA */
var cta = document.getElementById('final-cta');
if (cta) cta.classList.add('show');
/* refresh XP badge */
var xpBadge = document.getElementById('hero-xp-badge');
if (xpBadge) {
var newXp = parseInt(localStorage.getItem('geometry9_xp') || '0', 10) || 0;
xpBadge.style.display = '';
xpBadge.textContent = newXp + ' XP';
}
}
function buildFinBoss(b, state){
var solvedClass = state[b.n] ? ' solved' : '';
var stepAttr = ' step="' + (b.step || '0.01') + '"';
return '<div class="boss-card' + solvedClass + '" id="fin-boss-' + b.n + '-card">'
+ '<div class="boss-head">'
+ '<span class="boss-tag">' + b.tag + '</span>'
+ '<span class="boss-title">Босс ' + b.n + '. ' + b.title + '</span>'
+ '</div>'
+ '<div class="boss-q" id="fin-boss-' + b.n + '-q">' + b.q + '</div>'
+ '<div class="boss-row">'
+ '<input type="number"' + stepAttr + ' class="boss-input" id="fin-boss-' + b.n + '-inp" placeholder="число"' + (state[b.n] ? ' value="' + b.ans + '" disabled' : '') + '>'
+ '<button class="boss-btn primary" id="fin-boss-' + b.n + '-go"' + (state[b.n] ? ' disabled' : '') + '>Атаковать</button>'
+ '<button class="boss-btn" id="fin-boss-' + b.n + '-hint">Подсказка</button>'
+ '</div>'
+ '<div class="boss-hint-txt" id="fin-boss-' + b.n + '-hinttxt">' + b.hint + '</div>'
+ '<div class="boss-fb' + (state[b.n] ? ' ok' : '') + '" id="fin-boss-' + b.n + '-fb">' + (state[b.n] ? 'Победа! +15 XP. Босс уже повержен.' : '') + '</div>'
+ '</div>';
}
function bindFinBoss(b){
var state = loadFinBossState();
var goBtn = document.getElementById('fin-boss-' + b.n + '-go');
var hintBtn = document.getElementById('fin-boss-' + b.n + '-hint');
var inp = document.getElementById('fin-boss-' + b.n + '-inp');
var fb = document.getElementById('fin-boss-' + b.n + '-fb');
var hintTx = document.getElementById('fin-boss-' + b.n + '-hinttxt');
var card = document.getElementById('fin-boss-' + b.n + '-card');
if (!goBtn) return;
if (hintBtn) hintBtn.addEventListener('click', function(){
if (hintTx) hintTx.classList.toggle('show');
});
if (state[b.n]) return; /* already solved */
goBtn.addEventListener('click', function(){
var v = parseFloat((inp.value || '').replace(',', '.'));
if (isNaN(v)) {
fb.className = 'boss-fb fail';
fb.textContent = 'Введите число.';
return;
}
var tol = (typeof b.tol === 'number') ? b.tol : 1e-9;
if (Math.abs(v - b.ans) <= tol + 1e-9) {
fb.className = 'boss-fb ok';
fb.textContent = 'Победа! +15 XP. Босс повержен.';
card.classList.add('solved');
goBtn.disabled = true;
inp.disabled = true;
var s = loadFinBossState();
if (!s[b.n]) {
s[b.n] = true;
saveFinBossState(s);
/* +15 XP */
var xp = parseInt(localStorage.getItem('geometry9_xp') || '0', 10) || 0;
localStorage.setItem('geometry9_xp', String(xp + 15));
try {
if (window.LS && typeof window.LS.addXp === 'function') window.LS.addXp(15, 'fin-boss-' + b.n);
else if (typeof window.addXp === 'function') window.addXp(15, 'fin-boss-' + b.n);
} catch(e){}
var xpBadge = document.getElementById('hero-xp-badge');
if (xpBadge) {
var nXp = parseInt(localStorage.getItem('geometry9_xp') || '0', 10) || 0;
xpBadge.style.display = '';
xpBadge.textContent = nXp + ' XP';
}
updateFinBossBar(s);
maybeUnlockMaster(s);
}
} else {
fb.className = 'boss-fb fail';
fb.textContent = 'Не то. Перепроверь решение и попробуй снова.';
}
});
inp.addEventListener('keydown', function(e){
if (e.key === 'Enter') { e.preventDefault(); goBtn.click(); }
});
}
var FIN_BOSSES_RENDERED = false;
function renderFinBosses(){
if (FIN_BOSSES_RENDERED) return;
var cont = document.getElementById('fin-bosses-container');
if (!cont) return;
var state = loadFinBossState();
var html = '';
for (var i = 0; i < FIN_BOSSES.length; i++) html += buildFinBoss(FIN_BOSSES[i], state);
cont.innerHTML = html;
for (var j = 0; j < FIN_BOSSES.length; j++) bindFinBoss(FIN_BOSSES[j]);
var wrap = document.getElementById('course-final');
finRenderKatex(wrap);
updateFinBossBar(state);
if (localStorage.getItem(FIN_ACH_KEY) === '1') {
var cta = document.getElementById('final-cta');
if (cta) cta.classList.add('show');
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) sub.textContent = 'Выполнено! Вы — Магистр геометрии 9.';
}
FIN_BOSSES_RENDERED = true;
}
/* FINAL accordion */
(function bindFinalAccordion(){
var head = document.getElementById('final-head');
var wrap = document.getElementById('course-final');
if (!head || !wrap) return;
function toggle(){
var willOpen = !wrap.classList.contains('open');
wrap.classList.toggle('open');
head.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
if (willOpen) {
renderFinBosses();
finRenderKatex(wrap);
}
}
head.addEventListener('click', toggle);
head.addEventListener('keydown', function(e){
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
});
})();
/* sync ach-strip + CTA on load if already mastered */
(function syncMasterOnLoad(){
if (localStorage.getItem(FIN_ACH_KEY) === '1') {
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) sub.textContent = 'Выполнено! Вы — Магистр геометрии 9.';
}
})();
function loadProgress() {
if (typeof window.LS === 'undefined' || typeof window.LS.api !== 'function') {
renderProgress([]);
return;
}
window.LS.api('/api/textbooks/geometry-9/children')
.then(function(data) {
if (data && data.children) renderProgress(data.children);
else renderProgress([]);
})
.catch(function() { renderProgress([]); });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadProgress);
} else {
loadProgress();
}
window.addEventListener('focus', loadProgress);
/* === GEOM9 HUB POLISH JS === */
(function(){
/* Smooth scroll into view when final opens */
try{
var finalHead = document.getElementById('final-head');
var finalWrap = document.getElementById('course-final');
if(finalHead && finalWrap){
finalHead.addEventListener('click', function(){
setTimeout(function(){
if(finalWrap.classList.contains('open')){
try{ finalWrap.scrollIntoView({behavior:'smooth', block:'start'}); }catch(e){}
}
}, 80);
});
}
}catch(e){}
/* Boss-card glow on defeat (one-shot, only on user action, not page-load state) */
function observeFinBosses(){
var cards = document.querySelectorAll('#fin-bosses-container .boss-card');
cards.forEach(function(card){
if(card.__finObs) return;
card.__finObs = true;
try{
var go = card.querySelector('button[id*="-go"]');
/* If already defeated at observation start — suppress glow */
if(go && go.disabled) card.__glowed = true;
var mo = new MutationObserver(function(){
if(card.__glowed) return;
var g = card.querySelector('button[id*="-go"]');
if(g && g.disabled){
card.__glowed = true;
card.classList.add('glow-once','pulse-once');
setTimeout(function(){ try{ card.classList.remove('pulse-once'); }catch(e){} }, 900);
setTimeout(function(){ try{ card.classList.remove('glow-once'); }catch(e){} }, 1500);
}
});
mo.observe(card, {childList:true, subtree:true, attributes:true, attributeFilter:['class','disabled']});
}catch(e){}
});
}
/* Bump for boss overall counter */
var lastLab = '';
function bumpLab(){
try{
var lab = document.getElementById('fin-boss-lab');
if(!lab) return;
var nv = lab.textContent;
if(nv === lastLab){ return; }
lastLab = nv;
if(!lab.parentElement.classList.contains('fin-boss-bar-bump')){
lab.parentElement.classList.add('fin-boss-bar-bump');
}
/* wrap digits in a <b> if not already — simplest: bump the whole label briefly */
lab.style.transition = 'transform .25s ease, color .25s';
lab.style.transform = 'scale(1.04)';
lab.style.color = '#10b981';
setTimeout(function(){
try{ lab.style.transform=''; lab.style.color=''; }catch(e){}
}, 320);
}catch(e){}
}
try{
var labEl = document.getElementById('fin-boss-lab');
if(labEl){
lastLab = labEl.textContent;
var labObs = new MutationObserver(bumpLab);
labObs.observe(labEl, {childList:true, characterData:true, subtree:true});
}
}catch(e){}
/* Rescan periodically (final-body uses lazy render) */
setTimeout(observeFinBosses, 600);
var pollT = setInterval(observeFinBosses, 1500);
setTimeout(function(){ try{ clearInterval(pollT); }catch(e){} }, 90000);
})();
</script>
</body>
</html>