← Calendar results

Calendar

Qwen 3.7 Flash · full

Initial view of Calendar

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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 57,191 bytes · SHA-256 4e8ecd370a5d
<!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>
:root {
  --color-primary: #1a73e8;
  --color-primary-light: #e8f0fe;
  --color-danger: #d93025;
  --color-danger-light: #fce8e6;
  --color-success: #1e8e3e;
  --color-border: #e0e0e0;
  --color-text: #202124;
  --color-text-secondary: #5f6368;
  --color-bg: #ffffff;
  --color-bg-secondary: #f8f9fa;
  --header-height: 56px;
  --color-palette: #1a73e8,#d93025,#1e8e3e,#f9ab00,#9334e6,#e67c73,#185abc,#3c6e71,#b06000,#8430ce;
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI',Roboto,-apple-system,BlinkMacSystemFont,sans-serif;color:var(--color-text);background:var(--color-bg);overflow:hidden;height:100vh}

/* HEADER */
.header{display:flex;align-items:center;height:var(--header-height);padding:0 16px;border-bottom:1px solid var(--color-border);background:var(--color-bg);position:relative;z-index:10}
.header-title{font-size:22px;font-weight:400;flex-shrink:0;width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.header-nav{display:flex;gap:2px;flex-shrink:0}
.header-nav button{background:none;border:1px solid var(--color-border);border-radius:4px;padding:6px 12px;cursor:pointer;font-size:13px;color:var(--color-text);transition:background .15s}
.header-nav button:hover{background:var(--color-bg-secondary)}
#today-btn{border:1px solid var(--color-border);background:var(--color-bg);font-weight:500}
#today-btn:hover{background:#f1f3f4}
.spacer{flex:1}
.view-switcher{display:flex;border:1px solid var(--color-border);border-radius:4px;overflow:hidden;flex-shrink:0}
.view-switcher button{background:var(--color-bg);border:none;border-right:1px solid var(--color-border);padding:4px 16px;cursor:pointer;font-size:13px;color:var(--color-text-secondary);transition:all .15s}
.view-switcher button:last-child{border-right:none}
.view-switcher button:hover{background:var(--color-bg-secondary);color:var(--color-text)}
.view-switcher button.active{background:var(--color-primary-light);color:var(--color-primary);font-weight:500}
#settings-btn{background:none;border:1px solid var(--color-border);border-radius:50%;width:36px;height:36px;cursor:pointer;font-size:18px;margin-left:4px;color:var(--color-text-secondary)}

.main{height:calc(100vh - var(--header-height));overflow:hidden;position:relative}
.view-container{width:100%;height:100%}
.view-container.hidden{display:none}

/* MONTH VIEW */
.month-header{display:grid;grid-template-columns:repeat(7,1fr);border-bottom:1px solid var(--color-border)}
.month-header-cell{padding:8px 0;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);grid-template-rows:repeat(6,1fr);border-left:1px solid var(--color-border);border-bottom:1px solid var(--color-border);flex:1}
.month-cell{border-right:1px solid var(--color-border);border-bottom:1px solid var(--color-border);padding:2px 3px;min-height:0;cursor:pointer;overflow:hidden;transition:background .1s}
.month-cell:hover{background:var(--color-bg-secondary)}
.month-cell.other-month{background:var(--color-bg-secondary)}
.month-cell.other-month .day-number{color:var(--color-text-secondary);opacity:.5}
.month-cell.today .day-number{width:26px;height:26px;background:var(--color-today);border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--color-primary);font-weight:700;margin:0 auto}
.day-number{font-size:11px;font-weight:500;color:var(--color-text);padding:2px;min-height:18px;display:flex;align-items:center;justify-content:center}
.month-events{display:flex;flex-direction:column;gap:1px;overflow:hidden}
.month-event-chip{font-size:11px;padding:1px 4px;border-radius:2px;cursor:grab;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-left:3px solid;user-select:none;line-height:1.4}
.month-event-chip:active{cursor:grabbing}
.month-more-chip{font-size:11px;padding:1px 4px;color:var(--color-text-secondary);cursor:pointer;border-left:3px solid var(--color-border)}
.month-more-chip:hover{background:var(--color-bg-secondary);color:var(--color-text)}

/* DAY POPOVER */
.day-popover{position:fixed;background:var(--color-bg);border:1px solid var(--color-border);border-radius:8px;box-shadow:0 4px 24px rgba(0,0,0,.15);padding:12px;width:300px;max-height:400px;overflow-y:auto;z-index:100;display:none}
.day-popover.visible{display:block}
.day-popover-header{font-weight:500;margin-bottom:8px;font-size:14px}
.day-popover-event{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer;margin-bottom:2px}
.day-popover-event:hover{background:var(--color-bg-secondary)}
.day-popover-event-color{width:4px;height:24px;border-radius:2px;flex-shrink:0}
.day-popover-event-info{flex:1;min-width:0}
.day-popover-event-title{font-size:13px;font-weight:500}
.day-popover-event-time{font-size:11px;color:var(--color-text-secondary)}
.day-popover-add{margin-top:8px;padding:6px 12px;border:1px dashed var(--color-border);border-radius:4px;text-align:center;color:var(--color-primary);font-size:13px;cursor:pointer;width:100%}
.day-popover-add:hover{background:var(--color-primary-light);border-color:var(--color-primary)}

/* DRAG GHOST */
.drag-ghost{position:fixed;pointer-events:none;z-index:1000;padding:4px 10px;border-radius:3px;font-size:11px;font-weight:500;box-shadow:0 2px 8px rgba(0,0,0,.2);opacity:.85;display:none}

