Dalumat.aiDalumat.ai
Back to all resources
Thumbnail for video 2: Is Your Calendar Still Pre-AI? Build This Instead (Using Claude Cowork)

Video 2

Is Your Calendar Still Pre-AI? Build This Instead (Using Claude Cowork)

Build a calendar that fills itself in: it reads your syllabi, watches your email, flags your hardest week before it hits, and every morning tells you what matters most.

# Master Calendar — paste this into Claude Cowork (v2)

This builds the calendar from the video: one table that reads across your whole life, rendered as an interactive dashboard you can talk to. **v2 upgrade:** every "smart" view — Coach, Next 7, and Load — is now written by Claude and cached in the table, and Flags update the moment you say something's done. No view does its own guessing in the browser anymore.

**Before you paste this, you need three things from the last video:**

1. Your **Life HQ** folder, with a subfolder per class and per part of your life, connected to this Cowork project.
2. Your **Gmail** connected.
3. Your **Google Calendar** connected.

If you don't have those yet, go watch the setup video first. This won't work without them.

**How to use:** copy this entire file, paste it into a Claude Cowork chat that's connected to your Life HQ folder, and send it. Claude does the rest. After it's built, you don't edit anything by hand. You just talk to it.

---

You are building my master calendar. The guiding principle is simple: **`events.json` is the single source of truth.** It holds the raw rows. Every view is just a drawing of that table — and the "smart" views (Coach, Next 7, Load, Flags) are *cached tables you write from the rows*, so they give real feedback, render instantly, and update the moment I tell you something changed. Do it in the steps below, and do not skip the table.

## Step 0 — Confirm my connections first

Before anything else, make sure this chat can actually see my three sources, and confirm each by reading from it:

1. **Life HQ folder** — confirm it's connected and you can list my course and personal subfolders.
2. **Gmail** — confirm you can search my mail.
3. **Google Calendar** — confirm you can read my events.

**If Google Calendar is not connected, stop and tell me before doing anything else.** It matters: the calendar is much weaker without it, because it's how you catch the things that never touch a syllabus or an email — a one-off meeting, an appointment, an event someone added straight to my calendar. Tell me to connect it (in Claude's connectors/settings, add **Google Calendar** and authorize it), and wait for me to confirm it's connected before you continue. Do the same if Gmail or the Life HQ folder isn't readable. Only once all three are connected and you've read from each, move on to Step 1.

If I explicitly tell you to proceed without Google Calendar, that's fine — build from the folder and Gmail, and note in the finished calendar that Calendar wasn't connected so I know some things (like calendar-only events) may be missing.

## Step 1 — Read everything

Read across every subfolder inside my connected Life HQ folder. That's all my course syllabi and all my personal notes. Then read my connected Gmail and my Google Calendar. Pull out every single thing that has a date or a deadline.

Look hard. Some dates are buried in paragraphs, like a seminar response paper "due the night before each session." Some are in tables. Some are recurring, like a class that meets every Tuesday and Thursday, a workout, or rent. Catch the things in my email that no syllabus knows about, like a professor moving a midterm or a club meeting.

## Step 2 — Build one events table

Put every dated item into a single table, and save it as `events.json` in the root of my Life HQ folder. Use exactly these fields on every row:

- `id` — a short unique slug
- `title` — what it is
- `date` — "YYYY-MM-DD", or null if there's no date yet
- `time` — "HH:MM" in 24-hour time, or null if it's all-day
- `end_date` — for a span, or the last date of a recurring item; otherwise null
- `recurrence` — null for one-off items, or a rule for repeating ones. Use "WEEKLY;BYDAY=MO,WE,FR" style codes (MO TU WE TH FR SA SU), or "MONTHLY;BYMONTHDAY=1"
- `type` — one of: deadline, exam, paper, problem set, class, work, personal, chore
- `source` — which course or folder it came from (e.g. "Regression", "Workout")
- `category` — one of: school, work, life
- `weight` — low, medium, or high effort
- `provenance` — where it came from: syllabus, email, calendar, note, or you
- `flag` — null, or one of: changed, unscheduled, assumed
- `notes` — anything useful, including the reason for any flag
- `status` — upcoming, done, or unscheduled

Follow these rules:

- **Weight is about effort, not importance.** An exam, a final paper, a big project is high. A normal problem set or response paper is medium. A class meeting, a workout, a chore is low. Weight powers the "how brutal is this week" view, so be honest about it.
- **Recurring things are ONE row with a recurrence rule and an end_date, not fifty rows.** Classes, weekly labs, workouts, weekly groceries, monthly rent.
- **If two sources disagree, the newer and more direct one wins.** An email from a TA moving a midterm beats the date printed in the syllabus. Use the new date, set provenance to "email", set flag to "changed", and explain what moved in notes.
- **If a deadline is vague**, like "end of term" or "finals week", make your best call, set flag to "assumed", and explain the guess in notes. Don't guess silently.
- **If something has no date yet**, like a presentation slot that gets assigned later, keep it. Set date to null, flag to "unscheduled", status to "unscheduled", and note what you know. Never drop it.
- **Mark genuinely past items `status: "done"` instead of deleting them**, so the calendar carries a running history across terms.
- **Tag every row's provenance** so I can trust the calendar and see where each thing came from.
- **Record holidays and no-class dates** (a campus holiday, Thanksgiving, Veterans Day) so recurring classes skip them. You'll use these in the exceptions object in Step 4.
- **Add relationship nudges.** If I keep a list of people I want to stay in touch with (a mentor, a recruiter, a contact from an event) anywhere in my folders, create gentle recurring reminders to reach out to them, like "Coffee with [name] (keep in touch)" once a month. Make them low weight, category work, type personal. The point is to keep my relationships warm before I need them, which matters for landing analyst, consulting, and research roles. If I don't have such a list yet, add one soft monthly nudge to reach out to someone in my network, and tell me I can name specific people anytime.
- **Also output a `touchpoints` array** for the people and habits I want to keep warm. Each has a name, a `what` (office hours, coffee, a catch-up), a `last` date (the last time I did it), an `every` number (days between touches), and an optional `why`. The calendar uses these to nudge me when a relationship has gone stale.

