← Calendar results

Calendar

Qwen 3.6 27b MTP · full

Initial view of Calendar

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 75,998 bytes · SHA-256 ceb022fbeef1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar & Event Planner</title>
<style>
/* ============================================================
   CONSTANTS & CSS CUSTOM PROPERTIES
   ============================================================ */
:root {
  --color-primary: #1a73e8;
  --color-primary-light: #e8f0fe;
  --color-today-bg: #e8f0fe;
  --color-today-border: #1a73e8;
  --color-text: #202124;
  --color-text-secondary: #5f6368;
  --color-text-dimmed: #bbb;
  --color-bg: #fff;
  --color-bg-hover: #f1f3f4;
  --color-border: #dadce0;
  --color-grid-line: #e8eaed;
  --color-header-bg: #fff;
  --shadow-sm: 0 1px 2px rgba(0,0,0,.1);
  --shadow-md: 0 2px 8px rgba(0,0,0,.15);
  --shadow-lg: 0 4px 16px rgba(0,0,0,.2);
  --radius-sm: 4px;
  --radius-md: 8px;
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;

  /* Event colors */
  --ev-blue: #1a73e8;
  --ev-red: #d93025;
  --ev-green: #1e8e3e;
  --ev-yellow: #f9ab00;
  --ev-purple: #9334e6;
  --ev-teal: #12b5cb;
  --ev-orange: #ea8600;
  --ev-pink: #d91e78;

  /* Layout */
  --hour-height: 50px;
  --sidebar-width: 70px;
  --all-day-height: 32px;
}

/* ============================================================
   RESET & BASE
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; font-family: var(--font-sans); color: var(--color-text); background: var(--color-bg); overflow: hidden; }
button { font-family: inherit; cursor: pointer; border: none; background: none; font-size: inherit; color: inherit; }
input, select, textarea { font-family: inherit; font-size: 14px; }

/* ============================================================
   LAYOUT SHELL
   ============================================================ */
#app { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }

/* Header */
.header { display: flex; align-items: center; padding: 8px 16px; border-bottom: 1px solid var(--color-border); background: var(--color-header-bg); gap: 8px; flex-shrink: 0; z-index: 10; }
.header-left { display: flex; align-items: center; gap: 8px; }
.header-center { flex: 1; text-align: center; }
.header-right { display: flex; align-items: center; gap: 8px; }

.btn-icon { width: 36px; height: 36px; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; font-size: 18px; transition: background .15s; }
.btn-icon:hover { background: var(--color-bg-hover); }
.btn-text { padding: 6px 12px; border-radius: var(--radius-sm); font-size: 14px; font-weight: 500; transition: background .15s; }
.btn-text:hover { background: var(--color-bg-hover); }

