xref: /plugin/calendar/syntax.php (revision fb563d5f1ab751bc5683ff2e367f04c46c6e895e)
119378907SAtari911<?php
219378907SAtari911/**
319378907SAtari911 * DokuWiki Plugin calendar (Syntax Component)
419378907SAtari911 * Compact design with integrated event list
519378907SAtari911 *
619378907SAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
719378907SAtari911 * @author  DokuWiki Community
819378907SAtari911 */
919378907SAtari911
1019378907SAtari911if (!defined('DOKU_INC')) die();
1119378907SAtari911
1219378907SAtari911class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin {
1319378907SAtari911
142866e827SAtari911    /**
152866e827SAtari911     * Get the meta directory path (farm-safe)
162866e827SAtari911     * Uses $conf['metadir'] instead of hardcoded DOKU_INC . 'data/meta/'
172866e827SAtari911     */
182866e827SAtari911    private function metaDir() {
192866e827SAtari911        global $conf;
202866e827SAtari911        return rtrim($conf['metadir'], '/') . '/';
212866e827SAtari911    }
222866e827SAtari911
232866e827SAtari911    /**
242866e827SAtari911     * Check if the current user has read access to a namespace
252866e827SAtari911     *
262866e827SAtari911     * @param string $namespace Namespace to check (empty = root)
272866e827SAtari911     * @return bool True if user has at least AUTH_READ
282866e827SAtari911     */
292866e827SAtari911    private function checkNamespaceRead($namespace) {
302866e827SAtari911        if (empty($namespace) || $namespace === '*') return true;
312866e827SAtari911        $ns = str_replace(['*', ';'], '', $namespace);
322866e827SAtari911        if (empty($ns)) return true;
332866e827SAtari911        $perm = auth_quickaclcheck($ns . ':*');
342866e827SAtari911        return ($perm >= AUTH_READ);
352866e827SAtari911    }
362866e827SAtari911
372866e827SAtari911    /**
382866e827SAtari911     * Get sync config file path (farm-safe)
392866e827SAtari911     * Checks per-animal metadir first, falls back to shared plugin dir
402866e827SAtari911     */
412866e827SAtari911    private function syncConfigPath() {
422866e827SAtari911        $perAnimal = $this->metaDir() . 'calendar/sync_config.php';
432866e827SAtari911        if (file_exists($perAnimal)) return $perAnimal;
442866e827SAtari911        return DOKU_PLUGIN . 'calendar/sync_config.php';
452866e827SAtari911    }
462866e827SAtari911
4719378907SAtari911    public function getType() {
4819378907SAtari911        return 'substition';
4919378907SAtari911    }
5019378907SAtari911
5119378907SAtari911    public function getPType() {
5219378907SAtari911        return 'block';
5319378907SAtari911    }
5419378907SAtari911
5519378907SAtari911    public function getSort() {
5619378907SAtari911        return 155;
5719378907SAtari911    }
5819378907SAtari911
5919378907SAtari911    public function connectTo($mode) {
6019378907SAtari911        $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
6119378907SAtari911        $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
6219378907SAtari911        $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
6319378907SAtari911    }
6419378907SAtari911
6519378907SAtari911    public function handle($match, $state, $pos, Doku_Handler $handler) {
6619378907SAtari911        $isEventList = (strpos($match, '{{eventlist') === 0);
6719378907SAtari911        $isEventPanel = (strpos($match, '{{eventpanel') === 0);
6819378907SAtari911
6919378907SAtari911        if ($isEventList) {
7019378907SAtari911            $match = substr($match, 12, -2);
7119378907SAtari911        } elseif ($isEventPanel) {
7219378907SAtari911            $match = substr($match, 13, -2);
7319378907SAtari911        } else {
7419378907SAtari911            $match = substr($match, 10, -2);
7519378907SAtari911        }
7619378907SAtari911
7719378907SAtari911        $params = array(
7819378907SAtari911            'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'),
7919378907SAtari911            'year' => date('Y'),
8019378907SAtari911            'month' => date('n'),
8119378907SAtari911            'namespace' => '',
8219378907SAtari911            'daterange' => '',
83e3a9f44cSAtari911            'date' => '',
84da206178SAtari911            'range' => '',
85da206178SAtari911            'static' => false,
86da206178SAtari911            'title' => '',
87da206178SAtari911            'noprint' => false,
88da206178SAtari911            'theme' => '',
89da206178SAtari911            'locked' => false  // Will be set true if month/year specified
9019378907SAtari911        );
9119378907SAtari911
92da206178SAtari911        // Track if user explicitly set month or year
93da206178SAtari911        $userSetMonth = false;
94da206178SAtari911        $userSetYear = false;
95da206178SAtari911
9619378907SAtari911        if (trim($match)) {
97da206178SAtari911            // Parse parameters, handling quoted strings properly
98da206178SAtari911            // Match: key="value with spaces" OR key=value OR standalone_flag
99da206178SAtari911            preg_match_all('/(\w+)=["\']([^"\']+)["\']|(\w+)=(\S+)|(\w+)/', trim($match), $matches, PREG_SET_ORDER);
100da206178SAtari911
101da206178SAtari911            foreach ($matches as $m) {
102da206178SAtari911                if (!empty($m[1]) && isset($m[2])) {
103da206178SAtari911                    // key="quoted value"
104da206178SAtari911                    $key = $m[1];
105da206178SAtari911                    $value = $m[2];
106da206178SAtari911                    $params[$key] = $value;
107da206178SAtari911                    if ($key === 'month') $userSetMonth = true;
108da206178SAtari911                    if ($key === 'year') $userSetYear = true;
109da206178SAtari911                } elseif (!empty($m[3]) && isset($m[4])) {
110da206178SAtari911                    // key=unquoted_value
111da206178SAtari911                    $key = $m[3];
112da206178SAtari911                    $value = $m[4];
113da206178SAtari911                    $params[$key] = $value;
114da206178SAtari911                    if ($key === 'month') $userSetMonth = true;
115da206178SAtari911                    if ($key === 'year') $userSetYear = true;
116da206178SAtari911                } elseif (!empty($m[5])) {
117da206178SAtari911                    // standalone flag
118da206178SAtari911                    $params[$m[5]] = true;
11919378907SAtari911                }
12019378907SAtari911            }
12119378907SAtari911        }
12219378907SAtari911
123da206178SAtari911        // If user explicitly set month or year, lock navigation
124da206178SAtari911        if ($userSetMonth || $userSetYear) {
125da206178SAtari911            $params['locked'] = true;
126da206178SAtari911        }
127da206178SAtari911
12819378907SAtari911        return $params;
12919378907SAtari911    }
13019378907SAtari911
13119378907SAtari911    public function render($mode, Doku_Renderer $renderer, $data) {
13219378907SAtari911        if ($mode !== 'xhtml') return false;
13319378907SAtari911
1347e8ea635SAtari911        // Disable caching - theme can change via admin without page edit
1357e8ea635SAtari911        $renderer->nocache();
1367e8ea635SAtari911
13719378907SAtari911        if ($data['type'] === 'eventlist') {
13819378907SAtari911            $html = $this->renderStandaloneEventList($data);
13919378907SAtari911        } elseif ($data['type'] === 'eventpanel') {
14019378907SAtari911            $html = $this->renderEventPanelOnly($data);
141da206178SAtari911        } elseif ($data['static']) {
142da206178SAtari911            $html = $this->renderStaticCalendar($data);
14319378907SAtari911        } else {
14419378907SAtari911            $html = $this->renderCompactCalendar($data);
14519378907SAtari911        }
14619378907SAtari911
14719378907SAtari911        $renderer->doc .= $html;
14819378907SAtari911        return true;
14919378907SAtari911    }
15019378907SAtari911
15119378907SAtari911    private function renderCompactCalendar($data) {
15219378907SAtari911        $year = (int)$data['year'];
15319378907SAtari911        $month = (int)$data['month'];
15419378907SAtari911        $namespace = $data['namespace'];
1552866e827SAtari911        $exclude = isset($data['exclude']) ? $data['exclude'] : '';
1562866e827SAtari911        $excludeList = $this->parseExcludeList($exclude);
15719378907SAtari911
1580c3b6e81SAtari911        // Get theme - prefer inline theme= parameter, fall back to admin default
1590c3b6e81SAtari911        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
1609ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
1619ccd446eSAtari911        $themeClass = 'calendar-theme-' . $theme;
1629ccd446eSAtari911
1639ccd446eSAtari911        // Determine button text color: professional uses white, others use bg color
1649ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
1659ccd446eSAtari911
166e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
167e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
168e3a9f44cSAtari911
169e3a9f44cSAtari911        if ($isMultiNamespace) {
1702866e827SAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList);
171e3a9f44cSAtari911        } else {
17219378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
173e3a9f44cSAtari911        }
17419378907SAtari911        $calId = 'cal_' . md5(serialize($data) . microtime());
17519378907SAtari911
17619378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
17719378907SAtari911
17819378907SAtari911        $prevMonth = $month - 1;
17919378907SAtari911        $prevYear = $year;
18019378907SAtari911        if ($prevMonth < 1) {
18119378907SAtari911            $prevMonth = 12;
18219378907SAtari911            $prevYear--;
18319378907SAtari911        }
18419378907SAtari911
18519378907SAtari911        $nextMonth = $month + 1;
18619378907SAtari911        $nextYear = $year;
18719378907SAtari911        if ($nextMonth > 12) {
18819378907SAtari911            $nextMonth = 1;
18919378907SAtari911            $nextYear++;
19019378907SAtari911        }
19119378907SAtari911
19296df7d3eSAtari911        // Get important namespaces from config for highlighting
1932866e827SAtari911        $configFile = $this->syncConfigPath();
19496df7d3eSAtari911        $importantNsList = ['important']; // default
19596df7d3eSAtari911        if (file_exists($configFile)) {
19696df7d3eSAtari911            $config = include $configFile;
19796df7d3eSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
19896df7d3eSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
19996df7d3eSAtari911            }
20096df7d3eSAtari911        }
20196df7d3eSAtari911
2029ccd446eSAtari911        // Container - all styling via CSS variables
2032866e827SAtari911        $html = '<div class="calendar-compact-container ' . $themeClass . '" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-exclude="' . htmlspecialchars($exclude) . '" data-year="' . $year . '" data-month="' . $month . '" data-theme="' . $theme . '" data-theme-styles="' . htmlspecialchars(json_encode($themeStyles)) . '" data-important-namespaces="' . htmlspecialchars(json_encode($importantNsList)) . '">';
2049ccd446eSAtari911
2059ccd446eSAtari911        // Inject CSS variables for this calendar instance - all theming flows from here
2069ccd446eSAtari911        $html .= '<style>
2079ccd446eSAtari911        #' . $calId . ' {
2089ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
2099ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
2109ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
2119ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
2129ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
2139ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
2149ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
2159ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
2169ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
2179ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
2189ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
2199ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
2209ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
2219ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
2229ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
2237e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
2247e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
2257e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
2267e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
2277e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
2287e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
2297e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
2309ccd446eSAtari911        }
2319ccd446eSAtari911        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
2329ccd446eSAtari911        #event-search-' . $calId . '::-webkit-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
2339ccd446eSAtari911        #event-search-' . $calId . '::-moz-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
2349ccd446eSAtari911        #event-search-' . $calId . ':-ms-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
2359ccd446eSAtari911        </style>';
2361d05cddcSAtari911
2371d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
2381d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
2391d05cddcSAtari911
2401d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
2411d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
24219378907SAtari911
24319378907SAtari911        // Embed events data as JSON for JavaScript access
24419378907SAtari911        $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>';
24519378907SAtari911
24619378907SAtari911        // Left side: Calendar
24719378907SAtari911        $html .= '<div class="calendar-compact-left">';
24819378907SAtari911
24919378907SAtari911        // Header with navigation
25019378907SAtari911        $html .= '<div class="calendar-compact-header">';
25119378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
252da206178SAtari911        $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>';
25319378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
254da206178SAtari911        $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
25519378907SAtari911        $html .= '</div>';
25619378907SAtari911
2570c3b6e81SAtari911        // Calendar grid - day name headers as a separate div (avoids Firefox th height issues)
2580c3b6e81SAtari911        $html .= '<div class="calendar-day-headers">';
2590c3b6e81SAtari911        $html .= '<span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span>';
2600c3b6e81SAtari911        $html .= '</div>';
26119378907SAtari911        $html .= '<table class="calendar-compact-grid">';
2620c3b6e81SAtari911        $html .= '<tbody>';
26319378907SAtari911
26419378907SAtari911        $firstDay = mktime(0, 0, 0, $month, 1, $year);
26519378907SAtari911        $daysInMonth = date('t', $firstDay);
26619378907SAtari911        $dayOfWeek = date('w', $firstDay);
26719378907SAtari911
268e3a9f44cSAtari911        // Build a map of all events with their date ranges for the calendar grid
26987ac9bf3SAtari911        $eventRanges = array();
270e3a9f44cSAtari911        foreach ($events as $dateKey => $dayEvents) {
27187ac9bf3SAtari911            foreach ($dayEvents as $evt) {
27287ac9bf3SAtari911                $eventId = isset($evt['id']) ? $evt['id'] : '';
27387ac9bf3SAtari911                $startDate = $dateKey;
27487ac9bf3SAtari911                $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey;
27587ac9bf3SAtari911
27687ac9bf3SAtari911                // Only process events that touch this month
27787ac9bf3SAtari911                $eventStart = new DateTime($startDate);
27887ac9bf3SAtari911                $eventEnd = new DateTime($endDate);
27987ac9bf3SAtari911                $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month));
28087ac9bf3SAtari911                $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth));
28187ac9bf3SAtari911
28287ac9bf3SAtari911                // Skip if event doesn't overlap with current month
28387ac9bf3SAtari911                if ($eventEnd < $monthStart || $eventStart > $monthEnd) {
28487ac9bf3SAtari911                    continue;
28587ac9bf3SAtari911                }
28687ac9bf3SAtari911
28787ac9bf3SAtari911                // Create entry for each day the event spans
28887ac9bf3SAtari911                $current = clone $eventStart;
28987ac9bf3SAtari911                while ($current <= $eventEnd) {
29087ac9bf3SAtari911                    $currentKey = $current->format('Y-m-d');
29187ac9bf3SAtari911
29287ac9bf3SAtari911                    // Check if this date is in current month
29387ac9bf3SAtari911                    $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey);
29487ac9bf3SAtari911                    if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) {
29587ac9bf3SAtari911                        if (!isset($eventRanges[$currentKey])) {
29687ac9bf3SAtari911                            $eventRanges[$currentKey] = array();
29787ac9bf3SAtari911                        }
29887ac9bf3SAtari911
29987ac9bf3SAtari911                        // Add event with span information
30087ac9bf3SAtari911                        $evt['_span_start'] = $startDate;
30187ac9bf3SAtari911                        $evt['_span_end'] = $endDate;
30287ac9bf3SAtari911                        $evt['_is_first_day'] = ($currentKey === $startDate);
30387ac9bf3SAtari911                        $evt['_is_last_day'] = ($currentKey === $endDate);
30487ac9bf3SAtari911                        $evt['_original_date'] = $dateKey; // Keep track of original date
30587ac9bf3SAtari911
30687ac9bf3SAtari911                        // Check if event continues from previous month or to next month
30787ac9bf3SAtari911                        $evt['_continues_from_prev'] = ($eventStart < $monthStart);
30887ac9bf3SAtari911                        $evt['_continues_to_next'] = ($eventEnd > $monthEnd);
30987ac9bf3SAtari911
31087ac9bf3SAtari911                        $eventRanges[$currentKey][] = $evt;
31187ac9bf3SAtari911                    }
31287ac9bf3SAtari911
31387ac9bf3SAtari911                    $current->modify('+1 day');
31487ac9bf3SAtari911                }
31587ac9bf3SAtari911            }
31687ac9bf3SAtari911        }
31787ac9bf3SAtari911
3188e9c470bSAtari911        // Assign stable row slots to multi-day events (same algorithm as JS rebuildCalendar)
3198e9c470bSAtari911        $slotAssignments = array(); // dateKey -> array of {id, slot}
3208e9c470bSAtari911        $eventSlots = array();      // eventId -> slot number
3218e9c470bSAtari911
3228e9c470bSAtari911        $allDates = array_keys($eventRanges);
3238e9c470bSAtari911        sort($allDates);
3248e9c470bSAtari911
3258e9c470bSAtari911        // First pass: multi-day events get stable slots across all days they span
3268e9c470bSAtari911        foreach ($allDates as $dk) {
3278e9c470bSAtari911            foreach ($eventRanges[$dk] as $evt) {
3288e9c470bSAtari911                $eid = isset($evt['id']) ? $evt['id'] : $evt['title'];
3298e9c470bSAtari911                $isMultiDay = (isset($evt['_span_start']) && isset($evt['_span_end']) && $evt['_span_start'] !== $evt['_span_end']);
3308e9c470bSAtari911
3318e9c470bSAtari911                if ($isMultiDay && !isset($eventSlots[$eid])) {
3328e9c470bSAtari911                    // Find lowest slot free across all days this event spans
3338e9c470bSAtari911                    $slot = 0;
3348e9c470bSAtari911                    $found = false;
3358e9c470bSAtari911                    while (!$found) {
3368e9c470bSAtari911                        $found = true;
3378e9c470bSAtari911                        $checkCurrent = new DateTime($evt['_span_start']);
3388e9c470bSAtari911                        $checkEnd = new DateTime($evt['_span_end']);
3398e9c470bSAtari911                        while ($checkCurrent <= $checkEnd) {
3408e9c470bSAtari911                            $checkKey = $checkCurrent->format('Y-m-d');
3418e9c470bSAtari911                            if (isset($slotAssignments[$checkKey])) {
3428e9c470bSAtari911                                foreach ($slotAssignments[$checkKey] as $assigned) {
3438e9c470bSAtari911                                    if ($assigned['slot'] === $slot) {
3448e9c470bSAtari911                                        $found = false;
3458e9c470bSAtari911                                        break;
3468e9c470bSAtari911                                    }
3478e9c470bSAtari911                                }
3488e9c470bSAtari911                            }
3498e9c470bSAtari911                            if (!$found) break;
3508e9c470bSAtari911                            $checkCurrent->modify('+1 day');
3518e9c470bSAtari911                        }
3528e9c470bSAtari911                        if (!$found) $slot++;
3538e9c470bSAtari911                    }
3548e9c470bSAtari911                    $eventSlots[$eid] = $slot;
3558e9c470bSAtari911
3568e9c470bSAtari911                    // Reserve on all days
3578e9c470bSAtari911                    $resCurrent = new DateTime($evt['_span_start']);
3588e9c470bSAtari911                    $resEnd = new DateTime($evt['_span_end']);
3598e9c470bSAtari911                    while ($resCurrent <= $resEnd) {
3608e9c470bSAtari911                        $resKey = $resCurrent->format('Y-m-d');
3618e9c470bSAtari911                        if (!isset($slotAssignments[$resKey])) $slotAssignments[$resKey] = array();
3628e9c470bSAtari911                        $slotAssignments[$resKey][] = array('id' => $eid, 'slot' => $slot);
3638e9c470bSAtari911                        $resCurrent->modify('+1 day');
3648e9c470bSAtari911                    }
3658e9c470bSAtari911                }
3668e9c470bSAtari911            }
3678e9c470bSAtari911        }
3688e9c470bSAtari911
3698e9c470bSAtari911        // Second pass: single-day events fill remaining slots
3708e9c470bSAtari911        foreach ($allDates as $dk) {
3718e9c470bSAtari911            $singleDay = array();
3728e9c470bSAtari911            foreach ($eventRanges[$dk] as $evt) {
3738e9c470bSAtari911                $eid = isset($evt['id']) ? $evt['id'] : $evt['title'];
3748e9c470bSAtari911                if (!isset($eventSlots[$eid])) {
3758e9c470bSAtari911                    $singleDay[] = $evt;
3768e9c470bSAtari911                }
3778e9c470bSAtari911            }
3788e9c470bSAtari911            // Sort single-day by time
3798e9c470bSAtari911            usort($singleDay, function($a, $b) {
3808e9c470bSAtari911                $timeA = isset($a['time']) ? $a['time'] : '';
3818e9c470bSAtari911                $timeB = isset($b['time']) ? $b['time'] : '';
3828e9c470bSAtari911                if (empty($timeA) && !empty($timeB)) return -1;
3838e9c470bSAtari911                if (!empty($timeA) && empty($timeB)) return 1;
3848e9c470bSAtari911                return strcmp($timeA, $timeB);
3858e9c470bSAtari911            });
3868e9c470bSAtari911            foreach ($singleDay as $evt) {
3878e9c470bSAtari911                $eid = isset($evt['id']) ? $evt['id'] : $evt['title'];
3888e9c470bSAtari911                $slot = 0;
3898e9c470bSAtari911                while (true) {
3908e9c470bSAtari911                    $taken = false;
3918e9c470bSAtari911                    if (isset($slotAssignments[$dk])) {
3928e9c470bSAtari911                        foreach ($slotAssignments[$dk] as $a) {
3938e9c470bSAtari911                            if ($a['slot'] === $slot) { $taken = true; break; }
3948e9c470bSAtari911                        }
3958e9c470bSAtari911                    }
3968e9c470bSAtari911                    if (!$taken) break;
3978e9c470bSAtari911                    $slot++;
3988e9c470bSAtari911                }
3998e9c470bSAtari911                $eventSlots[$eid] = $slot;
4008e9c470bSAtari911                if (!isset($slotAssignments[$dk])) $slotAssignments[$dk] = array();
4018e9c470bSAtari911                $slotAssignments[$dk][] = array('id' => $eid, 'slot' => $slot);
4028e9c470bSAtari911            }
4038e9c470bSAtari911        }
4048e9c470bSAtari911
40519378907SAtari911        $currentDay = 1;
40619378907SAtari911        $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7);
40719378907SAtari911
40819378907SAtari911        for ($row = 0; $row < $rowCount; $row++) {
40919378907SAtari911            $html .= '<tr>';
41019378907SAtari911            for ($col = 0; $col < 7; $col++) {
41119378907SAtari911                if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) {
41219378907SAtari911                    $html .= '<td class="cal-empty"></td>';
41319378907SAtari911                } else {
41419378907SAtari911                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
41519378907SAtari911                    $isToday = ($dateKey === date('Y-m-d'));
41687ac9bf3SAtari911                    $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]);
41719378907SAtari911
41819378907SAtari911                    $classes = 'cal-day';
41919378907SAtari911                    if ($isToday) $classes .= ' cal-today';
42019378907SAtari911                    if ($hasEvents) $classes .= ' cal-has-events';
42119378907SAtari911
422815440faSAtari911                    $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" tabindex="0" role="gridcell" aria-label="' . date('F j, Y', strtotime($dateKey)) . ($hasEvents ? ', has events' : '') . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">';
4239ccd446eSAtari911
4249ccd446eSAtari911                    $dayNumClass = $isToday ? 'day-num day-num-today' : 'day-num';
4259ccd446eSAtari911                    $html .= '<span class="' . $dayNumClass . '">' . $currentDay . '</span>';
42619378907SAtari911
42719378907SAtari911                    if ($hasEvents) {
4288e9c470bSAtari911                        // Build slot map for this day
4298e9c470bSAtari911                        $daySlotList = isset($slotAssignments[$dateKey]) ? $slotAssignments[$dateKey] : array();
4308e9c470bSAtari911                        $maxSlot = -1;
4318e9c470bSAtari911                        foreach ($daySlotList as $ds) {
4328e9c470bSAtari911                            if ($ds['slot'] > $maxSlot) $maxSlot = $ds['slot'];
4338e9c470bSAtari911                        }
43419378907SAtari911
4358e9c470bSAtari911                        $slotMap = array();
4368e9c470bSAtari911                        foreach ($eventRanges[$dateKey] as $evt) {
4378e9c470bSAtari911                            $eid = isset($evt['id']) ? $evt['id'] : $evt['title'];
4388e9c470bSAtari911                            if (isset($eventSlots[$eid])) {
4398e9c470bSAtari911                                $slotMap[$eventSlots[$eid]] = $evt;
4408e9c470bSAtari911                            }
4418e9c470bSAtari911                        }
44219378907SAtari911
44319378907SAtari911                        $html .= '<div class="event-indicators">';
4448e9c470bSAtari911                        for ($s = 0; $s <= $maxSlot; $s++) {
4458e9c470bSAtari911                            if (!isset($slotMap[$s])) {
4468e9c470bSAtari911                                // Spacer
4478e9c470bSAtari911                                $html .= '<span style="display:block;width:100%;height:6px;min-height:6px;flex-shrink:0;"></span>';
4488e9c470bSAtari911                                continue;
4498e9c470bSAtari911                            }
4508e9c470bSAtari911
4518e9c470bSAtari911                            $evt = $slotMap[$s];
45219378907SAtari911                            $eventId = isset($evt['id']) ? $evt['id'] : '';
4538e9c470bSAtari911                            $eventColor = isset($evt['color']) ? hsc($evt['color']) : '#3498db';
45419378907SAtari911                            $eventTime = isset($evt['time']) ? $evt['time'] : '';
4558e9c470bSAtari911                            $eventTitle = isset($evt['title']) ? hsc($evt['title']) : 'Event';
45687ac9bf3SAtari911                            $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey;
45787ac9bf3SAtari911                            $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true;
45887ac9bf3SAtari911                            $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true;
45919378907SAtari911
46096df7d3eSAtari911                            $evtNs = isset($evt['namespace']) ? $evt['namespace'] : '';
4618e9c470bSAtari911                            if (!$evtNs && isset($evt['_namespace'])) $evtNs = $evt['_namespace'];
46296df7d3eSAtari911                            $isImportantEvent = false;
46396df7d3eSAtari911                            foreach ($importantNsList as $impNs) {
46496df7d3eSAtari911                                if ($evtNs === $impNs || strpos($evtNs, $impNs . ':') === 0) {
46596df7d3eSAtari911                                    $isImportantEvent = true;
46696df7d3eSAtari911                                    break;
46796df7d3eSAtari911                                }
46896df7d3eSAtari911                            }
46996df7d3eSAtari911
47019378907SAtari911                            $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed';
47187ac9bf3SAtari911                            if (!$isFirstDay) $barClass .= ' event-bar-continues';
47287ac9bf3SAtari911                            if (!$isLastDay) $barClass .= ' event-bar-continuing';
47396df7d3eSAtari911                            if ($isImportantEvent) {
47496df7d3eSAtari911                                $barClass .= ' event-bar-important';
4758e9c470bSAtari911                                if ($isFirstDay) $barClass .= ' event-bar-has-star';
47696df7d3eSAtari911                            }
47796df7d3eSAtari911
47896df7d3eSAtari911                            $titlePrefix = $isImportantEvent ? '⭐ ' : '';
47987ac9bf3SAtari911
48019378907SAtari911                            $html .= '<span class="event-bar ' . $barClass . '" ';
48119378907SAtari911                            $html .= 'style="background: ' . $eventColor . ';" ';
48296df7d3eSAtari911                            $html .= 'title="' . $titlePrefix . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" ';
48387ac9bf3SAtari911                            $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">';
48419378907SAtari911                            $html .= '</span>';
48519378907SAtari911                        }
48619378907SAtari911                        $html .= '</div>';
48719378907SAtari911                    }
48819378907SAtari911
48919378907SAtari911                    $html .= '</td>';
49019378907SAtari911                    $currentDay++;
49119378907SAtari911                }
49219378907SAtari911            }
49319378907SAtari911            $html .= '</tr>';
49419378907SAtari911        }
49519378907SAtari911
49619378907SAtari911        $html .= '</tbody></table>';
49719378907SAtari911        $html .= '</div>'; // End calendar-left
49819378907SAtari911
49919378907SAtari911        // Right side: Event list
50019378907SAtari911        $html .= '<div class="calendar-compact-right">';
50119378907SAtari911        $html .= '<div class="event-list-header">';
50219378907SAtari911        $html .= '<div class="event-list-header-content">';
503da206178SAtari911        $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>';
50419378907SAtari911        if ($namespace) {
50519378907SAtari911            $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>';
50619378907SAtari911        }
50719378907SAtari911        $html .= '</div>';
5081d05cddcSAtari911
5091d05cddcSAtari911        // Search bar in header
51064a96c92SAtari911        $searchDefault = $this->getSearchDefault();
51164a96c92SAtari911        $searchAllClass = $searchDefault === 'all' ? ' all-dates' : '';
51264a96c92SAtari911        $searchIcon = $searchDefault === 'all' ? '��' : '��';
51364a96c92SAtari911        $searchTitle = $searchDefault === 'all' ? 'Searching all dates' : 'Search this month only';
51464a96c92SAtari911        $searchPlaceholder = $searchDefault === 'all' ? 'Search all dates...' : '�� Search...';
5151d05cddcSAtari911        $html .= '<div class="event-search-container-inline">';
51664a96c92SAtari911        $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder="' . htmlspecialchars($searchPlaceholder) . '" oninput="filterEvents(\'' . $calId . '\', this.value)">';
5171d05cddcSAtari911        $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
51864a96c92SAtari911        $html .= '<button class="event-search-mode-inline' . $searchAllClass . '" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="' . htmlspecialchars($searchTitle) . '">' . $searchIcon . '</button>';
5191d05cddcSAtari911        $html .= '</div>';
5201d05cddcSAtari911
521da206178SAtari911        $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
52219378907SAtari911        $html .= '</div>';
52319378907SAtari911
52419378907SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
5259ccd446eSAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace, $themeStyles);
52619378907SAtari911        $html .= '</div>';
52719378907SAtari911
52819378907SAtari911        $html .= '</div>'; // End calendar-right
52919378907SAtari911
53019378907SAtari911        // Event dialog
5310c3b6e81SAtari911        $html .= $this->renderEventDialog($calId, $namespace, $theme);
53219378907SAtari911
53387ac9bf3SAtari911        // Month/Year picker dialog (at container level for proper overlay)
5349ccd446eSAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
53587ac9bf3SAtari911
53619378907SAtari911        $html .= '</div>'; // End container
53719378907SAtari911
53819378907SAtari911        return $html;
53919378907SAtari911    }
54019378907SAtari911
541da206178SAtari911    /**
542da206178SAtari911     * Render a static/read-only calendar for presentation and printing
543da206178SAtari911     * No edit buttons, clean layout, print-friendly itinerary
544da206178SAtari911     */
545da206178SAtari911    private function renderStaticCalendar($data) {
546da206178SAtari911        $year = (int)$data['year'];
547da206178SAtari911        $month = (int)$data['month'];
548da206178SAtari911        $namespace = isset($data['namespace']) ? $data['namespace'] : '';
5492866e827SAtari911        $exclude = isset($data['exclude']) ? $data['exclude'] : '';
5502866e827SAtari911        $excludeList = $this->parseExcludeList($exclude);
551da206178SAtari911        $customTitle = isset($data['title']) ? $data['title'] : '';
552da206178SAtari911        $noprint = isset($data['noprint']) && $data['noprint'];
553da206178SAtari911        $locked = isset($data['locked']) && $data['locked'];
554da206178SAtari911        $themeOverride = isset($data['theme']) ? $data['theme'] : '';
555da206178SAtari911
556da206178SAtari911        // Generate unique ID for this static calendar
557da206178SAtari911        $calId = 'static-cal-' . substr(md5($namespace . $year . $month . uniqid()), 0, 8);
558da206178SAtari911
559da206178SAtari911        // Get theme settings
560da206178SAtari911        if ($themeOverride && in_array($themeOverride, ['matrix', 'pink', 'purple', 'professional', 'wiki', 'dark', 'light'])) {
561da206178SAtari911            $theme = $themeOverride;
562da206178SAtari911        } else {
563da206178SAtari911            $theme = $this->getSidebarTheme();
564da206178SAtari911        }
565da206178SAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
566da206178SAtari911
567da206178SAtari911        // Get important namespaces
568da206178SAtari911        $importantNsList = $this->getImportantNamespaces();
569da206178SAtari911
570da206178SAtari911        // Load events - check for multi-namespace or wildcard
571da206178SAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
572da206178SAtari911        if ($isMultiNamespace) {
5732866e827SAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList);
574da206178SAtari911        } else {
575da206178SAtari911            $events = $this->loadEvents($namespace, $year, $month);
576da206178SAtari911        }
577da206178SAtari911
578da206178SAtari911        // Month info
579da206178SAtari911        $firstDay = mktime(0, 0, 0, $month, 1, $year);
580da206178SAtari911        $daysInMonth = date('t', $firstDay);
581da206178SAtari911        $startDayOfWeek = (int)date('w', $firstDay);
582da206178SAtari911        $monthName = date('F', $firstDay);
583da206178SAtari911
584da206178SAtari911        // Display title - custom or default month/year
585da206178SAtari911        $displayTitle = $customTitle ? $customTitle : $monthName . ' ' . $year;
586da206178SAtari911
587da206178SAtari911        // Theme class for styling
588da206178SAtari911        $themeClass = 'static-theme-' . $theme;
589da206178SAtari911
590da206178SAtari911        // Build HTML
5912866e827SAtari911        $html = '<div class="calendar-static ' . $themeClass . '" id="' . $calId . '" data-year="' . $year . '" data-month="' . $month . '" data-namespace="' . hsc($namespace) . '" data-exclude="' . hsc($exclude) . '" data-locked="' . ($locked ? '1' : '0') . '">';
592da206178SAtari911
593da206178SAtari911        // Screen view: Calendar Grid
594da206178SAtari911        $html .= '<div class="static-screen-view">';
595da206178SAtari911
596da206178SAtari911        // Header with navigation (hide nav buttons if locked)
597da206178SAtari911        $html .= '<div class="static-header">';
598da206178SAtari911        if (!$locked) {
599da206178SAtari911            $html .= '<button class="static-nav-btn" onclick="navStaticCalendar(\'' . $calId . '\', -1)" title="' . $this->getLang('previous_month') . '">◀</button>';
600da206178SAtari911        }
601da206178SAtari911        $html .= '<h2 class="static-month-title">' . hsc($displayTitle) . '</h2>';
602da206178SAtari911        if (!$locked) {
603da206178SAtari911            $html .= '<button class="static-nav-btn" onclick="navStaticCalendar(\'' . $calId . '\', 1)" title="' . $this->getLang('next_month') . '">▶</button>';
604da206178SAtari911        }
605da206178SAtari911        if (!$noprint) {
606da206178SAtari911            $html .= '<button class="static-print-btn" onclick="printStaticCalendar(\'' . $calId . '\')" title="' . $this->getLang('print_calendar') . '">��️</button>';
607da206178SAtari911        }
608da206178SAtari911        $html .= '</div>';
609da206178SAtari911
610da206178SAtari911        // Calendar grid
611da206178SAtari911        $html .= '<table class="static-calendar-grid">';
612da206178SAtari911        $html .= '<thead><tr>';
613da206178SAtari911        $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
614da206178SAtari911        foreach ($dayNames as $day) {
615da206178SAtari911            $html .= '<th>' . $day . '</th>';
616da206178SAtari911        }
617da206178SAtari911        $html .= '</tr></thead>';
618da206178SAtari911        $html .= '<tbody>';
619da206178SAtari911
620da206178SAtari911        $dayCount = 1;
621da206178SAtari911        $totalCells = $startDayOfWeek + $daysInMonth;
622da206178SAtari911        $rows = ceil($totalCells / 7);
623da206178SAtari911
624da206178SAtari911        for ($row = 0; $row < $rows; $row++) {
625da206178SAtari911            $html .= '<tr>';
626da206178SAtari911            for ($col = 0; $col < 7; $col++) {
627da206178SAtari911                $cellNum = $row * 7 + $col;
628da206178SAtari911
629da206178SAtari911                if ($cellNum < $startDayOfWeek || $dayCount > $daysInMonth) {
630da206178SAtari911                    $html .= '<td class="static-day-empty"></td>';
631da206178SAtari911                } else {
632da206178SAtari911                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $dayCount);
633da206178SAtari911                    $dayEvents = isset($events[$dateKey]) ? $events[$dateKey] : [];
634da206178SAtari911                    $isToday = ($dateKey === date('Y-m-d'));
635da206178SAtari911                    $isWeekend = ($col === 0 || $col === 6);
636da206178SAtari911
637da206178SAtari911                    $cellClass = 'static-day';
638da206178SAtari911                    if ($isToday) $cellClass .= ' static-day-today';
639da206178SAtari911                    if ($isWeekend) $cellClass .= ' static-day-weekend';
640da206178SAtari911                    if (!empty($dayEvents)) $cellClass .= ' static-day-has-events';
641da206178SAtari911
642da206178SAtari911                    $html .= '<td class="' . $cellClass . '">';
643da206178SAtari911                    $html .= '<div class="static-day-number">' . $dayCount . '</div>';
644da206178SAtari911
645da206178SAtari911                    if (!empty($dayEvents)) {
646da206178SAtari911                        $html .= '<div class="static-day-events">';
647da206178SAtari911                        foreach ($dayEvents as $event) {
648da206178SAtari911                            $color = isset($event['color']) ? $event['color'] : '#3498db';
649da206178SAtari911                            $title = hsc($event['title']);
650da206178SAtari911                            $time = isset($event['time']) && $event['time'] ? $event['time'] : '';
651da206178SAtari911                            $desc = isset($event['description']) ? $event['description'] : '';
652da206178SAtari911                            $eventNs = isset($event['namespace']) ? $event['namespace'] : $namespace;
653da206178SAtari911
654da206178SAtari911                            // Check if important
655da206178SAtari911                            $isImportant = false;
656da206178SAtari911                            foreach ($importantNsList as $impNs) {
657da206178SAtari911                                if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
658da206178SAtari911                                    $isImportant = true;
659da206178SAtari911                                    break;
660da206178SAtari911                                }
661da206178SAtari911                            }
662da206178SAtari911
663da206178SAtari911                            // Build tooltip - plain text with basic formatting indicators
664da206178SAtari911                            $tooltipText = $event['title'];
665da206178SAtari911                            if ($time) {
666da206178SAtari911                                $tooltipText .= "\n�� " . $this->formatTime12Hour($time);
667da206178SAtari911                                if (isset($event['endTime']) && $event['endTime']) {
668da206178SAtari911                                    $tooltipText .= ' - ' . $this->formatTime12Hour($event['endTime']);
669da206178SAtari911                                }
670da206178SAtari911                            }
671da206178SAtari911                            if ($desc) {
672da206178SAtari911                                // Convert formatting to plain text equivalents
673da206178SAtari911                                $plainDesc = $desc;
674da206178SAtari911                                $plainDesc = preg_replace('/\*\*(.+?)\*\*/', '*$1*', $plainDesc);
675da206178SAtari911                                $plainDesc = preg_replace('/__(.+?)__/', '*$1*', $plainDesc);
676da206178SAtari911                                $plainDesc = preg_replace('/\/\/(.+?)\/\//', '_$1_', $plainDesc);
677da206178SAtari911                                $plainDesc = preg_replace('/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/', '$2 ($1)', $plainDesc);
678da206178SAtari911                                $plainDesc = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1 ($2)', $plainDesc);
679da206178SAtari911                                $tooltipText .= "\n\n" . $plainDesc;
680da206178SAtari911                            }
681da206178SAtari911
682da206178SAtari911                            $eventClass = 'static-event';
683da206178SAtari911                            if ($isImportant) $eventClass .= ' static-event-important';
684da206178SAtari911
685da206178SAtari911                            $html .= '<div class="' . $eventClass . '" style="border-left-color: ' . $color . ';" title="' . hsc($tooltipText) . '">';
686da206178SAtari911                            if ($isImportant) {
687da206178SAtari911                                $html .= '<span class="static-event-star">⭐</span>';
688da206178SAtari911                            }
689da206178SAtari911                            if ($time) {
690da206178SAtari911                                $html .= '<span class="static-event-time">' . $this->formatTime12Hour($time) . '</span> ';
691da206178SAtari911                            }
692da206178SAtari911                            $html .= '<span class="static-event-title">' . $title . '</span>';
693da206178SAtari911                            $html .= '</div>';
694da206178SAtari911                        }
695da206178SAtari911                        $html .= '</div>';
696da206178SAtari911                    }
697da206178SAtari911
698da206178SAtari911                    $html .= '</td>';
699da206178SAtari911                    $dayCount++;
700da206178SAtari911                }
701da206178SAtari911            }
702da206178SAtari911            $html .= '</tr>';
703da206178SAtari911        }
704da206178SAtari911
705da206178SAtari911        $html .= '</tbody></table>';
706da206178SAtari911        $html .= '</div>'; // End screen view
707da206178SAtari911
708da206178SAtari911        // Print view: Itinerary format (skip if noprint)
709da206178SAtari911        if (!$noprint) {
710da206178SAtari911            $html .= '<div class="static-print-view">';
711da206178SAtari911            $html .= '<h2 class="static-print-title">' . hsc($displayTitle) . '</h2>';
712da206178SAtari911
713da206178SAtari911            if (!empty($namespace)) {
714da206178SAtari911                $html .= '<p class="static-print-namespace">' . $this->getLang('calendar_label') . ': ' . hsc($namespace) . '</p>';
715da206178SAtari911            }
716da206178SAtari911
717da206178SAtari911            // Collect all events sorted by date
718da206178SAtari911            $allEvents = [];
719da206178SAtari911        foreach ($events as $dateKey => $dayEvents) {
720da206178SAtari911            foreach ($dayEvents as $event) {
721da206178SAtari911                $event['_date'] = $dateKey;
722da206178SAtari911                $allEvents[] = $event;
723da206178SAtari911            }
724da206178SAtari911        }
725da206178SAtari911
726da206178SAtari911        // Sort by date, then time
727da206178SAtari911        usort($allEvents, function($a, $b) {
728da206178SAtari911            $dateCompare = strcmp($a['_date'], $b['_date']);
729da206178SAtari911            if ($dateCompare !== 0) return $dateCompare;
730da206178SAtari911            $timeA = isset($a['time']) ? $a['time'] : '99:99';
731da206178SAtari911            $timeB = isset($b['time']) ? $b['time'] : '99:99';
732da206178SAtari911            return strcmp($timeA, $timeB);
733da206178SAtari911        });
734da206178SAtari911
735da206178SAtari911        if (empty($allEvents)) {
736da206178SAtari911            $html .= '<p class="static-print-empty">' . $this->getLang('no_events_scheduled') . '</p>';
737da206178SAtari911        } else {
738da206178SAtari911            $html .= '<table class="static-itinerary">';
739da206178SAtari911            $html .= '<thead><tr><th>Date</th><th>Time</th><th>Event</th><th>Details</th></tr></thead>';
740da206178SAtari911            $html .= '<tbody>';
741da206178SAtari911
742da206178SAtari911            $lastDate = '';
743da206178SAtari911            foreach ($allEvents as $event) {
744da206178SAtari911                $dateKey = $event['_date'];
745da206178SAtari911                $dateObj = new \DateTime($dateKey);
746da206178SAtari911                $dateDisplay = $dateObj->format('D, M j');
747da206178SAtari911                $eventNs = isset($event['namespace']) ? $event['namespace'] : $namespace;
748da206178SAtari911
749da206178SAtari911                // Check if important
750da206178SAtari911                $isImportant = false;
751da206178SAtari911                foreach ($importantNsList as $impNs) {
752da206178SAtari911                    if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
753da206178SAtari911                        $isImportant = true;
754da206178SAtari911                        break;
755da206178SAtari911                    }
756da206178SAtari911                }
757da206178SAtari911
758da206178SAtari911                $rowClass = $isImportant ? 'static-itinerary-important' : '';
759da206178SAtari911
760da206178SAtari911                $html .= '<tr class="' . $rowClass . '">';
761da206178SAtari911
762da206178SAtari911                // Only show date if different from previous row
763da206178SAtari911                if ($dateKey !== $lastDate) {
764da206178SAtari911                    $html .= '<td class="static-itinerary-date">' . $dateDisplay . '</td>';
765da206178SAtari911                    $lastDate = $dateKey;
766da206178SAtari911                } else {
767da206178SAtari911                    $html .= '<td></td>';
768da206178SAtari911                }
769da206178SAtari911
770da206178SAtari911                // Time
771da206178SAtari911                $time = isset($event['time']) && $event['time'] ? $this->formatTime12Hour($event['time']) : $this->getLang('all_day');
772da206178SAtari911                if (isset($event['endTime']) && $event['endTime'] && isset($event['time']) && $event['time']) {
773da206178SAtari911                    $time .= ' - ' . $this->formatTime12Hour($event['endTime']);
774da206178SAtari911                }
775da206178SAtari911                $html .= '<td class="static-itinerary-time">' . $time . '</td>';
776da206178SAtari911
777da206178SAtari911                // Title with star for important
778da206178SAtari911                $html .= '<td class="static-itinerary-title">';
779da206178SAtari911                if ($isImportant) {
780da206178SAtari911                    $html .= '⭐ ';
781da206178SAtari911                }
782da206178SAtari911                $html .= hsc($event['title']);
783da206178SAtari911                $html .= '</td>';
784da206178SAtari911
785da206178SAtari911                // Description - with formatting
786da206178SAtari911                $desc = isset($event['description']) ? $this->renderDescription($event['description']) : '';
787da206178SAtari911                $html .= '<td class="static-itinerary-desc">' . $desc . '</td>';
788da206178SAtari911
789da206178SAtari911                $html .= '</tr>';
790da206178SAtari911            }
791da206178SAtari911
792da206178SAtari911            $html .= '</tbody></table>';
793da206178SAtari911        }
794da206178SAtari911
795da206178SAtari911        $html .= '</div>'; // End print view
796da206178SAtari911        } // End noprint check
797da206178SAtari911
798da206178SAtari911        $html .= '</div>'; // End container
799da206178SAtari911
800da206178SAtari911        return $html;
801da206178SAtari911    }
802da206178SAtari911
803da206178SAtari911    /**
804da206178SAtari911     * Format time to 12-hour format
805da206178SAtari911     */
806da206178SAtari911    private function formatTime12Hour($time) {
807da206178SAtari911        if (!$time) return '';
808da206178SAtari911        $parts = explode(':', $time);
809da206178SAtari911        $hour = (int)$parts[0];
810da206178SAtari911        $minute = isset($parts[1]) ? $parts[1] : '00';
811da206178SAtari911        $ampm = $hour >= 12 ? 'PM' : 'AM';
812da206178SAtari911        $hour12 = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
813da206178SAtari911        return $hour12 . ':' . $minute . ' ' . $ampm;
814da206178SAtari911    }
815da206178SAtari911
816da206178SAtari911    /**
817da206178SAtari911     * Get list of important namespaces from config
818da206178SAtari911     */
819da206178SAtari911    private function getImportantNamespaces() {
8202866e827SAtari911        $configFile = $this->syncConfigPath();
821da206178SAtari911        $importantNsList = ['important']; // default
822da206178SAtari911        if (file_exists($configFile)) {
823da206178SAtari911            $config = include $configFile;
824da206178SAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
825da206178SAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
826da206178SAtari911            }
827da206178SAtari911        }
828da206178SAtari911        return $importantNsList;
829da206178SAtari911    }
830da206178SAtari911
8319ccd446eSAtari911    private function renderEventListContent($events, $calId, $namespace, $themeStyles = null) {
83219378907SAtari911        if (empty($events)) {
83319378907SAtari911            return '<p class="no-events-msg">No events this month</p>';
83419378907SAtari911        }
83519378907SAtari911
8369ccd446eSAtari911        // Default theme styles if not provided
8379ccd446eSAtari911        if ($themeStyles === null) {
8389ccd446eSAtari911            $theme = $this->getSidebarTheme();
8399ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
84096df7d3eSAtari911        } else {
84196df7d3eSAtari911            $theme = $this->getSidebarTheme();
84296df7d3eSAtari911        }
84396df7d3eSAtari911
84496df7d3eSAtari911        // Get important namespaces from config
8452866e827SAtari911        $configFile = $this->syncConfigPath();
84696df7d3eSAtari911        $importantNsList = ['important']; // default
84796df7d3eSAtari911        if (file_exists($configFile)) {
84896df7d3eSAtari911            $config = include $configFile;
84996df7d3eSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
85096df7d3eSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
85196df7d3eSAtari911            }
8529ccd446eSAtari911        }
8539ccd446eSAtari911
8541d05cddcSAtari911        // Check for time conflicts
8551d05cddcSAtari911        $events = $this->checkTimeConflicts($events);
8561d05cddcSAtari911
857e3a9f44cSAtari911        // Sort by date ascending (chronological order - oldest first)
85819378907SAtari911        ksort($events);
85919378907SAtari911
860e3a9f44cSAtari911        // Sort events within each day by time
861e3a9f44cSAtari911        foreach ($events as $dateKey => &$dayEvents) {
862e3a9f44cSAtari911            usort($dayEvents, function($a, $b) {
8631d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
8641d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
8651d05cddcSAtari911
8661d05cddcSAtari911                // All-day events (no time) go to the TOP
8671d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
8681d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
8691d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
8701d05cddcSAtari911
8711d05cddcSAtari911                // Both have times, sort chronologically
872e3a9f44cSAtari911                return strcmp($timeA, $timeB);
873e3a9f44cSAtari911            });
874e3a9f44cSAtari911        }
875e3a9f44cSAtari911        unset($dayEvents); // Break reference
876e3a9f44cSAtari911
877e3a9f44cSAtari911        // Get today's date for comparison
878e3a9f44cSAtari911        $today = date('Y-m-d');
879e3a9f44cSAtari911        $firstFutureEventId = null;
880e3a9f44cSAtari911
8811d05cddcSAtari911        // Helper function to check if event is past (with 15-minute grace period for timed events)
8821d05cddcSAtari911        $isEventPast = function($dateKey, $time) use ($today) {
8831d05cddcSAtari911            // If event is on a past date, it's definitely past
8841d05cddcSAtari911            if ($dateKey < $today) {
8851d05cddcSAtari911                return true;
8861d05cddcSAtari911            }
8871d05cddcSAtari911
8881d05cddcSAtari911            // If event is on a future date, it's definitely not past
8891d05cddcSAtari911            if ($dateKey > $today) {
8901d05cddcSAtari911                return false;
8911d05cddcSAtari911            }
8921d05cddcSAtari911
8931d05cddcSAtari911            // Event is today - check time with grace period
8941d05cddcSAtari911            if ($time && $time !== '') {
8951d05cddcSAtari911                try {
8961d05cddcSAtari911                    $currentDateTime = new DateTime();
8971d05cddcSAtari911                    $eventDateTime = new DateTime($dateKey . ' ' . $time);
8981d05cddcSAtari911
8991d05cddcSAtari911                    // Add 15-minute grace period
9001d05cddcSAtari911                    $eventDateTime->modify('+15 minutes');
9011d05cddcSAtari911
9021d05cddcSAtari911                    // Event is past if current time > event time + 15 minutes
9031d05cddcSAtari911                    return $currentDateTime > $eventDateTime;
9041d05cddcSAtari911                } catch (Exception $e) {
9051d05cddcSAtari911                    // If time parsing fails, fall back to date-only comparison
9061d05cddcSAtari911                    return false;
9071d05cddcSAtari911                }
9081d05cddcSAtari911            }
9091d05cddcSAtari911
9101d05cddcSAtari911            // No time specified for today's event, treat as future
9111d05cddcSAtari911            return false;
9121d05cddcSAtari911        };
9131d05cddcSAtari911
9141d05cddcSAtari911        // Build HTML for each event - separate past/completed from future
9151d05cddcSAtari911        $pastHtml = '';
9161d05cddcSAtari911        $futureHtml = '';
9171d05cddcSAtari911        $pastCount = 0;
918e3a9f44cSAtari911
91919378907SAtari911        foreach ($events as $dateKey => $dayEvents) {
920e3a9f44cSAtari911
92119378907SAtari911            foreach ($dayEvents as $event) {
922e3a9f44cSAtari911                // Track first future/today event for auto-scroll
923e3a9f44cSAtari911                if (!$firstFutureEventId && $dateKey >= $today) {
924e3a9f44cSAtari911                    $firstFutureEventId = isset($event['id']) ? $event['id'] : '';
925e3a9f44cSAtari911                }
92619378907SAtari911                $eventId = isset($event['id']) ? $event['id'] : '';
92719378907SAtari911                $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
9281d05cddcSAtari911                $timeRaw = isset($event['time']) ? $event['time'] : '';
9291d05cddcSAtari911                $time = htmlspecialchars($timeRaw);
9301d05cddcSAtari911                $endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : '';
93119378907SAtari911                $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
93219378907SAtari911                $description = isset($event['description']) ? $event['description'] : '';
93319378907SAtari911                $isTask = isset($event['isTask']) ? $event['isTask'] : false;
93419378907SAtari911                $completed = isset($event['completed']) ? $event['completed'] : false;
93519378907SAtari911                $endDate = isset($event['endDate']) ? $event['endDate'] : '';
93619378907SAtari911
9371d05cddcSAtari911                // Use helper function to determine if event is past (with grace period)
9381d05cddcSAtari911                $isPast = $isEventPast($dateKey, $timeRaw);
9391d05cddcSAtari911                $isToday = $dateKey === $today;
9401d05cddcSAtari911
9411d05cddcSAtari911                // Check if event should be in past section
9421d05cddcSAtari911                // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past
9431d05cddcSAtari911                $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed;
9441d05cddcSAtari911                if ($isPastOrCompleted) {
9451d05cddcSAtari911                    $pastCount++;
9461d05cddcSAtari911                }
9471d05cddcSAtari911
9481d05cddcSAtari911                // Determine if task is past due (past date, is task, not completed)
9491d05cddcSAtari911                $isPastDue = $isPast && $isTask && !$completed;
9501d05cddcSAtari911
95119378907SAtari911                // Process description for wiki syntax, HTML, images, and links
9529ccd446eSAtari911                $renderedDescription = $this->renderDescription($description, $themeStyles);
95319378907SAtari911
9541d05cddcSAtari911                // Convert to 12-hour format and handle time ranges
95519378907SAtari911                $displayTime = '';
95619378907SAtari911                if ($time) {
95719378907SAtari911                    $timeObj = DateTime::createFromFormat('H:i', $time);
95819378907SAtari911                    if ($timeObj) {
95919378907SAtari911                        $displayTime = $timeObj->format('g:i A');
9601d05cddcSAtari911
9611d05cddcSAtari911                        // Add end time if present and different from start time
9621d05cddcSAtari911                        if ($endTime && $endTime !== $time) {
9631d05cddcSAtari911                            $endTimeObj = DateTime::createFromFormat('H:i', $endTime);
9641d05cddcSAtari911                            if ($endTimeObj) {
9651d05cddcSAtari911                                $displayTime .= ' - ' . $endTimeObj->format('g:i A');
9661d05cddcSAtari911                            }
9671d05cddcSAtari911                        }
96819378907SAtari911                    } else {
96919378907SAtari911                        $displayTime = $time;
97019378907SAtari911                    }
97119378907SAtari911                }
97219378907SAtari911
97387ac9bf3SAtari911                // Format date display with day of week
974e3a9f44cSAtari911                // Use originalStartDate if this is a multi-month event continuation
975e3a9f44cSAtari911                $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey;
976e3a9f44cSAtari911                $dateObj = new DateTime($displayDateKey);
97787ac9bf3SAtari911                $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24"
97819378907SAtari911
97919378907SAtari911                // Multi-day indicator
98019378907SAtari911                $multiDay = '';
981e3a9f44cSAtari911                if ($endDate && $endDate !== $displayDateKey) {
98219378907SAtari911                    $endObj = new DateTime($endDate);
98387ac9bf3SAtari911                    $multiDay = ' → ' . $endObj->format('D, M j');
98419378907SAtari911                }
98519378907SAtari911
98619378907SAtari911                $completedClass = $completed ? ' event-completed' : '';
9871d05cddcSAtari911                // Don't grey out past due tasks - they need attention!
9881d05cddcSAtari911                $pastClass = ($isPast && !$isPastDue) ? ' event-past' : '';
9891d05cddcSAtari911                $pastDueClass = $isPastDue ? ' event-pastdue' : '';
990e3a9f44cSAtari911                $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
99119378907SAtari911
99296df7d3eSAtari911                // Check if this is an important namespace event
99396df7d3eSAtari911                $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
99496df7d3eSAtari911                if (!$eventNamespace && isset($event['_namespace'])) {
99596df7d3eSAtari911                    $eventNamespace = $event['_namespace'];
99696df7d3eSAtari911                }
99796df7d3eSAtari911                $isImportantNs = false;
99896df7d3eSAtari911                foreach ($importantNsList as $impNs) {
99996df7d3eSAtari911                    if ($eventNamespace === $impNs || strpos($eventNamespace, $impNs . ':') === 0) {
100096df7d3eSAtari911                        $isImportantNs = true;
100196df7d3eSAtari911                        break;
100296df7d3eSAtari911                    }
100396df7d3eSAtari911                }
100496df7d3eSAtari911                $importantClass = $isImportantNs ? ' event-important' : '';
100596df7d3eSAtari911
10069ccd446eSAtari911                // For all themes: use CSS variables, only keep border-left-color as inline
10079ccd446eSAtari911                $pastClickHandler = ($isPast && !$isPastDue) ? ' onclick="togglePastEventExpand(this)"' : '';
100896df7d3eSAtari911                $eventHtml = '<div class="event-compact-item' . $completedClass . $pastClass . $pastDueClass . $importantClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ' !important;"' . $pastClickHandler . $firstFutureAttr . '>';
10091d05cddcSAtari911                $eventHtml .= '<div class="event-info">';
10109ccd446eSAtari911
10111d05cddcSAtari911                $eventHtml .= '<div class="event-title-row">';
101296df7d3eSAtari911                // Add star for important namespace events
101396df7d3eSAtari911                if ($isImportantNs) {
1014da206178SAtari911                    $eventHtml .= '<span class="event-important-star" title="Important">⭐</span> ';
101596df7d3eSAtari911                }
10161d05cddcSAtari911                $eventHtml .= '<span class="event-title-compact">' . $title . '</span>';
10171d05cddcSAtari911                $eventHtml .= '</div>';
101819378907SAtari911
1019e3a9f44cSAtari911                // For past events, hide meta and description (collapsed)
10201d05cddcSAtari911                // EXCEPTION: Past due tasks should show their details
10211d05cddcSAtari911                if (!$isPast || $isPastDue) {
10221d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact">';
10231d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
102419378907SAtari911                    if ($displayTime) {
10251d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
102619378907SAtari911                    }
10271d05cddcSAtari911                    // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks
10281d05cddcSAtari911                    if ($isPastDue) {
10297e8ea635SAtari911                        $eventHtml .= ' <span class="event-pastdue-badge" style="background:' . $themeStyles['pastdue_color'] . ' !important; color:white !important; -webkit-text-fill-color:white !important;">' . 'PAST DUE</span>';
10301d05cddcSAtari911                    } elseif ($isToday) {
10317e8ea635SAtari911                        $eventHtml .= ' <span class="event-today-badge" style="background:' . $themeStyles['border'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;">' . 'TODAY</span>';
1032e3a9f44cSAtari911                    }
10331d05cddcSAtari911                    // Add namespace badge - ALWAYS show if event has a namespace
1034e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
1035e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
1036e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility
1037e3a9f44cSAtari911                    }
10381d05cddcSAtari911                    // Show badge if namespace exists and is not empty
10391d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
10407e8ea635SAtari911                        $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer; background:' . $themeStyles['text_bright'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>';
1041e3a9f44cSAtari911                    }
10421d05cddcSAtari911
10431d05cddcSAtari911                    // Add conflict warning if event has time conflicts
10441d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
10451d05cddcSAtari911                        $conflictList = [];
10461d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
10479ccd446eSAtari911                            $conflictText = $conflict['title'];
10481d05cddcSAtari911                            if (!empty($conflict['time'])) {
10491d05cddcSAtari911                                // Format time range
10501d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
10511d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
10521d05cddcSAtari911
10531d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
10541d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
10551d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
10561d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
10571d05cddcSAtari911                                } else {
10581d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
10591d05cddcSAtari911                                }
10601d05cddcSAtari911                            }
10611d05cddcSAtari911                            $conflictList[] = $conflictText;
10621d05cddcSAtari911                        }
10631d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
10649ccd446eSAtari911                        $conflictJson = base64_encode(json_encode($conflictList));
10651d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
10661d05cddcSAtari911                    }
10671d05cddcSAtari911
10681d05cddcSAtari911                    $eventHtml .= '</span>';
10691d05cddcSAtari911                    $eventHtml .= '</div>';
107019378907SAtari911
107119378907SAtari911                    if ($description) {
10721d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
10731d05cddcSAtari911                    }
10741d05cddcSAtari911                } else {
10751d05cddcSAtari911                    // Past events: render with display:none for click-to-expand
10761d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact" style="display:none;">';
10771d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
10781d05cddcSAtari911                    if ($displayTime) {
10791d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
10801d05cddcSAtari911                    }
10811d05cddcSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
10821d05cddcSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
10831d05cddcSAtari911                        $eventNamespace = $event['_namespace'];
10841d05cddcSAtari911                    }
10851d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
10867e8ea635SAtari911                        $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer; background:' . $themeStyles['text_bright'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>';
10871d05cddcSAtari911                    }
10881d05cddcSAtari911
10891d05cddcSAtari911                    // Add conflict warning if event has time conflicts
10901d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
10911d05cddcSAtari911                        $conflictList = [];
10921d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
10939ccd446eSAtari911                            $conflictText = $conflict['title'];
10941d05cddcSAtari911                            if (!empty($conflict['time'])) {
10951d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
10961d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
10971d05cddcSAtari911
10981d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
10991d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
11001d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
11011d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
11021d05cddcSAtari911                                } else {
11031d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
11041d05cddcSAtari911                                }
11051d05cddcSAtari911                            }
11061d05cddcSAtari911                            $conflictList[] = $conflictText;
11071d05cddcSAtari911                        }
11081d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
11099ccd446eSAtari911                        $conflictJson = base64_encode(json_encode($conflictList));
11101d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
11111d05cddcSAtari911                    }
11121d05cddcSAtari911
11131d05cddcSAtari911                    $eventHtml .= '</span>';
11141d05cddcSAtari911                    $eventHtml .= '</div>';
11151d05cddcSAtari911
11161d05cddcSAtari911                    if ($description) {
11171d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>';
111819378907SAtari911                    }
1119e3a9f44cSAtari911                }
112019378907SAtari911
11211d05cddcSAtari911                $eventHtml .= '</div>'; // event-info
112219378907SAtari911
1123e3a9f44cSAtari911                // Use stored namespace from event, fallback to passed namespace
1124e3a9f44cSAtari911                $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace;
1125e3a9f44cSAtari911
11261d05cddcSAtari911                $eventHtml .= '<div class="event-actions-compact">';
11271d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">��️</button>';
11281d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>';
11291d05cddcSAtari911                $eventHtml .= '</div>';
113019378907SAtari911
113119378907SAtari911                // Checkbox for tasks - ON THE FAR RIGHT
113219378907SAtari911                if ($isTask) {
113319378907SAtari911                    $checked = $completed ? 'checked' : '';
11341d05cddcSAtari911                    $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">';
113519378907SAtari911                }
113619378907SAtari911
11371d05cddcSAtari911                $eventHtml .= '</div>';
11381d05cddcSAtari911
11391d05cddcSAtari911                // Add to appropriate section
11401d05cddcSAtari911                if ($isPastOrCompleted) {
11411d05cddcSAtari911                    $pastHtml .= $eventHtml;
11421d05cddcSAtari911                } else {
11431d05cddcSAtari911                    $futureHtml .= $eventHtml;
11441d05cddcSAtari911                }
11451d05cddcSAtari911            }
11461d05cddcSAtari911        }
11471d05cddcSAtari911
11481d05cddcSAtari911        // Build final HTML with collapsible past events section
11491d05cddcSAtari911        $html = '';
11501d05cddcSAtari911
11511d05cddcSAtari911        // Add collapsible past events section if any exist
11521d05cddcSAtari911        if ($pastCount > 0) {
11531d05cddcSAtari911            $html .= '<div class="past-events-section">';
11541d05cddcSAtari911            $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">';
11551d05cddcSAtari911            $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> ';
11561d05cddcSAtari911            $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>';
115719378907SAtari911            $html .= '</div>';
11581d05cddcSAtari911            $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">';
11591d05cddcSAtari911            $html .= $pastHtml;
11601d05cddcSAtari911            $html .= '</div>';
11611d05cddcSAtari911            $html .= '</div>';
11621d05cddcSAtari911        }
1163e3a9f44cSAtari911
11641d05cddcSAtari911        // Add future events
11651d05cddcSAtari911        $html .= $futureHtml;
116619378907SAtari911
116719378907SAtari911        return $html;
116819378907SAtari911    }
116919378907SAtari911
11701d05cddcSAtari911    /**
11711d05cddcSAtari911     * Check for time conflicts between events
11721d05cddcSAtari911     */
11731d05cddcSAtari911    private function checkTimeConflicts($events) {
11741d05cddcSAtari911        // Group events by date
11751d05cddcSAtari911        $eventsByDate = [];
11761d05cddcSAtari911        foreach ($events as $date => $dateEvents) {
11771d05cddcSAtari911            if (!is_array($dateEvents)) continue;
11781d05cddcSAtari911
11791d05cddcSAtari911            foreach ($dateEvents as $evt) {
11801d05cddcSAtari911                if (empty($evt['time'])) continue; // Skip all-day events
11811d05cddcSAtari911
11821d05cddcSAtari911                if (!isset($eventsByDate[$date])) {
11831d05cddcSAtari911                    $eventsByDate[$date] = [];
11841d05cddcSAtari911                }
11851d05cddcSAtari911                $eventsByDate[$date][] = $evt;
11861d05cddcSAtari911            }
11871d05cddcSAtari911        }
11881d05cddcSAtari911
11891d05cddcSAtari911        // Check for overlaps on each date
11901d05cddcSAtari911        foreach ($eventsByDate as $date => $dateEvents) {
11911d05cddcSAtari911            for ($i = 0; $i < count($dateEvents); $i++) {
11921d05cddcSAtari911                for ($j = $i + 1; $j < count($dateEvents); $j++) {
11931d05cddcSAtari911                    if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) {
11941d05cddcSAtari911                        // Mark both events as conflicting
11951d05cddcSAtari911                        $dateEvents[$i]['hasConflict'] = true;
11961d05cddcSAtari911                        $dateEvents[$j]['hasConflict'] = true;
11971d05cddcSAtari911
11981d05cddcSAtari911                        // Store conflict info
11991d05cddcSAtari911                        if (!isset($dateEvents[$i]['conflictsWith'])) {
12001d05cddcSAtari911                            $dateEvents[$i]['conflictsWith'] = [];
12011d05cddcSAtari911                        }
12021d05cddcSAtari911                        if (!isset($dateEvents[$j]['conflictsWith'])) {
12031d05cddcSAtari911                            $dateEvents[$j]['conflictsWith'] = [];
12041d05cddcSAtari911                        }
12051d05cddcSAtari911
12061d05cddcSAtari911                        $dateEvents[$i]['conflictsWith'][] = [
12071d05cddcSAtari911                            'id' => $dateEvents[$j]['id'],
12081d05cddcSAtari911                            'title' => $dateEvents[$j]['title'],
12091d05cddcSAtari911                            'time' => $dateEvents[$j]['time'],
12101d05cddcSAtari911                            'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : ''
12111d05cddcSAtari911                        ];
12121d05cddcSAtari911
12131d05cddcSAtari911                        $dateEvents[$j]['conflictsWith'][] = [
12141d05cddcSAtari911                            'id' => $dateEvents[$i]['id'],
12151d05cddcSAtari911                            'title' => $dateEvents[$i]['title'],
12161d05cddcSAtari911                            'time' => $dateEvents[$i]['time'],
12171d05cddcSAtari911                            'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : ''
12181d05cddcSAtari911                        ];
12191d05cddcSAtari911                    }
12201d05cddcSAtari911                }
12211d05cddcSAtari911            }
12221d05cddcSAtari911
12231d05cddcSAtari911            // Update the events array with conflict information
12241d05cddcSAtari911            foreach ($events[$date] as &$evt) {
12251d05cddcSAtari911                foreach ($dateEvents as $checkedEvt) {
12261d05cddcSAtari911                    if ($evt['id'] === $checkedEvt['id']) {
12271d05cddcSAtari911                        if (isset($checkedEvt['hasConflict'])) {
12281d05cddcSAtari911                            $evt['hasConflict'] = $checkedEvt['hasConflict'];
12291d05cddcSAtari911                        }
12301d05cddcSAtari911                        if (isset($checkedEvt['conflictsWith'])) {
12311d05cddcSAtari911                            $evt['conflictsWith'] = $checkedEvt['conflictsWith'];
12321d05cddcSAtari911                        }
12331d05cddcSAtari911                        break;
12341d05cddcSAtari911                    }
12351d05cddcSAtari911                }
12361d05cddcSAtari911            }
12371d05cddcSAtari911        }
12381d05cddcSAtari911
12391d05cddcSAtari911        return $events;
12401d05cddcSAtari911    }
12411d05cddcSAtari911
12421d05cddcSAtari911    /**
12431d05cddcSAtari911     * Check if two events overlap in time
12441d05cddcSAtari911     */
12451d05cddcSAtari911    private function eventsOverlap($evt1, $evt2) {
12461d05cddcSAtari911        if (empty($evt1['time']) || empty($evt2['time'])) {
12471d05cddcSAtari911            return false; // All-day events don't conflict
12481d05cddcSAtari911        }
12491d05cddcSAtari911
12501d05cddcSAtari911        $start1 = $evt1['time'];
12511d05cddcSAtari911        $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time'];
12521d05cddcSAtari911
12531d05cddcSAtari911        $start2 = $evt2['time'];
12541d05cddcSAtari911        $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time'];
12551d05cddcSAtari911
12561d05cddcSAtari911        // Convert to minutes for easier comparison
12571d05cddcSAtari911        $start1Mins = $this->timeToMinutes($start1);
12581d05cddcSAtari911        $end1Mins = $this->timeToMinutes($end1);
12591d05cddcSAtari911        $start2Mins = $this->timeToMinutes($start2);
12601d05cddcSAtari911        $end2Mins = $this->timeToMinutes($end2);
12611d05cddcSAtari911
12621d05cddcSAtari911        // Check for overlap: start1 < end2 AND start2 < end1
12631d05cddcSAtari911        return $start1Mins < $end2Mins && $start2Mins < $end1Mins;
12641d05cddcSAtari911    }
12651d05cddcSAtari911
12661d05cddcSAtari911    /**
12671d05cddcSAtari911     * Convert HH:MM time to minutes since midnight
12681d05cddcSAtari911     */
12691d05cddcSAtari911    private function timeToMinutes($timeStr) {
12701d05cddcSAtari911        $parts = explode(':', $timeStr);
12711d05cddcSAtari911        if (count($parts) !== 2) return 0;
12721d05cddcSAtari911
12731d05cddcSAtari911        return (int)$parts[0] * 60 + (int)$parts[1];
12741d05cddcSAtari911    }
12751d05cddcSAtari911
127619378907SAtari911    private function renderEventPanelOnly($data) {
127719378907SAtari911        $year = (int)$data['year'];
127819378907SAtari911        $month = (int)$data['month'];
127919378907SAtari911        $namespace = $data['namespace'];
12802866e827SAtari911        $exclude = isset($data['exclude']) ? $data['exclude'] : '';
12812866e827SAtari911        $excludeList = $this->parseExcludeList($exclude);
128287ac9bf3SAtari911        $height = isset($data['height']) ? $data['height'] : '400px';
128387ac9bf3SAtari911
128487ac9bf3SAtari911        // Validate height format (must be px, em, rem, vh, or %)
128587ac9bf3SAtari911        if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) {
128687ac9bf3SAtari911            $height = '400px'; // Default fallback
128787ac9bf3SAtari911        }
128819378907SAtari911
12890c3b6e81SAtari911        // Get theme - prefer inline theme= parameter, fall back to admin default
12900c3b6e81SAtari911        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();        $themeStyles = $this->getSidebarThemeStyles($theme);
12919ccd446eSAtari911
1292e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
1293e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
1294e3a9f44cSAtari911
1295e3a9f44cSAtari911        if ($isMultiNamespace) {
12962866e827SAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList);
1297e3a9f44cSAtari911        } else {
129819378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
1299e3a9f44cSAtari911        }
130019378907SAtari911        $calId = 'panel_' . md5(serialize($data) . microtime());
130119378907SAtari911
130219378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
130319378907SAtari911
130419378907SAtari911        $prevMonth = $month - 1;
130519378907SAtari911        $prevYear = $year;
130619378907SAtari911        if ($prevMonth < 1) {
130719378907SAtari911            $prevMonth = 12;
130819378907SAtari911            $prevYear--;
130919378907SAtari911        }
131019378907SAtari911
131119378907SAtari911        $nextMonth = $month + 1;
131219378907SAtari911        $nextYear = $year;
131319378907SAtari911        if ($nextMonth > 12) {
131419378907SAtari911            $nextMonth = 1;
131519378907SAtari911            $nextYear++;
131619378907SAtari911        }
131719378907SAtari911
13189ccd446eSAtari911        // Determine button text color based on theme
13199ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
13209ccd446eSAtari911
132196df7d3eSAtari911        // Get important namespaces from config for highlighting
13222866e827SAtari911        $configFile = $this->syncConfigPath();
132396df7d3eSAtari911        $importantNsList = ['important']; // default
132496df7d3eSAtari911        if (file_exists($configFile)) {
132596df7d3eSAtari911            $config = include $configFile;
132696df7d3eSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
132796df7d3eSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
132896df7d3eSAtari911            }
132996df7d3eSAtari911        }
133096df7d3eSAtari911
13312866e827SAtari911        $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-exclude="' . htmlspecialchars($exclude) . '" data-theme="' . $theme . '" data-theme-styles="' . htmlspecialchars(json_encode($themeStyles)) . '" data-important-namespaces="' . htmlspecialchars(json_encode($importantNsList)) . '">';
13329ccd446eSAtari911
13339ccd446eSAtari911        // Inject CSS variables for this panel instance - same as main calendar
13349ccd446eSAtari911        $html .= '<style>
13359ccd446eSAtari911        #' . $calId . ' {
13369ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
13379ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
13389ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
13399ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
13409ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
13419ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
13429ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
13439ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
13449ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
13459ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
13469ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
13479ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
13489ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
13499ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
13509ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
13517e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
13527e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
13537e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
13547e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
13557e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
13567e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
13577e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
13589ccd446eSAtari911        }
13599ccd446eSAtari911        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
13609ccd446eSAtari911        </style>';
136119378907SAtari911
13621d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
13631d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
13641d05cddcSAtari911
13651d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
13661d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
13671d05cddcSAtari911
13681d05cddcSAtari911        // Compact two-row header designed for ~500px width
13691d05cddcSAtari911        $html .= '<div class="panel-header-compact">';
13701d05cddcSAtari911
13711d05cddcSAtari911        // Row 1: Navigation and title
13721d05cddcSAtari911        $html .= '<div class="panel-header-row-1">';
13731d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
13741d05cddcSAtari911
13751d05cddcSAtari911        // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events")
13761d05cddcSAtari911        $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year));
13771d05cddcSAtari911        $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>';
13781d05cddcSAtari911
13791d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
13801d05cddcSAtari911
13811d05cddcSAtari911        // Namespace badge (if applicable)
138287ac9bf3SAtari911        if ($namespace) {
1383e3a9f44cSAtari911            if ($isMultiNamespace) {
1384e3a9f44cSAtari911                if (strpos($namespace, '*') !== false) {
13857e8ea635SAtari911                    $html .= '<span class="panel-ns-badge" style="background:var(--cell-today-bg) !important; color:var(--text-bright) !important; -webkit-text-fill-color:var(--text-bright) !important;" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
1386e3a9f44cSAtari911                } else {
1387e3a9f44cSAtari911                    $namespaceList = array_map('trim', explode(';', $namespace));
13881d05cddcSAtari911                    $nsCount = count($namespaceList);
13897e8ea635SAtari911                    $html .= '<span class="panel-ns-badge" style="background:var(--cell-today-bg) !important; color:var(--text-bright) !important; -webkit-text-fill-color:var(--text-bright) !important;" title="' . htmlspecialchars(implode(', ', $namespaceList)) . '">' . $nsCount . ' NS</span>';
1390e3a9f44cSAtari911                }
1391e3a9f44cSAtari911            } else {
13921d05cddcSAtari911                $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false);
13931d05cddcSAtari911                if ($isFiltering) {
13947e8ea635SAtari911                    $html .= '<span class="panel-ns-badge filter-on" style="background:var(--text-bright) !important; color:var(--background-site) !important; -webkit-text-fill-color:var(--background-site) !important;" title="Filtering by ' . htmlspecialchars($namespace) . ' - click to clear" onclick="clearNamespaceFilterPanel(\'' . $calId . '\')">' . htmlspecialchars($namespace) . ' ✕</span>';
13951d05cddcSAtari911                } else {
13967e8ea635SAtari911                    $html .= '<span class="panel-ns-badge" style="background:var(--cell-today-bg) !important; color:var(--text-bright) !important; -webkit-text-fill-color:var(--text-bright) !important;" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
139787ac9bf3SAtari911                }
1398e3a9f44cSAtari911            }
13991d05cddcSAtari911        }
14001d05cddcSAtari911
1401da206178SAtari911        $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
140219378907SAtari911        $html .= '</div>';
140319378907SAtari911
14041d05cddcSAtari911        // Row 2: Search and add button
140564a96c92SAtari911        $searchDefault = $this->getSearchDefault();
140664a96c92SAtari911        $searchAllClass = $searchDefault === 'all' ? ' all-dates' : '';
140764a96c92SAtari911        $searchIcon = $searchDefault === 'all' ? '��' : '��';
140864a96c92SAtari911        $searchTitle = $searchDefault === 'all' ? 'Searching all dates' : 'Search this month only';
140964a96c92SAtari911        $searchPlaceholder = $searchDefault === 'all' ? 'Search all dates...' : 'Search this month...';
14101d05cddcSAtari911        $html .= '<div class="panel-header-row-2">';
14111d05cddcSAtari911        $html .= '<div class="panel-search-box">';
141264a96c92SAtari911        $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="' . htmlspecialchars($searchPlaceholder) . '" oninput="filterEvents(\'' . $calId . '\', this.value)">';
14131d05cddcSAtari911        $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
141464a96c92SAtari911        $html .= '<button class="panel-search-mode' . $searchAllClass . '" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="' . htmlspecialchars($searchTitle) . '">' . $searchIcon . '</button>';
14151d05cddcSAtari911        $html .= '</div>';
14161d05cddcSAtari911        $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
14171d05cddcSAtari911        $html .= '</div>';
14181d05cddcSAtari911
141919378907SAtari911        $html .= '</div>';
142019378907SAtari911
142187ac9bf3SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">';
142219378907SAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace);
142319378907SAtari911        $html .= '</div>';
142419378907SAtari911
14250c3b6e81SAtari911        $html .= $this->renderEventDialog($calId, $namespace, $theme);
142619378907SAtari911
142787ac9bf3SAtari911        // Month/Year picker for event panel
14289ccd446eSAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
142987ac9bf3SAtari911
143019378907SAtari911        $html .= '</div>';
143119378907SAtari911
143219378907SAtari911        return $html;
143319378907SAtari911    }
143419378907SAtari911
143519378907SAtari911    private function renderStandaloneEventList($data) {
143619378907SAtari911        $namespace = $data['namespace'];
14371d05cddcSAtari911        // If no namespace specified, show all namespaces
14381d05cddcSAtari911        if (empty($namespace)) {
14391d05cddcSAtari911            $namespace = '*';
14401d05cddcSAtari911        }
14412866e827SAtari911        $exclude = isset($data['exclude']) ? $data['exclude'] : '';
14422866e827SAtari911        $excludeList = $this->parseExcludeList($exclude);
144319378907SAtari911        $daterange = $data['daterange'];
144419378907SAtari911        $date = $data['date'];
1445e3a9f44cSAtari911        $range = isset($data['range']) ? strtolower($data['range']) : '';
144687ac9bf3SAtari911        $today = isset($data['today']) ? true : false;
1447e3a9f44cSAtari911        $sidebar = isset($data['sidebar']) ? true : false;
14481d05cddcSAtari911        $showchecked = isset($data['showchecked']) ? true : false; // New parameter
14491d05cddcSAtari911        $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header
145019378907SAtari911
1451*fb563d5fSAtari911        // Handle "range" parameter - day, week, month, or extended format (>3m, >100d, >2w, >1y)
1452e3a9f44cSAtari911        if ($range === 'day') {
14531d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
145487ac9bf3SAtari911            $endDate = date('Y-m-d');
1455da206178SAtari911            $headerText = 'Today';
1456e3a9f44cSAtari911        } elseif ($range === 'week') {
14571d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
14581d05cddcSAtari911            $endDateTime = new DateTime();
1459e3a9f44cSAtari911            $endDateTime->modify('+7 days');
1460e3a9f44cSAtari911            $endDate = $endDateTime->format('Y-m-d');
1461da206178SAtari911            $headerText = 'This Week';
1462e3a9f44cSAtari911        } elseif ($range === 'month') {
14631d05cddcSAtari911            $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks
1464e3a9f44cSAtari911            $endDate = date('Y-m-t'); // Last of current month
14651d05cddcSAtari911            $dt = new DateTime();
1466e3a9f44cSAtari911            $headerText = $dt->format('F Y');
1467*fb563d5fSAtari911        } elseif (preg_match('/^>(\d+)(d|w|m|y)$/i', $range, $rangeMatch)) {
1468*fb563d5fSAtari911            // Extended range: >3m = next 3 months, >100d = next 100 days, >2w = next 2 weeks, >1y = next 1 year
1469*fb563d5fSAtari911            $rangeNum = (int)$rangeMatch[1];
1470*fb563d5fSAtari911            $rangeUnit = strtolower($rangeMatch[2]);
1471*fb563d5fSAtari911
1472*fb563d5fSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
1473*fb563d5fSAtari911            $endDateTime = new DateTime();
1474*fb563d5fSAtari911
1475*fb563d5fSAtari911            switch ($rangeUnit) {
1476*fb563d5fSAtari911                case 'd':
1477*fb563d5fSAtari911                    $endDateTime->modify('+' . $rangeNum . ' days');
1478*fb563d5fSAtari911                    $headerText = $this->getLang('next') . ' ' . $rangeNum . ' ' . ($rangeNum === 1 ? $this->getLang('range_day') : $this->getLang('range_days'));
1479*fb563d5fSAtari911                    break;
1480*fb563d5fSAtari911                case 'w':
1481*fb563d5fSAtari911                    $endDateTime->modify('+' . $rangeNum . ' weeks');
1482*fb563d5fSAtari911                    $headerText = $this->getLang('next') . ' ' . $rangeNum . ' ' . ($rangeNum === 1 ? $this->getLang('range_week') : $this->getLang('range_weeks'));
1483*fb563d5fSAtari911                    break;
1484*fb563d5fSAtari911                case 'm':
1485*fb563d5fSAtari911                    $endDateTime->modify('+' . $rangeNum . ' months');
1486*fb563d5fSAtari911                    $headerText = $this->getLang('next') . ' ' . $rangeNum . ' ' . ($rangeNum === 1 ? $this->getLang('range_month') : $this->getLang('range_months'));
1487*fb563d5fSAtari911                    break;
1488*fb563d5fSAtari911                case 'y':
1489*fb563d5fSAtari911                    $endDateTime->modify('+' . $rangeNum . ' years');
1490*fb563d5fSAtari911                    $headerText = $this->getLang('next') . ' ' . $rangeNum . ' ' . ($rangeNum === 1 ? $this->getLang('range_year') : $this->getLang('range_years'));
1491*fb563d5fSAtari911                    break;
1492*fb563d5fSAtari911            }
1493*fb563d5fSAtari911            $endDate = $endDateTime->format('Y-m-d');
1494e3a9f44cSAtari911        } elseif ($sidebar) {
14951d05cddcSAtari911            // NEW: Sidebar widget - load current week's events
14969ccd446eSAtari911            $weekStartDay = $this->getWeekStartDay(); // Get saved preference
14979ccd446eSAtari911
14989ccd446eSAtari911            if ($weekStartDay === 'monday') {
14999ccd446eSAtari911                // Monday start
15001d05cddcSAtari911                $weekStart = date('Y-m-d', strtotime('monday this week'));
15011d05cddcSAtari911                $weekEnd = date('Y-m-d', strtotime('sunday this week'));
15029ccd446eSAtari911            } else {
15039ccd446eSAtari911                // Sunday start (default - US/Canada standard)
15049ccd446eSAtari911                $today = date('w'); // 0 (Sun) to 6 (Sat)
15059ccd446eSAtari911                if ($today == 0) {
15069ccd446eSAtari911                    // Today is Sunday
15079ccd446eSAtari911                    $weekStart = date('Y-m-d');
15089ccd446eSAtari911                } else {
15099ccd446eSAtari911                    // Monday-Saturday: go back to last Sunday
15109ccd446eSAtari911                    $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
15119ccd446eSAtari911                }
15129ccd446eSAtari911                $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
15139ccd446eSAtari911            }
15141d05cddcSAtari911
15159ccd446eSAtari911            // Load events for the entire week PLUS tomorrow (if tomorrow is outside week)
15169ccd446eSAtari911            // PLUS next 2 weeks for Important events
15171d05cddcSAtari911            $start = new DateTime($weekStart);
15181d05cddcSAtari911            $end = new DateTime($weekEnd);
15199ccd446eSAtari911
15209ccd446eSAtari911            // Check if we need to extend to include tomorrow
15219ccd446eSAtari911            $tomorrowDate = date('Y-m-d', strtotime('+1 day'));
15229ccd446eSAtari911            if ($tomorrowDate > $weekEnd) {
15239ccd446eSAtari911                // Tomorrow is outside the week, extend end date to include it
15249ccd446eSAtari911                $end = new DateTime($tomorrowDate);
15259ccd446eSAtari911            }
15269ccd446eSAtari911
15279ccd446eSAtari911            // Extend 2 weeks into the future for Important events
15289ccd446eSAtari911            $twoWeeksOut = date('Y-m-d', strtotime($weekEnd . ' +14 days'));
15299ccd446eSAtari911            $end = new DateTime($twoWeeksOut);
15309ccd446eSAtari911
15311d05cddcSAtari911            $end->modify('+1 day'); // DatePeriod excludes end date
15321d05cddcSAtari911            $interval = new DateInterval('P1D');
15331d05cddcSAtari911            $period = new DatePeriod($start, $interval, $end);
15341d05cddcSAtari911
15351d05cddcSAtari911            $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
15361d05cddcSAtari911            $allEvents = [];
15371d05cddcSAtari911            $loadedMonths = [];
15381d05cddcSAtari911
15391d05cddcSAtari911            foreach ($period as $dt) {
15401d05cddcSAtari911                $year = (int)$dt->format('Y');
15411d05cddcSAtari911                $month = (int)$dt->format('n');
15421d05cddcSAtari911                $dateKey = $dt->format('Y-m-d');
15431d05cddcSAtari911
15441d05cddcSAtari911                $monthKey = $year . '-' . $month . '-' . $namespace;
15451d05cddcSAtari911
15461d05cddcSAtari911                if (!isset($loadedMonths[$monthKey])) {
15471d05cddcSAtari911                    if ($isMultiNamespace) {
15482866e827SAtari911                        $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList);
15491d05cddcSAtari911                    } else {
15501d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
15511d05cddcSAtari911                    }
15521d05cddcSAtari911                }
15531d05cddcSAtari911
15541d05cddcSAtari911                $monthEvents = $loadedMonths[$monthKey];
15551d05cddcSAtari911
15561d05cddcSAtari911                if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
15571d05cddcSAtari911                    $allEvents[$dateKey] = $monthEvents[$dateKey];
15581d05cddcSAtari911                }
15591d05cddcSAtari911            }
15601d05cddcSAtari911
15611d05cddcSAtari911            // Apply time conflict detection
15621d05cddcSAtari911            $allEvents = $this->checkTimeConflicts($allEvents);
15631d05cddcSAtari911
15641d05cddcSAtari911            $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8);
15651d05cddcSAtari911
15661d05cddcSAtari911            // Render sidebar widget and return immediately
15670c3b6e81SAtari911            $themeOverride = !empty($data['theme']) ? $data['theme'] : null;
15680c3b6e81SAtari911            return $this->renderSidebarWidget($allEvents, $namespace, $calId, $themeOverride);
1569e3a9f44cSAtari911        } elseif ($today) {
1570e3a9f44cSAtari911            $startDate = date('Y-m-d');
1571e3a9f44cSAtari911            $endDate = date('Y-m-d');
1572da206178SAtari911            $headerText = 'Today';
157387ac9bf3SAtari911        } elseif ($daterange) {
157419378907SAtari911            list($startDate, $endDate) = explode(':', $daterange);
1575e3a9f44cSAtari911            $start = new DateTime($startDate);
1576e3a9f44cSAtari911            $end = new DateTime($endDate);
1577e3a9f44cSAtari911            $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y');
157819378907SAtari911        } elseif ($date) {
157919378907SAtari911            $startDate = $date;
158019378907SAtari911            $endDate = $date;
1581e3a9f44cSAtari911            $dt = new DateTime($date);
1582e3a9f44cSAtari911            $headerText = $dt->format('l, F j, Y');
158319378907SAtari911        } else {
158419378907SAtari911            $startDate = date('Y-m-01');
158519378907SAtari911            $endDate = date('Y-m-t');
1586e3a9f44cSAtari911            $dt = new DateTime($startDate);
1587e3a9f44cSAtari911            $headerText = $dt->format('F Y');
158819378907SAtari911        }
158919378907SAtari911
1590e3a9f44cSAtari911        // Load all events in date range
159119378907SAtari911        $allEvents = array();
159219378907SAtari911        $start = new DateTime($startDate);
159319378907SAtari911        $end = new DateTime($endDate);
159419378907SAtari911        $end->modify('+1 day');
159519378907SAtari911
159619378907SAtari911        $interval = new DateInterval('P1D');
159719378907SAtari911        $period = new DatePeriod($start, $interval, $end);
159819378907SAtari911
1599e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
1600e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
1601e3a9f44cSAtari911
160219378907SAtari911        static $loadedMonths = array();
160319378907SAtari911
160419378907SAtari911        foreach ($period as $dt) {
160519378907SAtari911            $year = (int)$dt->format('Y');
160619378907SAtari911            $month = (int)$dt->format('n');
160719378907SAtari911            $dateKey = $dt->format('Y-m-d');
160819378907SAtari911
1609e3a9f44cSAtari911            $monthKey = $year . '-' . $month . '-' . $namespace;
161019378907SAtari911
161119378907SAtari911            if (!isset($loadedMonths[$monthKey])) {
1612e3a9f44cSAtari911                if ($isMultiNamespace) {
16132866e827SAtari911                    $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList);
1614e3a9f44cSAtari911                } else {
161519378907SAtari911                    $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
161619378907SAtari911                }
1617e3a9f44cSAtari911            }
161819378907SAtari911
161919378907SAtari911            $monthEvents = $loadedMonths[$monthKey];
162019378907SAtari911
162119378907SAtari911            if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
162219378907SAtari911                $allEvents[$dateKey] = $monthEvents[$dateKey];
162319378907SAtari911            }
162419378907SAtari911        }
162519378907SAtari911
16261d05cddcSAtari911        // Sort events by date (already sorted by dateKey), then by time within each day
16271d05cddcSAtari911        foreach ($allEvents as $dateKey => &$dayEvents) {
16281d05cddcSAtari911            usort($dayEvents, function($a, $b) {
16291d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
16301d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
16311d05cddcSAtari911
16321d05cddcSAtari911                // All-day events (no time) go to the TOP
16331d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
16341d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
16351d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
16361d05cddcSAtari911
16371d05cddcSAtari911                // Both have times, sort chronologically
16381d05cddcSAtari911                return strcmp($timeA, $timeB);
16391d05cddcSAtari911            });
16401d05cddcSAtari911        }
16411d05cddcSAtari911        unset($dayEvents); // Break reference
16421d05cddcSAtari911
1643e3a9f44cSAtari911        // Simple 2-line display widget
16441d05cddcSAtari911        $calId = 'eventlist_' . uniqid();
16457e8ea635SAtari911        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
16467e8ea635SAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
16477e8ea635SAtari911        $isDark = in_array($theme, ['matrix', 'purple', 'pink']);
16487e8ea635SAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
16497e8ea635SAtari911
16507e8ea635SAtari911        // Theme class for CSS targeting
16517e8ea635SAtari911        $themeClass = 'eventlist-theme-' . $theme;
16527e8ea635SAtari911
16537e8ea635SAtari911        // Container styling - dark themes get border + glow, light themes get subtle border
16547e8ea635SAtari911        $containerStyle = 'background:' . $themeStyles['bg'] . ' !important;';
16557e8ea635SAtari911        if ($isDark) {
16567e8ea635SAtari911            $containerStyle .= ' border:2px solid ' . $themeStyles['border'] . ';';
16577e8ea635SAtari911            $containerStyle .= ' border-radius:4px;';
16587e8ea635SAtari911            $containerStyle .= ' box-shadow:0 0 10px ' . $themeStyles['shadow'] . ';';
16597e8ea635SAtari911        } else {
16607e8ea635SAtari911            $containerStyle .= ' border:1px solid ' . $themeStyles['grid_border'] . ';';
16617e8ea635SAtari911            $containerStyle .= ' border-radius:4px;';
16627e8ea635SAtari911        }
16637e8ea635SAtari911
16642866e827SAtari911        $html = '<div class="eventlist-simple ' . $themeClass . '" id="' . $calId . '" style="' . $containerStyle . '" data-exclude="' . htmlspecialchars($exclude) . '">';
16657e8ea635SAtari911
16667e8ea635SAtari911        // Inject CSS variables for this eventlist instance
16677e8ea635SAtari911        $html .= '<style>
16687e8ea635SAtari911        #' . $calId . ' {
16697e8ea635SAtari911            --background-site: ' . $themeStyles['bg'] . ';
16707e8ea635SAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
16717e8ea635SAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
16727e8ea635SAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
16737e8ea635SAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
16747e8ea635SAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
16757e8ea635SAtari911            --border-main: ' . $themeStyles['border'] . ';
16767e8ea635SAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
16777e8ea635SAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
16787e8ea635SAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
16797e8ea635SAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
16807e8ea635SAtari911            --btn-text: ' . $btnTextColor . ';
16817e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
16827e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
16837e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
16847e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
16857e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
16867e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
16877e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
16887e8ea635SAtari911        }
16897e8ea635SAtari911        </style>';
16901d05cddcSAtari911
16911d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
16921d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
16931d05cddcSAtari911
16941d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
16951d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
16961d05cddcSAtari911
16971d05cddcSAtari911        // Add compact header with date and clock for "today" mode (unless noheader is set)
16981d05cddcSAtari911        if ($today && !empty($allEvents) && !$noheader) {
16991d05cddcSAtari911            $todayDate = new DateTime();
17001d05cddcSAtari911            $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026"
17011d05cddcSAtari911            $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM"
17021d05cddcSAtari911
17031d05cddcSAtari911            $html .= '<div class="eventlist-today-header">';
17041d05cddcSAtari911            $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>';
17051d05cddcSAtari911            $html .= '<div class="eventlist-bottom-info">';
17061d05cddcSAtari911            $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '">--°</span></span>';
17071d05cddcSAtari911            $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
17081d05cddcSAtari911            $html .= '</div>';
17091d05cddcSAtari911
17101d05cddcSAtari911            $html .= '</div>';
17111d05cddcSAtari911
17121d05cddcSAtari911            // Add JavaScript to update clock and weather
17131d05cddcSAtari911            $html .= '<script>
17141d05cddcSAtari911(function() {
17151d05cddcSAtari911    // Update clock every second
17161d05cddcSAtari911    function updateClock() {
17171d05cddcSAtari911        const now = new Date();
17181d05cddcSAtari911        let hours = now.getHours();
17191d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
17201d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
17211d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
17221d05cddcSAtari911        hours = hours % 12 || 12;
17231d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
17241d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
17251d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
17261d05cddcSAtari911    }
17271d05cddcSAtari911    setInterval(updateClock, 1000);
17281d05cddcSAtari911
172996df7d3eSAtari911    // Fetch weather - uses default location, click weather to get local
173096df7d3eSAtari911    var userLocationGranted = false;
173196df7d3eSAtari911    var userLat = 38.5816;  // Sacramento default
173296df7d3eSAtari911    var userLon = -121.4944;
17331d05cddcSAtari911
173496df7d3eSAtari911    function fetchWeatherData(lat, lon) {
173596df7d3eSAtari911        fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&temperature_unit=fahrenheit")
17361d05cddcSAtari911            .then(response => response.json())
17371d05cddcSAtari911            .then(data => {
17381d05cddcSAtari911                if (data.current_weather) {
17391d05cddcSAtari911                    const temp = Math.round(data.current_weather.temperature);
17401d05cddcSAtari911                    const weatherCode = data.current_weather.weathercode;
17411d05cddcSAtari911                    const icon = getWeatherIcon(weatherCode);
17421d05cddcSAtari911                    const iconEl = document.getElementById("weather-icon-' . $calId . '");
17431d05cddcSAtari911                    const tempEl = document.getElementById("weather-temp-' . $calId . '");
17441d05cddcSAtari911                    if (iconEl) iconEl.textContent = icon;
17451d05cddcSAtari911                    if (tempEl) tempEl.innerHTML = temp + "&deg;";
17461d05cddcSAtari911                }
17471d05cddcSAtari911            })
17481d05cddcSAtari911            .catch(error => {
17491d05cddcSAtari911                console.log("Weather fetch error:", error);
17501d05cddcSAtari911            });
175196df7d3eSAtari911    }
175296df7d3eSAtari911
175396df7d3eSAtari911    function updateWeather() {
175496df7d3eSAtari911        fetchWeatherData(userLat, userLon);
175596df7d3eSAtari911    }
175696df7d3eSAtari911
175796df7d3eSAtari911    // Allow user to click weather to get local weather (requires user gesture)
175896df7d3eSAtari911    function requestLocalWeather() {
175996df7d3eSAtari911        if (userLocationGranted) return; // Already have permission
176096df7d3eSAtari911
176196df7d3eSAtari911        if ("geolocation" in navigator) {
176296df7d3eSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
176396df7d3eSAtari911                userLat = position.coords.latitude;
176496df7d3eSAtari911                userLon = position.coords.longitude;
176596df7d3eSAtari911                userLocationGranted = true;
176696df7d3eSAtari911                fetchWeatherData(userLat, userLon);
17671d05cddcSAtari911            }, function(error) {
176896df7d3eSAtari911                console.log("Geolocation denied or unavailable, using default location");
17691d05cddcSAtari911            });
17701d05cddcSAtari911        }
17711d05cddcSAtari911    }
17721d05cddcSAtari911
177396df7d3eSAtari911    // Add click handler to weather widget for local weather
177496df7d3eSAtari911    setTimeout(function() {
177596df7d3eSAtari911        var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
177696df7d3eSAtari911        if (weatherEl) {
177796df7d3eSAtari911            weatherEl.style.cursor = "pointer";
177896df7d3eSAtari911            weatherEl.title = "Click for local weather";
177996df7d3eSAtari911            weatherEl.addEventListener("click", requestLocalWeather);
178096df7d3eSAtari911        }
178196df7d3eSAtari911    }, 100);
178296df7d3eSAtari911
17831d05cddcSAtari911    // WMO Weather interpretation codes
17841d05cddcSAtari911    function getWeatherIcon(code) {
17851d05cddcSAtari911        const icons = {
17861d05cddcSAtari911            0: "☀️",   // Clear sky
17871d05cddcSAtari911            1: "��️",   // Mainly clear
17881d05cddcSAtari911            2: "⛅",   // Partly cloudy
17891d05cddcSAtari911            3: "☁️",   // Overcast
17901d05cddcSAtari911            45: "��️",  // Fog
17911d05cddcSAtari911            48: "��️",  // Depositing rime fog
17921d05cddcSAtari911            51: "��️",  // Light drizzle
17931d05cddcSAtari911            53: "��️",  // Moderate drizzle
17941d05cddcSAtari911            55: "��️",  // Dense drizzle
17951d05cddcSAtari911            61: "��️",  // Slight rain
17961d05cddcSAtari911            63: "��️",  // Moderate rain
17971d05cddcSAtari911            65: "⛈️",  // Heavy rain
17981d05cddcSAtari911            71: "��️",  // Slight snow
17991d05cddcSAtari911            73: "��️",  // Moderate snow
18001d05cddcSAtari911            75: "❄️",  // Heavy snow
18011d05cddcSAtari911            77: "��️",  // Snow grains
18021d05cddcSAtari911            80: "��️",  // Slight rain showers
18031d05cddcSAtari911            81: "��️",  // Moderate rain showers
18041d05cddcSAtari911            82: "⛈️",  // Violent rain showers
18051d05cddcSAtari911            85: "��️",  // Slight snow showers
18061d05cddcSAtari911            86: "❄️",  // Heavy snow showers
18071d05cddcSAtari911            95: "⛈️",  // Thunderstorm
18081d05cddcSAtari911            96: "⛈️",  // Thunderstorm with slight hail
18091d05cddcSAtari911            99: "⛈️"   // Thunderstorm with heavy hail
18101d05cddcSAtari911        };
18111d05cddcSAtari911        return icons[code] || "��️";
18121d05cddcSAtari911    }
18131d05cddcSAtari911
18141d05cddcSAtari911    // Update weather immediately and every 10 minutes
18151d05cddcSAtari911    updateWeather();
18161d05cddcSAtari911    setInterval(updateWeather, 600000);
18171d05cddcSAtari911})();
18181d05cddcSAtari911</script>';
18191d05cddcSAtari911        }
182019378907SAtari911
182119378907SAtari911        if (empty($allEvents)) {
1822e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-empty">';
1823e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText);
1824e3a9f44cSAtari911            if ($namespace) {
1825e3a9f44cSAtari911                $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>';
182687ac9bf3SAtari911            }
1827e3a9f44cSAtari911            $html .= '</div>';
1828e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-body">No events</div>';
1829e3a9f44cSAtari911            $html .= '</div>';
1830e3a9f44cSAtari911        } else {
1831e3a9f44cSAtari911            // Calculate today and tomorrow's dates for highlighting
18321d05cddcSAtari911            $todayStr = date('Y-m-d');
1833e3a9f44cSAtari911            $tomorrow = date('Y-m-d', strtotime('+1 day'));
1834e3a9f44cSAtari911
1835e3a9f44cSAtari911            foreach ($allEvents as $dateKey => $dayEvents) {
1836e3a9f44cSAtari911                $dateObj = new DateTime($dateKey);
1837e3a9f44cSAtari911                $displayDate = $dateObj->format('D, M j');
1838e3a9f44cSAtari911
18391d05cddcSAtari911                // Check if this date is today or tomorrow or past
1840e3a9f44cSAtari911                // Enable highlighting for sidebar mode AND range modes (day, week, month)
1841e3a9f44cSAtari911                $enableHighlighting = $sidebar || !empty($range);
18421d05cddcSAtari911                $isToday = $enableHighlighting && ($dateKey === $todayStr);
1843e3a9f44cSAtari911                $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow);
18441d05cddcSAtari911                $isPast = $dateKey < $todayStr;
184519378907SAtari911
184619378907SAtari911                foreach ($dayEvents as $event) {
18471d05cddcSAtari911                    // Check if this is a task and if it's completed
18481d05cddcSAtari911                    $isTask = !empty($event['isTask']);
18491d05cddcSAtari911                    $completed = !empty($event['completed']);
18501d05cddcSAtari911
18511d05cddcSAtari911                    // ALWAYS skip completed tasks UNLESS showchecked is explicitly set
18521d05cddcSAtari911                    if (!$showchecked && $isTask && $completed) {
1853e3a9f44cSAtari911                        continue;
1854e3a9f44cSAtari911                    }
185519378907SAtari911
18561d05cddcSAtari911                    // Skip past events that are NOT tasks (only show past due tasks from the past)
18571d05cddcSAtari911                    if ($isPast && !$isTask) {
18581d05cddcSAtari911                        continue;
18591d05cddcSAtari911                    }
18601d05cddcSAtari911
18611d05cddcSAtari911                    // Determine if task is past due (past date, is task, not completed)
18621d05cddcSAtari911                    $isPastDue = $isPast && $isTask && !$completed;
18631d05cddcSAtari911
1864e3a9f44cSAtari911                    // Line 1: Header (Title, Time, Date, Namespace)
1865e3a9f44cSAtari911                    $todayClass = $isToday ? ' eventlist-simple-today' : '';
1866e3a9f44cSAtari911                    $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
18671d05cddcSAtari911                    $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : '';
18681d05cddcSAtari911                    $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">';
1869e3a9f44cSAtari911                    $html .= '<div class="eventlist-simple-header">';
1870e3a9f44cSAtari911
1871e3a9f44cSAtari911                    // Title
1872e3a9f44cSAtari911                    $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>';
1873e3a9f44cSAtari911
1874e3a9f44cSAtari911                    // Time (12-hour format)
1875e3a9f44cSAtari911                    if (!empty($event['time'])) {
1876e3a9f44cSAtari911                        $timeParts = explode(':', $event['time']);
187787ac9bf3SAtari911                        if (count($timeParts) === 2) {
187887ac9bf3SAtari911                            $hour = (int)$timeParts[0];
187987ac9bf3SAtari911                            $minute = $timeParts[1];
188087ac9bf3SAtari911                            $ampm = $hour >= 12 ? 'PM' : 'AM';
1881e3a9f44cSAtari911                            $hour = $hour % 12 ?: 12;
188287ac9bf3SAtari911                            $displayTime = $hour . ':' . $minute . ' ' . $ampm;
1883e3a9f44cSAtari911                            $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>';
188419378907SAtari911                        }
188587ac9bf3SAtari911                    }
188687ac9bf3SAtari911
1887e3a9f44cSAtari911                    // Date
1888e3a9f44cSAtari911                    $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>';
1889e3a9f44cSAtari911
18901d05cddcSAtari911                    // Badge: PAST DUE, TODAY, or nothing
18911d05cddcSAtari911                    if ($isPastDue) {
18927e8ea635SAtari911                        $html .= ' <span class="eventlist-simple-pastdue-badge" style="background:' . $themeStyles['pastdue_color'] . ' !important; color:white !important; -webkit-text-fill-color:white !important;">PAST DUE</span>';
18931d05cddcSAtari911                    } elseif ($isToday) {
18947e8ea635SAtari911                        $html .= ' <span class="eventlist-simple-today-badge" style="background:' . $themeStyles['border'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;">TODAY</span>';
189587ac9bf3SAtari911                    }
1896e3a9f44cSAtari911
1897e3a9f44cSAtari911                    // Namespace badge (show individual event's namespace)
1898e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
1899e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
1900e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading
190119378907SAtari911                    }
1902e3a9f44cSAtari911                    if ($eventNamespace) {
1903e3a9f44cSAtari911                        $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>';
1904e3a9f44cSAtari911                    }
1905e3a9f44cSAtari911
1906e3a9f44cSAtari911                    $html .= '</div>'; // header
1907e3a9f44cSAtari911
1908e3a9f44cSAtari911                    // Line 2: Body (Description only) - only show if description exists
1909e3a9f44cSAtari911                    if (!empty($event['description'])) {
1910e3a9f44cSAtari911                        $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>';
1911e3a9f44cSAtari911                    }
1912e3a9f44cSAtari911
1913e3a9f44cSAtari911                    $html .= '</div>'; // item
191419378907SAtari911                }
191519378907SAtari911            }
191687ac9bf3SAtari911        }
191719378907SAtari911
1918e3a9f44cSAtari911        $html .= '</div>'; // eventlist-simple
191919378907SAtari911
192019378907SAtari911        return $html;
192119378907SAtari911    }
192219378907SAtari911
19230c3b6e81SAtari911    private function renderEventDialog($calId, $namespace, $theme = null) {
19249ccd446eSAtari911        // Get theme for dialog
19250c3b6e81SAtari911        if ($theme === null) {
19269ccd446eSAtari911            $theme = $this->getSidebarTheme();
19270c3b6e81SAtari911        }
19289ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
19299ccd446eSAtari911
193019378907SAtari911        $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">';
193119378907SAtari911        $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>';
193219378907SAtari911
19339ccd446eSAtari911        // Draggable dialog with theme
193419378907SAtari911        $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">';
193519378907SAtari911
193619378907SAtari911        // Header with drag handle and close button
193719378907SAtari911        $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">';
1938da206178SAtari911        $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>';
193919378907SAtari911        $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>';
194019378907SAtari911        $html .= '</div>';
194119378907SAtari911
194219378907SAtari911        // Form content
194319378907SAtari911        $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">';
194419378907SAtari911
194519378907SAtari911        // Hidden ID field
194619378907SAtari911        $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">';
194719378907SAtari911
19481d05cddcSAtari911        // 1. TITLE
19491d05cddcSAtari911        $html .= '<div class="form-field">';
1950da206178SAtari911        $html .= '<label class="field-label">�� Title</label>';
1951da206178SAtari911        $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek input-compact" placeholder="Event or task title...">';
195219378907SAtari911        $html .= '</div>';
195319378907SAtari911
19541d05cddcSAtari911        // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching)
19551d05cddcSAtari911        $html .= '<div class="form-field">';
1956da206178SAtari911        $html .= '<label class="field-label">�� Namespace</label>';
19571d05cddcSAtari911
19581d05cddcSAtari911        // Hidden field to store actual selected namespace
19591d05cddcSAtari911        $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">';
19601d05cddcSAtari911
19611d05cddcSAtari911        // Searchable input
19621d05cddcSAtari911        $html .= '<div class="namespace-search-wrapper">';
1963da206178SAtari911        $html .= '<input type="text" id="event-namespace-search-' . $calId . '" class="input-sleek input-compact namespace-search-input" placeholder="Type to search or leave empty for default..." autocomplete="off">';
19641d05cddcSAtari911        $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>';
19651d05cddcSAtari911        $html .= '</div>';
19661d05cddcSAtari911
19671d05cddcSAtari911        // Store namespaces as JSON for JavaScript
19681d05cddcSAtari911        $allNamespaces = $this->getAllNamespaces();
19691d05cddcSAtari911        $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>';
19701d05cddcSAtari911
19711d05cddcSAtari911        $html .= '</div>';
19721d05cddcSAtari911
19731d05cddcSAtari911        // 2. DESCRIPTION
19741d05cddcSAtari911        $html .= '<div class="form-field">';
1975da206178SAtari911        $html .= '<label class="field-label">�� Description</label>';
1976da206178SAtari911        $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="2" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>';
19771d05cddcSAtari911        $html .= '</div>';
19781d05cddcSAtari911
19791d05cddcSAtari911        // 3. START DATE - END DATE (inline)
198019378907SAtari911        $html .= '<div class="form-row-group">';
198119378907SAtari911
19821d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
1983da206178SAtari911        $html .= '<label class="field-label-compact">�� Start Date</label>';
1984815440faSAtari911        $html .= '<div class="date-picker-wrapper">';
1985815440faSAtari911        $html .= '<input type="hidden" id="event-date-' . $calId . '" name="date" required value="">';
1986815440faSAtari911        $html .= '<button type="button" class="custom-date-picker input-sleek input-compact" id="date-picker-btn-' . $calId . '" data-target="event-date-' . $calId . '" data-type="start">';
1987815440faSAtari911        $html .= '<span class="date-display">Select date</span>';
1988815440faSAtari911        $html .= '<span class="date-arrow">▼</span>';
1989815440faSAtari911        $html .= '</button>';
1990815440faSAtari911        $html .= '<div class="date-dropdown" id="date-dropdown-' . $calId . '"></div>';
1991815440faSAtari911        $html .= '</div>';
199219378907SAtari911        $html .= '</div>';
199319378907SAtari911
19941d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
1995da206178SAtari911        $html .= '<label class="field-label-compact">�� End Date</label>';
1996815440faSAtari911        $html .= '<div class="date-picker-wrapper">';
1997815440faSAtari911        $html .= '<input type="hidden" id="event-end-date-' . $calId . '" name="endDate" value="">';
1998815440faSAtari911        $html .= '<button type="button" class="custom-date-picker input-sleek input-compact" id="end-date-picker-btn-' . $calId . '" data-target="event-end-date-' . $calId . '" data-type="end">';
1999815440faSAtari911        $html .= '<span class="date-display">Optional</span>';
2000815440faSAtari911        $html .= '<span class="date-arrow">▼</span>';
2001815440faSAtari911        $html .= '</button>';
2002815440faSAtari911        $html .= '<div class="date-dropdown" id="end-date-dropdown-' . $calId . '"></div>';
2003815440faSAtari911        $html .= '</div>';
200419378907SAtari911        $html .= '</div>';
200519378907SAtari911
20061d05cddcSAtari911        $html .= '</div>'; // End row
200719378907SAtari911
20081d05cddcSAtari911        // 4. IS REPEATING CHECKBOX
20091d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
20101d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
201187ac9bf3SAtari911        $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">';
2012da206178SAtari911        $html .= '<span>�� Repeating Event</span>';
201387ac9bf3SAtari911        $html .= '</label>';
201487ac9bf3SAtari911        $html .= '</div>';
201587ac9bf3SAtari911
20161d05cddcSAtari911        // Recurring options (shown when checkbox is checked)
201796df7d3eSAtari911        $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none; border:1px solid var(--border-color, #333); border-radius:4px; padding:8px; margin:4px 0; background:var(--background-alt, rgba(0,0,0,0.2));">';
201887ac9bf3SAtari911
201996df7d3eSAtari911        // Row 1: Repeat every [N] [period]
202096df7d3eSAtari911        $html .= '<div class="form-row-group" style="margin-bottom:6px;">';
20211d05cddcSAtari911
202296df7d3eSAtari911        $html .= '<div class="form-field" style="flex:0 0 auto; min-width:0;">';
2023da206178SAtari911        $html .= '<label class="field-label-compact">Repeat every</label>';
202496df7d3eSAtari911        $html .= '<input type="number" id="event-recurrence-interval-' . $calId . '" name="recurrenceInterval" class="input-sleek input-compact" value="1" min="1" max="99" style="width:50px;">';
202596df7d3eSAtari911        $html .= '</div>';
202696df7d3eSAtari911
202796df7d3eSAtari911        $html .= '<div class="form-field" style="flex:1; min-width:0;">';
202896df7d3eSAtari911        $html .= '<label class="field-label-compact">&nbsp;</label>';
202996df7d3eSAtari911        $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek input-compact" onchange="updateRecurrenceOptions(\'' . $calId . '\')">';
2030da206178SAtari911        $html .= '<option value="daily">Day(s)</option>';
2031da206178SAtari911        $html .= '<option value="weekly">Week(s)</option>';
2032da206178SAtari911        $html .= '<option value="monthly">Month(s)</option>';
2033da206178SAtari911        $html .= '<option value="yearly">Year(s)</option>';
203487ac9bf3SAtari911        $html .= '</select>';
203587ac9bf3SAtari911        $html .= '</div>';
203687ac9bf3SAtari911
203796df7d3eSAtari911        $html .= '</div>'; // End row 1
203896df7d3eSAtari911
203996df7d3eSAtari911        // Row 2: Weekly options - day of week checkboxes
204096df7d3eSAtari911        $html .= '<div id="weekly-options-' . $calId . '" class="weekly-options" style="display:none; margin-bottom:6px;">';
2041da206178SAtari911        $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">On these days:</label>';
204296df7d3eSAtari911        $html .= '<div style="display:flex; flex-wrap:wrap; gap:2px;">';
2043da206178SAtari911        $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
204496df7d3eSAtari911        foreach ($dayNames as $idx => $day) {
204596df7d3eSAtari911            $html .= '<label style="display:inline-flex; align-items:center; padding:2px 6px; background:var(--cell-bg, #1a1a1a); border:1px solid var(--border-color, #333); border-radius:3px; cursor:pointer; font-size:10px;">';
204696df7d3eSAtari911            $html .= '<input type="checkbox" name="weekDays[]" value="' . $idx . '" style="margin-right:3px; width:12px; height:12px;">';
204796df7d3eSAtari911            $html .= '<span>' . $day . '</span>';
204896df7d3eSAtari911            $html .= '</label>';
204996df7d3eSAtari911        }
205096df7d3eSAtari911        $html .= '</div>';
205196df7d3eSAtari911        $html .= '</div>'; // End weekly options
205296df7d3eSAtari911
205396df7d3eSAtari911        // Row 3: Monthly options - day of month OR ordinal weekday
205496df7d3eSAtari911        $html .= '<div id="monthly-options-' . $calId . '" class="monthly-options" style="display:none; margin-bottom:6px;">';
2055da206178SAtari911        $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">Repeat on:</label>';
205696df7d3eSAtari911
205796df7d3eSAtari911        // Radio: Day of month vs Ordinal weekday
205896df7d3eSAtari911        $html .= '<div style="margin-bottom:6px;">';
205996df7d3eSAtari911        $html .= '<label style="display:inline-flex; align-items:center; margin-right:12px; cursor:pointer; font-size:11px;">';
206096df7d3eSAtari911        $html .= '<input type="radio" name="monthlyType" value="dayOfMonth" checked onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
2061da206178SAtari911        $html .= 'Day of month';
206296df7d3eSAtari911        $html .= '</label>';
206396df7d3eSAtari911        $html .= '<label style="display:inline-flex; align-items:center; cursor:pointer; font-size:11px;">';
206496df7d3eSAtari911        $html .= '<input type="radio" name="monthlyType" value="ordinalWeekday" onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
2065da206178SAtari911        $html .= 'Weekday pattern';
206696df7d3eSAtari911        $html .= '</label>';
206787ac9bf3SAtari911        $html .= '</div>';
206887ac9bf3SAtari911
206996df7d3eSAtari911        // Day of month input (shown by default)
207096df7d3eSAtari911        $html .= '<div id="monthly-day-' . $calId . '" style="display:flex; align-items:center; gap:6px;">';
2071da206178SAtari911        $html .= '<span style="font-size:11px;">Day</span>';
207296df7d3eSAtari911        $html .= '<input type="number" id="event-month-day-' . $calId . '" name="monthDay" class="input-sleek input-compact" value="1" min="1" max="31" style="width:50px;">';
2073da206178SAtari911        $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>';
207496df7d3eSAtari911        $html .= '</div>';
207596df7d3eSAtari911
207696df7d3eSAtari911        // Ordinal weekday (hidden by default)
207796df7d3eSAtari911        $html .= '<div id="monthly-ordinal-' . $calId . '" style="display:none;">';
207896df7d3eSAtari911        $html .= '<div style="display:flex; align-items:center; gap:4px; flex-wrap:wrap;">';
207996df7d3eSAtari911        $html .= '<select id="event-ordinal-' . $calId . '" name="ordinalWeek" class="input-sleek input-compact" style="width:auto;">';
2080da206178SAtari911        $html .= '<option value="1">First</option>';
2081da206178SAtari911        $html .= '<option value="2">Second</option>';
2082da206178SAtari911        $html .= '<option value="3">Third</option>';
2083da206178SAtari911        $html .= '<option value="4">Fourth</option>';
2084da206178SAtari911        $html .= '<option value="5">Fifth</option>';
2085da206178SAtari911        $html .= '<option value="-1">Last</option>';
208696df7d3eSAtari911        $html .= '</select>';
208796df7d3eSAtari911        $html .= '<select id="event-ordinal-day-' . $calId . '" name="ordinalDay" class="input-sleek input-compact" style="width:auto;">';
2088da206178SAtari911        $html .= '<option value="0">Sunday</option>';
2089da206178SAtari911        $html .= '<option value="1">Monday</option>';
2090da206178SAtari911        $html .= '<option value="2">Tuesday</option>';
2091da206178SAtari911        $html .= '<option value="3">Wednesday</option>';
2092da206178SAtari911        $html .= '<option value="4">Thursday</option>';
2093da206178SAtari911        $html .= '<option value="5">Friday</option>';
2094da206178SAtari911        $html .= '<option value="6">Saturday</option>';
209596df7d3eSAtari911        $html .= '</select>';
2096da206178SAtari911        $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>';
209796df7d3eSAtari911        $html .= '</div>';
209896df7d3eSAtari911        $html .= '</div>';
209996df7d3eSAtari911
210096df7d3eSAtari911        $html .= '</div>'; // End monthly options
210196df7d3eSAtari911
210296df7d3eSAtari911        // Row 4: End date
210396df7d3eSAtari911        $html .= '<div class="form-row-group">';
210496df7d3eSAtari911        $html .= '<div class="form-field">';
2105da206178SAtari911        $html .= '<label class="field-label-compact">Repeat Until (optional)</label>';
210696df7d3eSAtari911        $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">';
2107da206178SAtari911        $html .= '<div style="font-size:9px; color:var(--text-dim, #666); margin-top:2px;">Leave empty for 1 year of events</div>';
210896df7d3eSAtari911        $html .= '</div>';
210996df7d3eSAtari911        $html .= '</div>'; // End row 4
211096df7d3eSAtari911
21111d05cddcSAtari911        $html .= '</div>'; // End recurring options
211287ac9bf3SAtari911
21131d05cddcSAtari911        // 5. TIME (Start & End) - COLOR (inline)
21141d05cddcSAtari911        $html .= '<div class="form-row-group">';
21151d05cddcSAtari911
21161d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
2117da206178SAtari911        $html .= '<label class="field-label-compact">�� Start Time</label>';
2118da206178SAtari911        $html .= '<div class="time-picker-wrapper">';
2119815440faSAtari911        // Custom time picker button instead of native select
2120815440faSAtari911        $html .= '<input type="hidden" id="event-time-' . $calId . '" name="time" value="">';
2121815440faSAtari911        $html .= '<button type="button" class="custom-time-picker input-sleek input-compact" id="time-picker-btn-' . $calId . '" data-target="event-time-' . $calId . '" data-type="start">';
2122815440faSAtari911        $html .= '<span class="time-display">All day</span>';
2123815440faSAtari911        $html .= '<span class="time-arrow">▼</span>';
2124815440faSAtari911        $html .= '</button>';
2125815440faSAtari911        $html .= '<div class="time-dropdown" id="time-dropdown-' . $calId . '"></div>';
212619378907SAtari911        $html .= '</div>';
2127da206178SAtari911        $html .= '</div>';
212819378907SAtari911
21291d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
2130da206178SAtari911        $html .= '<label class="field-label-compact">�� End Time</label>';
2131da206178SAtari911        $html .= '<div class="time-picker-wrapper">';
2132815440faSAtari911        // Custom end time picker
2133815440faSAtari911        $html .= '<input type="hidden" id="event-end-time-' . $calId . '" name="endTime" value="">';
2134815440faSAtari911        $html .= '<button type="button" class="custom-time-picker input-sleek input-compact" id="end-time-picker-btn-' . $calId . '" data-target="event-end-time-' . $calId . '" data-type="end" disabled>';
2135815440faSAtari911        $html .= '<span class="time-display">Same as start</span>';
2136815440faSAtari911        $html .= '<span class="time-arrow">▼</span>';
2137815440faSAtari911        $html .= '</button>';
2138815440faSAtari911        $html .= '<div class="time-dropdown" id="end-time-dropdown-' . $calId . '"></div>';
213919378907SAtari911        $html .= '</div>';
2140da206178SAtari911        $html .= '</div>';
214119378907SAtari911
21421d05cddcSAtari911        $html .= '</div>'; // End row
21431d05cddcSAtari911
21441d05cddcSAtari911        // Color field (new row)
21451d05cddcSAtari911        $html .= '<div class="form-row-group">';
21461d05cddcSAtari911
21471d05cddcSAtari911        $html .= '<div class="form-field form-field-full">';
2148da206178SAtari911        $html .= '<label class="field-label-compact">�� Color</label>';
21491d05cddcSAtari911        $html .= '<div class="color-picker-wrapper">';
21501d05cddcSAtari911        $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">';
2151da206178SAtari911        $html .= '<option value="#3498db" style="background:#3498db;color:white">�� Blue</option>';
2152da206178SAtari911        $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white">�� Green</option>';
2153da206178SAtari911        $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white">�� Red</option>';
2154da206178SAtari911        $html .= '<option value="#f39c12" style="background:#f39c12;color:white">�� Orange</option>';
2155da206178SAtari911        $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white">�� Purple</option>';
2156da206178SAtari911        $html .= '<option value="#e91e63" style="background:#e91e63;color:white">�� Pink</option>';
2157da206178SAtari911        $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white">�� Teal</option>';
2158da206178SAtari911        $html .= '<option value="custom">�� Custom...</option>';
21591d05cddcSAtari911        $html .= '</select>';
21601d05cddcSAtari911        $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">';
21611d05cddcSAtari911        $html .= '</div>';
216219378907SAtari911        $html .= '</div>';
216319378907SAtari911
21641d05cddcSAtari911        $html .= '</div>'; // End row
21651d05cddcSAtari911
21661d05cddcSAtari911        // Task checkbox
21671d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
21681d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
21691d05cddcSAtari911        $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">';
2170da206178SAtari911        $html .= '<span>�� This is a task (can be checked off)</span>';
21711d05cddcSAtari911        $html .= '</label>';
217219378907SAtari911        $html .= '</div>';
217319378907SAtari911
217419378907SAtari911        // Action buttons
217519378907SAtari911        $html .= '<div class="dialog-actions-sleek">';
2176da206178SAtari911        $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>';
2177da206178SAtari911        $html .= '<button type="submit" class="btn-sleek btn-save-sleek">�� Save</button>';
217819378907SAtari911        $html .= '</div>';
217919378907SAtari911
218019378907SAtari911        $html .= '</form>';
218119378907SAtari911        $html .= '</div>';
218219378907SAtari911        $html .= '</div>';
218319378907SAtari911
218419378907SAtari911        return $html;
218519378907SAtari911    }
218619378907SAtari911
21879ccd446eSAtari911    private function renderMonthPicker($calId, $year, $month, $namespace, $theme = 'matrix', $themeStyles = null) {
21889ccd446eSAtari911        // Fallback to default theme if not provided
21899ccd446eSAtari911        if ($themeStyles === null) {
21909ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
21919ccd446eSAtari911        }
21929ccd446eSAtari911
21939ccd446eSAtari911        $themeClass = 'calendar-theme-' . $theme;
21949ccd446eSAtari911
21959ccd446eSAtari911        $html = '<div class="month-picker-overlay ' . $themeClass . '" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">';
219687ac9bf3SAtari911        $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">';
219787ac9bf3SAtari911        $html .= '<h4>Jump to Month</h4>';
219887ac9bf3SAtari911
219987ac9bf3SAtari911        $html .= '<div class="month-picker-selects">';
220087ac9bf3SAtari911        $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">';
220187ac9bf3SAtari911        $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
220287ac9bf3SAtari911        for ($m = 1; $m <= 12; $m++) {
220387ac9bf3SAtari911            $selected = ($m == $month) ? ' selected' : '';
220487ac9bf3SAtari911            $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>';
220587ac9bf3SAtari911        }
220687ac9bf3SAtari911        $html .= '</select>';
220787ac9bf3SAtari911
220887ac9bf3SAtari911        $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">';
220987ac9bf3SAtari911        $currentYear = (int)date('Y');
221087ac9bf3SAtari911        for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) {
221187ac9bf3SAtari911            $selected = ($y == $year) ? ' selected' : '';
221287ac9bf3SAtari911            $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>';
221387ac9bf3SAtari911        }
221487ac9bf3SAtari911        $html .= '</select>';
221587ac9bf3SAtari911        $html .= '</div>';
221687ac9bf3SAtari911
221787ac9bf3SAtari911        $html .= '<div class="month-picker-actions">';
221887ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>';
221987ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>';
222087ac9bf3SAtari911        $html .= '</div>';
222187ac9bf3SAtari911
222287ac9bf3SAtari911        $html .= '</div>';
222387ac9bf3SAtari911        $html .= '</div>';
222487ac9bf3SAtari911
222587ac9bf3SAtari911        return $html;
222687ac9bf3SAtari911    }
222787ac9bf3SAtari911
22289ccd446eSAtari911    private function renderDescription($description, $themeStyles = null) {
222919378907SAtari911        if (empty($description)) {
223019378907SAtari911            return '';
223119378907SAtari911        }
223219378907SAtari911
22339ccd446eSAtari911        // Get theme for link colors if not provided
22349ccd446eSAtari911        if ($themeStyles === null) {
22359ccd446eSAtari911            $theme = $this->getSidebarTheme();
22369ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
22379ccd446eSAtari911        }
22389ccd446eSAtari911
22399ccd446eSAtari911        $linkColor = '';
22409ccd446eSAtari911        $linkStyle = ' class="cal-link"';
22419ccd446eSAtari911
2242e3a9f44cSAtari911        // Token-based parsing to avoid escaping issues
2243e3a9f44cSAtari911        $rendered = $description;
2244e3a9f44cSAtari911        $tokens = array();
2245e3a9f44cSAtari911        $tokenIndex = 0;
224619378907SAtari911
2247e3a9f44cSAtari911        // Convert DokuWiki image syntax {{image.jpg}} to tokens
2248e3a9f44cSAtari911        $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/';
2249e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2250e3a9f44cSAtari911        foreach ($matches as $match) {
2251e3a9f44cSAtari911            $imagePath = trim($match[1]);
2252e3a9f44cSAtari911            $alt = isset($match[2]) ? trim($match[2]) : '';
225319378907SAtari911
2254e3a9f44cSAtari911            // Handle external URLs
225519378907SAtari911            if (preg_match('/^https?:\/\//', $imagePath)) {
2256e3a9f44cSAtari911                $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
2257e3a9f44cSAtari911            } else {
225819378907SAtari911                // Handle internal DokuWiki images
225919378907SAtari911                $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath);
2260e3a9f44cSAtari911                $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
2261e3a9f44cSAtari911            }
226219378907SAtari911
2263e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2264e3a9f44cSAtari911            $tokens[$tokenIndex] = $imageHtml;
2265e3a9f44cSAtari911            $tokenIndex++;
2266e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2267e3a9f44cSAtari911        }
2268e3a9f44cSAtari911
2269e3a9f44cSAtari911        // Convert DokuWiki link syntax [[link|text]] to tokens
2270e3a9f44cSAtari911        $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/';
2271e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2272e3a9f44cSAtari911        foreach ($matches as $match) {
2273e3a9f44cSAtari911            $link = trim($match[1]);
2274e3a9f44cSAtari911            $text = isset($match[2]) ? trim($match[2]) : $link;
227519378907SAtari911
227619378907SAtari911            // Handle external URLs
227719378907SAtari911            if (preg_match('/^https?:\/\//', $link)) {
22789ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2279e3a9f44cSAtari911            } else {
228087ac9bf3SAtari911                // Handle internal DokuWiki links with section anchors
228187ac9bf3SAtari911                $parts = explode('#', $link, 2);
228287ac9bf3SAtari911                $pagePart = $parts[0];
228387ac9bf3SAtari911                $sectionPart = isset($parts[1]) ? '#' . $parts[1] : '';
228487ac9bf3SAtari911
228587ac9bf3SAtari911                $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart;
22869ccd446eSAtari911                $linkHtml = '<a href="' . $wikiUrl . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
228719378907SAtari911            }
228819378907SAtari911
2289e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2290e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
2291e3a9f44cSAtari911            $tokenIndex++;
2292e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2293e3a9f44cSAtari911        }
229419378907SAtari911
2295e3a9f44cSAtari911        // Convert markdown-style links [text](url) to tokens
2296e3a9f44cSAtari911        $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
2297e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2298e3a9f44cSAtari911        foreach ($matches as $match) {
2299e3a9f44cSAtari911            $text = trim($match[1]);
2300e3a9f44cSAtari911            $url = trim($match[2]);
230119378907SAtari911
2302e3a9f44cSAtari911            if (preg_match('/^https?:\/\//', $url)) {
23039ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2304e3a9f44cSAtari911            } else {
23059ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2306e3a9f44cSAtari911            }
2307e3a9f44cSAtari911
2308e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2309e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
2310e3a9f44cSAtari911            $tokenIndex++;
2311e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2312e3a9f44cSAtari911        }
2313e3a9f44cSAtari911
2314e3a9f44cSAtari911        // Convert plain URLs to tokens
2315e3a9f44cSAtari911        $pattern = '/(https?:\/\/[^\s<]+)/';
2316e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2317e3a9f44cSAtari911        foreach ($matches as $match) {
2318e3a9f44cSAtari911            $url = $match[1];
23199ccd446eSAtari911            $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($url) . '</a>';
2320e3a9f44cSAtari911
2321e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2322e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
2323e3a9f44cSAtari911            $tokenIndex++;
2324e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2325e3a9f44cSAtari911        }
2326e3a9f44cSAtari911
2327e3a9f44cSAtari911        // NOW escape HTML (tokens are protected)
2328e3a9f44cSAtari911        $rendered = htmlspecialchars($rendered);
2329e3a9f44cSAtari911
2330e3a9f44cSAtari911        // Convert newlines to <br>
2331e3a9f44cSAtari911        $rendered = nl2br($rendered);
2332e3a9f44cSAtari911
2333e3a9f44cSAtari911        // DokuWiki text formatting
2334e3a9f44cSAtari911        // Bold: **text** or __text__
23359ccd446eSAtari911        $boldStyle = '';
2336e3a9f44cSAtari911        $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered);
2337e3a9f44cSAtari911        $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered);
2338e3a9f44cSAtari911
2339e3a9f44cSAtari911        // Italic: //text//
2340e3a9f44cSAtari911        $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered);
2341e3a9f44cSAtari911
2342e3a9f44cSAtari911        // Strikethrough: <del>text</del>
2343e3a9f44cSAtari911        $rendered = preg_replace('/&lt;del&gt;(.+?)&lt;\/del&gt;/', '<del>$1</del>', $rendered);
2344e3a9f44cSAtari911
2345e3a9f44cSAtari911        // Monospace: ''text''
2346e3a9f44cSAtari911        $rendered = preg_replace('/&#039;&#039;(.+?)&#039;&#039;/', '<code>$1</code>', $rendered);
2347e3a9f44cSAtari911
2348e3a9f44cSAtari911        // Subscript: <sub>text</sub>
2349e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sub&gt;(.+?)&lt;\/sub&gt;/', '<sub>$1</sub>', $rendered);
2350e3a9f44cSAtari911
2351e3a9f44cSAtari911        // Superscript: <sup>text</sup>
2352e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sup&gt;(.+?)&lt;\/sup&gt;/', '<sup>$1</sup>', $rendered);
2353e3a9f44cSAtari911
2354e3a9f44cSAtari911        // Restore tokens
2355e3a9f44cSAtari911        foreach ($tokens as $i => $html) {
2356e3a9f44cSAtari911            $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered);
2357e3a9f44cSAtari911        }
235819378907SAtari911
235919378907SAtari911        return $rendered;
236019378907SAtari911    }
236119378907SAtari911
236219378907SAtari911    private function loadEvents($namespace, $year, $month) {
23632866e827SAtari911        $dataDir = $this->metaDir();
236419378907SAtari911        if ($namespace) {
236519378907SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
236619378907SAtari911        }
236719378907SAtari911        $dataDir .= 'calendar/';
236819378907SAtari911
236919378907SAtari911        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
237019378907SAtari911
237119378907SAtari911        if (file_exists($eventFile)) {
237219378907SAtari911            $json = file_get_contents($eventFile);
237319378907SAtari911            return json_decode($json, true);
237419378907SAtari911        }
237519378907SAtari911
237619378907SAtari911        return array();
237719378907SAtari911    }
2378e3a9f44cSAtari911
23792866e827SAtari911    private function loadEventsMultiNamespace($namespaces, $year, $month, $excludeList = []) {
2380e3a9f44cSAtari911        // Check for wildcard pattern (namespace:*)
2381e3a9f44cSAtari911        if (preg_match('/^(.+):\*$/', $namespaces, $matches)) {
2382e3a9f44cSAtari911            $baseNamespace = $matches[1];
23832866e827SAtari911            return $this->loadEventsWildcard($baseNamespace, $year, $month, $excludeList);
2384e3a9f44cSAtari911        }
2385e3a9f44cSAtari911
2386e3a9f44cSAtari911        // Check for root wildcard (just *)
2387e3a9f44cSAtari911        if ($namespaces === '*') {
23882866e827SAtari911            return $this->loadEventsWildcard('', $year, $month, $excludeList);
2389e3a9f44cSAtari911        }
2390e3a9f44cSAtari911
2391e3a9f44cSAtari911        // Parse namespace list (semicolon separated)
2392e3a9f44cSAtari911        // e.g., "team:projects;personal;work:tasks" = three namespaces
2393e3a9f44cSAtari911        $namespaceList = array_map('trim', explode(';', $namespaces));
2394e3a9f44cSAtari911
2395e3a9f44cSAtari911        // Load events from all namespaces
2396e3a9f44cSAtari911        $allEvents = array();
2397e3a9f44cSAtari911        foreach ($namespaceList as $ns) {
2398e3a9f44cSAtari911            $ns = trim($ns);
2399e3a9f44cSAtari911            if (empty($ns)) continue;
2400e3a9f44cSAtari911
24012866e827SAtari911            // Skip excluded namespaces
24022866e827SAtari911            if ($this->isNamespaceExcluded($ns, $excludeList)) continue;
24032866e827SAtari911
24042866e827SAtari911            // ACL check: skip namespaces user cannot read
24052866e827SAtari911            if (!$this->checkNamespaceRead($ns)) continue;
24062866e827SAtari911
2407e3a9f44cSAtari911            $events = $this->loadEvents($ns, $year, $month);
2408e3a9f44cSAtari911
2409e3a9f44cSAtari911            // Add namespace tag to each event
2410e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
2411e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
2412e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
2413e3a9f44cSAtari911                }
2414e3a9f44cSAtari911                foreach ($dayEvents as $event) {
2415e3a9f44cSAtari911                    $event['_namespace'] = $ns;
2416e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
2417e3a9f44cSAtari911                }
2418e3a9f44cSAtari911            }
2419e3a9f44cSAtari911        }
2420e3a9f44cSAtari911
2421e3a9f44cSAtari911        return $allEvents;
2422e3a9f44cSAtari911    }
2423e3a9f44cSAtari911
24242866e827SAtari911    private function loadEventsWildcard($baseNamespace, $year, $month, $excludeList = []) {
24252866e827SAtari911        $metaDir = $this->metaDir();
24262866e827SAtari911        $dataDir = $metaDir;
2427e3a9f44cSAtari911        if ($baseNamespace) {
2428e3a9f44cSAtari911            $dataDir .= str_replace(':', '/', $baseNamespace) . '/';
2429e3a9f44cSAtari911        }
2430e3a9f44cSAtari911
2431e3a9f44cSAtari911        $allEvents = array();
2432e3a9f44cSAtari911
24332866e827SAtari911        // Load events from the base namespace itself
2434e3a9f44cSAtari911        if (empty($baseNamespace)) {
2435e3a9f44cSAtari911            $events = $this->loadEvents('', $year, $month);
2436e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
2437e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
2438e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
2439e3a9f44cSAtari911                }
2440e3a9f44cSAtari911                foreach ($dayEvents as $event) {
2441e3a9f44cSAtari911                    $event['_namespace'] = '';
2442e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
2443e3a9f44cSAtari911                }
2444e3a9f44cSAtari911            }
2445e3a9f44cSAtari911        } else {
24462866e827SAtari911            if (!$this->isNamespaceExcluded($baseNamespace, $excludeList) && $this->checkNamespaceRead($baseNamespace)) {
2447e3a9f44cSAtari911                $events = $this->loadEvents($baseNamespace, $year, $month);
2448e3a9f44cSAtari911                foreach ($events as $dateKey => $dayEvents) {
2449e3a9f44cSAtari911                    if (!isset($allEvents[$dateKey])) {
2450e3a9f44cSAtari911                        $allEvents[$dateKey] = array();
2451e3a9f44cSAtari911                    }
2452e3a9f44cSAtari911                    foreach ($dayEvents as $event) {
2453e3a9f44cSAtari911                        $event['_namespace'] = $baseNamespace;
2454e3a9f44cSAtari911                        $allEvents[$dateKey][] = $event;
2455e3a9f44cSAtari911                    }
2456e3a9f44cSAtari911                }
2457e3a9f44cSAtari911            }
24582866e827SAtari911        }
2459e3a9f44cSAtari911
24602866e827SAtari911        // Find all calendar directories efficiently using iterative glob
24612866e827SAtari911        // This avoids recursing into every directory in data/meta (thousands on large wikis)
24622866e827SAtari911        $this->findCalendarNamespaces($dataDir, $metaDir, $year, $month, $allEvents, $excludeList);
2463e3a9f44cSAtari911
2464e3a9f44cSAtari911        return $allEvents;
2465e3a9f44cSAtari911    }
2466e3a9f44cSAtari911
24672866e827SAtari911    /**
24682866e827SAtari911     * Find namespaces with calendar data using iterative glob
24692866e827SAtari911     * Searches for 'calendar/' directories at increasing depth without
24702866e827SAtari911     * scanning every directory in data/meta
24712866e827SAtari911     */
24722866e827SAtari911    private function findCalendarNamespaces($baseDir, $metaDir, $year, $month, &$allEvents, $excludeList = []) {
24732866e827SAtari911        if (!is_dir($baseDir)) return;
2474e3a9f44cSAtari911
24752866e827SAtari911        // Use glob at increasing depths to find 'calendar' directories
24762866e827SAtari911        // This is vastly more efficient than recursive scandir on large wikis
24772866e827SAtari911        $maxDepth = 10;
24782866e827SAtari911        $metaDirLen = strlen($metaDir);
2479e3a9f44cSAtari911
24802866e827SAtari911        for ($depth = 1; $depth <= $maxDepth; $depth++) {
24812866e827SAtari911            $pattern = $baseDir . str_repeat('*/', $depth) . 'calendar';
24822866e827SAtari911            $calDirs = glob($pattern, GLOB_ONLYDIR);
2483e3a9f44cSAtari911
24842866e827SAtari911            if (empty($calDirs)) {
24852866e827SAtari911                // No calendar dirs at this depth or deeper - stop early
24862866e827SAtari911                // (only if we also found none at previous depths)
24872866e827SAtari911                if ($depth > 3) break;
24882866e827SAtari911                continue;
24892866e827SAtari911            }
24902866e827SAtari911
24912866e827SAtari911            foreach ($calDirs as $calDir) {
24922866e827SAtari911                // Derive namespace from the parent directory of 'calendar/'
24932866e827SAtari911                $nsDir = dirname($calDir);
24942866e827SAtari911                $relPath = substr($nsDir, $metaDirLen);
24952866e827SAtari911                $namespace = str_replace('/', ':', trim($relPath, '/'));
24962866e827SAtari911
24972866e827SAtari911                // Skip the root namespace (already handled above)
24982866e827SAtari911                if (empty($namespace)) continue;
24992866e827SAtari911
25002866e827SAtari911                // Skip excluded namespaces
25012866e827SAtari911                if ($this->isNamespaceExcluded($namespace, $excludeList)) continue;
25022866e827SAtari911
25032866e827SAtari911                // ACL check
25042866e827SAtari911                if (!$this->checkNamespaceRead($namespace)) continue;
25052866e827SAtari911
2506e3a9f44cSAtari911                $events = $this->loadEvents($namespace, $year, $month);
2507e3a9f44cSAtari911                foreach ($events as $dateKey => $dayEvents) {
2508e3a9f44cSAtari911                    if (!isset($allEvents[$dateKey])) {
2509e3a9f44cSAtari911                        $allEvents[$dateKey] = array();
2510e3a9f44cSAtari911                    }
2511e3a9f44cSAtari911                    foreach ($dayEvents as $event) {
2512e3a9f44cSAtari911                        $event['_namespace'] = $namespace;
2513e3a9f44cSAtari911                        $allEvents[$dateKey][] = $event;
2514e3a9f44cSAtari911                    }
2515e3a9f44cSAtari911                }
2516e3a9f44cSAtari911            }
2517e3a9f44cSAtari911        }
2518e3a9f44cSAtari911    }
25191d05cddcSAtari911
25201d05cddcSAtari911    private function getAllNamespaces() {
25212866e827SAtari911        $dataDir = $this->metaDir();
25221d05cddcSAtari911        $namespaces = [];
25231d05cddcSAtari911
25241d05cddcSAtari911        // Scan for namespaces that have calendar data
25251d05cddcSAtari911        $this->scanForCalendarNamespaces($dataDir, '', $namespaces);
25261d05cddcSAtari911
25271d05cddcSAtari911        // Sort alphabetically
25281d05cddcSAtari911        sort($namespaces);
25291d05cddcSAtari911
25301d05cddcSAtari911        return $namespaces;
25311d05cddcSAtari911    }
25321d05cddcSAtari911
25331d05cddcSAtari911    private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) {
25341d05cddcSAtari911        if (!is_dir($dir)) return;
25351d05cddcSAtari911
25361d05cddcSAtari911        $items = scandir($dir);
25371d05cddcSAtari911        foreach ($items as $item) {
25381d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
25391d05cddcSAtari911
25401d05cddcSAtari911            $path = $dir . $item;
25411d05cddcSAtari911            if (is_dir($path)) {
25421d05cddcSAtari911                // Check if this directory has a calendar subdirectory with data
25431d05cddcSAtari911                $calendarDir = $path . '/calendar/';
25441d05cddcSAtari911                if (is_dir($calendarDir)) {
25451d05cddcSAtari911                    // Check if there are any JSON files in the calendar directory
25461d05cddcSAtari911                    $jsonFiles = glob($calendarDir . '*.json');
25471d05cddcSAtari911                    if (!empty($jsonFiles)) {
25481d05cddcSAtari911                        // This namespace has calendar data
25491d05cddcSAtari911                        $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
25501d05cddcSAtari911                        $namespaces[] = $namespace;
25511d05cddcSAtari911                    }
25521d05cddcSAtari911                }
25531d05cddcSAtari911
25541d05cddcSAtari911                // Recurse into subdirectories
25551d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
25561d05cddcSAtari911                $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces);
25571d05cddcSAtari911            }
25581d05cddcSAtari911        }
25591d05cddcSAtari911    }
25601d05cddcSAtari911
25611d05cddcSAtari911    /**
25621d05cddcSAtari911     * Render new sidebar widget - Week at a glance itinerary (200px wide)
25631d05cddcSAtari911     */
25640c3b6e81SAtari911    private function renderSidebarWidget($events, $namespace, $calId, $themeOverride = null) {
25651d05cddcSAtari911        if (empty($events)) {
2566da206178SAtari911            return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>';
25671d05cddcSAtari911        }
25681d05cddcSAtari911
25691d05cddcSAtari911        // Get important namespaces from config
25702866e827SAtari911        $configFile = $this->syncConfigPath();
25711d05cddcSAtari911        $importantNsList = ['important']; // default
25721d05cddcSAtari911        if (file_exists($configFile)) {
25731d05cddcSAtari911            $config = include $configFile;
25741d05cddcSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
25751d05cddcSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
25761d05cddcSAtari911            }
25771d05cddcSAtari911        }
25781d05cddcSAtari911
25791d05cddcSAtari911        // Calculate date ranges
25801d05cddcSAtari911        $todayStr = date('Y-m-d');
25811d05cddcSAtari911        $tomorrowStr = date('Y-m-d', strtotime('+1 day'));
25829ccd446eSAtari911
25839ccd446eSAtari911        // Get week start preference and calculate week range
25849ccd446eSAtari911        $weekStartDay = $this->getWeekStartDay();
25859ccd446eSAtari911
25869ccd446eSAtari911        if ($weekStartDay === 'monday') {
25879ccd446eSAtari911            // Monday start
25881d05cddcSAtari911            $weekStart = date('Y-m-d', strtotime('monday this week'));
25891d05cddcSAtari911            $weekEnd = date('Y-m-d', strtotime('sunday this week'));
25909ccd446eSAtari911        } else {
25919ccd446eSAtari911            // Sunday start (default - US/Canada standard)
25929ccd446eSAtari911            $today = date('w'); // 0 (Sun) to 6 (Sat)
25939ccd446eSAtari911            if ($today == 0) {
25949ccd446eSAtari911                // Today is Sunday
25959ccd446eSAtari911                $weekStart = date('Y-m-d');
25969ccd446eSAtari911            } else {
25979ccd446eSAtari911                // Monday-Saturday: go back to last Sunday
25989ccd446eSAtari911                $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
25999ccd446eSAtari911            }
26009ccd446eSAtari911            $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
26019ccd446eSAtari911        }
26021d05cddcSAtari911
26031d05cddcSAtari911        // Group events by category
26041d05cddcSAtari911        $todayEvents = [];
26051d05cddcSAtari911        $tomorrowEvents = [];
26061d05cddcSAtari911        $importantEvents = [];
26071d05cddcSAtari911        $weekEvents = []; // For week grid
26081d05cddcSAtari911
26091d05cddcSAtari911        // Process all events
26101d05cddcSAtari911        foreach ($events as $dateKey => $dayEvents) {
26119ccd446eSAtari911            // Detect conflicts for events on this day
26129ccd446eSAtari911            $eventsWithConflicts = $this->detectTimeConflicts($dayEvents);
26131d05cddcSAtari911
26149ccd446eSAtari911            foreach ($eventsWithConflicts as $event) {
26159ccd446eSAtari911                // Always categorize Today and Tomorrow regardless of week boundaries
26169ccd446eSAtari911                if ($dateKey === $todayStr) {
26179ccd446eSAtari911                    $todayEvents[] = array_merge($event, ['date' => $dateKey]);
26189ccd446eSAtari911                }
26199ccd446eSAtari911                if ($dateKey === $tomorrowStr) {
26209ccd446eSAtari911                    $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]);
26219ccd446eSAtari911                }
26229ccd446eSAtari911
26239ccd446eSAtari911                // Process week grid events (only for current week)
26241d05cddcSAtari911                if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
26259ccd446eSAtari911                    // Initialize week grid day if not exists
26261d05cddcSAtari911                    if (!isset($weekEvents[$dateKey])) {
26271d05cddcSAtari911                        $weekEvents[$dateKey] = [];
26281d05cddcSAtari911                    }
26291d05cddcSAtari911
26301d05cddcSAtari911                    // Pre-render DokuWiki syntax to HTML for JavaScript display
26311d05cddcSAtari911                    $eventWithHtml = $event;
26321d05cddcSAtari911                    if (isset($event['title'])) {
26331d05cddcSAtari911                        $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']);
26341d05cddcSAtari911                    }
26351d05cddcSAtari911                    if (isset($event['description'])) {
26361d05cddcSAtari911                        $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']);
26371d05cddcSAtari911                    }
26381d05cddcSAtari911                    $weekEvents[$dateKey][] = $eventWithHtml;
26391d05cddcSAtari911                }
26401d05cddcSAtari911
26411d05cddcSAtari911                // Check if this is an important namespace
26421d05cddcSAtari911                $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
26431d05cddcSAtari911                $isImportant = false;
26441d05cddcSAtari911                foreach ($importantNsList as $impNs) {
26451d05cddcSAtari911                    if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
26461d05cddcSAtari911                        $isImportant = true;
26471d05cddcSAtari911                        break;
26481d05cddcSAtari911                    }
26491d05cddcSAtari911                }
26501d05cddcSAtari911
26519ccd446eSAtari911                // Important events: show from today through next 2 weeks
26529ccd446eSAtari911                if ($isImportant && $dateKey >= $todayStr) {
26531d05cddcSAtari911                    $importantEvents[] = array_merge($event, ['date' => $dateKey]);
26541d05cddcSAtari911                }
26551d05cddcSAtari911            }
26561d05cddcSAtari911        }
26579ccd446eSAtari911
26589ccd446eSAtari911        // Sort Important Events by date (earliest first)
26599ccd446eSAtari911        usort($importantEvents, function($a, $b) {
26609ccd446eSAtari911            $dateA = isset($a['date']) ? $a['date'] : '';
26619ccd446eSAtari911            $dateB = isset($b['date']) ? $b['date'] : '';
26629ccd446eSAtari911
26639ccd446eSAtari911            // Compare dates
26649ccd446eSAtari911            if ($dateA === $dateB) {
26659ccd446eSAtari911                // Same date - sort by time
26669ccd446eSAtari911                $timeA = isset($a['time']) ? $a['time'] : '';
26679ccd446eSAtari911                $timeB = isset($b['time']) ? $b['time'] : '';
26689ccd446eSAtari911
26699ccd446eSAtari911                if (empty($timeA) && !empty($timeB)) return 1;  // All-day events last
26709ccd446eSAtari911                if (!empty($timeA) && empty($timeB)) return -1;
26719ccd446eSAtari911                if (empty($timeA) && empty($timeB)) return 0;
26729ccd446eSAtari911
26739ccd446eSAtari911                // Both have times
26749ccd446eSAtari911                $aMinutes = $this->timeToMinutes($timeA);
26759ccd446eSAtari911                $bMinutes = $this->timeToMinutes($timeB);
26769ccd446eSAtari911                return $aMinutes - $bMinutes;
26771d05cddcSAtari911            }
26781d05cddcSAtari911
26799ccd446eSAtari911            return strcmp($dateA, $dateB);
26809ccd446eSAtari911        });
26819ccd446eSAtari911
26820c3b6e81SAtari911        // Get theme - prefer override from syntax parameter, fall back to admin default
26830c3b6e81SAtari911        $theme = !empty($themeOverride) ? $themeOverride : $this->getSidebarTheme();
26849ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
26859ccd446eSAtari911        $themeClass = 'sidebar-' . $theme;
26869ccd446eSAtari911
26879ccd446eSAtari911        // Start building HTML - Dynamic width with default font (overflow:visible for tooltips)
26889ccd446eSAtari911        $html = '<div class="sidebar-widget ' . $themeClass . '" id="sidebar-widget-' . $calId . '" style="width:100%; max-width:100%; box-sizing:border-box; font-family:system-ui, sans-serif; background:' . $themeStyles['bg'] . '; border:2px solid ' . $themeStyles['border'] . '; border-radius:4px; overflow:visible; box-shadow:0 0 10px ' . $themeStyles['shadow'] . '; position:relative;">';
26899ccd446eSAtari911
26909ccd446eSAtari911        // Inject CSS variables so the event dialog (shared component) picks up the theme
26919ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
26929ccd446eSAtari911        $html .= '<style>
26939ccd446eSAtari911        #sidebar-widget-' . $calId . ' {
26949ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
26959ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
26969ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
26979ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
26989ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
26999ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
27009ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
27019ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
27029ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
27039ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
27049ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
27059ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
27069ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
27079ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
27089ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
27097e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
27107e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
27117e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
27127e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
27137e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
27147e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
27157e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
27169ccd446eSAtari911        }
27179ccd446eSAtari911        </style>';
27189ccd446eSAtari911
27199ccd446eSAtari911        // Add sparkle effect for pink theme
27209ccd446eSAtari911        if ($theme === 'pink') {
27219ccd446eSAtari911            $html .= '<style>
27229ccd446eSAtari911            @keyframes sparkle-' . $calId . ' {
27239ccd446eSAtari911                0% {
27249ccd446eSAtari911                    opacity: 0;
27259ccd446eSAtari911                    transform: translate(0, 0) scale(0) rotate(0deg);
27269ccd446eSAtari911                }
27279ccd446eSAtari911                50% {
27289ccd446eSAtari911                    opacity: 1;
27299ccd446eSAtari911                    transform: translate(var(--tx), var(--ty)) scale(1) rotate(180deg);
27309ccd446eSAtari911                }
27319ccd446eSAtari911                100% {
27329ccd446eSAtari911                    opacity: 0;
27339ccd446eSAtari911                    transform: translate(calc(var(--tx) * 2), calc(var(--ty) * 2)) scale(0) rotate(360deg);
27349ccd446eSAtari911                }
27359ccd446eSAtari911            }
27369ccd446eSAtari911
27379ccd446eSAtari911            @keyframes pulse-glow-' . $calId . ' {
27389ccd446eSAtari911                0%, 100% { box-shadow: 0 0 10px rgba(255, 20, 147, 0.4); }
27399ccd446eSAtari911                50% { box-shadow: 0 0 25px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4); }
27409ccd446eSAtari911            }
27419ccd446eSAtari911
27429ccd446eSAtari911            @keyframes shimmer-' . $calId . ' {
27439ccd446eSAtari911                0% { background-position: -200% center; }
27449ccd446eSAtari911                100% { background-position: 200% center; }
27459ccd446eSAtari911            }
27469ccd446eSAtari911
27479ccd446eSAtari911            .sidebar-pink {
27489ccd446eSAtari911                animation: pulse-glow-' . $calId . ' 3s ease-in-out infinite;
27499ccd446eSAtari911            }
27509ccd446eSAtari911
27519ccd446eSAtari911            .sidebar-pink:hover {
27529ccd446eSAtari911                box-shadow: 0 0 30px rgba(255, 20, 147, 0.9), 0 0 50px rgba(255, 20, 147, 0.5) !important;
27539ccd446eSAtari911            }
27549ccd446eSAtari911
27559ccd446eSAtari911            .sparkle-' . $calId . ' {
27569ccd446eSAtari911                position: absolute;
27579ccd446eSAtari911                pointer-events: none;
27589ccd446eSAtari911                font-size: 20px;
27599ccd446eSAtari911                z-index: 1000;
27609ccd446eSAtari911                animation: sparkle-' . $calId . ' 1s ease-out forwards;
27619ccd446eSAtari911                filter: drop-shadow(0 0 3px rgba(255, 20, 147, 0.8));
27629ccd446eSAtari911            }
27639ccd446eSAtari911            </style>';
27649ccd446eSAtari911
27659ccd446eSAtari911            $html .= '<script>
27669ccd446eSAtari911            (function() {
27679ccd446eSAtari911                const container = document.getElementById("sidebar-widget-' . $calId . '");
27689ccd446eSAtari911                const sparkles = ["✨", "��", "��", "⭐", "��", "��", "��", "��", "��", "��"];
27699ccd446eSAtari911
27709ccd446eSAtari911                function createSparkle(x, y) {
27719ccd446eSAtari911                    const sparkle = document.createElement("div");
27729ccd446eSAtari911                    sparkle.className = "sparkle-' . $calId . '";
27739ccd446eSAtari911                    sparkle.textContent = sparkles[Math.floor(Math.random() * sparkles.length)];
27749ccd446eSAtari911                    sparkle.style.left = x + "px";
27759ccd446eSAtari911                    sparkle.style.top = y + "px";
27769ccd446eSAtari911
27779ccd446eSAtari911                    // Random direction
27789ccd446eSAtari911                    const angle = Math.random() * Math.PI * 2;
27799ccd446eSAtari911                    const distance = 30 + Math.random() * 40;
27809ccd446eSAtari911                    sparkle.style.setProperty("--tx", Math.cos(angle) * distance + "px");
27819ccd446eSAtari911                    sparkle.style.setProperty("--ty", Math.sin(angle) * distance + "px");
27829ccd446eSAtari911
27839ccd446eSAtari911                    container.appendChild(sparkle);
27849ccd446eSAtari911
27859ccd446eSAtari911                    setTimeout(() => sparkle.remove(), 1000);
27869ccd446eSAtari911                }
27879ccd446eSAtari911
27889ccd446eSAtari911                // Click sparkles
27899ccd446eSAtari911                container.addEventListener("click", function(e) {
27909ccd446eSAtari911                    const rect = container.getBoundingClientRect();
27919ccd446eSAtari911                    const x = e.clientX - rect.left;
27929ccd446eSAtari911                    const y = e.clientY - rect.top;
27939ccd446eSAtari911
27949ccd446eSAtari911                    // Create LOTS of sparkles for maximum bling!
27959ccd446eSAtari911                    for (let i = 0; i < 8; i++) {
27969ccd446eSAtari911                        setTimeout(() => {
27979ccd446eSAtari911                            const offsetX = x + (Math.random() - 0.5) * 30;
27989ccd446eSAtari911                            const offsetY = y + (Math.random() - 0.5) * 30;
27999ccd446eSAtari911                            createSparkle(offsetX, offsetY);
28009ccd446eSAtari911                        }, i * 40);
28019ccd446eSAtari911                    }
28029ccd446eSAtari911                });
28039ccd446eSAtari911
28049ccd446eSAtari911                // Random auto-sparkles for extra glamour
28059ccd446eSAtari911                setInterval(() => {
28069ccd446eSAtari911                    const x = Math.random() * container.offsetWidth;
28079ccd446eSAtari911                    const y = Math.random() * container.offsetHeight;
28089ccd446eSAtari911                    createSparkle(x, y);
28099ccd446eSAtari911                }, 3000);
28109ccd446eSAtari911            })();
28119ccd446eSAtari911            </script>';
28129ccd446eSAtari911        }
28131d05cddcSAtari911
28141d05cddcSAtari911        // Sanitize calId for use in JavaScript variable names (remove dashes)
28151d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
28161d05cddcSAtari911
28171d05cddcSAtari911        // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it
28181d05cddcSAtari911        $html .= '<script>
28191d05cddcSAtari911(function() {
28201d05cddcSAtari911    // Update clock every second
28211d05cddcSAtari911    function updateClock() {
28221d05cddcSAtari911        const now = new Date();
28231d05cddcSAtari911        let hours = now.getHours();
28241d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
28251d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
28261d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
28271d05cddcSAtari911        hours = hours % 12 || 12;
28281d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
28291d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
28301d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
28311d05cddcSAtari911    }
28321d05cddcSAtari911    setInterval(updateClock, 1000);
28331d05cddcSAtari911
283496df7d3eSAtari911    // Weather - uses default location, click weather to get local
283596df7d3eSAtari911    var userLocationGranted = false;
283696df7d3eSAtari911    var userLat = 38.5816;  // Sacramento default
283796df7d3eSAtari911    var userLon = -121.4944;
28381d05cddcSAtari911
283996df7d3eSAtari911    function fetchWeatherData(lat, lon) {
284096df7d3eSAtari911        fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&temperature_unit=fahrenheit")
28411d05cddcSAtari911            .then(response => response.json())
28421d05cddcSAtari911            .then(data => {
28431d05cddcSAtari911                if (data.current_weather) {
28441d05cddcSAtari911                    const temp = Math.round(data.current_weather.temperature);
28451d05cddcSAtari911                    const weatherCode = data.current_weather.weathercode;
28461d05cddcSAtari911                    const icon = getWeatherIcon(weatherCode);
28471d05cddcSAtari911                    const iconEl = document.getElementById("weather-icon-' . $calId . '");
28481d05cddcSAtari911                    const tempEl = document.getElementById("weather-temp-' . $calId . '");
28491d05cddcSAtari911                    if (iconEl) iconEl.textContent = icon;
28501d05cddcSAtari911                    if (tempEl) tempEl.innerHTML = temp + "&deg;";
28511d05cddcSAtari911                }
28521d05cddcSAtari911            })
28531d05cddcSAtari911            .catch(error => console.log("Weather fetch error:", error));
285496df7d3eSAtari911    }
285596df7d3eSAtari911
285696df7d3eSAtari911    function updateWeather() {
285796df7d3eSAtari911        fetchWeatherData(userLat, userLon);
285896df7d3eSAtari911    }
285996df7d3eSAtari911
286096df7d3eSAtari911    // Click weather icon to request local weather (user gesture required)
286196df7d3eSAtari911    function requestLocalWeather() {
286296df7d3eSAtari911        if (userLocationGranted) return;
286396df7d3eSAtari911        if ("geolocation" in navigator) {
286496df7d3eSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
286596df7d3eSAtari911                userLat = position.coords.latitude;
286696df7d3eSAtari911                userLon = position.coords.longitude;
286796df7d3eSAtari911                userLocationGranted = true;
286896df7d3eSAtari911                fetchWeatherData(userLat, userLon);
28691d05cddcSAtari911            }, function(error) {
287096df7d3eSAtari911                console.log("Geolocation denied, using default location");
28711d05cddcSAtari911            });
28721d05cddcSAtari911        }
28731d05cddcSAtari911    }
28741d05cddcSAtari911
287596df7d3eSAtari911    setTimeout(function() {
287696df7d3eSAtari911        var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
287796df7d3eSAtari911        if (weatherEl) {
287896df7d3eSAtari911            weatherEl.style.cursor = "pointer";
287996df7d3eSAtari911            weatherEl.title = "Click for local weather";
288096df7d3eSAtari911            weatherEl.addEventListener("click", requestLocalWeather);
288196df7d3eSAtari911        }
288296df7d3eSAtari911    }, 100);
288396df7d3eSAtari911
28841d05cddcSAtari911    function getWeatherIcon(code) {
28851d05cddcSAtari911        const icons = {
28861d05cddcSAtari911            0: "☀️", 1: "��️", 2: "⛅", 3: "☁️",
28871d05cddcSAtari911            45: "��️", 48: "��️", 51: "��️", 53: "��️", 55: "��️",
28881d05cddcSAtari911            61: "��️", 63: "��️", 65: "⛈️", 71: "��️", 73: "��️",
28891d05cddcSAtari911            75: "❄️", 77: "��️", 80: "��️", 81: "��️", 82: "⛈️",
28901d05cddcSAtari911            85: "��️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️"
28911d05cddcSAtari911        };
28921d05cddcSAtari911        return icons[code] || "��️";
28931d05cddcSAtari911    }
28941d05cddcSAtari911
28951d05cddcSAtari911    // Update weather immediately and every 10 minutes
28961d05cddcSAtari911    updateWeather();
28971d05cddcSAtari911    setInterval(updateWeather, 600000);
28981d05cddcSAtari911})();
28991d05cddcSAtari911</script>';
29001d05cddcSAtari911
29011d05cddcSAtari911        // NOW add the header HTML (after JavaScript is defined)
29021d05cddcSAtari911        $todayDate = new DateTime();
29031d05cddcSAtari911        $displayDate = $todayDate->format('D, M j, Y');
29041d05cddcSAtari911        $currentTime = $todayDate->format('g:i:s A');
29051d05cddcSAtari911
29069ccd446eSAtari911        $html .= '<div class="eventlist-today-header" style="background:' . $themeStyles['header_bg'] . '; border:2px solid ' . $themeStyles['header_border'] . '; box-shadow:' . $themeStyles['header_shadow'] . ';">';
29079ccd446eSAtari911        $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '" style="color:' . $themeStyles['text_bright'] . ';">' . $currentTime . '</span>';
29081d05cddcSAtari911        $html .= '<div class="eventlist-bottom-info">';
29099ccd446eSAtari911        $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '" style="color:' . $themeStyles['text_primary'] . ';">--°</span></span>';
29109ccd446eSAtari911        $html .= '<span class="eventlist-today-date" style="color:' . $themeStyles['text_dim'] . ';">' . $displayDate . '</span>';
29111d05cddcSAtari911        $html .= '</div>';
29121d05cddcSAtari911        $html .= '</div>';
29131d05cddcSAtari911
2914231d0edbSAtari911        // Get today's date for default event date
2915231d0edbSAtari911        $todayStr = date('Y-m-d');
2916231d0edbSAtari911
29179ccd446eSAtari911        // Thin "Add Event" bar between header and week grid - theme-aware colors
29187e8ea635SAtari911        $addBtnBg = $themeStyles['cell_today_bg'];
29197e8ea635SAtari911        $addBtnHover = $themeStyles['grid_bg'];
29207e8ea635SAtari911        $addBtnTextColor = ($theme === 'professional' || $theme === 'wiki') ?
29217e8ea635SAtari911                          $themeStyles['text_bright'] : $themeStyles['text_bright'];
29227e8ea635SAtari911        $addBtnShadow = ($theme === 'professional' || $theme === 'wiki') ?
29237e8ea635SAtari911                       '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow'];
29247e8ea635SAtari911        $addBtnHoverShadow = ($theme === 'professional' || $theme === 'wiki') ?
29257e8ea635SAtari911                            '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow'];
29269ccd446eSAtari911
29279ccd446eSAtari911        $html .= '<div style="background:' . $addBtnBg . '; padding:0; margin:0; height:12px; line-height:10px; text-align:center; cursor:pointer; border-top:1px solid rgba(0, 0, 0, 0.1); border-bottom:1px solid rgba(0, 0, 0, 0.1); box-shadow:' . $addBtnShadow . '; transition:all 0.2s;" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\', \'' . $todayStr . '\');" onmouseover="this.style.background=\'' . $addBtnHover . '\'; this.style.boxShadow=\'' . $addBtnHoverShadow . '\';" onmouseout="this.style.background=\'' . $addBtnBg . '\'; this.style.boxShadow=\'' . $addBtnShadow . '\';">';
29289ccd446eSAtari911        $addBtnTextShadow = ($theme === 'pink') ? '0 0 3px ' . $addBtnTextColor : 'none';
2929da206178SAtari911        $html .= '<span style="color:' . $addBtnTextColor . '; font-size:8px; font-weight:700; letter-spacing:0.4px; font-family:system-ui, sans-serif; text-shadow:' . $addBtnTextShadow . '; position:relative; top:-1px;">+ ADD EVENT</span>';
29301d05cddcSAtari911        $html .= '</div>';
29311d05cddcSAtari911
29321d05cddcSAtari911        // Week grid (7 cells)
29339ccd446eSAtari911        $html .= $this->renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme);
29341d05cddcSAtari911
29357e8ea635SAtari911        // Section colors - derived from theme palette
29367e8ea635SAtari911        // Today: brightest accent, Tomorrow: primary accent, Important: dim/secondary accent
29377e8ea635SAtari911        if ($theme === 'matrix') {
29387e8ea635SAtari911            $todayColor = '#00ff00';     // Bright green
29397e8ea635SAtari911            $tomorrowColor = '#00cc07';  // Standard green
29407e8ea635SAtari911            $importantColor = '#00aa00'; // Dim green
29417e8ea635SAtari911        } else if ($theme === 'purple') {
29427e8ea635SAtari911            $todayColor = '#d4a5ff';     // Bright purple
29437e8ea635SAtari911            $tomorrowColor = '#9b59b6';  // Standard purple
29447e8ea635SAtari911            $importantColor = '#8e7ab8'; // Dim purple
29457e8ea635SAtari911        } else if ($theme === 'pink') {
29467e8ea635SAtari911            $todayColor = '#ff1493';     // Hot pink
29477e8ea635SAtari911            $tomorrowColor = '#ff69b4';  // Medium pink
29487e8ea635SAtari911            $importantColor = '#ff85c1'; // Light pink
29497e8ea635SAtari911        } else if ($theme === 'professional') {
29507e8ea635SAtari911            $todayColor = '#4a90e2';     // Blue accent
29517e8ea635SAtari911            $tomorrowColor = '#5ba3e6';  // Lighter blue
29527e8ea635SAtari911            $importantColor = '#7fb8ec'; // Lightest blue
29539ccd446eSAtari911        } else {
29547e8ea635SAtari911            // Wiki - section header backgrounds from template colors
29557e8ea635SAtari911            $todayColor = $themeStyles['text_bright'];      // __link__
29567e8ea635SAtari911            $tomorrowColor = $themeStyles['header_bg'];     // __background_alt__
29577e8ea635SAtari911            $importantColor = $themeStyles['header_border'];// __border__
29589ccd446eSAtari911        }
29599ccd446eSAtari911
296096df7d3eSAtari911        // Check if there are any itinerary items
296196df7d3eSAtari911        $hasItinerary = !empty($todayEvents) || !empty($tomorrowEvents) || !empty($importantEvents);
296296df7d3eSAtari911
296396df7d3eSAtari911        // Itinerary bar (collapsible toggle) - styled like +Add bar
296496df7d3eSAtari911        $itineraryBg = $themeStyles['cell_today_bg'];
296596df7d3eSAtari911        $itineraryHover = $themeStyles['grid_bg'];
296696df7d3eSAtari911        $itineraryTextColor = ($theme === 'professional' || $theme === 'wiki') ?
296796df7d3eSAtari911                              $themeStyles['text_bright'] : $themeStyles['text_bright'];
296896df7d3eSAtari911        $itineraryShadow = ($theme === 'professional' || $theme === 'wiki') ?
296996df7d3eSAtari911                           '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow'];
297096df7d3eSAtari911        $itineraryHoverShadow = ($theme === 'professional' || $theme === 'wiki') ?
297196df7d3eSAtari911                                '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow'];
297296df7d3eSAtari911        $itineraryTextShadow = ($theme === 'pink') ? '0 0 3px ' . $itineraryTextColor : 'none';
297396df7d3eSAtari911
297496df7d3eSAtari911        // Sanitize calId for JavaScript
297596df7d3eSAtari911        $jsCalId = str_replace('-', '_', $calId);
297696df7d3eSAtari911
297796df7d3eSAtari911        // Get itinerary default state from settings
297896df7d3eSAtari911        $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
297996df7d3eSAtari911        $arrowDefaultStyle = $itineraryDefaultCollapsed ? 'transform:rotate(-90deg);' : '';
298096df7d3eSAtari911        $contentDefaultStyle = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : '';
298196df7d3eSAtari911
298296df7d3eSAtari911        $html .= '<div id="itinerary-bar-' . $calId . '" style="background:' . $itineraryBg . '; padding:0; margin:0; height:12px; line-height:10px; text-align:center; cursor:pointer; border-top:1px solid rgba(0, 0, 0, 0.1); border-bottom:1px solid rgba(0, 0, 0, 0.1); box-shadow:' . $itineraryShadow . '; transition:all 0.2s; display:flex; align-items:center; justify-content:center; gap:4px;" onclick="toggleItinerary_' . $jsCalId . '();" onmouseover="this.style.background=\'' . $itineraryHover . '\'; this.style.boxShadow=\'' . $itineraryHoverShadow . '\';" onmouseout="this.style.background=\'' . $itineraryBg . '\'; this.style.boxShadow=\'' . $itineraryShadow . '\';">';
298396df7d3eSAtari911        $html .= '<span id="itinerary-arrow-' . $calId . '" style="color:' . $itineraryTextColor . '; font-size:6px; font-weight:700; font-family:system-ui, sans-serif; text-shadow:' . $itineraryTextShadow . '; position:relative; top:-1px; transition:transform 0.2s; ' . $arrowDefaultStyle . '">▼</span>';
298496df7d3eSAtari911        $html .= '<span style="color:' . $itineraryTextColor . '; font-size:8px; font-weight:700; letter-spacing:0.4px; font-family:system-ui, sans-serif; text-shadow:' . $itineraryTextShadow . '; position:relative; top:-1px;">ITINERARY</span>';
298596df7d3eSAtari911        $html .= '</div>';
298696df7d3eSAtari911
298796df7d3eSAtari911        // Itinerary content container (collapsible)
298896df7d3eSAtari911        $html .= '<div id="itinerary-content-' . $calId . '" style="transition:max-height 0.3s ease-out, opacity 0.2s ease-out; overflow:hidden; ' . $contentDefaultStyle . '">';
298996df7d3eSAtari911
29909ccd446eSAtari911        // Today section
29911d05cddcSAtari911        if (!empty($todayEvents)) {
2992da206178SAtari911            $html .= $this->renderSidebarSection('Today', $todayEvents, $todayColor, $calId, $themeStyles, $theme, $importantNsList);
29931d05cddcSAtari911        }
29941d05cddcSAtari911
29959ccd446eSAtari911        // Tomorrow section
29961d05cddcSAtari911        if (!empty($tomorrowEvents)) {
2997da206178SAtari911            $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, $tomorrowColor, $calId, $themeStyles, $theme, $importantNsList);
29981d05cddcSAtari911        }
29991d05cddcSAtari911
30009ccd446eSAtari911        // Important events section
30011d05cddcSAtari911        if (!empty($importantEvents)) {
3002da206178SAtari911            $html .= $this->renderSidebarSection('Important Events', $importantEvents, $importantColor, $calId, $themeStyles, $theme, $importantNsList);
30031d05cddcSAtari911        }
30041d05cddcSAtari911
300596df7d3eSAtari911        // Empty state if no itinerary items
300696df7d3eSAtari911        if (!$hasItinerary) {
3007da206178SAtari911            $html .= '<div style="padding:8px; text-align:center; color:' . $themeStyles['text_dim'] . '; font-size:10px; font-family:system-ui, sans-serif;">No upcoming events</div>';
300896df7d3eSAtari911        }
300996df7d3eSAtari911
301096df7d3eSAtari911        $html .= '</div>'; // Close itinerary-content
301196df7d3eSAtari911
301296df7d3eSAtari911        // Get itinerary default state from settings
301396df7d3eSAtari911        $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
301496df7d3eSAtari911        $itineraryExpandedDefault = $itineraryDefaultCollapsed ? 'false' : 'true';
301596df7d3eSAtari911        $itineraryArrowDefault = $itineraryDefaultCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
301696df7d3eSAtari911        $itineraryContentDefault = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : 'max-height:none;';
301796df7d3eSAtari911
301896df7d3eSAtari911        // JavaScript for toggling itinerary
301996df7d3eSAtari911        $html .= '<script>
302096df7d3eSAtari911        (function() {
302196df7d3eSAtari911            let itineraryExpanded_' . $jsCalId . ' = ' . $itineraryExpandedDefault . ';
302296df7d3eSAtari911
302396df7d3eSAtari911            window.toggleItinerary_' . $jsCalId . ' = function() {
302496df7d3eSAtari911                const content = document.getElementById("itinerary-content-' . $calId . '");
302596df7d3eSAtari911                const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
302696df7d3eSAtari911
302796df7d3eSAtari911                if (itineraryExpanded_' . $jsCalId . ') {
302896df7d3eSAtari911                    // Collapse
302996df7d3eSAtari911                    content.style.maxHeight = "0px";
303096df7d3eSAtari911                    content.style.opacity = "0";
303196df7d3eSAtari911                    arrow.style.transform = "rotate(-90deg)";
303296df7d3eSAtari911                    itineraryExpanded_' . $jsCalId . ' = false;
303396df7d3eSAtari911                } else {
303496df7d3eSAtari911                    // Expand
303596df7d3eSAtari911                    content.style.maxHeight = content.scrollHeight + "px";
303696df7d3eSAtari911                    content.style.opacity = "1";
303796df7d3eSAtari911                    arrow.style.transform = "rotate(0deg)";
303896df7d3eSAtari911                    itineraryExpanded_' . $jsCalId . ' = true;
303996df7d3eSAtari911
304096df7d3eSAtari911                    // After transition, set to auto for dynamic content
304196df7d3eSAtari911                    setTimeout(function() {
304296df7d3eSAtari911                        if (itineraryExpanded_' . $jsCalId . ') {
304396df7d3eSAtari911                            content.style.maxHeight = "none";
304496df7d3eSAtari911                        }
304596df7d3eSAtari911                    }, 300);
304696df7d3eSAtari911                }
304796df7d3eSAtari911            };
304896df7d3eSAtari911
304996df7d3eSAtari911            // Initialize based on default state
305096df7d3eSAtari911            const content = document.getElementById("itinerary-content-' . $calId . '");
305196df7d3eSAtari911            const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
305296df7d3eSAtari911            if (content && arrow) {
305396df7d3eSAtari911                if (' . $itineraryExpandedDefault . ') {
305496df7d3eSAtari911                    content.style.maxHeight = "none";
305596df7d3eSAtari911                    arrow.style.transform = "rotate(0deg)";
305696df7d3eSAtari911                } else {
305796df7d3eSAtari911                    content.style.maxHeight = "0px";
305896df7d3eSAtari911                    content.style.opacity = "0";
305996df7d3eSAtari911                    arrow.style.transform = "rotate(-90deg)";
306096df7d3eSAtari911                }
306196df7d3eSAtari911            }
306296df7d3eSAtari911        })();
306396df7d3eSAtari911        </script>';
306496df7d3eSAtari911
30651d05cddcSAtari911        $html .= '</div>';
30661d05cddcSAtari911
3067231d0edbSAtari911        // Add event dialog for sidebar widget
30680c3b6e81SAtari911        $html .= $this->renderEventDialog($calId, $namespace, $theme);
3069231d0edbSAtari911
30709ccd446eSAtari911        // Add JavaScript for positioning data-tooltip elements
30719ccd446eSAtari911        $html .= '<script>
30729ccd446eSAtari911        // Position data-tooltip elements to prevent cutoff (up and to the LEFT)
30739ccd446eSAtari911        document.addEventListener("DOMContentLoaded", function() {
30749ccd446eSAtari911            const tooltipElements = document.querySelectorAll("[data-tooltip]");
30759ccd446eSAtari911            const isPinkTheme = document.querySelector(".sidebar-pink") !== null;
30769ccd446eSAtari911
30779ccd446eSAtari911            tooltipElements.forEach(function(element) {
30789ccd446eSAtari911                element.addEventListener("mouseenter", function() {
30799ccd446eSAtari911                    const rect = element.getBoundingClientRect();
30809ccd446eSAtari911                    const style = window.getComputedStyle(element, ":before");
30819ccd446eSAtari911
30829ccd446eSAtari911                    // Position above the element, aligned to LEFT (not right)
30839ccd446eSAtari911                    element.style.setProperty("--tooltip-left", (rect.left - 150) + "px");
30849ccd446eSAtari911                    element.style.setProperty("--tooltip-top", (rect.top - 30) + "px");
30859ccd446eSAtari911
30869ccd446eSAtari911                    // Pink theme: position heart to the right of tooltip
30879ccd446eSAtari911                    if (isPinkTheme) {
30889ccd446eSAtari911                        element.style.setProperty("--heart-left", (rect.left - 150 + 210) + "px");
30899ccd446eSAtari911                        element.style.setProperty("--heart-top", (rect.top - 30) + "px");
30909ccd446eSAtari911                    }
30919ccd446eSAtari911                });
30929ccd446eSAtari911            });
30939ccd446eSAtari911        });
30949ccd446eSAtari911
30959ccd446eSAtari911        // Apply custom properties to position tooltips
30969ccd446eSAtari911        const style = document.createElement("style");
30979ccd446eSAtari911        style.textContent = `
30989ccd446eSAtari911            [data-tooltip]:hover:before {
30999ccd446eSAtari911                left: var(--tooltip-left, 0) !important;
31009ccd446eSAtari911                top: var(--tooltip-top, 0) !important;
31019ccd446eSAtari911            }
31029ccd446eSAtari911            .sidebar-pink [data-tooltip]:hover:after {
31039ccd446eSAtari911                left: var(--heart-left, 0) !important;
31049ccd446eSAtari911                top: var(--heart-top, 0) !important;
31059ccd446eSAtari911            }
31069ccd446eSAtari911        `;
31079ccd446eSAtari911        document.head.appendChild(style);
31089ccd446eSAtari911        </script>';
31099ccd446eSAtari911
31101d05cddcSAtari911        return $html;
31111d05cddcSAtari911    }
31121d05cddcSAtari911
31131d05cddcSAtari911    /**
31149ccd446eSAtari911     * Render compact week grid (7 cells with event bars) - Theme-aware
31151d05cddcSAtari911     */
31169ccd446eSAtari911    private function renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme) {
31171d05cddcSAtari911        // Generate unique ID for this calendar instance - sanitize for JavaScript
31181d05cddcSAtari911        $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8);
31191d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);  // Sanitize for JS variable names
31201d05cddcSAtari911
31219ccd446eSAtari911        $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:' . $themeStyles['grid_bg'] . '; border-bottom:2px solid ' . $themeStyles['grid_border'] . ';">';
31221d05cddcSAtari911
31239ccd446eSAtari911        // Day names depend on week start setting
31249ccd446eSAtari911        $weekStartDay = $this->getWeekStartDay();
31259ccd446eSAtari911        if ($weekStartDay === 'monday') {
31269ccd446eSAtari911            $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];  // Monday to Sunday
31279ccd446eSAtari911        } else {
31289ccd446eSAtari911            $dayNames = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];  // Sunday to Saturday
31299ccd446eSAtari911        }
31301d05cddcSAtari911        $today = date('Y-m-d');
31311d05cddcSAtari911
31321d05cddcSAtari911        for ($i = 0; $i < 7; $i++) {
31331d05cddcSAtari911            $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days'));
31341d05cddcSAtari911            $dayNum = date('j', strtotime($date));
31351d05cddcSAtari911            $isToday = $date === $today;
31361d05cddcSAtari911
31371d05cddcSAtari911            $events = isset($weekEvents[$date]) ? $weekEvents[$date] : [];
31381d05cddcSAtari911            $eventCount = count($events);
31391d05cddcSAtari911
31409ccd446eSAtari911            $bgColor = $isToday ? $themeStyles['cell_today_bg'] : $themeStyles['cell_bg'];
31419ccd446eSAtari911            $textColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
31421d05cddcSAtari911            $fontWeight = $isToday ? '700' : '500';
31439ccd446eSAtari911
31449ccd446eSAtari911            // Theme-aware text shadow
31459ccd446eSAtari911            if ($theme === 'pink') {
31469ccd446eSAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
31477e8ea635SAtari911                $textShadow = $isToday ? 'text-shadow:0 0 3px ' . $glowColor . ';' : 'text-shadow:0 0 2px ' . $glowColor . ';';
31487e8ea635SAtari911            } else if ($theme === 'matrix') {
31497e8ea635SAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
31507e8ea635SAtari911                $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';';
31517e8ea635SAtari911            } else if ($theme === 'purple') {
31527e8ea635SAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
31537e8ea635SAtari911                $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';';
31549ccd446eSAtari911            } else {
31557e8ea635SAtari911                $textShadow = '';  // No glow for professional/wiki
31569ccd446eSAtari911            }
31579ccd446eSAtari911
31589ccd446eSAtari911            // Border color based on theme
31599ccd446eSAtari911            $borderColor = $themeStyles['grid_border'];
31601d05cddcSAtari911
31611d05cddcSAtari911            $hasEvents = $eventCount > 0;
31621d05cddcSAtari911            $clickableStyle = $hasEvents ? 'cursor:pointer;' : '';
31631d05cddcSAtari911            $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : '';
31641d05cddcSAtari911
31659ccd446eSAtari911            $html .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px; position:relative; border:1px solid ' . $borderColor . ' !important; ' . $clickableStyle . '" ' . $clickHandler . '>';
31661d05cddcSAtari911
31679ccd446eSAtari911            // Day letter - theme color
31689ccd446eSAtari911            $dayLetterColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
31699ccd446eSAtari911            $html .= '<div style="font-size:9px; color:' . $dayLetterColor . '; font-weight:500; font-family:system-ui, sans-serif;">' . $dayNames[$i] . '</div>';
31701d05cddcSAtari911
31711d05cddcSAtari911            // Day number
31721d05cddcSAtari911            $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>';
31731d05cddcSAtari911
31749ccd446eSAtari911            // Event bars (max 4 visible) with theme-aware glow
31751d05cddcSAtari911            if ($eventCount > 0) {
31769ccd446eSAtari911                $showCount = min($eventCount, 4);
31771d05cddcSAtari911                for ($j = 0; $j < $showCount; $j++) {
31781d05cddcSAtari911                    $event = $events[$j];
31799ccd446eSAtari911                    $color = isset($event['color']) ? $event['color'] : $themeStyles['text_primary'];
31809ccd446eSAtari911                    $barShadow = $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . htmlspecialchars($color);
31819ccd446eSAtari911                    $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:' . $barShadow . ';"></div>';
31821d05cddcSAtari911                }
31831d05cddcSAtari911
31849ccd446eSAtari911                // Show "+N more" if more than 4 - theme color
31859ccd446eSAtari911                if ($eventCount > 4) {
31869ccd446eSAtari911                    $moreTextColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
31879ccd446eSAtari911                    $html .= '<div style="font-size:7px; color:' . $moreTextColor . '; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 4) . '</div>';
31881d05cddcSAtari911                }
31891d05cddcSAtari911            }
31901d05cddcSAtari911
31911d05cddcSAtari911            $html .= '</div>';
31921d05cddcSAtari911        }
31931d05cddcSAtari911
31941d05cddcSAtari911        $html .= '</div>';
31951d05cddcSAtari911
31969ccd446eSAtari911        // Add container for selected day events display (with unique ID) - theme-aware
31977e8ea635SAtari911        $panelBorderColor = $themeStyles['border'];
31987e8ea635SAtari911        $panelHeaderBg = $themeStyles['border'];
31997e8ea635SAtari911        $panelShadow = ($theme === 'professional' || $theme === 'wiki') ?
32007e8ea635SAtari911                      '0 1px 3px rgba(0, 0, 0, 0.1)' :
32017e8ea635SAtari911                      '0 0 5px ' . $themeStyles['shadow'];
32027e8ea635SAtari911        $panelContentBg = ($theme === 'professional') ? 'rgba(255, 255, 255, 0.95)' :
32039ccd446eSAtari911                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.5)');
32049ccd446eSAtari911        $panelHeaderShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $panelHeaderBg;
32059ccd446eSAtari911
32067e8ea635SAtari911        // Header text color - dark bg text for dark themes, white for light theme accent headers
32077e8ea635SAtari911        $panelHeaderColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] :
32087e8ea635SAtari911                            (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff');
32099ccd446eSAtari911
32107e8ea635SAtari911        $html .= '<div id="selected-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid ' . $panelBorderColor . ($theme === 'wiki' ? '' : ' !important') . '; box-shadow:' . $panelShadow . ';">';
32117e8ea635SAtari911        if ($theme === 'wiki') {
32129ccd446eSAtari911            $html .= '<div style="background:' . $panelHeaderBg . '; color:' . $panelHeaderColor . '; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:' . $panelHeaderShadow . '; display:flex; justify-content:space-between; align-items:center;">';
32131d05cddcSAtari911            $html .= '<span id="selected-day-title-' . $calId . '"></span>';
32149ccd446eSAtari911            $html .= '<span onclick="document.getElementById(\'selected-day-events-' . $calId . '\').style.display=\'none\';" style="cursor:pointer; font-size:12px; padding:0 4px; font-weight:700; color:' . $panelHeaderColor . ';">✕</span>';
32157e8ea635SAtari911        } else {
32167e8ea635SAtari911            $html .= '<div style="background:' . $panelHeaderBg . ' !important; color:' . $panelHeaderColor . ' !important; -webkit-text-fill-color:' . $panelHeaderColor . ' !important; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:' . $panelHeaderShadow . '; display:flex; justify-content:space-between; align-items:center;">';
32177e8ea635SAtari911            $html .= '<span id="selected-day-title-' . $calId . '"></span>';
32187e8ea635SAtari911            $html .= '<span onclick="document.getElementById(\'selected-day-events-' . $calId . '\').style.display=\'none\';" style="cursor:pointer; font-size:12px; padding:0 4px; font-weight:700; color:' . $panelHeaderColor . ' !important; -webkit-text-fill-color:' . $panelHeaderColor . ' !important;">✕</span>';
32197e8ea635SAtari911        }
32201d05cddcSAtari911        $html .= '</div>';
32219ccd446eSAtari911        $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:' . $panelContentBg . ';"></div>';
32221d05cddcSAtari911        $html .= '</div>';
32231d05cddcSAtari911
32241d05cddcSAtari911        // Add JavaScript for day selection with event data
32251d05cddcSAtari911        $html .= '<script>';
32261d05cddcSAtari911        // Sanitize calId for JavaScript variable names
32271d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
32281d05cddcSAtari911        $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';';
32299ccd446eSAtari911
32309ccd446eSAtari911        // Pass theme colors to JavaScript
32319ccd446eSAtari911        $jsThemeColors = json_encode([
32329ccd446eSAtari911            'text_primary' => $themeStyles['text_primary'],
32339ccd446eSAtari911            'text_bright' => $themeStyles['text_bright'],
32349ccd446eSAtari911            'text_dim' => $themeStyles['text_dim'],
32357e8ea635SAtari911            'text_shadow' => ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $themeStyles['text_primary'] :
32367e8ea635SAtari911                             ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $themeStyles['text_primary'] : ''),
32379ccd446eSAtari911            'event_bg' => $theme === 'professional' ? 'rgba(255, 255, 255, 0.5)' :
32389ccd446eSAtari911                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.3)'),
32399ccd446eSAtari911            'border_color' => $theme === 'professional' ? 'rgba(0, 0, 0, 0.1)' :
32409ccd446eSAtari911                             ($theme === 'purple' ? 'rgba(155, 89, 182, 0.2)' :
32419ccd446eSAtari911                             ($theme === 'pink' ? 'rgba(255, 20, 147, 0.3)' :
32429ccd446eSAtari911                             ($theme === 'wiki' ? $themeStyles['grid_border'] : 'rgba(0, 204, 7, 0.2)'))),
32439ccd446eSAtari911            'bar_shadow' => $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' :
32449ccd446eSAtari911                           ($theme === 'wiki' ? '0 1px 2px rgba(0,0,0,0.15)' : '0 0 3px')
32459ccd446eSAtari911        ]);
32469ccd446eSAtari911        $html .= 'window.themeColors_' . $jsCalId . ' = ' . $jsThemeColors . ';';
32471d05cddcSAtari911        $html .= '
32481d05cddcSAtari911        window.showDayEvents_' . $jsCalId . ' = function(dateKey) {
32491d05cddcSAtari911            const eventsData = window.weekEventsData_' . $jsCalId . ';
32501d05cddcSAtari911            const container = document.getElementById("selected-day-events-' . $calId . '");
32511d05cddcSAtari911            const title = document.getElementById("selected-day-title-' . $calId . '");
32521d05cddcSAtari911            const content = document.getElementById("selected-day-content-' . $calId . '");
32531d05cddcSAtari911
32541d05cddcSAtari911            if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return;
32551d05cddcSAtari911
32561d05cddcSAtari911            // Format date for display
32571d05cddcSAtari911            const dateObj = new Date(dateKey + "T00:00:00");
32581d05cddcSAtari911            const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" });
32591d05cddcSAtari911            const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" });
32601d05cddcSAtari911            title.textContent = dayName + ", " + monthDay;
32611d05cddcSAtari911
32621d05cddcSAtari911            // Clear content
32631d05cddcSAtari911            content.innerHTML = "";
32641d05cddcSAtari911
3265231d0edbSAtari911            // Sort events by time (all-day events first, then timed events chronologically)
32661d05cddcSAtari911            const sortedEvents = [...eventsData[dateKey]].sort((a, b) => {
3267231d0edbSAtari911                // All-day events (no time) go to the beginning
32681d05cddcSAtari911                if (!a.time && !b.time) return 0;
3269231d0edbSAtari911                if (!a.time) return -1;  // a is all-day, comes first
3270231d0edbSAtari911                if (!b.time) return 1;   // b is all-day, comes first
32711d05cddcSAtari911
32721d05cddcSAtari911                // Compare times (format: "HH:MM")
32731d05cddcSAtari911                const timeA = a.time.split(":").map(Number);
32741d05cddcSAtari911                const timeB = b.time.split(":").map(Number);
32751d05cddcSAtari911                const minutesA = timeA[0] * 60 + timeA[1];
32761d05cddcSAtari911                const minutesB = timeB[0] * 60 + timeB[1];
32771d05cddcSAtari911
32781d05cddcSAtari911                return minutesA - minutesB;
32791d05cddcSAtari911            });
32801d05cddcSAtari911
32819ccd446eSAtari911            // Build events HTML with single color bar (event color only) - theme-aware
32829ccd446eSAtari911            const themeColors = window.themeColors_' . $jsCalId . ';
32831d05cddcSAtari911            sortedEvents.forEach(event => {
32849ccd446eSAtari911                const eventColor = event.color || themeColors.text_primary;
32851d05cddcSAtari911
32861d05cddcSAtari911                const eventDiv = document.createElement("div");
32879ccd446eSAtari911                eventDiv.style.cssText = "padding:4px 6px; border-bottom:1px solid " + themeColors.border_color + "; font-size:10px; display:flex; align-items:stretch; gap:6px; background:" + themeColors.event_bg + "; min-height:20px;";
32881d05cddcSAtari911
32891d05cddcSAtari911                let eventHTML = "";
32901d05cddcSAtari911
32919ccd446eSAtari911                // Event assigned color bar (single bar on left) - theme-aware shadow
32929ccd446eSAtari911                const barShadow = themeColors.bar_shadow + (themeColors.bar_shadow.includes("rgba") ? "" : " " + eventColor);
32939ccd446eSAtari911                eventHTML += "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:" + barShadow + ";\\"></div>";
32941d05cddcSAtari911
3295231d0edbSAtari911                // Content wrapper
3296231d0edbSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; align-items:start; gap:4px;\\">";
32971d05cddcSAtari911
3298231d0edbSAtari911                // Left side: event details
32991d05cddcSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0;\\">";
33009ccd446eSAtari911                eventHTML += "<div style=\\"font-weight:600; color:" + themeColors.text_primary + "; word-wrap:break-word; font-family:system-ui, sans-serif; " + themeColors.text_shadow + ";\\">";
33011d05cddcSAtari911
33021d05cddcSAtari911                // Time
33031d05cddcSAtari911                if (event.time) {
33041d05cddcSAtari911                    const timeParts = event.time.split(":");
33051d05cddcSAtari911                    let hours = parseInt(timeParts[0]);
33061d05cddcSAtari911                    const minutes = timeParts[1];
33071d05cddcSAtari911                    const ampm = hours >= 12 ? "PM" : "AM";
33081d05cddcSAtari911                    hours = hours % 12 || 12;
33099ccd446eSAtari911                    eventHTML += "<span style=\\"color:" + themeColors.text_bright + "; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> ";
33101d05cddcSAtari911                }
33111d05cddcSAtari911
33121d05cddcSAtari911                // Title - use HTML version if available
33131d05cddcSAtari911                const titleHTML = event.title_html || event.title || "Untitled";
33141d05cddcSAtari911                eventHTML += titleHTML;
33151d05cddcSAtari911                eventHTML += "</div>";
33161d05cddcSAtari911
33179ccd446eSAtari911                // Description if present - use HTML version - theme-aware color
33181d05cddcSAtari911                if (event.description_html || event.description) {
33191d05cddcSAtari911                    const descHTML = event.description_html || event.description;
33209ccd446eSAtari911                    eventHTML += "<div style=\\"font-size:9px; color:" + themeColors.text_dim + "; margin-top:2px;\\">" + descHTML + "</div>";
33211d05cddcSAtari911                }
33221d05cddcSAtari911
3323231d0edbSAtari911                eventHTML += "</div>"; // Close event details
3324231d0edbSAtari911
33259ccd446eSAtari911                // Right side: conflict badge with tooltip
3326231d0edbSAtari911                if (event.conflict) {
33279ccd446eSAtari911                    let conflictList = [];
33289ccd446eSAtari911                    if (event.conflictingWith && event.conflictingWith.length > 0) {
33299ccd446eSAtari911                        event.conflictingWith.forEach(conf => {
33309ccd446eSAtari911                            const confTime = conf.time + (conf.end_time ? " - " + conf.end_time : "");
33319ccd446eSAtari911                            conflictList.push(conf.title + " (" + confTime + ")");
33329ccd446eSAtari911                        });
33339ccd446eSAtari911                    }
33349ccd446eSAtari911                    const conflictData = btoa(unescape(encodeURIComponent(JSON.stringify(conflictList))));
33359ccd446eSAtari911                    eventHTML += "<span class=\\"event-conflict-badge\\" style=\\"font-size:10px;\\" data-conflicts=\\"" + conflictData + "\\" onmouseenter=\\"showConflictTooltip(this)\\" onmouseleave=\\"hideConflictTooltip()\\">⚠️ " + (event.conflictingWith ? event.conflictingWith.length : 1) + "</span>";
3336231d0edbSAtari911                }
3337231d0edbSAtari911
3338231d0edbSAtari911                eventHTML += "</div>"; // Close content wrapper
33391d05cddcSAtari911
33401d05cddcSAtari911                eventDiv.innerHTML = eventHTML;
33411d05cddcSAtari911                content.appendChild(eventDiv);
33421d05cddcSAtari911            });
33431d05cddcSAtari911
33441d05cddcSAtari911            container.style.display = "block";
33451d05cddcSAtari911        };
33461d05cddcSAtari911        ';
33471d05cddcSAtari911        $html .= '</script>';
33481d05cddcSAtari911
33491d05cddcSAtari911        return $html;
33501d05cddcSAtari911    }
33511d05cddcSAtari911
33521d05cddcSAtari911    /**
33531d05cddcSAtari911     * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders
33541d05cddcSAtari911     */
335596df7d3eSAtari911    private function renderSidebarSection($title, $events, $accentColor, $calId, $themeStyles, $theme, $importantNsList = ['important']) {
33561d05cddcSAtari911        // Keep the original accent colors for borders
33571d05cddcSAtari911        $borderColor = $accentColor;
33581d05cddcSAtari911
33591d05cddcSAtari911        // Show date for Important Events section
33601d05cddcSAtari911        $showDate = ($title === 'Important Events');
33611d05cddcSAtari911
33629ccd446eSAtari911        // Sort events differently based on section
33639ccd446eSAtari911        if ($title === 'Important Events') {
33649ccd446eSAtari911            // Important Events: sort by date first, then by time
33659ccd446eSAtari911            usort($events, function($a, $b) {
33669ccd446eSAtari911                $aDate = isset($a['date']) ? $a['date'] : '';
33679ccd446eSAtari911                $bDate = isset($b['date']) ? $b['date'] : '';
33681d05cddcSAtari911
33699ccd446eSAtari911                // Different dates - sort by date
33709ccd446eSAtari911                if ($aDate !== $bDate) {
33719ccd446eSAtari911                    return strcmp($aDate, $bDate);
33729ccd446eSAtari911                }
33739ccd446eSAtari911
33749ccd446eSAtari911                // Same date - sort by time
33759ccd446eSAtari911                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
33769ccd446eSAtari911                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
33779ccd446eSAtari911
33789ccd446eSAtari911                // All-day events last within same date
33799ccd446eSAtari911                if (empty($aTime) && !empty($bTime)) return 1;
33809ccd446eSAtari911                if (!empty($aTime) && empty($bTime)) return -1;
33819ccd446eSAtari911                if (empty($aTime) && empty($bTime)) return 0;
33829ccd446eSAtari911
33839ccd446eSAtari911                // Both have times
33849ccd446eSAtari911                $aMinutes = $this->timeToMinutes($aTime);
33859ccd446eSAtari911                $bMinutes = $this->timeToMinutes($bTime);
33869ccd446eSAtari911                return $aMinutes - $bMinutes;
33879ccd446eSAtari911            });
33889ccd446eSAtari911        } else {
33899ccd446eSAtari911            // Today/Tomorrow: sort by time only (all same date)
33909ccd446eSAtari911            usort($events, function($a, $b) {
33919ccd446eSAtari911                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
33929ccd446eSAtari911                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
33939ccd446eSAtari911
33949ccd446eSAtari911                // All-day events (no time) come first
33959ccd446eSAtari911                if (empty($aTime) && !empty($bTime)) return -1;
33969ccd446eSAtari911                if (!empty($aTime) && empty($bTime)) return 1;
33979ccd446eSAtari911                if (empty($aTime) && empty($bTime)) return 0;
33989ccd446eSAtari911
33999ccd446eSAtari911                // Both have times - convert to minutes for proper chronological sort
34009ccd446eSAtari911                $aMinutes = $this->timeToMinutes($aTime);
34019ccd446eSAtari911                $bMinutes = $this->timeToMinutes($bTime);
34029ccd446eSAtari911
34039ccd446eSAtari911                return $aMinutes - $bMinutes;
34049ccd446eSAtari911            });
34059ccd446eSAtari911        }
34069ccd446eSAtari911
34079ccd446eSAtari911        // Theme-aware section shadow
34087e8ea635SAtari911        $sectionShadow = ($theme === 'professional' || $theme === 'wiki') ?
34097e8ea635SAtari911                        '0 1px 3px rgba(0, 0, 0, 0.1)' :
34107e8ea635SAtari911                        '0 0 5px ' . $themeStyles['shadow'];
34119ccd446eSAtari911
34127e8ea635SAtari911        if ($theme === 'wiki') {
34137e8ea635SAtari911            // Wiki theme: use a background div for the left bar instead of border-left
34147e8ea635SAtari911            // Dark Reader maps border colors differently from background colors, causing mismatch
34157e8ea635SAtari911            $html = '<div style="display:flex; margin:8px 4px; box-shadow:' . $sectionShadow . '; background:' . $themeStyles['bg'] . ';">';
34167e8ea635SAtari911            $html .= '<div style="width:3px; flex-shrink:0; background:' . $borderColor . ';"></div>';
34177e8ea635SAtari911            $html .= '<div style="flex:1; min-width:0;">';
34187e8ea635SAtari911        } else {
34197e8ea635SAtari911            $html = '<div style="border-left:3px solid ' . $borderColor . ' !important; margin:8px 4px; box-shadow:' . $sectionShadow . ';">';
34207e8ea635SAtari911        }
34219ccd446eSAtari911
34227e8ea635SAtari911        // Section header with accent color background - theme-aware
34239ccd446eSAtari911        $headerShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $accentColor;
34247e8ea635SAtari911        $headerTextColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] :
34257e8ea635SAtari911                           (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff');
34267e8ea635SAtari911        if ($theme === 'wiki') {
34277e8ea635SAtari911            // Wiki theme: no !important — let Dark Reader adjust these
34289ccd446eSAtari911            $html .= '<div style="background:' . $accentColor . '; color:' . $headerTextColor . '; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:' . $headerShadow . ';">';
34297e8ea635SAtari911        } else {
34307e8ea635SAtari911            // Dark themes + professional: lock colors against Dark Reader
34317e8ea635SAtari911            $html .= '<div style="background:' . $accentColor . ' !important; color:' . $headerTextColor . ' !important; -webkit-text-fill-color:' . $headerTextColor . ' !important; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:' . $headerShadow . ';">';
34327e8ea635SAtari911        }
34331d05cddcSAtari911        $html .= htmlspecialchars($title);
34341d05cddcSAtari911        $html .= '</div>';
34351d05cddcSAtari911
34369ccd446eSAtari911        // Events - no background (transparent)
34379ccd446eSAtari911        $html .= '<div style="padding:4px 0;">';
34381d05cddcSAtari911
34391d05cddcSAtari911        foreach ($events as $event) {
344096df7d3eSAtari911            $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor, $themeStyles, $theme, $importantNsList);
34411d05cddcSAtari911        }
34421d05cddcSAtari911
34431d05cddcSAtari911        $html .= '</div>';
34441d05cddcSAtari911        $html .= '</div>';
34457e8ea635SAtari911        if ($theme === 'wiki') {
34467e8ea635SAtari911            $html .= '</div>'; // Close flex wrapper
34477e8ea635SAtari911        }
34481d05cddcSAtari911
34491d05cddcSAtari911        return $html;
34501d05cddcSAtari911    }
34511d05cddcSAtari911
34521d05cddcSAtari911    /**
34539ccd446eSAtari911     * Render individual event in sidebar - Theme-aware
34541d05cddcSAtari911     */
345596df7d3eSAtari911    private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07', $themeStyles = null, $theme = 'matrix', $importantNsList = ['important']) {
34561d05cddcSAtari911        $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
34571d05cddcSAtari911        $time = isset($event['time']) ? $event['time'] : '';
34581d05cddcSAtari911        $endTime = isset($event['endTime']) ? $event['endTime'] : '';
34599ccd446eSAtari911        $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : ($themeStyles ? $themeStyles['text_primary'] : '#00cc07');
34601d05cddcSAtari911        $date = isset($event['date']) ? $event['date'] : '';
34611d05cddcSAtari911        $isTask = isset($event['isTask']) && $event['isTask'];
34621d05cddcSAtari911        $completed = isset($event['completed']) && $event['completed'];
34631d05cddcSAtari911
346496df7d3eSAtari911        // Check if this is an important namespace event
346596df7d3eSAtari911        $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
346696df7d3eSAtari911        $isImportantNs = false;
346796df7d3eSAtari911        foreach ($importantNsList as $impNs) {
346896df7d3eSAtari911            if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
346996df7d3eSAtari911                $isImportantNs = true;
347096df7d3eSAtari911                break;
347196df7d3eSAtari911            }
347296df7d3eSAtari911        }
347396df7d3eSAtari911
34749ccd446eSAtari911        // Theme-aware colors
34759ccd446eSAtari911        $titleColor = $themeStyles ? $themeStyles['text_primary'] : '#00cc07';
34769ccd446eSAtari911        $timeColor = $themeStyles ? $themeStyles['text_bright'] : '#00dd00';
34777e8ea635SAtari911        $textShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $titleColor . ';' :
34787e8ea635SAtari911                      ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $titleColor . ';' : '');
34791d05cddcSAtari911
34809ccd446eSAtari911        // Check for conflicts (using 'conflict' field set by detectTimeConflicts)
34819ccd446eSAtari911        $hasConflict = isset($event['conflict']) && $event['conflict'];
34829ccd446eSAtari911        $conflictingWith = isset($event['conflictingWith']) ? $event['conflictingWith'] : [];
34839ccd446eSAtari911
34849ccd446eSAtari911        // Build conflict list for tooltip
34859ccd446eSAtari911        $conflictList = [];
34869ccd446eSAtari911        if ($hasConflict && !empty($conflictingWith)) {
34879ccd446eSAtari911            foreach ($conflictingWith as $conf) {
34889ccd446eSAtari911                $confTime = $this->formatTimeDisplay($conf['time'], isset($conf['end_time']) ? $conf['end_time'] : '');
34899ccd446eSAtari911                $conflictList[] = $conf['title'] . ' (' . $confTime . ')';
34909ccd446eSAtari911            }
34919ccd446eSAtari911        }
34929ccd446eSAtari911
349396df7d3eSAtari911        // No background on individual events (transparent) - unless important namespace
34949ccd446eSAtari911        // Use theme grid_border with slight opacity for subtle divider
34959ccd446eSAtari911        $borderColor = $themeStyles['grid_border'];
34969ccd446eSAtari911
349796df7d3eSAtari911        // Important namespace highlighting - subtle themed background
349896df7d3eSAtari911        $importantBg = '';
349996df7d3eSAtari911        $importantBorder = '';
350096df7d3eSAtari911        if ($isImportantNs) {
350196df7d3eSAtari911            // Theme-specific important highlighting
350296df7d3eSAtari911            switch ($theme) {
350396df7d3eSAtari911                case 'matrix':
350496df7d3eSAtari911                    $importantBg = 'background:rgba(0,204,7,0.08);';
350596df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
350696df7d3eSAtari911                    break;
350796df7d3eSAtari911                case 'purple':
350896df7d3eSAtari911                    $importantBg = 'background:rgba(156,39,176,0.08);';
350996df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(156,39,176,0.4);';
351096df7d3eSAtari911                    break;
351196df7d3eSAtari911                case 'pink':
351296df7d3eSAtari911                    $importantBg = 'background:rgba(255,105,180,0.1);';
351396df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(255,105,180,0.5);';
351496df7d3eSAtari911                    break;
351596df7d3eSAtari911                case 'professional':
351696df7d3eSAtari911                    $importantBg = 'background:rgba(33,150,243,0.08);';
351796df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(33,150,243,0.4);';
351896df7d3eSAtari911                    break;
351996df7d3eSAtari911                case 'wiki':
352096df7d3eSAtari911                    $importantBg = 'background:rgba(0,102,204,0.06);';
352196df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(0,102,204,0.3);';
352296df7d3eSAtari911                    break;
352396df7d3eSAtari911                default:
352496df7d3eSAtari911                    $importantBg = 'background:rgba(0,204,7,0.08);';
352596df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
352696df7d3eSAtari911            }
352796df7d3eSAtari911        }
352896df7d3eSAtari911
352996df7d3eSAtari911        $html = '<div style="padding:4px 6px; border-bottom:1px solid ' . $borderColor . ' !important; font-size:10px; display:flex; align-items:stretch; gap:6px; min-height:20px; ' . $importantBg . $importantBorder . '">';
35301d05cddcSAtari911
3531231d0edbSAtari911        // Event's assigned color bar (single bar on the left)
35329ccd446eSAtari911        $barShadow = ($theme === 'professional') ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . $eventColor;
35339ccd446eSAtari911        $html .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:' . $barShadow . ';"></div>';
35341d05cddcSAtari911
35351d05cddcSAtari911        // Content
35361d05cddcSAtari911        $html .= '<div style="flex:1; min-width:0;">';
35371d05cddcSAtari911
35381d05cddcSAtari911        // Time + title
35399ccd446eSAtari911        $html .= '<div style="font-weight:600; color:' . $titleColor . '; word-wrap:break-word; font-family:system-ui, sans-serif; ' . $textShadow . '">';
35401d05cddcSAtari911
35411d05cddcSAtari911        if ($time) {
35421d05cddcSAtari911            $displayTime = $this->formatTimeDisplay($time, $endTime);
35439ccd446eSAtari911            $html .= '<span style="color:' . $timeColor . '; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> ';
35441d05cddcSAtari911        }
35451d05cddcSAtari911
35461d05cddcSAtari911        // Task checkbox
35471d05cddcSAtari911        if ($isTask) {
35481d05cddcSAtari911            $checkIcon = $completed ? '☑' : '☐';
35499ccd446eSAtari911            $checkColor = $themeStyles ? $themeStyles['text_bright'] : '#00ff00';
35509ccd446eSAtari911            $html .= '<span style="font-size:11px; color:' . $checkColor . ';">' . $checkIcon . '</span> ';
35511d05cddcSAtari911        }
35521d05cddcSAtari911
355396df7d3eSAtari911        // Important indicator icon for important namespace events
355496df7d3eSAtari911        if ($isImportantNs) {
355596df7d3eSAtari911            $html .= '<span style="font-size:9px;" title="Important">⭐</span> ';
355696df7d3eSAtari911        }
355796df7d3eSAtari911
35589ccd446eSAtari911        $html .= $title; // Already HTML-escaped on line 2625
35591d05cddcSAtari911
35609ccd446eSAtari911        // Conflict badge using same system as main calendar
35619ccd446eSAtari911        if ($hasConflict && !empty($conflictList)) {
35629ccd446eSAtari911            $conflictJson = base64_encode(json_encode($conflictList));
35639ccd446eSAtari911            $html .= ' <span class="event-conflict-badge" style="font-size:10px;" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . count($conflictList) . '</span>';
35641d05cddcSAtari911        }
35651d05cddcSAtari911
35661d05cddcSAtari911        $html .= '</div>';
35671d05cddcSAtari911
35681d05cddcSAtari911        // Date display BELOW event name for Important events
35691d05cddcSAtari911        if ($showDate && $date) {
35701d05cddcSAtari911            $dateObj = new DateTime($date);
35711d05cddcSAtari911            $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10"
35729ccd446eSAtari911            $dateColor = $themeStyles ? $themeStyles['text_dim'] : '#00aa00';
35737e8ea635SAtari911            $dateShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $dateColor . ';' :
35747e8ea635SAtari911                          ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $dateColor . ';' : '');
35759ccd446eSAtari911            $html .= '<div style="font-size:8px; color:' . $dateColor . '; font-weight:500; margin-top:2px; ' . $dateShadow . '">' . htmlspecialchars($displayDate) . '</div>';
35761d05cddcSAtari911        }
35771d05cddcSAtari911
35781d05cddcSAtari911        $html .= '</div>';
35791d05cddcSAtari911        $html .= '</div>';
35801d05cddcSAtari911
35811d05cddcSAtari911        return $html;
35821d05cddcSAtari911    }
35831d05cddcSAtari911
35841d05cddcSAtari911    /**
35851d05cddcSAtari911     * Format time display (12-hour format with optional end time)
35861d05cddcSAtari911     */
35871d05cddcSAtari911    private function formatTimeDisplay($startTime, $endTime = '') {
35881d05cddcSAtari911        // Convert start time
35891d05cddcSAtari911        list($hour, $minute) = explode(':', $startTime);
35901d05cddcSAtari911        $hour = (int)$hour;
35911d05cddcSAtari911        $ampm = $hour >= 12 ? 'PM' : 'AM';
35921d05cddcSAtari911        $displayHour = $hour % 12;
35931d05cddcSAtari911        if ($displayHour === 0) $displayHour = 12;
35941d05cddcSAtari911
35951d05cddcSAtari911        $display = $displayHour . ':' . $minute . ' ' . $ampm;
35961d05cddcSAtari911
35971d05cddcSAtari911        // Add end time if provided
35981d05cddcSAtari911        if ($endTime && $endTime !== '') {
35991d05cddcSAtari911            list($endHour, $endMinute) = explode(':', $endTime);
36001d05cddcSAtari911            $endHour = (int)$endHour;
36011d05cddcSAtari911            $endAmpm = $endHour >= 12 ? 'PM' : 'AM';
36021d05cddcSAtari911            $endDisplayHour = $endHour % 12;
36031d05cddcSAtari911            if ($endDisplayHour === 0) $endDisplayHour = 12;
36041d05cddcSAtari911
36051d05cddcSAtari911            $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm;
36061d05cddcSAtari911        }
36071d05cddcSAtari911
36081d05cddcSAtari911        return $display;
36091d05cddcSAtari911    }
36101d05cddcSAtari911
36111d05cddcSAtari911    /**
36129ccd446eSAtari911     * Detect time conflicts among events on the same day
36139ccd446eSAtari911     * Returns events array with 'conflict' flag and 'conflictingWith' array
36149ccd446eSAtari911     */
36159ccd446eSAtari911    private function detectTimeConflicts($dayEvents) {
36169ccd446eSAtari911        if (empty($dayEvents)) {
36179ccd446eSAtari911            return $dayEvents;
36189ccd446eSAtari911        }
36199ccd446eSAtari911
36209ccd446eSAtari911        // If only 1 event, no conflicts possible but still add the flag
36219ccd446eSAtari911        if (count($dayEvents) === 1) {
36229ccd446eSAtari911            return [array_merge($dayEvents[0], ['conflict' => false, 'conflictingWith' => []])];
36239ccd446eSAtari911        }
36249ccd446eSAtari911
36259ccd446eSAtari911        $eventsWithFlags = [];
36269ccd446eSAtari911
36279ccd446eSAtari911        foreach ($dayEvents as $i => $event) {
36289ccd446eSAtari911            $hasConflict = false;
36299ccd446eSAtari911            $conflictingWith = [];
36309ccd446eSAtari911
36319ccd446eSAtari911            // Skip all-day events (no time)
36329ccd446eSAtari911            if (empty($event['time'])) {
36339ccd446eSAtari911                $eventsWithFlags[] = array_merge($event, ['conflict' => false, 'conflictingWith' => []]);
36349ccd446eSAtari911                continue;
36359ccd446eSAtari911            }
36369ccd446eSAtari911
36379ccd446eSAtari911            // Get this event's time range
36389ccd446eSAtari911            $startTime = $event['time'];
36399ccd446eSAtari911            // Check both 'end_time' (snake_case) and 'endTime' (camelCase) for compatibility
36409ccd446eSAtari911            $endTime = '';
36419ccd446eSAtari911            if (isset($event['end_time']) && $event['end_time'] !== '') {
36429ccd446eSAtari911                $endTime = $event['end_time'];
36439ccd446eSAtari911            } elseif (isset($event['endTime']) && $event['endTime'] !== '') {
36449ccd446eSAtari911                $endTime = $event['endTime'];
36459ccd446eSAtari911            } else {
36469ccd446eSAtari911                // If no end time, use start time (zero duration) - matches main calendar logic
36479ccd446eSAtari911                $endTime = $startTime;
36489ccd446eSAtari911            }
36499ccd446eSAtari911
36509ccd446eSAtari911            // Check against all other events
36519ccd446eSAtari911            foreach ($dayEvents as $j => $otherEvent) {
36529ccd446eSAtari911                if ($i === $j) continue; // Skip self
36539ccd446eSAtari911                if (empty($otherEvent['time'])) continue; // Skip all-day events
36549ccd446eSAtari911
36559ccd446eSAtari911                $otherStart = $otherEvent['time'];
36569ccd446eSAtari911                // Check both field name formats
36579ccd446eSAtari911                $otherEnd = '';
36589ccd446eSAtari911                if (isset($otherEvent['end_time']) && $otherEvent['end_time'] !== '') {
36599ccd446eSAtari911                    $otherEnd = $otherEvent['end_time'];
36609ccd446eSAtari911                } elseif (isset($otherEvent['endTime']) && $otherEvent['endTime'] !== '') {
36619ccd446eSAtari911                    $otherEnd = $otherEvent['endTime'];
36629ccd446eSAtari911                } else {
36639ccd446eSAtari911                    $otherEnd = $otherStart;
36649ccd446eSAtari911                }
36659ccd446eSAtari911
36669ccd446eSAtari911                // Check for overlap: convert to minutes and compare
36679ccd446eSAtari911                $start1Min = $this->timeToMinutes($startTime);
36689ccd446eSAtari911                $end1Min = $this->timeToMinutes($endTime);
36699ccd446eSAtari911                $start2Min = $this->timeToMinutes($otherStart);
36709ccd446eSAtari911                $end2Min = $this->timeToMinutes($otherEnd);
36719ccd446eSAtari911
36729ccd446eSAtari911                // Overlap if: start1 < end2 AND start2 < end1
36739ccd446eSAtari911                // Note: Using < (not <=) so events that just touch at boundaries don't conflict
36749ccd446eSAtari911                // e.g., 1:00-2:00 and 2:00-3:00 are NOT in conflict
36759ccd446eSAtari911                if ($start1Min < $end2Min && $start2Min < $end1Min) {
36769ccd446eSAtari911                    $hasConflict = true;
36779ccd446eSAtari911                    $conflictingWith[] = [
36789ccd446eSAtari911                        'title' => isset($otherEvent['title']) ? $otherEvent['title'] : 'Untitled',
36799ccd446eSAtari911                        'time' => $otherStart,
36809ccd446eSAtari911                        'end_time' => $otherEnd
36819ccd446eSAtari911                    ];
36829ccd446eSAtari911                }
36839ccd446eSAtari911            }
36849ccd446eSAtari911
36859ccd446eSAtari911            $eventsWithFlags[] = array_merge($event, [
36869ccd446eSAtari911                'conflict' => $hasConflict,
36879ccd446eSAtari911                'conflictingWith' => $conflictingWith
36889ccd446eSAtari911            ]);
36899ccd446eSAtari911        }
36909ccd446eSAtari911
36919ccd446eSAtari911        return $eventsWithFlags;
36929ccd446eSAtari911    }
36939ccd446eSAtari911
36949ccd446eSAtari911    /**
36959ccd446eSAtari911     * Add hours to a time string
36969ccd446eSAtari911     */
36979ccd446eSAtari911    private function addHoursToTime($time, $hours) {
36989ccd446eSAtari911        $totalMinutes = $this->timeToMinutes($time) + ($hours * 60);
36999ccd446eSAtari911        $h = floor($totalMinutes / 60) % 24;
37009ccd446eSAtari911        $m = $totalMinutes % 60;
37019ccd446eSAtari911        return sprintf('%02d:%02d', $h, $m);
37029ccd446eSAtari911    }
37039ccd446eSAtari911
37049ccd446eSAtari911    /**
37051d05cddcSAtari911     * Render DokuWiki syntax to HTML
37061d05cddcSAtari911     * Converts **bold**, //italic//, [[links]], etc. to HTML
37071d05cddcSAtari911     */
37081d05cddcSAtari911    private function renderDokuWikiToHtml($text) {
37091d05cddcSAtari911        if (empty($text)) return '';
37101d05cddcSAtari911
37111d05cddcSAtari911        // Use DokuWiki's parser to render the text
37121d05cddcSAtari911        $instructions = p_get_instructions($text);
37131d05cddcSAtari911
37141d05cddcSAtari911        // Render instructions to XHTML
37151d05cddcSAtari911        $xhtml = p_render('xhtml', $instructions, $info);
37161d05cddcSAtari911
37171d05cddcSAtari911        // Remove surrounding <p> tags if present (we're rendering inline)
37181d05cddcSAtari911        $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml));
37191d05cddcSAtari911
37201d05cddcSAtari911        return $xhtml;
37211d05cddcSAtari911    }
37221d05cddcSAtari911
37231d05cddcSAtari911    // Keep old scanForNamespaces for backward compatibility (not used anymore)
37241d05cddcSAtari911    private function scanForNamespaces($dir, $baseNamespace, &$namespaces) {
37251d05cddcSAtari911        if (!is_dir($dir)) return;
37261d05cddcSAtari911
37271d05cddcSAtari911        $items = scandir($dir);
37281d05cddcSAtari911        foreach ($items as $item) {
37291d05cddcSAtari911            if ($item === '.' || $item === '..' || $item === 'calendar') continue;
37301d05cddcSAtari911
37311d05cddcSAtari911            $path = $dir . $item;
37321d05cddcSAtari911            if (is_dir($path)) {
37331d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
37341d05cddcSAtari911                $namespaces[] = $namespace;
37351d05cddcSAtari911                $this->scanForNamespaces($path . '/', $namespace, $namespaces);
37361d05cddcSAtari911            }
37371d05cddcSAtari911        }
37381d05cddcSAtari911    }
37399ccd446eSAtari911
37409ccd446eSAtari911    /**
37419ccd446eSAtari911     * Get current sidebar theme
37429ccd446eSAtari911     */
37439ccd446eSAtari911    private function getSidebarTheme() {
37442866e827SAtari911        $configFile = $this->metaDir() . 'calendar_theme.txt';
37459ccd446eSAtari911        if (file_exists($configFile)) {
37469ccd446eSAtari911            $theme = trim(file_get_contents($configFile));
37479ccd446eSAtari911            if (in_array($theme, ['matrix', 'purple', 'professional', 'pink', 'wiki'])) {
37489ccd446eSAtari911                return $theme;
37499ccd446eSAtari911            }
37509ccd446eSAtari911        }
37519ccd446eSAtari911        return 'matrix'; // Default
37529ccd446eSAtari911    }
37539ccd446eSAtari911
37549ccd446eSAtari911    /**
37559ccd446eSAtari911     * Get colors from DokuWiki template's style.ini file
37569ccd446eSAtari911     */
37579ccd446eSAtari911    private function getWikiTemplateColors() {
37589ccd446eSAtari911        global $conf;
37599ccd446eSAtari911
37609ccd446eSAtari911        // Get current template name
37619ccd446eSAtari911        $template = $conf['template'];
37629ccd446eSAtari911
37632866e827SAtari911        // Try multiple possible locations for style.ini (farm-safe)
37649ccd446eSAtari911        $possiblePaths = [
37659ccd446eSAtari911            DOKU_INC . 'lib/tpl/' . $template . '/style.ini',
37669ccd446eSAtari911        ];
37672866e827SAtari911        // Add farm-specific conf override path if available
37682866e827SAtari911        if (!empty($conf['savedir'])) {
37692866e827SAtari911            array_unshift($possiblePaths, $conf['savedir'] . '/tpl/' . $template . '/style.ini');
37702866e827SAtari911        }
37712866e827SAtari911        array_unshift($possiblePaths, DOKU_INC . 'conf/tpl/' . $template . '/style.ini');
37729ccd446eSAtari911
37739ccd446eSAtari911        $styleIni = null;
37749ccd446eSAtari911        foreach ($possiblePaths as $path) {
37759ccd446eSAtari911            if (file_exists($path)) {
37769ccd446eSAtari911                $styleIni = parse_ini_file($path, true);
37779ccd446eSAtari911                break;
37789ccd446eSAtari911            }
37799ccd446eSAtari911        }
37809ccd446eSAtari911
37819ccd446eSAtari911        if (!$styleIni) {
37829ccd446eSAtari911            return null; // Fall back to CSS variables
37839ccd446eSAtari911        }
37849ccd446eSAtari911
37859ccd446eSAtari911        // Extract color replacements
37869ccd446eSAtari911        $replacements = isset($styleIni['replacements']) ? $styleIni['replacements'] : [];
37879ccd446eSAtari911
37889ccd446eSAtari911        // Map style.ini colors to our theme structure
37899ccd446eSAtari911        $bgSite = isset($replacements['__background_site__']) ? $replacements['__background_site__'] : '#f5f5f5';
37909ccd446eSAtari911        $background = isset($replacements['__background__']) ? $replacements['__background__'] : '#fff';
37919ccd446eSAtari911        $bgAlt = isset($replacements['__background_alt__']) ? $replacements['__background_alt__'] : '#e8e8e8';
37929ccd446eSAtari911        $bgNeu = isset($replacements['__background_neu__']) ? $replacements['__background_neu__'] : '#eee';
37939ccd446eSAtari911        $text = isset($replacements['__text__']) ? $replacements['__text__'] : '#333';
37949ccd446eSAtari911        $textAlt = isset($replacements['__text_alt__']) ? $replacements['__text_alt__'] : '#999';
37959ccd446eSAtari911        $textNeu = isset($replacements['__text_neu__']) ? $replacements['__text_neu__'] : '#666';
37969ccd446eSAtari911        $border = isset($replacements['__border__']) ? $replacements['__border__'] : '#ccc';
37979ccd446eSAtari911        $link = isset($replacements['__link__']) ? $replacements['__link__'] : '#2b73b7';
37989ccd446eSAtari911        $existing = isset($replacements['__existing__']) ? $replacements['__existing__'] : $link;
37999ccd446eSAtari911
38009ccd446eSAtari911        // Build theme colors from template colors
38019ccd446eSAtari911        // ============================================
38029ccd446eSAtari911        // DokuWiki style.ini → Calendar CSS Variable Mapping
38039ccd446eSAtari911        // ============================================
38049ccd446eSAtari911        //   style.ini key         → CSS variable          → Used for
38059ccd446eSAtari911        //   __background_site__   → --background-site     → Container, panel backgrounds
38069ccd446eSAtari911        //   __background__        → --cell-bg             → Cell/input backgrounds (typically white)
38079ccd446eSAtari911        //   __background_alt__    → --background-alt      → Hover states, header backgrounds
38089ccd446eSAtari911        //                         → --background-header
38099ccd446eSAtari911        //   __background_neu__    → --cell-today-bg       → Today cell highlight
38109ccd446eSAtari911        //   __text__              → --text-primary        → Primary text, labels, titles
38119ccd446eSAtari911        //   __text_neu__          → --text-dim            → Secondary text, dates, descriptions
38129ccd446eSAtari911        //   __text_alt__          → (not mapped)          → Available for future use
38139ccd446eSAtari911        //   __border__            → --border-color        → Grid lines, input borders
38147e8ea635SAtari911        //                         → --border-main         → Accent color: buttons, badges, active elements, section headers
38159ccd446eSAtari911        //                         → --header-border
38167e8ea635SAtari911        //   __link__              → --text-bright         → Links, accent text
38179ccd446eSAtari911        //   __existing__          → (fallback to __link__)→ Available for future use
38189ccd446eSAtari911        //
38199ccd446eSAtari911        // To customize: edit your template's conf/style.ini [replacements]
38209ccd446eSAtari911        return [
38219ccd446eSAtari911            'bg' => $bgSite,
38227e8ea635SAtari911            'border' => $border,         // Accent color from template border
38239ccd446eSAtari911            'shadow' => 'rgba(0, 0, 0, 0.1)',
38249ccd446eSAtari911            'header_bg' => $bgAlt,       // Headers use alt background
38259ccd446eSAtari911            'header_border' => $border,
38269ccd446eSAtari911            'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
38279ccd446eSAtari911            'text_primary' => $text,
38289ccd446eSAtari911            'text_bright' => $link,
38299ccd446eSAtari911            'text_dim' => $textNeu,
38309ccd446eSAtari911            'grid_bg' => $bgSite,
38319ccd446eSAtari911            'grid_border' => $border,
38329ccd446eSAtari911            'cell_bg' => $background,    // Cells use __background__ (white/light)
38339ccd446eSAtari911            'cell_today_bg' => $bgNeu,
38349ccd446eSAtari911            'bar_glow' => '0 1px 2px',
383596df7d3eSAtari911            'pastdue_color' => '#e74c3c',
383696df7d3eSAtari911            'pastdue_bg' => '#ffe6e6',
383796df7d3eSAtari911            'pastdue_bg_strong' => '#ffd9d9',
383896df7d3eSAtari911            'pastdue_bg_light' => '#fff2f2',
383996df7d3eSAtari911            'tomorrow_bg' => '#fff9e6',
384096df7d3eSAtari911            'tomorrow_bg_strong' => '#fff4cc',
384196df7d3eSAtari911            'tomorrow_bg_light' => '#fffbf0',
38429ccd446eSAtari911        ];
38439ccd446eSAtari911    }
38449ccd446eSAtari911
38459ccd446eSAtari911    /**
38469ccd446eSAtari911     * Get theme-specific color styles
38479ccd446eSAtari911     */
38489ccd446eSAtari911    private function getSidebarThemeStyles($theme) {
38499ccd446eSAtari911        // For wiki theme, try to read colors from template's style.ini
38509ccd446eSAtari911        if ($theme === 'wiki') {
38519ccd446eSAtari911            $wikiColors = $this->getWikiTemplateColors();
38529ccd446eSAtari911            if (!empty($wikiColors)) {
38539ccd446eSAtari911                return $wikiColors;
38549ccd446eSAtari911            }
38559ccd446eSAtari911            // Fall through to default wiki colors if reading fails
38569ccd446eSAtari911        }
38579ccd446eSAtari911
38589ccd446eSAtari911        $themes = [
38599ccd446eSAtari911            'matrix' => [
38609ccd446eSAtari911                'bg' => '#242424',
38619ccd446eSAtari911                'border' => '#00cc07',
38629ccd446eSAtari911                'shadow' => 'rgba(0, 204, 7, 0.3)',
38639ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2a2a2a 0%, #242424 100%)',
38649ccd446eSAtari911                'header_border' => '#00cc07',
38659ccd446eSAtari911                'header_shadow' => '0 2px 8px rgba(0, 204, 7, 0.3)',
38669ccd446eSAtari911                'text_primary' => '#00cc07',
38679ccd446eSAtari911                'text_bright' => '#00ff00',
38689ccd446eSAtari911                'text_dim' => '#00aa00',
38699ccd446eSAtari911                'grid_bg' => '#1a3d1a',
38709ccd446eSAtari911                'grid_border' => '#00cc07',
38719ccd446eSAtari911                'cell_bg' => '#242424',
38729ccd446eSAtari911                'cell_today_bg' => '#2a4d2a',
38739ccd446eSAtari911                'bar_glow' => '0 0 3px',
38747e8ea635SAtari911                'pastdue_color' => '#e74c3c',
38757e8ea635SAtari911                'pastdue_bg' => '#3d1a1a',
38767e8ea635SAtari911                'pastdue_bg_strong' => '#4d2020',
38777e8ea635SAtari911                'pastdue_bg_light' => '#2d1515',
38787e8ea635SAtari911                'tomorrow_bg' => '#3d3d1a',
38797e8ea635SAtari911                'tomorrow_bg_strong' => '#4d4d20',
38807e8ea635SAtari911                'tomorrow_bg_light' => '#2d2d15',
38819ccd446eSAtari911            ],
38829ccd446eSAtari911            'purple' => [
38839ccd446eSAtari911                'bg' => '#2a2030',
38849ccd446eSAtari911                'border' => '#9b59b6',
38859ccd446eSAtari911                'shadow' => 'rgba(155, 89, 182, 0.3)',
38869ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2f2438 0%, #2a2030 100%)',
38879ccd446eSAtari911                'header_border' => '#9b59b6',
38889ccd446eSAtari911                'header_shadow' => '0 2px 8px rgba(155, 89, 182, 0.3)',
38899ccd446eSAtari911                'text_primary' => '#b19cd9',
38909ccd446eSAtari911                'text_bright' => '#d4a5ff',
38919ccd446eSAtari911                'text_dim' => '#8e7ab8',
38929ccd446eSAtari911                'grid_bg' => '#3d2b4d',
38939ccd446eSAtari911                'grid_border' => '#9b59b6',
38949ccd446eSAtari911                'cell_bg' => '#2a2030',
38959ccd446eSAtari911                'cell_today_bg' => '#3d2b4d',
38969ccd446eSAtari911                'bar_glow' => '0 0 3px',
38977e8ea635SAtari911                'pastdue_color' => '#e74c3c',
38987e8ea635SAtari911                'pastdue_bg' => '#3d1a2a',
38997e8ea635SAtari911                'pastdue_bg_strong' => '#4d2035',
39007e8ea635SAtari911                'pastdue_bg_light' => '#2d1520',
39017e8ea635SAtari911                'tomorrow_bg' => '#3d3520',
39027e8ea635SAtari911                'tomorrow_bg_strong' => '#4d4028',
39037e8ea635SAtari911                'tomorrow_bg_light' => '#2d2a18',
39049ccd446eSAtari911            ],
39059ccd446eSAtari911            'professional' => [
39069ccd446eSAtari911                'bg' => '#f5f7fa',
39079ccd446eSAtari911                'border' => '#4a90e2',
39089ccd446eSAtari911                'shadow' => 'rgba(74, 144, 226, 0.2)',
39099ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #ffffff 0%, #f5f7fa 100%)',
39109ccd446eSAtari911                'header_border' => '#4a90e2',
39119ccd446eSAtari911                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
39129ccd446eSAtari911                'text_primary' => '#2c3e50',
39139ccd446eSAtari911                'text_bright' => '#4a90e2',
39149ccd446eSAtari911                'text_dim' => '#7f8c8d',
39159ccd446eSAtari911                'grid_bg' => '#e8ecf1',
39169ccd446eSAtari911                'grid_border' => '#d0d7de',
39179ccd446eSAtari911                'cell_bg' => '#ffffff',
39189ccd446eSAtari911                'cell_today_bg' => '#dce8f7',
39199ccd446eSAtari911                'bar_glow' => '0 1px 2px',
39207e8ea635SAtari911                'pastdue_color' => '#e74c3c',
39217e8ea635SAtari911                'pastdue_bg' => '#ffe6e6',
39227e8ea635SAtari911                'pastdue_bg_strong' => '#ffd9d9',
39237e8ea635SAtari911                'pastdue_bg_light' => '#fff2f2',
39247e8ea635SAtari911                'tomorrow_bg' => '#fff9e6',
39257e8ea635SAtari911                'tomorrow_bg_strong' => '#fff4cc',
39267e8ea635SAtari911                'tomorrow_bg_light' => '#fffbf0',
39279ccd446eSAtari911            ],
39289ccd446eSAtari911            'pink' => [
39299ccd446eSAtari911                'bg' => '#1a0d14',
39309ccd446eSAtari911                'border' => '#ff1493',
39319ccd446eSAtari911                'shadow' => 'rgba(255, 20, 147, 0.4)',
39329ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2d1a24 0%, #1a0d14 100%)',
39339ccd446eSAtari911                'header_border' => '#ff1493',
39349ccd446eSAtari911                'header_shadow' => '0 0 12px rgba(255, 20, 147, 0.6)',
39359ccd446eSAtari911                'text_primary' => '#ff69b4',
39369ccd446eSAtari911                'text_bright' => '#ff1493',
39379ccd446eSAtari911                'text_dim' => '#ff85c1',
39389ccd446eSAtari911                'grid_bg' => '#2d1a24',
39399ccd446eSAtari911                'grid_border' => '#ff1493',
39409ccd446eSAtari911                'cell_bg' => '#1a0d14',
39419ccd446eSAtari911                'cell_today_bg' => '#3d2030',
39429ccd446eSAtari911                'bar_glow' => '0 0 5px',
39437e8ea635SAtari911                'pastdue_color' => '#e74c3c',
39447e8ea635SAtari911                'pastdue_bg' => '#3d1520',
39457e8ea635SAtari911                'pastdue_bg_strong' => '#4d1a28',
39467e8ea635SAtari911                'pastdue_bg_light' => '#2d1018',
39477e8ea635SAtari911                'tomorrow_bg' => '#3d3020',
39487e8ea635SAtari911                'tomorrow_bg_strong' => '#4d3a28',
39497e8ea635SAtari911                'tomorrow_bg_light' => '#2d2518',
39509ccd446eSAtari911            ],
39519ccd446eSAtari911            'wiki' => [
39529ccd446eSAtari911                'bg' => '#f5f5f5',
39537e8ea635SAtari911                'border' => '#ccc',          // Template __border__ color
39549ccd446eSAtari911                'shadow' => 'rgba(0, 0, 0, 0.1)',
39559ccd446eSAtari911                'header_bg' => '#e8e8e8',
39569ccd446eSAtari911                'header_border' => '#ccc',
39579ccd446eSAtari911                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
39589ccd446eSAtari911                'text_primary' => '#333',
39597e8ea635SAtari911                'text_bright' => '#2b73b7',  // Template __link__ color
39609ccd446eSAtari911                'text_dim' => '#666',
39619ccd446eSAtari911                'grid_bg' => '#f5f5f5',
39629ccd446eSAtari911                'grid_border' => '#ccc',
39639ccd446eSAtari911                'cell_bg' => '#fff',
39649ccd446eSAtari911                'cell_today_bg' => '#eee',
39659ccd446eSAtari911                'bar_glow' => '0 1px 2px',
39667e8ea635SAtari911                'pastdue_color' => '#e74c3c',
39677e8ea635SAtari911                'pastdue_bg' => '#ffe6e6',
39687e8ea635SAtari911                'pastdue_bg_strong' => '#ffd9d9',
39697e8ea635SAtari911                'pastdue_bg_light' => '#fff2f2',
39707e8ea635SAtari911                'tomorrow_bg' => '#fff9e6',
39717e8ea635SAtari911                'tomorrow_bg_strong' => '#fff4cc',
39727e8ea635SAtari911                'tomorrow_bg_light' => '#fffbf0',
39739ccd446eSAtari911            ],
39749ccd446eSAtari911        ];
39759ccd446eSAtari911
39769ccd446eSAtari911        return isset($themes[$theme]) ? $themes[$theme] : $themes['matrix'];
39779ccd446eSAtari911    }
39789ccd446eSAtari911
39799ccd446eSAtari911    /**
39809ccd446eSAtari911     * Get week start day preference
39819ccd446eSAtari911     */
39829ccd446eSAtari911    private function getWeekStartDay() {
39832866e827SAtari911        $configFile = $this->metaDir() . 'calendar_week_start.txt';
39849ccd446eSAtari911        if (file_exists($configFile)) {
39859ccd446eSAtari911            $start = trim(file_get_contents($configFile));
39869ccd446eSAtari911            if (in_array($start, ['monday', 'sunday'])) {
39879ccd446eSAtari911                return $start;
39889ccd446eSAtari911            }
39899ccd446eSAtari911        }
39909ccd446eSAtari911        return 'sunday'; // Default to Sunday (US/Canada standard)
39919ccd446eSAtari911    }
399296df7d3eSAtari911
399396df7d3eSAtari911    /**
399496df7d3eSAtari911     * Get itinerary collapsed default state
399596df7d3eSAtari911     */
399696df7d3eSAtari911    private function getItineraryCollapsed() {
39972866e827SAtari911        $configFile = $this->metaDir() . 'calendar_itinerary_collapsed.txt';
399896df7d3eSAtari911        if (file_exists($configFile)) {
399996df7d3eSAtari911            return trim(file_get_contents($configFile)) === 'yes';
400096df7d3eSAtari911        }
400196df7d3eSAtari911        return false; // Default to expanded
400296df7d3eSAtari911    }
40030b7aadb5SAtari911
40040b7aadb5SAtari911    /**
400564a96c92SAtari911     * Get default search scope (month or all)
400664a96c92SAtari911     */
400764a96c92SAtari911    private function getSearchDefault() {
40082866e827SAtari911        $configFile = $this->metaDir() . 'calendar_search_default.txt';
400964a96c92SAtari911        if (file_exists($configFile)) {
401064a96c92SAtari911            $value = trim(file_get_contents($configFile));
401164a96c92SAtari911            if (in_array($value, ['month', 'all'])) {
401264a96c92SAtari911                return $value;
401364a96c92SAtari911            }
401464a96c92SAtari911        }
401564a96c92SAtari911        return 'month'; // Default to month search
401664a96c92SAtari911    }
40172866e827SAtari911
40182866e827SAtari911    /**
40192866e827SAtari911     * Parse exclude parameter into an array of namespace strings
40202866e827SAtari911     * Supports semicolon-separated list: "journal;drafts;personal:private"
40212866e827SAtari911     */
40222866e827SAtari911    private function parseExcludeList($exclude) {
40232866e827SAtari911        if (empty($exclude)) return [];
40242866e827SAtari911        return array_filter(array_map('trim', explode(';', $exclude)), function($v) {
40252866e827SAtari911            return $v !== '';
40262866e827SAtari911        });
40272866e827SAtari911    }
40282866e827SAtari911
40292866e827SAtari911    /**
40302866e827SAtari911     * Check if a namespace should be excluded
40312866e827SAtari911     * Matches exact names and prefixes (e.g., exclude "journal" also excludes "journal:sub")
40322866e827SAtari911     */
40332866e827SAtari911    private function isNamespaceExcluded($namespace, $excludeList) {
40342866e827SAtari911        if (empty($excludeList) || $namespace === '') return false;
40352866e827SAtari911        foreach ($excludeList as $excluded) {
40362866e827SAtari911            // Exact match
40372866e827SAtari911            if ($namespace === $excluded) return true;
40382866e827SAtari911            // Prefix match: "journal" excludes "journal:sub:deep"
40392866e827SAtari911            if (strpos($namespace, $excluded . ':') === 0) return true;
40402866e827SAtari911        }
40412866e827SAtari911        return false;
40422866e827SAtari911    }
404319378907SAtari911}