## Step 3 — Write the cached "smart view" tables

This is the important upgrade. The Coach, the Next-7 read, and the Load read are **not** computed by the browser with hardcoded rules — a rule can only do dumb relative-max math and will call an empty summer week your "heaviest stretch." Instead, **you (the LLM) write these entries from the events table, using judgment, and cache them inside `events.json`** so the artifact renders them instantly by lookup. Rebuild all of them for every week from the current week's Monday through the end of the term, keyed by that week's Monday (ISO "YYYY-MM-DD"). Store them under these keys:

- **`coach`** — `{ "<weekMonday>": [ {"cls": "plan" or "rel", "tag": "<2–4 word label>", "body": "<1–2 sentences, may use <b>…</b>>"} ] }`. For each week, look at that week **and the next ~6 weeks**, then write the advice a good human coach would give:
  - If the week and the near horizon are genuinely light (e.g. only a habit or a check-in), say so plainly — calm, runway, build the habit. **Never call a lone check-in a "crunch."**
  - If a real crunch is coming within ~6 weeks, name it, and name the single **moveable** thing (a paper, problem set, project, or application) to start early to flatten it. Exams are locked in — advise guarding the week before and keeping it clear to study.
  - Use "rel" cards for relationship nudges only where genuinely useful (e.g. "ask Prof. Okafor for the fellowship reference, well before the deadline"), not every week.
- **`agenda`** — `{ "<weekMonday>": "<one-sentence read of that literal next-7-day stretch>" }`. A punchy summary of what's actually due that week and the vibe (a true light week / a real exam week / three deliverables stacked, etc.). This shows above the accurate ranked list on the Next 7 tab.
- **`load`** — `{ "summary": "<2–4 sentence narrative of the term's overall load shape — where the real peaks are, and the 1–2 things to pull earlier to flatten them>" }`. This shows above the computed heatmap on the Load tab.

To judge load, use effort points as an **input to your judgment, not the final word**: high = 6, medium = 3, low = 1 per item, classes excluded. A week is only a "crunch" if it genuinely holds high-effort work — not just because it's the max of an otherwise empty stretch.

## Step 4 — Render the dashboard

Create a Cowork artifact with the id `master-calendar`, using the HTML template at the bottom of this file, exactly as written. Make only these replacements, and change nothing else:

- Replace `/*DATA*/` with the events object: `{"term":"Fall 2026","events":[ ... every row ... ],"touchpoints":[ ... ]}`. Use a short term label. (If you're carrying past terms as history, a label like `"Fall 2026 (+ history)"` is fine.)
- Replace `/*EXCEPTIONS*/` with an object that maps a recurring row's `id` to the dates it should skip, like `{"micro-lecture":["2026-11-11"],"seminar":["2026-11-11"]}`. If there are none, use `{}`.
- Replace `/*COACH*/` with the `coach` table you wrote in Step 3.
- Replace `/*AGENDA*/` with the `agenda` table.
- Replace `/*LOAD*/` with the `load` object.

The template gives me a Month, Week, and Day toggle, a **Next 7** agenda (your written "read" on top of the ranked-by-effort list), a weekly **Load** heatmap (your written narrative on top of the computed bars), filters for course, type, and category, a "Today" date control, a **Flags & provenance** panel that shows where each thing came from and anything you changed, assumed, or left unscheduled — **and that drops any row the moment it's marked `done`** — and a **Coach** view that is a pure lookup into the `coach` table for whatever week is active.

## Step 5 — Keep it live (talk to it; never hand-edit)

From here on, this calendar is a snapshot I refresh by talking to you. Not by hand-editing.

- When I tell you something changed or something new came up — "I've got a group meeting Thursday at 3 in the library," "I work Saturdays and Sundays starting next week," "the econ final moved to the 9th" — you parse it, update the row in `events.json` (add it, change it, set provenance to "you"), rewrite any affected cached tables from Step 3, and re-render.
- **When I tell you something is finished or no longer needed** — "I updated my resume," "sent the fellowship app" — set that row's `status` to `"done"` and clear its `flag` (null). Because Flags & Provenance is derived live from the rows, that item immediately drops off the panel. If I give a date to an unscheduled item, set the date and status to "upcoming" so it moves from Flags onto the calendar.
- **When I say I did a touchpoint** — "went to office hours today," "grabbed coffee with my mentor" — update that touchpoint's `last` date so it stops nudging me.
- When I ask a question — "what've I got this week?", "when's my next exam?", "which week is the worst?" — answer in plain words straight from `events.json`. Read the table. Don't guess.

Be a coach, not just a calendar. Always treat `events.json` as the one source of truth, and never let the calendar — or any cached view table — drift from it.

## Step 6 — Put it on autopilot (a recurring briefing)

Set up a scheduled task so the calendar keeps itself current and checks in with me, instead of waiting for me to open it.

First, ask me whether I want this every morning or once a week, and what time (default to every weekday morning at 7am, kept short). Then create a scheduled task that, on that cadence, does all of this on its own:

1. Re-reads my Life HQ folders, my Gmail, and my Google Calendar, and updates `events.json` — reconciling conflicts so the newer source wins, keeping recurring things as single rules, tagging provenance, flagging vague or undated items, applying any "I finished X" updates I mentioned, and marking past items done.
2. **Regenerates the `coach`, `agenda`, and `load` cached tables** with fresh judgment (the same LLM pass as Step 3).
3. Re-renders the Master Calendar artifact so it is up to date (the five replacements from Step 4).
4. Sends me a short briefing in plain English:
   - What I've got this week (pull from the `agenda` entry you wrote).
   - The heaviest week coming up, and what I can start right now to flatten it (pull from the `coach` entry).
   - Anyone I'm overdue to reach out to (a stale touchpoint).
   - Anything that changed since the last briefing, like a moved deadline, a new item, or something I marked done.

Keep the briefing short and skimmable, a few lines, not a wall of text.

One thing to tell me: a scheduled task only runs while my desktop app is open and my computer is awake, so it fires the next time both are true.

## Each new term (keep it going)

This calendar is built once and it carries from one term into the next. It does not expire.

When a new semester starts, I'll drop my new syllabi into their course folders and say something like "new semester, sweep my Life HQ folder again and rebuild my calendar." When I do:

- Re-read every folder, plus Gmail and Google Calendar, from scratch, exactly the way you did the first time.
- Rebuild `events.json` with the new term's items, and rebuild the `coach` / `agenda` / `load` tables for the new weeks. Keep the old items (marked `done`) if I want a running history of my whole degree, or clear them if I only want the current term. If it isn't obvious which I want, ask.
- Re-render the artifact.

I never edit this file or the calendar by hand, and I never touch a date in the template. I can start it on any day, in any term. The calendar centers itself on today automatically.

## The template

Paste-and-go. Do not edit it except for the five replacements in Step 4.

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Master Calendar</title>
<style>
:root{ color-scheme: light;
  --cream:#F6F4EE; --card:#FFFFFF; --card2:#FBF9F3; --line:#E8E3D8; --line2:#F0ECE2;
  --ink:#211F1B; --ink2:#524E46; --mut:#8A847A;
  --clay:#D97757; --clay-deep:#BE5C3D; --clay-soft:#F3E2D8;
  --school:#3F6A86; --work:#7C5E3B; --life:#5E7A57;
  --hi:#BE4A30; --med:#C2873B; --lo:#B5AE9F;
  --hibg:#F8EAE2; --medbg:#F8F0DF; --lobg:#FAF8F1;
  --shadow:0 1px 2px rgba(33,31,27,.04), 0 10px 26px -16px rgba(33,31,27,.18);
  --serif:"Tiempos Text",Georgia,"Times New Roman",serif;
  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--cream);color:var(--ink);font:14.5px/1.5 var(--sans);-webkit-font-smoothing:antialiased}
