← Calendar results

Calendar

Deepseek v4.1 Flash · 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 89,854 bytes · SHA-256 3108000efff1
<!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>
/* ============================================================
   THEME / TUNABLES (mirrored in JS CONFIG block)
   ============================================================ */
:root{
  --bg:#f6f7f9;
  --surface:#ffffff;
  --surface-2:#f1f3f4;
  --line:#e3e6ea;
  --line-2:#eef0f3;
  --text:#1f2328;
  --text-2:#5f6673;
  --text-3:#8a919c;
  --accent:#1a73e8;
  --accent-soft:#e8f0fe;
  --today:#1a73e8;
  --danger:#d93025;
  --radius:10px;
  --radius-sm:6px;
  --shadow:0 1px 2px rgba(0,0,0,.08),0 2px 8px rgba(0,0,0,.06);
  --shadow-lg:0 8px 30px rgba(0,0,0,.18);
  --chipH:20px;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  background:var(--bg);
  color:var(--text);
  font-size:14px;
  -webkit-font-smoothing:antialiased;
  overflow:hidden;
}
button{font-family:inherit;font-size:inherit;color:inherit;cursor:pointer;border:none;background:none}
input,select,textarea{font-family:inherit;font-size:inherit;color:inherit}

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

/* ============================ TOP BAR ============================ */
.topbar{
  display:flex;align-items:center;gap:10px;
  padding:8px 14px;background:var(--surface);
  border-bottom:1px solid var(--line);
  flex:0 0 auto;z-index:40;
}
.brand{display:flex;align-items:center;gap:9px;margin-right:6px;min-width:0}
.logo{
  width:26px;height:26px;border-radius:50%;background:var(--accent);
  display:grid;place-items:center;color:#fff;font-weight:700;font-size:13px;flex:0 0 auto;
}
.brand h1{font-size:16px;font-weight:600;margin:0;white-space:nowrap}
.tbtn{
  display:inline-flex;align-items:center;gap:6px;
  padding:7px 12px;border-radius:var(--radius-sm);border:1px solid var(--line);
  background:var(--surface);white-space:nowrap;transition:background .12s,border-color .12s;
}
.tbtn:hover{background:var(--surface-2)}
.tbtn.ghost{border-color:transparent}
.tbtn.ghost:hover{background:var(--surface-2)}
.icon-btn{
  width:34px;height:34px;border-radius:50%;display:grid;place-items:center;
  border:1px solid var(--line);background:var(--surface);transition:background .12s;
}
.icon-btn:hover{background:var(--surface-2)}
.date-title{
  font-size:18px;font-weight:600;min-width:190px;text-align:center;
  letter-spacing:-.2px;white-space:nowrap;
}
.spacer{flex:1 1 auto}
.seg{display:flex;border:1px solid var(--line);border-radius:var(--radius-sm);overflow:hidden}
.seg button{padding:7px 13px;background:var(--surface);transition:background .12s}
.seg button+button{border-left:1px solid var(--line)}
.seg button:hover{background:var(--surface-2)}
.seg button.active{background:var(--accent-soft);color:var(--accent);font-weight:600}

/* ============================ VIEW AREA ============================ */
.viewwrap{position:relative;flex:1 1 auto;overflow:hidden}
.view{position:absolute;inset:0;display:flex;flex-direction:column;will-change:transform,opacity}
.view.anim-next{animation:slideNext .26s cubic-bezier(.3,.7,.3,1)}
.view.anim-prev{animation:slidePrev .26s cubic-bezier(.3,.7,.3,1)}
.view.anim-fade{animation:fadeIn .2s ease}
@keyframes slideNext{from{transform:translateX(46px);opacity:0}to{transform:none;opacity:1}}
@keyframes slidePrev{from{transform:translateX(-46px);opacity:0}to{transform:none;opacity:1}}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}