/* WEEK VIEW */
.week-view{height:100%;display:flex;flex-direction:column}
.week-header{display:flex;border-bottom:1px solid var(--color-border);background:var(--color-bg);flex-shrink:0}
.week-header-corner{width:56px;flex-shrink:0;border-right:1px solid var(--color-border)}
.week-header-days{display:flex}
.week-header-day{flex:1;text-align:center;padding:6px 0 4px;border-right:1px solid var(--color-border)}
.week-header-day:last-child{border-right:none}
.week-header-day .day-label{font-size:11px;color:var(--color-text-secondary);text-transform:uppercase}
.week-header-day .day-number{font-size:18px;font-weight:400;margin:2px 0 0;width:36px;height:36px;display:flex;align-items:center;justify-content:center;margin-inline:auto}
.week-header-day.today .day-number{background:var(--color-primary);color:white;border-radius:50%;font-weight:700}
.week-body{flex:1;display:flex;flex-direction:column;overflow:hidden}
.week-body-scroll{flex:1;display:flex;overflow-y:auto;overflow-x:hidden;position:relative}
.week-time-gutter{width:56px;flex-shrink:0}
.week-time-label{font-size:10px;color:var(--color-text-secondary);text-align:right;padding-right:8px;height:48px;line-height:48px;transform:translateY(-7px)}
.week-day-columns{flex:1;display:flex}
.week-day-column{flex:1;position:relative;border-right:1px solid var(--color-border)}
.week-day-column:last-child{border-right:none}
.week-hour-row{border-bottom:1px solid var(--color-border);height:48px;position:relative}
.week-hour-row.half-hour{border-bottom:1px solid #f5f5f5;height:24px}
.week-now-line{position:absolute;left:0;right:0;height:2px;background:var(--color-danger);z-index:5;pointer-events:none}
.week-now-line::before{content:'';position:absolute;left:-5px;top:-4px;width:10px;height:10px;background:var(--color-danger);border-radius:50%}
.week-all-day-area{height:32px;border-bottom:1px solid var(--color-border);display:flex;flex-shrink:0;position:relative}
.week-all-day-area .all-day-divider{width:56px;flex-shrink:0;border-right:1px solid var(--color-border);background:var(--color-bg)}
.week-event-block{position:absolute;left:2px;right:2px;border-radius:3px;padding:1px 4px;font-size:11px;overflow:hidden;cursor:grab;user-select:none;border-left:3px solid;line-height:1.3;z-index:3}
.week-event-block:active{cursor:grabbing}
.week-event-block .event-block-title{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.week-event-block .event-block-time{font-size:10px;opacity:.8}

/* AGENDA VIEW */
.agenda-view{height:100%;overflow-y:auto;padding:8px 0}
.agenda-day{margin-bottom:4px}
.agenda-day-header{font-size:13px;font-weight:500;color:var(--color-text-secondary);padding:8px 16px;border-bottom:1px solid var(--color-border)}
.agenda-day-events{padding:4px 0}
.agenda-event{display:flex;align-items:center;padding:6px 16px;gap:12px;cursor:pointer;border-radius:4px;margin:0 8px}
.agenda-event:hover{background:var(--color-bg-secondary)}
.agenda-event-color{width:4px;height:32px;border-radius:2px;flex-shrink:0}
.agenda-event-time{font-size:12px;color:var(--color-text-secondary);white-space:nowrap;min-width:80px}
.agenda-event-title{font-size:14px;font-weight:500;flex:1}

/* MODAL */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:200;display:none;align-items:center;justify-content:center}
.modal-overlay.visible{display:flex}
.dialog{background:var(--color-bg);border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.2);width:100%;max-width:480px;max-height:90vh;overflow-y:auto;padding:24px}
.modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.modal-title{font-size:20px;font-weight:400}
.modal-close{background:none;border:none;font-size:22px;cursor:pointer;color:var(--color-text-secondary);padding:4px 8px;border-radius:50%}
.modal-close:hover{background:var(--color-bg-secondary)}
.form-group{margin-bottom:16px}
.form-group label{display:block;font-size:12px;font-weight:500;color:var(--color-text-secondary);margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px}
.form-group input,.form-group textarea,.form-group select{width:100%;padding:8px 12px;border:1px solid var(--color-border);border-radius:6px;font-size:14px;font-family:inherit;color:var(--color-text);outline:none;transition:border-color .15s}
.form-group input:focus,.form-group textarea:focus,.form-group select:focus{border-color:var(--color-primary);box-shadow:0 0 0 2px var(--color-primary-light)}
.form-group textarea{resize:vertical;min-height:60px}
.form-group .error-msg{font-size:12px;color:var(--color-danger);margin-top:4px;display:none}
.form-group .error-msg.visible{display:block}
.form-row{display:flex;gap:12px}
.form-row .form-group{flex:1}
.toggle-group{display:flex;gap:6px}
.toggle-btn{padding:6px 14px;border:1px solid var(--color-border);border-radius:16px;background:var(--color-bg);cursor:pointer;font-size:13px;color:var(--color-text-secondary);transition:all .15s}
.toggle-btn:hover{border-color:var(--color-text-secondary)}
.toggle-btn.active{background:var(--color-primary-light);border-color:var(--color-primary);color:var(--color-primary);font-weight:500}
.color-picker{display:flex;gap:6px;flex-wrap:wrap}
.color-swatch{width:28px;height:28px;border-radius:50%;cursor:pointer;border:2px solid transparent;transition:transform .15s}
.color-swatch:hover{transform:scale(1.15)}
.color-swatch.selected{border-color:var(--color-text);transform:scale(1.15)}
.recurrence-group{border:1px solid var(--color-border);border-radius:8px;padding:12px;margin-top:4px}
.recurrence-options{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}
.weekday-selector{display:flex;gap:4px;margin-top:8px;flex-wrap:wrap}
.weekday-btn{padding:4px 8px;border:1px solid var(--color-border);border-radius:4px;background:var(--color-bg);cursor:pointer;font-size:12px;color:var(--color-text-secondary);transition:all .15s}
.weekday-btn.selected{background:var(--color-primary-light);border-color:var(--color-primary);color:var(--color-primary)}
.recurrence-extra{margin-top:6px;font-size:12px;color:var(--color-text-secondary)}
.modal-actions{display:flex;justify-content:space-between;margin-top:24px;gap:8px}
.btn{padding:8px 20px;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;border:none;transition:all .15s;font-family:inherit}
.btn-primary{background:var(--color-primary);color:white}
.btn-primary:hover{background:#1557b0}
.btn-secondary{background:var(--color-bg-secondary);color:var(--color-text);border:1px solid var(--color-border)}
.btn-secondary:hover{background:#e8eaed}
.btn-danger{background:var(--color-danger);color:white}
.btn-danger:hover{background:#b3261e}
.btn-ghost{background:none;color:var(--color-danger);padding:8px 12px}
.btn-ghost:hover{background:var(--color-danger-light)}

/* RECURRING EDIT DIALOG */
.recurring-dialog{width:100%;max-width:400px;padding:24px}
.recurring-option{padding:12px 16px;border:1px solid var(--color-border);border-radius:8px;margin-bottom:8px;cursor:pointer;transition:all .15s}
.recurring-option:hover{background:var(--color-bg-secondary)}
.recurring-option.selected{border-color:var(--color-primary);background:var(--color-primary-light)}
.recurring-option-title{font-size:14px;font-weight:500}
.recurring-option-desc{font-size:12px;color:var(--color-text-secondary);margin-top:2px}

/* SETTINGS */
.settings-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:200;display:none;align-items:center;justify-content:center}
.settings-overlay.visible{display:flex}
.settings-panel{background:var(--color-bg);border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.2);padding:24px;width:340px}
.settings-panel h3{font-size:18px;font-weight:400;margin-bottom:20px}
.setting-item{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--color-border)}
.setting-item:last-of-type{border-bottom:none}
.setting-label{font-size:14px}

