// screens-calendar.jsx — the Calendar, as its own screen.
//
// Lifted out of the Action Dashboard's "Calendar" tab (screens-actions.jsx),
// which is being retired. Nothing about the behaviour changes: the same ICS
// feed, the same Day/Week views, the same click-to-focus highlighting, the same
// type filter tiles and the same Touchpoint Forecast.
//
// WHAT IS DIFFERENT, and why:
//
//  1. It reads the calendar feed from the notification store rather than
//     fetching its own. The store already pulls the feed every five minutes for
//     meeting reminders; a second fetch here would double the network cost and
//     let the two copies disagree about what is on your calendar. If the store
//     has not loaded yet, this falls back to fetching directly — the screen is
//     reachable by URL restore before the store settles.
//
//  2. Focus and type-filter selections persist per user in localStorage, as
//     before. The keys are unchanged, so existing selections survive the move.
//
//  3. The Touchpoint Forecast only ever showed Update Calls despite computing
//     conf/intro/visit as well. That is preserved — it is a cadence tracker for
//     client update calls, not a general list — but the other types now feed
//     the type-filter counts, which they always did.
//
// DAY-OF vs FORWARD. The Notification Center deliberately shows only day-of
// meetings (a meeting is an action on the day it happens). This screen is the
// forward view: two weeks of grid, and a forecast that looks further out. The
// two are complements, not duplicates.

// Same story as NC: the pre-8/20 root palette, inline. See screens-notifications.jsx.
const CV2 = {
  canvas: "var(--bg)", ink: "var(--ink)", sec: "var(--muted)", muted: "var(--muted-2)",
  line: "var(--line)", hair: "var(--line-2)",
  accent: "var(--accent)", accentDeep: "var(--accent-2)", accentSoft: "var(--accent-soft)",
  good: "var(--ok)", over: "var(--risk)",
  mono: "var(--f-mono)",
  panel: { background: "var(--surface)", border: "1px solid var(--line)", borderRadius: "var(--r-card)", boxShadow: "var(--shadow-none)" },
};
const CV2_KICKER = { fontSize: 11, fontWeight: 600, letterSpacing: ".12em", textTransform: "uppercase", color: CV2.muted };

// Delegates to VaultAlpha. This parsed hex text, so once its inputs became
// tokens in the 8/20 sweep it returned "rgba(NaN,NaN,NaN,a)" -- an invalid
// colour the browser drops silently, which is how the aging ramp on Weekly
// Research Progression went grey without an error.
function cvHexA(color, a) {
  return window.VaultAlpha ? window.VaultAlpha(color, a)
    : `color-mix(in srgb, ${color} ${Math.round((a || 0) * 100)}%, transparent)`;
}
function calTodayStr() {
  const d = new Date();
  return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
}
function calAddDays(dateStr, days) {
  const d = new Date(dateStr + "T00:00:00Z");
  d.setUTCDate(d.getUTCDate() + days);
  return d.toISOString().slice(0, 10);
}
function calMondayOf(dateStr) {
  const d = new Date(dateStr + "T00:00:00Z");
  const dow = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() - (dow - 1));
  return d.toISOString().slice(0, 10);
}
// Same classifier the feed engine uses. Read from there when available so the
// two can never drift; the literal is a boot-order fallback only.
function calClassify(title) {
  if (window.VaultActionsFeed && window.VaultActionsFeed.classifyEvent) {
    return window.VaultActionsFeed.classifyEvent(title);
  }
  const t = (title || "").toLowerCase();
  if (/\bupdate call\b/.test(t)) return { key: "update", color: "var(--evt-update)", label: "Update" };
  if (/conference call|conf call/.test(t)) return { key: "conf", color: "var(--cat-3)", label: "Conf" };
  if (/intro call/.test(t)) return { key: "intro", color: "var(--evt-intro)", label: "Intro" };
  if (/\bcall\b/.test(t)) return { key: "call", color: "var(--evt-call)", label: "Call" };
  if (/\bvisit|site visit|on-?site\b/.test(t)) return { key: "visit", color: "var(--evt-call)", label: "Visit" };
  if (/meeting|mtg/.test(t)) return { key: "meeting", color: "var(--evt-meeting)", label: "Mtg" };
  if (/\bpto|out\b|graduation|vacation/.test(t)) return { key: "ooo", color: "var(--muted-2)", label: "OOO" };
  return { key: "other", color: "var(--evt-meeting)", label: "" };
}