/* ============================ MONTH VIEW ============================ */
.month-head{
  display:grid;grid-template-columns:repeat(7,1fr);
  border-bottom:1px solid var(--line);background:var(--surface);flex:0 0 auto;
}
.month-head div{
  padding:8px 10px;font-size:11px;font-weight:700;color:var(--text-2);
  text-transform:uppercase;letter-spacing:.6px;text-align:left;
}
.month-head div.we{color:var(--text-3)}
.month-grid{flex:1 1 auto;display:grid;grid-template-rows:repeat(6,1fr);min-height:0}
.week-row{display:grid;grid-template-columns:repeat(7,1fr);min-height:0}
.day-cell{
  border-right:1px solid var(--line-2);border-bottom:1px solid var(--line-2);
  padding:5px 5px 4px;min-height:0;overflow:hidden;position:relative;
  display:flex;flex-direction:column;gap:2px;background:var(--surface);
  transition:background .12s;
}
.week-row .day-cell:last-child{border-right:none}
.day-cell.other{background:#fafbfc}
.day-cell.other .daynum{color:var(--text-3)}
.day-cell:hover{background:#fbfcfe}
.day-cell.drop-target{background:var(--accent-soft);box-shadow:inset 0 0 0 2px var(--accent)}
.day-head{display:flex;align-items:center;justify-content:space-between;gap:4px}
.daynum{
  font-size:12px;font-weight:600;color:var(--text-2);
  width:22px;height:22px;display:grid;place-items:center;border-radius:50%;
}
.day-cell.today .daynum{background:var(--today);color:#fff;font-weight:700}
.daynum.weekend{color:var(--text-3)}
.chips{display:flex;flex-direction:column;gap:2px;overflow:hidden}
.chip{
  display:flex;align-items:center;gap:5px;min-height:var(--chipH);
  padding:1px 6px 1px 5px;border-radius:4px;cursor:pointer;
  font-size:11.5px;line-height:1.25;overflow:hidden;position:relative;
  background:var(--chip-bg,#e8f0fe);color:var(--chip-fg,#174ea6);font-weight:500;
  border-left:3px solid var(--chip-bar,#1a73e8);
  transition:filter .12s,box-shadow .12s;
  user-select:none;
}
.chip:hover{filter:brightness(.95);box-shadow:0 1px 3px rgba(0,0,0,.14)}
.chip .ct{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1 1 auto}
.chip .ctime{flex:0 0 auto;opacity:.85;font-variant-numeric:tabular-nums}
.chip .ck{flex:0 0 auto;font-size:9px;opacity:.8}
.chip.dragging{opacity:.4}
.chip.timed{border-left-width:3px}
.chip.allday{background:var(--chip-bar);color:#fff;border-left-color:rgba(0,0,0,.18);font-weight:600}
.chip.allday:hover{filter:brightness(1.08)}
.wev.allday{background:var(--chip-bar);color:#fff;border-left-color:rgba(0,0,0,.18)}
.chip span.dot{width:6px;height:6px;border-radius:50%;flex:0 0 auto}
.more{
  font-size:11px;font-weight:600;color:var(--text-2);padding:1px 5px;border-radius:4px;
  text-align:left;background:transparent;
}
.more:hover{background:var(--surface-2);color:var(--text)}

/* ============================ WEEK VIEW ============================ */
.week-wrap{flex:1 1 auto;display:flex;flex-direction:column;min-height:0}
.allstrip{
  display:grid;grid-template-columns:58px repeat(7,1fr);
  border-bottom:1px solid var(--line);background:var(--surface);flex:0 0 auto;
  min-height:26px;
}
.allstrip .asl{
  font-size:10px;color:var(--text-3);text-align:right;padding:5px 7px 0 0;
  text-transform:uppercase;letter-spacing:.4px;
}
.allcell{
  border-left:1px solid var(--line-2);padding:3px;display:flex;flex-direction:column;gap:2px;min-height:26px;
}
.wk-head{
  display:grid;grid-template-columns:58px repeat(7,1fr);
  border-bottom:1px solid var(--line);background:var(--surface);flex:0 0 auto;
}
.wk-head .gcorner{border-right:1px solid var(--line-2)}
.wk-day{
  padding:6px 4px;text-align:center;border-left:1px solid var(--line-2);cursor:pointer;
}
.wk-day .dow{font-size:10.5px;font-weight:700;color:var(--text-2);text-transform:uppercase;letter-spacing:.5px}
.wk-day .dnum{font-size:20px;font-weight:500;width:34px;height:34px;line-height:34px;margin:2px auto 0;border-radius:50%}
.wk-day.today .dnum{background:var(--today);color:#fff;font-weight:600}
.wk-day.today .dow{color:var(--today)}
.wk-day:hover .dnum{background:var(--surface-2)}
.wk-day.today:hover .dnum{background:var(--today)}
.scroller{flex:1 1 auto;overflow-y:auto;overflow-x:hidden;position:relative;background:var(--surface)}
.wk-body{display:grid;grid-template-columns:58px repeat(7,1fr);position:relative}
.hourcol{position:relative}
.hourlab{
  height:var(--hourH,44px);font-size:10.5px;color:var(--text-3);text-align:right;
  padding-right:7px;position:relative;top:-6px;
}
.wkcol{position:relative;border-left:1px solid var(--line-2)}
.wkcol.we{background:#fbfcfd}
.wkcol .hr{height:var(--hourH,44px);border-bottom:1px solid var(--line-2)}
.wkcol .hr.half{border-bottom:1px dotted #f0f2f5}
.wev{
  position:absolute;left:2px;right:3px;border-radius:5px;padding:2px 6px 2px 5px;
  font-size:11px;line-height:1.2;overflow:hidden;cursor:pointer;
  background:var(--chip-bg,#e8f0fe);color:var(--chip-fg,#174ea6);
  border-left:3px solid var(--chip-bar,#1a73e8);
  box-shadow:0 0 0 1px rgba(255,255,255,.7);
  transition:filter .12s,box-shadow .12s;
}
.wev:hover{filter:brightness(.94);box-shadow:0 2px 6px rgba(0,0,0,.18);z-index:6}
.wev b{font-weight:600;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wev i{font-style:normal;opacity:.8;font-size:10px;white-space:nowrap;overflow:hidden;display:block}
.wev.dragging{opacity:.45}
.wev.resizing{cursor:ns-resize}
.nowline{position:absolute;left:0;right:0;height:0;border-top:2px solid #ea4335;z-index:8;pointer-events:none}
.nowline::before{
  content:"";position:absolute;left:52px;top:-5px;width:9px;height:9px;border-radius:50%;
  background:#ea4335;
}
.nowline .nltime{
  position:absolute;left:2px;top:-8px;font-size:9.5px;color:#ea4335;background:var(--surface);
  padding:0 2px;border-radius:3px;font-weight:600;
}
.resizeh{position:absolute;left:0;right:0;bottom:0;height:8px;cursor:ns-resize;z-index:3}

/* ============================ AGENDA VIEW ============================ */
.agenda{flex:1 1 auto;overflow-y:auto;padding:14px 18px 60px}
.ag-empty{color:var(--text-3);text-align:center;padding:60px 20px;font-size:14px}
.ag-group{display:grid;grid-template-columns:132px 1fr;gap:14px;margin-bottom:6px}
.ag-date{
  text-align:right;padding-top:10px;position:sticky;top:0;background:var(--bg);
}
.ag-date .ad-dow{font-size:11px;font-weight:700;color:var(--text-2);text-transform:uppercase;letter-spacing:.5px}
.ag-date .ad-num{font-size:22px;font-weight:500;color:var(--text-2);line-height:1.1}
.ag-date.today .ad-num,.ag-date.today .ad-dow{color:var(--today)}
.ag-date .ad-mo{font-size:11px;color:var(--text-3)}
.ag-items{border-top:1px solid var(--line);padding:4px 0 10px}
.ag-row{
  display:flex;align-items:center;gap:10px;padding:7px 8px;border-radius:var(--radius-sm);
  cursor:pointer;transition:background .12s;
}
.ag-row:hover{background:var(--surface)}
.ag-row .bar{width:4px;align-self:stretch;min-height:26px;border-radius:2px;background:var(--chip-bar,#1a73e8)}
.ag-row .agt{font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ag-row .agtime{color:var(--text-2);font-size:12.5px;font-variant-numeric:tabular-nums;min-width:118px}
.ag-row .agnote{color:var(--text-3);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ag-past{opacity:.55}

/* ============================ MODAL ============================ */
.scrim{
  position:fixed;inset:0;background:rgba(28,32,36,.45);z-index:100;
  display:flex;align-items:center;justify-content:center;padding:16px;
  animation:fadeIn .16s ease;
}
.scrim[hidden]{display:none}
.modal{
  background:var(--surface);border-radius:14px;box-shadow:var(--shadow-lg);
  width:100%;max-width:560px;max-height:92vh;overflow:auto;
  animation:popIn .18s cubic-bezier(.2,.9,.3,1.2);
}
@keyframes popIn{from{transform:scale(.96) translateY(8px);opacity:0}to{transform:none;opacity:1}}
.modal h2{margin:0;font-size:17px;font-weight:600;padding:16px 18px 10px}
.mbody{padding:0 18px 6px;display:flex;flex-direction:column;gap:13px}
.field{display:flex;flex-direction:column;gap:5px}
.field label{font-size:11.5px;font-weight:600;color:var(--text-2);text-transform:uppercase;letter-spacing:.4px}
.field input[type=text],.field input[type=date],.field input[type=time],.field select,.field textarea{
  padding:9px 11px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);
  outline:none;transition:border-color .12s,box-shadow .12s;width:100%;
}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.field textarea{resize:vertical;min-height:64px}
.field.err input,.field.err select{border-color:var(--danger);box-shadow:0 0 0 3px #fce8e6}
.errmsg{color:var(--danger);font-size:12px;min-height:0}
.errmsg:empty{display:none}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}
.swatches{display:flex;flex-wrap:wrap;gap:8px}
.sw{
  width:30px;height:30px;border-radius:50%;cursor:pointer;position:relative;
  border:2px solid transparent;transition:transform .12s;
}
.sw:hover{transform:scale(1.1)}
.sw.sel{box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--text-2)}
.sw.sel::after{content:"✓";position:absolute;inset:0;display:grid;place-items:center;color:#fff;font-size:14px;font-weight:700}
.dow-toggle{display:flex;gap:6px;flex-wrap:wrap}
.dow-toggle button{
  width:38px;height:38px;border-radius:50%;border:1px solid var(--line);font-weight:600;font-size:12.5px;
  transition:background .12s,color .12s;
}
.dow-toggle button.on{background:var(--accent);color:#fff;border-color:var(--accent)}
.chk{display:flex;align-items:center;gap:8px;font-size:13.5px;cursor:pointer;text-transform:none;letter-spacing:0;color:var(--text)}
.chk input{width:17px;height:17px;accent-color:var(--accent);cursor:pointer}
.mfoot{
  display:flex;align-items:center;gap:8px;padding:14px 18px 16px;flex-wrap:wrap;
}
.mfoot .del{margin-right:auto;color:var(--danger);font-weight:600}
.mfoot .del:hover{background:#fce8e6}
.primary{
  background:var(--accent);color:#fff;padding:9px 18px;border-radius:var(--radius-sm);font-weight:600;
  transition:filter .12s;
}
.primary:hover{filter:brightness(1.08)}
.hint{font-size:12px;color:var(--text-3);padding:0 18px 14px}
.menu{display:flex;flex-direction:column;gap:6px;padding:8px 18px 18px}
.menu button{
  text-align:left;padding:11px 13px;border-radius:var(--radius-sm);border:1px solid var(--line);
  transition:background .12s;
}
.menu button:hover{background:var(--surface-2)}
.menu button span{display:block;font-size:12px;color:var(--text-2);margin-top:2px}
.menu button.dngr{color:var(--danger)}

/* ============================ POPOVER / TOAST ============================ */
.popover{
  position:fixed;z-index:90;background:var(--surface);border-radius:12px;box-shadow:var(--shadow-lg);
  width:290px;max-height:340px;overflow:auto;padding:6px;border:1px solid var(--line);
  animation:popIn .14s ease;
}
.popover .ph{
  display:flex;justify-content:space-between;align-items:center;padding:8px 8px 6px;font-weight:600;
}
.popover .ph small{font-weight:400;color:var(--text-2)}
.popover .pi{
  display:flex;gap:8px;align-items:flex-start;padding:7px 8px;border-radius:8px;cursor:pointer;
}
.popover .pi:hover{background:var(--surface-2)}
.popover .pi .dot2{width:10px;height:10px;border-radius:50%;margin-top:4px;flex:0 0 auto}
.popover .pi .pt{font-weight:600}
.popover .pi .ps{color:var(--text-2);font-size:12px}
.toastwrap{
  position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:200;
  display:flex;flex-direction:column;gap:8px;align-items:center;pointer-events:none;
}
.toast{
  background:#323639;color:#fff;padding:11px 16px;border-radius:8px;box-shadow:var(--shadow-lg);
  display:flex;align-items:center;gap:14px;font-size:13.5px;pointer-events:auto;
  animation:toastIn .2s ease;
}
@keyframes toastIn{from{transform:translateY(14px);opacity:0}to{transform:none;opacity:1}}
.toast button{color:#8ab4f8;font-weight:600;white-space:nowrap}
.toast button:hover{text-decoration:underline}
.confirm{padding:6px 18px 18px;color:var(--text-2);line-height:1.5}

/* ============================ SETTINGS ============================ */
.settings-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}
.settings-row .sl{font-weight:600}
.settings-row .sd{font-size:12px;color:var(--text-2);margin-top:2px}
.switch{width:44px;height:24px;border-radius:12px;background:#c9ced6;position:relative;transition:background .16s;flex:0 0 auto}
.switch.on{background:var(--accent)}
.switch::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;transition:transform .16s;box-shadow:0 1px 3px rgba(0,0,0,.3)}
.switch.on::after{transform:translateX(20px)}

/* ============================ RESPONSIVE ============================ */
@media (max-width:900px){
  .date-title{min-width:110px;font-size:16px}
  .brand h1{display:none}
  .seg button{padding:7px 9px;font-size:13px}
  .day-cell{padding:3px 3px 2px}
  .chip{font-size:10.5px;padding:1px 4px}
  .agenda{padding:10px 10px 60px}
  .ag-group{grid-template-columns:92px 1fr;gap:8px}
  .ag-row .agtime{min-width:94px;font-size:11.5px}
}
@media (max-width:640px){
  .topbar{flex-wrap:wrap;gap:6px;padding:6px 8px}
  .month-head div{padding:6px 4px;font-size:10px}
  .ag-row{flex-wrap:wrap;gap:4px}
  .chip .ctime{display:none}
  .chip .ck{display:none}
  .chip .dot{display:none}
  .chip{padding:1px 4px 1px 3px}
}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:#c8cdd4;border-radius:6px;border:2px solid var(--surface)}
::-webkit-scrollbar-track{background:transparent}
</style>
</head>
<body>
<div id="app">
  <header class="topbar">
    <div class="brand"><div class="logo">C</div><h1>Calendar &amp; Planner</h1></div>
    <button class="tbtn" id="btnToday">Today</button>
    <button class="icon-btn" id="btnPrev" title="Previous (←)">‹</button>
    <button class="icon-btn" id="btnNext" title="Next (→)">›</button>
    <div class="date-title" id="dateTitle">—</div>
    <div class="spacer"></div>
    <div class="seg" id="viewSeg">
      <button data-view="month">Month</button>
      <button data-view="week">Week</button>
      <button data-view="agenda">Agenda</button>
    </div>
    <button class="tbtn" id="btnNew">+ Create</button>
    <button class="icon-btn" id="btnSettings" title="Settings">⚙</button>
  </header>
  <div class="viewwrap" id="viewwrap"></div>
</div>
<div class="toastwrap" id="toastwrap"></div>
<div class="scrim" id="modalScrim" hidden><div class="modal" id="modalBox"></div></div>
<script>
/* ============================================================================
   SECTION 1 — TUNABLE CONSTANTS
   ============================================================================ */
const CONFIG = {
  HOUR_START: 6,              // week view first hour shown
  HOUR_END: 22,               // week view last hour shown
  HOUR_HEIGHT: 44,            // px per hour in week view
  SNAP_MINUTES: 15,           // drag snap in week view
  WEEK_START_DEFAULT: 1,      // 0 = Sunday, 1 = Monday
  MAX_CHIPS: 3,               // chips shown before "+N more"
  AGENDA_DAYS: 120,           // how far ahead agenda looks
  MONTH_ROW_EST: 6,           // month grid rows
  COLORS: [
    { id:'tomato',   bar:'#d93025', bg:'#fce8e6', fg:'#a50e0e', solid:'#d93025' },
    { id:'peacock',  bar:'#1a73e8', bg:'#e8f0fe', fg:'#174ea6', solid:'#1a73e8' },
    { id:'sage',     bar:'#188038', bg:'#e6f4ea', fg:'#0d652d', solid:'#188038' },
    { id:'banana',   bar:'#f29900', bg:'#fef7e0', fg:'#b06000', solid:'#f29900' },
    { id:'lavender', bar:'#8430ce', bg:'#f3e8fd', fg:'#5b1d91', solid:'#8430ce' },
    { id:'grape',    bar:'#c2185b', bg:'#fde7ef', fg:'#8e1142', solid:'#c2185b' },
    { id:'graphite', bar:'#3c4043', bg:'#eceff1', fg:'#202124', solid:'#3c4043' },
    { id:'teal',     bar:'#00796b', bg:'#e0f2f1', fg:'#00574d', solid:'#00796b' }
  ]
};
function colorById(id){
  return CONFIG.COLORS.find(c=>c.id===id) || CONFIG.COLORS[1];
}

/* ============================================================================
   SECTION 2 — DATE UTIL MODULE (pure functions, no DOM, no state)
   All arithmetic on Y/M/D components. Native Date used ONLY for conversions.
   ============================================================================ */
const MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const MONTH_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const DAY_NAMES   = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const DAY_SHORT   = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const DAY_MIN     = ['S','M','T','W','T','F','S'];

const DateUtil = (() => {

  /** Is `y` a leap year? Gregorian century rule: /4, not /100 unless /400. */
  function isLeapYear(y){
    return (y % 4 === 0 && y % 100 !== 0) || (y % 400 === 0);
  }
  /** Days in month (1-based month). */
  function daysInMonth(y, m){
    const d = [31, isLeapYear(y) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    return d[m - 1];
  }
  /** Day of week for a Y/M/D. 0=Sun..6=Sat. Uses Zeller-ish via native UTC Date (no TZ drift). */
  function dayOfWeek(y, m, d){
    // Native Date in UTC avoids local-time/DST surprises entirely.
    return new Date(Date.UTC(y, m - 1, d)).getUTCDay();
  }
  /** Normalize an out-of-range {y,m,d} (m is 1-based, may be <1 or >12). */
  function normalize(y, m, d){
    // Month rollover first
    let yy = y + Math.floor((m - 1) / 12);
    let mm = ((m - 1) % 12 + 12) % 12 + 1;
    // Day rollover via month-length walk (component math, no ms tricks)
    while (d > daysInMonth(yy, mm)) { d -= daysInMonth(yy, mm); mm++; if (mm > 12){ mm = 1; yy++; } }
    while (d < 1) { mm--; if (mm < 1){ mm = 12; yy--; } d += daysInMonth(yy, mm); }
    return { y: yy, m: mm, d: d };
  }
  /** Canonical key string YYYY-MM-DD (padded). */
  function key(y, m, d){
    return y + '-' + String(m).padStart(2,'0') + '-' + String(d).padStart(2,'0');
  }
  function keyOf(obj){ return key(obj.y, obj.m, obj.d); }
  function parseKey(s){
    const p = s.split('-').map(Number);
    return { y: p[0], m: p[1], d: p[2] };
  }
  /** Add n days to a Y/M/D (n may be negative). Pure component math. */
  function addDays(y, m, d, n){
    const base = daysInMonth(y, m);
    const total = d + n;
    if (total >= 1 && total <= base) return { y: y, m: m, d: total };
    return normalize(y, m, total);
  }
  function addDaysTo(obj, n){ return addDays(obj.y, obj.m, obj.d, n); }
  /** Add n months, clamping the day to the target month length (Jan31 +1mo = Feb28/29). */
  function addMonths(y, m, d, n, clamp){
    let yy = y + Math.floor((m - 1 + n) / 12);
    let mm = ((m - 1 + n) % 12 + 12) % 12 + 1;
    if (clamp !== false){
      const dim = daysInMonth(yy, mm);
      if (d > dim) d = dim;
    }
    return { y: yy, m: mm, d: d };
  }
  /** Days between two dates (signed, from a to b). */
  function diffDays(a, b){
    const ta = Date.UTC(a.y, a.m - 1, a.d), tb = Date.UTC(b.y, b.m - 1, b.d);
    return Math.round((tb - ta) / 86400000);
  }
  /** Compare two dates: -1, 0, 1 */
  function cmp(a, b){
    if (a.y !== b.y) return a.y < b.y ? -1 : 1;
    if (a.m !== b.m) return a.m < b.m ? -1 : 1;
    if (a.d !== b.d) return a.d < b.d ? -1 : 1;
    return 0;
  }
  function today(){
    const n = new Date();
    return { y: n.getFullYear(), m: n.getMonth() + 1, d: n.getDate() };
  }
  /** ISO-ish week number honoring the given week start.
   *  weekStart=1 (Monday): ISO 8601. weekStart=0 (Sunday): week containing Jan 1 = week 1,
   *  weeks start Sunday. Both computed with component math + a stable "week anchor". */
  function weekNumber(y, m, d, weekStart){
    if (weekStart === 1){ // ISO 8601
      const doy = dayOfYear(y, m, d);
      const dow = (dayOfWeek(y, m, d) + 6) % 7; // Mon=0..Sun=6
      const week = Math.floor((doy - dow + 10) / 7);
      if (week < 1) return weeksInYear(y - 1, weekStart);
      if (week > weeksInYear(y, weekStart)) return 1;
      return week;
    }
    // Sunday-start: formula on the year's first-Sunday anchor
    const jan1dow = dayOfWeek(y, 1, 1);            // 0..6
    const doy = dayOfYear(y, m, d);
    // Week 1 is the week (Sun..Sat) containing Jan 1.
    // Offset of this day back to the start of its week, then count weeks.
    const dowSun = dayOfWeek(y, m, d);             // 0=Sun
    const week = Math.floor((doy - 1 - dowSun + 7) / 7) + 1;
    // If Jan 1 itself is Sunday, week 1 starts Jan 1 -> formula above already right.
    if (jan1dow === 0) return week;
    return week;
  }
  function dayOfYear(y, m, d){
    let n = d;
    for (let i = 1; i < m; i++) n += daysInMonth(y, i);
    return n;
  }
  /** Number of ISO-ish weeks in a year. */
  function weeksInYear(y, weekStart){
    if (weekStart === 1){
      // ISO: 53 weeks if Jan 1 is Thursday, or Dec 31 is Thursday
      const jan1 = dayOfWeek(y, 1, 1);
      const dec31 = dayOfWeek(y, 12, 31);
      return (jan1 === 4 || dec31 === 4) ? 53 : 52;
    }
    // Sunday-start: 53 if Jan 1 is Saturday (leap: Friday or Saturday)
    const jan1 = dayOfWeek(y, 1, 1);
    if (isLeapYear(y)) return (jan1 === 6 || jan1 === 5) ? 53 : 52;
    return jan1 === 6 ? 53 : 52;
  }
  /** First cell of the month grid (the week-start day on/before the 1st). */
  function gridStart(y, m, weekStart){
    const dow = dayOfWeek(y, m, 1);
    const back = ((dow - weekStart) % 7 + 7) % 7;
    return addDays(y, m, 1, -back);
  }
  /** Build a 6x7 grid of {y,m,d,inMonth}. */
  function monthGrid(y, m, weekStart, rows){
    rows = rows || CONFIG.MONTH_ROW_EST;
    const start = gridStart(y, m, weekStart);
    const out = [];
    let cur = start;
    for (let r = 0; r < rows; r++){
      const row = [];
      for (let c = 0; c < 7; c++){
        row.push({ y: cur.y, m: cur.m, d: cur.d, inMonth: (cur.y === y && cur.m === m) });
        cur = addDays(cur.y, cur.m, cur.d, 1);
      }
      out.push(row);
    }
    return out;
  }
  /** The 7 dates of the week containing (y,m,d), honoring weekStart. */
  function weekDates(y, m, d, weekStart){
    const dow = dayOfWeek(y, m, d);
    const back = ((dow - weekStart) % 7 + 7) % 7;
    const start = addDays(y, m, d, -back);
    const out = [];
    let cur = start;
    for (let i = 0; i < 7; i++){ out.push(cur); cur = addDays(cur.y, cur.m, cur.d, 1); }
    return out;
  }
  /** Format helpers */
  function fmtLong(d){ return DAY_NAMES[dayOfWeek(d.y,d.m,d.d)] + ', ' + MONTH_NAMES[d.m-1] + ' ' + d.d + ', ' + d.y; }
  function fmtShort(d){ return DAY_SHORT[dayOfWeek(d.y,d.m,d.d)] + ', ' + MONTH_SHORT[d.m-1] + ' ' + d.d; }
  function fmtTime(hh, mm){
    const ampm = hh < 12 ? 'AM' : 'PM';
    let h = hh % 12; if (h === 0) h = 12;
    return h + ':' + String(mm).padStart(2,'0') + ' ' + ampm;
  }
  function monthsBetween(a, b){ return (b.y - a.y) * 12 + (b.m - a.m); }

  return { isLeapYear, daysInMonth, dayOfWeek, normalize, key, keyOf, parseKey,
    addDays, addDaysTo, addMonths, diffDays, cmp, today, weekNumber, weeksInYear,
    dayOfYear, gridStart, monthGrid, weekDates, fmtLong, fmtShort, fmtTime, monthsBetween };
})();

/* ============================================================================
   SECTION 3 — RECURRENCE ENGINE (pure)
   Expands a series ONLY for the requested window. Never materializes infinity.
   ============================================================================ */

/** Candidate occurrence dates of one event inside [fromKey, toKey] inclusive. */
function recurrenceOccurrences(ev, from, to){
  const out = [];
  const start = DateUtil.parseKey(ev.date);
  if (from && DateUtil.cmp(start, to) > 0) { /* series starts after window */ }
  const hardEnd = ev.recurrence && ev.recurrence.until ? DateUtil.parseKey(ev.recurrence.until) : null;
  const rec = ev.recurrence;

  // Effective last date: series until/end OR window end
  let last = to ? to : start;
  if (hardEnd && DateUtil.cmp(hardEnd, last) < 0) last = hardEnd;
  if (DateUtil.cmp(start, last) > 0) return out;

  if (!rec){
    if (DateUtil.cmp(start, from) >= 0 && DateUtil.cmp(start, to) <= 0) out.push(start);
    return out;
  }

  if (rec.freq === 'daily'){
    const step = Math.max(1, rec.interval || 1);
    // Jump straight into window using day diff math
    let n = Math.max(0, Math.floor(DateUtil.diffDays(start, from) / step) - 1);
    let cur = DateUtil.addDaysTo(start, n * step);
    let guard = 0;
    while (DateUtil.cmp(cur, last) <= 0 && guard++ < 4000){
      if (DateUtil.cmp(cur, from) >= 0) out.push({ ...cur });
      cur = DateUtil.addDaysTo(cur, step);
    }
    return out;
  }

  if (rec.freq === 'weekly'){
    const step = Math.max(1, rec.interval || 1);
    const wds = (rec.weekdays && rec.weekdays.length) ? rec.weekdays.slice().sort() : [DateUtil.dayOfWeek(start.y,start.m,start.d)];
    // Week anchor = start of the week (weekStart=WEEK_START_DEFAULT) containing series start
    const anchorY = start.y, anchorM = start.m, anchorD = start.d;
    const dow0 = DateUtil.dayOfWeek(anchorY, anchorM, anchorD);
    const back = ((dow0 - CONFIG.WEEK_START_DEFAULT) % 7 + 7) % 7;
    const anchor = DateUtil.addDays(anchorY, anchorM, anchorD, -back); // week-start of week 0
    // Start iterating a little before the window
    const w0 = Math.max(0, Math.floor(DateUtil.diffDays(anchor, from) / (7 * step)) - 1);
    for (let w = w0; w < 4000; w++){
      const weekStart = DateUtil.addDaysTo(anchor, w * step * 7);
      if (DateUtil.cmp(weekStart, last) > 0) break;
      let any = false;
      for (const wd of wds){
        const off = ((wd - CONFIG.WEEK_START_DEFAULT) % 7 + 7) % 7;
        const cand = DateUtil.addDaysTo(weekStart, off);
        if (DateUtil.cmp(cand, start) < 0) continue;           // before series began
        if (DateUtil.cmp(cand, last) > 0) continue;
        if (DateUtil.cmp(cand, from) < 0) continue;
        out.push(cand); any = true;
      }
      if (DateUtil.cmp(weekStart, to) > 0 && any) break;
      if (DateUtil.cmp(weekStart, to) > 0) break;
    }
    return out;
  }

  if (rec.freq === 'monthly'){
    const step = Math.max(1, rec.interval || 1);
    const dom = rec.dayOfMonth || start.d;             // the ORIGINAL day-of-month (e.g. 31)
    const startIdx = start.y * 12 + (start.m - 1);
    const fromIdx = from.y * 12 + (from.m - 1);
    let k = Math.max(0, Math.floor((fromIdx - startIdx) / step) - 1);
    for (let guard = 0; guard < 500; guard++, k++){
      const idx = startIdx + k * step;
      const yy = Math.floor(idx / 12), mm = (idx % 12) + 1;
      // SKIP months that do not have `dom` (Feb for the 31st) — the event simply
      // does not exist in those months; it reconvenes in the next month that does.
      if (dom > DateUtil.daysInMonth(yy, mm)) continue;
      const cand = { y: yy, m: mm, d: dom };
      if (DateUtil.cmp(cand, start) < 0) continue;
      if (DateUtil.cmp(cand, last) > 0) break;
      if (DateUtil.cmp(cand, from) >= 0) out.push(cand);
    }
    return out;
  }

  if (rec.freq === 'yearly'){
    const step = Math.max(1, rec.interval || 1);
    const mm = rec.month || start.m, dom = rec.dayOfMonth || start.d;
    for (let yy = start.y, guard = 0; guard < 400; yy += step, guard++){
      // Feb 29 yearly only exists in leap years.
      if (mm === 2 && dom === 29 && !DateUtil.isLeapYear(yy)) continue;
      if (dom > DateUtil.daysInMonth(yy, mm)) continue;
      const cand = { y: yy, m: mm, d: dom };
      if (DateUtil.cmp(cand, start) < 0) continue;
      if (DateUtil.cmp(cand, last) > 0) break;
      if (DateUtil.cmp(cand, from) >= 0) out.push(cand);
    }
    return out;
  }
  return out;
}

/* ============================================================================
   SECTION 4 — STORE (state + persistence)
   ============================================================================ */
const STORE_KEY = 'calendar.planner.v1';

const Store = (() => {
  let state = {
    events: [],
    settings: { weekStart: CONFIG.WEEK_START_DEFAULT, view: 'month', showWeekNums: false },
    cursor: null,               // {y,m,d} anchor date
    version: 1
  };

  function uid(){ return 'e' + Date.now().toString(36) + Math.random().toString(36).slice(2,7); }

  function defaults(){
    state.cursor = DateUtil.today();
  }

  function load(){
    try {
      const raw = localStorage.getItem(STORE_KEY);
      if (raw){
        const p = JSON.parse(raw);
        if (p && Array.isArray(p.events)){
          state.events = p.events.map(normalizeEvent);
          state.settings = Object.assign({ weekStart: CONFIG.WEEK_START_DEFAULT, view:'month', showWeekNums:false }, p.settings || {});
          state.cursor = p.cursor || DateUtil.today();
          return true;
        }
      }
    } catch(e){ console.warn('load failed', e); }
    defaults();
    seedDemo();
    return false;
  }
  /** First run: a small, pleasant starting calendar (relative to today). */
  function seedDemo(){
    const t = DateUtil.today();
    const K = (n) => DateUtil.keyOf(DateUtil.addDays(t.y, t.m, t.d, n));
    const mk = (o) => normalizeEvent(Object.assign({ id: uid(), exceptions:{} }, o));
    // Anchor the "31st of the month" demo on a month that actually has 31 days.
    let my = t.y, mm = t.m;
    for (let i = 0; i < 12 && DateUtil.daysInMonth(my, mm) < 31; i++){
      const nx = DateUtil.addMonths(my, mm, 1, 1, true); my = nx.y; mm = nx.m;
    }
    state.events = [
      mk({ title:'Team standup', color:'peacock', date: K(0), start:570, end:585, notes:'Daily sync — 15 minutes.',
        recurrence:{ freq:'weekly', interval:1, weekdays:[1,2,3,4,5], until:null } }),
      mk({ title:'Design review', color:'lavender', date: K(1), allDay:true, start:0, end:0, notes:'Bring the latest mockups.' }),
      mk({ title:'1:1 with Sam', color:'sage', date: K(2), start:840, end:900 }),
      mk({ title:'Sprint planning', color:'banana', date: K(4), start:600, end:690, notes:'Plan the next sprint.' }),
      mk({ title:'Monthly report', color:'tomato', date: DateUtil.key(my, mm, 31), start:1020, end:1080,
        notes:'Always filed on the 31st — months without a 31st are skipped.',
        recurrence:{ freq:'monthly', interval:1, dayOfMonth:31, until:null } }),
      mk({ title:'Quarterly offsite', color:'teal', date: K(9), allDay:true, start:0, end:0 })
    ];
    state.cursor = t;
    save(true);
  }
  function normalizeEvent(e){
    return {
      id: e.id || uid(),
      title: e.title || '(No title)',
      color: e.color || 'peacock',
      notes: e.notes || '',
      allDay: !!e.allDay,
      date: e.date,
      start: typeof e.start === 'number' ? e.start : 540,
      end: typeof e.end === 'number' ? e.end : 600,
      recurrence: e.recurrence || null,
      exceptions: e.exceptions || {}
    };
  }
  let saveTimer = null;
  function save(immediate){
    if (saveTimer) clearTimeout(saveTimer);
    const doIt = () => {
      try { localStorage.setItem(STORE_KEY, JSON.stringify(state)); }
      catch(err){ console.warn('save failed', err); }
    };
    if (immediate) doIt(); else saveTimer = setTimeout(doIt, 120);
  }

  function getEvents(){ return state.events; }
  function getEvent(id){ return state.events.find(e => e.id === id); }
  function getSettings(){ return state.settings; }
  function setSetting(k, v){ state.settings[k] = v; save(); }
  function getCursor(){ return state.cursor; }
  function setCursor(y, m, d){ state.cursor = { y, m, d }; save(); }

  function addEvent(data){
    const ev = normalizeEvent(Object.assign({ id: uid() }, data));
    state.events.push(ev); save(true);
    return ev;
  }
  function updateEvent(id, patch){
    const ev = getEvent(id); if (!ev) return null;
    Object.assign(ev, patch); save(true);
    return ev;
  }
  function removeEvent(id){
    const i = state.events.findIndex(e => e.id === id);
    if (i >= 0){ const [ev] = state.events.splice(i, 1); save(true); return ev; }
    return null;
  }
  /** Re-insert a removed event (for undo). */
  function restoreEvent(ev){
    if (getEvent(ev.id)) return;
    state.events.push(ev); save(true);
  }

  /** Overrides / cancels on a single occurrence. */
  function setException(id, occKey, ex){
    const ev = getEvent(id); if (!ev) return;
    ev.exceptions = ev.exceptions || {};
    if (ex === null) delete ev.exceptions[occKey];
    else ev.exceptions[occKey] = ex;
    save(true);
  }

  return { load, save, getEvents, getEvent, getSettings, setSetting, getCursor, setCursor,
    addEvent, updateEvent, removeEvent, restoreEvent, setException, uid,
    _raw: () => state };
})();

/* ============================================================================
   SECTION 5 — OCCURRENCE FEED
   Expands all series over a window and merges exceptions.
   ============================================================================ */
function occurrencesInRange(fromD, toD){
  const fromKey = DateUtil.keyOf(fromD), toKey = DateUtil.keyOf(toD);
  const res = [];
  for (const ev of Store.getEvents()){
    const dates = recurrenceOccurrences(ev, fromD, toD);
    const seen = new Set();
    for (const d of dates){
      const k = DateUtil.keyOf(d);
      if (seen.has(k)) continue; seen.add(k);
      const ex = ev.exceptions && ev.exceptions[k];
      if (ex && ex.type === 'cancel') continue;
      const occ = makeOccurrence(ev, d, k, ex && ex.type === 'override' ? ex.patch : null);
      res.push(occ);
    }
    // Overrides that were MOVED into this window from outside it
    if (ev.exceptions){
      for (const [ok, ex] of Object.entries(ev.exceptions)){
        if (!ex || ex.type !== 'override' || !ex.patch || !ex.patch.date) continue;
        if (seen.has(ok)) continue;                 // already produced above
        const pk = ex.patch.date;
        if (pk < fromKey || pk > toKey) continue;
        // Only if the original occurrence is NOT itself in-range (avoid dupes)
        const pd = DateUtil.parseKey(pk);
        res.push(makeOccurrence(ev, pd, ok, ex.patch, true));
      }
    }
  }
  res.sort(occCompare);
  return res;
}
function makeOccurrence(ev, occDate, occKey, patch, movedIn){
  const p = patch || {};
  const dateObj = p.date ? DateUtil.parseKey(p.date) : occDate;
  const allDay = p.allDay !== undefined ? p.allDay : ev.allDay;
  return {
    evId: ev.id,
    occKey,
    date: dateObj,
    fromDate: occDate,
    allDay,
    start: p.start !== undefined ? p.start : ev.start,
    end: p.end !== undefined ? p.end : ev.end,
    title: p.title !== undefined ? p.title : ev.title,
    color: p.color !== undefined ? p.color : ev.color,
    notes: p.notes !== undefined ? p.notes : ev.notes,
    isException: !!patch,
    movedIn: !!movedIn,
    recurring: !!ev.recurrence
  };
}
function occCompare(a, b){
  const c = DateUtil.cmp(a.date, b.date); if (c) return c;
  // all-day first, then by start time, then longer first
  if (a.allDay !== b.allDay) return a.allDay ? -1 : 1;
  if (a.start !== b.start) return a.start - b.start;
  return (b.end - b.start) - (a.end - a.start);
}
/** Occurrences of ONE event across a wide net (for agenda + lookups by instance). */

/* ============================================================================
   SECTION 6 — VIEW STATE + NAVIGATION
   ============================================================================ */
const ViewState = {
  view: 'month',            // 'month' | 'week' | 'agenda'
  animDir: 'fade',
  scrollTop: null
};
function settings(){ return Store.getSettings(); }
function weekStart(){ return settings().weekStart | 0; }

function navStep(delta){
  const c = Store.getCursor();
  if (ViewState.view === 'month'){
    // Walk month-by-month on Y/M/D components; keep the day-of-month where the
    // target month allows it (clamped to the month length otherwise).
    const d = DateUtil.addMonths(c.y, c.m, c.d, delta, true);
    Store.setCursor(d.y, d.m, d.d);
  } else {
    // Week + agenda advance by whole weeks (component math, DST-safe).
    const d = DateUtil.addDays(c.y, c.m, c.d, delta * 7);
    Store.setCursor(d.y, d.m, d.d);
  }
  ViewState.animDir = delta > 0 ? 'next' : 'prev';
  render();
}
function goToday(){
  const t = DateUtil.today();
  Store.setCursor(t.y, t.m, t.d);
  ViewState.animDir = 'fade';
  render();
}
function setView(v){
  ViewState.view = v;
  Store.setSetting('view', v);
  ViewState.animDir = 'fade';
  ViewState.scrollTop = null;
  render();
}

/* ============================================================================
   SECTION 7 — RENDERERS
   ============================================================================ */
const viewwrap = document.getElementById('viewwrap');
const dateTitle = document.getElementById('dateTitle');

function render(){
  const c = Store.getCursor();
  // Update segmented control
  document.querySelectorAll('#viewSeg button').forEach(b => {
    b.classList.toggle('active', b.dataset.view === ViewState.view);
  });
  // Title
  if (ViewState.view === 'month'){
    dateTitle.textContent = MONTH_NAMES[c.m - 1] + ' ' + c.y;
  } else if (ViewState.view === 'week'){
    const wd = DateUtil.weekDates(c.y, c.m, c.d, weekStart());
    const a = wd[0], b = wd[6];
    dateTitle.textContent = (a.m === b.m)
      ? MONTH_SHORT[a.m - 1] + ' ' + a.d + ' – ' + b.d + ', ' + b.y
      : MONTH_SHORT[a.m - 1] + ' ' + a.d + ' – ' + MONTH_SHORT[b.m - 1] + ' ' + b.d + ', ' + b.y;
  } else {
    dateTitle.textContent = 'Upcoming';
  }

  viewwrap.innerHTML = '';
  const root = document.createElement('div');
  root.className = 'view';
  if (ViewState.view === 'month') renderMonth(root);
  else if (ViewState.view === 'week') renderWeek(root);
  else renderAgenda(root);
  viewwrap.appendChild(root);
  const cls = ViewState.animDir === 'next' ? 'anim-next' : ViewState.animDir === 'prev' ? 'anim-prev' : 'anim-fade';
  root.classList.add(cls);
  ViewState.animDir = 'fade';
}

function bucketByKey(occs){
  const map = new Map();
  for (const o of occs){
    const k = DateUtil.keyOf(o.date);
    if (!map.has(k)) map.set(k, []);
    map.get(k).push(o);
  }
  return map;
}

/* ---------------------------- MONTH ---------------------------- */
function renderMonth(root){
  const c = Store.getCursor();
  const rows = CONFIG.MONTH_ROW_EST;
  const grid = DateUtil.monthGrid(c.y, c.m, weekStart(), rows);
  const first = grid[0][0], last = grid[rows - 1][6];
  const occs = occurrencesInRange(first, last);
  const bucket = bucketByKey(occs);
  const t = DateUtil.today();
  const tKey = DateUtil.key(t.y, t.m, t.d);
  const sw = weekStart();

  const head = document.createElement('div');
  head.className = 'month-head';
  for (let i = 0; i < 7; i++){
    const dow = (sw + i) % 7;
    const el = document.createElement('div');
    el.textContent = DAY_SHORT[dow];
    if (dow === 0 || dow === 6) el.classList.add('we');
    head.appendChild(el);
  }
  root.appendChild(head);

  const g = document.createElement('div');
  g.className = 'month-grid';
  g.id = 'monthGrid';

  for (let r = 0; r < rows; r++){
    const rowEl = document.createElement('div');
    rowEl.className = 'week-row';
    for (let ci = 0; ci < 7; ci++){
      const cellD = grid[r][ci];
      const k = DateUtil.keyOf(cellD);
      const cell = document.createElement('div');
      cell.className = 'day-cell';
      cell.dataset.key = k;
      cell.dataset.date = k;
      if (!cellD.inMonth) cell.classList.add('other');
      if (k === tKey) cell.classList.add('today');
      const dow = (sw + ci) % 7;
      if (dow === 0 || dow === 6) cell.classList.add('weekend');

      const dh = document.createElement('div');
      dh.className = 'day-head';
      const num = document.createElement('div');
      num.className = 'daynum';
      num.textContent = cellD.d;
      dh.appendChild(num);
      if (settings().showWeekNums && ci === 0){
        const wk = document.createElement('div');
        wk.style.cssText = 'font-size:9.5px;color:var(--text-3);font-weight:600;padding-right:2px';
        wk.textContent = 'W' + DateUtil.weekNumber(cellD.y, cellD.m, cellD.d, sw);
        dh.appendChild(wk);
      }
      cell.appendChild(dh);

      const chips = document.createElement('div');
      chips.className = 'chips';
      const list = bucket.get(k) || [];
      const show = list.slice(0, CONFIG.MAX_CHIPS);
      for (const o of show) chips.appendChild(makeChip(o, cellD));
      cell.appendChild(chips);
      if (list.length > CONFIG.MAX_CHIPS){
        const more = document.createElement('button');
        more.className = 'more';
        more.textContent = '+' + (list.length - CONFIG.MAX_CHIPS) + ' more';
        more.dataset.key = k;
        more.addEventListener('click', (e) => { e.stopPropagation(); openDayPopover(k, more); });
        cell.appendChild(more);
      }
      cell.addEventListener('click', (e) => {
        if (e.target.closest('.chip')) return;
        if (e.target.closest('.more')) return;
        openEditor({ date: k });
      });
      rowEl.appendChild(cell);
    }
    g.appendChild(rowEl);
  }
  root.appendChild(g);
}

function chipStyle(el, colorId){
  const col = colorById(colorId);
  el.style.setProperty('--chip-bar', col.bar);
  el.style.setProperty('--chip-bg', col.bg);
  el.style.setProperty('--chip-fg', col.fg);
}
function makeChip(o, cellD){
  const el = document.createElement('div');
  el.className = 'chip' + (o.allDay ? ' allday' : ' timed');
  el.dataset.evId = o.evId;
  el.dataset.occKey = o.occKey;
  chipStyle(el, o.color);
  if (!o.allDay){
    const dot = document.createElement('span');
    dot.className = 'dot';
    dot.style.background = colorById(o.color).solid;
    el.appendChild(dot);
  }
  const t = document.createElement('span');
  t.className = 'ct';
  let prefix = '';
  if (!o.allDay) prefix = timeShort(o.start) + ' ';
  if (prefix){
    const pre = document.createElement('span');
    pre.className = 'ctime';
    pre.textContent = prefix;
    el.appendChild(pre);
  }
  t.textContent = o.title;
  el.appendChild(t);
  if (o.recurring){
    const kk = document.createElement('span');
    kk.className = 'ck'; kk.textContent = '↻';
    el.appendChild(kk);
  }
  el.title = (o.allDay ? 'All day' : DateUtil.fmtTime(Math.floor(o.start/60), o.start%60) + ' – ' + DateUtil.fmtTime(Math.floor(o.end/60), o.end%60)) + ' · ' + o.title;
  el.addEventListener('click', (e) => {
    e.stopPropagation();
    openOccurrence(o);
  });
  el._occ = o;
  return el;
}
function timeShort(mins){
  const hh = Math.floor(mins / 60), mm = mins % 60;
  return hh + (mm ? ':' + String(mm).padStart(2, '0') : '');
}

/* ---------------------------- WEEK ---------------------------- */
function renderWeek(root){
  const c = Store.getCursor();
  const sw = weekStart();
  const days = DateUtil.weekDates(c.y, c.m, c.d, sw);
  const t = DateUtil.today();
  const tKey = DateUtil.key(t.y, t.m, t.d);
  const H0 = CONFIG.HOUR_START, H1 = CONFIG.HOUR_END, HH = CONFIG.HOUR_HEIGHT;
  const totalH = (H1 - H0) * HH;

  // occurrences for the 7 days (plus padding for multi-day? keep single-day)
  const occs = occurrencesInRange(days[0], days[6]);
  const byDay = new Map();
  for (const d of days) byDay.set(DateUtil.keyOf(d), []);
  for (const o of occs){
    const k = DateUtil.keyOf(o.date);
    if (byDay.has(k)) byDay.get(k).push(o);
  }

  const wrap = document.createElement('div');
  wrap.className = 'week-wrap';

  // ---- all-day strip ----
  const strip = document.createElement('div');
  strip.className = 'allstrip';
  const label = document.createElement('div');
  label.className = 'asl'; label.textContent = 'all-day';
  strip.appendChild(label);
  for (const d of days){
    const k = DateUtil.keyOf(d);
    const cell = document.createElement('div');
    cell.className = 'allcell';
    cell.dataset.key = k;
    const list = (byDay.get(k) || []).filter(o => o.allDay);
    for (const o of list){
      const chip = document.createElement('div');
      chip.className = 'chip allday';
      chip.dataset.evId = o.evId; chip.dataset.occKey = o.occKey;
      chipStyle(chip, o.color);
      const sp = document.createElement('span'); sp.className = 'ct'; sp.textContent = o.title;
      chip.appendChild(sp); chip._occ = o;
      chip.addEventListener('click', (e) => { e.stopPropagation(); openOccurrence(o); });
      cell.appendChild(chip);
    }
    cell.addEventListener('click', (e) => { if (!e.target.closest('.chip')) openEditor({ date: k, allDay: true }); });
    strip.appendChild(cell);
  }
  wrap.appendChild(strip);

  // ---- day header ----
  const head = document.createElement('div');
  head.className = 'wk-head';
  const corner = document.createElement('div');
  corner.className = 'gcorner';
  head.appendChild(corner);
  days.forEach((d, i) => {
    const k = DateUtil.keyOf(d);
    const el = document.createElement('div');
    el.className = 'wk-day';
    el.dataset.key = k;
    if (k === tKey) el.classList.add('today');
    const dow = new Date(Date.UTC(d.y, d.m - 1, d.d)).getUTCDay();
    el.innerHTML = '<div class="dow">' + DAY_SHORT[dow] + '</div><div class="dnum">' + d.d + '</div>';
    el.addEventListener('click', () => { Store.setCursor(d.y, d.m, d.d); Store.setSetting('view','month'); ViewState.view='month'; render(); });
    head.appendChild(el);
  });
  wrap.appendChild(head);

  // ---- scrolling body ----
  const scroller = document.createElement('div');
  scroller.className = 'scroller';
  scroller.id = 'wkScroller';
  const body = document.createElement('div');
  body.className = 'wk-body';
  body.style.height = totalH + 'px';
  const gutter = document.createElement('div');
  gutter.className = 'hourcol';
  gutter.style.height = totalH + 'px';
  for (let h = H0; h < H1; h++){
    const lab = document.createElement('div');
    lab.className = 'hourlab';
    lab.style.height = HH + 'px';
    lab.textContent = DateUtil.fmtTime(h, 0);
    gutter.appendChild(lab);
  }
  body.appendChild(gutter);

  const cols = [];
  days.forEach((d, ci) => {
    const k = DateUtil.keyOf(d);
    const col = document.createElement('div');
    col.className = 'wkcol';
    col.dataset.key = k;
    col.dataset.colIndex = ci;
    col.style.height = totalH + 'px';
    const dow = new Date(Date.UTC(d.y, d.m - 1, d.d)).getUTCDay();
    if (dow === 0 || dow === 6) col.classList.add('we');
    for (let h = H0; h < H1; h++){
      const hr = document.createElement('div');
      hr.className = 'hr'; hr.style.height = HH + 'px';
      col.appendChild(hr);
    }
    // timed events (exclude all-day)
    const timed = (byDay.get(k) || []).filter(o => !o.allDay);
    const laid = layoutColumns(timed);
    for (const item of laid){
      col.appendChild(makeWeekEvent(item, HH, H0));
    }
    // double-click empty area to create at that time
    col.addEventListener('click', (e) => {
      if (e.target.closest('.wev')) return;
      const rect = col.getBoundingClientRect();
      const y = e.clientY - rect.top;
      const mins = H0 * 60 + Math.round((y / HH) * 60 / CONFIG.SNAP_MINUTES) * CONFIG.SNAP_MINUTES;
      const clamped = Math.max(0, Math.min(23 * 60 + 59, mins));
      openEditor({ date: k, start: clamped, end: Math.min(1439, clamped + 60), allDay: false });
    });
    col.addEventListener('dblclick', (e) => e.preventDefault());
    cols.push(col);
    body.appendChild(col);
  });
  scroller.appendChild(body);

  // ---- current time line ----
  if (days.some(d => DateUtil.keyOf(d) === tKey)){
    const now = new Date();
    const mins = now.getHours() * 60 + now.getMinutes();
    const line = document.createElement('div');
    line.className = 'nowline';
    line.id = 'nowLine';
    line.style.top = ((mins - H0 * 60) / 60 * HH) + 'px';
    const lt = document.createElement('span');
    lt.className = 'nltime';
    lt.textContent = DateUtil.fmtTime(now.getHours(), now.getMinutes());
    line.appendChild(lt);
    body.appendChild(line);
  }

  wrap.appendChild(scroller);
  root.appendChild(wrap);

  // restore / initial scroll
  const saved = ViewState.scrollTop;
  requestAnimationFrame(() => {
    if (saved != null) scroller.scrollTop = saved;
    else scroller.scrollTop = Math.max(0, (8 - H0) * HH - 8);
  });
  scroller.addEventListener('scroll', () => { ViewState.scrollTop = scroller.scrollTop; });
}

/** Overlap column layout: greedy packing into columns within overlap clusters. */
function layoutColumns(events){
  const sorted = events.slice().sort((a, b) => a.start - b.start || a.end - b.end);
  const items = sorted.map(o => ({ o, col: 0, cols: 1 }));
  let cluster = [];
  let clusterEnd = -1;
  const flush = () => {
    if (!cluster.length) return;
    const n = Math.max(...cluster.map(i => i.col)) + 1;
    cluster.forEach(i => { i.cols = n; });
    cluster = [];
  };
  for (const it of items){
    if (cluster.length && it.o.start >= clusterEnd){ flush(); clusterEnd = -1; }
    if (!cluster.length) clusterEnd = it.o.end;
    clusterEnd = Math.max(clusterEnd, it.o.end);
    // find first free column
    const used = new Set(cluster.filter(c => c.o.end > it.o.start).map(c => c.col));
    let c = 0; while (used.has(c)) c++;
    it.col = c;
    cluster.push(it);
  }
  flush();
  return items;
}

function makeWeekEvent(item, HH, H0){
  const o = item.o;
  const el = document.createElement('div');
  el.className = 'wev';
  el.dataset.evId = o.evId;
  el.dataset.occKey = o.occKey;
  chipStyle(el, o.color);
  const top = (o.start - H0 * 60) / 60 * HH;
  const hgt = Math.max(16, (o.end - o.start) / 60 * HH);
  const w = 100 / item.cols;
  el.style.top = top + 'px';
  el.style.height = (o.end === o.start ? 20 : hgt) + 'px';
  el.style.left = 'calc(' + (item.col * w) + '% + 2px)';
  el.style.right = 'auto';
  el.style.width = 'calc(' + w + '% - 5px)';
  const b = document.createElement('b'); b.textContent = o.title;
  const i = document.createElement('i');
  i.textContent = DateUtil.fmtTime(Math.floor(o.start/60), o.start%60) + '–' + DateUtil.fmtTime(Math.floor(o.end/60), o.end%60);
  el.appendChild(b);
  if (o.end - o.start >= 30) el.appendChild(i);
  const rz = document.createElement('div');
  rz.className = 'resizeh';
  el.appendChild(rz);
  el._occ = o;
  el.addEventListener('click', (e) => { e.stopPropagation(); openOccurrence(o); });
  return el;
}

/* ---------------------------- AGENDA ---------------------------- */
function renderAgenda(root){
  const c = Store.getCursor();
  const t = DateUtil.today();
  const from = DateUtil.cmp(c, t) < 0 ? t : c;
  const to = DateUtil.addDaysTo(from, CONFIG.AGENDA_DAYS);
  const occs = occurrencesInRange(from, to);
  const bucket = bucketByKey(occs);
  const tKey = DateUtil.key(t.y, t.m, t.d);

  const wrap = document.createElement('div');
  wrap.className = 'agenda';
  if (!occs.length){
    const e = document.createElement('div');
    e.className = 'ag-empty';
    e.textContent = 'Nothing scheduled in the next ' + CONFIG.AGENDA_DAYS + ' days.';
    wrap.appendChild(e);
    root.appendChild(wrap);
    return;
  }
  const keys = Array.from(bucket.keys()).sort();
  for (const k of keys){
    const list = bucket.get(k);
    const d = DateUtil.parseKey(k);
    const grp = document.createElement('div');
    grp.className = 'ag-group';
    const dh = document.createElement('div');
    dh.className = 'ag-date' + (k === tKey ? ' today' : '');
    const dow = new Date(Date.UTC(d.y, d.m - 1, d.d)).getUTCDay();
    dh.innerHTML = '<div class="ad-dow">' + (k === tKey ? 'Today' : DAY_SHORT[dow]) + '</div>' +
                   '<div class="ad-num">' + d.d + '</div>' +
                   '<div class="ad-mo">' + MONTH_SHORT[d.m - 1] + (d.m === 1 && d.d === 1 ? ' ' + d.y : '') + '</div>';
    const items = document.createElement('div');
    items.className = 'ag-items';
    for (const o of list){
      const row = document.createElement('div');
      row.className = 'ag-row';
      row.dataset.evId = o.evId; row.dataset.occKey = o.occKey;
      const bar = document.createElement('div');
      bar.className = 'bar'; bar.style.background = colorById(o.color).solid;
      const time = document.createElement('div');
      time.className = 'agtime';
      time.textContent = o.allDay ? 'All day' : DateUtil.fmtTime(Math.floor(o.start/60), o.start%60) + ' – ' + DateUtil.fmtTime(Math.floor(o.end/60), o.end%60);
      const title = document.createElement('div');
      title.className = 'agt';
      title.textContent = o.title + (o.recurring ? '  ↻' : '');
      row.appendChild(bar); row.appendChild(time); row.appendChild(title);
      if (o.notes){
        const n = document.createElement('div'); n.className = 'agnote'; n.textContent = o.notes;
        row.appendChild(n);
      }
      row.addEventListener('click', () => openOccurrence(o));
      items.appendChild(row);
    }
    grp.appendChild(dh); grp.appendChild(items);
    wrap.appendChild(grp);
  }
  root.appendChild(wrap);
}

/* ============================================================================
   SECTION 8 — MODAL FRAMEWORK
   ============================================================================ */
const scrim = document.getElementById('modalScrim');
const modalBox = document.getElementById('modalBox');
let modalOnClose = null;
function showModal(innerHTML, onClose){
  modalBox.innerHTML = innerHTML;
  scrim.hidden = false;
  modalOnClose = onClose || null;
  return modalBox;
}
function closeModal(){
  scrim.hidden = true;
  modalBox.innerHTML = '';
  const cb = modalOnClose; modalOnClose = null;
  if (cb) cb();
}
scrim.addEventListener('click', (e) => { if (e.target === scrim) closeModal(); });
window.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && !scrim.hidden){ closeModal(); e.preventDefault(); return; }
  if (!scrim.hidden) return;
  const tag = (document.activeElement && document.activeElement.tagName) || '';
  if (/INPUT|TEXTAREA|SELECT/.test(tag)) return;
  if (e.key === 'ArrowLeft'){ navStep(-1); e.preventDefault(); }
  else if (e.key === 'ArrowRight'){ navStep(1); e.preventDefault(); }
  else if (e.key === 't' || e.key === 'T'){ goToday(); }
  else if (e.key === 'm' || e.key === 'M'){ setView('month'); }
  else if (e.key === 'w' || e.key === 'W'){ setView('week'); }
  else if (e.key === 'a' || e.key === 'A'){ setView('agenda'); }
  else if (e.key === 'n' || e.key === 'N'){ openEditor({ date: DateUtil.keyOf(Store.getCursor()) }); }
});

/* ============================================================================
   SECTION 9 — TOASTS + CONFIRM
   ============================================================================ */
const toastwrap = document.getElementById('toastwrap');
function toast(msg, actions, ms){
  const el = document.createElement('div');
  el.className = 'toast';
  const s = document.createElement('span'); s.textContent = msg;
  el.appendChild(s);
  (actions || []).forEach(a => {
    const b = document.createElement('button');
    b.textContent = a.label;
    b.addEventListener('click', () => { a.fn(); el.remove(); });
    el.appendChild(b);
  });
  toastwrap.appendChild(el);
  setTimeout(() => { el.style.transition = 'opacity .25s'; el.style.opacity = '0';
    setTimeout(() => el.remove(), 260); }, ms || 6000);
  return el;
}
function confirmDialog(title, msg, onYes, opts){
  const o = opts || {};
  showModal(
    '<h2>' + esc(title) + '</h2>' +
    '<div class="confirm">' + msg + '</div>' +
    '<div class="mfoot">' +
      '<div class="spacer" style="flex:1"></div>' +
      '<button class="tbtn" data-act="cancel">Cancel</button>' +
      '<button class="primary" data-act="yes" style="' + (o.danger ? 'background:var(--danger)' : '') + '">' + (o.yesLabel || 'Delete') + '</button>' +
    '</div>');
  modalBox.querySelector('[data-act=cancel]').onclick = closeModal;
  modalBox.querySelector('[data-act=yes]').onclick = () => { closeModal(); onYes(); };
}
function esc(s){
  return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function pad2(n){ return String(n).padStart(2, '0'); }
function timeInputValue(mins){
  const h = Math.floor(mins / 60), m = mins % 60;
  return pad2(h) + ':' + pad2(m);
}
function parseTimeInput(v){
  const m = /^(\d{1,2}):(\d{2})/.exec(v || '');
  if (!m) return null;
  const h = +m[1], mi = +m[2];
  if (h > 23 || mi > 59) return null;
  return h * 60 + mi;
}

/* ============================================================================
   SECTION 10 — EVENT EDITOR MODAL
   ============================================================================ */
const FREQ_LABEL = { none:'Does not repeat', daily:'Daily', weekly:'Weekly', monthly:'Monthly (same day-of-month)', yearly:'Yearly' };

/**
 * openEditor(opts):
 *   { date, start, end, allDay }            -> create new
 *   { eventId }                             -> edit master series (entire)
 *   { occ }                                 -> edit one occurrence (scope asked on save)
 */
function openEditor(opts){
  const o = opts || {};
  const editing = !!o.eventId || !!o.occ;
  const ev = o.eventId ? Store.getEvent(o.eventId) : (o.occ ? Store.getEvent(o.occ.evId) : null);
  const occ = o.occ || null;

  const value = {
    title: ev ? (occ && occ.isException ? occ.title : ev.title) : '',
    color: ev ? (occ && occ.isException ? occ.color : ev.color) : 'peacock',
    notes: ev ? (occ && occ.isException ? occ.notes : ev.notes) : '',
    allDay: ev ? (occ && occ.isException ? occ.allDay : ev.allDay) : !!o.allDay,
    date: o.date || (occ ? DateUtil.keyOf(occ.date) : (ev ? ev.date : DateUtil.keyOf(Store.getCursor()))),
    start: o.start !== undefined ? o.start : (occ && !occ.isException ? occ.start : (ev ? ev.start : 540)),
    end: o.end !== undefined ? o.end : (occ && !occ.isException ? occ.end : (ev ? ev.end : 600)),
    recurrence: ev ? (ev.recurrence ? ev.recurrence.freq : 'none') : 'none',
    interval: ev && ev.recurrence ? (ev.recurrence.interval || 1) : 1,
    weekdays: ev && ev.recurrence && ev.recurrence.weekdays ? ev.recurrence.weekdays.slice() : [],
    until: ev && ev.recurrence && ev.recurrence.until ? ev.recurrence.until : ''
  };
  if (value.recurrence === 'weekly' && !value.weekdays.length && value.date){
    value.weekdays = [DateUtil.dayOfWeek(DateUtil.parseKey(value.date).y, DateUtil.parseKey(value.date).m, DateUtil.parseKey(value.date).d)];
  }
  if (value.end <= value.start) value.end = Math.min(1439, value.start + 60);

  const title = occ && occ.recurring ? 'Edit occurrence' : (editing ? 'Edit event' : 'New event');

  showModal(
    '<h2>' + title + '</h2>' +
    '<div class="mbody">' +
      '<div class="field" id="fTitle"><label>Title</label><input type="text" id="inTitle" placeholder="Add a title" value="' + esc(value.title) + '"><div class="errmsg" id="eTitle"></div></div>' +
      '<div class="field"><label>When</label>' +
        '<label class="chk"><input type="checkbox" id="inAllDay"' + (value.allDay ? ' checked' : '') + '> All day</label>' +
      '</div>' +
      '<div class="row2">' +
        '<div class="field" id="fDate"><label>Date</label><input type="date" id="inDate" value="' + esc(value.date) + '"><div class="errmsg" id="eDate"></div></div>' +
        '<div class="field" id="fUntil"><label>Repeat until (optional)</label><input type="date" id="inUntil" value="' + esc(value.until) + '"></div>' +
      '</div>' +
      '<div class="row2" id="timeRow">' +
        '<div class="field" id="fStart"><label>Start time</label><input type="time" id="inStart" value="' + timeInputValue(value.start) + '" step="900"><div class="errmsg" id="eStart"></div></div>' +
        '<div class="field" id="fEnd"><label>End time</label><input type="time" id="inEnd" value="' + timeInputValue(value.end) + '" step="900"><div class="errmsg" id="eTime"></div></div>' +
      '</div>' +
      '<div class="field" id="fRec"><label>Repeat</label>' +
        '<select id="inFreq">' + Object.keys(FREQ_LABEL).map(k => '<option value="' + k + '"' + (k === value.recurrence ? ' selected' : '') + '>' + FREQ_LABEL[k] + '</option>').join('') + '</select>' +
        '<div class="row2" id="repRow" style="margin-top:9px;align-items:start">' +
          '<div class="field" id="ivWrap"><label>Every</label>' +
            '<select id="inInterval">' + [1,2,3,4,6].map(n => '<option value="' + n + '"' + (n === value.interval ? ' selected' : '') + '>every ' + n + '</option>').join('') + '</select>' +
          '</div>' +
          '<div class="field" id="wdWrap"><label>On days</label>' +
            '<div class="dow-toggle" id="dowToggle">' + DAY_SHORT.map((d, i) =>
              '<button type="button" data-dow="' + i + '" class="' + (value.weekdays.includes(i) ? 'on' : '') + '">' + d.slice(0,3) + '</button>').join('') +
            '</div>' +
          '</div>' +
        '</div>' +
      '</div>' +
      '<div class="field"><label>Color</label><div class="swatches" id="swatches">' +
        CONFIG.COLORS.map(c => '<button type="button" class="sw' + (c.id === value.color ? ' sel' : '') + '" data-color="' + c.id + '" style="background:' + c.solid + '" title="' + c.id + '"></button>').join('') +
      '</div></div>' +
      '<div class="field"><label>Notes</label><textarea id="inNotes" placeholder="Add notes...">' + esc(value.notes) + '</textarea></div>' +
    '</div>' +
    '<div class="mfoot">' +
      (editing ? '<button class="tbtn del" id="btnDelete">Delete</button>' : '') +
      '<div style="flex:1"></div>' +
      '<button class="tbtn" id="btnCancel">Cancel</button>' +
      '<button class="primary" id="btnSave">Save</button>' +
    '</div>'
  );

  const $ = id => modalBox.querySelector(id);
  const inAllDay = $('#inAllDay'), timeRow = $('#timeRow'), wdWrap = $('#wdWrap'), inFreq = $('#inFreq'), inInterval = $('#inInterval');

  function syncFreqUI(){
    const f = inFreq.value;
    const repRow = $('#repRow');
    repRow.style.display = f === 'none' ? 'none' : 'grid';
    repRow.style.gridTemplateColumns = (f === 'daily' || f === 'monthly') ? '1fr' : '1fr 1fr';
    $('#wdWrap').style.display = f === 'weekly' ? 'flex' : 'none';
    $('#fUntil').style.display = f === 'none' ? 'none' : 'flex';
    // Convenience: if the user just switched to Weekly and picked no days,
    // default to the weekday of the chosen date.
    if (f === 'weekly' && !modalBox.querySelector('#dowToggle .on')){
      const dv = $('#inDate').value;
      if (/^\d{4}-\d{2}-\d{2}$/.test(dv)){
        const dd = DateUtil.parseKey(dv);
        const dow = DateUtil.dayOfWeek(dd.y, dd.m, dd.d);
        const btn = modalBox.querySelector('#dowToggle [data-dow="' + dow + '"]');
        if (btn) btn.classList.add('on');
      }
    }
  }
  function syncAllDay(){
    timeRow.style.display = inAllDay.checked ? 'none' : 'grid';
    syncFreqUI();
  }
  inAllDay.addEventListener('change', () => { syncAllDay(); });
  inFreq.addEventListener('change', syncFreqUI);
  $('#dowToggle').addEventListener('click', (e) => {
    const b = e.target.closest('[data-dow]'); if (!b) return;
    b.classList.toggle('on');
  });
  $('#swatches').addEventListener('click', (e) => {
    const b = e.target.closest('[data-color]'); if (!b) return;
    modalBox.querySelectorAll('.sw').forEach(s => s.classList.remove('sel'));
    b.classList.add('sel');
  });
  syncFreqUI(); syncAllDay();
  setTimeout(() => $('#inTitle').focus(), 30);

  $('#btnCancel').onclick = closeModal;
  if (editing) $('#btnDelete').onclick = () => { closeModal(); requestDelete(ev, occ); };
  $('#btnSave').onclick = () => {
    const data = collect();
    if (!data) return;
    closeModal();
    commit(data);
  };

  function setErr(fieldId, msgId, msg){
    const f = modalBox.querySelector(fieldId), m = modalBox.querySelector(msgId);
    if (f) f.classList.toggle('err', !!msg);
    if (m) m.textContent = msg || '';
    return !!msg;
  }
  function collect(){
    let bad = false;
    const t = $('#inTitle').value.trim();
    if (!t) bad = setErr('#fTitle', '#eTitle', 'Title is required.') || bad;
    else setErr('#fTitle', '#eTitle', '');
    const dstr = $('#inDate').value;
    const dObj = dstr ? DateUtil.parseKey(dstr) : null;
    const dValid = dObj && /^\d{4}-\d{2}-\d{2}$/.test(dstr) && dObj.m >= 1 && dObj.m <= 12 && dObj.d >= 1 && dObj.d <= DateUtil.daysInMonth(dObj.y, dObj.m);
    if (!dValid) bad = setErr('#fDate', '#eDate', 'Enter a valid date.') || bad;
    else setErr('#fDate', '#eDate', '');
    const allDay = inAllDay.checked;
    let s = value.start, e = value.end;
    if (!allDay){
      s = parseTimeInput($('#inStart').value);
      e = parseTimeInput($('#inEnd').value);
      if (s === null) bad = setErr('#fStart', '#eStart', 'Invalid start time.') || bad; else setErr('#fStart', '#eStart', '');
      if (e === null) bad = setErr('#fEnd', '#eTime', 'Invalid end time.') || bad;
      else if (s !== null && e <= s) bad = setErr('#fEnd', '#eTime', 'End time must be after start time.') || bad;
      else setErr('#fEnd', '#eTime', '');
    }
    const untilStr = $('#inUntil').value;
    if (untilStr && (!dValid || untilStr < dstr)) bad = setErr('#fUntil', '#eDate', '') || setErr('#fDate', '#eDate', 'Repeat-until must be on or after the start date.') || bad;
    if (bad) return null;
    let weekdays = [];
    if (inFreq.value === 'weekly'){
      weekdays = Array.from(modalBox.querySelectorAll('#dowToggle [data-dow].on')).map(b => +b.dataset.dow).sort((a,b)=>a-b);
      if (!weekdays.length){
        toast('Pick at least one weekday for a weekly repeat.');
        return null;
      }
    }
    const freq = inFreq.value;
    const recurrence = freq === 'none' ? null : {
      freq,
      interval: +inInterval.value || 1,
      weekdays: freq === 'weekly' ? weekdays : null,
      until: untilStr || null,
      dayOfMonth: freq === 'monthly' || freq === 'yearly' ? dObj.d : null,
      month: freq === 'yearly' ? dObj.m : null
    };
    return {
      title: t, color: (modalBox.querySelector('.sw.sel') || {}).dataset ? modalBox.querySelector('.sw.sel').dataset.color : 'peacock',
      notes: $('#inNotes').value.trim(), allDay, date: dstr, start: s, end: e, recurrence
    };
  }
  function commit(data){
    if (!editing){
      const n = Store.addEvent(data);
      render();
      toast('Event created', [{ label:'Undo', fn: () => { Store.removeEvent(n.id); render(); } }]);
      return;
    }
    if (occ && ev && ev.recurrence){
      askScope('edit', ev, occ, data);
      return;
    }
    if (occ && occ.isException){
      // plain event moved via exception — collapse back onto the master
      Store.updateEvent(ev.id, { title:data.title, color:data.color, notes:data.notes, allDay:data.allDay, date:data.date, start:data.start, end:data.end });
      Store.setException(ev.id, occ.occKey, null);
      render(); toast('Event updated');
      return;
    }
    Store.updateEvent(ev.id, data);
    render();
    toast('Event updated', [{ label:'Undo', fn: () => { Store.updateEvent(ev.id, snapshot); render(); } }]);
  }
  var snapshot = ev ? JSON.parse(JSON.stringify(ev)) : null;
}

/* ============================================================================
   SECTION 11 — SCOPE DIALOG (this / following / all) + DELETE FLOWS
   ============================================================================ */
function askScope(kind, ev, occ, data){
  const isEdit = kind === 'edit';
  const occKey = occ.occKey;
  showModal(
    '<h2>' + (isEdit ? 'Edit repeating event' : 'Delete repeating event') + '</h2>' +
    '<div class="confirm">“' + esc(ev.title) + '” repeats.<br>How much of the series do you want to ' + (isEdit ? 'change' : 'delete') + '?</div>' +
    '<div class="menu">' +
      '<button data-scope="one">This event only<span>' + (isEdit ? 'Changes' : 'Deletes') + ' just ' + fmtKeyLong(occKey) + '</span></button>' +
      '<button data-scope="following">This and following events<span>' + (isEdit ? 'Changes' : 'Deletes') + ' everything from ' + fmtKeyLong(occKey) + ' onward</span></button>' +
      '<button data-scope="all" class="' + (isEdit ? '' : 'dngr') + '">All events<span>' + (isEdit ? 'Changes' : 'Deletes') + ' the entire series</span></button>' +
    '</div>' +
    '<div class="mfoot"><div style="flex:1"></div><button class="tbtn" id="btnCancelScope">Cancel</button></div>'
  );
  modalBox.querySelector('#btnCancelScope').onclick = closeModal;
  modalBox.querySelectorAll('[data-scope]').forEach(b => {
    b.onclick = () => {
      const scope = b.dataset.scope;
      closeModal();
      setTimeout(() => {
        if (isEdit) applyEditScope(scope, ev, occ, data);
        else applyDeleteScope(scope, ev, occ);
      }, 10);
    };
  });
}
function fmtKeyLong(k){ const d = DateUtil.parseKey(k); return MONTH_SHORT[d.m-1] + ' ' + d.d + ', ' + d.y; }

function applyEditScope(scope, ev, occ, data){
  const occKey = occ.occKey;
  if (scope === 'one'){
    const merged = mergePatch(ev, occKey, {
      title:data.title, color:data.color, notes:data.notes, allDay:data.allDay,
      date:data.date, start:data.start, end:data.end
    });
    Store.setException(ev.id, occKey, { type:'override', patch: merged });
    render();
    toast('One occurrence updated', [{ label:'Undo', fn: () => { Store.setException(ev.id, occKey, null); render(); } }]);
    return;
  }
  if (scope === 'following'){
    // 1) truncate the master series the day before this occurrence
    const cut = DateUtil.addDaysTo(DateUtil.parseKey(occKey), -1);
    const masterSnap = JSON.parse(JSON.stringify(ev));
    const origRec = JSON.parse(JSON.stringify(ev.recurrence));   // capture BEFORE mutating
    const newStartObj = DateUtil.parseKey(data.date ? data.date : occKey);
    Store.updateEvent(ev.id, { recurrence: Object.assign({}, origRec, { until: DateUtil.keyOf(cut) }) });
    // 2) start a new series at this occurrence with the new values
    const newEv = Store.addEvent({
      title:data.title, color:data.color, notes:data.notes, allDay:data.allDay,
      date:data.date ? data.date : occKey, start:data.start, end:data.end,
      recurrence: Object.assign({}, origRec, {
        until: origRec.until || null,
        weekdays: origRec.freq === 'weekly'
          ? (origRec.weekdays && origRec.weekdays.length ? origRec.weekdays.slice() : [DateUtil.dayOfWeek(newStartObj.y, newStartObj.m, newStartObj.d)])
          : null,
        dayOfMonth: (origRec.freq === 'monthly' || origRec.freq === 'yearly') ? newStartObj.d : null,
        month: origRec.freq === 'yearly' ? newStartObj.m : null
      })
    });
    render();
    toast('New series started', [{ label:'Undo', fn: () => {
      Store.removeEvent(newEv.id);
      Store.updateEvent(ev.id, { recurrence: masterSnap.recurrence });
      render();
    } }]);
    return;
  }
  // all
  const snap = JSON.parse(JSON.stringify(ev));
  const oldStart = DateUtil.parseKey(ev.date);
  const shift = DateUtil.diffDays(DateUtil.parseKey(occKey), DateUtil.parseKey(data.date));
  const newStart = DateUtil.addDaysTo(oldStart, shift);
  Store.updateEvent(ev.id, {
    title:data.title, color:data.color, notes:data.notes, allDay:data.allDay,
    date: (shift === 0 ? ev.date : DateUtil.keyOf(newStart)),
    start:data.start, end:data.end,
    recurrence: data.recurrence || ev.recurrence,
    exceptions: shift === 0 ? ev.exceptions : {}
  });
  render();
  toast('Series updated', [{ label:'Undo', fn: () => { Store.updateEvent(ev.id, snap); render(); } }]);
}
function mergePatch(ev, occKey, patch){
  const ex = ev.exceptions && ev.exceptions[occKey];
  const base = (ex && ex.type === 'override' && ex.patch) ? ex.patch : {};
  return Object.assign({}, base, patch);
}

function requestDelete(ev, occ){
  if (occ && ev.recurrence){ askScope('delete', ev, occ, null); return; }
  const snap = JSON.parse(JSON.stringify(ev));
  removeWithUndo([snap], 'Event deleted');
}
function applyDeleteScope(scope, ev, occ){
  const occKey = occ.occKey;
  if (scope === 'one'){
    Store.setException(ev.id, occKey, { type:'cancel' });
    render();
    toast('Occurrence deleted', [{ label:'Undo', fn: () => { Store.setException(ev.id, occKey, null); render(); } }]);
    return;
  }
  if (scope === 'following'){
    const snap = JSON.parse(JSON.stringify(ev));
    const cut = DateUtil.addDaysTo(DateUtil.parseKey(occKey), -1);
    Store.updateEvent(ev.id, { recurrence: Object.assign({}, ev.recurrence, { until: DateUtil.keyOf(cut) }) });
    render();
    toast('Remaining series deleted', [{ label:'Undo', fn: () => { Store.updateEvent(ev.id, snap); render(); } }]);
    return;
  }
  const snap = JSON.parse(JSON.stringify(ev));
  removeWithUndo([snap], 'Series deleted');
}
function removeWithUndo(snaps, label){
  snaps.forEach(s => Store.removeEvent(s.id));
  render();
  toast(label, [{ label:'Undo', fn: () => { snaps.forEach(s => Store.restoreEvent(s)); render(); } }]);
}

/** Clicking an occurrence chip/row: plain event -> edit; series -> occurrence editor. */
function openOccurrence(occ){
  if (occ.recurring) openEditor({ occ });
  else {
    const ev = Store.getEvent(occ.evId);
    if (!ev) return;
    if (occ.isException) openEditor({ occ });
    else openEditor({ eventId: ev.id });
  }
}

/* ============================================================================
   SECTION 12 — DAY POPOVER (overflow "+N more")
   ============================================================================ */
let popoverEl = null;
function closePopover(){
  if (popoverEl){ popoverEl.remove(); popoverEl = null; }
  document.removeEventListener('mousedown', popoverOutside, true);
}
function popoverOutside(e){
  if (popoverEl && !popoverEl.contains(e.target)) closePopover();
}
function openDayPopover(key, anchor){
  closePopover();
  const d = DateUtil.parseKey(key);
  const occs = occurrencesInRange(d, d);
  const pop = document.createElement('div');
  pop.className = 'popover';
  const dow = new Date(Date.UTC(d.y, d.m - 1, d.d)).getUTCDay();
  const h = document.createElement('div');
  h.className = 'ph';
  h.innerHTML = '<span>' + MONTH_NAMES[d.m-1] + ' ' + d.d + ', ' + d.y + '</span><small>' + DAY_NAMES[dow] + '</small>';
  pop.appendChild(h);
  if (!occs.length){
    const e = document.createElement('div');
    e.className = 'pi'; e.innerHTML = '<div class="ps">No events</div>';
    pop.appendChild(e);
  }
  for (const o of occs){
    const row = document.createElement('div');
    row.className = 'pi';
    row.innerHTML = '<div class="dot2" style="background:' + colorById(o.color).solid + '"></div>' +
      '<div style="min-width:0"><div class="pt">' + esc(o.title) + '</div><div class="ps">' +
      (o.allDay ? 'All day' : DateUtil.fmtTime(Math.floor(o.start/60), o.start%60) + ' – ' + DateUtil.fmtTime(Math.floor(o.end/60), o.end%60)) +
      (o.recurring ? ' · repeats' : '') + '</div></div>';
    row.addEventListener('click', () => { closePopover(); openOccurrence(o); });
    pop.appendChild(row);
  }
  const add = document.createElement('div');
  add.className = 'pi';
  add.innerHTML = '<div class="dot2" style="background:var(--accent)"></div><div class="pt">Add event</div>';
  add.addEventListener('click', () => { closePopover(); openEditor({ date:key }); });
  pop.appendChild(add);
  document.body.appendChild(pop);
  const r = anchor.getBoundingClientRect();
  let left = Math.min(window.innerWidth - 300, Math.max(8, r.left - 60));
  let top = r.bottom + 6;
  if (top + 340 > window.innerHeight) top = Math.max(8, r.top - 346);
  pop.style.left = left + 'px';
  pop.style.top = top + 'px';
  popoverEl = pop;
  setTimeout(() => document.addEventListener('mousedown', popoverOutside, true), 0);
}

/* ============================================================================
   SECTION 13 — SETTINGS MODAL
   ============================================================================ */
function openSettings(){
  const s = settings();
  showModal(
    '<h2>Settings</h2>' +
    '<div class="mbody">' +
      '<div class="settings-row"><div><div class="sl">Week starts on Monday</div><div class="sd">Affects month grid, week view and week numbers.</div></div>' +
        '<button class="switch ' + (s.weekStart === 1 ? 'on' : '') + '" id="swWeek"></button></div>' +
      '<div class="settings-row"><div><div class="sl">Show week numbers</div><div class="sd">Displays W-numbers in the month grid.</div></div>' +
        '<button class="switch ' + (s.showWeekNums ? 'on' : '') + '" id="swWk"></button></div>' +
      '<div class="settings-row"><div><div class="sl">Hour range in week view</div><div class="sd">Currently ' + CONFIG.HOUR_START + ':00 – ' + CONFIG.HOUR_END + ':00</div></div>' +
        '<div style="color:var(--text-2);font-size:12.5px">editable in CONFIG</div></div>' +
    '</div>' +
    '<div class="mfoot"><button class="tbtn del" id="btnClearAll">Clear all events</button>' +
      '<div style="flex:1"></div><button class="primary" id="btnDone">Done</button></div>' +
    '<div class="hint">Data is stored locally in your browser (localStorage).</div>'
  );
  modalBox.querySelector('#swWeek').onclick = (e) => {
    const on = !e.currentTarget.classList.contains('on');
    e.currentTarget.classList.toggle('on', on);
    Store.setSetting('weekStart', on ? 1 : 0);
    render();
  };
  modalBox.querySelector('#swWk').onclick = (e) => {
    const on = !e.currentTarget.classList.contains('on');
    e.currentTarget.classList.toggle('on', on);
    Store.setSetting('showWeekNums', on);
    render();
  };
  modalBox.querySelector('#btnClearAll').onclick = () => {
    closeModal();
    setTimeout(() => confirmDialog('Clear all events?', 'This removes every event from this calendar. This cannot be undone once you dismiss the notification.', () => {
      const snap = JSON.parse(JSON.stringify(Store.getEvents()));
      Store.getEvents().slice().forEach(e => Store.removeEvent(e.id));
      render();
      toast('All events cleared', [{ label:'Undo', fn: () => { snap.forEach(e => Store.restoreEvent(e)); render(); } }]);
    }, { danger:true, yesLabel:'Clear all' }), 10);
  };
  modalBox.querySelector('#btnDone').onclick = closeModal;
}

/* ============================================================================
   SECTION 14 — DRAG & DROP (pointer based)
   Month: move a chip to another day.
   Week:  move vertically to change time (SNAP_MINUTES); drag bottom edge to resize.
   ============================================================================ */
const dragState = { active:false };
document.addEventListener('pointerdown', onDragDown, true);
function onDragDown(e){
  if (e.button !== 0) return;
  const handle = e.target.closest('.resizeh');
  const wev = e.target.closest('.wev');
  const chip = e.target.closest('.chip');
  if (handle && wev){ beginWeekDrag(e, wev, 'resize'); return; }
  if (wev){ beginWeekDrag(e, wev, 'move'); return; }
  if (chip && chip.dataset.evId){ beginMonthDrag(e, chip); return; }
}
function beginMonthDrag(e, chip){
  const occ = chip._occ; if (!occ) return;
  const startX = e.clientX, startY = e.clientY;
  let started = false, ghost = null, targetKey = null;
  const move = (ev) => {
    if (!started){
      if (Math.hypot(ev.clientX - startX, ev.clientY - startY) < 5) return;
      started = true;
      document.body.style.userSelect = 'none';
      chip.classList.add('dragging');
      ghost = chip.cloneNode(true);
      ghost.style.position = 'fixed'; ghost.style.zIndex = 300; ghost.style.pointerEvents = 'none';
      ghost.style.opacity = '.9'; ghost.style.width = chip.offsetWidth + 'px'; ghost.style.boxShadow = 'var(--shadow-lg)';
      ghost.classList.remove('dragging');
      document.body.appendChild(ghost);
    }
    ghost.style.left = (ev.clientX - 60) + 'px';
    ghost.style.top = (ev.clientY - 12) + 'px';
    const under = document.elementFromPoint(ev.clientX, ev.clientY);
    const cell = under && under.closest('.day-cell');
    document.querySelectorAll('.day-cell.drop-target').forEach(c => c.classList.remove('drop-target'));
    targetKey = null;
    if (cell){ cell.classList.add('drop-target'); targetKey = cell.dataset.key; }
  };
  const up = (ev) => {
    document.removeEventListener('pointermove', move);
    document.removeEventListener('pointerup', up);
    document.body.style.userSelect = '';
    document.querySelectorAll('.day-cell.drop-target').forEach(c => c.classList.remove('drop-target'));
    if (ghost) ghost.remove();
    chip.classList.remove('dragging');
    if (!started || !targetKey) return;
    const fromKey = DateUtil.keyOf(occ.date);
    if (targetKey === fromKey) return;
    moveOccurrenceToDate(occ, targetKey);
  };
  document.addEventListener('pointermove', move);
  document.addEventListener('pointerup', up);
}
function moveOccurrenceToDate(occ, newKey){
  const ev = Store.getEvent(occ.evId); if (!ev) return;
  if (ev.recurrence){
    const patch = mergePatch(ev, occ.occKey, { date: newKey });
    Store.setException(ev.id, occ.occKey, { type:'override', patch });
    render();
    toast('Occurrence moved to ' + fmtKeyLong(newKey), [{ label:'Undo', fn: () => { Store.setException(ev.id, occ.occKey, null); render(); } }]);
  } else {
    const prev = ev.date;
    Store.updateEvent(ev.id, { date: newKey });
    render();
    toast('Moved to ' + fmtKeyLong(newKey), [{ label:'Undo', fn: () => { Store.updateEvent(ev.id, { date: prev }); render(); } }]);
  }
}
function beginWeekDrag(e, el, mode){
  const occ = el._occ; if (!occ) return;
  e.preventDefault();
  const ev = Store.getEvent(occ.evId); if (!ev) return;
  const H0 = CONFIG.HOUR_START, HH = CONFIG.HOUR_HEIGHT, snap = CONFIG.SNAP_MINUTES;
  const col = el.closest('.wkcol');
  const colRect = col.getBoundingClientRect();
  const startY = e.clientY, startX = e.clientX;
  const origH = el.offsetHeight;
  let dur = Math.max(snap, occ.end - occ.start);
  let curStart = occ.start, curEnd = occ.end, curKey = DateUtil.keyOf(occ.date);
  let moved = false;

  const move = (mev) => {
    const dy = mev.clientY - startY;
    if (!moved && Math.abs(dy) < 4 && Math.abs(mev.clientX - startX) < 4) return;
    moved = true;
    el.classList.add('dragging');
    const dMin = Math.round((dy / HH * 60) / snap) * snap;
    if (mode === 'resize'){
      curEnd = Math.max(curStart + snap, Math.min(1440, occ.end + dMin));
    } else {
      let s = occ.start + dMin;
      s = Math.max(0, Math.min(1440 - dur, s));
      curStart = s; curEnd = s + dur;
    }
    el.style.top = ((curStart - H0 * 60) / 60 * HH) + 'px';
    el.style.height = Math.max(16, (curEnd - curStart) / 60 * HH) + 'px';
    // horizontal: move across day columns
    const under = document.elementFromPoint(mev.clientX, mev.clientY);
    const tcol = under && under.closest('.wkcol');
    if (tcol && tcol !== col){ curKey = tcol.dataset.key; }
  };
  const up = (uev) => {
    document.removeEventListener('pointermove', move);
    document.removeEventListener('pointerup', up);
    el.classList.remove('dragging');
    if (!moved) return;
    commit();
  };
  document.addEventListener('pointermove', move);
  document.addEventListener('pointerup', up);

  function commit(){
    const changedTime = curStart !== occ.start || curEnd !== occ.end;
    const changedDate = curKey !== DateUtil.keyOf(occ.date);
    if (!changedTime && !changedDate) return;
    const patch = { start: curStart, end: curEnd };
    if (changedDate) patch.date = curKey;
    if (ev.recurrence){
      const merged = mergePatch(ev, occ.occKey, patch);
      Store.setException(ev.id, occ.occKey, { type:'override', patch: merged });
      render();
      toast('Occurrence updated', [{ label:'Undo', fn: () => { Store.setException(ev.id, occ.occKey, null); render(); } }]);
    } else {
      const prev = { date: ev.date, start: ev.start, end: ev.end };
      Store.updateEvent(ev.id, patch);
      render();
      toast('Event updated', [{ label:'Undo', fn: () => { Store.updateEvent(ev.id, prev); render(); } }]);
    }
  }
}

/* ============================================================================
   SECTION 15 — BOOT
   ============================================================================ */
document.getElementById('btnToday').onclick = goToday;
document.getElementById('btnPrev').onclick = () => navStep(-1);
document.getElementById('btnNext').onclick = () => navStep(1);
document.getElementById('btnNew').onclick = () => openEditor({ date: DateUtil.keyOf(Store.getCursor()) });
document.getElementById('btnSettings').onclick = openSettings;
document.getElementById('viewSeg').addEventListener('click', (e) => {
  const b = e.target.closest('button[data-view]'); if (b) setView(b.dataset.view);
});

Store.load();
ViewState.view = settings().view || 'month';
if (!['month','week','agenda'].includes(ViewState.view)) ViewState.view = 'month';
render();

// Keep the current-time indicator fresh
setInterval(() => {
  const line = document.getElementById('nowLine');
  if (!line) return;
  const now = new Date();
  const mins = now.getHours() * 60 + now.getMinutes();
  line.style.top = ((mins - CONFIG.HOUR_START * 60) / 60 * CONFIG.HOUR_HEIGHT) + 'px';
  line.querySelector('.nltime').textContent = DateUtil.fmtTime(now.getHours(), now.getMinutes());
}, 30000);

// Re-render at midnight so the "today" highlight stays correct
let lastDayKey = DateUtil.keyOf(DateUtil.today());
setInterval(() => {
  const k = DateUtil.keyOf(DateUtil.today());
  if (k !== lastDayKey){ lastDayKey = k; render(); }
}, 60000);

// Expose internals for testing / debugging
window.CalendarAPI = {
  CONFIG, DateUtil, Store, occurrencesInRange, recurrenceOccurrences,
  ViewState, render, navStep, goToday, setView, openEditor, openOccurrence,
  mergePatch, applyEditScope, applyDeleteScope, moveOccurrenceToDate,
  settings, weekStart
};
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"f23ebe99-81bc-4819-917d-a754f5c6a775","tokensIn":5604190,"tokensOut":63341,"tokensTotal":5667531,"cost":0.072969612,"turns":51,"toolCalls":50,"failedToolCalls":0,"timestamp":"2026-09-11T00:40:31.445Z"} -->