{"id":5569,"date":"2026-03-01T22:49:28","date_gmt":"2026-03-01T20:49:28","guid":{"rendered":"https:\/\/science.knu.ua\/new\/?page_id=5569"},"modified":"2026-03-01T22:56:07","modified_gmt":"2026-03-01T20:56:07","slug":"plan-grafik-podij-2","status":"publish","type":"page","link":"https:\/\/science.knu.ua\/new\/?page_id=5569","title":{"rendered":"\u041f\u043b\u0430\u043d-\u0433\u0440\u0430\u0444\u0456\u043a \u043f\u043e\u0434\u0456\u0439"},"content":{"rendered":"<h1 style=\"color: #b80000;\"><u>\u0421\u0422\u041e\u0420\u0406\u041d\u041a\u0410 \u0423 \u041f\u0420\u041e\u0426\u0415\u0421\u0406 \u0420\u041e\u0417\u0420\u041e\u0411\u041a\u0418<\/u><\/h1>\n<!DOCTYPE html>\r\n<html lang=\"uk\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n    <title>\u041d\u0430\u0443\u043a\u043e\u0432\u0438\u0439 \u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440 \u041a\u041d\u0423<\/title>\r\n    <link href=\"https:\/\/fonts.googleapis.com\/css2?family=Montserrat:ital,wght@0,400;0,600;0,700;1,400&display=swap\" rel=\"stylesheet\">\r\n    <script src=\"https:\/\/unpkg.com\/lucide@latest\"><\/script>\r\n    <!-- Fuzzy Search Engine -->\r\n    <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/fuse.js\/dist\/fuse.js\"><\/script>\r\n    <!-- 3D Model Viewer -->\r\n    <script type=\"module\" src=\"https:\/\/ajax.googleapis.com\/ajax\/libs\/model-viewer\/3.4.0\/model-viewer.min.js\"><\/script>\r\n    <!-- HTML to Canvas for PNG Export -->\r\n    <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/html2canvas\/1.4.1\/html2canvas.min.js\"><\/script>\r\n    <style>\r\n      #knu-calendar-app {\r\n        font-family: 'Montserrat', sans-serif;\r\n        color: #222;\r\n        max-width: 1200px;\r\n        margin: 0 auto;\r\n        padding: 20px 10px;\r\n        box-sizing: border-box;\r\n        font-size: 16px; \r\n        line-height: 1.5;\r\n      }\r\n      #knu-calendar-app *, \r\n      #knu-calendar-app *::before, \r\n      #knu-calendar-app *::after {\r\n        box-sizing: border-box;\r\n      }\r\n      :root {\r\n        --knu-blue: #0b5394;\r\n        --knu-red: #b80000;\r\n        --knu-green: #2e7d32;\r\n        --knu-bg: #f9f9f9;\r\n        --knu-card-bg: #ffffff;\r\n        --knu-border: #e2e8f0;\r\n        --knu-hover: #f1f5f9;\r\n      }\r\n      \/* -------------------------------------\r\n         Unified Header & Controls (Clean UI)\r\n      -------------------------------------- *\/\r\n      .knu-cal-header {\r\n        display: flex;\r\n        justify-content: space-between;\r\n        align-items: center;\r\n        background: var(--knu-card-bg);\r\n        padding: 16px 24px;\r\n        border-radius: 16px;\r\n        box-shadow: 0 4px 12px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.02);\r\n        border: 1px solid var(--knu-border);\r\n        margin-bottom: 20px;\r\n        flex-wrap: nowrap;\r\n        gap: 20px;\r\n      }\r\n      .knu-header-left {\r\n        display: flex;\r\n        flex-direction: column;\r\n        gap: 12px;\r\n        flex: 1;\r\n        min-width: 0;\r\n        justify-content: center;\r\n      }\r\n      .knu-header-row {\r\n        display: flex;\r\n        align-items: center;\r\n        gap: 12px;\r\n        flex-wrap: wrap;\r\n        justify-content: flex-start;\r\n      }\r\n      .knu-header-top-row {\r\n        display: flex;\r\n        align-items: center;\r\n        justify-content: flex-start;\r\n        width: 100%;\r\n        flex-wrap: wrap;\r\n        gap: 16px;\r\n      }\r\n      .knu-header-divider {\r\n        width: 1px;\r\n        height: 24px;\r\n        background-color: #cbd5e1;\r\n        margin: 0 4px;\r\n      }\r\n      \/* Unified Clean Clock Jumper *\/\r\n      .knu-clock-text {\r\n        display: inline-flex;\r\n        align-items: center;\r\n        gap: 8px;\r\n        color: #64748b;\r\n        font-size: 0.9em;\r\n        font-weight: 600;\r\n        background: #f8fafc;\r\n        padding: 6px 14px;\r\n        border-radius: 20px;\r\n        border: 1px solid #e2e8f0;\r\n        cursor: pointer;\r\n        transition: all 0.2s;\r\n        font-family: inherit;\r\n      }\r\n      .knu-clock-text:hover {\r\n        background: #f0f9ff;\r\n        border-color: #bae6fd;\r\n        box-shadow: 0 2px 6px rgba(11,83,148,0.05);\r\n        transform: translateY(-1px);\r\n      }\r\n      .knu-clock-text:active {\r\n        transform: translateY(0);\r\n      }\r\n      .knu-dot-divider { opacity: 0.4; }\r\n      #knu-live-time { color: var(--knu-blue); font-variant-numeric: tabular-nums; font-weight: 700; transition: color 0.2s; }\r\n      #knu-live-date { color: #334155; transition: color 0.2s; }\r\n      .knu-clock-text:hover #knu-live-time,\r\n      .knu-clock-text:hover #knu-live-date,\r\n      .knu-clock-text:hover i {\r\n        color: #0284c7;\r\n      }\r\n      \/* Search Field *\/\r\n      .knu-search-wrapper {\r\n        display: inline-flex;\r\n        align-items: center;\r\n        background: #f8fafc;\r\n        border: 1px solid #e2e8f0;\r\n        border-radius: 20px;\r\n        padding: 6px 14px;\r\n        gap: 8px;\r\n        transition: all 0.2s;\r\n        flex: 1;\r\n        max-width: 320px;\r\n      }\r\n      .knu-search-wrapper:focus-within {\r\n        background: #fff;\r\n        border-color: var(--knu-blue);\r\n        box-shadow: 0 0 0 3px rgba(11,83,148,0.1);\r\n      }\r\n      #knu-search-input {\r\n        border: none;\r\n        outline: none;\r\n        background: transparent;\r\n        font-family: inherit;\r\n        font-size: 0.9em;\r\n        width: 100%;\r\n        color: #334155;\r\n        font-weight: 500;\r\n      }\r\n      #knu-search-input::placeholder {\r\n        color: #94a3b8;\r\n      }\r\n      #knu-search-clear {\r\n        background: #e2e8f0;\r\n        border: none;\r\n        border-radius: 50%;\r\n        color: #64748b;\r\n        cursor: pointer;\r\n        padding: 2px;\r\n        display: flex;\r\n        align-items: center;\r\n        justify-content: center;\r\n        transition: all 0.2s;\r\n      }\r\n      #knu-search-clear:hover {\r\n        background: #cbd5e1;\r\n        color: var(--knu-red);\r\n      }\r\n      \/* Navigation Group *\/\r\n      .knu-nav-group {\r\n        display: inline-flex;\r\n        align-items: center;\r\n        gap: 6px;\r\n      }\r\n      #knu-header-display {\r\n        font-size: 1.15em;\r\n        font-weight: 800;\r\n        color: var(--knu-blue);\r\n        min-width: 160px; \r\n        text-align: center;\r\n        text-transform: capitalize;\r\n        white-space: nowrap;\r\n      }\r\n      \/* \u0412\u0438\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e UI \u043a\u043d\u043e\u043f\u043e\u043a-\u0441\u0442\u0440\u0456\u043b\u043e\u043a: \u0434\u043e\u0434\u0430\u043d\u043e \u0436\u043e\u0440\u0441\u0442\u043a\u0456 \u0440\u043e\u0437\u043c\u0456\u0440\u0438 \u0434\u043b\u044f \u0437\u0430\u043f\u043e\u0431\u0456\u0433\u0430\u043d\u043d\u044f \u0441\u0445\u043b\u043e\u043f\u0443\u0432\u0430\u043d\u043d\u044e SVGs *\/\r\n      .knu-nav-btn {\r\n        display: inline-flex; align-items: center; justify-content: center;\r\n        width: 34px; height: 34px;\r\n        flex-shrink: 0;\r\n        padding: 0;\r\n        border-radius: 8px;\r\n        border: none;\r\n        background: transparent;\r\n        color: #64748b;\r\n        cursor: pointer;\r\n        transition: all 0.2s;\r\n      }\r\n      .knu-nav-btn:hover { background: #e2e8f0; color: var(--knu-blue); }\r\n      .knu-nav-btn svg, .knu-nav-btn i {\r\n        width: 20px !important;\r\n        height: 20px !important;\r\n        flex-shrink: 0;\r\n        display: block;\r\n        fill: none;\r\n        stroke: currentColor;\r\n        stroke-width: 2;\r\n        stroke-linecap: round;\r\n        stroke-linejoin: round;\r\n      }\r\n      \/* Segmented Controls (View & Zoom) *\/\r\n      .knu-segmented-control {\r\n        display: inline-flex;\r\n        align-items: center;\r\n        background: #f1f5f9;\r\n        border-radius: 10px;\r\n        padding: 4px;\r\n        gap: 2px;\r\n      }\r\n      .knu-seg-btn {\r\n        display: inline-flex; align-items: center; justify-content: center; gap: 6px;\r\n        height: 30px; padding: 0 12px;\r\n        font-size: 0.85em; font-weight: 600;\r\n        color: #64748b; border: none; background: transparent;\r\n        border-radius: 6px; cursor: pointer; transition: all 0.2s;\r\n        font-family: inherit;\r\n      }\r\n      .knu-seg-btn:hover:not(.active):not(:disabled) { color: #334155; }\r\n      .knu-seg-btn.active {\r\n        background: #fff; color: var(--knu-blue);\r\n        box-shadow: 0 1px 3px rgba(0,0,0,0.08); font-weight: 700;\r\n      }\r\n      \/* Zoom Switcher Radio logic *\/\r\n      .knu-zoom-switcher label { margin: 0; display: inline-block; }\r\n      .knu-zoom-switcher input[type=\"radio\"] { display: none; }\r\n      .knu-zoom-switcher input[type=\"radio\"]:checked + .knu-seg-btn {\r\n        background: #fff; color: var(--knu-blue);\r\n        box-shadow: 0 1px 3px rgba(0,0,0,0.08); font-weight: 700;\r\n      }\r\n      .knu-zoom-switcher input[type=\"radio\"]:disabled + .knu-seg-btn {\r\n        opacity: 0.4; cursor: not-allowed; color: #94a3b8;\r\n      }\r\n      \/* Action Pill Buttons *\/\r\n      .knu-action-btn {\r\n        display: inline-flex; align-items: center; justify-content: center; gap: 6px;\r\n        height: 34px; padding: 0 16px;\r\n        font-size: 0.85em; font-weight: 600;\r\n        border-radius: 17px;\r\n        border: 1px solid transparent; \r\n        cursor: pointer; transition: all 0.2s;\r\n        font-family: inherit;\r\n      }\r\n      .btn-export { background: #f0fdf4; color: #16a34a; border-color: #bbf7d0; }\r\n      .btn-export:hover { background: #dcfce7; color: #15803d; }\r\n      \/* -------------------------------------\r\n         Visual Group (3D & Photo)\r\n      -------------------------------------- *\/\r\n      .visual-group {\r\n        position: relative;\r\n        flex-shrink: 0;\r\n        width: 140px;\r\n        height: 140px;\r\n      }\r\n      .visual-circle {\r\n        position: absolute;\r\n        width: 85px;\r\n        height: 85px;\r\n        border-radius: 50%;\r\n        overflow: hidden;\r\n        background: #fff;\r\n        border: 3px solid var(--knu-blue);\r\n        box-shadow: 0 4px 12px rgba(0,0,0,.1);\r\n        transition: all .35s cubic-bezier(.175,.885,.32,1.275);\r\n        cursor: pointer;\r\n      }\r\n      .visual-circle img, .visual-circle model-viewer { width: 100%; height: 100%; }\r\n      .visual-circle img { object-fit: cover; }\r\n      .visual-circle img.fade-img {\r\n        position: absolute; top: 0; left: 0; opacity: 0; transition: opacity 1.2s ease-in-out; z-index: 2; pointer-events: none;\r\n      }\r\n      .photo-hint-icon {\r\n        position: absolute; bottom: 6px; right: 6px; color: #fff; background: rgba(0,0,0,.15);\r\n        border-radius: 50%; padding: 5px; display: flex; align-items: center; justify-content: center;\r\n        pointer-events: none; z-index: 3; transition: all .3s ease;\r\n      }\r\n      .photo-hint-icon i { width: 14px; height: 14px; opacity: .5; transition: opacity .3s ease; }\r\n      .vc-photo:hover .photo-hint-icon { opacity: 0.1; }\r\n      .visual-circle model-viewer { --poster-color: transparent; background: #f0f4f8; cursor: grab; }\r\n      .visual-circle model-viewer:active { cursor: grabbing; }\r\n      @keyframes gentle-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2px); } }\r\n      model-viewer.interactive-3d { animation: gentle-float 5s ease-in-out infinite; }\r\n      model-viewer.user-controlled { animation: none!important; }\r\n      .vc-3d { top: 0; left: 0; z-index: 2; }\r\n      .vc-photo { bottom: 0; right: 0; z-index: 1; }\r\n      .visual-circle:hover { z-index: 100!important; box-shadow: 0 15px 30px rgba(0,0,0,.2); border-color: var(--knu-red); }\r\n      .vc-3d:hover { transform: translate(-10px, 15px) scale(1.5); }\r\n      .vc-photo:hover { transform: translate(-20px, -15px) scale(1.5); }\r\n      .visual-hint {\r\n        position: absolute; background: #f1f5f9; padding: 1px 4px; border-radius: 4px;\r\n        box-shadow: 0 1px 3px rgba(0,0,0,.05); display: flex; align-items: center; gap: 3px;\r\n        font-size: 8px!important; font-weight: 700; color: #64748b; z-index: 0; pointer-events: none;\r\n        border: 1px solid #cbd5e1; transition: opacity .3s; letter-spacing: .5px;\r\n      }\r\n      .visual-hint i { width: 8px; height: 8px; }\r\n      .hint-3d { top: 10px; right: -5px; }\r\n      .hint-photo { bottom: 10px; left: -5px; }\r\n      .hint-3d::after, .hint-3d::before, .hint-photo::after, .hint-photo::before {\r\n        content: ''; position: absolute; top: 50%; transform: translateY(-50%); border: 3px solid transparent;\r\n      }\r\n      .hint-3d::after { left: -4px; border-right-color: #f1f5f9; }\r\n      .hint-3d::before { left: -5px; border-right-color: #cbd5e1; }\r\n      .hint-photo::after { right: -4px; border-left-color: #f1f5f9; }\r\n      .hint-photo::before { right: -5px; border-left-color: #cbd5e1; }\r\n      .visual-group:hover .visual-hint { opacity: .3; }\r\n      \/* View Containers *\/\r\n      .knu-view-container { display: none; animation: fadeIn 0.3s ease; }\r\n      .knu-view-container.active { display: block; }\r\n      \/* Calendar Layout *\/\r\n      .knu-cal-layout { display: grid; grid-template-columns: 1fr; gap: 20px; align-items: start; }\r\n      @media (min-width: 850px) { .knu-cal-layout { grid-template-columns: 3fr 2fr; } }\r\n      \/* Calendar Grid *\/\r\n      .knu-calendar-box {\r\n        background: var(--knu-card-bg); border: 1px solid var(--knu-border);\r\n        border-radius: 12px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.03);\r\n      }\r\n      .knu-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; text-align: center; transition: all 0.3s; }\r\n      .knu-day-header { font-weight: 700; color: #64748b; padding: 10px 0; font-size: 0.8em; }\r\n      .knu-day-cell {\r\n        aspect-ratio: 1 \/ 1; display: flex; flex-direction: column; align-items: center;\r\n        justify-content: center; border: 1px solid #f1f5f9; border-radius: 8px; cursor: pointer;\r\n        transition: all 0.2s; position: relative; font-weight: 600; background: #fff;\r\n      }\r\n      .knu-day-cell:hover:not(.empty) {\r\n        background: var(--knu-hover); border-color: #bae6fd; transform: scale(1.02); box-shadow: 0 2px 8px rgba(0,0,0,0.05);\r\n      }\r\n      .knu-day-cell.empty { background: transparent; border-color: transparent; cursor: default; }\r\n      .knu-day-cell.today { background: #f0f9ff; border: 2px solid var(--knu-blue); color: var(--knu-blue); }\r\n      .knu-day-cell.selected { background: var(--knu-blue); color: #fff; border-color: var(--knu-blue); }\r\n      .knu-event-dots { display: flex; gap: 4px; position: absolute; bottom: 6px; width: 100%; justify-content: center; align-items: center; }\r\n      .knu-dot { width: 6px; height: 6px; border-radius: 50%; background-color: var(--knu-red); transition: all 0.2s; }\r\n      .knu-dot.multi-day-dot { width: 16px; height: 4px; border-radius: 2px; background-color: var(--knu-green); }\r\n      .knu-day-cell.selected .knu-dot { background-color: #fff; }\r\n      \/* Horizontal Timeline View *\/\r\n      .knu-timeline-box {\r\n        background: var(--knu-card-bg); border: 1px solid var(--knu-border); border-radius: 12px;\r\n        padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); margin-bottom: 20px;\r\n      }\r\n      .knu-timeline-scroll {\r\n        width: 100%; overflow-x: auto; overflow-y: hidden; position: relative; margin-top: 10px;\r\n        padding-bottom: 10px; scrollbar-width: thin; scrollbar-color: #cbd5e1 transparent;\r\n      }\r\n      .knu-timeline-scroll::-webkit-scrollbar { height: 8px; }\r\n      .knu-timeline-scroll::-webkit-scrollbar-track { background: transparent; }\r\n      .knu-timeline-scroll::-webkit-scrollbar-thumb { background-color: #cbd5e1; border-radius: 4px; }\r\n      .knu-timeline-inner { min-width: 1000px; position: relative; padding-right: 30px; }\r\n      .knu-timeline-grid-bg { position: absolute; top: 30px; bottom: 0; left: 0; right: 0; display: flex; pointer-events: none; }\r\n      .knu-timeline-grid-line { flex: 1; border-left: 1px dashed #e2e8f0; }\r\n      .knu-timeline-hours { display: flex; border-bottom: 2px solid #e2e8f0; margin-bottom: 10px; height: 30px; }\r\n      .knu-timeline-hour { flex: 1; font-size: 0.75em; color: #94a3b8; position: relative; font-weight: 600; }\r\n      .knu-timeline-hour span { position: absolute; left: 0; transform: translateX(-50%); top: 5px; background: #fff; padding: 0 4px; white-space: nowrap; z-index: 2; }\r\n      .knu-timeline-hour:first-child span { transform: translateX(0); }\r\n      .knu-timeline-tracks { position: relative; min-height: 80px; padding-bottom: 20px; }\r\n      .knu-timeline-event { position: absolute; height: 40px; cursor: pointer; z-index: 10; }\r\n      .knu-timeline-event-text {\r\n        position: absolute; top: 0; left: 0; z-index: 2; padding: 3px 10px; color: var(--knu-blue);\r\n        font-size: 0.75em; font-weight: 700; white-space: nowrap; background: #f0f9ff;\r\n        border: 1px solid rgba(11, 83, 148, 0.2); border-radius: 12px; box-shadow: 0 2px 6px rgba(0,0,0,0.03);\r\n        transition: transform 0.2s, border-color 0.2s, color 0.2s, background 0.2s, z-index 0s;\r\n      }\r\n      .knu-timeline-event-text::before {\r\n        content: ''; display: inline-block; width: 8px; height: 8px; border-radius: 50%;\r\n        background-color: var(--knu-blue); margin-right: 6px; vertical-align: middle; transform: translateY(-1px);\r\n      }\r\n      .knu-timeline-event.multi-day .knu-timeline-event-text { color: var(--knu-green); background: #f0fdf4; border-color: rgba(46, 125, 50, 0.2); }\r\n      .knu-timeline-event.multi-day .knu-timeline-event-text::before { background-color: var(--knu-green); }\r\n      .knu-timeline-event-bar {\r\n        position: absolute; top: 28px; left: 0; width: 100%; height: 8px; background: var(--knu-blue);\r\n        border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: all 0.2s;\r\n      }\r\n      .knu-timeline-event.multi-day .knu-timeline-event-bar { background: var(--knu-green); }\r\n      .knu-timeline-event:hover { z-index: 20; }\r\n      .knu-timeline-event:hover .knu-timeline-event-bar { filter: brightness(1.15); box-shadow: 0 3px 6px rgba(0,0,0,0.15); top: 27px; height: 10px; }\r\n      .knu-timeline-event:hover .knu-timeline-event-text { border-color: var(--knu-blue); background: #fff; transform: translateY(-2px); z-index: 30; box-shadow: 0 4px 8px rgba(11, 83, 148, 0.1); }\r\n      .knu-timeline-event.multi-day:hover .knu-timeline-event-text { border-color: var(--knu-green); box-shadow: 0 4px 8px rgba(46, 125, 50, 0.1); }\r\n      .knu-timeline-event.active-track { z-index: 30; }\r\n      .knu-timeline-event.active-track .knu-timeline-event-bar { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--knu-red); background: var(--knu-red); filter: brightness(1.1); }\r\n      .knu-timeline-event.active-track .knu-timeline-event-text { border-color: var(--knu-red) !important; color: var(--knu-red) !important; background: #fff !important; box-shadow: 0 2px 8px rgba(184, 0, 0, 0.15) !important; }\r\n      .knu-timeline-event.active-track .knu-timeline-event-text::before { background-color: var(--knu-red) !important; }\r\n      .knu-timeline-current-time { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--knu-red); z-index: 5; pointer-events: none; }\r\n      .knu-timeline-current-time::after { content: ''; position: absolute; top: 0; left: -4px; width: 10px; height: 10px; border-radius: 50%; background: var(--knu-red); }\r\n      \/* Event Details Sidebar & Cards *\/\r\n      .knu-events-panel {\r\n        background: var(--knu-card-bg); border: 1px solid var(--knu-border); border-radius: 12px; padding: 20px;\r\n        box-shadow: 0 4px 15px rgba(0,0,0,0.03); min-height: 400px; max-height: 600px; overflow-y: auto; scroll-behavior: smooth;\r\n      }\r\n      .knu-panel-title {\r\n        font-size: 1.1em; font-weight: 700; color: var(--knu-blue); margin-top: 0; margin-bottom: 15px;\r\n        display: flex; align-items: center; gap: 8px; border-bottom: 2px solid #f1f5f9; padding-bottom: 10px;\r\n      }\r\n      .knu-no-events { text-align: center; color: #94a3b8; padding: 30px 10px; font-size: 0.95em; display: flex; flex-direction: column; align-items: center; gap: 10px; }\r\n      .knu-event-card {\r\n        border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 15px; background: #fafafa; border-left: 4px solid var(--knu-blue);\r\n        animation: fadeIn 0.3s ease; transition: border-color 0.3s, box-shadow 0.3s;\r\n      }\r\n      .knu-event-card.highlight { border-color: var(--knu-blue); box-shadow: 0 0 12px rgba(11, 83, 148, 0.15); }\r\n      .knu-event-card-header { padding: 15px; cursor: pointer; position: relative; }\r\n      .knu-event-card:not(.no-body) .knu-event-card-header:hover { background-color: #f8fafc; }\r\n      .knu-event-card:not(.no-body) .knu-event-card-header::after {\r\n        content: ''; position: absolute; right: 15px; top: 20px; width: 8px; height: 8px;\r\n        border-right: 2px solid #94a3b8; border-bottom: 2px solid #94a3b8; transform: rotate(45deg); transition: transform 0.2s;\r\n      }\r\n      .knu-event-card.expanded .knu-event-card-header::after { transform: rotate(-135deg); top: 24px; }\r\n      .knu-event-card.no-body .knu-event-card-header { cursor: default; }\r\n      .knu-event-title { font-weight: 700; font-size: 1.05em; color: #1e293b; margin-bottom: 5px; padding-right: 20px; }\r\n      .knu-event-time { font-size: 0.8em; color: var(--knu-red); font-weight: 600; display: flex; align-items: center; gap: 4px; }\r\n      .knu-event-card-body { display: none; padding: 0 15px 15px 15px; border-top: 1px dashed #e2e8f0; margin-top: 5px; padding-top: 15px; }\r\n      .knu-event-card.expanded .knu-event-card-body { display: block; animation: fadeIn 0.3s ease; }\r\n      .knu-event-desc { font-size: 0.9em; color: #475569; margin-bottom: 15px; line-height: 1.4; }\r\n      .knu-event-links { display: flex; flex-wrap: wrap; gap: 8px; }\r\n      .knu-tag-link {\r\n        display: inline-flex; align-items: center; gap: 5px; font-size: 0.75em; color: #475569;\r\n        background-color: #fff; border: 1px solid #cbd5e1; padding: 5px 10px; border-radius: 6px;\r\n        text-decoration: none; transition: all 0.2s; font-weight: 600;\r\n      }\r\n      .knu-tag-link:hover { background-color: #f0f9ff; border-color: #bae6fd; color: var(--knu-blue); }\r\n      .knu-tag-link.important { background-color: #f0fdf4; border-color: #bbf7d0; color: var(--knu-green); }\r\n      \/* Media Cover *\/\r\n      .knu-event-cover { margin-bottom: 15px; border-radius: 8px; overflow: hidden; background: #fdfdfd; border: 1px solid #e2e8f0; }\r\n      .knu-event-cover img { width: 100%; height: auto; display: block; max-height: 400px; object-fit: contain; }\r\n      .knu-event-cover iframe { width: 100%; aspect-ratio: 16 \/ 9; display: block; border: none; }\r\n      \/* Status Area *\/\r\n      .knu-status-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; font-size: 0.85em; color: #64748b; }\r\n      .knu-close-filter {\r\n        margin-left: auto; background: transparent; border: none; color: var(--knu-red); cursor: pointer;\r\n        padding: 4px; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: all 0.2s;\r\n      }\r\n      .knu-close-filter:hover { background: #fef2f2; transform: scale(1.1); }\r\n      \/* Cards Layout (View 3) *\/\r\n      #view-cards .knu-cards-layout {\r\n        display: grid;\r\n        grid-template-columns: repeat(3, 1fr);\r\n        gap: 20px;\r\n        align-items: stretch;\r\n      }\r\n      #view-cards .knu-event-card {\r\n        height: 100%; display: flex; flex-direction: column;\r\n        background: var(--knu-card-bg); margin-bottom: 0;\r\n        box-shadow: 0 4px 15px rgba(0,0,0,0.03);\r\n      }\r\n      #view-cards .knu-event-card-body {\r\n        display: block !important; flex-grow: 1; animation: none;\r\n      }\r\n      #view-cards .knu-event-card-header::after { display: none !important; }\r\n      #view-cards .knu-event-card-header { cursor: default; }\r\n      #view-cards .knu-event-card:not(.no-body) .knu-event-card-header:hover { background-color: transparent; }\r\n      \/* Mobile Adjustments *\/\r\n      @media (max-width: 950px) {\r\n        .hide-mobile { display: none; }\r\n        .knu-cal-header { flex-direction: column; text-align: center; gap: 15px; padding: 15px; }\r\n        .knu-header-left { align-items: center; width: 100%; }\r\n        .knu-header-row { justify-content: center; width: 100%; }\r\n        .knu-header-top-row { justify-content: center; flex-direction: column; }\r\n        .knu-search-wrapper { max-width: 100%; width: 100%; }\r\n        .knu-header-divider { display: none; }\r\n        .visual-group { margin: 0 auto; }\r\n        #view-cards .knu-cards-layout { grid-template-columns: repeat(2, 1fr); }\r\n      }\r\n      @media (max-width: 650px) {\r\n        #view-cards .knu-cards-layout { grid-template-columns: 1fr; }\r\n      }\r\n      \/* -------------------------------------\r\n         Export Mode (Universal Offline View)\r\n      -------------------------------------- *\/\r\n      .export-mode {\r\n        max-width: fit-content !important; min-width: 100% !important; background: var(--knu-bg) !important; padding: 40px !important;\r\n      }\r\n      .export-mode .knu-events-panel {\r\n        max-height: none !important; overflow: visible !important; box-shadow: none !important; border: none !important; padding: 0 !important; margin-top: 20px;\r\n      }\r\n      .export-mode .knu-timeline-box { box-shadow: none !important; border: none !important; padding: 0 !important; }\r\n      .export-mode .knu-timeline-scroll { overflow: visible !important; }\r\n      .export-mode .knu-event-card-body { display: block !important; animation: none !important; }\r\n      .export-mode .knu-event-card-header::after { display: none !important; }\r\n      .export-mode #knu-export-btn, .export-mode .knu-search-wrapper, .export-mode .knu-close-filter,\r\n      .export-mode .visual-hint, .export-mode .photo-hint-icon, .export-mode .vc-3d { display: none !important; }\r\n      .export-mode .knu-cal-layout { display: flex !important; flex-direction: column !important; gap: 30px !important; }\r\n      .export-mode .knu-cards-layout { display: flex !important; flex-direction: column !important; gap: 15px !important; }\r\n      .export-mode .knu-cal-header { border: none !important; box-shadow: none !important; background: transparent !important; padding: 0 0 20px 0 !important; }\r\n      @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\r\n      @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }\r\n    <\/style>\r\n<\/head>\r\n<body>\r\n<div id=\"knu-calendar-app\">\r\n    <!-- Unified Header with Clean User-Friendly Controls -->\r\n    <div class=\"knu-cal-header\">\r\n        <div class=\"knu-header-left\">\r\n            <div class=\"knu-header-top-row\">\r\n                <div class=\"knu-search-wrapper\">\r\n                    <i data-lucide=\"search\" size=\"16\" style=\"color: #94a3b8;\"><\/i>\r\n                    <input type=\"text\" id=\"knu-search-input\" placeholder=\"\u041f\u043e\u0448\u0443\u043a \u0437\u0430 \u043d\u0430\u0437\u0432\u043e\u044e, \u043e\u043f\u0438\u0441\u043e\u043c\" autocomplete=\"off\">\r\n                    <button id=\"knu-search-clear\" style=\"display:none;\" title=\"\u041e\u0447\u0438\u0441\u0442\u0438\u0442\u0438\"><i data-lucide=\"x\" size=\"14\"><\/i><\/button>\r\n                <\/div>\r\n                <button class=\"knu-clock-text\" id=\"knu-today-btn\" title=\"\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0456\u0437\u043e\u0432\u0430\u043d\u043e \u0437 \u0456\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043e\u043c. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c, \u0449\u043e\u0431 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0438\u0441\u044f \u0434\u043e \u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0433\u043e \u0447\u0430\u0441\u0443\">\r\n                    <i data-lucide=\"calendar-clock\" size=\"16\"><\/i>\r\n                    <span id=\"knu-live-date\">-- -- ----<\/span>\r\n                    <span class=\"knu-dot-divider\">\u2022<\/span>\r\n                    <span id=\"knu-live-time\">--:--:--<\/span>\r\n                    <i data-lucide=\"mouse-pointer-click\" size=\"16\" style=\"margin-left: 2px;\"><\/i>\r\n                <\/button>\r\n            <\/div>\r\n            <div class=\"knu-header-row\">\r\n                <div class=\"knu-nav-group\">\r\n                    <button class=\"knu-nav-btn\" id=\"knu-prev-btn\" title=\"\u041f\u043e\u043f\u0435\u0440\u0435\u0434\u043d\u0456\u0439 \u043f\u0435\u0440\u0456\u043e\u0434 \/ \u0434\u0435\u043d\u044c\">\r\n                        <i data-lucide=\"chevron-left\" size=\"20\"><\/i>\r\n                    <\/button>\r\n                    <div id=\"knu-header-display\">\u0417\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f...<\/div>\r\n                    <button class=\"knu-nav-btn\" id=\"knu-next-btn\" title=\"\u041d\u0430\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u043f\u0435\u0440\u0456\u043e\u0434 \/ \u0434\u0435\u043d\u044c\">\r\n                        <i data-lucide=\"chevron-right\" size=\"20\"><\/i>\r\n                    <\/button>\r\n                <\/div>\r\n                <div class=\"knu-header-divider hide-mobile\"><\/div>\r\n                <!-- \u041f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447\u0456 \u0432\u0438\u0433\u043b\u044f\u0434\u0443 -->\r\n                <div class=\"knu-segmented-control\">\r\n                    <button class=\"knu-seg-btn active\" id=\"btn-view-cal\" data-view=\"calendar\" title=\"\u041a\u043b\u0430\u0441\u0438\u0447\u043d\u0438\u0439 \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\">\r\n                        <i data-lucide=\"calendar\" size=\"16\"><\/i> <span class=\"hide-mobile\">\u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440<\/span>\r\n                    <\/button>\r\n                    <button class=\"knu-seg-btn\" id=\"btn-view-timeline\" data-view=\"timeline\" title=\"\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u0438\u0439 \u0440\u043e\u0437\u043a\u043b\u0430\u0434\">\r\n                        <i data-lucide=\"bar-chart-horizontal\" size=\"16\"><\/i> <span class=\"hide-mobile\">\u0425\u0440\u043e\u043d\u043e\u043b\u043e\u0433\u0456\u044f<\/span>\r\n                    <\/button>\r\n                    <button class=\"knu-seg-btn\" id=\"btn-view-cards\" data-view=\"cards\" title=\"\u0428\u0432\u0438\u0434\u043a\u0438\u0439 \u043f\u0435\u0440\u0435\u0433\u043b\u044f\u0434 \u043a\u0430\u0440\u0442\u043e\u043a \u043f\u043e\u0434\u0456\u0439\">\r\n                        <i data-lucide=\"layout-grid\" size=\"16\"><\/i> <span class=\"hide-mobile\">\u041a\u0430\u0440\u0442\u043a\u0438<\/span>\r\n                    <\/button>\r\n                <\/div>\r\n                <div class=\"knu-segmented-control knu-zoom-switcher\" title=\"\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u0437\u0440\u0443\u0447\u043d\u0438\u0439 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\">\r\n                    <label title=\"\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u0440\u043e\u0437\u043a\u043b\u0430\u0434 \u043f\u043e \u0433\u043e\u0434\u0438\u043d\u0430\u0445 (\u0442\u0456\u043b\u044c\u043a\u0438 \u0422\u0430\u0439\u043c\u043b\u0430\u0439\u043d)\">\r\n                        <input type=\"radio\" name=\"zoom\" value=\"1\" id=\"zoom-1\">\r\n                        <span class=\"knu-seg-btn\">\u0414\u0435\u043d\u044c<\/span>\r\n                    <\/label>\r\n                    <label>\r\n                        <input type=\"radio\" name=\"zoom\" value=\"2\" id=\"zoom-2\">\r\n                        <span class=\"knu-seg-btn\">\u0422\u0438\u0436\u0434\u0435\u043d\u044c<\/span>\r\n                    <\/label>\r\n                    <label>\r\n                        <input type=\"radio\" name=\"zoom\" value=\"3\" id=\"zoom-3\" checked>\r\n                        <span class=\"knu-seg-btn active-radio\">\u041c\u0456\u0441\u044f\u0446\u044c<\/span>\r\n                    <\/label>\r\n                    <label>\r\n                        <input type=\"radio\" name=\"zoom\" value=\"4\" id=\"zoom-4\">\r\n                        <span class=\"knu-seg-btn\">\u0420\u0456\u043a<\/span>\r\n                    <\/label>\r\n                <\/div>\r\n                <button class=\"knu-action-btn btn-export\" id=\"knu-export-btn\" title=\"\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438 \u0440\u043e\u0437\u043a\u043b\u0430\u0434 \u044f\u043a \u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439\">\r\n                    <i data-lucide=\"download\" size=\"16\"><\/i> <span class=\"hide-mobile\">\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438 PNG<\/span>\r\n                <\/button>\r\n            <\/div>\r\n        <\/div>\r\n        <!-- Right Side: Visual Group (3D Model & Images) -->\r\n        <div class=\"visual-group\">\r\n            <div class=\"visual-hint hint-3d\"><i data-lucide=\"box\"><\/i> 3D<\/div>\r\n            <div class=\"visual-hint hint-photo\"><i data-lucide=\"camera\"><\/i> \u0424\u043e\u0442\u043e<\/div>\r\n            <div class=\"visual-circle vc-3d\">\r\n                <model-viewer \r\n                    id=\"hero-model\" class=\"interactive-3d\" camera-controls disable-zoom disable-pan interaction-prompt=\"none\"\r\n                    interpolation-decay=\"200\" shadow-intensity=\"1\" field-of-view=\"25deg\" environment-image=\"neutral\"\r\n                ><\/model-viewer>\r\n            <\/div>\r\n            <div class=\"visual-circle vc-photo\" onclick=\"cyclePhoto(this)\" data-index=\"0\" data-photos='[]'>\r\n                <img decoding=\"async\" id=\"hero-photo\" class=\"base-img\" src=\"\" alt=\"\u0413\u043e\u043b\u043e\u0432\u043d\u0438\u0439 \u043a\u043e\u0440\u043f\u0443\u0441 \u041a\u041d\u0423\">\r\n                <div class=\"photo-hint-icon\"><i data-lucide=\"mouse-pointer-click\"><\/i><\/div>\r\n            <\/div>\r\n        <\/div>\r\n    <\/div>\r\n    <!-- VIEW 1: Standard Calendar -->\r\n    <div id=\"view-calendar\" class=\"knu-view-container active\">\r\n        <div class=\"knu-cal-layout\">\r\n            <div class=\"knu-calendar-box\">\r\n                <div class=\"knu-cal-grid\" id=\"knu-days-header\">\r\n                    <div class=\"knu-day-header\">\u041f\u043d<\/div>\r\n                    <div class=\"knu-day-header\">\u0412\u0432<\/div>\r\n                    <div class=\"knu-day-header\">\u0421\u0440<\/div>\r\n                    <div class=\"knu-day-header\">\u0427\u0442<\/div>\r\n                    <div class=\"knu-day-header\">\u041f\u0442<\/div>\r\n                    <div class=\"knu-day-header\" style=\"color:var(--knu-red)\">\u0421\u0431<\/div>\r\n                    <div class=\"knu-day-header\" style=\"color:var(--knu-red)\">\u041d\u0434<\/div>\r\n                <\/div>\r\n                <div class=\"knu-cal-grid\" id=\"knu-calendar-grid\"><\/div>\r\n                <div class=\"knu-status-bar\">\r\n                    <span id=\"knu-fetch-status\"><i data-lucide=\"loader-2\" class=\"animate-spin\" size=\"14\"><\/i> \u041e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u043e\u0434\u0456\u0439...<\/span>\r\n                    <span><i data-lucide=\"info\" size=\"14\"><\/i> \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u043e\u0431'\u0454\u043a\u0442 \u0434\u043b\u044f \u0434\u0435\u0442\u0430\u043b\u0435\u0439<\/span>\r\n                <\/div>\r\n            <\/div>\r\n            <div class=\"knu-events-panel\">\r\n                <h3 class=\"knu-panel-title\">\r\n                    <i data-lucide=\"calendar-days\" size=\"20\"><\/i> <span id=\"knu-selected-date-title\">\u041f\u043e\u0434\u0456\u0457<\/span>\r\n                <\/h3>\r\n                <div id=\"knu-events-list\"><\/div>\r\n            <\/div>\r\n        <\/div>\r\n    <\/div>\r\n    <!-- VIEW 2: Horizontal Timeline -->\r\n    <div id=\"view-timeline\" class=\"knu-view-container\">\r\n        <div class=\"knu-timeline-box\">\r\n            <h3 class=\"knu-panel-title\" style=\"margin-bottom: 5px; border-bottom: none;\">\r\n                <i data-lucide=\"clock-4\" size=\"20\"><\/i> \u0428\u043a\u0430\u043b\u0430 \u0447\u0430\u0441\u0443\r\n            <\/h3>\r\n            <p style=\"font-size: 0.85em; color:#64748b; margin-top:0; margin-bottom:15px;\">\u041d\u0430\u043e\u0447\u043d\u0435 \u0440\u043e\u0437\u043c\u0456\u0449\u0435\u043d\u043d\u044f \u043f\u043e\u0434\u0456\u0439. \u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0430\u0441\u0448\u0442\u0430\u0431 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0433\u043b\u044f\u0434\u0443 \u0434\u043d\u044f, \u0442\u0438\u0436\u043d\u044f \u0430\u0431\u043e \u043c\u0456\u0441\u044f\u0446\u044f.<\/p>\r\n            <div class=\"knu-timeline-scroll\" id=\"knu-timeline-scroll\">\r\n                <div class=\"knu-timeline-inner\" id=\"knu-timeline-inner\">\r\n                    <div class=\"knu-timeline-hours\" id=\"knu-timeline-hours\"><\/div>\r\n                    <div class=\"knu-timeline-grid-bg\" id=\"knu-timeline-grid\"><\/div>\r\n                    <div class=\"knu-timeline-tracks\" id=\"knu-timeline-tracks\"><\/div>\r\n                <\/div>\r\n            <\/div>\r\n        <\/div>\r\n        <div class=\"knu-events-panel\">\r\n            <h3 class=\"knu-panel-title\">\r\n                <i data-lucide=\"list\" size=\"20\"><\/i> <span id=\"knu-timeline-date-title\">\u0414\u0435\u0442\u0430\u043b\u0456 \u043f\u043e\u0434\u0456\u0439<\/span>\r\n            <\/h3>\r\n            <div id=\"knu-timeline-events-list\"><\/div>\r\n        <\/div>\r\n    <\/div>\r\n    <!-- VIEW 3: Cards Look -->\r\n    <div id=\"view-cards\" class=\"knu-view-container\">\r\n        <div class=\"knu-cards-layout\" id=\"knu-cards-grid\"><\/div>\r\n    <\/div>\r\n<\/div>\r\n<script>\r\n\/\/ --- Visual Group 3D & Image Handling Logic ---\r\nconst modelsDB = {\r\n    \"main\": {\r\n        url: \"https:\/\/nd4s.github.io\/science_knu_ua_new_files\/3d_models\/comp_tsnuk.glb\",\r\n        photos:[\r\n            \"https:\/\/science.knu.ua\/new\/wp-content\/uploads\/2026\/02\/foto_korpus.png\",\r\n            \"https:\/\/science.knu.ua\/new\/wp-content\/uploads\/2026\/02\/foto_korpus_2.png\",\r\n            \"https:\/\/science.knu.ua\/new\/wp-content\/uploads\/2026\/02\/foto_korpus_3.png\"\r\n        ],\r\n        orbit: \"-90deg 65deg 105%\" \r\n    }\r\n};\r\nfunction getOrbitalLimits(orbitString) {\r\n    if(!orbitString) return { min: '-30deg 50deg auto', max: '110deg 100deg auto' };\r\n    const parts = orbitString.split(' ');\r\n    const theta = parseFloat(parts[0]); \r\n    const range = 75; \r\n    return { min: `${theta - range}deg 50deg auto`, max: `${theta + range}deg 100deg auto` };\r\n}\r\nfunction initHero() {\r\n    const heroData = modelsDB['main'];\r\n    if (heroData) {\r\n        const mv = document.getElementById('hero-model');\r\n        const img = document.getElementById('hero-photo');\r\n        if (mv) {\r\n            const limits = getOrbitalLimits(heroData.orbit);\r\n            mv.src = heroData.url; mv.cameraOrbit = heroData.orbit;\r\n            mv.minCameraOrbit = limits.min; mv.maxCameraOrbit = limits.max; mv.dataset.defaultOrbit = heroData.orbit;\r\n        }\r\n        if (img && heroData.photos && heroData.photos.length > 0) {\r\n            img.src = heroData.photos[0];\r\n            const vcPhoto = img.closest('.vc-photo'); if (vcPhoto) vcPhoto.setAttribute('data-photos', JSON.stringify(heroData.photos));\r\n        }\r\n    }\r\n}\r\nwindow.crossfadePhoto = function(el, newSrc, newIndex, onComplete) {\r\n    if (el.dataset.isAnimating === 'true') return;\r\n    el.dataset.isAnimating = 'true';\r\n    const oldImg = el.querySelector('img.base-img');\r\n    if (!oldImg) { el.dataset.isAnimating = 'false'; if(onComplete) onComplete(); return; }\r\n    const existingFade = el.querySelector('.fade-img'); if (existingFade) existingFade.remove();\r\n    const newImg = new Image(); newImg.className = 'fade-img';\r\n    newImg.onload = () => {\r\n        el.appendChild(newImg); el.setAttribute('data-index', newIndex); void newImg.offsetWidth;\r\n        newImg.style.opacity = '1';\r\n        setTimeout(() => { if (el.contains(newImg)) { oldImg.src = newSrc; newImg.remove(); } el.dataset.isAnimating = 'false'; if(onComplete) onComplete(); }, 1200);\r\n    };\r\n    newImg.onerror = () => { el.dataset.isAnimating = 'false'; if(onComplete) onComplete(); };\r\n    newImg.src = newSrc;\r\n}\r\nwindow.startAutoCycle = function(el, photos) {\r\n    if(el._autoCycleTimer) clearTimeout(el._autoCycleTimer);\r\n    const scheduleNext = () => {\r\n        let cur = parseInt(el.getAttribute('data-index') || '0'); let delay = (cur === 0) ? 300000 : 8000; \r\n        el._autoCycleTimer = setTimeout(() => {\r\n            let current = parseInt(el.getAttribute('data-index') || '0'); let next = (current + 1) % photos.length;\r\n            window.crossfadePhoto(el, photos[next], next, () => { scheduleNext(); });\r\n        }, delay);\r\n    };\r\n    scheduleNext();\r\n}\r\nwindow.cyclePhoto = function(el) {\r\n    if (el.dataset.isAnimating === 'true') return;\r\n    const photosStr = el.getAttribute('data-photos'); if(!photosStr) return;\r\n    try {\r\n        const photos = JSON.parse(photosStr); if(photos.length <= 1) return;\r\n        if(el._autoCycleTimer) clearTimeout(el._autoCycleTimer);\r\n        let current = parseInt(el.getAttribute('data-index') || '0'); let next = (current + 1) % photos.length;\r\n        window.crossfadePhoto(el, photos[next], next, () => { if (el.dataset.hovered !== \"true\") window.startAutoCycle(el, photos); });\r\n    } catch(e) {}\r\n}\r\nfunction initPhotoInteractions() {\r\n    const photoEls = document.querySelectorAll('.vc-photo');\r\n    photoEls.forEach(el => {\r\n        const img = el.querySelector('img:not(.fade-img)'); if (img && !img.classList.contains('base-img')) img.classList.add('base-img');\r\n        const photosStr = el.getAttribute('data-photos'); if (!photosStr) return;\r\n        const photos = JSON.parse(photosStr); if (photos.length <= 1) return;\r\n        window.startAutoCycle(el, photos);\r\n        if (!el.dataset.interactionInit) {\r\n            el.dataset.interactionInit = \"true\";\r\n            el.addEventListener('mouseenter', () => { el.dataset.hovered = \"true\"; if(el._autoCycleTimer) clearTimeout(el._autoCycleTimer); });\r\n            el.addEventListener('mouseleave', () => { el.dataset.hovered = \"false\"; window.startAutoCycle(el, photos); });\r\n        }\r\n    });\r\n}\r\nconst handleInputStart = (e) => {\r\n    if (e.target.tagName !== 'MODEL-VIEWER') return;\r\n    const mv = e.target; mv.classList.add('user-controlled');\r\n    if(mv.resumeTimeout) { clearTimeout(mv.resumeTimeout); mv.resumeTimeout = null; }\r\n};\r\nconst handleInputEnd = (e) => {\r\n    if (e.target.tagName !== 'MODEL-VIEWER') return;\r\n    const mv = e.target; mv.resumeTimeout = setTimeout(() => { mv.classList.remove('user-controlled'); }, 3000);\r\n};\r\nconst animateModelOrbits = () => {\r\n    const models = document.querySelectorAll('model-viewer.interactive-3d');\r\n    const time = Date.now();\r\n    models.forEach(mv => {\r\n        if(mv.classList.contains('user-controlled')) return;\r\n        const baseOrbit = mv.dataset.defaultOrbit; if(!baseOrbit) return;\r\n        const parts = baseOrbit.split(' '); const baseTheta = parseFloat(parts[0]);\r\n        const offset = Math.sin(time \/ 4000) * 35; mv.cameraOrbit = `${baseTheta + offset}deg ${parts[1]} ${parts[2]}`;\r\n    });\r\n    requestAnimationFrame(animateModelOrbits);\r\n};\r\ndocument.addEventListener('mousedown', handleInputStart, true);\r\ndocument.addEventListener('touchstart', handleInputStart, true);\r\ndocument.addEventListener('mouseup', handleInputEnd, true);\r\ndocument.addEventListener('touchend', handleInputEnd, true);\r\n\/\/ --- Calendar Logic ---\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n    if (window.lucide) { lucide.createIcons(); }\r\n    initHero();\r\n    initPhotoInteractions();\r\n    requestAnimationFrame(animateModelOrbits);\r\n    const SOURCE_URL = \"https:\/\/science.knu.ua\/new\/?p=5565\";\r\n    \/\/ Core Elements\r\n    const headerDisplayEl = document.getElementById('knu-header-display');\r\n    const prevBtn = document.getElementById('knu-prev-btn');\r\n    const nextBtn = document.getElementById('knu-next-btn');\r\n    const todayBtn = document.getElementById('knu-today-btn'); \r\n    const exportBtn = document.getElementById('knu-export-btn');\r\n    const clockTimeEl = document.getElementById('knu-live-time');\r\n    const clockDateEl = document.getElementById('knu-live-date');\r\n    const statusEl = document.getElementById('knu-fetch-status');\r\n    const zoomInputs = document.querySelectorAll('input[name=\"zoom\"]');\r\n    \/\/ Search Elements\r\n    const searchInput = document.getElementById('knu-search-input');\r\n    const searchClear = document.getElementById('knu-search-clear');\r\n    \/\/ Calendar View Elements\r\n    const gridEl = document.getElementById('knu-calendar-grid');\r\n    const daysHeaderEl = document.getElementById('knu-days-header');\r\n    const eventsListEl = document.getElementById('knu-events-list');\r\n    \/\/ Timeline View Elements\r\n    const timelineHoursEl = document.getElementById('knu-timeline-hours');\r\n    const timelineGridEl = document.getElementById('knu-timeline-grid');\r\n    const timelineTracksEl = document.getElementById('knu-timeline-tracks');\r\n    const timelineListEl = document.getElementById('knu-timeline-events-list');\r\n    \/\/ State\r\n    let eventsData =[];\r\n    let timeOffsetMs = 0;\r\n    let currentView = 'calendar'; \r\n    let currentZoom = 3; \r\n    let selectedDate = new Date(); \r\n    let activeDayFilter = null; \r\n    let activeEventFilter = null; \r\n    let activeSearchFilter = null;\r\n    let fuseInstance = null;\r\n    let searchTimeout = null;\r\n    \/\/ Sync & Time\r\n    async function syncInternetTime() {\r\n        try {\r\n            const res = await fetch('https:\/\/worldtimeapi.org\/api\/timezone\/Europe\/Kiev');\r\n            if(res.ok) { const data = await res.json(); timeOffsetMs = new Date(data.datetime).getTime() - new Date().getTime(); }\r\n        } catch(e) {}\r\n        updateClock(); setInterval(updateClock, 1000); \r\n    }\r\n    function getNow() { return new Date(new Date().getTime() + timeOffsetMs); }\r\n    selectedDate = getNow();\r\n    function updateClock() {\r\n        const now = getNow();\r\n        clockTimeEl.textContent = now.toLocaleTimeString('uk-UA', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\r\n        clockDateEl.textContent = now.toLocaleDateString('uk-UA', { day: 'numeric', month: 'long', year: 'numeric' }).replace(\/\\s*\u0440\\.\/g, '');\r\n    }\r\n    \/\/ --- Switcher Logic ---\r\n    function switchView(view) {\r\n        currentView = view;\r\n        activeDayFilter = null; \r\n        activeEventFilter = null;\r\n        document.getElementById('btn-view-cal').classList.toggle('active', view === 'calendar');\r\n        document.getElementById('btn-view-timeline').classList.toggle('active', view === 'timeline');\r\n        document.getElementById('btn-view-cards').classList.toggle('active', view === 'cards');\r\n        document.getElementById('view-calendar').classList.toggle('active', view === 'calendar');\r\n        document.getElementById('view-timeline').classList.toggle('active', view === 'timeline');\r\n        document.getElementById('view-cards').classList.toggle('active', view === 'cards');\r\n        const dayZoomInput = document.querySelector('input[name=\"zoom\"][value=\"1\"]');\r\n        const zoomSwitcher = document.querySelector('.knu-zoom-switcher');\r\n        if (view === 'cards') {\r\n            zoomSwitcher.style.opacity = '0.4';\r\n            zoomSwitcher.style.pointerEvents = 'none'; \/\/ \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u043a\u0430\u0440\u0442\u043e\u043a \u043c\u0430\u0441\u0448\u0442\u0430\u0431 \u0437\u0430\u0432\u0436\u0434\u0438 - \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u0434\u0435\u043d\u044c\r\n        } else {\r\n            zoomSwitcher.style.opacity = '1';\r\n            zoomSwitcher.style.pointerEvents = 'auto';\r\n            if (view === 'calendar') {\r\n                dayZoomInput.disabled = true;\r\n                if (currentZoom === 1) {\r\n                    currentZoom = 2; \/\/ \u041f\u0435\u0440\u0435\u0445\u0456\u0434 \u043d\u0430 \u0422\u0438\u0436\u0434\u0435\u043d\u044c\r\n                    document.querySelector('input[name=\"zoom\"][value=\"2\"]').checked = true;\r\n                }\r\n            } else {\r\n                dayZoomInput.disabled = false;\r\n            }\r\n        }\r\n        updateView();\r\n    }\r\n    document.getElementById('btn-view-cal').addEventListener('click', () => switchView('calendar'));\r\n    document.getElementById('btn-view-timeline').addEventListener('click', () => switchView('timeline'));\r\n    document.getElementById('btn-view-cards').addEventListener('click', () => switchView('cards'));\r\n    zoomInputs.forEach(input => {\r\n        input.addEventListener('change', (e) => {\r\n            activeDayFilter = null; activeEventFilter = null;\r\n            currentZoom = parseInt(e.target.value, 10); updateView();\r\n        });\r\n    });\r\n    \/\/ --- Fuzzy Search Logic ---\r\n    searchInput.addEventListener('input', (e) => {\r\n        const query = e.target.value.trim(); searchClear.style.display = query ? 'flex' : 'none';\r\n        clearTimeout(searchTimeout);\r\n        searchTimeout = setTimeout(() => {\r\n            if (query && fuseInstance) {\r\n                const results = fuseInstance.search(query); activeSearchFilter = new Set(results.map(r => r.item.id));\r\n            } else activeSearchFilter = null;\r\n            activeEventFilter = null; if (query) activeDayFilter = null; updateView();\r\n        }, 300);\r\n    });\r\n    searchClear.addEventListener('click', () => { searchInput.value = ''; searchClear.style.display = 'none'; activeSearchFilter = null; updateView(); });\r\n    \/\/ --- Export PNG Logic ---\r\n    exportBtn.addEventListener('click', async () => {\r\n        const originalHtml = exportBtn.innerHTML; exportBtn.innerHTML = `<i data-lucide=\"loader-2\" class=\"animate-spin\" size=\"16\"><\/i> \u0424\u043e\u0440\u043c\u0443\u0432\u0430\u043d\u043d\u044f...`;\r\n        if(window.lucide) lucide.createIcons();\r\n        const appEl = document.getElementById('knu-calendar-app');\r\n        try {\r\n            appEl.classList.add('export-mode'); await new Promise(r => setTimeout(r, 400));\r\n            const canvas = await html2canvas(appEl, { scale: 2, useCORS: true, backgroundColor: '#f9f9f9', logging: false, width: appEl.scrollWidth, height: appEl.scrollHeight, windowWidth: appEl.scrollWidth, windowHeight: appEl.scrollHeight, scrollY: -window.scrollY });\r\n            appEl.classList.remove('export-mode');\r\n            const link = document.createElement('a'); link.download = `\u041d\u0430\u0443\u043a\u043e\u0432\u0438\u0439_\u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440_\u041a\u041d\u0423_${new Date().toISOString().slice(0,10)}.png`; link.href = canvas.toDataURL('image\/png'); link.click();\r\n        } catch(e) { appEl.classList.remove('export-mode'); } finally { exportBtn.innerHTML = originalHtml; if(window.lucide) lucide.createIcons(); }\r\n    });\r\n    const monthsUK =['\u0421\u0456\u0447\u0435\u043d\u044c','\u041b\u044e\u0442\u0438\u0439','\u0411\u0435\u0440\u0435\u0437\u0435\u043d\u044c','\u041a\u0432\u0456\u0442\u0435\u043d\u044c','\u0422\u0440\u0430\u0432\u0435\u043d\u044c','\u0427\u0435\u0440\u0432\u0435\u043d\u044c','\u041b\u0438\u043f\u0435\u043d\u044c','\u0421\u0435\u0440\u043f\u0435\u043d\u044c','\u0412\u0435\u0440\u0435\u0441\u0435\u043d\u044c','\u0416\u043e\u0432\u0442\u0435\u043d\u044c','\u041b\u0438\u0441\u0442\u043e\u043f\u0430\u0434','\u0413\u0440\u0443\u0434\u0435\u043d\u044c'];\r\n    const monthsGenitive =['\u0441\u0456\u0447\u043d\u044f', '\u043b\u044e\u0442\u043e\u0433\u043e', '\u0431\u0435\u0440\u0435\u0437\u043d\u044f', '\u043a\u0432\u0456\u0442\u043d\u044f', '\u0442\u0440\u0430\u0432\u043d\u044f', '\u0447\u0435\u0440\u0432\u043d\u044f', '\u043b\u0438\u043f\u043d\u044f', '\u0441\u0435\u0440\u043f\u043d\u044f', '\u0432\u0435\u0440\u0435\u0441\u043d\u044f', '\u0436\u043e\u0432\u0442\u043d\u044f', '\u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434\u0430', '\u0433\u0440\u0443\u0434\u043d\u044f'];\r\n    function getPeriod(date, zoom) {\r\n        let start, end, title, headerStr;\r\n        if (zoom === 1) { \r\n            start = new Date(date.getFullYear(), date.getMonth(), date.getDate()); end = new Date(start.getTime() + 86400000 - 1);\r\n            headerStr = date.toLocaleDateString('uk-UA', { day: 'numeric', month: 'long', year: 'numeric' }).replace(\/\\s*\u0440\\.\/g, ''); title = `\u041f\u043e\u0434\u0456\u0457 \u043d\u0430 ${headerStr}`;\r\n        } else if (zoom === 2) { \r\n            const day = date.getDay(); const diff = date.getDate() - day + (day === 0 ? -6 : 1);\r\n            start = new Date(date.getFullYear(), date.getMonth(), diff); end = new Date(start.getTime() + 7 * 86400000 - 1);\r\n            headerStr = `${start.getDate()} - ${end.getDate()} ${monthsUK[end.getMonth()]} ${end.getFullYear()}`;\r\n            title = `\u041f\u043e\u0434\u0456\u0457 \u0437\u0430 \u0442\u0438\u0436\u0434\u0435\u043d\u044c (${start.toLocaleDateString('uk-UA', {day:'2-digit', month:'2-digit'})} - ${end.toLocaleDateString('uk-UA', {day:'2-digit', month:'2-digit'})})`;\r\n        } else if (zoom === 3) { \r\n            start = new Date(date.getFullYear(), date.getMonth(), 1); end = new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59);\r\n            headerStr = `${monthsUK[date.getMonth()]} ${date.getFullYear()}`; title = `\u041f\u043e\u0434\u0456\u0457 \u0437\u0430 ${monthsUK[date.getMonth()].toLowerCase()} ${date.getFullYear()}`;\r\n        } else if (zoom === 4) { \r\n            start = new Date(date.getFullYear(), 0, 1); end = new Date(date.getFullYear(), 11, 31, 23, 59, 59);\r\n            headerStr = `${date.getFullYear()} \u0420\u0456\u043a`; title = `\u041f\u043e\u0434\u0456\u0457 \u0437\u0430 ${date.getFullYear()} \u0440\u0456\u043a`;\r\n        }\r\n        return { start, end, title, headerStr };\r\n    }\r\n    function updateView() {\r\n        if (currentView === 'cards') {\r\n            \/\/ \u0412 \u0440\u0435\u0436\u0438\u043c\u0456 \u043a\u0430\u0440\u0442\u043e\u043a, \u044f\u043a\u0449\u043e \u043d\u0430 \u043f\u043e\u0442\u043e\u0447\u043d\u0438\u0439 \u0434\u0435\u043d\u044c \u043d\u0435\u043c\u0430\u0454 \u043f\u043e\u0434\u0456\u0439, \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u0442\u0440\u0438\u0431\u0430\u0454\u043c\u043e \u0434\u043e \u043d\u0430\u0439\u0431\u043b\u0438\u0436\u0447\u043e\u0433\u043e \u0434\u043d\u044f \u0437 \u043f\u043e\u0434\u0456\u044f\u043c\u0438\r\n            let currentDayEvents = getEventsForPeriod(new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate(), 0, 0, 0).getTime(), new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate(), 23, 59, 59).getTime());\r\n            if (eventsData.length > 0 && currentDayEvents.length === 0) {\r\n                const nextDay = findNextDayWithEvents(selectedDate, 1) || findNextDayWithEvents(selectedDate, -1);\r\n                if (nextDay) selectedDate = nextDay;\r\n            }\r\n            const period = getPeriod(selectedDate, 1); \/\/ \u0414\u043b\u044f \u043a\u0430\u0440\u0442\u043e\u043a \u0437\u0430\u0432\u0436\u0434\u0438 \u0434\u0435\u043d\u044c\r\n            headerDisplayEl.textContent = period.headerStr;\r\n            renderCardsView(period);\r\n        } else {\r\n            const period = getPeriod(selectedDate, currentZoom);\r\n            headerDisplayEl.textContent = period.headerStr;\r\n            if (currentView === 'calendar') {\r\n                renderCalendar(period); renderEventsPanel(period, eventsListEl);\r\n            } else if (currentView === 'timeline') {\r\n                renderTimeline(period); renderEventsPanel(period, timelineListEl);\r\n            }\r\n        }\r\n    }\r\n    \/\/ --- Data Fetching & Parsing ---\r\n    async function fetchWithFallback(targetUrl) {\r\n        const t = new Date().getTime(); const urlWithTime = `${targetUrl}?_t=${t}`;\r\n        try { const response = await fetch(urlWithTime, { cache: 'no-cache' }); if (response.ok) return await response.text(); } catch (e) {}\r\n        try { const proxyUrl = `https:\/\/api.allorigins.win\/get?url=${encodeURIComponent(urlWithTime)}`; const response = await fetch(proxyUrl, { cache: \"no-store\" }); if (response.ok) { const data = await response.json(); if (data.contents) return data.contents; } } catch (e) {}\r\n        throw new Error(\"Unable to fetch\");\r\n    }\r\n    function cleanValue(val) {\r\n        if(!val) return \"\"; val = val.trim(); const lower = val.toLowerCase();\r\n        if(lower === \"-\" || lower === \"\u0432\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439\" || lower === \"\u0432\u0456\u0434\u0441\u0443\u0442\u043d\u0454\" || lower === \"\u043d\u0435\u043c\u0430\u0454\") return \"\"; return val;\r\n    }\r\n    function parseDateString(dateStr) {\r\n        if (!dateStr) return null; const match = dateStr.match(\/(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})(?:\\s+(\\d{1,2}):(\\d{1,2}))?\/);\r\n        if(match) return new Date(parseInt(match[3], 10), parseInt(match[2], 10) - 1, parseInt(match[1], 10), match[4] ? parseInt(match[4], 10) : 0, match[5] ? parseInt(match[5], 10) : 0);\r\n        return null;\r\n    }\r\n    function formatEventTimeDisplay(startRaw, endRaw) {\r\n        if (!startRaw) return \"\";\r\n        const parseToParts = (rawStr) => {\r\n            if (!rawStr) return null; const match = rawStr.match(\/(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})(?:\\s+(\\d{1,2}):(\\d{1,2}))?\/); if (!match) return null;\r\n            return { day: parseInt(match[1], 10), month: monthsGenitive[parseInt(match[2], 10) - 1], year: parseInt(match[3], 10), time: !!match[4] ? `${match[4].padStart(2, '0')}:${match[5].padStart(2, '0')}` : null, rawStr };\r\n        };\r\n        const start = parseToParts(startRaw), end = parseToParts(endRaw);\r\n        if (!start) return endRaw ? `${startRaw} &mdash; ${endRaw}` : startRaw; \r\n        const startStr = `${start.day} ${start.month} ${start.year}`;\r\n        if (!end) return start.time ? `${startStr} \u0432\u0456\u0434 ${start.time}` : startStr;\r\n        const endStr = `${end.day} ${end.month} ${end.year}`;\r\n        if (startStr === endStr) {\r\n            if (start.time && end.time) return `${startStr} \u0432\u0456\u0434 ${start.time} \u0434\u043e ${end.time}`;\r\n            else if (start.time && !end.time) return `${startStr} \u0432\u0456\u0434 ${start.time}`;\r\n            else return !start.time && end.time ? `${startStr} \u0434\u043e ${end.time}` : startStr;\r\n        } else {\r\n            let p1 = startStr; if (start.time) p1 += ` \u0432\u0456\u0434 ${start.time}`; let p2 = endStr; if (end.time) p2 += ` \u0434\u043e ${end.time}`; return `${p1} &mdash; ${p2}`;\r\n        }\r\n    }\r\n    function parseCalendarData(htmlContent) {\r\n        const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, \"text\/html\"); doc.querySelectorAll('script, style, header, footer, nav').forEach(n => n.remove());\r\n        let rawText = doc.body.innerText || \"\"; rawText = rawText.replace(\/\\u00A0\/g, ' ');\r\n        const parsedEvents =[]; const regex = \/1a\\.[^:]*:\\s*([\\s\\S]*?)2a\\.[^:]*:\\s*([\\s\\S]*?)3a\\.[^:]*:\\s*([\\s\\S]*?)4a\\.[^:]*:\\s*([\\s\\S]*?)5a\\.[^:]*:\\s*([\\s\\S]*?)6a\\.[^:]*:\\s*([\\s\\S]*?)7a\\.[^:]*:\\s*([\\s\\S]*?)8a\\.[^:]*:\\s*([\\s\\S]*?)9a\\.[^:]*:\\s*([\\s\\S]*?)10a\\.[^:]*:\\s*([\\s\\S]*?)(?=\\s*(?:-\\\/-\\\/-|1a\\.|=\\\/=|$))\/gi;\r\n        let match;\r\n        while ((match = regex.exec(rawText)) !== null) {\r\n            const title = cleanValue(match[1]); const startRaw = cleanValue(match[3]); if(!title || !startRaw) continue; \r\n            const startDate = parseDateString(startRaw); const endDate = parseDateString(cleanValue(match[4])); let merged = false;\r\n            for (let i = 0; i < parsedEvents.length; i++) {\r\n                let e = parsedEvents[i];\r\n                if (e.title === title && startDate && e.startDate) {\r\n                    const nS = startDate.getTime(), nE = endDate ? endDate.getTime() : nS; const eS = e.startDate.getTime(), eE = e.endDate ? e.endDate.getTime() : eS;\r\n                    const overlap = (nS <= eE && nE >= eS); const gap1 = nS - eE, gap2 = eS - nE;\r\n                    if (overlap || (gap1 > 0 && gap1 <= 432000000) || (gap2 > 0 && gap2 <= 432000000)) {\r\n                        if (nS < eS) { e.startDate = startDate; e.startRaw = startRaw; }\r\n                        if (nE > eE) { e.endDate = endDate || startDate; e.endRaw = cleanValue(match[4]) || startRaw; }\r\n                        const newDesc = cleanValue(match[2]); if (newDesc.length > e.description.length) e.description = newDesc;\r\n                        if (!e.zoomLink) e.zoomLink = cleanValue(match[5]); if (!e.newsLink) e.newsLink = cleanValue(match[6]); if (!e.extLink) e.extLink = cleanValue(match[7]);\r\n                        if (!e.mediaLink) e.mediaLink = cleanValue(match[8]); if (!e.fileLink) e.fileLink = cleanValue(match[9]); if (!e.regLink) e.regLink = cleanValue(match[10]);\r\n                        merged = true; break;\r\n                    }\r\n                }\r\n            }\r\n            if (!merged) {\r\n                parsedEvents.push({\r\n                    id: 'ev-' + Math.random().toString(36).substr(2, 9), title: title, description: cleanValue(match[2]),\r\n                    startRaw: startRaw, endRaw: cleanValue(match[4]), startDate: startDate, endDate: endDate,\r\n                    zoomLink: cleanValue(match[5]), newsLink: cleanValue(match[6]), extLink: cleanValue(match[7]), mediaLink: cleanValue(match[8]),\r\n                    fileLink: cleanValue(match[9]), regLink: cleanValue(match[10])\r\n                });\r\n            }\r\n        }\r\n        return parsedEvents;\r\n    }\r\n    async function loadEvents() {\r\n        try {\r\n            statusEl.innerHTML = `<i data-lucide=\"loader-2\" class=\"animate-spin\" size=\"14\"><\/i> \u041e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u043e\u0434\u0456\u0439...`;\r\n            if (window.lucide) lucide.createIcons();\r\n            const htmlContent = await fetchWithFallback(SOURCE_URL); eventsData = parseCalendarData(htmlContent);\r\n            if (window.Fuse) fuseInstance = new Fuse(eventsData, { keys:['title', 'description', 'startRaw', 'endRaw', 'zoomLink', 'newsLink', 'extLink', 'mediaLink', 'fileLink', 'regLink'], threshold: 0.3, ignoreLocation: true, minMatchCharLength: 2 });\r\n            statusEl.innerHTML = `<i data-lucide=\"check-circle\" size=\"14\" style=\"color:var(--knu-green)\"><\/i> \u0414\u0430\u043d\u0456 \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043e (${eventsData.length} \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u043e\u0434\u0456\u0439)`;\r\n            updateView();\r\n        } catch (error) { statusEl.innerHTML = `<i data-lucide=\"alert-triangle\" size=\"14\" style=\"color:var(--knu-red)\"><\/i> \u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u043d\u043e\u0432\u0438\u0442\u0438 \u0434\u0430\u043d\u0456`; }\r\n        if (window.lucide) lucide.createIcons();\r\n    }\r\n    function isSameDay(d1, d2) { return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); }\r\n    function getEventsForPeriod(startMs, endMs) {\r\n        return eventsData.filter(e => {\r\n            if (activeSearchFilter && !activeSearchFilter.has(e.id)) return false;\r\n            if (!e.startDate) return false; \r\n            const eStart = e.startDate.getTime(); const eEnd = e.endDate ? e.endDate.getTime() : eStart + 3600000; \r\n            return (eStart <= endMs && eEnd >= startMs);\r\n        });\r\n    }\r\n    function renderDotIndicators(cellElement, events) {\r\n        if (events.length > 0) {\r\n            const dots = document.createElement('div'); dots.className = 'knu-event-dots';\r\n            for(let k = 0; k < Math.min(events.length, 3); k++) {\r\n                const e = events[k]; const d = document.createElement('div'); d.className = 'knu-dot';\r\n                if (e.startDate && e.endDate && new Date(e.endDate).setHours(0,0,0,0) > new Date(e.startDate).setHours(0,0,0,0)) { d.classList.add('multi-day-dot'); d.title = \"\u0411\u0430\u0433\u0430\u0442\u043e\u0434\u0435\u043d\u043d\u0430 \u043f\u043e\u0434\u0456\u044f\"; }\r\n                dots.appendChild(d);\r\n            }\r\n            if (events.length > 3) { const extra = document.createElement('div'); extra.style.cssText = \"font-size:10px; font-weight:bold; color:#888; line-height:1;\"; extra.textContent = \"+\"; dots.appendChild(extra); }\r\n            cellElement.appendChild(dots);\r\n        }\r\n    }\r\n    \/\/ --- Card Generator Helper ---\r\n    function generateEventCardHtml(e, forceExpand, viewPrefix) {\r\n        const createLinkTag = (url, iconName, label, extraClass=\"\") => {\r\n            if(!url) return \"\"; let finalUrl = url.startsWith('http') ? url : `https:\/\/${url}`;\r\n            return `<a href=\"${finalUrl}\" target=\"_blank\" class=\"knu-tag-link ${extraClass}\"><i data-lucide=\"${iconName}\" size=\"14\"><\/i> ${label}<\/a>`;\r\n        };\r\n        let headerLinksHtml = createLinkTag(e.regLink, 'user-check', '\u0420\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044f', 'important');\r\n        if (e.zoomLink) {\r\n            const urlMatch = e.zoomLink.match(\/(https?:\\\/\\\/[^\\s()<>]+|[a-zA-Z0-9.-]+\\.(?:com|org|net|edu|gov|uk|ua|us|io|me|info)[^\\s()<>]*)\/i);\r\n            const clickIconHtml = `<i data-lucide=\"mouse-pointer-click\" size=\"14\" style=\"opacity: 0.7; margin-left: 2px;\"><\/i>`;\r\n            if (urlMatch) {\r\n                let url = urlMatch[0]; let finalUrl = url.startsWith('http') ? url : `https:\/\/${url}`;\r\n                let remainingText = e.zoomLink.replace(url, '').trim();\r\n                if (remainingText.startsWith('(') && remainingText.endsWith(')')) remainingText = remainingText.slice(1, -1).trim();\r\n                remainingText = remainingText.replace(\/^[\\s,;-]+\/, '').trim();\r\n                if (remainingText.length > 0) headerLinksHtml += `<a href=\"${finalUrl}\" target=\"_blank\" class=\"knu-tag-link\"><i data-lucide=\"map-pin\" size=\"14\"><\/i> <i data-lucide=\"video\" size=\"14\"><\/i> ${remainingText} ${clickIconHtml}<\/a>`;\r\n                else headerLinksHtml += `<a href=\"${finalUrl}\" target=\"_blank\" class=\"knu-tag-link\"><i data-lucide=\"video\" size=\"14\"><\/i> \u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043e\u043d\u043b\u0430\u0439\u043d<\/a>`;\r\n            } else {\r\n                headerLinksHtml += `<span class=\"knu-tag-link\" style=\"cursor:pointer;\" title=\"\u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0456\u0448\u0435\"><i data-lucide=\"map-pin\" size=\"14\"><\/i> ${e.zoomLink}<\/span>`;\r\n            }\r\n        }\r\n        let bodyLinksHtml = createLinkTag(e.newsLink, 'newspaper', '\u0414\u0435\u0442\u0430\u043b\u044c\u043d\u043e') + createLinkTag(e.extLink, 'external-link', '\u0421\u0442\u043e\u0440\u0456\u043d\u043a\u0430 \u0437\u0430\u0445\u043e\u0434\u0443');\r\n        let mediaCoverHtml = '';\r\n        if (e.mediaLink) {\r\n            const ytMatch = e.mediaLink.match(\/(?:youtube\\.com\\\/(?:[^\\\/]+\\\/.+\\\/|(?:v|e(?:mbed)?)\\\/|.*[?&]v=)|youtu\\.be\\\/)([^\"&?\\\/\\s]{11})\/i);\r\n            if (ytMatch && ytMatch[1]) mediaCoverHtml = `<div class=\"knu-event-cover\"><iframe src=\"https:\/\/www.youtube.com\/embed\/${ytMatch[1]}\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe><\/div>`;\r\n            else if (e.mediaLink.match(\/\\.(jpeg|jpg|gif|png|webp|bmp)(\\?.*)?$\/i)) mediaCoverHtml = `<div class=\"knu-event-cover\"><img decoding=\"async\" src=\"${e.mediaLink}\" alt=\"\u041e\u0431\u043a\u043b\u0430\u0434\u0438\u043d\u043a\u0430 \u043f\u043e\u0434\u0456\u0457\" loading=\"lazy\"><\/div>`;\r\n            else bodyLinksHtml += createLinkTag(e.mediaLink, 'youtube', '\u041c\u0435\u0434\u0456\u0430 \/ \u0412\u0456\u0434\u0435\u043e');\r\n        }\r\n        bodyLinksHtml += createLinkTag(e.fileLink, 'file-text', '\u0424\u0430\u0439\u043b');\r\n        const hasBody = e.description || bodyLinksHtml || mediaCoverHtml;\r\n        return `\r\n            <div class=\"knu-event-card ${forceExpand ? 'expanded' : ''} ${!hasBody ? 'no-body' : ''}\" id=\"${viewPrefix}${e.id}\">\r\n                <div class=\"knu-event-card-header\" ${hasBody && !forceExpand ? `onclick=\"this.parentElement.classList.toggle('expanded')\"` : ''}>\r\n                    <div class=\"knu-event-title\">${e.title}<\/div>\r\n                    <div class=\"knu-event-time\"><i data-lucide=\"clock\" size=\"14\"><\/i> ${formatEventTimeDisplay(e.startRaw, e.endRaw)}<\/div>\r\n                    ${headerLinksHtml ? `<div class=\"knu-event-links\" style=\"margin-top:10px;\" onclick=\"event.stopPropagation()\">${headerLinksHtml}<\/div>` : ''}\r\n                <\/div>\r\n                ${hasBody ? `\r\n                <div class=\"knu-event-card-body\">\r\n                    ${mediaCoverHtml}\r\n                    ${e.description ? `<div class=\"knu-event-desc\">${e.description}<\/div>` : ''}\r\n                    ${bodyLinksHtml ? `<div class=\"knu-event-links\">${bodyLinksHtml}<\/div>` : ''}\r\n                <\/div>\r\n                ` : ''}\r\n            <\/div>\r\n        `;\r\n    }\r\n    \/\/ --- Calendar UI Logic ---\r\n    function renderCalendar(period) {\r\n        gridEl.innerHTML = ''; const now = getNow();\r\n        if (currentZoom === 4) { \r\n            daysHeaderEl.style.display = 'none'; gridEl.style.gridTemplateColumns = 'repeat(auto-fill, minmax(130px, 1fr))';\r\n            for (let m = 0; m < 12; m++) {\r\n                const cellDate = new Date(selectedDate.getFullYear(), m, 1); const cell = document.createElement('div'); cell.className = 'knu-day-cell';\r\n                if (m === selectedDate.getMonth()) cell.classList.add('selected');\r\n                cell.innerHTML = `<span style=\"font-size: 1.1em; font-weight:700;\">${monthsUK[m]}<\/span>`;\r\n                const evts = getEventsForPeriod(cellDate.getTime(), new Date(selectedDate.getFullYear(), m + 1, 0, 23, 59, 59).getTime());\r\n                renderDotIndicators(cell, evts); cell.title = evts.length > 0 ? `\u041f\u043e\u0434\u0456\u0457 \u0437\u0430 ${monthsUK[m].toLowerCase()}:\\n` + evts.map(e => \"\u2022 \" + e.title).join(\"\\n\") : `\u041d\u0435\u043c\u0430\u0454 \u043f\u043e\u0434\u0456\u0439 \u0437\u0430 ${monthsUK[m].toLowerCase()}`;\r\n                cell.addEventListener('click', () => { selectedDate.setMonth(m); activeDayFilter = null; activeEventFilter = null; currentZoom = 3; document.querySelector('input[name=\"zoom\"][value=\"3\"]').checked = true; updateView(); });\r\n                gridEl.appendChild(cell);\r\n            }\r\n        } else if (currentZoom === 3 || currentZoom === 2) { \r\n            daysHeaderEl.style.display = 'grid'; gridEl.style.gridTemplateColumns = 'repeat(7, 1fr)';\r\n            let startDay, loopStart, loopEnd;\r\n            if (currentZoom === 3) { startDay = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1).getDay() - 1; if (startDay === -1) startDay = 6; loopStart = 1; loopEnd = new Date(selectedDate.getFullYear(), selectedDate.getMonth() + 1, 0).getDate(); } else { startDay = 0; loopStart = 0; loopEnd = 6; }\r\n            for (let i = 0; i < startDay; i++) { const emptyCell = document.createElement('div'); emptyCell.className = 'knu-day-cell empty'; gridEl.appendChild(emptyCell); }\r\n            for (let i = loopStart; i <= loopEnd; i++) {\r\n                const cellDate = currentZoom === 3 ? new Date(selectedDate.getFullYear(), selectedDate.getMonth(), i) : new Date(period.start.getTime() + i * 86400000);\r\n                const cell = document.createElement('div'); cell.className = 'knu-day-cell'; cell.textContent = cellDate.getDate();\r\n                if (isSameDay(cellDate, now)) cell.classList.add('today');\r\n                if ((activeDayFilter && isSameDay(cellDate, activeDayFilter)) || (!activeDayFilter && isSameDay(cellDate, selectedDate))) cell.classList.add('selected');\r\n                const evts = getEventsForPeriod(cellDate.getTime(), cellDate.getTime() + 86399999);\r\n                renderDotIndicators(cell, evts); cell.title = evts.length > 0 ? `\u041f\u043e\u0434\u0456\u0457 \u043d\u0430 ${cellDate.toLocaleDateString('uk-UA')}:\\n` + evts.map(e => \"\u2022 \" + e.title).join(\"\\n\") : `\u041d\u0435\u043c\u0430\u0454 \u043f\u043e\u0434\u0456\u0439 \u043d\u0430 ${cellDate.toLocaleDateString('uk-UA')}`;\r\n                cell.addEventListener('click', () => { selectedDate = cellDate; activeDayFilter = new Date(cellDate); activeEventFilter = null; updateView(); });\r\n                gridEl.appendChild(cell);\r\n            }\r\n        }\r\n    }\r\n    \/\/ --- Timeline UI Logic ---\r\n    function renderTimeline(period) {\r\n        let colCount, getColLabel;\r\n        if (currentZoom === 1) { colCount = 24; getColLabel = (i) => `${i.toString().padStart(2, '0')}:00`; } \r\n        else if (currentZoom === 2) { colCount = 7; const daysShort =['\u041f\u043d','\u0412\u0432','\u0421\u0440','\u0427\u0442','\u041f\u0442','\u0421\u0431','\u041d\u0434']; getColLabel = (i) => { let d = new Date(period.start.getTime() + i*86400000); return `${daysShort[i]} ${d.getDate()}`; }; } \r\n        else if (currentZoom === 3) { colCount = period.end.getDate(); getColLabel = (i) => `${i + 1}`; } \r\n        else if (currentZoom === 4) { colCount = 12; getColLabel = (i) => monthsUK[i].substring(0,3); }\r\n        const innerContainer = document.getElementById('knu-timeline-inner');\r\n        let minW = 800; if (currentZoom === 1) minW = 1440; else if (currentZoom === 3) minW = Math.max(800, colCount * 45);\r\n        innerContainer.style.minWidth = `${minW}px`; timelineHoursEl.innerHTML = ''; timelineGridEl.innerHTML = '';\r\n        const hoursFragment = document.createDocumentFragment(); const gridFragment = document.createDocumentFragment();\r\n        for(let i=0; i<colCount; i++) {\r\n            const colDiv = document.createElement('div'); colDiv.className = 'knu-timeline-hour'; colDiv.innerHTML = `<span>${getColLabel(i)}<\/span>`; hoursFragment.appendChild(colDiv);\r\n            const gridLine = document.createElement('div'); gridLine.className = 'knu-timeline-grid-line'; gridFragment.appendChild(gridLine);\r\n        }\r\n        timelineHoursEl.appendChild(hoursFragment); timelineGridEl.appendChild(gridFragment);\r\n        const now = getNow();\r\n        if (now.getTime() >= period.start.getTime() && now.getTime() <= period.end.getTime()) {\r\n            const totalMins = (period.end.getTime() - period.start.getTime()) \/ 60000; const currentMins = (now.getTime() - period.start.getTime()) \/ 60000;\r\n            const timeLine = document.createElement('div'); timeLine.className = 'knu-timeline-current-time';\r\n            timeLine.style.left = `${(currentMins \/ totalMins) * 100}%`; timelineGridEl.appendChild(timeLine);\r\n        }\r\n        timelineTracksEl.innerHTML = ''; const periodEvents = getEventsForPeriod(period.start.getTime(), period.end.getTime());\r\n        if (periodEvents.length === 0) {\r\n            let emptyMsg = activeSearchFilter ? \"\u041f\u043e\u0434\u0456\u0439, \u0449\u043e \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u044e\u0442\u044c \u043f\u043e\u0448\u0443\u043a\u0443 \u0432 \u0446\u044c\u043e\u043c\u0443 \u043f\u0435\u0440\u0456\u043e\u0434\u0456, \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.\" : \"\u041d\u0435\u043c\u0430\u0454 \u043f\u043e\u0434\u0456\u0439 \u0443 \u0432\u0438\u0431\u0440\u0430\u043d\u043e\u043c\u0443 \u043f\u0435\u0440\u0456\u043e\u0434\u0456.\";\r\n            timelineTracksEl.innerHTML = `<div style=\"text-align:center; padding: 30px; color:#94a3b8; font-weight:600;\">${emptyMsg}<\/div>`; return;\r\n        }\r\n        const scrollContainer = document.getElementById('knu-timeline-scroll'); const containerWidth = Math.max(minW, scrollContainer.clientWidth || minW);\r\n        const totalMins = Math.max((period.end.getTime() - period.start.getTime()) \/ 60000, 1);\r\n        const placedEvents = periodEvents.map(e => {\r\n            const evStartMs = e.startDate.getTime(); const evEndMs = e.endDate ? e.endDate.getTime() : evStartMs + 3600000; \r\n            const actualStartMs = Math.max(evStartMs, period.start.getTime()); const actualEndMs = Math.min(evEndMs, period.end.getTime());\r\n            let startMins = (actualStartMs - period.start.getTime()) \/ 60000; let endMins = (actualEndMs - period.start.getTime()) \/ 60000;\r\n            if (endMins - startMins < totalMins * 0.005) endMins = startMins + totalMins * 0.005; \r\n            const textWidthMins = (((e.title.length * 7.5) + 30) \/ containerWidth) * totalMins;\r\n            let textStartMins = startMins, textEndMins = textStartMins + textWidthMins;\r\n            if (textEndMins > totalMins) { textStartMins -= (textEndMins - totalMins); textEndMins -= (textEndMins - totalMins); if (textStartMins < 0) { textStartMins = 0; textEndMins = textWidthMins; } }\r\n            return {\r\n                event: e, startMins, endMins, requiredStartMins: Math.min(startMins, textStartMins), requiredEndMins: Math.max(endMins, textEndMins),\r\n                textLeftPx: -((startMins - textStartMins) \/ totalMins) * containerWidth, isMultiDay: (evStartMs < period.start.getTime()) || (evEndMs > period.end.getTime()) || ((evEndMs - evStartMs) > 86400000)\r\n            };\r\n        });\r\n        placedEvents.sort((a, b) => a.requiredStartMins - b.requiredStartMins);\r\n        const tracks =[]; const tracksFragment = document.createDocumentFragment();\r\n        placedEvents.forEach(pe => {\r\n            let assignedTrack = -1; for (let i = 0; i < tracks.length; i++) if (tracks[i] <= pe.requiredStartMins - (totalMins * 0.01)) { assignedTrack = i; break; }\r\n            if (assignedTrack === -1) { assignedTrack = tracks.length; tracks.push(0); } tracks[assignedTrack] = pe.requiredEndMins; \r\n            const el = document.createElement('div'); el.className = 'knu-timeline-event'; if(pe.isMultiDay) el.classList.add('multi-day');\r\n            if (activeEventFilter && pe.event.id === activeEventFilter.id) el.classList.add('active-track');\r\n            el.style.left = `${(pe.startMins \/ totalMins) * 100}%`; el.style.width = `${Math.max(((pe.endMins - pe.startMins) \/ totalMins) * 100, 0.5)}%`; el.style.top = `${assignedTrack * 48}px`; \r\n            el.title = `${pe.event.title}\\n\u23f0 ${formatEventTimeDisplay(pe.event.startRaw, pe.event.endRaw)}`;\r\n            el.innerHTML = `<div class=\"knu-timeline-event-text\" style=\"left: ${pe.textLeftPx}px;\">${pe.event.title}<\/div><div class=\"knu-timeline-event-bar\"><\/div>`;\r\n            el.addEventListener('click', () => { activeEventFilter = (activeEventFilter && activeEventFilter.id === pe.event.id) ? null : pe.event; updateView(); });\r\n            tracksFragment.appendChild(el);\r\n        });\r\n        timelineTracksEl.appendChild(tracksFragment); timelineTracksEl.style.minHeight = `${tracks.length * 48 + 10}px`; \r\n        if (currentZoom === 1) scrollContainer.scrollLeft = (8 \/ 24) * scrollContainer.scrollWidth;\r\n        else if (scrollContainer.scrollLeft === 0) scrollContainer.scrollLeft = 0;\r\n    }\r\n    \/\/ --- Details Panel Accordion Logic ---\r\n    function renderEventsPanel(period, containerEl) {\r\n        let panelStart = period.start.getTime(), panelEnd = period.end.getTime(), panelTitle = period.title, isDayFiltered = false;\r\n        if (activeEventFilter) { panelTitle = \"\u041e\u0431\u0440\u0430\u043d\u0430 \u043f\u043e\u0434\u0456\u044f\"; } \r\n        else if (currentView === 'calendar' && activeDayFilter) {\r\n            panelStart = new Date(activeDayFilter.getFullYear(), activeDayFilter.getMonth(), activeDayFilter.getDate(), 0, 0, 0).getTime();\r\n            panelEnd = new Date(activeDayFilter.getFullYear(), activeDayFilter.getMonth(), activeDayFilter.getDate(), 23, 59, 59).getTime();\r\n            panelTitle = `\u041f\u043e\u0434\u0456\u0457 \u043d\u0430 ${activeDayFilter.toLocaleDateString('uk-UA', { day: 'numeric', month: 'long', year: 'numeric' }).replace(\/\\s*\u0440\\.\/g, '')}`; isDayFiltered = true;\r\n        } else if (currentZoom === 1) { isDayFiltered = true; } else if (activeSearchFilter && !isDayFiltered) { panelTitle = `\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0438 \u043f\u043e\u0448\u0443\u043a\u0443`; }\r\n        const titleSpan = (containerEl === eventsListEl) ? document.getElementById('knu-selected-date-title') : document.getElementById('knu-timeline-date-title');\r\n        const titleContainer = titleSpan.parentElement; const existingClose = titleContainer.querySelector('.knu-close-filter');\r\n        if (existingClose) existingClose.remove(); titleSpan.textContent = panelTitle;\r\n        if (activeEventFilter || (currentView === 'calendar' && activeDayFilter) || (activeSearchFilter && !isDayFiltered)) {\r\n            const closeBtn = document.createElement('button'); closeBtn.className = 'knu-close-filter'; closeBtn.title = '\u0421\u043a\u0438\u043d\u0443\u0442\u0438 \u0432\u0438\u0431\u0456\u0440 \/ \u041e\u0447\u0438\u0441\u0442\u0438\u0442\u0438 \u043f\u043e\u0448\u0443\u043a'; closeBtn.innerHTML = '<i data-lucide=\"x\" size=\"20\"><\/i>';\r\n            closeBtn.onclick = (e) => { \r\n                e.stopPropagation(); \r\n                if (activeEventFilter) activeEventFilter = null; \r\n                else if (currentView === 'calendar' && activeDayFilter) activeDayFilter = null;\r\n                else if (activeSearchFilter) { searchInput.value = ''; searchClear.style.display = 'none'; activeSearchFilter = null; }\r\n                updateView(); \r\n            };\r\n            titleContainer.appendChild(closeBtn);\r\n        }\r\n        let periodEvents;\r\n        if (activeEventFilter) periodEvents =[activeEventFilter];\r\n        else if (activeSearchFilter && !isDayFiltered) {\r\n            periodEvents = eventsData.filter(e => activeSearchFilter.has(e.id)).sort((a, b) => (a.startDate || 0) - (b.startDate || 0));\r\n            if(periodEvents.length > 0) titleSpan.textContent += ` (${periodEvents.length})`;\r\n        } else periodEvents = getEventsForPeriod(panelStart, panelEnd).sort((a, b) => (a.startDate || 0) - (b.startDate || 0));\r\n        if (periodEvents.length === 0) {\r\n            containerEl.innerHTML = `<div class=\"knu-no-events\"><i data-lucide=\"calendar-x-2\" size=\"48\" style=\"color:#cbd5e1; margin-bottom:10px;\"><\/i> \u041f\u043e\u0434\u0456\u0439 \u0437\u0430 \u0432\u0438\u0431\u0440\u0430\u043d\u0438\u0439 \u043f\u0435\u0440\u0456\u043e\u0434 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.<\/div>`;\r\n            if (window.lucide) lucide.createIcons(); return;\r\n        }\r\n        let html = '';\r\n        const forceExpand = (activeEventFilter != null) || (isDayFiltered && periodEvents.length === 1);\r\n        const viewPrefix = (containerEl === eventsListEl) ? 'cal-' : 'tl-';\r\n        periodEvents.forEach(e => { html += generateEventCardHtml(e, forceExpand, viewPrefix); });\r\n        containerEl.innerHTML = html; if (window.lucide) lucide.createIcons();\r\n    }\r\n    \/\/ --- View 3: Cards Logic ---\r\n    function renderCardsView(period) {\r\n        const containerEl = document.getElementById('knu-cards-grid');\r\n        const periodEvents = getEventsForPeriod(period.start.getTime(), period.end.getTime()).sort((a, b) => (a.startDate || 0) - (b.startDate || 0));\r\n        if (periodEvents.length === 0) {\r\n            containerEl.innerHTML = `<div class=\"knu-no-events\" style=\"grid-column: 1 \/ -1; padding: 60px 20px;\"><i data-lucide=\"calendar-x-2\" size=\"48\" style=\"color:#cbd5e1; margin-bottom:10px;\"><\/i> \u041d\u0435\u043c\u0430\u0454 \u043f\u043e\u0434\u0456\u0439 \u043d\u0430 \u0446\u044e \u0434\u0430\u0442\u0443. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043f\u0435\u0440\u0456\u043e\u0434.<\/div>`;\r\n            if (window.lucide) lucide.createIcons(); return;\r\n        }\r\n        let html = '';\r\n        periodEvents.forEach(e => { html += generateEventCardHtml(e, true, 'card-'); });\r\n        containerEl.innerHTML = html; if (window.lucide) lucide.createIcons();\r\n    }\r\n    \/\/ \u041f\u043e\u0448\u0443\u043a \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0434\u043d\u044f, \u0449\u043e \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u043f\u043e\u0434\u0456\u0457 (\u0434\u043b\u044f \u0437\u0440\u0443\u0447\u043d\u043e\u0433\u043e \u043a\u043b\u0456\u043a\u0435\u0440\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \"\u041a\u0430\u0440\u0442\u043a\u0438\")\r\n    function findNextDayWithEvents(baseDate, dir) {\r\n        let testDate = new Date(baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate());\r\n        for (let i = 1; i <= 730; i++) { \r\n            testDate.setDate(testDate.getDate() + dir);\r\n            const startMs = new Date(testDate.getFullYear(), testDate.getMonth(), testDate.getDate(), 0, 0, 0).getTime();\r\n            const endMs = new Date(testDate.getFullYear(), testDate.getMonth(), testDate.getDate(), 23, 59, 59).getTime();\r\n            const evts = getEventsForPeriod(startMs, endMs);\r\n            if (evts.length > 0) return new Date(testDate);\r\n        }\r\n        return null;\r\n    }\r\n    \/\/ --- Control Listeners ---\r\n    function shiftDate(dir) {\r\n        activeDayFilter = null; activeEventFilter = null;\r\n        if (currentView === 'cards') {\r\n            const nextDay = findNextDayWithEvents(selectedDate, dir);\r\n            if (nextDay) selectedDate = nextDay;\r\n        } else {\r\n            if (currentZoom === 1) selectedDate.setDate(selectedDate.getDate() + dir); \r\n            else if (currentZoom === 2) selectedDate.setDate(selectedDate.getDate() + dir * 7); \r\n            else if (currentZoom === 3) selectedDate.setMonth(selectedDate.getMonth() + dir); \r\n            else if (currentZoom === 4) selectedDate.setFullYear(selectedDate.getFullYear() + dir);\r\n        }\r\n        updateView();\r\n    }\r\n    prevBtn.addEventListener('click', () => shiftDate(-1));\r\n    nextBtn.addEventListener('click', () => shiftDate(1));\r\n    todayBtn.addEventListener('click', () => { selectedDate = getNow(); activeDayFilter = null; activeEventFilter = null; updateView(); });\r\n    \/\/ Boot\r\n    (async () => { await syncInternetTime(); selectedDate = getNow(); switchView('calendar'); await loadEvents(); })();\r\n});\r\n<\/script>\r\n<\/body>\r\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>\u0421\u0422\u041e\u0420\u0406\u041d\u041a\u0410 \u0423 \u041f\u0420\u041e\u0426\u0415\u0421\u0406 \u0420\u041e\u0417\u0420\u041e\u0411\u041a\u0418<\/p>\n","protected":false},"author":4,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-5569","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/pages\/5569","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5569"}],"version-history":[{"count":2,"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/pages\/5569\/revisions"}],"predecessor-version":[{"id":5577,"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/pages\/5569\/revisions\/5577"}],"wp:attachment":[{"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5569"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}