.wrap{max-width:1200px;margin:0 auto;padding:22px 22px 40px}
/* header */
.topbar{display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end;justify-content:space-between;margin-bottom:18px}
.brand{display:flex;gap:12px;align-items:center}
.mark{display:flex;align-items:center;justify-content:center;width:38px;height:38px;border-radius:11px;background:var(--clay-soft);flex:none}
h1{font-family:var(--serif);font-size:25px;font-weight:600;letter-spacing:-.01em;margin:0;line-height:1}
.sub{color:var(--mut);font-size:12px;margin-top:5px;letter-spacing:.01em}
.tabs{display:flex;gap:3px;background:var(--card);border:1px solid var(--line);border-radius:12px;padding:4px;box-shadow:var(--shadow)}
.tab{border:0;background:transparent;color:var(--ink2);padding:7px 14px;border-radius:9px;cursor:pointer;font-size:13px;font-weight:600;font-family:var(--sans);transition:all .15s}
.tab:hover{background:var(--card2);color:var(--ink)}
.tab.active{background:var(--clay);color:#fff;box-shadow:0 1px 2px rgba(190,92,61,.35)}
/* controls */
.controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:11px 14px;margin-bottom:16px;box-shadow:var(--shadow)}
.controls label{font-size:11px;color:var(--mut);font-weight:700;text-transform:uppercase;letter-spacing:.06em}
select,input[type=date]{font:inherit;font-size:13px;padding:6px 9px;border:1px solid var(--line);border-radius:9px;background:var(--card2);color:var(--ink);font-weight:500}
select:hover,input[type=date]:hover{border-color:var(--clay)}
.divider{width:1px;height:22px;background:var(--line)}
.chip{border:1px solid var(--line);background:var(--card2);border-radius:999px;padding:5px 13px;font-size:12px;cursor:pointer;font-weight:600;color:var(--mut);transition:all .15s}
.chip.on{color:#fff;border-color:transparent}
.chip.school.on{background:var(--school)} .chip.work.on{background:var(--work)} .chip.life.on{background:var(--life)}
.toggle{font-size:12.5px;color:var(--ink2);display:flex;align-items:center;gap:6px;cursor:pointer;font-weight:600}
.toggle input{accent-color:var(--clay)}
/* layout */
.layout{display:grid;grid-template-columns:1fr 296px;gap:16px}
@media(max-width:900px){.layout{grid-template-columns:1fr}}
.view{min-height:360px;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:18px;box-shadow:var(--shadow)}
.flags{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:16px;height:fit-content;position:sticky;top:14px;box-shadow:var(--shadow)}
.flags h3{margin:0 0 4px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);font-weight:800}
.flagcard{border:1px solid var(--line2);background:var(--card2);border-radius:11px;padding:10px 11px;margin:9px 0;font-size:12.5px}
.flagcard .ttl{font-weight:700;display:flex;align-items:center;gap:7px;color:var(--ink)}
.flagcard.changed{border-left:3px solid var(--med)}
.flagcard.unsched{border-left:3px solid var(--hi)}
.flagcard.assumed{border-left:3px solid var(--school)}
.flagcard .meta{color:var(--mut);margin-top:4px;line-height:1.45}
.tagrow{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:6px}
.badge{font-size:9.5px;font-weight:800;border-radius:5px;padding:2px 6px;letter-spacing:.05em}
.b-syllabus{background:#E3EBF1;color:#355A73}
.b-email{background:#F1E4EE;color:#7A4A6B}
.b-calendar{background:#DDEDE9;color:#2E6657}
.b-note{background:#E6EEDF;color:#4B6340}
.b-you{background:var(--clay-soft);color:var(--clay-deep)}
/* month */
.calhead{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.calhead .mname{font-family:var(--serif);font-weight:600;font-size:18px;letter-spacing:-.01em}
.navbtn{border:1px solid var(--line);background:var(--card2);border-radius:9px;width:32px;height:30px;cursor:pointer;font-weight:700;color:var(--ink2);font-size:15px;transition:all .15s}
.navbtn:hover{border-color:var(--clay);color:var(--clay)}
.grid7{display:grid;grid-template-columns:repeat(7,1fr);gap:6px}
.dow{font-size:10.5px;color:var(--mut);text-align:center;font-weight:800;text-transform:uppercase;letter-spacing:.06em;padding:2px 0 6px}
.cell{border:1px solid var(--line2);border-radius:11px;min-height:104px;padding:6px;background:var(--card);overflow:hidden;transition:border-color .15s}
.cell:hover{border-color:var(--line)}
.cell.out{background:var(--card2);opacity:.5}
.cell.today{border-color:var(--clay);box-shadow:inset 0 0 0 1px var(--clay)}
.daynum{font-size:11px;color:var(--mut);font-weight:700;padding:1px 2px}
.cell.today .daynum{color:var(--clay-deep)}
.ev{font-size:11px;border-radius:6px;padding:2px 6px;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-left:3px solid var(--lo);background:var(--lobg);cursor:default;font-weight:500}
.ev.school{border-left-color:var(--school)} .ev.work{border-left-color:var(--work)} .ev.life{border-left-color:var(--life)}
.ev.hi{background:var(--hibg)} .ev.med{background:var(--medbg)}
.ev .dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px;vertical-align:middle}
.dot.hi{background:var(--hi)} .dot.med{background:var(--med)} .dot.lo{background:var(--lo)}
.more{font-size:10px;color:var(--mut);font-weight:700;margin-top:3px;padding-left:2px}
/* week */
.weekgrid{display:grid;grid-template-columns:48px repeat(7,1fr);border:1px solid var(--line2);border-radius:12px;overflow:hidden}
.wkcolhd{font-size:11px;font-weight:700;text-align:center;padding:7px 2px;border-left:1px solid var(--line2);border-bottom:1px solid var(--line2);background:var(--card2);color:var(--ink2)}
.wkcolhd.today{background:var(--clay);color:#fff}
.timecol{border-bottom:1px solid var(--line2);font-size:10px;color:var(--mut);text-align:right;padding:2px 5px;height:34px;font-weight:600}
.daycol{position:relative;border-left:1px solid var(--line2)}
.hourline{height:34px;border-bottom:1px solid var(--line2)}
.allday{display:grid;grid-template-columns:48px repeat(7,1fr);border:1px solid var(--line2);border-top:none;border-radius:0 0 12px 12px;overflow:hidden}
.adlabel{font-size:9.5px;color:var(--mut);padding:5px;font-weight:800;text-transform:uppercase;letter-spacing:.05em}
.adcell{border-left:1px solid var(--line2);padding:4px;min-height:28px}
.wev{position:absolute;left:3px;right:3px;border-radius:6px;padding:2px 5px;font-size:10px;overflow:hidden;border-left:3px solid var(--lo);background:var(--lobg);font-weight:500}
.wev.school{border-left-color:var(--school)} .wev.work{border-left-color:var(--work)} .wev.life{border-left-color:var(--life)}
.wev.hi{background:var(--hibg)} .wev.med{background:var(--medbg)}
/* lists */
.daywrap{border:1px solid var(--line2);border-radius:12px;overflow:hidden}
.row{display:flex;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line2);align-items:flex-start}
.row:last-child{border-bottom:none}
.row:hover{background:var(--card2)}
.row .time{width:66px;color:var(--ink2);font-size:12px;font-weight:700;flex:none;padding-top:1px}
.row .body{flex:1}
.row .title{font-weight:600;color:var(--ink)}
.row .sub2{color:var(--mut);font-size:12px;margin-top:3px}
.wpill{font-size:9.5px;font-weight:800;border-radius:5px;padding:2px 7px;flex:none;text-transform:uppercase;letter-spacing:.04em}
.wpill.hi{background:var(--hi);color:#fff} .wpill.med{background:var(--med);color:#fff} .wpill.lo{background:var(--line);color:var(--ink2)}
.rank{width:24px;height:24px;border-radius:50%;background:var(--ink);color:var(--cream);font-size:11px;font-weight:800;display:flex;align-items:center;justify-content:center;flex:none}
.callout{background:var(--clay-soft);border:1px solid #EBCBB9;border-radius:11px;padding:11px 13px;margin-bottom:12px;font-size:12.5px;color:var(--clay-deep)}
.callout b{color:var(--clay-deep)}
.sectlbl{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--mut);font-weight:800;margin:2px 0 12px}
/* heatmap */
.hm{display:flex;flex-direction:column;gap:6px}
.hmrow{display:grid;grid-template-columns:128px 1fr;gap:12px;align-items:center;padding:3px 4px;border-radius:9px}
.hmlabel{font-size:11px;color:var(--mut);font-weight:700;text-align:right}
.hmlabel b{color:var(--ink);display:block;font-size:12.5px;font-family:var(--serif);font-weight:600}
.hmbar{display:flex;align-items:center;gap:10px}
.bar{height:28px;border-radius:7px;min-width:4px;display:flex;align-items:center;padding:0 9px;color:#fff;font-size:11px;font-weight:800;text-shadow:0 1px 1px rgba(0,0,0,.12)}
.hmitems{font-size:11.5px;color:var(--ink2)}
.hmitems b{color:var(--hi)}
.hmrow.now{background:var(--clay-soft)}
.legend{font-size:11.5px;color:var(--mut);margin-top:12px;line-height:1.5}
.empty{color:var(--mut);padding:40px;text-align:center;font-size:13px}
.coachcard{border:1px solid var(--line2);background:var(--card2);border-radius:12px;padding:13px 15px;margin:10px 0;font-size:13.5px;line-height:1.5}
.coachcard.plan{border-left:3px solid var(--clay)}
.coachcard.rel{border-left:3px solid var(--life)}
.ctag{font-size:10px;text-transform:uppercase;letter-spacing:.07em;font-weight:800;color:var(--mut);margin-bottom:5px}
.coachcard b{color:var(--ink)}
</style>
</head>
<body>
<div class="wrap">
  <div class="topbar">
    <div class="brand">
      <span class="mark"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M12 1.6c.7 6 4.8 10.1 10.4 10.4-5.6.3-9.7 4.4-10.4 10.4-.7-6-4.8-10.1-10.4-10.4C7.2 11.7 11.3 7.6 12 1.6Z" fill="#D97757"/></svg></span>
      <div>
        <h1>Master Calendar</h1>
        <div class="sub" id="sub"></div>
      </div>
    </div>
    <div class="tabs" id="tabs"></div>
  </div>
  <div class="controls">
    <label>Course</label><select id="fCourse"></select>
    <label style="margin-left:4px">Type</label><select id="fType"></select>
    <span class="divider"></span>
    <span class="chip school on" data-cat="school">School</span>
    <span class="chip work on" data-cat="work">Work</span>
    <span class="chip life on" data-cat="life">Life</span>
    <span class="divider"></span>
    <label class="toggle"><input type="checkbox" id="tDead"> Deadlines only</label>
    <label class="toggle"><input type="checkbox" id="tHab"> Hide habits</label>
    <span style="flex:1"></span>
    <label>Today</label><input type="date" id="refDate">
  </div>
  <div class="layout"><div class="view" id="view"></div><div class="flags" id="flags"></div></div>
</div>
<script>
const DB = /*DATA*/;
const EXC = /*EXCEPTIONS*/;
const COACH = /*COACH*/;
const AGENDA = /*AGENDA*/;
const LOADINS = /*LOAD*/;
const EVENTS = DB.events;
const TOUCH = DB.touchpoints||[];
const DAYIDX={SU:0,MO:1,TU:2,WE:3,TH:4,FR:5,SA:6};
const DOW=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
const MON=["January","February","March","April","May","June","July","August","September","October","November","December"];
const WSCORE={high:6,medium:3,low:1};
function prov(o){return o.provenance||"note";}
function provLabel(p){return ({syllabus:"SYLLABUS",email:"EMAIL",calendar:"CALENDAR",note:"NOTE",you:"YOU"})[p]||"NOTE";}
function parseD(s){const[a,b,c]=s.split("-").map(Number);return new Date(a,b-1,c);}
function iso(d){return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0");}
function todayISO(){return iso(new Date());}
function addDays(d,n){const x=new Date(d);x.setDate(x.getDate()+n);return x;}
function monday(d){const x=new Date(d);const w=(x.getDay()+6)%7;x.setDate(x.getDate()-w);x.setHours(0,0,0,0);return x;}
function expandRule(ev){
  const out=[]; const start=parseD(ev.date); const end=ev.end_date?parseD(ev.end_date):start;
  const exc=(EXC&&EXC[ev.id])||[];
  if(ev.recurrence.startsWith("WEEKLY")){
    const days=ev.recurrence.split("BYDAY=")[1].split(",").map(x=>DAYIDX[x]);
    for(let d=new Date(start); d<=end; d=addDays(d,1)){ if(days.includes(d.getDay())&&!exc.includes(iso(d))) out.push(iso(d)); }
  } else if(ev.recurrence.startsWith("MONTHLY")){
    const day=Number(ev.recurrence.split("BYMONTHDAY=")[1]);
    let d=new Date(start.getFullYear(),start.getMonth(),day);
    while(d<=end){ if(!exc.includes(iso(d))) out.push(iso(d)); d=new Date(d.getFullYear(),d.getMonth()+1,day);}
  }
  return out;
}
function occurrences(){
  const occ=[];
  for(const ev of EVENTS){
    if(ev.recurrence){ for(const dt of expandRule(ev)) occ.push({...ev,date:dt,_rec:true}); }
    else if(ev.date){ occ.push({...ev,_rec:false}); }
  }
  return occ;
}
const ALL=occurrences();
const UNSCHEDULED=EVENTS.filter(e=>!e.date && !e.recurrence && e.status!=="done");
const state={view:"month",course:"all",type:"all",cats:{school:true,work:true,life:true},
  deadOnly:false,hideHab:false,ref:todayISO()};
state.monthCursor=new Date(parseD(state.ref).getFullYear(),parseD(state.ref).getMonth(),1);
state.weekCursor=monday(parseD(state.ref));
state.dayCursor=parseD(state.ref);
function passFilters(o){
  if(!state.cats[o.category]) return false;
  if(state.course!=="all" && o.source!==state.course) return false;
  if(state.type!=="all" && o.type!==state.type) return false;
  if(state.deadOnly && (o.type==="class"||o.type==="personal"||o.type==="chore")) return false;
  if(state.hideHab && o._rec && (o.category==="life")) return false;
  return true;
}
function visible(){return ALL.filter(passFilters);}
const TABS=[["month","Month"],["week","Week"],["day","Day"],["agenda","Next 7"],["load","Load"],["coach","Coach"]];
function renderTabs(){
  document.getElementById("tabs").innerHTML=TABS.map(([k,l])=>`<button class="tab ${state.view===k?"active":""}" data-v="${k}">${l}</button>`).join("");
  document.querySelectorAll(".tab").forEach(b=>b.onclick=()=>{state.view=b.dataset.v;render();});
}
function evChip(o){
  const w=o.weight==="high"?"hi":o.weight==="medium"?"med":"";
  const dot=o.weight==="high"?"hi":o.weight==="medium"?"med":"lo";
  const t=o.time?o.time+" ":"";
  return `<div class="ev ${o.category} ${w}" title="${o.title}${o.notes?' - '+String(o.notes).replace(/"/g,"'"):''}"><span class="dot ${dot}"></span>${t}${o.title}</div>`;
}
function renderMonth(){
  const cur=state.monthCursor; const y=cur.getFullYear(), m=cur.getMonth();
  const gridStart=new Date(y,m,1); gridStart.setDate(1-gridStart.getDay());
  const byday={}; visible().forEach(o=>{(byday[o.date]=byday[o.date]||[]).push(o);});
  const today=state.ref; let cells="";
  for(let i=0;i<42;i++){
    const d=addDays(gridStart,i); const out=d.getMonth()!==m;
    const list=(byday[iso(d)]||[]).sort((a,b)=>(a.time||"").localeCompare(b.time||""));
    const show=list.slice(0,4).map(evChip).join("");
    const more=list.length>4?`<div class="more">+${list.length-4} more</div>`:"";
    cells+=`<div class="cell ${out?"out":""} ${iso(d)===today?"today":""}"><div class="daynum">${d.getDate()}</div>${show}${more}</div>`;
  }
  document.getElementById("view").innerHTML=`
    <div class="calhead"><button class="navbtn" id="pm">‹</button><div class="mname">${MON[m]} ${y}</div><button class="navbtn" id="nm">›</button></div>
    <div class="grid7">${DOW.map(d=>`<div class="dow">${d}</div>`).join("")}</div>
    <div class="grid7" style="margin-top:2px">${cells}</div>`;
  document.getElementById("pm").onclick=()=>{state.monthCursor=new Date(y,m-1,1);render();};
  document.getElementById("nm").onclick=()=>{state.monthCursor=new Date(y,m+1,1);render();};
}
function renderWeek(){
  const start=state.weekCursor; const days=[...Array(7)].map((_,i)=>addDays(start,i));
  const byday={}; visible().forEach(o=>{(byday[o.date]=byday[o.date]||[]).push(o);});
  const H0=7,H1=22, hrs=[]; for(let h=H0;h<=H1;h++)hrs.push(h);
  let head=`<div class="wkcolhd"></div>`+days.map(d=>`<div class="wkcolhd ${iso(d)===state.ref?"today":""}">${DOW[d.getDay()]} ${d.getDate()}</div>`).join("");
  let cols="";
  for(const d of days){
    const list=(byday[iso(d)]||[]); let blocks="";
    list.filter(o=>o.time).forEach(o=>{
      const[hh,mm]=o.time.split(":").map(Number); const top=((hh-H0)*60+mm)/60*34;
      const w=o.weight==="high"?"hi":o.weight==="medium"?"med":"";
      blocks+=`<div class="wev ${o.category} ${w}" style="top:${top}px;height:32px" title="${o.title}">${o.time} ${o.title}</div>`;
    });
    let lines=hrs.map(()=>`<div class="hourline"></div>`).join("");
    cols+=`<div class="daycol">${lines}${blocks}</div>`;
  }
  let ad=`<div class="adlabel">all-day</div>`+days.map(d=>{
    const list=(byday[iso(d)]||[]).filter(o=>!o.time);
    return `<div class="adcell">${list.map(o=>{const w=o.weight==="high"?"hi":o.weight==="medium"?"med":"";return `<div class="wev ${o.category} ${w}" style="position:static;margin-bottom:3px" title="${o.title}">${o.title}</div>`;}).join("")}</div>`;
  }).join("");
  document.getElementById("view").innerHTML=`
    <div class="calhead"><button class="navbtn" id="pw">‹</button><div class="mname">Week of ${MON[start.getMonth()]} ${start.getDate()}</div><button class="navbtn" id="nw">›</button></div>
    <div class="weekgrid">${head}<div style="grid-column:1">${hrs.map(h=>`<div class="timecol">${h>12?h-12:h}${h>=12?"p":"a"}</div>`).join("")}</div>${cols}</div>
    <div class="allday">${ad}</div>`;
  document.getElementById("pw").onclick=()=>{state.weekCursor=addDays(start,-7);render();};
  document.getElementById("nw").onclick=()=>{state.weekCursor=addDays(start,7);render();};
}
function renderDay(){
  const d=state.dayCursor;
  const list=visible().filter(o=>o.date===iso(d)).sort((a,b)=>(a.time||"0").localeCompare(b.time||"0"));
  const rows=list.length?list.map(o=>{
    const w=o.weight==="high"?"hi":o.weight==="medium"?"med":"lo"; const p=prov(o);
    return `<div class="row"><div class="time">${o.time||"all-day"}</div><div class="body">
      <div class="title">${o.title}</div>
      <div class="tagrow"><span class="wpill ${w}">${o.weight}</span><span class="badge b-${p}">${provLabel(p)}</span><span class="sub2">${o.source}</span></div>
      ${o.notes?`<div class="sub2">${o.notes}</div>`:""}</div></div>`;
  }).join(""):`<div class="empty">Nothing scheduled.</div>`;
  document.getElementById("view").innerHTML=`
    <div class="calhead"><button class="navbtn" id="pd">‹</button><div class="mname">${DOW[d.getDay()]}, ${MON[d.getMonth()]} ${d.getDate()}</div><button class="navbtn" id="nd">›</button></div>
    <div class="daywrap">${rows}</div>`;
  document.getElementById("pd").onclick=()=>{state.dayCursor=addDays(d,-1);render();};
  document.getElementById("nd").onclick=()=>{state.dayCursor=addDays(d,1);render();};
}
function renderAgenda(){
  const t0=parseD(state.ref); const t1=addDays(t0,7);
  let list=visible().filter(o=>{const d=parseD(o.date);return d>=t0&&d<t1;});
  const rankW={high:0,medium:1,low:2};
  list.sort((a,b)=>rankW[a.weight]-rankW[b.weight] || a.date.localeCompare(b.date) || (a.time||"").localeCompare(b.time||""));
  const tbd=UNSCHEDULED.length?`<div class="callout">✦ <b>Needs a date:</b> ${UNSCHEDULED.map(e=>String(e.title).replace(/ \(.*/,"")).join(", ")}. Tell me when, and it drops onto the calendar.</div>`:"";
  let body=list.length?list.map((o,i)=>{
    const w=o.weight==="high"?"hi":o.weight==="medium"?"med":"lo"; const p=prov(o);
    const dd=parseD(o.date); const days=Math.round((dd-t0)/864e5);
    const when=days===0?"today":days===1?"tomorrow":DOW[dd.getDay()]+" "+(dd.getMonth()+1)+"/"+dd.getDate();
    return `<div class="row"><div class="rank">${i+1}</div><div class="body">
      <div class="title">${o.title}</div>
      <div class="tagrow"><span class="wpill ${w}">${o.weight}</span><span class="badge b-${p}">${provLabel(p)}</span>
      <span class="sub2">${when}${o.time?" · "+o.time:""} · ${o.source}</span></div>
      ${o.notes?`<div class="sub2">${o.notes}</div>`:""}</div></div>`;
  }).join(""):`<div class="empty">Nothing in this 7-day window. Move the “Today” date.</div>`;
  // Cached LLM "read" for this stretch, keyed by the active week's Monday. Written by the
  // weekly pass from the table; rendered here as a lookup on top of the accurate ranked list.
  const anote=(AGENDA&&AGENDA[iso(monday(parseD(state.ref)))])||"";
  const noteHtml=anote?`<div class="coachcard plan"><div class="ctag">The read</div><div>${anote}</div></div>`:"";
  document.getElementById("view").innerHTML=`<div class="sectlbl">Next 7 days from ${state.ref} · ranked by effort</div>${noteHtml}${tbd}<div class="daywrap">${body}</div>`;
}
function termBounds(){
  const ds=ALL.map(o=>parseD(o.date)).sort((a,b)=>a-b);
  const lo=ds.length?ds[0]:new Date(); const hi=ds.length?ds[ds.length-1]:new Date();
  return [monday(lo), addDays(monday(hi),7)];
}
function renderLoad(){
  const [start,end]=termBounds(); const weeks=[];
  for(let d=new Date(start);d<=end;d=addDays(d,7)) weeks.push(new Date(d));
  const occ=visible().filter(o=>o.type!=="class");
  const rows=weeks.map(wk=>{
    const wEnd=addDays(wk,7);
    const items=occ.filter(o=>{const d=parseD(o.date);return d>=wk&&d<wEnd;});
    const score=items.reduce((s,o)=>s+(WSCORE[o.weight]||0),0);
    const highs=items.filter(o=>o.weight==="high");
    return {wk,items,score,highs};
  });
  const maxS=Math.max(1,...rows.map(r=>r.score));
  const refMon=+monday(parseD(state.ref));
  const color=s=>{const t=s/maxS; if(t===0)return"#E8E3D8"; if(t<.34)return"#D8C39A"; if(t<.6)return"#D89F5C"; if(t<.8)return"#CB7A4E"; return"#B23E2E";};
  let html=rows.map(r=>{
    const lbl=`${MON[r.wk.getMonth()].slice(0,3)} ${r.wk.getDate()}`;
    const names=r.items.sort((a,b)=>(WSCORE[b.weight])-(WSCORE[a.weight])).slice(0,6).map(o=>o.weight==="high"?`<b>${String(o.title).replace(/ due| \(cumulative\)/g,"")}</b>`:String(o.title).replace(/ due/,"")).join(", ");
    const now=+r.wk===refMon?"now":"";
    return `<div class="hmrow ${now}"><div class="hmlabel"><b>${lbl}</b>${r.highs.length?r.highs.length+" high-effort":(r.items.length+" items")}</div>
      <div class="hmbar"><div class="bar" style="width:${Math.max(7,r.score/maxS*100)}%;background:${color(r.score)}">${r.score||""}</div>
      <div class="hmitems">${names||"<span style='color:#A8A296'>clear</span>"}</div></div></div>`;
  }).join("");
  // Cached LLM narrative for the term's load shape (written by the weekly pass from the table).
  const lsum=(LOADINS&&LOADINS.summary)||"";
  const sumHtml=lsum?`<div class="coachcard plan"><div class="ctag">Reading the shape</div><div>${lsum}</div></div>`:"";
  document.getElementById("view").innerHTML=`<div class="sectlbl">Weekly load · summed effort (high 6, medium 3, low 1), classes excluded</div>
    ${sumHtml}
    <div class="hm">${html}</div>
    <div class="legend">Bars are computed straight from the table; the note above is the LLM's read of them. <b style="color:var(--hi)">Bold</b> marks high-effort items. The highlighted row is your “today” week.</div>`;
}
function renderCoach(){
  // Coach entries are pre-authored per week (LLM pass) and cached in COACH, keyed by
  // the Monday of the active "Today" week. Rendering is a pure lookup — no heuristics here.
  const wk=iso(monday(parseD(state.ref)));
  const entry=(COACH&&COACH[wk])?COACH[wk]:null;
  const dstart=monday(parseD(state.ref)); const dend=addDays(dstart,6);
  const range=MON[dstart.getMonth()].slice(0,3)+" "+dstart.getDate()+" – "+MON[dend.getMonth()].slice(0,3)+" "+dend.getDate();
  let cards="";
  if(entry && entry.length){
    cards=entry.map(function(c){
      return '<div class="coachcard '+(c.cls||"plan")+'"><div class="ctag">'+c.tag+'</div><div>'+c.body+'</div></div>';
    }).join("");
  } else {
    cards='<div class="empty">No coaching note saved for the week of '+range+' yet. The Coach refreshes when the calendar updates.</div>';
  }
  document.getElementById("view").innerHTML='<div class="sectlbl">Coach · week of '+range+'</div>'+cards;
}
function renderFlags(){
  // Flags are derived live from the table (source of truth). "done" rows drop off — so when
  // you say e.g. "I updated my resume", flipping that row's status clears it from here.
  const changed=EVENTS.filter(e=>e.flag==="changed" && e.status!=="done");
  const unsched=EVENTS.filter(e=>(e.flag==="unscheduled" || (!e.date && !e.recurrence)) && e.status!=="done");
  const assumed=EVENTS.filter(e=>e.flag==="assumed" && e.status!=="done");
  const seen=new Set();
  const card=(cls,icon,e)=>{ if(seen.has(e.id))return""; seen.add(e.id); const p=prov(e);
    return `<div class="flagcard ${cls}"><div class="ttl">${icon} ${String(e.title).replace(/ \(.*/,"")}</div>
      <div class="tagrow"><span class="badge b-${p}">${provLabel(p)}</span><span class="meta">${e.source||""}</span></div>
      ${e.notes?`<div class="meta">${e.notes}</div>`:""}</div>`; };
  let h=`<h3>Flags &amp; provenance</h3><div style="font-size:11.5px;color:var(--mut);margin-bottom:4px">Why things look the way they do.</div>`;
  changed.forEach(e=>h+=card("changed","↻",e));
  unsched.forEach(e=>h+=card("unsched","✦",e));
  assumed.forEach(e=>h+=card("assumed","≈",e));
  if(!changed.length&&!unsched.length&&!assumed.length) h+=`<div style="font-size:12px;color:var(--mut)">Nothing flagged. Everything traced cleanly to a source.</div>`;
  h+=`<h3 style="margin-top:16px">Source key</h3>
    <div class="tagrow"><span class="badge b-syllabus">SYLLABUS</span><span class="badge b-email">EMAIL</span><span class="badge b-calendar">CALENDAR</span><span class="badge b-note">NOTE</span><span class="badge b-you">YOU</span></div>
    <div style="font-size:11.5px;color:var(--mut);margin-top:7px">Every item carries the source it came from. “YOU” means you added it by talking to the calendar.</div>`;
  document.getElementById("flags").innerHTML=h;
}
function render(){
  renderTabs();
  document.getElementById("sub").textContent=`${DB.term||"This term"} · ${ALL.length} occurrences from ${EVENTS.length} rows`;
  if(state.view==="month")renderMonth();
  else if(state.view==="week")renderWeek();
  else if(state.view==="day")renderDay();
  else if(state.view==="agenda")renderAgenda();
  else if(state.view==="load")renderLoad();
  else if(state.view==="coach")renderCoach();
  renderFlags();
}
function initControls(){
  const c=document.getElementById("fCourse"); const sources=[...new Set(EVENTS.map(e=>e.source))];
  c.innerHTML=`<option value="all">All courses</option>`+sources.map(s=>`<option>${s}</option>`).join("");
  c.onchange=()=>{state.course=c.value;render();};
  const t=document.getElementById("fType"); const types=[...new Set(EVENTS.map(e=>e.type))];
  t.innerHTML=`<option value="all">All types</option>`+types.map(s=>`<option>${s}</option>`).join("");
  t.onchange=()=>{state.type=t.value;render();};
  document.querySelectorAll(".chip[data-cat]").forEach(ch=>ch.onclick=()=>{const k=ch.dataset.cat;state.cats[k]=!state.cats[k];ch.classList.toggle("on",state.cats[k]);render();});
  document.getElementById("tDead").onchange=e=>{state.deadOnly=e.target.checked;render();};
  document.getElementById("tHab").onchange=e=>{state.hideHab=e.target.checked;render();};
  const rd=document.getElementById("refDate"); rd.value=state.ref;
  rd.onchange=()=>{state.ref=rd.value;state.dayCursor=parseD(rd.value);state.weekCursor=monday(parseD(rd.value));state.monthCursor=new Date(parseD(rd.value).getFullYear(),parseD(rd.value).getMonth(),1);render();};
}
initControls(); render();
</script>
</body>
</html>
```