.today-btn { background: var(--color-primary); color: #fff !important; border-radius: var(--radius-sm); padding: 6px 14px; font-weight: 500; font-size: 13px; }
.today-btn:hover { opacity: .9; }

.nav-title { font-size: 22px; font-weight: 400; cursor: pointer; min-width: 200px; text-align: center; user-select: none; }
.nav-title:hover { color: var(--color-primary); }

.view-switcher { display: flex; border: 1px solid var(--color-border); border-radius: var(--radius-sm); overflow: hidden; }
.view-btn { padding: 6px 14px; font-size: 13px; font-weight: 500; color: var(--color-text-secondary); transition: all .15s; }
.view-btn.active { background: var(--color-primary-light); color: var(--color-primary); }
.view-btn:hover:not(.active) { background: var(--color-bg-hover); }

/* Main content area */
.main-content { flex: 1; display: flex; overflow: hidden; position: relative; }
.view-container { flex: 1; overflow: auto; position: relative; }

/* ============================================================
   MONTH VIEW
   ============================================================ */
.month-view { display: flex; flex-direction: column; height: 100%; animation: fadeIn .2s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }

.month-header-row { display: grid; grid-template-columns: repeat(7, 1fr); border-bottom: 1px solid var(--color-border); flex-shrink: 0; }
.month-header-cell { padding: 8px 4px; text-align: center; font-size: 11px; font-weight: 500; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: .5px; }

.month-grid { display: grid; grid-template-columns: repeat(7, 1fr); flex: 1; overflow-y: auto; }
.month-cell { border-right: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); min-height: 80px; padding: 2px 4px; cursor: pointer; position: relative; transition: background .1s; overflow: hidden; }
.month-cell:hover { background: #f8f9fa; }
.month-cell.other-month { background: #fafafa; }
.month-cell.today { background: var(--color-today-bg); }

.cell-date { font-size: 12px; font-weight: 500; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 50%; margin-bottom: 2px; }
.month-cell.other-month .cell-date { color: var(--color-text-dimmed); }
.month-cell.today .cell-date { background: var(--color-primary); color: #fff; font-weight: 700; }

.event-chip { font-size: 11px; padding: 1px 4px; border-radius: 2px; margin-bottom: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; line-height: 18px; }
.event-chip:hover { opacity: .85; }

.overflow-link { font-size: 11px; color: var(--color-text-secondary); padding: 1px 4px; cursor: pointer; border-radius: 2px; }
.overflow-link:hover { background: rgba(0,0,0,.06); }

/* ============================================================
   WEEK VIEW
   ============================================================ */
.week-view { display: flex; flex-direction: column; height: 100%; animation: fadeIn .2s ease; }

.week-header-row { display: grid; border-bottom: 1px solid var(--color-border); flex-shrink: 0; position: sticky; top: 0; background: #fff; z-index: 5; }
.week-header-cell { padding: 8px 4px; text-align: center; font-size: 12px; border-left: 1px solid var(--color-border); }
.week-header-cell .day-name { color: var(--color-text-secondary); font-weight: 500; text-transform: uppercase; letter-spacing: .3px; }
.week-header-cell .day-num { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%; margin: 4px auto 0; font-size: 18px; font-weight: 400; }
.week-header-cell.today .day-num { background: var(--color-primary); color: #fff; font-weight: 700; }

.week-body-wrapper { flex: 1; overflow-y: auto; position: relative; }
.week-body-inner { display: grid; min-height: calc(var(--hour-height) * 17); }

.all-day-strip { display: grid; border-bottom: 2px solid var(--color-border); background: #fafafa; }
.all-day-cell { border-left: 1px solid var(--color-border); padding: 2px 4px; min-height: var(--all-day-height); position: relative; }

.time-grid { display: grid; position: relative; }
.hour-row { display: contents; }
.hour-label { font-size: 10px; color: var(--color-text-secondary); text-align: right; padding-right: 8px; padding-top: -4px; height: var(--hour-height); border-left: 1px solid transparent; position: relative; }
.hour-label span { position: absolute; top: -6px; right: 8px; }
.hour-cell { border-left: 1px solid var(--color-border); border-bottom: 1px solid var(--color-grid-line); height: var(--hour-height); position: relative; }

.time-grid-container { position: relative; }
.event-overlay { pointer-events: none; }
.event-overlay > * { pointer-events: auto; }

.week-event-block { border-radius: var(--radius-sm); padding: 2px 6px; font-size: 11px; overflow: hidden; cursor: pointer; z-index: 2; transition: box-shadow .1s; line-height: 1.3; word-break: break-word; }
.week-event-block:hover { box-shadow: var(--shadow-md); z-index: 3; }

.current-time-line { position: absolute; height: 2px; background: #d93025; z-index: 4; pointer-events: none; }
.current-time-line::before { content: ''; position: absolute; left: -5px; top: -4px; width: 10px; height: 10px; border-radius: 50%; background: #d93025; }

.all-day-strip { display: grid; border-bottom: 2px solid var(--color-border); background: #fafafa; }

/* ============================================================
   AGENDA VIEW
   ============================================================ */
.agenda-view { padding: 24px; max-width: 700px; margin: 0 auto; animation: fadeIn .2s ease; overflow-y: auto; height: 100%; }
.agenda-date-group { margin-bottom: 24px; }
.agenda-date-header { font-size: 13px; font-weight: 500; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: .5px; padding-bottom: 8px; border-bottom: 1px solid var(--color-border); margin-bottom: 8px; }
.agenda-event-item { display: flex; gap: 12px; padding: 8px 0; cursor: pointer; transition: background .1s; border-radius: var(--radius-sm); padding-left: 4px; padding-right: 4px; }
.agenda-event-item:hover { background: var(--color-bg-hover); }
.agenda-color-bar { width: 4px; border-radius: 2px; flex-shrink: 0; align-self: stretch; }
.agenda-event-info { flex: 1; min-width: 0; }
.agenda-event-title { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agenda-event-time { font-size: 12px; color: var(--color-text-secondary); margin-top: 2px; }

/* ============================================================
   MODAL
   ============================================================ */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 100; display: flex; align-items: center; justify-content: center; animation: fadeIn .15s ease; }
.modal { background: #fff; border-radius: var(--radius-md); box-shadow: var(--shadow-lg); width: 90%; max-width: 460px; max-height: 90vh; overflow-y: auto; animation: modalSlideIn .2s ease; }
@keyframes modalSlideIn { from { transform: translateY(20px) scale(.97); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } }

.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--color-border); }
.modal-title { font-size: 18px; font-weight: 500; }
.modal-close { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; color: var(--color-text-secondary); transition: background .15s; }
.modal-close:hover { background: var(--color-bg-hover); }

.modal-body { padding: 20px; }
.form-group { margin-bottom: 16px; }
.form-label { display: block; font-size: 12px; font-weight: 500; color: var(--color-text-secondary); margin-bottom: 4px; text-transform: uppercase; letter-spacing: .3px; }
.form-input, .form-select, .form-textarea { width: 100%; padding: 8px 12px; border: 1px solid var(--color-border); border-radius: var(--radius-sm); font-size: 14px; outline: none; transition: border-color .15s; }
.form-input:focus, .form-select:focus, .form-textarea:focus { border-color: var(--color-primary); box-shadow: 0 0 0 2px rgba(26,115,232,.15); }
.form-textarea { resize: vertical; min-height: 60px; }

.form-row { display: flex; gap: 12px; }
.form-row .form-group { flex: 1; }

.color-picker { display: flex; gap: 8px; flex-wrap: wrap; }
.color-swatch { width: 28px; height: 28px; border-radius: 50%; cursor: pointer; border: 3px solid transparent; transition: all .15s; }
.color-swatch:hover { transform: scale(1.1); }
.color-swatch.selected { border-color: var(--color-text); box-shadow: 0 0 0 2px #fff inset; }

.checkbox-group { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.checkbox-group input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--color-primary); }

.recurrence-options { margin-top: 8px; padding-left: 24px; }
.recurrence-option { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; cursor: pointer; }
.recurrence-option input[type="radio"] { accent-color: var(--color-primary); }

.weekday-pills { display: flex; gap: 4px; margin-top: 6px; padding-left: 24px; }
.weekday-pill { width: 30px; height: 24px; border-radius: 12px; font-size: 11px; font-weight: 500; display: flex; align-items: center; justify-content: center; cursor: pointer; border: 1px solid var(--color-border); transition: all .15s; }
.weekday-pill.selected { background: var(--color-primary-light); color: var(--color-primary); border-color: var(--color-primary); }

.modal-footer { display: flex; justify-content: space-between; padding: 12px 20px; border-top: 1px solid var(--color-border); }
.btn-delete { color: #d93025; font-size: 13px; font-weight: 500; padding: 6px 12px; border-radius: var(--radius-sm); transition: background .15s; }
.btn-delete:hover { background: #fce8e6; }
.btn-save { background: var(--color-primary); color: #fff; font-size: 13px; font-weight: 500; padding: 8px 20px; border-radius: var(--radius-sm); transition: opacity .15s; }
.btn-save:hover { opacity: .9; }

.error-text { color: #d93025; font-size: 12px; margin-top: 4px; display: none; }
.error-text.visible { display: block; }

/* ============================================================
   DAY POPOVER
   ============================================================ */
.day-popover { position: fixed; background: #fff; border-radius: var(--radius-md); box-shadow: var(--shadow-lg); width: 300px; z-index: 50; animation: fadeIn .12s ease; }
.popover-header { padding: 12px 16px; font-size: 14px; font-weight: 500; border-bottom: 1px solid var(--color-border); display: flex; justify-content: space-between; align-items: center; }
.popover-body { max-height: 300px; overflow-y: auto; padding: 8px; }
.popover-event { display: flex; gap: 8px; padding: 6px 8px; border-radius: var(--radius-sm); cursor: pointer; margin-bottom: 2px; transition: background .1s; }
.popover-event:hover { background: var(--color-bg-hover); }
.popover-color-bar { width: 4px; border-radius: 2px; flex-shrink: 0; }
.popover-event-info { flex: 1; min-width: 0; }
.popover-event-title { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.popover-event-time { font-size: 11px; color: var(--color-text-secondary); }

/* ============================================================
   TOAST
   ============================================================ */
.toast-container { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: 200; display: flex; flex-direction: column; gap: 8px; align-items: center; }
.toast { background: #323232; color: #fff; padding: 10px 20px; border-radius: var(--radius-sm); font-size: 14px; box-shadow: var(--shadow-md); animation: toastIn .2s ease; display: flex; align-items: center; gap: 12px; }
@keyframes toastIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.toast-action { color: var(--color-primary-light); font-weight: 500; cursor: pointer; text-transform: uppercase; font-size: 13px; padding: 4px 8px; border-radius: var(--radius-sm); transition: background .1s; }
.toast-action:hover { background: rgba(255,255,255,.1); }

/* ============================================================
   DRAG STYLES
   ============================================================ */
.dragging-chip { opacity: .4 !important; }
.drag-preview { position: fixed; pointer-events: none; z-index: 300; padding: 4px 8px; border-radius: var(--radius-sm); font-size: 12px; color: #fff; box-shadow: var(--shadow-md); transform: translate(-50%, -50%); }
.drop-highlight { background: rgba(26,115,232,.1) !important; outline: 2px solid var(--color-primary); outline-offset: -2px; }

/* ============================================================
   YEAR JUMP OVERLAY
   ============================================================ */
.year-jump-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 100; display: flex; align-items: center; justify-content: center; animation: fadeIn .12s ease; }
.year-jump-modal { background: #fff; border-radius: var(--radius-md); box-shadow: var(--shadow-lg); padding: 24px; width: 320px; text-align: center; }
.year-jump-input { font-size: 36px; text-align: center; width: 120px; border: none; border-bottom: 2px solid var(--color-border); outline: none; padding: 4px; margin: 12px auto; display: block; }
.year-jump-input:focus { border-color: var(--color-primary); }

/* ============================================================
   SETTINGS PANEL
   ============================================================ */
.settings-btn { font-size: 18px; color: var(--color-text-secondary); padding: 4px; }
.settings-panel { position: absolute; top: 56px; right: 16px; background: #fff; border-radius: var(--radius-md); box-shadow: var(--shadow-lg); width: 240px; z-index: 30; padding: 16px; animation: fadeIn .12s ease; }
.settings-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; font-size: 14px; border-bottom: 1px solid var(--color-border); }
.settings-item:last-child { border-bottom: none; }

/* ============================================================
   RESPONSIVE
   ============================================================ */
@media (max-width: 768px) {
  .header { padding: 6px 8px; gap: 4px; flex-wrap: wrap; }
  .nav-title { font-size: 16px; min-width: auto; }
  .month-cell { min-height: 60px; padding: 1px 2px; }
  .cell-date { font-size: 10px; width: 20px; height: 20px; }
  .event-chip { font-size: 9px; line-height: 14px; padding: 0 2px; }
  .week-header-cell .day-num { width: 26px; height: 26px; font-size: 14px; }
}

/* ============================================================
   RECURRING SERIES EDIT OPTIONS
   ============================================================ */
.series-edit-options { margin-bottom: 16px; padding: 12px; background: var(--color-bg-hover); border-radius: var(--radius-sm); }
.series-edit-title { font-size: 13px; font-weight: 500; margin-bottom: 8px; color: var(--color-text-secondary); }
</style>
</head>
<body>

<div id="app">
  <!-- HEADER -->
  <div class="header">
    <div class="header-left">
      <button class="today-btn" id="btnToday">Today</button>
      <button class="btn-icon" id="btnPrev" title="Previous">&#9664;</button>
      <button class="btn-icon" id="btnNext" title="Next">&#9654;</button>
    </div>
    <div class="header-center">
      <span class="nav-title" id="navTitle"></span>
    </div>
    <div class="header-right">
      <div class="view-switcher">
        <button class="view-btn active" data-view="month">Month</button>
        <button class="view-btn" data-view="week">Week</button>
        <button class="view-btn" data-view="agenda">Agenda</button>
      </div>
      <button class="btn-icon settings-btn" id="btnSettings" title="Settings">&#9881;</button>
    </div>
  </div>

  <!-- MAIN CONTENT -->
  <div class="main-content">
    <div class="view-container" id="viewContainer"></div>
  </div>
</div>

<!-- MODAL CONTAINER -->
<div id="modalContainer"></div>
<!-- TOAST CONTAINER -->
<div class="toast-container" id="toastContainer"></div>

<script>
/* ============================================================
   SECTION: TUNABLE CONSTANTS
   ============================================================ */
const CONFIG = {
  COLORS: [
    { name: 'Blue', value: '#1a73e8' },
    { name: 'Red', value: '#d93025' },
    { name: 'Green', value: '#1e8e3e' },
    { name: 'Yellow', value: '#f9ab00' },
    { name: 'Purple', value: '#9334e6' },
    { name: 'Teal', value: '#12b5cb' },
    { name: 'Orange', value: '#ea8600' },
    { name: 'Pink', value: '#d91e78' },
  ],
  HOUR_START: 6,
  HOUR_END: 22,
  HOUR_HEIGHT: 50,
  SNAP_MINUTES: 15,
  DEFAULT_WEEK_START: 0, // 0 = Sunday, 1 = Monday
  MAX_CHIPS_DISPLAY: 3,
};

const MONTH_NAMES = ['January','February','March','April','May','June',
                     'July','August','September','October','November','December'];
const DAY_NAMES_SHORT = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const DAY_NAMES_FULL = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];

/* ============================================================
   SECTION: DATE UTILITIES (Pure Functions)
   ============================================================ */
const DateUtil = {
  /** Check if a year is a leap year (handles century rule) */
  isLeap(year) {
    return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
  },

  /** Number of days in a given month/year */
  daysInMonth(year, month) { // month: 0-11
    if (month === 1) return this.isLeap(year) ? 29 : 28;
    const table = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    return table[month];
  },

  /** Day of week for a given date: 0=Sun, 1=Mon, ..., 6=Sat */
  dayOfWeek(year, month, day) { // month: 0-11
    const d = new Date(year, month, day);
    return d.getDay();
  },

  /** Add N days to a date (component-based, no ms hacks) */
  addDays(year, month, day, n) {
    let y = year, m = month, d = day + n;
    while (d < 1) {
      m--;
      if (m < 0) { m = 11; y--; }
      d += this.daysInMonth(y, m);
    }
    const dim = this.daysInMonth(y, m);
    while (d > dim) {
      d -= dim;
      m++;
      if (m > 11) { m = 0; y++; }
    }
    return { year: y, month: m, day: d };
  },

  /** Add N months to a date */
  addMonths(year, month, n) {
    let m = month + n;
    let y = year + Math.floor(m / 12);
    m = ((m % 12) + 12) % 12;
    return { year: y, month: m };
  },

  /** Get the start of week for a given date */
  getWeekStart(year, month, day, weekStartDay) { // weekStartDay: 0=Sun, 1=Mon
    const dow = this.dayOfWeek(year, month, day);
    const diff = (dow - weekStartDay + 7) % 7;
    return this.addDays(year, month, day, -diff);
  },

  /** Generate the full month grid: array of {year,month,day,isCurrentMonth} */
  generateMonthGrid(year, month, weekStartDay) {
    const firstDow = this.dayOfWeek(year, month, 1);
    const startOffset = (firstDow - weekStartDay + 7) % 7;
    const prevResult = this.addMonths(year, month, -1);
    const prevDays = this.daysInMonth(prevResult.year, prevResult.month);

    const cells = [];
    // Previous month trailing days
    for (let i = startOffset - 1; i >= 0; i--) {
      cells.push({ year: prevResult.year, month: prevResult.month, day: prevDays - i, isCurrentMonth: false });
    }
    // Current month days
    const dim = this.daysInMonth(year, month);
    for (let d = 1; d <= dim; d++) {
      cells.push({ year, month, day: d, isCurrentMonth: true });
    }
    // Next month leading days — fill to 6 rows (42 cells) or 5 rows (35)
    const remaining = 42 - cells.length;
    for (let d = 1; d <= remaining; d++) {
      const nextResult = this.addMonths(year, month, 1);
      cells.push({ year: nextResult.year, month: nextResult.month, day: d, isCurrentMonth: false });
    }
    return cells;
  },

  /** Format date as YYYY-MM-DD */
  formatDateKey(y, m, d) {
    return `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
  },

  /** Parse YYYY-MM-DD to {year, month, day} (month 0-11) */
  parseDateKey(key) {
    const [y, m, d] = key.split('-').map(Number);
    return { year: y, month: m - 1, day: d };
  },

  /** Check if two dates are the same day */
  isSameDay(y1, m1, d1, y2, m2, d2) {
    return y1 === y2 && m1 === m2 && d1 === d2;
  },

  /** Get today's date components */
  getToday() {
    const now = new Date();
    return { year: now.getFullYear(), month: now.getMonth(), day: now.getDate() };
  },

  /** Format time as HH:MM */
  formatTime(hours, minutes) {
    const h = hours % 12 || 12;
    const ampm = hours < 12 ? 'AM' : 'PM';
    return `${h}:${String(minutes).padStart(2, '0')} ${ampm}`;
  },

  /** Parse "HH:MM" to {hours, minutes} */
  parseTime(timeStr) {
    const [h, m] = timeStr.split(':').map(Number);
    return { hours: h, minutes: m };
  },

  /** Minutes from midnight */
  timeToMinutes(hours, minutes) {
    return hours * 60 + minutes;
  },

  /** Snap minutes to interval */
  snapMinutes(minutes, interval) {
    return Math.round(minutes / interval) * interval;
  },
};

/* ============================================================
   SECTION: RECURRENCE ENGINE
   ============================================================ */
const RecurrenceEngine = {
  /** Expand a recurrence rule for events within [startKey, endKey] date range */
  expand(event, startKey, endKey) {
    if (!event.recurrence || !event.recurrence.type || event.recurrence.type === 'none') {
      return [{ ...event, _occurrenceDate: DateUtil.formatDateKey(event.year, event.month, event.day), _isMaster: true }];
    }

    const startDate = DateUtil.parseDateKey(startKey);
    const endDate = DateUtil.parseDateKey(endKey);
    const occurrences = [];
    const rule = event.recurrence;

    // Determine the effective start date for expansion
    let expandStartYear = event.year;
    let expandStartMonth = event.month;
    let expandStartDay = event.day;

    if (event._seriesStartDate) {
      const sd = DateUtil.parseDateKey(event._seriesStartDate);
      expandStartYear = sd.year;
      expandStartMonth = sd.month;
      expandStartDay = sd.day;
    }

    // Safety: limit expansion to avoid infinite loops
    let iterations = 0;
    const MAX_ITERATIONS = 500;

    switch (rule.type) {
      case 'daily': {
        let curY = expandStartYear, curM = expandStartMonth, curD = expandStartDay;
        while (iterations < MAX_ITERATIONS) {
          iterations++;
          const key = DateUtil.formatDateKey(curY, curM, curD);
          if (key > endKey) break;
          if (key >= startKey && (!event._seriesStartDate || key >= event._seriesStartDate)) {
            occurrences.push(this.makeOccurrence(event, curY, curM, curD));
          }
          const next = DateUtil.addDays(curY, curM, curD, rule.interval || 1);
          curY = next.year; curM = next.month; curD = next.day;
        }
        break;
      }
      case 'weekly': {
        let curY = expandStartYear, curM = expandStartMonth, curD = expandStartDay;
        while (iterations < MAX_ITERATIONS) {
          iterations++;
          const dow = DateUtil.dayOfWeek(curY, curM, curD);
          if ((rule.weekdays || [dow]).includes(dow)) {
            const key = DateUtil.formatDateKey(curY, curM, curD);
            if (key > endKey) break;
            if (key >= startKey && (!event._seriesStartDate || key >= event._seriesStartDate)) {
              occurrences.push(this.makeOccurrence(event, curY, curM, curD));
            }
          }
          const next = DateUtil.addDays(curY, curM, curD, rule.interval ? (rule.interval - 1) * 7 + 7 : 7);
          curY = next.year; curM = next.month; curD = next.day;
        }
        break;
      }
      case 'monthly': {
        let curM = expandStartMonth;
        let curY = expandStartYear;
        const targetDay = rule.dayOfMonth || event.day;
        while (iterations < MAX_ITERATIONS) {
          iterations++;
          const dim = DateUtil.daysInMonth(curY, curM);
          if (targetDay <= dim) {
            const key = DateUtil.formatDateKey(curY, curM, targetDay);
            if (key > endKey) break;
            if (key >= startKey && (!event._seriesStartDate || key >= event._seriesStartDate)) {
              occurrences.push(this.makeOccurrence(event, curY, curM, targetDay));
            }
          }
          // Move to next month by interval
          const inc = rule.interval || 1;
          let totalMonths = curM + inc;
          curY += Math.floor(totalMonths / 12);
          curM = ((totalMonths % 12) + 12) % 12;
        }
        break;
      }
      case 'yearly': {
        let curY = expandStartYear;
        const targetMonth = event.month;
        const targetDay = event.day;
        while (iterations < MAX_ITERATIONS) {
          iterations++;
          const dim = DateUtil.daysInMonth(curY, targetMonth);
          if (targetDay <= dim) {
            const key = DateUtil.formatDateKey(curY, targetMonth, targetDay);
            if (key > endKey) break;
            if (key >= startKey && (!event._seriesStartDate || key >= event._seriesStartDate)) {
              occurrences.push(this.makeOccurrence(event, curY, targetMonth, targetDay));
            }
          }
          const inc = rule.interval || 1;
          curY += inc;
        }
        break;
      }
    }

    return occurrences;
  },

  makeOccurrence(event, year, month, day) {
    return {
      ...event,
      year, month, day,
      _occurrenceDate: DateUtil.formatDateKey(year, month, day),
      _isMaster: false,
      _seriesId: event.id,
    };
  },

  /** Get all events for a specific date (expanding recurrences) */
  getEventsForDate(events, year, month, day, exceptions = {}) {
    const key = DateUtil.formatDateKey(year, month, day);
    const result = [];

    // Visible range: expand from 30 days before to 60 days after for safety
    const startResult = DateUtil.addDays(year, month, day, -30);
    const endResult = DateUtil.addDays(year, month, day, 60);
    const startKey = DateUtil.formatDateKey(startResult.year, startResult.month, startResult.day);
    const endKey = DateUtil.formatDateKey(endResult.year, endResult.month, endResult.day);

    for (const event of events) {
      if (!event.recurrence || !event.recurrence.type || event.recurrence.type === 'none') {
        if (DateUtil.isSameDay(event.year, event.month, event.day, year, month, day)) {
          // Check exceptions
          const excKey = `${event.id}::${key}`;
          if (exceptions[excKey] && exceptions[excKey].excluded) continue;
          result.push({ ...event, _occurrenceDate: key });
        }
      } else {
        const occurrences = this.expand(event, startKey, endKey);
        for (const occ of occurrences) {
          if (!DateUtil.isSameDay(occ.year, occ.month, occ.day, year, month, day)) continue;
          const excKey = `${event.id}::${key}`;
          if (exceptions[excKey] && exceptions[excKey].excluded) continue;
          // Apply exception overrides
          if (exceptions[excKey]) {
            result.push({ ...occ, ...exceptions[excKey].overrides });
          } else {
            result.push(occ);
          }
        }
      }
    }

    return result.sort((a, b) => {
      const aMin = a.allDay ? -1 : DateUtil.timeToMinutes(a.startHours || 0, a.startMinutes || 0);
      const bMin = b.allDay ? -1 : DateUtil.timeToMinutes(b.startHours || 0, b.startMinutes || 0);
      return aMin - bMin;
    });
  },

  /** Get all events in a date range */
  getEventsInRange(events, startKey, endKey, exceptions = {}) {
    const result = [];
    for (const event of events) {
      if (!event.recurrence || !event.recurrence.type || event.recurrence.type === 'none') {
        const ekey = DateUtil.formatDateKey(event.year, event.month, event.day);
        if (ekey >= startKey && ekey <= endKey) {
          result.push({ ...event, _occurrenceDate: ekey });
        }
      } else {
        const occurrences = this.expand(event, startKey, endKey);
        for (const occ of occurrences) {
          const excKey = `${event.id}::${occ._occurrenceDate}`;
          if (exceptions[excKey] && exceptions[excKey].excluded) continue;
          let finalOcc = occ;
          if (exceptions[excKey]) {
            finalOcc = { ...occ, ...exceptions[excKey].overrides };
          }
          result.push(finalOcc);
        }
      }
    }
    return result.sort((a, b) => a._occurrenceDate.localeCompare(b._occurrenceDate));
  },
};

/* ============================================================
   SECTION: EVENT STORE (CRUD + Persistence)
   ============================================================ */
const Store = {
  STORAGE_KEY_EVENTS: 'cal_events',
  STORAGE_KEY_EXCEPTIONS: 'cal_exceptions',
  STORAGE_KEY_SETTINGS: 'cal_settings',

  events: [],
  exceptions: {}, // key: "eventId::YYYY-MM-DD" -> { excluded?: bool, overrides?: {} }
  settings: { weekStart: CONFIG.DEFAULT_WEEK_START },

  init() {
    try {
      const ev = localStorage.getItem(this.STORAGE_KEY_EVENTS);
      if (ev) this.events = JSON.parse(ev);
      const ex = localStorage.getItem(this.STORAGE_KEY_EXCEPTIONS);
      if (ex) this.exceptions = JSON.parse(ex);
      const st = localStorage.getItem(this.STORAGE_KEY_SETTINGS);
      if (st) this.settings = { ...this.settings, ...JSON.parse(st) };
    } catch (e) { console.error('Store init error:', e); }
  },

  save() {
    try {
      localStorage.setItem(this.STORAGE_KEY_EVENTS, JSON.stringify(this.events));
      localStorage.setItem(this.STORAGE_KEY_EXCEPTIONS, JSON.stringify(this.exceptions));
      localStorage.setItem(this.STORAGE_KEY_SETTINGS, JSON.stringify(this.settings));
    } catch (e) { console.error('Store save error:', e); }
  },

  _genId() { return 'evt_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7); },

  createEvent(data) {
    const event = {
      id: this._genId(),
      title: data.title || 'Untitled',
      year: data.year,
      month: data.month,
      day: data.day,
      allDay: !!data.allDay,
      startHours: data.startHours ?? 9,
      startMinutes: data.startMinutes ?? 0,
      endHours: data.endHours ?? 10,
      endMinutes: data.endMinutes ?? 0,
      color: data.color || CONFIG.COLORS[0].value,
      notes: data.notes || '',
      recurrence: data.recurrence || { type: 'none' },
    };
    this.events.push(event);
    this.save();
    return event;
  },

  updateEvent(id, updates) {
    const idx = this.events.findIndex(e => e.id === id);
    if (idx === -1) return null;
    Object.assign(this.events[idx], updates);
    this.save();
    return this.events[idx];
  },

  deleteEvent(id) {
    this.events = this.events.filter(e => e.id !== id);
    // Clean up exceptions for deleted event
    const keysToRemove = Object.keys(this.exceptions).filter(k => k.startsWith(id + '::'));
    for (const k of keysToRemove) delete this.exceptions[k];
    this.save();
  },

  getEvent(id) { return this.events.find(e => e.id === id); },

  addException(eventId, dateKey, data) {
    const key = `${eventId}::${dateKey}`;
    this.exceptions[key] = data;
    this.save();
  },

  removeException(eventId, dateKey) {
    delete this.exceptions[`${eventId}::${dateKey}`];
    this.save();
  },

  getEventsForDate(year, month, day) {
    return RecurrenceEngine.getEventsForDate(this.events, year, month, day, this.exceptions);
  },

  getEventsInRange(startKey, endKey) {
    return RecurrenceEngine.getEventsInRange(this.events, startKey, endKey, this.exceptions);
  },
};

/* ============================================================
   SECTION: APP STATE & CONTROLLER
   ============================================================ */
const App = {
  currentYear: null,
  currentMonth: null, // 0-11
  currentView: 'month', // month | week | agenda
  animating: false,

  init() {
    Store.init();
    const today = DateUtil.getToday();
    this.currentYear = today.year;
    this.currentMonth = today.month;
    this.bindEvents();
    this.render();
  },

  bindEvents() {
    document.getElementById('btnToday').addEventListener('click', () => {
      const t = DateUtil.getToday();
      this.navigate(t.year, t.month);
    });
    document.getElementById('btnPrev').addEventListener('click', () => this.prev());
    document.getElementById('btnNext').addEventListener('click', () => this.next());
    document.getElementById('navTitle').addEventListener('click', () => this.showYearJump());

    document.querySelectorAll('.view-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        const view = btn.dataset.view;
        if (view !== this.currentView) {
          this.currentView = view;
          document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
          this.render();
        }
      });
    });

    document.getElementById('btnSettings').addEventListener('click', (e) => {
      e.stopPropagation();
      const panel = document.querySelector('.settings-panel');
      if (panel) { panel.remove(); return; }
      this.showSettingsPanel(e.target);
    });

    // Close popovers/panels on outside click
    document.addEventListener('click', () => {
      document.querySelectorAll('.day-popover, .settings-panel').forEach(el => el.remove());
    });
  },

  navigate(year, month) {
    if (this.currentYear === year && this.currentMonth === month) return;
    const oldView = this.currentView;
    this.currentYear = year;
    this.currentMonth = month;
    this.render();
  },

  prev() {
    let m = this.currentMonth - 1;
    let y = this.currentYear;
    if (m < 0) { m = 11; y--; }
    this.navigate(y, m);
  },

  next() {
    let m = this.currentMonth + 1;
    let y = this.currentYear;
    if (m > 11) { m = 0; y++; }
    this.navigate(y, m);
  },

  showSettingsPanel(btnEl) {
    const panel = document.createElement('div');
    panel.className = 'settings-panel';
    const ws = Store.settings.weekStart;
    panel.innerHTML = `
      <div class="settings-item">
        <span>Week starts on</span>
        <select id="settingWeekStart" style="padding:4px 8px;border-radius:4px;border:1px solid var(--color-border);font-size:13px;">
          <option value="0" ${ws===0?'selected':''}>Sunday</option>
          <option value="1" ${ws===1?'selected':''}>Monday</option>
        </select>
      </div>
    `;
    document.body.appendChild(panel);

    panel.querySelector('#settingWeekStart').addEventListener('change', (e) => {
      Store.settings.weekStart = parseInt(e.target.value);
      Store.save();
      this.render();
    });
  },

  showYearJump() {
    const overlay = document.createElement('div');
    overlay.className = 'year-jump-overlay';
    overlay.innerHTML = `
      <div class="year-jump-modal">
        <div style="font-size:14px;color:var(--color-text-secondary);margin-bottom:8px;">Jump to year</div>
        <input type="number" class="year-jump-input" id="yearInput" value="${this.currentYear}" min="1900" max="2200">
        <div style="display:flex;gap:8px;justify-content:center;margin-top:16px;">
          <button class="btn-text" id="yearCancel">Cancel</button>
          <button class="btn-save" id="yearGo">Go</button>
        </div>
      </div>
    `;
    document.body.appendChild(overlay);

    const input = overlay.querySelector('#yearInput');
    setTimeout(() => { input.focus(); input.select(); }, 100);

    overlay.querySelector('#yearCancel').addEventListener('click', () => overlay.remove());
    overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
    overlay.querySelector('#yearGo').addEventListener('click', () => {
      const y = parseInt(input.value);
      if (!isNaN(y) && y >= 1900 && y <= 2200) {
        this.navigate(y, this.currentMonth);
      }
      overlay.remove();
    });
    input.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') overlay.querySelector('#yearGo').click();
      if (e.key === 'Escape') overlay.remove();
    });
  },

  render() {
    const container = document.getElementById('viewContainer');
    this.updateTitle();

    switch (this.currentView) {
      case 'month': container.innerHTML = ''; this.renderMonth(container); break;
      case 'week': container.innerHTML = ''; this.renderWeek(container); break;
      case 'agenda': container.innerHTML = ''; this.renderAgenda(container); break;
    }
  },

  updateTitle() {
    const titleEl = document.getElementById('navTitle');
    if (this.currentView === 'month') {
      titleEl.textContent = `${MONTH_NAMES[this.currentMonth]} ${this.currentYear}`;
    } else if (this.currentView === 'week') {
      const ws = Store.settings.weekStart;
      const start = DateUtil.getWeekStart(this.currentYear, this.currentMonth, 15, ws);
      const end = DateUtil.addDays(start.year, start.month, start.day, 6);
      if (start.month === end.month) {
        titleEl.textContent = `${MONTH_NAMES[start.month]} ${start.day}–${end.day}, ${start.year}`;
      } else if (start.year === end.year) {
        titleEl.textContent = `${MONTH_NAMES[start.month].slice(0,3)} ${start.day} – ${MONTH_NAMES[end.month].slice(0,3)} ${end.day}, ${start.year}`;
      } else {
        titleEl.textContent = `${MONTH_NAMES[start.month]} ${start.day}, ${start.year} – ${MONTH_NAMES[end.month]} ${end.day}, ${end.year}`;
      }
    } else if (this.currentView === 'agenda') {
      const today = DateUtil.getToday();
      if (DateUtil.isSameDay(this.currentYear, this.currentMonth, 15, today.year, today.month, today.day)) {
        titleEl.textContent = `Starting Today`;
      } else {
        titleEl.textContent = `${MONTH_NAMES[this.currentMonth]} ${this.currentYear}`;
      }
    }
  },

  /* ============================================================
     RENDER: MONTH VIEW
     ============================================================ */
  renderMonth(container) {
    const ws = Store.settings.weekStart;
    const grid = DateUtil.generateMonthGrid(this.currentYear, this.currentMonth, ws);
    const today = DateUtil.getToday();

    let html = '<div class="month-view">';
    // Header row
    html += '<div class="month-header-row">';
    for (let i = 0; i < 7; i++) {
      const dayIdx = (ws + i) % 7;
      html += `<div class="month-header-cell">${DAY_NAMES_SHORT[dayIdx]}</div>`;
    }
    html += '</div>';

    // Grid
    html += '<div class="month-grid">';
    for (const cell of grid) {
      const isToday = DateUtil.isSameDay(cell.year, cell.month, cell.day, today.year, today.month, today.day);
      const classes = ['month-cell'];
      if (!cell.isCurrentMonth) classes.push('other-month');
      if (isToday) classes.push('today');

      html += `<div class="${classes.join(' ')}" data-date="${DateUtil.formatDateKey(cell.year, cell.month, cell.day)}">`;
      html += `<div class="cell-date">${cell.day}</div>`;

      // Events for this day
      const events = Store.getEventsForDate(cell.year, cell.month, cell.day);
      const maxChips = CONFIG.MAX_CHIPS_DISPLAY;
      let shown = 0;
      for (const ev of events) {
        if (shown >= maxChips) break;
        html += `<div class="event-chip" style="background:${ev.color}22;color:${ev.color};border-left:3px solid ${ev.color}" data-event-id="${ev.id}" data-date="${DateUtil.formatDateKey(cell.year, cell.month, cell.day)}">${ev.title}</div>`;
        shown++;
      }
      if (events.length > maxChips) {
        html += `<div class="overflow-link" data-date="${DateUtil.formatDateKey(cell.year, cell.month, cell.day)}">+${events.length - maxChips} more</div>`;
      }

      html += '</div>';
    }
    html += '</div></div>';
    container.innerHTML = html;

    // Bind month view events
    this.bindMonthEvents(container);
  },

  bindMonthEvents(container) {
    const grid = container.querySelector('.month-grid');

    // Click on cell to create event
    grid.addEventListener('click', (e) => {
      const chip = e.target.closest('.event-chip');
      if (chip) return; // handled separately
      const overflow = e.target.closest('.overflow-link');
      if (overflow) return; // handled separately

      const cell = e.target.closest('.month-cell');
      if (!cell) return;
      const dateKey = cell.dataset.date;
      const d = DateUtil.parseDateKey(dateKey);
      this.openEventModal(null, d.year, d.month, d.day);
    });

    // Click on event chip to edit
    grid.addEventListener('click', (e) => {
      const chip = e.target.closest('.event-chip');
      if (!chip) return;
      const eventId = chip.dataset.eventId;
      const dateKey = chip.dataset.date || chip.closest('[data-date]')?.dataset.date;
      this.openEventModal(eventId, null, null, null, dateKey);
    });

    // Overflow link -> day popover
    grid.addEventListener('click', (e) => {
      const overflow = e.target.closest('.overflow-link');
      if (!overflow) return;
      const dateKey = overflow.dataset.date;
      this.showDayPopover(overflow, dateKey);
    });

    // Drag to reschedule in month view
    let dragState = null;
    grid.addEventListener('mousedown', (e) => {
      const chip = e.target.closest('.event-chip');
      if (!chip) return;
      e.preventDefault();
      const eventId = chip.dataset.eventId;
      const sourceDateKey = chip.dataset.date || chip.closest('[data-date]')?.dataset.date;
      dragState = { eventId, sourceDateKey, chip };
      chip.classList.add('dragging-chip');

      // Create preview
      const preview = document.createElement('div');
      preview.className = 'drag-preview';
      preview.style.background = chip.style.borderLeftColor || '#1a73e8';
      preview.textContent = chip.textContent;
      preview.id = '_dragPreview';
      document.body.appendChild(preview);

      const onMove = (ev) => {
        const p = document.getElementById('_dragPreview');
        if (p) { p.style.left = ev.clientX + 'px'; p.style.top = ev.clientY + 'px'; }
        // Highlight drop target
        grid.querySelectorAll('.month-cell').forEach(c => c.classList.remove('drop-highlight'));
        const el = document.elementFromPoint(ev.clientX, ev.clientY);
        if (el) {
          const cell = el.closest?.('.month-cell');
          if (cell && cell !== chip.closest('.month-cell')) cell.classList.add('drop-highlight');
        }
      };

      const onUp = (ev) => {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
        grid.querySelectorAll('.month-cell').forEach(c => c.classList.remove('drop-highlight'));
        chip.classList.remove('dragging-chip');
        const p = document.getElementById('_dragPreview');
        if (p) p.remove();

        // Find drop target
        const el = document.elementFromPoint(ev.clientX, ev.clientY);
        if (el) {
          const cell = el.closest?.('.month-cell');
          if (cell && cell !== chip.closest('.month-cell')) {
            const newDateKey = cell.dataset.date;
            this.rescheduleEvent(eventId, sourceDateKey, newDateKey);
          }
        }
        dragState = null;
      };

      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
    });
  },

  rescheduleEvent(eventId, oldDateKey, newDateKey) {
    const event = Store.getEvent(eventId);
    if (!event) return;
    const d = DateUtil.parseDateKey(newDateKey);

    // If recurring, add exception for old date and create new one-time event
    if (event.recurrence && event.recurrence.type !== 'none') {
      // Exclude the old occurrence
      Store.addException(eventId, oldDateKey, { excluded: true });
      // Create a new standalone event on the new date
      const newEvent = Store.createEvent({
        title: event.title,
        year: d.year, month: d.month, day: d.day,
        allDay: event.allDay,
        startHours: event.startHours, startMinutes: event.startMinutes,
        endHours: event.endHours, endMinutes: event.endMinutes,
        color: event.color,
        notes: event.notes,
        recurrence: { type: 'none' },
      });
      showToast(`Event moved to ${MONTH_NAMES[d.month]} ${d.day}`);
    } else {
      // Simple move
      Store.updateEvent(eventId, { year: d.year, month: d.month, day: d.day });
      showToast(`Event moved to ${MONTH_NAMES[d.month]} ${d.day}`);
    }
    this.render();
  },

  showDayPopover(anchorEl, dateKey) {
    // Remove existing popovers
    document.querySelectorAll('.day-popover').forEach(el => el.remove());

    const d = DateUtil.parseDateKey(dateKey);
    const events = Store.getEventsForDate(d.year, d.month, d.day);
    const rect = anchorEl.getBoundingClientRect();

    const popover = document.createElement('div');
    popover.className = 'day-popover';
    popover.style.left = Math.min(rect.left, window.innerWidth - 320) + 'px';
    popover.style.top = (rect.bottom + 4) + 'px';

    let html = `<div class="popover-header">
      <span>${MONTH_NAMES[d.month]} ${d.day}, ${d.year}</span>
      <button style="font-size:18px;color:var(--color-text-secondary)" id="popoverClose">&times;</button>
    </div><div class="popover-body">`;

    if (events.length === 0) {
      html += '<div style="padding:12px;text-align:center;color:var(--color-text-secondary);font-size:13px;">No events</div>';
    } else {
      for (const ev of events) {
        const timeStr = ev.allDay ? 'All day' : `${DateUtil.formatTime(ev.startHours, ev.startMinutes)} – ${DateUtil.formatTime(ev.endHours, ev.endMinutes)}`;
        html += `<div class="popover-event" data-event-id="${ev.id}" data-date="${dateKey}">
          <div class="popover-color-bar" style="background:${ev.color}"></div>
          <div class="popover-event-info">
            <div class="popover-event-title">${ev.title}</div>
            <div class="popover-event-time">${timeStr}</div>
          </div>
        </div>`;
      }
    }

    html += '</div>';
    popover.innerHTML = html;
    document.body.appendChild(popover);

    popover.querySelector('#popoverClose').addEventListener('click', (e) => { e.stopPropagation(); popover.remove(); });

    // Click event in popover to edit
    popover.addEventListener('click', (e) => {
      const item = e.target.closest('.popover-event');
      if (!item) return;
      e.stopPropagation();
      popover.remove();
      this.openEventModal(item.dataset.eventId, null, null, null, item.dataset.date);
    });

    // Click outside to close is handled by global listener
  },

  /* ============================================================
     RENDER: WEEK VIEW
     ============================================================ */
  renderWeek(container) {
    const ws = Store.settings.weekStart;
    const today = DateUtil.getToday();
    const weekStart = DateUtil.getWeekStart(this.currentYear, this.currentMonth, 15, ws);

    let html = '<div class="week-view">';

    // Header row
    html += '<div class="week-header-row" style="grid-template-columns:48px repeat(7,1fr)">';
    html += '<div></div>'; // time gutter
    for (let i = 0; i < 7; i++) {
      const d = DateUtil.addDays(weekStart.year, weekStart.month, weekStart.day, i);
      const isToday = DateUtil.isSameDay(d.year, d.month, d.day, today.year, today.month, today.day);
      html += `<div class="week-header-cell${isToday ? ' today' : ''}" data-date="${DateUtil.formatDateKey(d.year, d.month, d.day)}">`;
      html += `<div class="day-name">${DAY_NAMES_SHORT[DateUtil.dayOfWeek(d.year, d.month, d.day)]}</div>`;
      html += `<div class="day-num">${d.day}</div></div>`;
    }
    html += '</div>';

    // Body
    const totalHours = CONFIG.HOUR_END - CONFIG.HOUR_START + 1;
    const gridHeight = totalHours * CONFIG.HOUR_HEIGHT;

    html += '<div class="week-body-wrapper" id="weekBody">';
    html += `<div class="time-grid-container" style="position:relative;height:${gridHeight}px;">`;

    // All-day strip (above time grid)
    html += '<div class="all-day-strip" style="grid-template-columns:48px repeat(7,1fr);">';
    html += '<div></div>';
    for (let i = 0; i < 7; i++) {
      const d = DateUtil.addDays(weekStart.year, weekStart.month, weekStart.day, i);
      const dateKey = DateUtil.formatDateKey(d.year, d.month, d.day);
      const events = Store.getEventsForDate(d.year, d.month, d.day).filter(e => e.allDay);
      html += `<div class="all-day-cell" data-date="${dateKey}">`;
      for (const ev of events) {
        html += `<div class="event-chip" style="background:${ev.color}22;color:${ev.color};border-left:3px solid ${ev.color}" data-event-id="${ev.id}" data-date="${dateKey}">${ev.title}</div>`;
      }
      html += '</div>';
    }
    html += '</div>';

    // Time grid (hour cells as click targets)
    html += '<div class="time-grid" style="display:grid;grid-template-columns:48px repeat(7,1fr);height:' + gridHeight + 'px;">';
    for (let h = CONFIG.HOUR_START; h <= CONFIG.HOUR_END; h++) {
      html += `<div class="hour-label"><span>${DateUtil.formatTime(h, 0)}</span></div>`;
      for (let i = 0; i < 7; i++) {
        const d = DateUtil.addDays(weekStart.year, weekStart.month, weekStart.day, i);
        html += `<div class="hour-cell" data-date="${DateUtil.formatDateKey(d.year, d.month, d.day)}" data-hour="${h}"></div>`;
      }
    }
    html += '</div>';

    // Event overlay (absolute positioned on top of time grid)
    html += '<div class="event-overlay" style="position:absolute;top:0;left:48px;right:0;bottom:0;">';

    for (let i = 0; i < 7; i++) {
      const d = DateUtil.addDays(weekStart.year, weekStart.month, weekStart.day, i);
      const dateKey = DateUtil.formatDateKey(d.year, d.month, d.day);
      const events = Store.getEventsForDate(d.year, d.month, d.day).filter(e => !e.allDay);

      for (const ev of events) {
        const startMin = DateUtil.timeToMinutes(ev.startHours || 0, ev.startMinutes || 0);
        const endMin = DateUtil.timeToMinutes(ev.endHours || 0, ev.endMinutes || 0);
        const topOffset = ((startMin - CONFIG.HOUR_START * 60) / 60) * CONFIG.HOUR_HEIGHT;
        const height = Math.max(((endMin - startMin) / 60) * CONFIG.HOUR_HEIGHT, 20);

        if (topOffset + height > 0 && topOffset < gridHeight) {
          html += `<div class="week-event-block" data-event-id="${ev.id}" data-date="${dateKey}" style="
            position:absolute;
            left:calc((100% / 7) * ${i} + 2px);
            width:calc(100% / 7 - 4px);
            top:${topOffset}px;
            height:${height}px;
            background:${ev.color}22;
            border-left:3px solid ${ev.color};
            color:${ev.color};
          ">${ev.title}</div>`;
        }
      }
    }

    // Current time indicator
    const now = new Date();
    if (DateUtil.isSameDay(weekStart.year, weekStart.month, weekStart.day, today.year, today.month, today.day) ||
        this._isInWeekRange(today.year, today.month, today.day, weekStart)) {
      const todayOffset = this._getTodayWeekOffset(weekStart);
      if (todayOffset >= 0 && todayOffset < 7) {
        const nowMin = DateUtil.timeToMinutes(now.getHours(), now.getMinutes());
        const topPos = ((nowMin - CONFIG.HOUR_START * 60) / 60) * CONFIG.HOUR_HEIGHT;
        html += `<div class="current-time-line" style="left:calc((100% / 7) * ${todayOffset});width:calc(100% / 7);top:${Math.max(topPos, 0)}px;"></div>`;
      }
    }

    html += '</div></div></div>';
    container.innerHTML = html;

    // Bind week view events
    this.bindWeekEvents(container, weekStart);
  },

  _isInWeekRange(y, m, d, weekStart) {
    for (let i = 0; i < 7; i++) {
      const dd = DateUtil.addDays(weekStart.year, weekStart.month, weekStart.day, i);
      if (DateUtil.isSameDay(dd.year, dd.month, dd.day, y, m, d)) return true;
    }
    return false;
  },

  _getTodayWeekOffset(weekStart) {
    const today = DateUtil.getToday();
    for (let i = 0; i < 7; i++) {
      const dd = DateUtil.addDays(weekStart.year, weekStart.month, weekStart.day, i);
      if (DateUtil.isSameDay(dd.year, dd.month, dd.day, today.year, today.month, today.day)) return i;
    }
    return -1;
  },

  bindWeekEvents(container, weekStart) {
    const body = container.querySelector('.week-body-wrapper');

    // Click on hour cell to create event
    body.addEventListener('click', (e) => {
      const block = e.target.closest('.week-event-block');
      if (block) return;
      const chip = e.target.closest('.event-chip');
      if (chip) return;

      const cell = e.target.closest('.hour-cell');
      if (!cell) return;
      const d = DateUtil.parseDateKey(cell.dataset.date);
      const hour = parseInt(cell.dataset.hour);
      this.openEventModal(null, d.year, d.month, d.day, null, false, hour, 0, hour + 1, 0);
    });

    // Click on event block to edit
    body.addEventListener('click', (e) => {
      const block = e.target.closest('.week-event-block');
      if (!block) return;
      const eventId = block.dataset.eventId;
      this.openEventModal(eventId, null, null, null, block.dataset.date);
    });

    // Click on all-day chip to edit
    body.addEventListener('click', (e) => {
      const chip = e.target.closest('.all-day-cell .event-chip');
      if (!chip) return;
      this.openEventModal(chip.dataset.eventId, null, null, null, chip.dataset.date);
    });

    // Drag to reschedule in week view (vertical drag for time change)
    let dragState = null;
    body.addEventListener('mousedown', (e) => {
      const block = e.target.closest('.week-event-block');
      if (!block) return;
      e.preventDefault();
      const eventId = block.dataset.eventId;
      const dateKey = block.dataset.date;
      const startY = e.clientY;
      const origTop = parseFloat(block.style.top);

      block.classList.add('dragging-chip');

      const onMove = (ev) => {
        const dy = ev.clientY - startY;
        let newTop = origTop + dy;
        // Snap to 15-minute intervals
        const snappedMin = DateUtil.snapMinutes((newTop / CONFIG.HOUR_HEIGHT) * 60, CONFIG.SNAP_MINUTES);
        newTop = (snappedMin / 60) * CONFIG.HOUR_HEIGHT;
        block.style.top = Math.max(0, newTop) + 'px';
      };

      const onUp = (ev) => {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
        block.classList.remove('dragging-chip');

        const dy = ev.clientY - startY;
        let newTop = origTop + dy;
        const snappedMin = DateUtil.snapMinutes((newTop / CONFIG.HOUR_HEIGHT) * 60, CONFIG.SNAP_MINUTES);
        newTop = (snappedMin / 60) * CONFIG.HOUR_HEIGHT;

        if (Math.abs(newTop - origTop) > 5) {
          const event = Store.getEvent(eventId);
          if (!event) return;
          const startMinutesFromMidnight = Math.round((newTop / CONFIG.HOUR_HEIGHT) * 60);
          const durationMin = DateUtil.timeToMinutes(event.endHours, event.endMinutes) - DateUtil.timeToMinutes(event.startHours, event.startMinutes);

          let newStartH = Math.floor(startMinutesFromMidnight / 60);
          let newStartM = startMinutesFromMidnight % 60;
          let newEndMin = startMinutesFromMidnight + durationMin;
          let newEndH = Math.min(Math.floor(newEndMin / 60), 23);
          let newEndM = newEndMin % 60;

          if (event.recurrence && event.recurrence.type !== 'none') {
            // Add exception for this occurrence with new time
            Store.addException(eventId, dateKey, { overrides: { startHours: newStartH, startMinutes: newStartM, endHours: newEndH, endMinutes: newEndM } });
          } else {
            Store.updateEvent(eventId, { startHours: newStartH, startMinutes: newStartM, endHours: newEndH, endMinutes: newEndM });
          }
          this.render();
        }
      };

      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
    });
  },

  /* ============================================================
     RENDER: AGENDA VIEW
     ============================================================ */
  renderAgenda(container) {
    const today = DateUtil.getToday();
    // Show events from today for the next 7 days
    let html = '<div class="agenda-view">';

    let foundEvents = false;
    for (let i = 0; i < 14; i++) {
      const d = DateUtil.addDays(this.currentYear, this.currentMonth, 15, i - 7);
      const events = Store.getEventsForDate(d.year, d.month, d.day);

      if (events.length === 0) continue;
      foundEvents = true;

      const isToday = DateUtil.isSameDay(d.year, d.month, d.day, today.year, today.month, today.day);
      let dateLabel = `${MONTH_NAMES[d.month]} ${d.day}, ${d.year}`;
      if (isToday) dateLabel = 'Today';
      else {
        const tomorrow = DateUtil.addDays(today.year, today.month, today.day, 1);
        if (DateUtil.isSameDay(d.year, d.month, d.day, tomorrow.year, tomorrow.month, tomorrow.day)) dateLabel = 'Tomorrow';
      }

      html += `<div class="agenda-date-group">`;
      html += `<div class="agenda-date-header">${dateLabel}</div>`;

      for (const ev of events) {
        const timeStr = ev.allDay ? 'All day' : `${DateUtil.formatTime(ev.startHours, ev.startMinutes)} – ${DateUtil.formatTime(ev.endHours, ev.endMinutes)}`;
        html += `<div class="agenda-event-item" data-event-id="${ev.id}" data-date="${DateUtil.formatDateKey(d.year, d.month, d.day)}">`;
        html += `<div class="agenda-color-bar" style="background:${ev.color}"></div>`;
        html += `<div class="agenda-event-info"><div class="agenda-event-title">${ev.title}</div><div class="agenda-event-time">${timeStr}</div></div>`;
        html += '</div>';
      }

      html += '</div>';
    }

    if (!foundEvents) {
      html += '<div style="text-align:center;padding:48px;color:var(--color-text-secondary);font-size:15px;">No upcoming events</div>';
    }

    html += '</div>';
    container.innerHTML = html;

    // Bind agenda click to edit
    container.addEventListener('click', (e) => {
      const item = e.target.closest('.agenda-event-item');
      if (!item) return;
      this.openEventModal(item.dataset.eventId, null, null, null, item.dataset.date);
    });
  },

  /* ============================================================
     EVENT MODAL
     ============================================================ */
  openEventModal(eventId, year, month, day, occurrenceDateKey, allDay, startH, startM, endH, endM) {
    const overlay = document.createElement('div');
    overlay.className = 'modal-overlay';

    let event = null;
    if (eventId) {
      event = Store.getEvent(eventId);
    }

    // Determine the occurrence date for display/editing
    let occYear, occMonth, occDay;
    if (occurrenceDateKey) {
      const d = DateUtil.parseDateKey(occurrenceDateKey);
      occYear = d.year; occMonth = d.month; occDay = d.day;
    } else if (year !== null && month !== null && day !== null) {
      occYear = year; occMonth = month; occDay = day;
    } else if (event) {
      occYear = event.year; occMonth = event.month; occDay = event.day;
    }

    const isRecurringSeries = event && event.recurrence && event.recurrence.type !== 'none';
    const isExceptionEdit = occurrenceDateKey && isRecurringSeries && !Store.getEvent(eventId)?.recurrence?.type || false;

    // Check if this is an exception instance of a recurring series
    let isInstanceOnly = false;
    if (isRecurringSeries && occurrenceDateKey) {
      const excKey = `${eventId}::${occurrenceDateKey}`;
      isInstanceOnly = !!Store.exceptions[excKey] || !DateUtil.isSameDay(event.year, event.month, event.day, occYear, occMonth, occDay);
    }

    // Default values for new event
    const title = event ? event.title : '';
    const evtAllDay = event ? event.allDay : (allDay ?? false);
    const sH = event ? event.startHours : (startH ?? 9);
    const sM = event ? event.startMinutes : (startM ?? 0);
    const eH = event ? event.endHours : (endH ?? 10);
    const eM = event ? event.endMinutes : (endM ?? 0);
    const color = event ? event.color : CONFIG.COLORS[0].value;
    const notes = event ? event.notes : '';

    // Recurrence defaults
    let recType = 'none';
    let recWeekdays = [];
    if (event && event.recurrence) {
      recType = event.recurrence.type || 'none';
      recWeekdays = event.recurrence.weekdays || [DateUtil.dayOfWeek(event.year, event.month, event.day)];
    }

    const dateStr = DateUtil.formatDateKey(occYear, occMonth, occDay);

    let seriesOptionsHtml = '';
    if (isInstanceOnly) {
      seriesOptionsHtml = `
        <div class="series-edit-options">
          <div class="series-edit-title">This is a recurring event</div>
          <label class="recurrence-option"><input type="radio" name="seriesEditScope" value="instance" checked> This event only</label>
          <label class="recurrence-option"><input type="radio" name="seriesEditScope" value="following"> This and following events</label>
          <label class="recurrence-option"><input type="radio" name="seriesEditScope" value="all"> All events in series</label>
        </div>`;
    }

    overlay.innerHTML = `
      <div class="modal">
        <div class="modal-header">
          <span class="modal-title">${event ? 'Edit Event' : 'New Event'}</span>
          <button class="modal-close" id="modalClose">&times;</button>
        </div>
        <div class="modal-body">
          ${seriesOptionsHtml}
          <div class="form-group">
            <label class="form-label">Title</label>
            <input type="text" class="form-input" id="evtTitle" value="${this._esc(title)}" placeholder="Add title">
          </div>
          <div class="form-group">
            <label class="form-label">Date</label>
            <input type="date" class="form-input" id="evtDate" value="${dateStr}">
          </div>
          <div class="form-row">
            <div class="form-group">
              <label class="checkbox-group">
                <input type="checkbox" id="evtAllDay" ${evtAllDay ? 'checked' : ''}>
                <span>All day</span>
              </label>
            </div>
          </div>
          <div class="form-row" id="timeRow">
            <div class="form-group">
              <label class="form-label">Start time</label>
              <input type="time" class="form-input" id="evtStartTime" value="${String(sH).padStart(2,'0')}:${String(sM).padStart(2,'0')}">
            </div>
            <div class="form-group">
              <label class="form-label">End time</label>
              <input type="time" class="form-input" id="evtEndTime" value="${String(eH).padStart(2,'0')}:${String(eM).padStart(2,'0')}">
            </div>
          </div>
          <div id="timeError" class="error-text">End time must be after start time</div>
          <div class="form-group">
            <label class="form-label">Color</label>
            <div class="color-picker" id="colorPicker">
              ${CONFIG.COLORS.map(c => `<div class="color-swatch${c.value === color ? ' selected' : ''}" style="background:${c.value}" data-color="${c.value}"></div>`).join('')}
            </div>
          </div>
          <div class="form-group">
            <label class="form-label">Recurrence</label>
            <select class="form-select" id="evtRecType">
              <option value="none" ${recType==='none'?'selected':''}>Does not repeat</option>
              <option value="daily" ${recType==='daily'?'selected':''}>Daily</option>
              <option value="weekly" ${recType==='weekly'?'selected':''}>Weekly</option>
              <option value="monthly" ${recType==='monthly'?'selected':''}>Monthly</option>
              <option value="yearly" ${recType==='yearly'?'selected':''}>Yearly</option>
            </select>
          </div>
          <div id="weeklyOptions" style="${recType==='weekly'?'':'display:none'}">
            <label class="form-label" style="margin-bottom:4px;">Repeat on</label>
            <div class="weekday-pills" id="weekdayPills">
              ${DAY_NAMES_SHORT.map((name, i) => `<div class="weekday-pill${recWeekdays.includes(i)?' selected':''}" data-day="${i}">${name.charAt(0)}</div>`).join('')}
            </div>
          </div>
          <div class="form-group">
            <label class="form-label">Notes</label>
            <textarea class="form-textarea" id="evtNotes" placeholder="Add notes...">${this._esc(notes)}</textarea>
          </div>
        </div>
        <div class="modal-footer">
          ${event ? `<button class="btn-delete" id="evtDelete">Delete</button>` : '<span></span>'}
          <button class="btn-save" id="evtSave">Save</button>
        </div>
      </div>
    `;

    document.getElementById('modalContainer').appendChild(overlay);

    // Focus title
    setTimeout(() => overlay.querySelector('#evtTitle')?.focus(), 100);

    // All-day toggle
    const allDayCb = overlay.querySelector('#evtAllDay');
    const timeRow = overlay.querySelector('#timeRow');
    allDayCb.addEventListener('change', () => {
      timeRow.style.display = allDayCb.checked ? 'none' : '';
    });

    // Color picker
    let selectedColor = color;
    overlay.querySelectorAll('.color-swatch').forEach(sw => {
      sw.addEventListener('click', () => {
        overlay.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
        sw.classList.add('selected');
        selectedColor = sw.dataset.color;
      });
    });

    // Recurrence type change
    const recSelect = overlay.querySelector('#evtRecType');
    const weeklyOpts = overlay.querySelector('#weeklyOptions');
    recSelect.addEventListener('change', () => {
      weeklyOpts.style.display = recSelect.value === 'weekly' ? '' : 'none';
    });

    // Weekday pills toggle
    overlay.querySelectorAll('.weekday-pill').forEach(pill => {
      pill.addEventListener('click', () => pill.classList.toggle('selected'));
    });

    // Close modal
    const closeModal = () => overlay.remove();
    overlay.querySelector('#modalClose').addEventListener('click', closeModal);
    overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });

    // Save
    overlay.querySelector('#evtSave').addEventListener('click', () => {
      const titleVal = overlay.querySelector('#evtTitle').value.trim() || 'Untitled';
      const dateVal = overlay.querySelector('#evtDate').value;
      if (!dateVal) return;
      const d = DateUtil.parseDateKey(dateVal);

      const allDayVal = overlay.querySelector('#evtAllDay').checked;
      let startHr = sH, startMi = sM, endHr = eH, endMi = eM;
      if (!allDayVal) {
        const st = DateUtil.parseTime(overlay.querySelector('#evtStartTime').value || '09:00');
        const et = DateUtil.parseTime(overlay.querySelector('#evtEndTime').value || '10:00');
        startHr = st.hours; startMi = st.minutes; endHr = et.hours; endMi = et.minutes;

        // Validation
        if (DateUtil.timeToMinutes(endHr, endMi) <= DateUtil.timeToMinutes(startHr, startMi)) {
          overlay.querySelector('#timeError').classList.add('visible');
          return;
        }
      }
      overlay.querySelector('#timeError')?.classList.remove('visible');

      const notesVal = overlay.querySelector('#evtNotes').value.trim();
      const recTypeVal = overlay.querySelector('#evtRecType').value;
      const selectedWeekdays = [];
      overlay.querySelectorAll('.weekday-pill.selected').forEach(p => selectedWeekdays.push(parseInt(p.dataset.day)));

      const recurrence = { type: recTypeVal };
      if (recTypeVal === 'weekly') recurrence.weekdays = selectedWeekdays.length ? selectedWeekdays : [DateUtil.dayOfWeek(d.year, d.month, d.day)];
      if (recTypeVal === 'monthly') recurrence.dayOfMonth = d.day;

      const seriesScope = overlay.querySelector('input[name="seriesEditScope"]:checked');
      const scope = seriesScope ? seriesScope.value : null;

      if (event) {
        // Editing existing event
        if (isInstanceOnly && scope === 'instance') {
          // Edit this occurrence only — add exception override
          Store.addException(eventId, dateVal, { overrides: { title: titleVal, allDay: allDayVal, startHours: startHr, startMinutes: startMi, endHours: endHr, endMinutes: endMi, color: selectedColor, notes: notesVal } });
        } else if (isInstanceOnly && scope === 'following') {
          // Edit this and following — set series start date to current occurrence
          Store.updateEvent(eventId, { title: titleVal, allDay: allDayVal, startHours: startHr, startMinutes: startMi, endHours: endHr, endMinutes: endMi, color: selectedColor, notes: notesVal, recurrence, _seriesStartDate: dateVal });
        } else if (isInstanceOnly && scope === 'all') {
          // Edit entire series — update master and clear exceptions
          Store.updateEvent(eventId, { title: titleVal, allDay: allDayVal, startHours: startHr, startMinutes: startMi, endHours: endHr, endMinutes: endMi, color: selectedColor, notes: notesVal, recurrence });
          // Clear all exceptions for this series
          const keysToRemove = Object.keys(Store.exceptions).filter(k => k.startsWith(eventId + '::'));
          for (const k of keysToRemove) delete Store.exceptions[k];
          Store.save();
        } else {
          // Normal edit (non-recurring or editing master directly)
          Store.updateEvent(eventId, { title: titleVal, year: d.year, month: d.month, day: d.day, allDay: allDayVal, startHours: startHr, startMinutes: startMi, endHours: endHr, endMinutes: endMi, color: selectedColor, notes: notesVal, recurrence });
        }
      } else {
        // Creating new event
        Store.createEvent({ title: titleVal, year: d.year, month: d.month, day: d.day, allDay: allDayVal, startHours: startHr, startMinutes: startMi, endHours: endHr, endMinutes: endMi, color: selectedColor, notes: notesVal, recurrence });
      }

      closeModal();
      this.render();
    });

    // Delete
    if (event) {
      overlay.querySelector('#evtDelete').addEventListener('click', () => {
        const seriesScope = overlay.querySelector('input[name="seriesEditScope"]:checked');
        const scope = seriesScope ? seriesScope.value : null;

        if (isInstanceOnly && scope === 'instance') {
          // Delete only this occurrence
          Store.addException(eventId, dateVal, { excluded: true });
          showToast('Event deleted', () => {});
        } else if (isInstanceOnly && scope === 'following') {
          // Delete this and following — update series start
          const newStart = DateUtil.addDays(occYear, occMonth, occDay, 1);
          Store.updateEvent(eventId, { _seriesStartDate: DateUtil.formatDateKey(newStart.year, newStart.month, newStart.day) });
          showToast('Future events deleted');
        } else if (isInstanceOnly && scope === 'all') {
          // Delete entire series
          if (!confirm('Delete the entire recurring event?')) return;
          Store.deleteEvent(eventId);
          showToast('Series deleted');
        } else {
          if (!confirm('Delete this event?')) return;
          Store.deleteEvent(eventId);
          showToast('Event deleted');
        }

        closeModal();
        this.render();
      });
    }

    // Keyboard shortcuts
    overlay.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') closeModal();
    });
  },

  _esc(str) { return String(str).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
};

/* ============================================================
   SECTION: TOAST NOTIFICATIONS
   ============================================================ */
function showToast(message, undoAction) {
  const container = document.getElementById('toastContainer');
  const toast = document.createElement('div');
  toast.className = 'toast';
  let html = `<span>${message}</span>`;
  if (undoAction) html += `<button class="toast-action" id="toastUndo">UNDO</button>`;
  toast.innerHTML = html;
  container.appendChild(toast);

  if (undoAction) {
    toast.querySelector('#toastUndo').addEventListener('click', () => { undoAction(); toast.remove(); });
  }

  setTimeout(() => { if (toast.parentNode) toast.remove(); }, 4000);
}

/* ============================================================
   SECTION: INITIALIZATION
   ============================================================ */
document.addEventListener('DOMContentLoaded', () => App.init());
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"full","sessionId":"cceb6fa9-34f3-479f-b157-e346201e76ec","tokensIn":1741634,"tokensOut":40302,"tokensTotal":1781936,"turns":39,"toolCalls":38,"failedToolCalls":0,"timestamp":"2026-07-29T21:57:06.019Z"} -->