/* TOAST */
.toast-container{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:300;display:flex;flex-direction:column;align-items:center;gap:8px}
.toast{background:#323232;color:white;padding:10px 20px;border-radius:8px;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,.3);display:flex;align-items:center;gap:12px}
.toast-action{color:var(--color-primary);font-weight:500;cursor:pointer;background:none;border:none;font-size:14px;font-family:inherit}

/* RESPONSIVE */
@media(max-width:768px){.header-title{font-size:18px}.month-header-cell{font-size:10px;padding:4px 0}.day-number{font-size:10px}.month-event-chip{font-size:10px;padding:1px 4px}.header{padding:0 8px;gap:6px}.view-switcher button{padding:4px 10px;font-size:12px}}
</style>
</head>
<body>

<div class="header">
  <div class="header-title" id="header-title">Calendar</div>
  <div class="header-nav">
    <button id="today-btn">Today</button>
    <button id="prev-btn">‹</button>
    <button id="next-btn">›</button>
  </div>
  <div class="spacer"></div>
  <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 id="settings-btn" title="Settings">⚙</button>
</div>

<div class="main">
  <div class="view-container" id="view-month">
    <div class="month-header" id="month-header"></div>
    <div class="month-grid" id="month-grid"></div>
  </div>
  <div class="view-container" id="view-week" style="display:none">
    <div class="week-view">
      <div class="week-header">
        <div class="week-header-corner"></div>
        <div class="week-header-days" id="week-header-days"></div>
      </div>
      <div class="week-body">
        <div class="week-body-scroll" id="week-body-scroll">
          <div class="week-time-gutter" id="week-time-gutter"></div>
          <div class="week-day-columns" id="week-day-columns"></div>
        </div>
      </div>
    </div>
  </div>
  <div class="view-container" id="view-agenda" style="display:none">
    <div class="agenda-view" id="agenda-container"></div>
  </div>
</div>

<!-- EVENT EDITOR MODAL -->
<div class="modal-overlay" id="editor-overlay">
  <div class="dialog">
    <div class="modal-header">
      <div class="modal-title" id="editor-title">New Event</div>
      <button class="modal-close" id="editor-close">&times;</button>
    </div>
    <div class="form-group">
      <label>Title</label>
      <input type="text" id="e-title" placeholder="Add title" autocomplete="off">
      <div class="error-msg" id="e-title-err"></div>
    </div>
    <div class="form-group">
      <div class="toggle-group" id="e-allday">
        <button class="toggle-btn active" data-val="true">All day</button>
        <button class="toggle-btn" data-val="false">Has start/end time</button>
      </div>
    </div>
    <div class="form-group"><label>Date</label><input type="date" id="e-date"></div>
    <div class="form-row" id="e-time-row">
      <div class="form-group"><label>Start</label><input type="time" id="e-start"><div class="error-msg" id="e-start-err"></div></div>
      <div class="form-group"><label>End</label><input type="time" id="e-end"><div class="error-msg" id="e-end-err"></div></div>
    </div>
    <div class="form-group"><label>Notes</label><textarea id="e-notes" rows="2"></textarea></div>
    <div class="form-group">
      <label>Color</label>
      <div class="color-picker" id="e-colors"></div>
    </div>
    <div class="form-group">
      <label>Recurrence</label>
      <div class="recurrence-group">
        <div class="recurrence-options">
          <button class="toggle-btn active" data-r="none">None</button>
          <button class="toggle-btn" data-r="daily">Daily</button>
          <button class="toggle-btn" data-r="weekly">Weekly</button>
          <button class="toggle-btn" data-r="monthly">Monthly</button>
          <button class="toggle-btn" data-r="yearly">Yearly</button>
        </div>
        <div id="e-weekday-group" style="display:none">
          <div class="weekday-selector" id="e-weekdays">
            <button class="weekday-btn" data-d="1">Mon</button>
            <button class="weekday-btn" data-d="2">Tue</button>
            <button class="weekday-btn" data-d="3">Wed</button>
            <button class="weekday-btn" data-d="4">Thu</button>
            <button class="weekday-btn" data-d="5">Fri</button>
            <button class="weekday-btn" data-d="6">Sat</button>
            <button class="weekday-btn" data-d="0">Sun</button>
          </div>
        </div>
      </div>
    </div>
    <div class="modal-actions" style="font-size:12px">
      <div style="display:flex;gap:8px">
        <button class="btn btn-danger" id="e-delete" style="display:none">Delete</button>
        <button class="btn btn-ghost" id="e-cancel" style="display:none">Cancel</button>
      </div>
      <div style="display:flex;gap:8px">
        <button class="btn btn-primary" id="e-save">Save</button>
      </div>
    </div>
  </div>
</div>

<!-- RECURRING EDIT CHOSE MODAL -->
<div class="modal-overlay" id="recur-overlay">
  <div class="recurring-dialog">
    <div class="modal-title" style="margin-bottom:16px">Edit Recurring Event</div>
    <div class="recurring-option" data-c="entire">
      <div class="recurring-option-title">Entire series</div>
      <div class="recurring-option-desc">Edit all occurrences</div>
    </div>
    <div class="recurring-option selected" data-c="this">
      <div class="recurring-option-title">This event only</div>
      <div class="recurring-option-desc">Edit only this occurrence</div>
    </div>
    <div class="recurring-option" data-c="future">
      <div class="recurring-option-title">This and following</div>
      <div class="recurring-option-desc">Edit this and all future occurrences</div>
    </div>
    <div class="modal-actions" style="margin-top:16px">
      <button class="btn btn-secondary" id="recur-cancel">Cancel</button>
      <button class="btn btn-primary" id="recur-ok">OK</button>
    </div>
  </div>
</div>

<!-- DAY POPOVER -->
<div class="day-popover" id="day-popover">
  <div class="day-popover-header" id="dp-header"></div>
  <div id="dp-events"></div>
  <div class="day-popover-add" id="dp-add">+ Add event</div>
</div>

<!-- SETTINGS -->
<div class="settings-overlay" id="settings-overlay">
  <div class="settings-panel">
    <h3>Settings</h3>
    <div class="setting-item">
      <span class="setting-label">Week starts on</span>
      <div class="toggle-group" id="s-weekstart">
        <button class="toggle-btn" data-ws="1">Monday</button>
        <button class="toggle-btn" data-ws="0">Sunday</button>
      </div>
    </div>
    <div class="modal-actions" style="margin-top:16px">
      <button class="btn btn-primary" id="s-save">Done</button>
    </div>
  </div>
</div>

<div class="toast-container" id="toasts"></div>
<div class="drag-ghost" id="drag-ghost"></div>

<script>
/* ============================================================
   CONSTANTS
   ============================================================ */
const CFG = Object.freeze({
  PALETTE: ['#1a73e8','#d93025','#1e8e3e','#f9ab00','#9334e6','#e67c73','#185abc','#3c6e71','#b06000','#8430ce'],
  HOUR_START: 6, HOUR_END: 22, SNAP: 15,
  MAX_MONTH_EVENTS: 3,
  LS_EVENTS: 'cal_events',
  LS_SETTINGS: 'cal_settings',
  LS_VIEW: 'cal_view'
});

/* ============================================================
   DATE UTILITIES — pure functions, no Date arithmetic hacks
   ============================================================ */
const DU = {
  isLeap(y){ return (y%400===0)||(y%4===0&&y%100!==0); },
  dim(y,m){ return [31,DU.isLeap(y)?29:28,31,30,31,30,31,31,30,31,30,31][m]; },

  dow(y,m,d){ return new Date(y,m,d).getDay(); }, // 0=Sun

  addDays(y,m,d,n){
    let cy=y,cm=m,cd=d+n;
    while(cd<1){cm--;if(cm<0){cm=11;cy--}cd+=DU.dim(cy,cm)}
    while(cd>DU.dim(cy,cm)){cd-=DU.dim(cy,cm);cm++;if(cm>11){cm=0;cy++}}
    return {y:cy,m:cm,d:cd};
  },
  addMonths(y,m,d,n){
    let cy=y,cm=m+n;
    while(cm<0){cm+=12;cy--}while(cm>11){cm-=12;cy++}
    const cd2=Math.min(d,DU.dim(cy,cm));
    return {y:cy,m:cm,d:cd2};
  },
  addYears(y,m,d,n){ return {y:y+n,m:m,d:d}; }, // typo fixed below

  same(a,b){ return a.y===b.y&&a.m===b.m&&a.d===b.d; },

  weekStart(y,m,d,ws){
    const dw=DU.dow(y,m,d);
    let diff=dw-ws;if(diff<0)diff+=7;
    return DU.addDays(y,m,d,-diff);
  },
  weekEnd(y,m,d,ws){ const ws2=DU.weekStart(y,m,d,ws); return DU.addDays(ws2.y,ws2.m,ws2.d,6); },

  fmt(y,m,d){ return `${Z2(y)}-${Z2(m+1)}-${Z2(d)}`; },
  parse(s){const p=s.split('-').map(Number);return{y:p[0],m:p[1]-1,d:p[2]};},
  fmtTime(h,m){const a=h>=12?'PM':'AM'; return`${h%12||12}:${Z2(m)} ${a}`;},
  shortDay(dw){return['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][dw];},
  dayName(dw){return['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][dw];},
  monthName(m){return['January','February','March','April','May','June','July','August','September','October','November','December'][m];},
  shortMonth(m){return['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][m];},
  pad(n,w=2){return String(n).padStart(w,'0');}
};
const Z2=DU.pad;

// Fix addYears
DU.addYears=function(y,m,d,n){return{y:y+n,m:m,d:d};};

/* ============================================================
   RECURRENCE ENGINE
   ============================================================ */
const Rec = {
  advance(r,c,wd){
    switch(r){
      case'daily': return DU.addDays(c.y,c.m,c.d,1);
      case'weekly':
        if(wd&&wd.length>0){
          let nx=DU.addDays(c.y,c.m,c.d,1),it=0;
          while(it<14){if(wd.includes(DU.dow(nx.y,nx.m,nx.d)))return nx;nx=DU.addDays(nx.y,nx.m,nx.d,1);it++}
          return DU.addDays(c.y,c.m,c.d,7);
        }
        return DU.addDays(c.y,c.m,c.d,7);
      case'monthly':{
        // Try successive months until we find one with the same day
        let nx=DU.addMonths(c.y,c.m,c.d,1),lim=186;
        while(nx.d!==c.d&&lim-->0){
          nx=DU.addMonths(nx.y,nx.m,nx.d,1);
        }
        return nx;
      }
      case'yearly': return {y:c.y+1,m:c.m,d:c.d};
      default: return DU.addDays(c.y,c.m,c.d,1);
    }
  },

  // Expand recurring events into occurrence array for [startStr..endStr]
  expand(masterDateStr, master, allEvents, startStr, endStr){
    if(!master.recurrence||master.recurrence==='none'){
      if(masterDateStr>=startStr&&masterDateStr<=endStr) return[{...master,date:masterDateStr,isOcc:true}];
      return [];
    }
    const exc={};
    allEvents.forEach(e=>{
      if(e.parentEvent===master.id&&(e.recurrence==='exception'||e._exc)) exc[e.date]=e;
    });
    const md=DU.parse(masterDateStr);
    let cur={...md}, occs=[], mx=366*4;
    const pad=DU.pad;
    while(cur.y<=endStr.substring(0,4)&&mx-->0){
      const ds=`${pad(cur.y,4)}-${pad(cur.m+1,2)}-${pad(cur.d,2)}`;
      if(ds>endStr) break;
      if(ds>=startStr){
        if(exc[ds]){
          occs.push({...exc[ds],date:ds,isOcc:true,_isExc:true});
        } else {
          occs.push({...master,date:ds,isOcc:true,_isExc:false});
        }
      }
      cur=this.advance(master.recurrence,cur,master.weekdays);
    }
    return occs;
  }
};

/* ============================================================
   STATE
   ============================================================ */
const S = {
  events: [],      // raw stored events
  ocs: [],          // expanded occurrences for current view
  settings: {ws:1}, // ws = weekStart: 1=Mon, 0=Sun
  view: {v:'month',y:null,m:null}, // v=view, y=year, m=month
  _dragId: null,    // event id being dragged
  _editId: null,    // event id being edited
  _isModalNew: true,
  _allday: true,
  _rec: 'none',
  _color: CFG.PALETTE[0],
  _weekdays: [],
  _recurChoice: 'this',  // for edit-choosing modal
  _recurMaster: null,    // master event for recurring-edit
  _editContext: null     // extra context for save
};

function nowDS(){
  const t=new Date();return DU.fmt(t.getFullYear(),t.getMonth(),t.getDate());
}

function visibleRange(){
  const {v,y,m}=S.view;
  let s,e;
  if(v==='month'){
    const ws=DU.weekStart(y,m,1,S.settings.ws);
    s=DU.fmt(ws.y,ws.m,ws.d);
    const we=DU.weekEnd(y,m,DU.dim(y,m),S.settings.ws);
    e=DU.addDays(we.y,we.m,we.d,6);
    e=DU.fmt(e.y,e.m,e.d);
  } else if(v==='week'){
    const ws=DU.weekStart(y,m,1,S.settings.ws);
    s=DU.fmt(ws.y,ws.m,ws.d);
    const we=DU.weekEnd(y,m,1,S.settings.ws);
    e=DU.fmt(we.y,we.m,we.d);
  } else {
    const t=new Date();
    s=DU.fmt(t.getFullYear(),t.getMonth(),t.getDate());
    const ft={y:t.getFullYear(),m:t.getMonth()+2,d:t.getDate()};
    e=DU.fmt(ft.y,ft.m,Math.min(ft.d,DU.dim(ft.y,ft.m)));
  }
  return {s,e};
}

function refresh(){
  const {s,e}=visibleRange();
  S.ocs=[];
  for(const ev of S.events){
    const md=DU.parse(ev.date);
    const ms=DU.fmt(md.y,md.m,md.d);
    S.ocs.push(...Rec.expand(ms,ev,S.events,s,e));
  }
  S.ocs.sort((a,b)=>{if(a.date!==b.date)return a.date<b.date?-1:1;if(a.allDay!==b.allDay)return a.allDay?-1:1;if((a.startTime||'')<(b.startTime||''))return -1;return 0;});
}

function ocsForDay(ds){return S.ocs.filter(o=>o.date===ds)}

/* ============================================================
   STORAGE
   ============================================================ */
function load(){
  try{S.events=JSON.parse(localStorage.getItem(CFG.LS_EVENTS))||[]}catch(S){S.events=[]}
  try{const st=JSON.parse(localStorage.getItem(CFG.LS_SETTINGS));if(st)S.settings=st}catch(S){}
  try{const sv=JSON.parse(localStorage.getItem(CFG.LS_VIEW));if(sv)S.view=sv}catch(S){}
  if(!S.view.v)S.view={v:'month',y:new Date().getFullYear(),m:new Date().getMonth()};
  if(!S.view.y)S.view.y=new Date().getFullYear();
  if(S.view.m==null)S.view.m=new Date().getMonth();
}

function saveAll(){
  localStorage.setItem(CFG.LS_EVENTS,JSON.stringify(S.events));
  localStorage.setItem(CFG.LS_SETTINGS,JSON.stringify(S.settings));
  localStorage.setItem(CFG.LS_VIEW,JSON.stringify(S.view));
}

function gid(){return Date.now().toString(36)+Math.random().toString(36).slice(2,8)}

/* ============================================================
   RENDERING — month header
   ============================================================ */
function renderMonthHeader(){
  const el=document.getElementById('month-header');
  let h='';
  for(let i=0;i<7;i++){
    const d=(S.settings.ws+i)%7;
    h+=`<div class="month-header-cell">${DU.shortDay(d)}</div>`;
  }
  el.innerHTML=h;
}

/* ============================================================
   RENDERING — month grid
   ============================================================ */
let _monthPopover=null;
let _monthPopoverDate=null;

function renderMonthGrid(){
  const {y,m}=S.view;
  const el=document.getElementById('month-grid');
  el.innerHTML='';

  const todayDS=nowDS();
  // First day of month
  const firstDow=DU.dow(y,m,1);
  let lead=firstDow-S.settings.ws;if(lead<0)lead+=7;

  let cur=DU.addDays(y,m,1,-lead);

  for(let i=0;i<42;i++){
    const ds=DU.fmt(cur.y,cur.m,cur.d);
    const isCM=cur.m===m;
    const isT=ds===todayDS;
    const cl=['month-cell'];
    if(!isCM)cl.push('other-month');
    if(isT)cl.push('today');

    const num=document.createElement('div');
    num.className='day-number';
    num.textContent=cur.d;

    const evDiv=document.createElement('div');
    evDiv.className='month-events';
    const dayOcs=ocsForDay(ds);
    const show=dayOcs.slice(0,CFG.MAX_MONTH_EVENTS);
    const extra=Math.max(0,dayOcs.length-CFG.MAX_MONTH_EVENTS);

    for(const ev of show){
      const chip=document.createElement('div');
      chip.className='month-event-chip';
      chip.style.color=ev.color;
      chip.style.background=ev.color+'20';
      chip.style.borderLeftColor=ev.color;
      const ts=ev.allDay?ev.title:DU.fmtTime(parseInt(ev.startTime.split(':')[0]),parseInt(ev.startTime.split(':')[1]))+' '+ev.title;
      chip.textContent=ev.title||'(No title)';
      chip.addEventListener('click',e=>{e.stopPropagation();openClickEvent(ev,ds)});
      chip.draggable=true;
      chip.addEventListener('dragstart',e=>{e.stopPropagation();S._dragId=ev.id;e.dataTransfer.effectAllowed='move';const g=document.getElementById('drag-ghost');g.style.display='block';g.style.color=ev.color;g.style.background=ev.color+'20';g.textContent=(ev.title||'(event)')});

      evDiv.appendChild(chip);
    }

    if(extra>0){
      const more=document.createElement('div');
      more.className='month-more-chip';
      more.textContent=`+${extra} more`;
      more.addEventListener('click',e=>{e.stopPropagation();showDayPicker(ds,e)});
      evDiv.appendChild(more);
    }

    const cell=document.createElement('div');
    cell.className=cl.join(' ');
    cell.dataset.date=ds;
    cell.appendChild(num);
    cell.appendChild(evDiv);

    cell.addEventListener('click',()=>{hideDayPicker();openNewEvent(ds)});

    cell.addEventListener('dragover',e=>{e.preventDefault();cell.style.background='#e8f0fe'});
    cell.addEventListener('dragleave',()=>{cell.style.background=''});
    cell.addEventListener('drop',e=>{
      e.preventDefault();cell.style.background='';e.stopPropagation();
      const eid=e.dataTransfer.getData('text/plain');
      const ev=S.events.find(z=>z.id===eid);
      if(!ev)return;
      // If it's a recurring master or instance, create an exception
      if(ev.parentEvent){
        // Create exception on new date
        const excId=gid();
        const exc={...ev,id:excId,parentEvent:ev.parentEvent,date:ds,recurrence:'exception',_exc:true};
        S.events.push(exc);
        saveAll();refresh();renderCurrent();showToast('Moved to '+ds);
        return;
      }
      ev.date=ds;
      saveAll();refresh();renderCurrent();showToast('Event rescheduled');
    });

    el.appendChild(cell);
    cur=DU.addDays(cur.y,cur.m,cur.d,1);
  }

  // Store date on popover for reference
  _monthPopoverDate=null;
}

function openClickEvent(ev,ds){
  // Recurring check
  if(ev.parentEvent&&!ev._exc){
    S._recurMaster=ev;
    document.getElementById('recur-overlay').classList.add('visible');
    document.querySelectorAll('.recurring-option').forEach(o=>o.classList.toggle('selected',o.dataset.c==='this'));
    S._recurChoice='this';
    return;
  }
  openEditModal(ev.id);
}

/* ============================================================
   DAY PICKER (popover)
   ============================================================ */
function showDayPicker(ds,ev){
  _monthPopoverDate=ds;
  const pop=document.getElementById('day-popover');
  const parsed=DU.parse(ds);
  document.getElementById('dp-header').textContent=DU.dayName(DU.dow(parsed.y,parsed.m,parsed.d))+', '+DU.monthName(parsed.m)+' '+parsed.d+', '+parsed.y;

  const cd=document.getElementById('dp-events');
  cd.innerHTML='';
  for(const o of ocsForDay(ds)){
    const d=document.createElement('div');
    d.className='day-popover-event';
    const bar=document.createElement('div');
    bar.className='day-popover-event-color';bar.style.background=o.color;
    const info=document.createElement('div');info.className='day-popover-event-info';
    const tt=document.createElement('div');tt.className='day-popover-event-title';tt.textContent=o.title;
    const tm=document.createElement('div');tm.className='day-popover-event-time';
    tm.textContent=o.allDay?'All day':(o.startTime&&o.endTime?DU.fmtTime(parseInt(o.startTime.slice(0,2)),parseInt(o.startTime.slice(3)))+' - '+DU.fmtTime(parseInt(o.endTime.slice(0,2)),parseInt(o.endTime.slice(3))):'');
    info.append(tt,tm);d.append(bar,info);
    d.addEventListener('click',()=>{hideDayPicker();openClickEvent(o,ds)});
    cd.appendChild(d);
  }

  // Position
  const rect=ev?ev.target.closest('.month-cell').getBoundingClientRect():null;
  if(rect){pop.style.left=Math.min(rect.right+4,window.innerWidth-310)+'px';pop.style.top=rect.top+'px';}else{pop.style.left='100px';pop.style.top='100px';}
  pop.classList.add('visible');
}

function hideDayPicker(){document.getElementById('day-popover').classList.remove('visible')}

document.getElementById('dp-add').addEventListener('click',()=>{
  hideDayPicker();
  if(_monthPopoverDate) openNewEvent(_monthPopoverDate);
});

document.addEventListener('click',e=>{
  const pop=document.getElementById('day-popover');
  if(pop.classList.contains('visible')&&!pop.contains(e.target)&&!e.target.closest('.month-more-chip')&&!e.target.closest('.month-event-chip')&&!e.target.closest('.day-popover-event')) hideDayPicker();
});

/* ============================================================
   RENDERING — week view
   ============================================================ */
function renderWeek(){
  const {y,m}=S.view;
  const ws=DU.weekStart(y,m,1,S.settings.ws);

  // Header
  let hh='';
  let ws2=DU.fmt(ws.y,ws.m,ws.d);
  for(let i=0;i<7;i++){
    const pd=DU.parse(ws2);
    const isT=ws2===nowDS();
    hh+=`<div class="week-header-day${isT?' today':''}"><div class="day-label">${DU.shortDay(DU.dow(pd.y,pd.m,pd.d))}</div><div class="day-number">${pd.d}</div></div>`;
    ws2=DU.addDays(pd.y,pd.m,pd.d,1);
  }
  document.getElementById('week-header-days').innerHTML=hh;

  // Build body
  const scroll=document.getElementById('week-body-scroll');
  scroll.innerHTML='';

  // Time gutter
  const gut=document.createElement('div');gut.className='week-time-gutter';
  for(let h=1;h<=CFG.HOUR_END;h++){
    const lb=document.createElement('div');lb.className='week-time-label';
    lb.textContent=h>=CFG.HOUR_START?DU.fmtTime(h,0):'';
    gut.appendChild(lb);
  }
  scroll.appendChild(gut);

  // Columns
  const cols=document.createElement('div');cols.style.cssText='flex:1;display:flex;';
  const ppm=48/60;
  const todayDS=nowDS();

  let wd=DU.fmt(ws.y,ws.m,ws.d);
  for(let i=0;i<7;i++){
    const col=document.createElement('div');col.className='week-day-column';col.dataset.date=wd;

    // Events for day
    const dayO=ocsForDay(wd);

    // Hour rows + event blocks
    for(let h=CFG.HOUR_START;h<=CFG.HOUR_END;h++){
      const hr=document.createElement('div');
      hr.className='week-hour-row';
      hr.addEventListener('click',()=>openNewEvent(wd,DU.pad(h,2)+':00'));
      col.appendChild(hr);

      const hr2=document.createElement('div');
      hr2.className='week-hour-row half-hour';
      hr2.addEventListener('click',()=>openNewEvent(wd,DU.pad(h,2)+':30'));
      col.appendChild(hr2);

      const tStart=`${DU.pad(h,2)}:00`;
      const tEnd2=`${DU.pad(h+1,2)}:00`;
      for(const ev of dayO){
        if(ev.allDay)continue;
        if(!ev.startTime||!ev.endTime)continue;
        if(ev.startTime<tStart||ev.startTime>=tEnd2)continue;
        const bk=document.createElement('div');
        bk.className='week-event-block';
        const[sh,sm]=ev.startTime.split(':').map(Number);
        const[eh,em]=ev.endTime.split(':').map(Number);
        const dur=Math.max(CFG.SNAP,(eh*60+em)-(sh*60+sm));
        bk.style.top=`${(sh*60+sm)*ppm}px`;
        bk.style.height=`${dur*ppm}px`;
        bk.style.background=ev.color+'20';bk.style.color=ev.color;bk.style.borderLeftColor=ev.color;
        bk.innerHTML=`<div class="event-block-title">${ev.title||'(No title)'}</div><div class="event-block-time">${DU.fmtTime(sh,sm)}–${DU.fmtTime(eh,em)}</div>`;
        bk.addEventListener('click',e=>{e.stopPropagation();openClickEvent(ev,wd)});
        bk.draggable=true;
        bk.addEventListener('dragstart',e=>{e.stopPropagation();S._dragId=ev.id;e.dataTransfer.effectAllowed='move'});
        col.appendChild(bk);
      }
    }

    // Now line
    if(wd===todayDS){
      const n=new Date();
      const nl=document.createElement('div');nl.className='week-now-line';
      nl.style.top=`${(n.getHours()*60+n.getMinutes())*ppm}px`;
      col.appendChild(nl);
    }

    // Drag-drop for re-time
    col.addEventListener('dragover',e=>e.preventDefault());
    col.addEventListener('drop',e=>{
      e.preventDefault();
      const eid=e.dataTransfer.getData('text/plain');
      const ev=S.events.find(z=>z.id===eid);
      if(!ev||ev.allDay)return;
      const rect=col.getBoundingClientRect();
      const y2=e.clientY-rect.top;
      const totMin=Math.round((y2/ppm)/CFG.SNAP)*CFG.SNAP;
      const nh=Math.floor(totMin/60),nm=totMin%60;
      if(nh>=CFG.HOUR_START&&nh<=CFG.HOUR_END){
        ev.startTime=`${DU.pad(nh,2)}:${DU.pad(nm,2)}`;
        if(ev.endTime){
          const[eh,em]=ev.endTime.split(':').map(Number);
          if(eh*60+em<=nh*60+nm)ev.endTime=`${DU.pad(nh,2)}:${DU.pad(Math.max(nm+CFG.SNAP,15),2)}`;
        }
        saveAll();refresh();renderCurrent();showToast('Time updated');
      }
    });

    cols.appendChild(col);
    wd=DU.addDays(DU.parse(wd).y,DU.parse(wd).m,DU.parse(wd).d,1);
  }

  scroll.appendChild(cols);
}

/* ============================================================
   RENDERING — agenda
   ============================================================ */
function renderAgenda(){
  const c=document.getElementById('agenda-container');
  c.innerHTML='';
  const {s}=visibleRange();
  let curS=null,group=null;
  const todayDS=nowDS();

  for(const o of S.ocs){
    if(o.date<s)continue;
    if(o.date>visibleRange().e)break;
    if(o.date!==curS){
      curS=o.date;
      if(group)c.appendChild(group);
      const pd=DU.parse(curS);
      const label=todayDS===curS?'Today ':''+DU.dayName(DU.dow(pd.y,pd.m,pd.d))+' — '+DU.monthName(pd.m)+' '+pd.d;
      group=document.createElement('div');group.className='agenda-day';
      const hdr=document.createElement('div');hdr.className='agenda-day-header';hdr.textContent=label;group.appendChild(hdr);
      const evs=document.createElement('div');evs.className='agenda-day-events';group.appendChild(evs);
    }
    const ed=document.createElement('div');ed.className='agenda-event';ed.addEventListener('click',()=>openClickEvent(o,curS));
    const bar=document.createElement('div');bar.className='agenda-event-color';bar.style.background=o.color;
    const tm=document.createElement('span');tm.className='agenda-event-time';
    tm.textContent=o.allDay?'All day':(o.startTime&&o.endTime?DU.fmtTime(parseInt(o.startTime.slice(0,2)),parseInt(o.startTime.slice(3)))+
      ' – '+DU.fmtTime(parseInt(o.endTime.slice(0,2)),parseInt(o.endTime.slice(3))):'None');
    const tl=document.createElement('span');tl.className='agenda-event-title';tl.textContent=o.title||'(No title)';
    ed.append(bar,tm,tl);group.querySelector('.agenda-day-events').appendChild(ed);
  }
  if(group)c.appendChild(group);
  if(!c.children.length)c.innerHTML='<div style="text-align:center;padding:60px 16px;color:var(--color-text-secondary)"><h3 style="font-weight:400;margin-bottom:8px">No upcoming events</h3><p>Click any day to create an event.</p></div>';
}

/* ============================================================
   EVENT EDITOR MODAL
   ============================================================ */
function renderColors(){
  const cp=document.getElementById('e-colors');cp.innerHTML='';
  for(const c of CFG.PALETTE){
    const sw=document.createElement('div');sw.className='color-swatch'+(c===S._color?' selected':'');sw.style.background=c;
    sw.addEventListener('click',()=>{S._color=c;renderColors()});
    cp.appendChild(sw);
  }
}

function renderRecOpts(){
  document.querySelectorAll('#e-allday .toggle-btn').forEach(b=>b.classList.toggle('active',b.dataset.val===(S._allday?'true':'false')));
  const tr=document.getElementById('e-time-row');
  tr.style.display=S._allday?'none':'flex';
  document.querySelectorAll('#e-allday .toggle-btn').forEach(b=>b.dataset.val=b.dataset.val==='true'?'true':'false');
  document.querySelector('#e-allday .toggle-btn[data-val="'+(S._allday?'true':'false')+'"]').classList.add('active');
  document.querySelector('#e-allday .toggle-btn[data-val="'+(S._allday?'false':'true')+'"]');
  document.querySelectorAll('#e-allday .toggle-btn').forEach(b=>{
    const want=b.dataset.val==='true'?true:false;
    b.classList.toggle('active',want===S._allday);
  });

  document.querySelectorAll('#editor-rc-opt .toggle-btn, .recurrence-options .toggle-btn').forEach(b=>b.classList.toggle('active',b.dataset.r===S._rec));
  document.getElementById('e-weekday-group').style.display=S._rec==='weekly'?'block':'none';
  document.querySelectorAll('#e-weekdays .weekday-btn').forEach(b=>b.classList.toggle('selected',S._weekdays.includes(parseInt(b.dataset.d))));
}

function openNewEvent(ds,time){
  S._editId=null;S._isModalNew=true;S._editContext=null;
  document.getElementById('editor-title').textContent='New Event';
  document.getElementById('e-delete').style.display='none';
  document.getElementById('e-cancel').style.display='none';
  document.getElementById('e-title').value='';
  document.getElementById('e-date').value=ds||new Date().toISOString().slice(0,10);
  document.getElementById('e-notes').value='';
  document.getElementById('e-start').value=time||'';
  document.getElementById('e-end').value='';

  S._allday=true;S._rec='none';S._color=CFG.PALETTE[0];S._weekdays=[];
  document.querySelectorAll('.error-msg').forEach(e=>{e.textContent='';e.classList.remove('visible')});
  renderRecOpts();renderColors();
  document.getElementById('editor-overlay').classList.add('visible');
  setTimeout(()=>document.getElementById('e-title').focus(),80);
}

function openEditModal(id){
  const ev=S.events.find(z=>z.id===id);
  if(!ev)return;
  S._editId=id;S._isModalNew=false;S._editContext=null;
  document.getElementById('editor-title').textContent='Edit Event';
  document.getElementById('e-title').value=ev.title||'';
  document.getElementById('e-date').value=ev.date;
  document.getElementById('e-start').value=ev.startTime||'';
  document.getElementById('e-end').value=ev.endTime||'';
  document.getElementById('e-notes').value=ev.notes||'';
  document.getElementById('e-delete').style.display='inline-block';
  document.getElementById('e-cancel').style.display='inline-block';
  S._allday=!!ev.allDay;S._color=ev.color||CFG.PALETTE[0];S._rec=ev.recurrence||'none';S._weekdays=ev.weekdays||[];
  document.querySelectorAll('.error-msg').forEach(e=>{e.textContent='';e.classList.remove('visible')});
  renderRecOpts();renderColors();
  document.getElementById('editor-overlay').classList.add('visible');
}

function closeEditor(){
  document.getElementById('editor-overlay').classList.remove('visible');
  document.querySelectorAll('.error-msg').forEach(e=>{e.textContent='';e.classList.remove('visible')});
}

function saveEditor(){
  // Clear errors
  document.querySelectorAll('.error-msg').forEach(e=>{e.textContent='';e.classList.remove('visible')});
  let ok=true;
  const title=document.getElementById('e-title').value.trim();
  if(!title){document.getElementById('e-title-err').textContent='Title required';document.getElementById('e-title-err').classList.add('visible');ok=false}
  if(!S._allday){
    const st=document.getElementById('e-start').value,ent=document.getElementById('e-end').value;
    if(!st){document.getElementById('e-start-err').textContent='Required';document.getElementById('e-start-err').classList.add('visible');ok=false}
    if(!ent){document.getElementById('e-end-err').textContent='Required';document.getElementById('e-end-err').classList.add('visible');ok=false}
    if(st&&ent&&st>=ent){document.getElementById('e-end-err').textContent='Must be after start';document.getElementById('e-end-err').classList.add('visible');ok=false}
  }
  if(!ok)return;

  const data={
    title,date:document.getElementById('e-date').value,
    allDay:S._allday,
    startTime:S._allday?'':document.getElementById('e-start').value,
    endTime:S._allday?'':document.getElementById('e-end').value,
    notes:document.getElementById('e-notes').value.trim(),
    color:S._color,recurrence:S._rec,weekdays:S._rec==='weekly'?[...S._weekdays]:undefined
  };

  if(S._isModalNew){
    data.id=gid();
    S.events.push(data);
    showToast('Event created');
  } else {
    // Determine edit context
    if(S._editContext==='exception'){
      // Create exception on the current date, edit master
      const masterIdx=S.events.findIndex(z=>z.id===S._editId);
      if(masterIdx===-1)return;
      S.events[masterIdx]={...S.events[masterIdx],...data};
      const origDate=DU.parse(S.events[masterIdx].origDate||S.events[masterIdx].date);
      showToast('Series updated');
    } else if(S._editContext==='split'){
      // Clone and keep master for now, just clone the event
      const idx=S.events.findIndex(z=>z.id===S._editId);
      if(idx===-1)return;
      data.id=gid();
      // Remove old instance, put new one
      S.events[idx]={...S.events[idx],...data};
      showToast('Series split and updated');
    } else {
      const idx=S.events.findIndex(z=>z.id===S._editId);
      if(idx===-1)return;
      if(S.events[idx].parentEvent){
        // Editing a master that has instances
        // Save new data on master
        data.id=S.events[idx].id;
        data.parentEvent=S.events[idx].parentEvent;
      }
      S.events[idx]={...S.events[idx],...data};
      showToast('Event updated');
    }
  }

  saveAll();closeEditor();refresh();renderCurrent();
}

function deleteEditor(){
  const ev=S.events.find(z=>z.id===S._editId);
  if(!ev)return;
  if(ev.parentEvent){
    // Exception — just delete
    S.events=S.events.filter(z=>z.id!==S._editId);
    saveAll();closeEditor();refresh();renderCurrent();showToast('Deleted');
  } else if(ev.recurrence&&ev.recurrence!=='none'){
    // Delete entire series — ask for confirmation
    if(confirm('Delete this entire recurring series?')){
      S.events=S.events.filter(z=>z.id!==S._editId);
      saveAll();closeEditor();refresh();renderCurrent();showToast('Series deleted');
    }
  } else {
    S.events=S.events.filter(z=>z.id!==S._editId);
    saveAll();closeEditor();refresh();renderCurrent();showToast('Deleted');
  }
}

/* ============================================================
   RECURRING EDIT MODAL
   ============================================================ */
document.querySelectorAll('.recurring-option').forEach(o=>{
  o.addEventListener('click',()=>{
    S._recurChoice=o.dataset.c;
    document.querySelectorAll('.recurring-option').forEach(x=>x.classList.toggle('selected',x===o));
  });
});

document.getElementById('recur-ok').addEventListener('click',()=>{
  document.getElementById('recur-overlay').classList.remove('visible');
  const master=S._recurMaster;
  if(!master)return;
  const choice=S._recurChoice;
  const ds=DU.parse(master.date);

  if(choice==='this'){
    // Create an exception instance
    const excId=gid();
    const exc={
      ...master,id:excId,parentEvent:master.id,
      date:master.date,
      recurrence:'exception',_exc:true
    };
    S.events.push(exc);
    saveAll();refresh();
    openEditModal(excId); // Open editor for this specific instance
    S._editContext='exception-edit';
  } else if(choice==='future'){
    // Terminate master at prev date, clone for future
    // Delete original, create two: original (terminated) and clone (future starts from master date)
    const idx=S.events.findIndex(z=>z.id===master.id);
    if(idx===-1)return;
    const orig={...S.events[idx]};
    S.events.splice(idx,1);

    // Original: change its date to day before the selected date
    const prev=DU.addDays(ds.y,ds.m,ds.d,-1);
    orig.date=DU.fmt(prev.y,prev.m,prev.d);

    // Clone: starts from selected date, has original recurrence
    const clone={
      ...orig,id:gid(),date:master.date,
      // keep all fields same except id and date
    };
    S.events.push(orig,clone);
    saveAll();refresh();openEditModal(clone.id);
  } else {
    // Entire series
    openEditModal(master.id);
  }
});

document.getElementById('recur-cancel').addEventListener('click',()=>{
  document.getElementById('recur-overlay').classList.remove('visible');
});
document.getElementById('editor-close').addEventListener('click',closeEditor);
document.getElementById('e-cancel').addEventListener('click',closeEditor);
document.getElementById('e-save').addEventListener('click',saveEditor);
document.getElementById('e-delete').addEventListener('click',deleteEditor);

// All-day toggle
document.getElementById('e-allday').addEventListener('click',e=>{
  const b=e.target.closest('.toggle-btn');if(!b)return;
  S._allday=b.dataset.val==='true';
  // re-render all-day toggles
  document.querySelectorAll('#e-allday .toggle-btn').forEach(x=>{
    const want=x.dataset.val==='true';
    x.classList.toggle('active',want===S._allday);
  });
  renderRecOpts();
});

// Recurrence toggle
document.querySelector('.recurrence-options').addEventListener('click',e=>{
  const b=e.target.closest('.toggle-btn');if(!b)return;
  S._rec=b.dataset.r;
  document.querySelectorAll('.recurrence-options .toggle-btn').forEach(x=>x.classList.toggle('active',x.dataset.r===S._rec));
  document.getElementById('e-weekday-group').style.display=S._rec==='weekly'?'block':'none';
});

// Weekday toggle
document.getElementById('e-weekdays').addEventListener('click',e=>{
  const b=e.target.closest('.weekday-btn');if(!b)return;
  const d=parseInt(b.dataset.d);
  S._weekdays=S._weekdays.includes(d)?S._weekdays.filter(x=>x!==d):[...S._weekdays,DU.pad(d).length?d:Math.floor(Math.random()*7)];
  // Actually fix: just add/remove the integer day
  S._weekdays=S._weekdays.filter(x=>typeof x==='number');
  if(S._weekdays.includes(d)&&!b.classList.contains('selected'))S._weekdays=S._weekdays.filter(x=>x!==d);
  else if(!S._weekdays.includes(d))S._weekdays.push(d);
  document.querySelectorAll('#e-weekdays .weekday-btn').forEach(x=>x.classList.toggle('selected',S._weekdays.includes(parseInt(x.dataset.d))));
});

/* ============================================================
   NAVIGATION
   ============================================================ */
function goToToday(){
  const t=new Date();S.view.y=t.getFullYear();S.view.m=t.getMonth();
  refresh();renderCurrent();
}

function nav(dir){
  if(S.view.v==='month'){
    S.view.m+=dir;
    if(S.view.m<0){S.view.m=11;S.view.y--}if(S.view.m>11){S.view.m=0;S.view.y++}
  } else if(S.view.v==='week'){
    const ws=DU.weekStart(S.view.y,S.view.m,1,S.settings.ws);
    const nw=DU.addDays(ws.y,ws.m,ws.d,7*dir);
    S.view.y=nw.y;S.view.m=nw.m;
  } else {
    // Agenda: show next/prev month's events
    S.view.m+=dir;
    if(S.view.m<0){S.view.m=11;S.view.y--}if(S.view.m>11){S.view.m=0;S.view.y++}
  }
  renderCurrent();
}

function switchView(v){
  S.view.v=v;
  document.querySelectorAll('.view-btn').forEach(b=>b.classList.toggle('active',b.dataset.view===v));
  document.querySelectorAll('.view-container').forEach(c=>c.style.display=c.id==='view-'+v?'':'none');
  refresh();renderCurrent();
}

function headerTitle(){
  const {v,y,m}=S.view;
  if(v==='month')return DU.monthName(m)+' '+y;
  if(v==='week'){
    const ws=DU.weekStart(y,m,1,S.settings.ws);
    const we=DU.weekEnd(y,m,1,S.settings.ws);
    if(ws.m===we.m)return DU.monthName(ws.m)+' '+ws.d+'-'+we.d+', '+ws.y;
    return DU.shortMonth(ws.m)+' '+ws.d+' – '+DU.shortMonth(we.m)+' '+we.d+', '+we.y;
  }
  return'Event Planner';
}

function renderCurrent(){
  document.getElementById('header-title').textContent=headerTitle();
  renderMonthHeader();
  if(S.view.v==='month')renderMonthGrid();
  else if(S.view.v==='week')renderWeek();
  else renderAgenda();
  saveAll();
}

/* ============================================================
   SETTINGS
   ============================================================ */
document.getElementById('settings-btn').addEventListener('click',()=>{
  document.querySelectorAll('#s-weekstart .toggle-btn').forEach(b=>b.classList.toggle('active',parseInt(b.dataset.ws)===S.settings.ws));
  document.getElementById('settings-overlay').classList.add('visible');
});
document.getElementById('s-save').addEventListener('click',()=>{
  const act=document.querySelector('#s-weekstart .toggle-btn.active');
  S.settings.ws=act?parseInt(act.dataset.ws):1;
  document.getElementById('settings-overlay').classList.remove('visible');
  saveAll();refresh();renderCurrent();
});
document.getElementById('s-weekstart').addEventListener('click',e=>{
  const b=e.target.closest('.toggle-btn');if(!b)return;
  document.querySelectorAll('#s-weekstart .toggle-btn').forEach(x=>x.classList.remove('active'));b.classList.add('active');
});

/* ============================================================
   TOAST
   ============================================================ */
let _toastTo=null;
function showToast(msg,action,acFn){
  const c=document.getElementById('toasts');
  const t=document.createElement('div');t.className='toast';
  t.innerHTML=`<span>${msg}</span>`;
  if(action){
    const ab=document.createElement('button');ab.className='toast-action';ab.textContent=action;
    if(acFn)ab.addEventListener('click',acFn);
    t.appendChild(ab);
  }
  c.appendChild(t);
  clearTimeout(_toastTo);
  _toastTo=setTimeout(()=>{if(t.parentNode)t.remove()},4000);
}

/* ============================================================
   INIT
   ============================================================ */
load();
refresh();
renderMonthHeader();
renderCurrent();

// Event listeners
document.getElementById('today-btn').addEventListener('click',goToToday);
document.getElementById('prev-btn').addEventListener('click',()=>nav(-1));
document.getElementById('next-btn').addEventListener('click',()=>nav(1));
document.querySelectorAll('.view-btn').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)));

// Close overlays
document.getElementById('editor-overlay').addEventListener('click',e=>{if(e.target.id==='editor-overlay')closeEditor()});
document.getElementById('recur-overlay').addEventListener('click',e=>{if(e.target.id==='recur-overlay')document.getElementById('recur-overlay').classList.remove('visible')});
document.getElementById('settings-overlay').addEventListener('click',e=>{if(e.target.id==='settings-overlay')document.getElementById('settings-overlay').classList.remove('visible')});

// Keyboard
document.addEventListener('keydown',e=>{
  if(e.key==='Escape'){closeEditor();document.getElementById('recur-overlay').classList.remove('visible');document.getElementById('settings-overlay').classList.remove('visible');hideDayPicker()}
  if(e.key==='Enter'&&document.getElementById('e-title').value&&document.getElementById('editor-overlay').classList.contains('visible')){e.preventDefault();saveEditor()}
});

// Drag end
document.addEventListener('dragend',()=>{
  const g=document.getElementById('drag-ghost');g.style.display='none';
  document.querySelectorAll('.month-cell').forEach(c=>c.style.background='');
});

// Click outside month-cell to clear hover
document.addEventListener('click',()=>{
  document.querySelectorAll('.month-cell').forEach(c=>c.style.background='');
});

/* ============================================================
   FIX WEEKDAY BUTTONS — re-attach
   ============================================================ */
document.querySelectorAll('#e-weekdays .weekday-btn').forEach(btn=>{
  btn.addEventListener('click',()=>{
    const d=parseInt(btn.dataset.d);
    if(S._weekdays.includes(d))S._weekdays=S._weekdays.filter(x=>x!==d);
    else S._weekdays.push(d);
    btn.classList.toggle('selected');
  });
});
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.7-flash","provider":"openrouter","persona":"full","sessionId":"ad5e2252-8639-4256-99f9-c74395dfd86a","tokensIn":3797925,"tokensOut":63166,"tokensTotal":3861091,"turns":42,"toolCalls":41,"failedToolCalls":1,"timestamp":"2026-07-30T00:48:10.699Z"} -->