// Connect-your-calendar modal. Hoisted to module scope: a component defined
// inside a render closure is a new type on every keystroke, which remounts the
// input and steals focus after each character.
function CalFeedModal({ url, setUrl, onSave, onDisconnect, onClose }) {
  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "var(--scrim)", zIndex: 1000,
      display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
      <div onClick={e => e.stopPropagation()} style={{ background: "var(--surface)", borderRadius: "var(--r-card)", width: "100%", maxWidth: 480,
        boxShadow: "var(--shadow-modal)", overflow: "hidden" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "16px 20px", borderBottom: "1px solid " + CV2.line }}>
          <strong style={{ fontSize: 16, color: CV2.ink }}>Connect your calendar</strong>
          <button onClick={onClose} style={{ border: "none", background: "transparent", fontSize: 22, color: CV2.muted, cursor: "pointer", lineHeight: 1 }}>{"\u00D7"}</button>
        </div>
        <div style={{ padding: 20 }}>
          <div style={{ fontSize: 12.5, color: "var(--prio-med)", marginBottom: 4, fontWeight: 600 }}>Calendar feed URL (.ics)</div>
          <input value={url} onChange={e => setUrl(e.target.value)}
            placeholder="https://outlook.office365.com/owa/calendar/.../calendar.ics"
            style={{ width: "100%", padding: "10px 12px", borderRadius: "var(--r-ctl)", border: "1px solid " + CV2.line,
              fontSize: 12.5, color: CV2.ink, background: "var(--surface)", boxSizing: "border-box" }}/>
          <div style={{ fontSize: 11.5, color: CV2.muted, marginTop: 12, lineHeight: 1.5 }}>
            <strong>Outlook:</strong> Calendar → Settings → Shared calendars → Publish a calendar → choose “Can view all details” → copy the <strong>ICS</strong> link.<br/>
            <strong>Google:</strong> Calendar settings → your calendar → “Integrate calendar” → copy the <strong>Secret address in iCal format</strong>.<br/>
            This is read-only and only shows on your dashboard.
          </div>
          <div style={{ display: "flex", gap: 10, marginTop: 18 }}>
            {/* Modal primary buttons are fully inline-styled: the app's .btn base
                is a ghost/white style, so a class-only primary renders invisible. */}
            <button onClick={onSave} disabled={!url.trim()}
              style={{ padding: "9px 18px", borderRadius: "var(--r-ctl)", border: "none", background: CV2.accent, color: "var(--accent-ink)",
                fontSize: 14, fontWeight: 600, fontFamily: "inherit",
                cursor: url.trim() ? "pointer" : "default", opacity: url.trim() ? 1 : .5 }}>Save &amp; load</button>
            {url.trim() && (
              <button onClick={onDisconnect}
                style={{ padding: "9px 16px", borderRadius: "var(--r-ctl)", border: "1px solid " + CV2.line, background: "var(--surface)",
                  color: "var(--prio-med)", fontSize: 14, cursor: "pointer", fontFamily: "inherit" }}>Disconnect</button>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

function CalendarScreen({ user }) {
  const STORE = window.VaultNotifications;

  const [orgTick, setOrgTick] = React.useState(0);
  React.useEffect(() => {
    const onOrg = () => setOrgTick(t => t + 1);
    window.addEventListener("vault:org-updated", onOrg);
    return () => window.removeEventListener("vault:org-updated", onOrg);
  }, []);

  const viewer = React.useMemo(() => {
    if (!user) return null;
    const PEOPLE_BY_ID = (window.VAULT_FIRM && window.VAULT_FIRM.PEOPLE_BY_ID) || {};
    const cands = [];
    if (user.id) cands.push(user.id);
    if (user.personId) {
      cands.push(user.personId);
      cands.push(String(user.personId).replace(/^p_/, ""));
      cands.push(String(user.personId).replace(/^p_/, "").replace(/_gmail$/, ""));
    }
    if (user.email) cands.push(String(user.email).split("@")[0]);
    for (const c of cands) if (PEOPLE_BY_ID[c]) return Object.assign({}, PEOPLE_BY_ID[c]);
    return { id: user.personId ? String(user.personId).replace(/^p_/, "").replace(/_gmail$/, "") : (user.email || "unknown"),
             name: user.name || user.email || "Unknown user" };
  }, [user, orgTick]);

  const today = React.useMemo(() => calTodayStr(), []);
  const weekStart = React.useMemo(() => calMondayOf(today), [today]);
  const weekEnd = React.useMemo(() => calAddDays(weekStart, 6), [weekStart]);

  // ---- calendar source
  // Prefer the store's copy: it already fetches this feed for meeting reminders.
  const [, storeTick] = React.useState(0);
  React.useEffect(() => {
    if (!STORE) return;
    return STORE.subscribe(() => storeTick(n => n + 1));
  }, [STORE]);
  const storeState = STORE ? STORE.getState() : null;
  const storeEvents = storeState ? storeState.calEvents : null;

  const [ownEvents, setOwnEvents] = React.useState(null);
  // One-shot: whichever source wins the race, we do not re-decide on every
  // store tick (the store publishes many times as its sources land).
  const adoptedRef = React.useRef(false);
  const [status, setStatus] = React.useState("idle"); // idle|loading|ok|error|nofeed
  const [urlInput, setUrlInput] = React.useState("");
  const [showFeedModal, setShowFeedModal] = React.useState(false);
  const personId = viewer && viewer.id;

  const loadOwn = React.useCallback(async () => {
    if (!personId || !window.VaultAPI) return;
    setStatus("loading");
    try {
      const feed = await window.VaultAPI.getCalendarFeed(personId);
      if (!feed || !feed.ics_url) { setStatus("nofeed"); setOwnEvents(null); return; }
      setUrlInput(feed.ics_url);
      const evs = await window.VaultAPI.fetchCalendarEvents(feed.ics_url);
      if (evs === null) { setStatus("error"); setOwnEvents(null); return; }
      setOwnEvents(evs); setStatus("ok");
    } catch (e) { setStatus("error"); }
  }, [personId]);

  // WHY THIS FETCHES IMMEDIATELY RATHER THAN WAITING FOR THE STORE.
  //
  // First version adopted the store's calendar copy to avoid a duplicate
  // request. That cost 25 seconds of blank screen: the store awaits ALL eleven
  // sources — pipeline rows, CR universe, five weeks of research — before it
  // publishes anything, and the calendar is one of the first things ready. So
  // this screen sat idle waiting on pipeline data it does not use.
  //
  // Now: if the store ALREADY has the feed (you arrived after it settled), adopt
  // it instantly and skip the network entirely. Otherwise fetch straight away —
  // one ICS request, ~1s — rather than blocking on work that has nothing to do
  // with the calendar. The duplicate request in that path is worth 24 seconds.
  React.useEffect(() => {
    if (!personId) return;
    if (adoptedRef.current) return;
    if (storeEvents && storeEvents.length >= 0) {
      adoptedRef.current = true;
      setStatus("ok");
      if (!urlInput && window.VaultAPI && window.VaultAPI.getCalendarFeed) {
        window.VaultAPI.getCalendarFeed(personId).then(f => { if (f && f.ics_url) setUrlInput(f.ics_url); }).catch(() => {});
      }
      return;
    }
    if (status === "idle") { adoptedRef.current = true; loadOwn(); }
  }, [personId, storeEvents, loadOwn, status, urlInput]);

  const calEvents = storeEvents || ownEvents;

  async function saveUrl() {
    const url = urlInput.trim();
    const provider = /office365|outlook|microsoft/i.test(url) ? "outlook"
      : (/google|calendar\.google/i.test(url) ? "google" : "other");
    try {
      await window.VaultAPI.saveCalendarFeed(personId, url, provider);
      setShowFeedModal(false);
      await loadOwn();
      // The store caches the feed for meeting reminders; make it re-read too, or
      // notifications keep firing off the OLD calendar until the next poll.
      if (STORE) STORE.reload();
    } catch (e) {
      window.VaultUI && window.VaultUI.toast("error", "Couldn't save calendar URL: " + (e.message || e));
    }
  }
  async function disconnect() {
    try {
      await window.VaultAPI.saveCalendarFeed(personId, "", "other");
      setUrlInput(""); setShowFeedModal(false); setOwnEvents(null); setStatus("nofeed");
      if (STORE) STORE.reload();
    } catch (e) {
      window.VaultUI && window.VaultUI.toast("error", "Couldn't disconnect: " + (e.message || e));
    }
  }

  // ---- persisted view state (keys unchanged from the Action Dashboard, so
  // existing selections survive the move)
  const focusKey = "vault:cal-focus:" + ((user && user.id) || "anon");
  const filterKey = "vault:cal-typefilter:" + ((user && user.id) || "anon");
  // Week by default (Brian 8/19): the day view answers "what is left today",
  // which the Notification Center already does. The reason to open the
  // Calendar is the shape of the week.
  const [view, setView] = React.useState("week");
  const [focus, setFocusRaw] = React.useState(() => {
    try { return new Set(JSON.parse(window.localStorage.getItem(focusKey) || "[]")); } catch (e) { return new Set(); }
  });
  const setFocus = (next) => setFocusRaw(prev => {
    const v = typeof next === "function" ? next(prev) : next;
    try { window.localStorage.setItem(focusKey, JSON.stringify([...v])); } catch (e) {}
    return v;
  });
  const toggleFocus = (k) => setFocus(prev => { const n = new Set(prev); n.has(k) ? n.delete(k) : n.add(k); return n; });
  const [typeFilter, setTypeFilterRaw] = React.useState(() => {
    try { return new Set(JSON.parse(window.localStorage.getItem(filterKey) || "[]")); } catch (e) { return new Set(); }
  });
  const toggleType = (k) => setTypeFilterRaw(prev => {
    const n = new Set(prev); n.has(k) ? n.delete(k) : n.add(k);
    try { window.localStorage.setItem(filterKey, JSON.stringify([...n])); } catch (e) {}
    return n;
  });
  const typeVisible = (title) => typeFilter.size === 0 || typeFilter.has(calClassify(title).key);

  // ---- derived
  const eventsThisWeek = React.useMemo(() => {
    if (!calEvents) return {};
    const out = {};
    calEvents.forEach(ev => {
      const d = ev.start && ev.start.date;
      if (!d || d < weekStart || d > weekEnd) return;
      (out[d] = out[d] || []).push(ev);
    });
    Object.values(out).forEach(list => list.sort((a, b) => (a.start.ts || 0) - (b.start.ts || 0)));
    return out;
  }, [calEvents, weekStart, weekEnd]);

  // Two-week grid from Monday of this week. Includes empty days on purpose:
  // a gap in the week is information.
  const grid = React.useMemo(() => {
    const byDay = {};
    (calEvents || []).forEach(ev => { const d = ev.start && ev.start.date; if (d) (byDay[d] = byDay[d] || []).push(ev); });
    const cells = [];
    for (let i = 0; i < 14; i++) {
      const date = calAddDays(weekStart, i);
      cells.push({ date: date, events: (byDay[date] || []).slice().sort((a, b) => (a.start.ts || 0) - (b.start.ts || 0)) });
    }
    return cells;
  }, [calEvents, weekStart]);

  // Forward-looking touchpoint forecast. Parses future events, classifies the
  // touchpoint, extracts the company and matches it against real Vault clients.
  const touchpoints = React.useMemo(() => {
    if (!calEvents) return [];
    const clients = window.HARVEY_CLIENTS || [];
    const norm = (s) => (s || "").toLowerCase()
      .replace(/\(.*?\)/g, " ")
      .replace(/\b(llc|inc|ltd|lp|corp|co|holdings?|group|company|companies|the)\b/g, " ")
      .replace(/[^a-z0-9 ]/g, " ").replace(/\s+/g, " ").trim();
    const clientIndex = clients.map(c => ({ rec: c, n: norm(c.client) })).filter(x => x.n.length >= 3);
    const matchClient = (raw) => {
      const n = norm(raw);
      if (n.length < 3) return null;
      let best = null, bestLen = 0;
      for (const { rec, n: cn } of clientIndex) {
        if (cn === n || cn.startsWith(n + " ") || n.startsWith(cn + " ") || cn.includes(" " + n + " ")) {
          if (n.length > bestLen) { best = rec; bestLen = n.length; }
        }
      }
      if (best) return best;
      for (const { rec, n: cn } of clientIndex) {
        const a = n.split(" ")[0], b = cn.split(" ")[0];
        if (a.length >= 4 && a === b && Math.abs(cn.length - n.length) < 14) return rec;
      }
      return null;
    };
    const classify = (title) => {
      const t = (title || "").toLowerCase();
      if (/\bupdate call\b/.test(t)) return "update";
      if (/conference call|conf call/.test(t)) return "conf";
      if (/intro call/.test(t)) return "intro";
      if (/\bvisit|site visit|on-?site\b/.test(t)) return "visit";
      return null;
    };
    const extractSides = (title) => {
      let s = (title || "").replace(/^\s*\d{1,2}:\d{2}\s*(am|pm)?\s*/i, "");
      s = s.replace(/^(cc|cf|uc|ic)\s*[-\u2013:]\s*/i, "");
      s = s.replace(/^(update call|conference call|conf call|intro call|visit|site visit)\s*[:\-\u2013]\s*/i, "");
      return s.split("/").map(x => x.trim()).filter(Boolean);
    };
    const out = [];
    calEvents.forEach(ev => {
      const type = classify(ev.title);
      if (!type) return;
      const d = ev.start && ev.start.date;
      if (!d || d < today) return;
      const sides = extractSides(ev.title);
      if (sides.length === 0) return;
      let project = null, company = null, matched = null;
      if (type === "update") {
        for (const side of sides) { const m = matchClient(side); if (m) { matched = m; project = m.client; break; } }
        if (!project) project = sides[0];
        company = project;
      } else {
        company = sides[0];
        matched = matchClient(company);
        project = matched ? matched.client : company;
      }
      const days = Math.round((new Date(d + "T00:00:00Z") - new Date(today + "T00:00:00Z")) / 86400000);
      const codeMap = { update: "Update Call", conf: "CC", intro: "Intro Call", visit: "Visit" };
      out.push({
        type: type, date: d, days: days, title: ev.title,
        label: type === "conf" ? ("CC - " + company) : (matched ? matched.client : company),
        sub: codeMap[type], matched: !!matched, leadId: matched ? matched.lead : null,
        // The clock, carried through so the forecast can show it AND sort by it.
        // Ordering by `days` alone left same-day calls in feed order, which is
        // arbitrary — the list claimed an order it did not actually have.
        ts: (ev.start && ev.start.ts) || null,
        allDay: !!(ev.start && ev.start.allDay),
        time: (ev.start && !ev.start.allDay && ev.start.iso)
          ? new Date(ev.start.iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })
          : null,
      });
    });
    const seen = {};
    // Day first, then clock within the day. All-day entries sort last.
    return out.sort((a, b) => (a.days - b.days)
        || ((a.ts == null ? Infinity : a.ts) - (b.ts == null ? Infinity : b.ts))).filter(x => {
      const k = x.type + "|" + x.label.toLowerCase();
      if (seen[k]) return false; seen[k] = true; return true;
    });
  }, [calEvents, today]);

  // ---- render helpers
  const weekCells = grid.slice(0, 5); // Mon–Fri
  const viewEvents = view === "day" ? (eventsThisWeek[today] || []) : weekCells.flatMap(c => c.events);
  const tally = { conf: 0, visit: 0, intro: 0, update: 0 };
  viewEvents.forEach(ev => { const k = calClassify(ev.title).key; if (k in tally) tally[k]++; });
  const counts = [
    { kicker: "Conference Calls", value: tally.conf, color: "var(--cat-3)", filterKey: "conf" },
    { kicker: "Visits", value: tally.visit, color: "var(--evt-call)", filterKey: "visit" },
    { kicker: "Intro Calls", value: tally.intro, color: "var(--evt-intro)", filterKey: "intro" },
    { kicker: "Update Calls", value: tally.update, color: "var(--evt-update)", filterKey: "update" },
  ];
  // Same-day-only dimming: an event dims ONLY when a selection exists on ITS
  // day, so focusing Tuesday does not grey out Thursday.
  const focusedDays = new Set([...focus].map(k => String(k).split("#")[0]));
  const visOf = (date, ev) => {
    const k = date + "#" + ((ev && ev.title) || "");
    const sel = focus.has(k);
    return { k: k, sel: sel, dim: !sel && focusedDays.has(date) };
  };
  const toggleBtn = (active) => active
    ? { border: "none", cursor: "pointer", fontFamily: CV2.mono, fontSize: 11, letterSpacing: ".06em",
        color: "var(--accent-ink)", padding: "6px 14px", borderRadius: "var(--r-ctl)",
        background: CV2.accent,
        boxShadow: "var(--shadow-none)" }
    : { border: "none", cursor: "pointer", fontFamily: CV2.mono, fontSize: 11, letterSpacing: ".06em",
        color: CV2.sec, padding: "6px 14px", borderRadius: "var(--r-ctl)", background: "transparent" };
  const timeOf = (ev) => (!ev.start.allDay && ev.start.iso)
    ? new Date(ev.start.iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).toUpperCase()
    : "ALL DAY";

  const dObj = new Date(today + "T00:00:00Z");

  return (
    <div style={{ padding: "20px 28px 60px", minHeight: "100vh", background: CV2.canvas, color: CV2.ink }}>

      {/* Header */}
      <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 24, marginBottom: 22, flexWrap: "wrap" }}>
        <div>
          <h2 className="v-h1">Calendar</h2>
          <div style={{ marginTop: 7, fontFamily: CV2.mono, fontSize: 12, letterSpacing: ".02em", color: CV2.sec }}>
            {window.VaultDate(dObj, { weekday: "long", month: "long", day: "numeric" })}
            {" \u00B7 "}your connected feed
          </div>
        </div>
        <button onClick={() => setShowFeedModal(true)}
          style={{ border: "1px solid " + CV2.line, background: "var(--surface)", color: CV2.sec, borderRadius: "var(--r-ctl)",
            cursor: "pointer", fontSize: 12.5, padding: "8px 14px", fontFamily: "inherit", flexShrink: 0 }}>
          {status === "nofeed" ? "+ Connect calendar" : "Calendar settings"}
        </button>
      </div>

      {status === "ok" && (
        <div className="kpi-grid" style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12, marginBottom: 16 }}>
          {counts.map(c => (
            <div key={c.kicker} onClick={() => toggleType(c.filterKey)}
              title={typeFilter.has(c.filterKey) ? "Showing only selected types — click to remove" : "Click to filter the calendar to " + c.kicker}
              style={{ padding: "15px 16px", borderRadius: "var(--r-card)", cursor: "pointer",
                background: typeFilter.has(c.filterKey) ? cvHexA(c.color, .08) : "var(--surface)",
                border: "1px solid " + (typeFilter.has(c.filterKey) ? cvHexA(c.color, .5) : CV2.line),
                boxShadow: typeFilter.has(c.filterKey) ? ("0 0 0 1px " + cvHexA(c.color, .35)) : "0 1px 2px var(--line-2)",
                display: "flex", alignItems: "center", gap: 12 }}>
              <span style={{ width: 8, height: 8, borderRadius: "50%", background: c.color, flexShrink: 0 }}/>
              <div>
                <div style={{ fontFamily: CV2.mono, fontWeight: 600, fontSize: 24, color: CV2.ink, lineHeight: 1 }}>{c.value}</div>
                <div style={{ ...CV2_KICKER, marginTop: 6 }}>{c.kicker}</div>
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Agenda */}
      <div style={{ ...CV2.panel, padding: "18px 22px", marginBottom: 16 }}>
        <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 16, flexWrap: "wrap", marginBottom: 12 }}>
          <div>
            <div style={{ ...CV2_KICKER, marginBottom: 4 }}>{view === "day" ? "Today's Calendar" : "This Week"}</div>
            <div style={{ fontSize: 12.5, color: CV2.sec }}>
              {view === "day" ? "Today's agenda · click events to select" : "Mon–Fri · select events to focus their days"}
            </div>
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
            {focus.size > 0 && (
              <button onClick={() => setFocus(new Set())}
                style={{ border: "none", background: "transparent", color: CV2.accent, cursor: "pointer",
                  fontFamily: CV2.mono, fontSize: 11, padding: 0 }}>
                Clear highlights · {focus.size}
              </button>
            )}
            <div style={{ display: "flex", padding: 3, borderRadius: "var(--r-ctl)", background: "color-mix(in srgb, var(--ink) 5%, transparent)", border: "1px solid " + CV2.line }}>
              <button onClick={() => setView("day")} style={toggleBtn(view === "day")}>Day</button>
              <button onClick={() => setView("week")} style={toggleBtn(view === "week")}>Week</button>
            </div>
          </div>
        </div>

        {(status === "loading" || status === "idle") && (
          window.VaultLoader
            ? <window.VaultLoader/>
            : <div style={{ fontFamily: CV2.mono, fontSize: 12, color: CV2.muted, padding: "8px 0" }}>{"Loading your calendar\u2026"}</div>
        )}
        {status === "nofeed" && <div style={{ fontSize: 13, color: CV2.muted, padding: "8px 0" }}>Connect your Outlook or Google calendar to see your meetings here.</div>}
        {status === "error" && (
          <div style={{ fontSize: 13, color: CV2.sec, padding: "8px 0" }}>
            <span style={{ color: CV2.over, fontWeight: 600 }}>Couldn't load this calendar feed.</span> Check the URL in{" "}
            <button onClick={() => setShowFeedModal(true)}
              style={{ border: "none", background: "transparent", color: CV2.accent, cursor: "pointer", padding: 0,
                fontSize: "inherit", textDecoration: "underline", fontFamily: "inherit" }}>calendar settings</button>.
          </div>
        )}

        {status === "ok" && view === "day" && (
          (eventsThisWeek[today] || []).length === 0
            ? <div style={{ fontSize: 13, color: CV2.muted, padding: "6px 0" }}>No calendar events today.</div>
            : (
              <div style={{ display: "flex", flexDirection: "column" }}>
                {(eventsThisWeek[today] || []).filter(ev => typeVisible(ev.title)).map((ev, i) => {
                  const c = calClassify(ev.title);
                  const v = visOf(today, ev);
                  return (
                    <div key={i} title={ev.title} onClick={() => toggleFocus(v.k)}
                      style={{ display: "flex", alignItems: "center", gap: 14, padding: "11px 12px", borderRadius: "var(--r-card)", cursor: "pointer",
                        opacity: v.dim ? .26 : 1,
                        background: v.sel ? cvHexA(c.color, .1) : "transparent",
                        boxShadow: v.sel ? ("0 0 0 1px " + cvHexA(c.color, .55) + ", 0 0 16px " + cvHexA(c.color, .3)) : "none",
                        transition: "opacity .2s, box-shadow .2s, background .2s" }}>
                      <span style={{ width: 3, alignSelf: "stretch", borderRadius: "var(--r-chip)", background: c.color }}/>
                      <span style={{ fontFamily: CV2.mono, fontSize: 12, fontWeight: 500, letterSpacing: ".02em", color: c.color, width: 70, flexShrink: 0 }}>
                        {timeOf(ev)}
                      </span>
                      <span style={{ fontSize: 14, color: CV2.ink, flex: 1, minWidth: 0 }}>{ev.title || "(busy)"}</span>
                      {v.sel && <span style={{ fontFamily: CV2.mono, fontSize: 10, color: c.color }}>{"\u25CF"}</span>}
                    </div>
                  );
                })}
              </div>
            )
        )}

        {status === "ok" && view === "week" && (
          <div className="kpi-grid" style={{ display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 12 }}>
            {weekCells.map(col => {
              const cObj = new Date(col.date + "T00:00:00Z");
              const isToday = col.date === today;
              return (
                <div key={col.date} style={{ display: "flex", flexDirection: "column", gap: 9 }}>
                  <div style={{ display: "flex", flexDirection: "column", gap: 2, padding: "10px 12px", borderRadius: "var(--r-ctl)",
                    border: "1px solid " + (isToday ? cvHexA(CV2.accent, .45) : CV2.line),
                    background: isToday ? cvHexA(CV2.accent, .07) : "var(--surface-2)" }}>
                    <span style={{ fontFamily: CV2.mono, fontSize: 10, letterSpacing: ".14em", textTransform: "uppercase", color: CV2.muted }}>
                      {window.VaultDate(cObj, { weekday: "short" })}
                    </span>
                    <span style={{ fontFamily: CV2.mono, fontSize: 16, fontWeight: 600, color: isToday ? CV2.accent : CV2.sec }}>{cObj.getUTCDate()}</span>
                  </div>
                  {col.events.filter(ev => typeVisible(ev.title)).map((ev, i) => {
                    const c = calClassify(ev.title);
                    const v = visOf(col.date, ev);
                    return (
                      <div key={i} title={ev.title} onClick={() => toggleFocus(v.k)}
                        style={{ padding: "9px 10px", borderRadius: "var(--r-ctl)", cursor: "pointer",
                          opacity: v.dim ? .26 : 1, borderLeft: "3px solid " + c.color,
                          background: v.sel ? cvHexA(c.color, .12) : "var(--surface-2)",
                          boxShadow: v.sel ? ("0 0 0 1px " + cvHexA(c.color, .55) + ", 0 0 14px " + cvHexA(c.color, .32)) : "none",
                          transition: "opacity .2s, box-shadow .2s, background .2s" }}>
                        <div style={{ fontFamily: CV2.mono, fontSize: 10, color: c.color, marginBottom: 4 }}>{timeOf(ev)}</div>
                        <div style={{ fontSize: 12, color: CV2.ink, lineHeight: 1.35 }}>{ev.title || "(busy)"}</div>
                      </div>
                    );
                  })}
                  {col.events.length === 0 && (
                    <div style={{ fontFamily: CV2.mono, fontSize: 10, color: "color-mix(in srgb, var(--ink) 25%, transparent)", textAlign: "center", padding: "8px 0" }}>{"\u2014"}</div>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </div>

      {/* Touchpoint forecast — update-call cadence, forward-looking */}
      {status === "ok" && touchpoints.some(tp => tp.type === "update") && (
        <div style={{ ...CV2.panel, overflow: "hidden" }}>
          <div style={{ padding: "16px 24px 12px", borderBottom: "1px solid " + CV2.hair }}>
            <div style={{ ...CV2_KICKER, marginBottom: 4 }}>Touchpoint Forecast</div>
            <strong style={{ fontSize: 15, color: CV2.ink }}>Update Call Cadence</strong>
            <span style={{ fontFamily: CV2.mono, fontSize: 11, color: CV2.muted, marginLeft: 8 }}>next update calls from your calendar</span>
          </div>
          <div style={{ padding: "6px 0" }}>
            {touchpoints.filter(tp => tp.type === "update").slice(0, 12).map((tp, i) => {
              const tColor = { update: "var(--seq-6)", conf: "var(--cat-3)", intro: "var(--evt-call)", visit: "var(--warn)" }[tp.type] || "var(--evt-meeting)";
              const dayLabel = tp.days === 0 ? "Today" : tp.days === 1 ? "Tomorrow" : (tp.days + " days");
              return (
                <div key={i} style={{ display: "flex", alignItems: "center", gap: 12, padding: "9px 16px",
                  borderTop: i ? "1px solid " + CV2.hair : "none" }}>
                  <span style={{ fontSize: 9.5, fontWeight: 700, padding: "3px 7px", borderRadius: "var(--r-chip)",
                    background: tColor + "1a", color: tColor, minWidth: 70, textAlign: "center",
                    textTransform: "uppercase", letterSpacing: ".03em" }}>{tp.sub}</span>
                  <div style={{ flex: "1 1 auto", minWidth: 0 }}>
                    <div style={{ fontSize: 13.5, fontWeight: 600, color: CV2.ink, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                      {tp.label}
                      {!tp.matched && <span title="Not matched to a Vault client" style={{ marginLeft: 6, fontSize: 10, color: CV2.muted, fontWeight: 400 }}>(unmatched)</span>}
                    </div>
                    <div style={{ fontSize: 11, color: CV2.muted }}>{tp.title}</div>
                  </div>
                  <div style={{ textAlign: "right", flexShrink: 0 }}>
                    <div style={{ fontSize: 15, fontWeight: 700, color: tp.days <= 2 ? tColor : CV2.ink }}>{dayLabel}</div>
                    <div style={{ fontSize: 10.5, color: CV2.muted }}>
                      {window.VaultDate(tp.date + "T00:00:00Z", { month: "short", day: "numeric" })}
                      {tp.time ? <span style={{ fontFamily: CV2.mono, marginLeft: 6, color: CV2.sec }}>{tp.time}</span> : null}
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      )}

      {showFeedModal && (
        <CalFeedModal url={urlInput} setUrl={setUrlInput} onSave={saveUrl}
          onDisconnect={disconnect} onClose={() => setShowFeedModal(false)}/>
      )}
    </div>
  );
}

window.CalendarScreen = CalendarScreen;
