← Calendar results

Calendar

GPT 6 Luna · 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 45,660 bytes · SHA-256 387ef3b58f82
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Daymark — Calendar</title>
<style>
:root{--ink:#202124;--muted:#70757a;--line:#e6e8eb;--blue:#1a73e8;--blue-soft:#e8f0fe;--surface:#fff;--bg:#f8fafd;--shadow:0 8px 28px #20212424;--radius:14px;--app-h:100vh}
*{box-sizing:border-box}body{margin:0;color:var(--ink);font:14px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:var(--bg)}button,input,select,textarea{font:inherit}button{cursor:pointer;color:inherit}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid #4285f4;outline-offset:2px}.app{min-height:100vh;display:flex;flex-direction:column}.topbar{height:72px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;padding:0 26px;gap:22px;position:relative;z-index:4}.brand{display:flex;align-items:center;gap:11px;min-width:214px}.brand-mark{height:38px;width:38px;border-radius:12px;background:linear-gradient(145deg,#4285f4,#1769d2);display:grid;place-items:center;color:white;font-size:20px;box-shadow:0 4px 10px #1a73e833}.brand-name{font-size:19px;letter-spacing:-.5px;font-weight:650}.brand-name span{font-weight:400;color:#6b7280}.nav-controls{display:flex;align-items:center;gap:8px}.icon-btn,.today-btn,.view-btn,.primary-btn,.subtle-btn{border:0;background:transparent;border-radius:9px;min-height:36px}.icon-btn{width:36px;font-size:20px;color:#5f6368}.icon-btn:hover,.subtle-btn:hover{background:#f1f3f4}.today-btn{border:1px solid #dadce0;padding:0 14px;font-weight:550;margin:0 5px}.today-btn:hover{background:#f8f9fa}.period-title{font-size:22px;font-weight:550;letter-spacing:-.4px;min-width:200px}.top-spacer{flex:1}.view-switch{display:flex;background:#f1f3f4;border-radius:9px;padding:3px;gap:2px}.view-btn{padding:0 12px;color:#5f6368;font-size:13px}.view-btn.active{background:#fff;color:#1a73e8;box-shadow:0 1px 3px #0002;font-weight:600}.settings-btn{font-size:18px}.layout{display:grid;grid-template-columns:250px minmax(0,1fr);gap:24px;padding:20px 24px 28px;flex:1;min-height:0}.sidebar{display:flex;flex-direction:column;gap:20px}.create-btn{height:48px;border:0;background:#fff;border-radius:16px;box-shadow:0 2px 8px #20212422;display:flex;align-items:center;gap:13px;padding:0 18px;font-weight:600;width:max-content;min-width:150px}.create-btn:hover{box-shadow:0 4px 12px #20212430}.plus{font-size:24px;color:var(--blue);font-weight:300}.mini-card{background:#fff;border:1px solid var(--line);border-radius:13px;padding:13px 12px 10px}.mini-head{display:flex;align-items:center;justify-content:space-between;font-size:13px;font-weight:600;padding:0 3px 8px}.mini-head button{border:0;background:none;color:#687078;font-size:17px;border-radius:6px;width:25px;height:25px}.mini-head button:hover{background:#f2f4f5}.mini-grid{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;gap:2px}.mini-grid .dow{font-size:10px;color:#73777d;height:23px;display:grid;place-items:center}.mini-day{height:29px;width:29px;margin:auto;border:0;background:none;border-radius:50%;font-size:11px;color:#34383c}.mini-day.outside{color:#b6b9bd}.mini-day.selected{background:var(--blue-soft);color:var(--blue);font-weight:700}.mini-day.today{background:var(--blue);color:#fff;font-weight:700}.side-note{color:#858b91;font-size:12px;padding:0 4px;line-height:1.55}.calendar-surface{background:#fff;border:1px solid var(--line);border-radius:14px;min-width:0;overflow:hidden;box-shadow:0 1px 3px #20212408;position:relative;transition:opacity .16s ease,transform .16s ease}.calendar-surface.changing{opacity:.35;transform:translateY(5px)}.month-view{height:calc(100vh - 116px);min-height:510px;display:flex;flex-direction:column}.weekday-row{height:37px;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));border-bottom:1px solid var(--line);flex:none}.weekday{display:flex;align-items:center;justify-content:center;color:#70757a;font-size:11px;font-weight:600;letter-spacing:.5px;text-transform:uppercase}.month-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));grid-template-rows:repeat(6,minmax(0,1fr));flex:1;min-height:0}.month-cell{position:relative;border-right:1px solid var(--line);border-bottom:1px solid var(--line);padding:7px 5px 3px;min-width:0;overflow:hidden;transition:background .12s}.month-cell:nth-child(7n){border-right:0}.month-cell:nth-last-child(-n+7){border-bottom:0}.month-cell:hover{background:#f9fbff}.month-cell.outside{background:#fbfcfd}.month-cell.outside .date-number{color:#b7bbc0}.date-number{height:26px;width:26px;display:grid;place-items:center;border-radius:50%;font-size:12px;margin:0 auto 3px;color:#50555a}.date-number.today{background:var(--blue);color:white;font-weight:700}.event-list{display:flex;flex-direction:column;gap:2px}.event-chip{border:0;border-left:3px solid var(--event);background:var(--event-bg);color:var(--event-text);border-radius:4px;padding:2px 5px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:11px;line-height:16px;font-weight:550;min-height:20px}.event-chip:hover{filter:saturate(1.18);box-shadow:0 1px 3px #0002}.more-link{background:none;border:0;padding:2px 4px;color:#5f6368;font-size:11px;text-align:left}.more-link:hover{color:var(--blue);text-decoration:underline}.week-view{height:calc(100vh - 116px);min-height:510px;display:flex;flex-direction:column;overflow:hidden}.week-head{display:grid;grid-template-columns:58px repeat(7,minmax(0,1fr));height:60px;border-bottom:1px solid var(--line);flex:none}.week-head-day{border-left:1px solid var(--line);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px}.week-head-day .wd{font-size:10px;color:#70757a;font-weight:600;letter-spacing:.5px}.week-head-day .dn{font-size:21px;line-height:24px}.week-head-day .dn.today{color:#fff;background:var(--blue);border-radius:50%;width:29px;height:29px;display:grid;place-items:center;font-size:16px}.all-day-row{display:grid;grid-template-columns:58px repeat(7,minmax(0,1fr));min-height:37px;border-bottom:1px solid var(--line);flex:none}.all-label{font-size:10px;color:#777;align-self:center;text-align:center}.all-day-cell{border-left:1px solid var(--line);padding:3px;min-width:0}.week-scroller{overflow:auto;flex:1;position:relative}.week-body{display:grid;grid-template-columns:58px repeat(7,minmax(0,1fr));min-height:calc(var(--hours)*58px);position:relative}.hour-labels{position:relative}.hour-label{height:58px;position:relative;color:#85898e;font-size:10px;text-align:right;padding-right:9px;transform:translateY(-7px)}.day-track{position:relative;border-left:1px solid var(--line);background:repeating-linear-gradient(to bottom,transparent 0,transparent 57px,#e9ebee 57px,#e9ebee 58px);min-width:0}.time-event{position:absolute;left:3px;right:4px;border-left:3px solid var(--event);background:var(--event-bg);color:var(--event-text);border-radius:5px;padding:4px 5px;font-size:11px;overflow:hidden;z-index:1;min-height:18px;cursor:grab}.time-event strong{font-size:11px;display:block;line-height:1.25}.time-event span{font-size:10px;opacity:.8}.time-event:active{cursor:grabbing}.now-line{position:absolute;left:58px;right:0;height:2px;background:#ea4335;z-index:3;pointer-events:none}.now-line:before{content:"";position:absolute;left:-4px;top:-3px;width:8px;height:8px;background:#ea4335;border-radius:50%}.agenda-view{height:calc(100vh - 116px);min-height:510px;overflow:auto;padding:24px clamp(20px,5vw,72px)}.agenda-title{font-size:12px;text-transform:uppercase;color:#77808a;font-weight:650;letter-spacing:.8px;margin:5px 0 13px}.agenda-day{display:grid;grid-template-columns:125px 1fr;border-top:1px solid var(--line);padding:14px 0;min-height:62px}.agenda-date{font-weight:600}.agenda-date small{display:block;color:#85898f;font-size:11px;font-weight:400}.agenda-events{display:flex;flex-direction:column;gap:7px}.agenda-item{border:0;border-left:4px solid var(--event);border-radius:6px;background:var(--event-bg);color:var(--event-text);text-align:left;padding:8px 12px;display:flex;gap:14px;align-items:center}.agenda-time{font-size:11px;min-width:82px;color:#60666d}.agenda-name{font-weight:600}.agenda-empty{color:#92979c;padding:12px 0}.empty-range{text-align:center;color:#8a9096;padding:60px 0}.settings-panel{position:absolute;right:24px;top:62px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:17px;width:245px;box-shadow:var(--shadow);z-index:7}.settings-panel h3{margin:0 0 13px;font-size:15px}.setting-row{display:flex;align-items:center;justify-content:space-between;margin:12px 0;color:#4e5358;font-size:13px}.setting-row select{border:1px solid #dadce0;padding:6px 8px;border-radius:7px;background:#fff}.modal-backdrop{position:fixed;inset:0;background:#20212455;display:grid;place-items:center;z-index:10;padding:16px;animation:fade .14s ease}.modal{width:min(540px,100%);background:#fff;border-radius:16px;box-shadow:var(--shadow);padding:22px 24px 20px;animation:rise .18s ease}.modal.small{width:min(400px,100%)}@keyframes fade{from{opacity:0}to{opacity:1}}@keyframes rise{from{transform:translateY(8px);opacity:.7}to{transform:none;opacity:1}}.modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.modal-head h2{font-size:19px;margin:0;font-weight:600}.close-btn{border:0;background:none;border-radius:50%;font-size:22px;width:33px;height:33px;color:#70757a}.close-btn:hover{background:#f1f3f4}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:13px 14px}.field{display:flex;flex-direction:column;gap:5px}.field.full{grid-column:1/-1}.field label{font-size:12px;color:#636a70;font-weight:550}.field input,.field select,.field textarea{width:100%;border:1px solid #dadce0;border-radius:8px;padding:9px 10px;background:white;color:#292d32}.field textarea{resize:vertical;min-height:65px}.field input:focus,.field select:focus,.field textarea:focus{border-color:#4285f4;outline:none;box-shadow:0 0 0 2px #4285f422}.check-row{display:flex;align-items:center;gap:8px;color:#50565d;font-size:13px;margin-top:4px}.check-row input{accent-color:var(--blue)}.error{color:#c5221f;font-size:11px;min-height:15px}.palette{display:flex;gap:9px;align-items:center;padding:3px 0}.color-dot{width:23px;height:23px;border:2px solid transparent;border-radius:50%;background:var(--c);cursor:pointer}.color-dot.selected{outline:2px solid var(--c);outline-offset:2px;border-color:#fff}.modal-actions{display:flex;align-items:center;gap:8px;margin-top:18px}.modal-actions .push{flex:1}.primary-btn{background:var(--blue);color:#fff;padding:0 17px;font-weight:600}.primary-btn:hover{background:#1765cc}.text-btn{border:0;background:none;color:#5f6368;border-radius:7px;padding:8px 10px}.text-btn:hover{background:#f2f4f5}.delete-btn{color:#c5221f}.weekday-picks{display:flex;gap:5px}.weekday-pick{height:28px;width:28px;border:1px solid #dadce0;border-radius:50%;background:white;color:#6b7177;font-size:11px}.weekday-pick.selected{background:var(--blue);border-color:var(--blue);color:white}.scope-list{display:flex;flex-direction:column;gap:8px}.scope-choice{padding:12px 13px;border:1px solid var(--line);background:#fff;border-radius:9px;text-align:left}.scope-choice:hover{border-color:#8ab4f8;background:#f7faff}.scope-choice strong{display:block;font-size:13px}.scope-choice span{font-size:11px;color:#777}.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:#303134;color:#fff;border-radius:8px;padding:11px 15px;box-shadow:var(--shadow);z-index:20;display:flex;gap:18px;align-items:center;font-size:13px}.toast button{border:0;background:none;color:#8ab4f8;font-weight:650}.popover{position:fixed;z-index:8;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);padding:13px;width:290px;max-height:70vh;overflow:auto}.popover-head{display:flex;justify-content:space-between;align-items:center;font-weight:600;margin-bottom:10px}.popover-event{display:block;width:100%;text-align:left;border:0;border-left:4px solid var(--event);background:var(--event-bg);color:var(--event-text);border-radius:5px;padding:8px;margin:5px 0;font-size:12px}.popover-event small{display:block;opacity:.75;margin-top:2px}.recurrence-note{font-size:11px;color:#737980;margin-top:-4px}.hide{display:none!important}
@media(max-width:900px){.layout{grid-template-columns:205px minmax(0,1fr);gap:14px;padding:14px}.topbar{padding:0 16px;gap:12px}.brand{min-width:170px}.period-title{min-width:155px;font-size:19px}.month-cell{padding:5px 3px}.event-chip{font-size:10px;padding:1px 3px}.weekday{font-size:9px}.sidebar{gap:14px}}
@media(max-width:700px){.topbar{height:auto;min-height:64px;flex-wrap:wrap;padding:10px 12px;gap:7px}.brand{min-width:0;margin-right:auto}.brand-name{font-size:17px}.brand-mark{width:32px;height:32px}.top-spacer{display:none}.period-title{order:3;min-width:0;flex:1;font-size:18px}.nav-controls{order:2}.view-switch{order:4;margin-left:auto}.settings-btn{order:5}.layout{display:block;padding:10px}.sidebar{display:none}.month-view,.week-view,.agenda-view{height:calc(100vh - 138px);min-height:440px}.month-cell{padding:4px 1px}.date-number{height:22px;width:22px;font-size:11px;margin-bottom:2px}.event-chip{font-size:9px;line-height:13px;min-height:15px;padding:1px 2px;border-left-width:2px}.more-link{font-size:9px;padding:1px}.week-head,.all-day-row,.week-body{grid-template-columns:42px repeat(7,minmax(70px,1fr))}.week-head,.all-day-row{min-width:532px}.week-scroller{overflow:auto}.week-body{min-width:532px}.week-head{height:54px}.agenda-view{padding:18px 12px}.agenda-day{grid-template-columns:90px 1fr}.agenda-time{min-width:65px}.modal{padding:18px}.settings-panel{right:10px;top:58px}}
</style></head>
<body><div class="app">
<header class="topbar"><div class="brand"><div class="brand-mark">▦</div><div class="brand-name">Daymark <span>calendar</span></div></div><div class="nav-controls"><button class="today-btn" id="todayBtn">Today</button><button class="icon-btn" id="prevBtn" aria-label="Previous">‹</button><button class="icon-btn" id="nextBtn" aria-label="Next">›</button></div><div class="period-title" id="periodTitle"></div><div class="top-spacer"></div><div class="view-switch"><button class="view-btn active" data-view="month">Month</button><button class="view-btn" data-view="week">Week</button><button class="view-btn" data-view="agenda">Agenda</button></div><button class="icon-btn settings-btn" id="settingsBtn" aria-label="Settings">⚙</button></header>
<div class="layout"><aside class="sidebar"><button class="create-btn" id="createBtn"><span class="plus">+</span>Create event</button><div class="mini-card"><div class="mini-head"><button id="miniPrev">‹</button><span id="miniTitle"></span><button id="miniNext">›</button></div><div class="mini-grid" id="miniGrid"></div></div><div class="side-note">Make room for what matters.<br>Click any date to add something to your calendar.</div></aside><main class="calendar-surface" id="surface"></main></div><div id="settingsHost"></div><div id="modalHost"></div><div id="toastHost"></div><div id="popoverHost"></div>
</div>
<script>
'use strict';
/* ============================== Tunable constants ============================== */
const CONFIG={weekStart:1,hourStart:6,hourEnd:22,snapMinutes:15,colors:[{id:'blue',hex:'#4285f4',bg:'#e8f0fe',text:'#174ea6'},{id:'green',hex:'#34a853',bg:'#e6f4ea',text:'#137333'},{id:'purple',hex:'#a142f4',bg:'#f3e8fd',text:'#7627bb'},{id:'red',hex:'#ea4335',bg:'#fce8e6',text:'#b3261e'},{id:'orange',hex:'#fa7b17',bg:'#fef0e1',text:'#b45309'},{id:'teal',hex:'#12a594',bg:'#e0f2f1',text:'#087f72'}],weekdays:['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],weekdaysShort:['S','M','T','W','T','F','S'],hoursPx:58};
/* ============================== Date utilities (pure component math) ============================== */
const DateUtil={
 daysInMonth(y,m){if(m<0||m>11)throw new RangeError('Month must be 0–11');return [31,(y%4===0&&(y%100!==0||y%400===0))?29:28,31,30,31,30,31,31,30,31,30,31][m]},
 valid(y,m,d){return Number.isInteger(y)&&Number.isInteger(m)&&Number.isInteger(d)&&m>=0&&m<12&&d>=1&&d<=this.daysInMonth(y,m)},
 key(y,m,d){return `${String(y).padStart(4,'0')}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`},
 parse(key){const [y,m,d]=key.split('-').map(Number);return {y,m:m-1,d}},
 cmp(a,b){return a< b?-1:a>b?1:0},
 weekday(y,m,d){return new Date(Date.UTC(y,m,d)).getUTCDay()},
 addDays(date,n){let {y,m,d}=typeof date==='string'?this.parse(date):date;let left=Math.trunc(n);while(left>0){const available=this.daysInMonth(y,m)-d;if(left<=available){d+=left;left=0}else{left-=available+1;d=1;m++;if(m>11){m=0;y++}}}while(left<0){const available=d-1;if(-left<=available){d+=left;left=0}else{left+=available+1;m--;if(m<0){m=11;y--}d=this.daysInMonth(y,m)}}return this.key(y,m,d)},
 addMonths(date,n){let {y,m,d}=typeof date==='string'?this.parse(date):date;const total=y*12+m+n,ny=Math.floor(total/12),nm=((total%12)+12)%12;return this.key(ny,nm,Math.min(d,this.daysInMonth(ny,nm)))},
 weekStart(date,start=1){const p=typeof date==='string'?this.parse(date):date;const wd=this.weekday(p.y,p.m,p.d);return this.addDays(this.key(p.y,p.m,p.d),-((wd-start+7)%7))},
 monthGrid(y,m,start=1){const first=this.key(y,m,1),gridStart=this.weekStart(first,start);return Array.from({length:42},(_,i)=>this.addDays(gridStart,i))},
 weekNumber(date,start=1){const {y}=this.parse(date),jan1=this.key(y,0,1),first=this.weekStart(jan1,start),cur=this.weekStart(date,start);return Math.floor((this.dayOrdinal(cur)-this.dayOrdinal(first))/7)+1},
 dayOrdinal(date){const {y,m,d}=this.parse(date);let n=d-1;for(let i=0;i<m;i++)n+=this.daysInMonth(y,i);return y*366+n},
 timeMinutes(t){if(!t)return 0;const [h,m]=t.split(':').map(Number);return h*60+m},
 formatKey(key,opts={weekday:'long',month:'long',day:'numeric',year:'numeric'}){const p=this.parse(key);return new Intl.DateTimeFormat(undefined,opts).format(new Date(Date.UTC(p.y,p.m,p.d,12)))},
 expand(master,from,to){const out=[],{y:fy,m:fm,d:fd}=this.parse(from),{y:ty,m:tm,d:td}=this.parse(to);const start=master.date,rec=master.recurrence;if(!rec){if(start>=from&&start<=to)out.push(this.instance(master,start));return out}if(start>to||(rec.until&&rec.until<from))return out;let cursor=start; // component increments; guarded to the requested interval
 if(rec.freq==='daily'){cursor=start<from?from:start;while(cursor<=to){if(cursor>=start)this.pushOccurrence(master,cursor,out);cursor=this.addDays(cursor,1)}}
 else if(rec.freq==='weekly'){cursor=start<from?this.weekStart(from,0):start;let guard=0;while(cursor<=to&&guard++<5000){if(cursor>=start){const wd=this.weekday(...Object.values(this.parse(cursor)));if((rec.weekdays||[wd]).includes(wd))this.pushOccurrence(master,cursor,out)}cursor=this.addDays(cursor,1)}}
 else if(rec.freq==='monthly'){let p=this.parse(start),idx=p.y*12+p.m;const endIdx=ty*12+tm;idx=Math.max(idx,fy*12+fm);for(;idx<=endIdx;idx++){const yy=Math.floor(idx/12),mm=idx%12;if(p.d<=this.daysInMonth(yy,mm)){const k=this.key(yy,mm,p.d);if(k>=start&&k>=from&&k<=to)this.pushOccurrence(master,k,out)}}}
 else if(rec.freq==='yearly'){const sp=this.parse(start);for(let yy=Math.max(sp.y,fy);yy<=ty;yy++){if(sp.d<=this.daysInMonth(yy,sp.m)){const k=this.key(yy,sp.m,sp.d);if(k>=start&&k>=from&&k<=to)this.pushOccurrence(master,k,out)}}}
 return out},
 pushOccurrence(master,date,out){if(master.recurrence?.until&&date>master.recurrence.until)return;const ex=master.exceptions?.[date];if(ex?.cancelled)return;out.push(this.instance(master,date,ex?.override))},
 instance(master,date,override){return {...master,...(override||{}),date,id:`${master.id}@${date}`,masterId:master.id,recurring:!!master.recurrence,exception:!!override}}
};
/* ============================== Application state & persistence ============================== */
const STORE='daymark-calendar-v1';const todayKey=()=>{const d=new Date();return DateUtil.key(d.getFullYear(),d.getMonth(),d.getDate())};
function initialState(){const today=todayKey(),p=DateUtil.parse(today);return {events:[{id:'welcome-1',title:'A little planning time',date:today,start:'09:30',end:'10:15',allDay:false,color:'blue',notes:'Your calendar is ready when you are.',recurrence:null,exceptions:{}},{id:'welcome-2',title:'Weekly reset',date:DateUtil.addDays(today,2),start:'16:00',end:'16:30',allDay:false,color:'green',notes:'',recurrence:{freq:'weekly',weekdays:[DateUtil.weekday(...Object.values(DateUtil.parse(DateUtil.addDays(today,2))))]},exceptions:{}}],settings:{weekStart:CONFIG.weekStart},view:'month',focus:DateUtil.key(p.y,p.m,1)}};
function loadState(){try{const x=JSON.parse(localStorage.getItem(STORE));if(x&&Array.isArray(x.events)&&x.settings&&x.focus)return {...initialState(),...x,settings:{...initialState().settings,...x.settings}}}catch(e){}return initialState()}
let state=loadState(),selectedDate=todayKey(),activePopover=null,toastTimer=null,undoAction=null,editing=null;const $=s=>document.querySelector(s),surface=$('#surface');
function save(){localStorage.setItem(STORE,JSON.stringify(state))}function color(id){return CONFIG.colors.find(c=>c.id===id)||CONFIG.colors[0]}function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
function rangeForView(){if(state.view==='month'){const p=DateUtil.parse(state.focus);const g=DateUtil.monthGrid(p.y,p.m,state.settings.weekStart);return[g[0],g[41]]}if(state.view==='week'){const start=DateUtil.weekStart(state.focus,state.settings.weekStart);return[start,DateUtil.addDays(start,6)]}return[todayKey(),DateUtil.addDays(todayKey(),59)]}
function instances(from,to){return state.events.flatMap(ev=>DateUtil.expand(ev,from,to)).sort((a,b)=>a.date.localeCompare(b.date)||(a.allDay?-1:0)-(b.allDay?-1:0)||DateUtil.timeMinutes(a.start)-DateUtil.timeMinutes(b.start)||a.title.localeCompare(b.title))}
function setFocus(date){const p=DateUtil.parse(date);state.focus=DateUtil.key(p.y,p.m,1);selectedDate=date;save();render()}
function transition(fn){surface.classList.add('changing');setTimeout(()=>{fn();surface.classList.remove('changing')},90)}
function navigate(delta){transition(()=>{if(state.view==='week')state.focus=DateUtil.addDays(state.focus,delta*7);else if(state.view==='agenda')state.focus=DateUtil.addDays(state.focus,delta*30);else state.focus=DateUtil.addMonths(state.focus,delta);save();render()})}
/* ============================== Rendering ============================== */
function render(){document.querySelectorAll('.view-btn').forEach(b=>b.classList.toggle('active',b.dataset.view===state.view));const p=DateUtil.parse(state.focus);$('#periodTitle').textContent=state.view==='month'?new Intl.DateTimeFormat(undefined,{month:'long',year:'numeric'}).format(new Date(Date.UTC(p.y,p.m,1,12))):state.view==='week'?weekTitle():`Next 60 days`;renderMini();if(state.view==='month')renderMonth();if(state.view==='week')renderWeek();if(state.view==='agenda')renderAgenda();}
function weekTitle(){const a=DateUtil.weekStart(state.focus,state.settings.weekStart),b=DateUtil.addDays(a,6),pa=DateUtil.parse(a),pb=DateUtil.parse(b);const fmt=(k,o)=>DateUtil.formatKey(k,o);return pa.y===pb.y?(pa.m===pb.m?`${fmt(a,{month:'long',day:'numeric'})} – ${fmt(b,{day:'numeric',year:'numeric'})}`:`${fmt(a,{month:'short',day:'numeric'})} – ${fmt(b,{month:'short',day:'numeric',year:'numeric'})}`):`${fmt(a,{month:'short',day:'numeric',year:'numeric'})} – ${fmt(b,{month:'short',day:'numeric',year:'numeric'})}`}
function renderMini(){const p=DateUtil.parse(state.focus),grid=DateUtil.monthGrid(p.y,p.m,state.settings.weekStart);$('#miniTitle').textContent=new Intl.DateTimeFormat(undefined,{month:'short',year:'numeric'}).format(new Date(Date.UTC(p.y,p.m,1,12)));let html=weekdayLabels(true).map(x=>`<div class="dow">${x}</div>`).join('');for(const k of grid){const q=DateUtil.parse(k);html+=`<button class="mini-day ${q.m!==p.m?'outside':''} ${k===todayKey()?'today':''} ${k===selectedDate&&k!==todayKey()?'selected':''}" data-date="${k}">${q.d}</button>`}$('#miniGrid').innerHTML=html;$('#miniGrid').querySelectorAll('.mini-day').forEach(b=>b.onclick=()=>{selectedDate=b.dataset.date;state.focus=DateUtil.key(...Object.values(DateUtil.parse(selectedDate)));save();render()});$('#miniPrev').onclick=()=>{state.focus=DateUtil.addMonths(state.focus,-1);save();render()};$('#miniNext').onclick=()=>{state.focus=DateUtil.addMonths(state.focus,1);save();render()}}
function weekdayLabels(short=false){let a=[];for(let i=0;i<7;i++)a.push(CONFIG.weekdaysShort[(state.settings.weekStart+i)%7]);return a}
function eventStyle(ev){const c=color(ev.color);return `--event:${c.hex};--event-bg:${c.bg};--event-text:${c.text}`}
function chipLabel(ev){return `${ev.allDay?'':(ev.start+' ')}${ev.title}`}
function renderMonth(){const p=DateUtil.parse(state.focus),grid=DateUtil.monthGrid(p.y,p.m,state.settings.weekStart),list=instances(grid[0],grid[41]);surface.innerHTML=`<div class="month-view"><div class="weekday-row">${weekdayLabels().map(x=>`<div class="weekday">${x}</div>`).join('')}</div><div class="month-grid">${grid.map(k=>{const q=DateUtil.parse(k),day=list.filter(e=>e.date===k);return `<div class="month-cell ${q.m!==p.m?'outside':''}" data-date="${k}" tabindex="0"><div class="date-number ${k===todayKey()?'today':''}">${q.d}</div><div class="event-list">${day.slice(0,3).map(ev=>`<button class="event-chip" draggable="true" data-id="${ev.id}" style="${eventStyle(ev)}" title="${esc(ev.title)}">${esc(chipLabel(ev))}</button>`).join('')}${day.length>3?`<button class="more-link" data-more="${k}">+${day.length-3} more</button>`:''}</div></div>`}).join('')}</div></div>`;
 surface.querySelectorAll('.month-cell').forEach(cell=>{cell.onclick=e=>{if(e.target.closest('.event-chip,.more-link'))return;selectedDate=cell.dataset.date;openEditor(null,selectedDate)};cell.ondragover=e=>{e.preventDefault();cell.style.background='#eaf2ff'};cell.ondragleave=()=>cell.style.background='';cell.ondrop=e=>{e.preventDefault();cell.style.background='';const payload=JSON.parse(e.dataTransfer.getData('text/plain')||'{}');if(payload.id)reschedule(payload.id,cell.dataset.date)}});surface.querySelectorAll('.event-chip').forEach(b=>{b.onclick=e=>{e.stopPropagation();openEditorByInstance(b.dataset.id)};b.ondragstart=e=>{e.dataTransfer.setData('text/plain',JSON.stringify({id:b.dataset.id}));e.dataTransfer.effectAllowed='move'}});surface.querySelectorAll('[data-more]').forEach(b=>b.onclick=e=>{e.stopPropagation();openDayPopover(b.dataset.more,b)})}
function renderWeek(){const start=DateUtil.weekStart(state.focus,state.settings.weekStart),dates=Array.from({length:7},(_,i)=>DateUtil.addDays(start,i)),all=instances(start,DateUtil.addDays(start,6)),hours=CONFIG.hourEnd-CONFIG.hourStart;let html=`<div class="week-view"><div class="week-head"><div></div>${dates.map(k=>{const q=DateUtil.parse(k);return `<div class="week-head-day"><span class="wd">${CONFIG.weekdays[DateUtil.weekday(q.y,q.m,q.d)].toUpperCase()}</span><span class="dn ${k===todayKey()?'today':''}">${q.d}</span></div>`}).join('')}</div><div class="all-day-row"><div class="all-label">all-day</div>${dates.map(k=>`<div class="all-day-cell" data-day="${k}">${all.filter(e=>e.date===k&&e.allDay).map(e=>`<button class="event-chip" draggable="true" data-id="${e.id}" style="${eventStyle(e)}">${esc(e.title)}</button>`).join('')}</div>`).join('')}</div><div class="week-scroller"><div class="week-body" style="--hours:${hours}"><div class="hour-labels">${Array.from({length:hours},(_,i)=>`<div class="hour-label">${formatHour(CONFIG.hourStart+i)}</div>`).join('')}</div>${dates.map(k=>`<div class="day-track" data-day="${k}"></div>`).join('')}</div><div id="weekEvents"></div></div></div>`;surface.innerHTML=html;const body=surface.querySelector('.week-body'),tracks=[...body.querySelectorAll('.day-track')],boxH=hours*CONFIG.hoursPx;tracks.forEach((track,index)=>{track.ondragover=e=>e.preventDefault();track.ondrop=e=>{e.preventDefault();const data=JSON.parse(e.dataTransfer.getData('text/plain')||'{}'),id=data.id;if(!id)return;const rect=track.getBoundingClientRect(),scroll=surface.querySelector('.week-scroller').scrollTop;let mins=CONFIG.hourStart*60+((e.clientY-rect.top+scroll)/CONFIG.hoursPx)*60;mins=Math.round(mins/CONFIG.snapMinutes)*CONFIG.snapMinutes;const hh=Math.max(0,Math.min(23,Math.floor(mins/60))),mm=((mins%60)+60)%60;reschedule(id,dates[index],`${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`)}});
 const nonAll=all.filter(e=>!e.allDay);for(const ev of nonAll){const idx=dates.indexOf(ev.date);if(idx<0)continue;const startM=DateUtil.timeMinutes(ev.start||'09:00'),endM=DateUtil.timeMinutes(ev.end||'10:00'),base=CONFIG.hourStart*60,top=Math.max(0,(startM-base)/60*CONFIG.hoursPx),bottom=Math.min(boxH,(endM-base)/60*CONFIG.hoursPx),height=Math.max(18,bottom-top);const el=document.createElement('button');el.className='time-event';el.draggable=true;el.dataset.id=ev.id;el.style.cssText=`${eventStyle(ev)};top:${top}px;height:${height}px`;el.innerHTML=`<strong>${esc(ev.title)}</strong><span>${esc(ev.start||'')} – ${esc(ev.end||'')}</span>`;el.onclick=()=>openEditorByInstance(ev.id);el.ondragstart=e=>{e.dataTransfer.setData('text/plain',JSON.stringify({id:ev.id}));e.dataTransfer.effectAllowed='move'};tracks[idx].appendChild(el)}
 surface.querySelectorAll('.all-day-cell').forEach(cell=>{cell.ondragover=e=>e.preventDefault();cell.ondrop=e=>{e.preventDefault();const dat=JSON.parse(e.dataTransfer.getData('text/plain')||'{}');if(dat.id)reschedule(dat.id,cell.dataset.day)}});surface.querySelectorAll('.event-chip').forEach(b=>{b.onclick=e=>{e.stopPropagation();openEditorByInstance(b.dataset.id)};b.ondragstart=e=>e.dataTransfer.setData('text/plain',JSON.stringify({id:b.dataset.id}))});surface.querySelectorAll('.day-track').forEach(t=>t.onclick=e=>{if(e.target.closest('.time-event'))return;const rect=t.getBoundingClientRect(),sc=surface.querySelector('.week-scroller').scrollTop;let mins=CONFIG.hourStart*60+(e.clientY-rect.top+sc)/CONFIG.hoursPx*60;mins=Math.round(mins/CONFIG.snapMinutes)*CONFIG.snapMinutes;const hh=Math.floor(mins/60),mm=mins%60;openEditor(null,t.dataset.day,`${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`)});
 if(todayKey()>=start&&todayKey()<=DateUtil.addDays(start,6)){const now=new Date(),minutes=now.getHours()*60+now.getMinutes(),top=(minutes-CONFIG.hourStart*60)/60*CONFIG.hoursPx;if(top>=0&&top<=boxH){const line=document.createElement('div');line.className='now-line';line.style.top=`${60+37+top}px`; // header and all-day strip in overall view; adjusted relative to week view
 surface.querySelector('.week-view').appendChild(line);line.style.top=`114px`;line.style.transform=`translateY(${top}px)`;}}
}
function formatHour(h){const n=h%12||12;return `${n}${h<12?' AM':' PM'}`}
function renderAgenda(){const from=todayKey(),to=DateUtil.addDays(from,59),list=instances(from,to),groups=new Map();for(const ev of list){if(!groups.has(ev.date))groups.set(ev.date,[]);groups.get(ev.date).push(ev)}let html=`<div class="agenda-view"><div class="agenda-title">Upcoming · ${DateUtil.formatKey(from,{month:'short',day:'numeric'})} – ${DateUtil.formatKey(to,{month:'short',day:'numeric'})}</div>`;if(!groups.size)html+=`<div class="empty-range">Nothing on the calendar yet. Add an event to get started.</div>`;for(const [date,events] of groups){const p=DateUtil.parse(date),dow=DateUtil.weekday(p.y,p.m,p.d);html+=`<div class="agenda-day"><div class="agenda-date">${CONFIG.weekdays[dow]}, ${DateUtil.formatKey(date,{month:'short',day:'numeric'})}${date===todayKey()?'<small>Today</small>':''}</div><div class="agenda-events">${events.map(ev=>`<button class="agenda-item" data-id="${ev.id}" style="${eventStyle(ev)}"><span class="agenda-time">${ev.allDay?'All day':`${esc(ev.start)} – ${esc(ev.end)}`}</span><span class="agenda-name">${esc(ev.title)}</span></button>`).join('')}</div></div>`}html+='</div>';surface.innerHTML=html;surface.querySelectorAll('.agenda-item').forEach(b=>b.onclick=()=>openEditorByInstance(b.dataset.id))}
/* ============================== Event editor / recurrence scope ============================== */
function openEditor(ev,date=todayKey(),time='09:00'){editing=ev?{...ev}:null;const d=ev?.date||date,isAll=!!ev?.allDay;const recurring=ev?.recurring;$('#modalHost').innerHTML=`<div class="modal-backdrop" id="editorBackdrop"><div class="modal" role="dialog" aria-modal="true"><div class="modal-head"><h2>${ev?'Edit event':'New event'}</h2><button class="close-btn" id="closeModal">×</button></div><form id="eventForm"><div class="form-grid"><div class="field full"><label for="eventTitle">Title</label><input id="eventTitle" maxlength="100" required placeholder="Add a title" value="${esc(ev?.title||'')}"></div><div class="field"><label for="eventDate">Date</label><input id="eventDate" type="date" required value="${d}"></div><div class="field"><label>Color</label><div class="palette">${CONFIG.colors.map(c=>`<button type="button" class="color-dot ${(ev?.color||'blue')===c.id?'selected':''}" data-color="${c.id}" style="--c:${c.hex}" aria-label="${c.id}"></button>`).join('')}</div></div><div class="field" id="startField"><label for="eventStart">Starts</label><input id="eventStart" type="time" value="${ev?.start||time}"></div><div class="field" id="endField"><label for="eventEnd">Ends</label><input id="eventEnd" type="time" value="${ev?.end||'10:00'}"></div><div class="field full"><label class="check-row"><input id="allDay" type="checkbox" ${isAll?'checked':''}> All day</label></div><div class="field full"><label for="eventRepeat">Does not repeat</label><select id="eventRepeat"><option value="none">Does not repeat</option><option value="daily">Daily</option><option value="weekly">Weekly</option><option value="monthly">Monthly (same date)</option><option value="yearly">Yearly</option></select><div class="recurrence-note hide" id="repeatNote">Monthly occurrences only appear in months that contain this day.</div></div><div class="field full hide" id="weekdayField"><label>Repeat on</label><div class="weekday-picks" id="weekdayPicks"></div></div><div class="field full"><label for="eventNotes">Notes <span style="font-weight:400;color:#8a8f94">· optional</span></label><textarea id="eventNotes" maxlength="1000" placeholder="Add notes">${esc(ev?.notes||'')}</textarea></div><div class="field full"><div id="formError" class="error"></div></div></div><div class="modal-actions">${ev?'<button type="button" class="text-btn delete-btn" id="deleteEvent">Delete</button>':''}<span class="push"></span><button type="button" class="text-btn" id="cancelModal">Cancel</button><button class="primary-btn" type="submit">Save</button></div></form></div></div>`;
 const form=$('#eventForm');let chosenColor=ev?.color||'blue';$('.palette').querySelectorAll('.color-dot').forEach(b=>b.onclick=()=>{chosenColor=b.dataset.color;$('.color-dot.selected')?.classList.remove('selected');b.classList.add('selected')});const rep=$('#eventRepeat');if(ev?.recurring)rep.value=ev.recurrence?.freq||'none';else if(ev?.masterId){const master=state.events.find(x=>x.id===ev.masterId);rep.value=master?.recurrence?.freq||'none'}
 const weekdaySet=new Set(ev?.recurrence?.weekdays||[DateUtil.weekday(...Object.values(DateUtil.parse(d)))]);const renderPicks=()=>{$('#weekdayPicks').innerHTML=CONFIG.weekdaysShort.map((x,i)=>`<button type="button" class="weekday-pick ${weekdaySet.has(i)?'selected':''}" data-wd="${i}">${x}</button>`).join('');$('#weekdayPicks').querySelectorAll('button').forEach(b=>b.onclick=()=>{const n=+b.dataset.wd;if(weekdaySet.has(n))weekdaySet.delete(n);else weekdaySet.add(n);b.classList.toggle('selected',weekdaySet.has(n))})};renderPicks();const updateRepeat=()=>{$('#weekdayField').classList.toggle('hide',rep.value!=='weekly');$('#repeatNote').classList.toggle('hide',rep.value!=='monthly')};rep.onchange=updateRepeat;updateRepeat();$('#allDay').onchange=()=>{const hide=$('#allDay').checked;$('#startField').classList.toggle('hide',hide);$('#endField').classList.toggle('hide',hide)};$('#allDay').dispatchEvent(new Event('change'));const close=()=>$('#modalHost').innerHTML='';$('#closeModal').onclick=close;$('#cancelModal').onclick=close;$('#editorBackdrop').onclick=e=>{if(e.target.id==='editorBackdrop')close()};$('#deleteEvent')?.addEventListener('click',()=>{if(ev.recurring||ev.masterId)chooseScope('delete',ev,null);else deleteSingle(ev.id)});
 form.onsubmit=e=>{e.preventDefault();const title=$('#eventTitle').value.trim(),date=$('#eventDate').value,start=$('#eventStart').value,end=$('#eventEnd').value,allDay=$('#allDay').checked;if(!title){$('#formError').textContent='Please enter an event title.';return}if(!DateUtil.valid(...(()=>{const q=DateUtil.parse(date);return[q.y,q.m,q.d]})())){$('#formError').textContent='Choose a valid date.';return}if(!allDay&&(!start||!end||DateUtil.timeMinutes(end)<=DateUtil.timeMinutes(start))){$('#formError').textContent='End time must be later than the start time.';return}if(rep.value==='weekly'&&!weekdaySet.size){$('#formError').textContent='Choose at least one weekday.';return}const payload={title,date,start:allDay?'':start,end:allDay?'':end,allDay,color:chosenColor,notes:$('#eventNotes').value.trim(),recurrence:rep.value==='none'?null:{freq:rep.value,...(rep.value==='weekly'?{weekdays:[...weekdaySet].sort()}: {})},exceptions:{}};if(ev&&(ev.recurring||ev.masterId))chooseScope('save',ev,payload);else saveEvent(ev,payload)}
}
function openEditorByInstance(instanceId){const [masterId,date]=instanceId.split('@');const master=state.events.find(e=>e.id===masterId);if(!master)return;const ev=DateUtil.expand(master,date,date)[0];if(ev)openEditor(ev,date)}
function saveEvent(old,payload){if(old){const id=old.masterId||old.id;const master=state.events.find(e=>e.id===id);if(!master)return;Object.assign(master,payload,{id:master.id});}else state.events.push({...payload,id:crypto.randomUUID?crypto.randomUUID():`ev-${Date.now()}-${Math.random()}`});save();$('#modalHost').innerHTML='';render();toast(old?'Event updated':'Event created')}
function chooseScope(action,ev,payload){const id=ev.masterId||ev.id,date=ev.date;$('#modalHost').innerHTML=`<div class="modal-backdrop"><div class="modal small"><div class="modal-head"><h2>${action==='save'?'Edit':'Delete'} recurring event</h2><button class="close-btn" id="scopeClose">×</button></div><p style="color:#62686e;margin:0 0 14px;font-size:13px">${action==='save'?'Which events in the series should be changed?':'Which events in the series should be deleted?'}</p><div class="scope-list"><button class="scope-choice" data-scope="only"><strong>This event only</strong><span>Only ${DateUtil.formatKey(date,{month:'long',day:'numeric'})}</span></button><button class="scope-choice" data-scope="following"><strong>This and following</strong><span>This date and future events in the series</span></button><button class="scope-choice" data-scope="all"><strong>Entire series</strong><span>Every event in this series</span></button></div><div class="modal-actions"><span class="push"></span><button class="text-btn" id="scopeCancel">Cancel</button></div></div></div>`;const close=()=>{$('#modalHost').innerHTML=''};$('#scopeClose').onclick=close;$('#scopeCancel').onclick=close;document.querySelectorAll('.scope-choice').forEach(b=>b.onclick=()=>{const scope=b.dataset.scope;close();if(action==='save')applyRecurringEdit(id,date,scope,payload);else applyRecurringDelete(id,date,scope)})}
function applyRecurringEdit(id,date,scope,payload){const master=state.events.find(e=>e.id===id);if(!master)return;if(scope==='all'){const keep={id:master.id,exceptions:master.exceptions||{}};Object.assign(master,payload,keep);}
 else if(scope==='only'){master.exceptions=master.exceptions||{};master.exceptions[date]={...(master.exceptions[date]||{}),cancelled:false,override:{...payload,date,recurrence:null,exceptions:undefined}}}
 else{const prev=DateUtil.addDays(date,-1);const original={...master,exceptions:{...(master.exceptions||{})}};original.recurrence={...original.recurrence,until:prev};master.recurrence=original.recurrence;const newMaster={...payload,id:`ev-${Date.now()}-${Math.random().toString(36).slice(2)}`,date,exceptions:{}};if(payload.recurrence)newMaster.recurrence={...payload.recurrence};else newMaster.recurrence=null;state.events.push(newMaster)}save();$('#modalHost').innerHTML='';render();toast('Recurring event updated')}
function applyRecurringDelete(id,date,scope){const master=state.events.find(e=>e.id===id);if(!master)return;if(scope==='all')state.events=state.events.filter(e=>e.id!==id);else if(scope==='only'){master.exceptions=master.exceptions||{};master.exceptions[date]={...(master.exceptions[date]||{}),cancelled:true}}else master.recurrence={...master.recurrence,until:DateUtil.addDays(date,-1)};save();render();toast('Event deleted')}
function deleteSingle(id){const before=state.events.find(e=>e.id===id);if(!before)return;const idx=state.events.indexOf(before);state.events.splice(idx,1);save();$('#modalHost').innerHTML='';render();showUndo('Event deleted',()=>{state.events.splice(idx,0,before);save();render()})}
function reschedule(instanceId,newDate,newTime){const [id,date]=instanceId.split('@'),master=state.events.find(e=>e.id===id);if(!master)return;if(master.recurrence){master.exceptions=master.exceptions||{};let old=master.exceptions[date]?.override||{};master.exceptions[date]={...(master.exceptions[date]||{}),cancelled:false,override:{...old,date:newDate,...(newTime?{start:newTime,end:DateUtil.timeMinutes(old.end||master.end)>DateUtil.timeMinutes(old.start||master.start)?addMinutesClock(newTime,DateUtil.timeMinutes(old.end||master.end)-DateUtil.timeMinutes(old.start||master.start)):addMinutesClock(newTime,60)}:{})}}}else{master.date=newDate;if(newTime){const duration=DateUtil.timeMinutes(master.end)-DateUtil.timeMinutes(master.start);master.start=newTime;master.end=addMinutesClock(newTime,duration)}}save();render();toast('Event rescheduled')}
function addMinutesClock(t,delta){let mins=(DateUtil.timeMinutes(t)+delta)%(24*60);if(mins<0)mins+=1440;return `${String(Math.floor(mins/60)).padStart(2,'0')}:${String(mins%60).padStart(2,'0')}`}
/* ============================== Day overflow, settings, undo ============================== */
function openDayPopover(date,anchor){const list=instances(date,date);const rect=anchor.getBoundingClientRect();$('#popoverHost').innerHTML=`<div class="popover" style="left:${Math.max(8,Math.min(innerWidth-300,rect.left))}px;top:${Math.min(innerHeight-300,rect.bottom+5)}px"><div class="popover-head"><span>${DateUtil.formatKey(date,{weekday:'long',month:'long',day:'numeric'})}</span><button class="close-btn" id="popClose">×</button></div>${list.map(e=>`<button class="popover-event" data-id="${e.id}" style="${eventStyle(e)}">${esc(e.title)}<small>${e.allDay?'All day':`${esc(e.start)} – ${esc(e.end)}`}</small></button>`).join('')||'<div class="side-note">No events</div>'}<button class="text-btn" id="popAdd">+ Add event</button></div>`;$('#popClose').onclick=()=>$('#popoverHost').innerHTML='';$('#popAdd').onclick=()=>{ $('#popoverHost').innerHTML='';openEditor(null,date)};document.querySelectorAll('.popover-event').forEach(b=>b.onclick=()=>{const id=b.dataset.id;$('#popoverHost').innerHTML='';openEditorByInstance(id)})}
$('#settingsBtn').onclick=()=>{const host=$('#settingsHost');if(host.innerHTML){host.innerHTML='';return}host.innerHTML=`<div class="settings-panel"><h3>Calendar settings</h3><div class="setting-row"><span>Week starts on</span><select id="weekStartSelect"><option value="1">Monday</option><option value="0">Sunday</option></select></div><div class="setting-row"><span>Time grid</span><span>${CONFIG.hourStart}:00 – ${CONFIG.hourEnd}:00</span></div></div>`;$('#weekStartSelect').value=state.settings.weekStart;$('#weekStartSelect').onchange=e=>{state.settings.weekStart=+e.target.value;save();render()};setTimeout(()=>document.addEventListener('click',function outside(e){if(!e.target.closest('.settings-panel,#settingsBtn')){$('#settingsHost').innerHTML='';document.removeEventListener('click',outside)}}, {once:true}),0)};
function toast(msg){$('#toastHost').innerHTML=`<div class="toast">${esc(msg)}<button id="toastDismiss">Dismiss</button></div>`;$('#toastDismiss').onclick=()=>$('#toastHost').innerHTML='';clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('#toastHost').innerHTML='',3500)}function showUndo(msg,undo){undoAction=undo;$('#toastHost').innerHTML=`<div class="toast">${esc(msg)}<button id="undoBtn">Undo</button></div>`;$('#undoBtn').onclick=()=>{undoAction?.();undoAction=null;$('#toastHost').innerHTML=''};setTimeout(()=>{if($('#undoBtn'))$('#toastHost').innerHTML=''},5000)}
/* ============================== Navigation & startup ============================== */
$('#prevBtn').onclick=()=>navigate(-1);$('#nextBtn').onclick=()=>navigate(1);$('#todayBtn').onclick=()=>{const t=todayKey();selectedDate=t;state.focus=DateUtil.key(...Object.values(DateUtil.parse(t)));save();render()};$('#createBtn').onclick=()=>openEditor(null,selectedDate);document.querySelectorAll('.view-btn').forEach(b=>b.onclick=()=>{transition(()=>{state.view=b.dataset.view;save();render()})});document.addEventListener('keydown',e=>{if(e.key==='Escape'){if($('#modalHost').innerHTML)$('#modalHost').innerHTML='';if($('#popoverHost').innerHTML)$('#popoverHost').innerHTML=''}if(e.key==='ArrowLeft'&&!$('#modalHost').innerHTML)navigate(-1);if(e.key==='ArrowRight'&&!$('#modalHost').innerHTML)navigate(1)});render();
</script></body></html>