---
title: "Workspace"
description: "Personal productivity page — private."
page-layout: full
toc: false
sidebar: false
comments: false
search: false
slug: workspace
code-tools: false
repo-actions: false
anchor-sections: false
number-sections: false
execute:
  echo: false
format:
  html:
    include-in-header:
      text: |
        <meta name="robots" content="noindex, nofollow">
---

```{=html}
<style>
/* ═════════════════════════════════════════════════════════
   WORKSPACE — scoped styles. Inherits site fonts & respects
   Quarto's light/dark toggle via body.quarto-dark selector.
═════════════════════════════════════════════════════════ */

#ws-root {
  /* Site-matching palette */
  --ws-primary:   #4895ab;
  --ws-primary-2: #5aa9bd;
  --ws-accent:    #d46c5b;
  --ws-success:   #7aa864;
  --ws-info:      #aabfc9;
  --ws-warn:      #e0a23a;

  /* Light-mode surfaces (default) */
  --ws-bg:       #ffffff;
  --ws-bg-2:     #f8f9fa;
  --ws-bg-3:     #f1f3f5;
  --ws-border:   #dee2e6;
  --ws-border-2: #ced4da;
  --ws-text:     #333333;
  --ws-text-2:   #6c757d;
  --ws-text-3:   #adb5bd;
  --ws-shadow:   0 2px 8px rgba(0,0,0,0.06);

  --ws-serif:    "PT Serif", Georgia, "Times New Roman", serif;
  --ws-mono:     "Fira Code", SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  --ws-sans:     -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

body.quarto-dark #ws-root {
  --ws-bg:       #1a1a1a;
  --ws-bg-2:     #242424;
  --ws-bg-3:     #2d2d2d;
  --ws-border:   #404040;
  --ws-border-2: #525252;
  --ws-text:     #e0e0e0;
  --ws-text-2:   #adb5bd;
  --ws-text-3:   #6c757d;
  --ws-shadow:   0 2px 8px rgba(0,0,0,0.3);
}

#ws-root * { box-sizing: border-box; }

#ws-root {
  font-family: var(--ws-sans);
  color: var(--ws-text);
  width: 100%;
  min-height: calc(100vh - 200px);
  display: block;
  position: relative;
}

/* Kill Quarto's article max-width for this page */
main.content, #quarto-content main.content {
  max-width: none !important;
  padding: 0 !important;
}
body article, main article, #quarto-content article {
  max-width: none !important;
  padding: 0 !important;
  font-size: inherit !important;
  line-height: inherit !important;
}
body article p { font-size: inherit !important; line-height: inherit !important; }

/* Hide default Quarto title block on this page */
#title-block-header { display: none !important; }

/* ═══════════════════════════════════════════════
   LOCK SCREEN
═══════════════════════════════════════════════ */
.ws-lock {
  position: fixed;
  inset: 57px 0 0 0;     /* below site navbar */
  background: var(--ws-bg);
  z-index: 900;
  display: flex; align-items: center; justify-content: center;
  transition: opacity 0.35s ease;
}
.ws-lock.dismissed { opacity: 0; pointer-events: none; }

.ws-lock-card {
  width: 380px; max-width: calc(100% - 2rem);
  background: var(--ws-bg);
  border: 1px solid var(--ws-border);
  border-radius: 8px;
  padding: 40px 36px 32px;
  text-align: center;
  box-shadow: var(--ws-shadow);
  position: relative;
}
.ws-lock-card::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: linear-gradient(90deg, var(--ws-primary), var(--ws-accent));
  border-radius: 8px 8px 0 0;
}

.ws-lock-brand {
  font-family: var(--ws-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--ws-primary);
  text-transform: uppercase;
  margin-bottom: 22px;
}

.ws-lock-icon {
  width: 52px; height: 52px; margin: 0 auto 18px;
  background: rgba(72,149,171,0.1);
  border: 1px solid rgba(72,149,171,0.25);
  border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  color: var(--ws-primary);
}

.ws-lock-title {
  font-family: var(--ws-serif);
  font-size: 24px; font-weight: 700;
  color: var(--ws-text);
  margin-bottom: 6px;
}
.ws-lock-sub {
  font-size: 13px; color: var(--ws-text-2);
  margin-bottom: 26px;
  line-height: 1.5;
}

.ws-input {
  width: 100%;
  background: var(--ws-bg-2);
  border: 1px solid var(--ws-border);
  border-radius: 6px;
  padding: 10px 14px;
  font-size: 14px; color: var(--ws-text);
  font-family: var(--ws-mono);
  letter-spacing: 0.08em;
  outline: none;
  text-align: center;
  transition: border-color 0.15s;
  margin-bottom: 10px;
}
.ws-input:focus { border-color: var(--ws-primary); }
.ws-input.err { border-color: var(--ws-accent); animation: shake 0.35s ease; }
@keyframes shake {
  0%,100% { transform: translateX(0); }
  25%,75% { transform: translateX(-5px); }
  50%     { transform: translateX(5px); }
}

.ws-btn-primary {
  width: 100%;
  background: var(--ws-primary);
  border: none; border-radius: 6px;
  padding: 10px 16px;
  color: #fff;
  font-size: 13px; font-weight: 600;
  font-family: var(--ws-sans);
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background 0.15s, transform 0.1s;
}
.ws-btn-primary:hover { background: var(--ws-primary-2); transform: translateY(-1px); }

.ws-lock-err {
  font-size: 11px; color: var(--ws-accent);
  margin-top: 8px; min-height: 15px;
  font-family: var(--ws-mono);
}
.ws-lock-links {
  margin-top: 18px;
  font-size: 11px; color: var(--ws-text-3);
  font-family: var(--ws-mono);
}
.ws-lock-links a {
  color: var(--ws-text-2); cursor: pointer;
  text-decoration: none; border-bottom: 1px dotted var(--ws-border-2);
}
.ws-lock-links a:hover { color: var(--ws-primary); border-color: var(--ws-primary); }

/* ═══════════════════════════════════════════════
   APP SHELL
═══════════════════════════════════════════════ */
#ws-app { display: none; }
#ws-app.visible {
  display: flex;
  height: calc(100vh - 57px);
  width: 100%;
}

/* Sidebar */
.ws-sidebar {
  width: 220px; flex-shrink: 0;
  background: var(--ws-bg-2);
  border-right: 1px solid var(--ws-border);
  padding: 14px 10px;
  display: flex; flex-direction: column;
  overflow-y: auto;
}
.ws-brand {
  padding: 4px 8px 14px;
  display: flex; align-items: center; gap: 9px;
  border-bottom: 1px solid var(--ws-border);
  margin-bottom: 10px;
}
.ws-brand-icon {
  width: 26px; height: 26px;
  background: var(--ws-primary);
  border-radius: 5px;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 700; font-size: 11px;
  font-family: var(--ws-mono);
}
.ws-brand-name {
  font-family: var(--ws-serif);
  font-size: 15px; font-weight: 700;
  color: var(--ws-text);
}
.ws-sec-label {
  font-family: var(--ws-mono);
  font-size: 9px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ws-text-3);
  padding: 8px 10px 4px;
}
.ws-nav {
  display: flex; align-items: center; justify-content: space-between;
  padding: 7px 10px; border-radius: 5px;
  font-size: 13px; color: var(--ws-text-2);
  cursor: pointer; user-select: none;
  transition: background 0.1s, color 0.1s;
  border-left: 3px solid transparent;
  margin-bottom: 2px;
}
.ws-nav:hover { background: var(--ws-bg-3); color: var(--ws-text); }
.ws-nav.active {
  background: rgba(72,149,171,0.1);
  color: var(--ws-primary);
  border-left-color: var(--ws-primary);
}
.ws-nav-left { display: flex; align-items: center; gap: 8px; }
.ws-nav-icon { width: 16px; text-align: center; }
.ws-nav-count {
  font-family: var(--ws-mono);
  font-size: 10px; color: var(--ws-text-3);
  background: var(--ws-bg-3);
  padding: 1px 6px; border-radius: 10px;
}
.ws-sidebar-footer {
  margin-top: auto; padding: 10px;
  border-top: 1px solid var(--ws-border);
  display: flex; align-items: center; justify-content: space-between;
  font-size: 11px; color: var(--ws-text-3);
  font-family: var(--ws-mono);
}
.ws-lock-btn {
  background: none; border: none;
  color: var(--ws-text-3); cursor: pointer;
  padding: 3px; border-radius: 3px;
}
.ws-lock-btn:hover { color: var(--ws-accent); }

/* Main */
.ws-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.ws-header {
  padding: 14px 20px 10px;
  border-bottom: 1px solid var(--ws-border);
  display: flex; align-items: center; justify-content: space-between;
  background: var(--ws-bg);
  flex-shrink: 0;
}
.ws-header-l { display: flex; align-items: center; gap: 10px; }
.ws-proj-title {
  font-family: var(--ws-serif);
  font-size: 19px; font-weight: 700;
  color: var(--ws-text);
}
.ws-count-badge {
  font-family: var(--ws-mono);
  font-size: 10px;
  background: var(--ws-bg-3);
  border: 1px solid var(--ws-border);
  color: var(--ws-text-2);
  padding: 2px 8px; border-radius: 20px;
}
.ws-header-r { display: flex; align-items: center; gap: 6px; }

.ws-vbtn {
  background: var(--ws-bg-2);
  border: 1px solid var(--ws-border);
  border-radius: 5px;
  padding: 5px 10px;
  font-size: 11px; font-family: var(--ws-sans);
  color: var(--ws-text-2); cursor: pointer; font-weight: 500;
  transition: all 0.1s;
}
.ws-vbtn:hover { background: var(--ws-bg-3); color: var(--ws-text); }
.ws-vbtn.active {
  background: var(--ws-primary); color: #fff;
  border-color: var(--ws-primary);
}
.ws-add-btn {
  background: var(--ws-accent); color: #fff;
  border: none; border-radius: 5px;
  padding: 6px 13px;
  font-size: 12px; font-weight: 600;
  font-family: var(--ws-sans); cursor: pointer;
  transition: background 0.12s, transform 0.1s;
}
.ws-add-btn:hover { background: #be5a4a; transform: translateY(-1px); }

/* Filter chips */
.ws-filter-bar {
  padding: 8px 20px 0;
  display: flex; gap: 5px; flex-shrink: 0;
}
.ws-chip {
  background: transparent;
  border: 1px solid var(--ws-border);
  border-radius: 20px;
  padding: 3px 12px;
  font-size: 11px; font-weight: 500;
  color: var(--ws-text-2); cursor: pointer;
  font-family: var(--ws-sans);
  text-transform: capitalize;
}
.ws-chip:hover { border-color: var(--ws-primary); color: var(--ws-primary); }
.ws-chip.active {
  background: var(--ws-primary);
  color: #fff; border-color: var(--ws-primary);
}

/* Add form */
.ws-add-form {
  margin: 10px 20px 0; padding: 13px 14px;
  background: rgba(72,149,171,0.05);
  border: 1px solid rgba(72,149,171,0.25);
  border-radius: 6px;
  display: none; flex-direction: column; gap: 9px;
  flex-shrink: 0;
}
.ws-add-form.open { display: flex; }
#ws-add-title {
  background: transparent; border: none; outline: none;
  font-size: 13px; color: var(--ws-text);
  font-family: var(--ws-sans); width: 100%;
}
#ws-add-title::placeholder { color: var(--ws-text-3); }
.ws-add-row { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; }
.ws-add-row select, .ws-add-row input[type="date"] {
  background: var(--ws-bg); border: 1px solid var(--ws-border);
  border-radius: 4px; padding: 3px 7px;
  font-size: 11px; color: var(--ws-text);
  font-family: var(--ws-sans); cursor: pointer; outline: none;
}
.ws-add-row input[type="text"] {
  flex: 1; min-width: 110px;
  background: transparent; border: 1px solid var(--ws-border);
  border-radius: 4px; padding: 3px 7px;
  font-size: 11px; color: var(--ws-text);
  font-family: var(--ws-sans); outline: none;
}
.ws-add-row input[type="text"]::placeholder { color: var(--ws-text-3); }
.ws-confirm {
  background: var(--ws-primary); border: none; border-radius: 4px;
  padding: 4px 12px; color: #fff;
  font-size: 11px; font-weight: 600; cursor: pointer;
  font-family: var(--ws-sans);
}
.ws-cancel {
  background: none; border: none;
  font-size: 11px; color: var(--ws-text-3);
  cursor: pointer; font-family: var(--ws-sans);
}

/* Content area */
.ws-content { flex: 1; display: flex; overflow: hidden; }
.ws-view-hidden { display: none !important; }

/* List */
.ws-list { flex: 1; overflow-y: auto; padding: 10px 20px 20px; }
.ws-sec-head {
  display: flex; align-items: center; gap: 7px;
  padding: 4px 6px; margin: 18px 0 5px;
}
.ws-sec-head:first-child { margin-top: 4px; }
.ws-sec-name {
  font-family: var(--ws-mono);
  font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ws-text-3);
}
.ws-sec-count {
  font-family: var(--ws-mono);
  font-size: 10px; color: var(--ws-text-3);
  background: var(--ws-bg-3); padding: 0 6px; border-radius: 9px;
}

.ws-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px; border-radius: 5px;
  cursor: pointer; border: 1px solid transparent;
  transition: background 0.1s;
}
.ws-row:hover { background: var(--ws-bg-2); }
.ws-row.selected {
  background: rgba(72,149,171,0.08);
  border-color: rgba(72,149,171,0.2);
}
.ws-check {
  width: 15px; height: 15px; flex-shrink: 0;
  border: 2px solid var(--ws-text-3);
  border-radius: 3px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: all 0.1s;
}
.ws-check.done { background: var(--ws-primary); border-color: var(--ws-primary); }
.ws-check.done::after { content: '✓'; color: #fff; font-size: 10px; font-weight: bold; }
.ws-row-body { flex: 1; min-width: 0; }
.ws-row-title {
  font-size: 13.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--ws-text);
}
.ws-row-title.done {
  text-decoration: line-through;
  color: var(--ws-text-3);
}
.ws-labels { display: flex; gap: 4px; margin-top: 3px; flex-wrap: wrap; }
.ws-label {
  font-family: var(--ws-mono);
  font-size: 9px; color: var(--ws-text-2);
  background: var(--ws-bg-3);
  padding: 1px 5px; border-radius: 6px;
}
.ws-row-meta { display: flex; align-items: center; gap: 7px; flex-shrink: 0; }
.ws-due {
  font-family: var(--ws-mono);
  font-size: 10px;
}
.ws-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.ws-del {
  background: none; border: none;
  color: var(--ws-text-3); font-size: 14px; cursor: pointer;
  opacity: 0; padding: 0 3px; line-height: 1;
}
.ws-row:hover .ws-del { opacity: 1; }
.ws-del:hover { color: var(--ws-accent); }

/* Board */
.ws-board {
  flex: 1; overflow-x: auto; display: flex; gap: 12px;
  padding: 14px 20px; align-items: flex-start;
}
.ws-col {
  width: 260px; flex-shrink: 0;
  background: var(--ws-bg-2);
  border: 1px solid var(--ws-border);
  border-radius: 8px; padding: 11px;
  max-height: calc(100vh - 200px);
  display: flex; flex-direction: column; gap: 7px;
  overflow: hidden;
}
.ws-col-head {
  display: flex; justify-content: space-between; align-items: center;
  padding-bottom: 4px; margin-bottom: 2px;
  border-bottom: 1px solid var(--ws-border);
}
.ws-col-title {
  font-family: var(--ws-mono);
  font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ws-text-3);
}
.ws-col-count {
  font-family: var(--ws-mono);
  font-size: 10px; color: var(--ws-text-3);
  background: var(--ws-bg-3); padding: 0 5px; border-radius: 8px;
}
.ws-col-cards { overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
.ws-card {
  background: var(--ws-bg);
  border: 1px solid var(--ws-border);
  border-left: 3px solid var(--ws-primary);
  border-radius: 5px; padding: 10px 11px;
  cursor: pointer; transition: border-color 0.1s, box-shadow 0.1s;
}
.ws-card:hover { border-color: var(--ws-border-2); box-shadow: var(--ws-shadow); }
.ws-card.selected {
  border-left-color: var(--ws-accent);
  background: rgba(72,149,171,0.05);
}
.ws-card-title {
  font-size: 12.5px; line-height: 1.45;
  margin-bottom: 7px; color: var(--ws-text);
}
.ws-card-title.done {
  text-decoration: line-through; color: var(--ws-text-3);
}
.ws-card-foot {
  display: flex; justify-content: space-between; align-items: center;
}

/* Notes */
.ws-notes {
  flex: 1; padding: 22px 32px;
  overflow-y: auto; max-width: 820px;
}
.ws-notes-head {
  display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px;
}
.ws-notes-title {
  font-family: var(--ws-serif);
  font-size: 22px; font-weight: 700;
  color: var(--ws-text);
}
.ws-notes-hint {
  font-family: var(--ws-mono);
  font-size: 10px; color: var(--ws-text-3);
}
#ws-notes-area {
  width: 100%; height: calc(100vh - 260px);
  background: var(--ws-bg-2);
  border: 1px solid var(--ws-border);
  border-radius: 6px; padding: 18px;
  color: var(--ws-text);
  font-size: 13.5px; line-height: 1.75;
  font-family: var(--ws-mono);
  outline: none; resize: none;
  transition: border-color 0.15s;
}
#ws-notes-area:focus { border-color: var(--ws-primary); }

/* Detail panel */
.ws-detail {
  width: 300px; flex-shrink: 0;
  background: var(--ws-bg-2);
  border-left: 1px solid var(--ws-border);
  padding: 16px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px;
  animation: slideIn 0.18s ease;
}
@keyframes slideIn {
  from { transform: translateX(20px); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}
.ws-detail-head {
  display: flex; justify-content: space-between; align-items: flex-start;
}
.ws-detail-title {
  font-family: var(--ws-serif);
  font-size: 15px; font-weight: 700; line-height: 1.4;
  flex: 1; margin-right: 8px; color: var(--ws-text);
}
.ws-detail-close {
  background: none; border: none;
  color: var(--ws-text-3); font-size: 18px;
  cursor: pointer; line-height: 1;
}
.ws-detail-close:hover { color: var(--ws-text); }

.ws-detail-label {
  font-family: var(--ws-mono);
  font-size: 9px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ws-text-3);
  margin-bottom: 6px;
}
.ws-badges { display: flex; flex-wrap: wrap; gap: 5px; }
.ws-badge {
  font-size: 11px; padding: 2px 9px;
  border-radius: 20px; font-weight: 500;
}
.ws-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.ws-tag {
  font-family: var(--ws-mono);
  font-size: 9px; padding: 2px 7px; border-radius: 9px;
  background: rgba(72,149,171,0.1);
  color: var(--ws-primary);
}
.ws-sec-btns { display: flex; flex-wrap: wrap; gap: 4px; }
.ws-sec-btn {
  background: var(--ws-bg);
  border: 1px solid var(--ws-border);
  border-radius: 4px; padding: 3px 9px;
  font-size: 11px; color: var(--ws-text-2);
  cursor: pointer; font-family: var(--ws-sans);
}
.ws-sec-btn:hover { border-color: var(--ws-primary); color: var(--ws-primary); }
.ws-sec-btn.active {
  background: var(--ws-primary); color: #fff;
  border-color: var(--ws-primary);
}
.ws-detail-notes {
  width: 100%; height: 100px;
  background: var(--ws-bg);
  border: 1px solid var(--ws-border);
  border-radius: 5px; padding: 9px;
  color: var(--ws-text); font-size: 12px;
  font-family: var(--ws-sans); line-height: 1.5;
  outline: none; resize: none;
}
.ws-detail-notes:focus { border-color: var(--ws-primary); }
.ws-delete {
  margin-top: auto;
  background: transparent;
  border: 1px solid var(--ws-accent);
  color: var(--ws-accent);
  border-radius: 5px; padding: 6px;
  font-size: 12px; font-weight: 500; cursor: pointer;
  font-family: var(--ws-sans);
}
.ws-delete:hover { background: var(--ws-accent); color: #fff; }

/* Empty */
.ws-empty {
  text-align: center; color: var(--ws-text-3);
  padding: 50px 20px;
  font-family: var(--ws-mono); font-size: 12px; line-height: 1.8;
}

/* Scrollbars */
#ws-root *::-webkit-scrollbar { width: 5px; height: 5px; }
#ws-root *::-webkit-scrollbar-track { background: transparent; }
#ws-root *::-webkit-scrollbar-thumb {
  background: var(--ws-border-2); border-radius: 3px;
}

/* Zotero panel */
.ws-zotero { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.ws-zt-toolbar {
  padding: 12px 20px 10px;
  border-bottom: 1px solid var(--ws-border);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  background: var(--ws-bg); flex-shrink: 0;
}
.ws-zt-search {
  flex: 1; min-width: 160px;
  background: var(--ws-bg-2); border: 1px solid var(--ws-border);
  border-radius: 5px; padding: 6px 10px;
  font-size: 12px; color: var(--ws-text);
  font-family: var(--ws-sans); outline: none;
}
.ws-zt-search:focus { border-color: var(--ws-primary); }
.ws-zt-setup-btn {
  background: var(--ws-bg-2); border: 1px solid var(--ws-border);
  border-radius: 5px; padding: 5px 10px;
  font-size: 11px; color: var(--ws-text-2);
  font-family: var(--ws-sans); cursor: pointer;
}
.ws-zt-setup-btn:hover { border-color: var(--ws-primary); color: var(--ws-primary); }
.ws-zt-reload-btn {
  background: var(--ws-primary); border: none;
  border-radius: 5px; padding: 5px 11px;
  font-size: 11px; font-weight: 600; color: #fff;
  font-family: var(--ws-sans); cursor: pointer;
}
.ws-zt-reload-btn:hover { background: var(--ws-primary-2); }
.ws-zt-body { flex: 1; display: flex; overflow: hidden; }
.ws-zt-collections {
  width: 200px; flex-shrink: 0;
  border-right: 1px solid var(--ws-border);
  overflow-y: auto; padding: 8px 6px;
  background: var(--ws-bg-2);
}
.ws-zt-coll {
  padding: 6px 10px; border-radius: 4px;
  font-size: 12px; color: var(--ws-text-2);
  cursor: pointer; border-left: 3px solid transparent;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ws-zt-coll:hover { background: var(--ws-bg-3); color: var(--ws-text); }
.ws-zt-coll.active {
  background: rgba(72,149,171,0.1);
  color: var(--ws-primary); border-left-color: var(--ws-primary);
}
.ws-zt-items { flex: 1; overflow-y: auto; padding: 10px 18px; }
.ws-zt-item {
  padding: 9px 11px; border-radius: 5px;
  border: 1px solid var(--ws-border);
  margin-bottom: 6px; cursor: pointer;
  transition: border-color 0.1s, background 0.1s;
  background: var(--ws-bg);
}
.ws-zt-item:hover { border-color: var(--ws-primary); background: var(--ws-bg-2); }
.ws-zt-item-title {
  font-size: 13px; color: var(--ws-text); line-height: 1.4;
  margin-bottom: 3px;
}
.ws-zt-item-meta {
  font-family: var(--ws-mono); font-size: 10px;
  color: var(--ws-text-3); display: flex; gap: 8px; flex-wrap: wrap;
}
.ws-zt-item-type {
  font-family: var(--ws-mono); font-size: 9px;
  background: rgba(72,149,171,0.1); color: var(--ws-primary);
  padding: 1px 6px; border-radius: 6px;
  text-transform: uppercase;
}
.ws-zt-copy {
  float: right; background: none; border: none;
  color: var(--ws-text-3); font-size: 12px;
  cursor: pointer; padding: 0 2px;
  font-family: var(--ws-mono);
}
.ws-zt-copy:hover { color: var(--ws-primary); }
.ws-zt-setup {
  flex: 1; display: flex; align-items: center; justify-content: center;
  padding: 40px 30px;
}
.ws-zt-setup-card {
  width: 420px; max-width: 100%;
  background: var(--ws-bg-2); border: 1px solid var(--ws-border);
  border-radius: 8px; padding: 30px 28px;
}
.ws-zt-setup-title {
  font-family: var(--ws-serif); font-size: 18px; font-weight: 700;
  color: var(--ws-text); margin-bottom: 6px;
}
.ws-zt-setup-hint {
  font-size: 12px; color: var(--ws-text-2); margin-bottom: 18px; line-height: 1.5;
}
.ws-zt-field { margin-bottom: 12px; }
.ws-zt-field label {
  display: block; font-family: var(--ws-mono);
  font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ws-text-3); margin-bottom: 4px;
}
.ws-zt-field input {
  width: 100%; background: var(--ws-bg);
  border: 1px solid var(--ws-border); border-radius: 5px;
  padding: 7px 10px; font-size: 12px;
  color: var(--ws-text); font-family: var(--ws-mono); outline: none;
}
.ws-zt-field input:focus { border-color: var(--ws-primary); }
.ws-zt-save-btn {
  width: 100%; background: var(--ws-primary);
  border: none; border-radius: 5px;
  padding: 9px; color: #fff;
  font-size: 13px; font-weight: 600;
  font-family: var(--ws-sans); cursor: pointer;
  margin-top: 4px;
}
.ws-zt-save-btn:hover { background: var(--ws-primary-2); }
.ws-zt-msg {
  font-family: var(--ws-mono); font-size: 11px;
  color: var(--ws-text-2); margin-top: 10px; min-height: 16px;
}

/* ─── Week-at-a-glance view ─── */
.ws-week { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.ws-wk-head {
  padding: 12px 20px 10px;
  border-bottom: 1px solid var(--ws-border);
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 14px; flex-wrap: wrap; flex-shrink: 0;
  background: var(--ws-bg);
}
.ws-wk-title {
  font-family: var(--ws-serif);
  font-size: 17px; font-weight: 700; color: var(--ws-text);
}
.ws-wk-range {
  font-family: var(--ws-mono); font-size: 11px;
  color: var(--ws-text-2); font-variant-numeric: tabular-nums;
}
.ws-wk-actions { display: flex; align-items: center; gap: 10px; }
.ws-wk-toggle {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; color: var(--ws-text-2);
  font-family: var(--ws-sans); cursor: pointer; user-select: none;
}
.ws-wk-toggle input { margin: 0; }
.ws-wk-status {
  padding: 10px 20px; font-size: 12px;
  color: var(--ws-text-2); background: var(--ws-bg-2);
  border-bottom: 1px solid var(--ws-border);
}
.ws-wk-status.warn {
  color: var(--ws-warn); background: rgba(224,162,58,0.08);
  border-bottom-color: rgba(224,162,58,0.3);
}
.ws-wk-status.error {
  color: var(--ws-accent); background: rgba(212,108,91,0.08);
  border-bottom-color: rgba(212,108,91,0.3);
}
.ws-wk-grid {
  flex: 1; overflow-y: auto;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 12px; padding: 14px 18px 20px;
  align-content: start;
}
.ws-wk-day {
  background: var(--ws-bg);
  border: 1px solid var(--ws-border);
  border-radius: 7px; padding: 11px 13px;
  display: flex; flex-direction: column; gap: 9px;
  min-height: 130px;
}
.ws-wk-day.today {
  border-color: var(--ws-accent);
  box-shadow: 0 0 0 1px rgba(212,108,91,0.25);
}
.ws-wk-day.weekend { background: var(--ws-bg-2); }
.ws-wk-dhead {
  display: flex; align-items: baseline; justify-content: space-between;
}
.ws-wk-dname {
  font-family: var(--ws-sans); font-weight: 600; font-size: 13px;
  color: var(--ws-text);
}
.ws-wk-day.today .ws-wk-dname { color: var(--ws-accent); }
.ws-wk-ddate {
  font-family: var(--ws-mono); font-size: 10px;
  color: var(--ws-text-3); font-variant-numeric: tabular-nums;
}
.ws-wk-sum {
  font-size: 11px; color: var(--ws-text-2);
  background: var(--ws-bg-2);
  padding: 6px 9px; border-radius: 5px;
  border-left: 3px solid var(--ws-border-2);
}
.ws-wk-day.today .ws-wk-sum { border-left-color: var(--ws-accent); }
.ws-wk-sum-stats {
  color: var(--ws-text-3); margin-top: 2px;
  font-family: var(--ws-mono); font-size: 10px;
  font-variant-numeric: tabular-nums;
}
.ws-wk-evs { display: flex; flex-direction: column; gap: 3px; }
.ws-wk-ev {
  display: grid; grid-template-columns: 56px 1fr;
  gap: 7px; align-items: baseline;
  font-size: 11.5px; padding: 1px 0;
}
.ws-wk-ev .t {
  font-family: var(--ws-mono); font-size: 10.5px;
  color: var(--ws-text-3); font-variant-numeric: tabular-nums;
}
.ws-wk-ev .n {
  color: var(--ws-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ws-wk-ev.cancelled .n { color: var(--ws-text-3); text-decoration: line-through; }
.ws-wk-ev.cancelled .t { color: var(--ws-border-2); }
.ws-wk-ev.allday .t { color: var(--ws-accent); font-weight: 500; }
.ws-wk-ev.focus .n, .ws-wk-ev.lunch .n { color: var(--ws-success); }
.ws-wk-empty {
  color: var(--ws-text-3); font-style: italic;
  font-size: 11.5px; font-family: var(--ws-sans);
}
.ws-wk-blank {
  padding: 30px 20px; text-align: center;
  color: var(--ws-text-3); font-family: var(--ws-mono);
  font-size: 12px; line-height: 1.7;
}

/* Todoist — reuses .ws-zt-* setup styles; adds connection bits */
.ws-td-chip {
  font-family: var(--ws-mono);
  font-size: 9px; letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 1px 5px; border-radius: 8px;
  background: var(--ws-bg-3); color: var(--ws-text-3);
  margin-left: auto;
}
.ws-td-chip.on { background: rgba(122,168,100,0.15); color: var(--ws-success); }
.ws-td-chip.err { background: rgba(212,108,91,0.15); color: var(--ws-accent); }
.ws-td-panel {
  flex: 1; padding: 22px 28px; overflow-y: auto;
}
.ws-td-panel-head {
  display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px;
}
.ws-td-panel-title {
  font-family: var(--ws-serif); font-size: 20px; font-weight: 700; color: var(--ws-text);
}
.ws-td-panel-hint {
  font-family: var(--ws-mono); font-size: 10px; color: var(--ws-text-3);
}
.ws-td-status {
  font-family: var(--ws-mono); font-size: 11px;
  color: var(--ws-text-2);
  background: var(--ws-bg-2);
  border: 1px solid var(--ws-border);
  border-radius: 6px;
  padding: 9px 12px; margin-bottom: 14px;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.ws-td-status.ok    { color: var(--ws-success); border-color: rgba(122,168,100,0.35); }
.ws-td-status.warn  { color: var(--ws-warn);    border-color: rgba(224,162,58,0.35); }
.ws-td-status.error { color: var(--ws-accent);  border-color: rgba(212,108,91,0.35); }
.ws-td-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.ws-td-bridge-row {
  display: flex; align-items: center; gap: 6px;
  font-size: 11px; color: var(--ws-text-2);
  margin-top: 8px; font-family: var(--ws-sans);
}
.ws-td-projects {
  background: var(--ws-bg-2);
  border: 1px solid var(--ws-border);
  border-radius: 6px; padding: 10px 12px;
  font-family: var(--ws-mono); font-size: 11px;
  color: var(--ws-text-2);
  max-height: 240px; overflow-y: auto;
}
.ws-td-projects ul { margin: 4px 0 0 0; padding-left: 16px; }
.ws-td-projects li { line-height: 1.6; }
.ws-td-danger {
  background: transparent;
  border: 1px solid var(--ws-accent);
  color: var(--ws-accent);
  border-radius: 5px; padding: 6px 11px;
  font-size: 11px; font-weight: 500; cursor: pointer;
  font-family: var(--ws-sans);
}
.ws-td-danger:hover { background: var(--ws-accent); color: #fff; }
.ws-nav-badge {
  font-family: var(--ws-mono);
  font-size: 9px; letter-spacing: 0.05em;
  color: var(--ws-success);
  background: rgba(122,168,100,0.12);
  padding: 1px 5px; border-radius: 8px;
}
.ws-nav-badge.err { color: var(--ws-accent); background: rgba(212,108,91,0.12); }

/* Responsive */
@media (max-width: 768px) {
  .ws-sidebar { display: none; }
  .ws-detail { display: none !important; }
  .ws-header { padding: 10px 14px; flex-wrap: wrap; gap: 8px; }
  .ws-proj-title { font-size: 16px; }
  .ws-filter-bar { padding: 6px 14px 0; }
  .ws-list, .ws-notes { padding: 10px 14px; }
  .ws-zt-collections { display: none; }
  .ws-wk-grid { padding: 10px 14px; grid-template-columns: 1fr; }
  .ws-td-panel { padding: 14px 16px; }
}
</style>

<div id="ws-root">

  <!-- ═══════════════ LOCK ═══════════════ -->
  <div class="ws-lock" id="ws-lock">
    <div class="ws-lock-card">
      <div class="ws-lock-brand">Less Likely · Private</div>
      <div class="ws-lock-icon">
        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
          <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
        </svg>
      </div>
      <div class="ws-lock-title" id="ws-lock-title">Workspace</div>
      <div class="ws-lock-sub" id="ws-lock-sub">Personal productivity · private area</div>
      <input type="password" class="ws-input" id="ws-lock-input" placeholder="Enter password" autocomplete="current-password">
      <button class="ws-btn-primary" id="ws-lock-submit">Unlock →</button>
      <div class="ws-lock-err" id="ws-lock-err"></div>
      <div class="ws-lock-links">
        <a id="ws-reset">Reset</a> &nbsp;·&nbsp;
        <a href="/">← Home</a>
      </div>
    </div>
  </div>

  <!-- ═══════════════ APP ═══════════════ -->
  <div id="ws-app">

    <!-- Sidebar -->
    <div class="ws-sidebar">
      <div class="ws-brand">
        <div class="ws-brand-icon">LL</div>
        <div class="ws-brand-name">Workspace</div>
      </div>

      <div class="ws-sec-label">Tools</div>
      <div class="ws-nav" data-nav="zotero">
        <div class="ws-nav-left"><span class="ws-nav-icon">📚</span> References</div>
      </div>
      <div class="ws-nav" data-nav="todoist">
        <div class="ws-nav-left"><span class="ws-nav-icon">✓</span> Todoist</div>
        <span class="ws-nav-badge" id="ws-td-nav-badge" style="display:none">offline</span>
      </div>

      <div class="ws-sec-label">Navigation</div>
      <div class="ws-nav" data-nav="today">
        <div class="ws-nav-left"><span class="ws-nav-icon">☀</span> Today</div>
      </div>
      <div class="ws-nav" data-nav="upcoming">
        <div class="ws-nav-left"><span class="ws-nav-icon">📅</span> Upcoming</div>
      </div>
      <div class="ws-nav" data-nav="week">
        <div class="ws-nav-left"><span class="ws-nav-icon">📅</span> Week</div>
      </div>

      <div class="ws-sec-label">Projects</div>
      <div id="ws-proj-nav"></div>

      <div class="ws-sidebar-footer">
        <span>stored locally</span>
        <button class="ws-lock-btn" id="ws-relock" title="Lock workspace">🔒</button>
      </div>
    </div>

    <!-- Main -->
    <div class="ws-main">
      <div class="ws-header">
        <div class="ws-header-l">
          <span id="ws-proj-icon" style="font-size:17px">💼</span>
          <span class="ws-proj-title" id="ws-proj-name">Work</span>
          <span class="ws-count-badge" id="ws-task-count">0 remaining</span>
        </div>
        <div class="ws-header-r">
          <button class="ws-vbtn active" data-view="list">≡ List</button>
          <button class="ws-vbtn" data-view="board">⊞ Board</button>
          <button class="ws-vbtn" data-view="notes">✎ Notes</button>
          <button class="ws-add-btn" id="ws-toggle-add">+ Task</button>
        </div>
      </div>

      <div class="ws-filter-bar" id="ws-filter-bar">
        <button class="ws-chip active" data-filter="all">All</button>
        <button class="ws-chip" data-filter="active">Active</button>
        <button class="ws-chip" data-filter="done">Done</button>
      </div>

      <div class="ws-add-form" id="ws-add-form">
        <input type="text" id="ws-add-title" placeholder="Task name…">
        <div class="ws-add-row">
          <select id="ws-add-priority">
            <option value="high">🔴 High</option>
            <option value="medium" selected>🟡 Medium</option>
            <option value="low">⚪ Low</option>
          </select>
          <input type="date" id="ws-add-due">
          <input type="text" id="ws-add-labels" placeholder="Labels (comma separated)">
          <button class="ws-confirm" id="ws-submit-add">Add</button>
          <button class="ws-cancel" id="ws-cancel-add">Cancel</button>
        </div>
      </div>

      <div class="ws-content">
        <div class="ws-list" id="ws-list-view"></div>
        <div class="ws-board ws-view-hidden" id="ws-board-view"></div>
        <div class="ws-notes ws-view-hidden" id="ws-notes-view">
          <div class="ws-notes-head">
            <span class="ws-notes-title" id="ws-notes-title">Notes</span>
            <span class="ws-notes-hint">auto-saved · localStorage</span>
          </div>
          <textarea id="ws-notes-area" placeholder="# Project Notes&#10;&#10;Meeting notes, links, ideas…"></textarea>
        </div>
        <div class="ws-detail ws-view-hidden" id="ws-detail"></div>
        <div class="ws-zotero ws-view-hidden" id="ws-zotero-view"></div>
        <div class="ws-week ws-view-hidden" id="ws-week-view"></div>
        <div class="ws-td-panel ws-view-hidden" id="ws-todoist-view"></div>
      </div>
    </div>
  </div>
</div>

<script>
(function() {
  const $ = id => document.getElementById(id);
  const $$ = sel => document.querySelectorAll(sel);

  async function sha256(str) {
    const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
    return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2,'0')).join('');
  }

  const today = () => new Date().toISOString().slice(0,10);

  function formatDue(d) {
    if (!d) return null;
    const diff = Math.round((new Date(d) - new Date(today())) / 86400000);
    if (diff < 0)   return { label: 'Overdue',  color: 'var(--ws-accent)' };
    if (diff === 0) return { label: 'Today',    color: 'var(--ws-success)' };
    if (diff === 1) return { label: 'Tomorrow', color: 'var(--ws-primary)' };
    const dt = new Date(d + 'T00:00:00');
    return { label: dt.toLocaleDateString('en-US', { month:'short', day:'numeric' }),
             color: 'var(--ws-text-2)' };
  }

  const PRIORITY = {
    high:   { color: 'var(--ws-accent)',  bg: 'rgba(212,108,91,0.12)', label: 'High' },
    medium: { color: 'var(--ws-warn)',    bg: 'rgba(224,162,58,0.12)', label: 'Medium' },
    low:    { color: 'var(--ws-text-3)',  bg: 'rgba(173,181,189,0.15)', label: 'Low' },
  };
  const SECTIONS = ['To Do', 'In Progress', 'Done'];

  // Local-only seed projects (used when Todoist isn't connected, or alongside it).
  // Project shape: { id, name, icon, tdId? (Todoist project id when synced) }
  const SEED_PROJECTS = [
    { id:'work',     name:'Work',     icon:'💼' },
    { id:'research', name:'Research', icon:'🔬' },
    { id:'personal', name:'Personal', icon:'🌿' },
    { id:'inbox',    name:'Inbox',    icon:'⬇' },
  ];

  // Returns true iff `tasks` looks like the untouched demo SEED (used for first-sync wipe).
  function tasksMatchSeed(tasks) {
    const seed = SEED();
    if (!Array.isArray(tasks) || tasks.length !== seed.length) return false;
    return tasks.every((t, i) => {
      const s = seed[i];
      return s && t && t.id === s.id && t.title === s.title && t.project === s.project;
    });
  }

  function SEED() {
    return [
      { id:1, title:'Finalize Q2 sales pipeline model',     project:'work',     priority:'high',   done:false, due:'2026-04-25', labels:['analytics','pipeline'], section:'In Progress', notes:'Check Snowflake staging tables first.' },
      { id:2, title:'LessLikely post: AIPW estimators',     project:'research', priority:'medium', done:false, due:'2026-04-30', labels:['statistics','writing'],  section:'To Do',       notes:'' },
      { id:3, title:'Review Azure Data Factory copy logs',  project:'work',     priority:'high',   done:false, due:'2026-04-24', labels:['azure','data'],          section:'To Do',       notes:'' },
      { id:4, title:'Set up Quarto site refresh',           project:'personal', priority:'low',    done:false, due:'2026-05-05', labels:['blog'],                  section:'To Do',       notes:'' },
      { id:5, title:'DAX: POS_TAX fuel measure validation', project:'work',     priority:'medium', done:true,  due:'2026-04-22', labels:['dax','powerbi'],         section:'Done',        notes:'' },
      { id:6, title:'GLP-1 bone density meta-analysis',     project:'research', priority:'low',    done:false, due:'2026-05-01', labels:['medical','statistics'],  section:'To Do',       notes:'' },
    ];
  }

  const Store = {
    KH:  'll_ws_hash',
    KT:  'll_ws_tasks',
    KN:  'll_ws_notes',
    KP:  'll_ws_projects',     // user's project list (local-only + cached Todoist)
    KTD: 'll_ws_td_cfg',       // Todoist config: { token, useMcp, mcpPrefix }
    KTC: 'll_ws_td_cache',     // Todoist cache: { projects, sections, tasks, fetchedAt }
    getHash() { return localStorage.getItem(this.KH); },
    setHash(h){ localStorage.setItem(this.KH, h); },
    clearHash(){ localStorage.removeItem(this.KH); },
    getTasks(){ try { return JSON.parse(localStorage.getItem(this.KT)) || SEED(); } catch { return SEED(); } },
    saveTasks(t){ localStorage.setItem(this.KT, JSON.stringify(t)); },
    getNotes(){ try { return JSON.parse(localStorage.getItem(this.KN)) || {}; } catch { return {}; } },
    saveNotes(n){ localStorage.setItem(this.KN, JSON.stringify(n)); },
    getProjects(){
      try { const v = JSON.parse(localStorage.getItem(this.KP)); return Array.isArray(v) && v.length ? v : SEED_PROJECTS.slice(); }
      catch { return SEED_PROJECTS.slice(); }
    },
    saveProjects(p){ localStorage.setItem(this.KP, JSON.stringify(p)); },
    getTdCfg(){ try { return JSON.parse(localStorage.getItem(this.KTD)) || {}; } catch { return {}; } },
    saveTdCfg(c){ localStorage.setItem(this.KTD, JSON.stringify(c)); },
    clearTdCfg(){ localStorage.removeItem(this.KTD); localStorage.removeItem(this.KTC); },
    getTdCache(){ try { return JSON.parse(localStorage.getItem(this.KTC)) || null; } catch { return null; } },
    saveTdCache(c){ localStorage.setItem(this.KTC, JSON.stringify(c)); },
    clearAll(){
      [this.KH, this.KT, this.KN, this.KP, this.KTD, this.KTC]
        .forEach(k => localStorage.removeItem(k));
    },
  };

  const S = {
    tasks:[], notes:{}, projects:[],
    project:'work', view:'list', filter:'all',
    selId:null, addOpen:false,
    td: { connected:false, token:'', useMcp:false, mcpPrefix:'', lastSync:null, status:'', statusKind:'', syncing:false },
  };
  const setState = p => { Object.assign(S, p); render(); };
  const getProject = () => S.projects.find(p => p.id === S.project) || S.projects[0] || null;
  const getSelected = () => S.tasks.find(t => t.id === S.selId) || null;

  const esc = s => String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');

  function render() {
    // Sidebar
    const nav = $('ws-proj-nav');
    nav.innerHTML = S.projects.map(p => {
      const c = S.tasks.filter(t => t.project === p.id && !t.done).length;
      const synced = !!p.tdId;
      return `<div class="ws-nav${S.project===p.id?' active':''}" data-proj="${p.id}" title="${synced ? 'Synced with Todoist' : 'Local only'}">
        <div class="ws-nav-left"><span class="ws-nav-icon">${p.icon || (synced ? '✓' : '•')}</span> ${esc(p.name)}</div>
        ${c>0 ? `<span class="ws-nav-count">${c}</span>` : ''}
      </div>`;
    }).join('');
    // Todoist nav badge state
    const badge = $('ws-td-nav-badge');
    if (badge) {
      if (S.td.syncing) { badge.style.display = ''; badge.textContent = 'syncing'; badge.classList.remove('err'); }
      else if (S.td.statusKind === 'error') { badge.style.display = ''; badge.textContent = 'error'; badge.classList.add('err'); }
      else if (S.td.connected) { badge.style.display = ''; badge.textContent = 'synced'; badge.classList.remove('err'); }
      else { badge.style.display = 'none'; }
    }
    nav.querySelectorAll('[data-proj]').forEach(el => {
      el.addEventListener('click', () => setState({ project: el.dataset.proj, selId:null, addOpen:false }));
    });

    // Header
    const p = getProject();
    const rem = S.tasks.filter(t => t.project === S.project && !t.done).length;
    $('ws-proj-icon').textContent = p.icon;
    $('ws-proj-name').textContent = p.name;
    $('ws-task-count').textContent = `${rem} remaining`;
    $$('.ws-vbtn').forEach(b => b.classList.toggle('active', b.dataset.view === S.view));
    $$('.ws-chip').forEach(b => b.classList.toggle('active', b.dataset.filter === S.filter));

    $('ws-filter-bar').classList.toggle('ws-view-hidden', S.view === 'notes');
    $('ws-add-form').classList.toggle('open', S.addOpen && S.view !== 'notes');

    $('ws-list-view').classList.toggle('ws-view-hidden', S.view !== 'list');
    $('ws-board-view').classList.toggle('ws-view-hidden', S.view !== 'board');
    $('ws-notes-view').classList.toggle('ws-view-hidden', S.view !== 'notes');

    if (S.view === 'list')  renderList();
    if (S.view === 'board') renderBoard();
    if (S.view === 'notes') renderNotes();
    renderDetail();
  }

  function projTasks() {
    return S.tasks.filter(t => {
      if (t.project !== S.project) return false;
      if (S.filter === 'done')   return t.done;
      if (S.filter === 'active') return !t.done;
      return true;
    });
  }

  function rowHTML(t) {
    const due = formatDue(t.due);
    const pr = PRIORITY[t.priority];
    const labels = (t.labels||[]).slice(0,3).map(l=>`<span class="ws-label">${esc(l)}</span>`).join('');
    return `<div class="ws-row${S.selId===t.id?' selected':''}" data-id="${t.id}">
      <div class="ws-check${t.done?' done':''}"></div>
      <div class="ws-row-body">
        <div class="ws-row-title${t.done?' done':''}">${esc(t.title)}</div>
        ${labels ? `<div class="ws-labels">${labels}</div>` : ''}
      </div>
      <div class="ws-row-meta">
        ${due ? `<span class="ws-due" style="color:${due.color}">${due.label}</span>` : ''}
        <div class="ws-dot" style="background:${pr.color}"></div>
        <button class="ws-del">×</button>
      </div>
    </div>`;
  }

  function renderList() {
    const el = $('ws-list-view');
    const tasks = projTasks();
    const secs = S.filter === 'done'   ? ['Done']
              : S.filter === 'active' ? ['To Do','In Progress']
              : SECTIONS;
    let html = '';
    secs.forEach(sec => {
      const st = tasks.filter(t => sec === 'Done' ? t.done : !t.done && t.section === sec);
      if (!st.length) return;
      html += `<div class="ws-sec-head">
        <span class="ws-sec-name">${sec}</span>
        <span class="ws-sec-count">${st.length}</span>
      </div>`;
      st.forEach(t => html += rowHTML(t));
    });
    if (!html) html = `<div class="ws-empty">Nothing here.<br>Add a task above ↑</div>`;
    el.innerHTML = html;
    el.querySelectorAll('.ws-row').forEach(r => {
      const id = +r.dataset.id;
      r.querySelector('.ws-check').addEventListener('click', e => { e.stopPropagation(); toggleDone(id); });
      r.querySelector('.ws-del').addEventListener('click', e => { e.stopPropagation(); deleteTask(id); });
      r.addEventListener('click', () => setState({ selId: S.selId === id ? null : id }));
    });
  }

  function renderBoard() {
    const el = $('ws-board-view');
    const tasks = projTasks();
    el.innerHTML = SECTIONS.map(sec => {
      const col = tasks.filter(t => sec === 'Done' ? t.done : !t.done && t.section === sec);
      const cards = col.map(t => {
        const due = formatDue(t.due);
        const pr = PRIORITY[t.priority];
        const labels = (t.labels||[]).slice(0,2).map(l=>`<span class="ws-label">${esc(l)}</span>`).join('');
        return `<div class="ws-card${S.selId===t.id?' selected':''}" data-id="${t.id}" style="border-left-color:${pr.color}">
          <div class="ws-card-title${t.done?' done':''}">${esc(t.title)}</div>
          <div class="ws-card-foot">
            <div class="ws-labels">${labels}</div>
            ${due ? `<span class="ws-due" style="color:${due.color}">${due.label}</span>` : ''}
          </div>
        </div>`;
      }).join('');
      return `<div class="ws-col">
        <div class="ws-col-head">
          <span class="ws-col-title">${sec}</span>
          <span class="ws-col-count">${col.length}</span>
        </div>
        <div class="ws-col-cards">${cards || '<div style="color:var(--ws-text-3);font-size:11px;padding:6px 2px;font-family:var(--ws-mono)">Empty</div>'}</div>
      </div>`;
    }).join('');
    el.querySelectorAll('.ws-card').forEach(c => {
      const id = +c.dataset.id;
      c.addEventListener('click', () => setState({ selId: S.selId === id ? null : id }));
    });
  }

  function renderNotes() {
    const p = getProject();
    $('ws-notes-title').textContent = `${p.icon} ${p.name} Notes`;
    const area = $('ws-notes-area');
    if (area._proj !== S.project) {
      area.value = S.notes[S.project] || '';
      area._proj = S.project;
    }
  }

  function renderDetail() {
    const d = $('ws-detail');
    const t = getSelected();
    if (!t || S.view === 'notes') { d.classList.add('ws-view-hidden'); return; }
    d.classList.remove('ws-view-hidden');
    const pr = PRIORITY[t.priority];
    const due = formatDue(t.due);
    const tags = (t.labels||[]).map(l=>`<span class="ws-tag">${esc(l)}</span>`).join('');
    const secBtns = SECTIONS.map(s=>`<button class="ws-sec-btn${(s==='Done'&&t.done)||(s===t.section&&!t.done)?' active':''}" data-sec="${s}">${s}</button>`).join('');
    d.innerHTML = `
      <div class="ws-detail-head">
        <div class="ws-detail-title">${esc(t.title)}</div>
        <button class="ws-detail-close">×</button>
      </div>
      <div class="ws-badges">
        <span class="ws-badge" style="background:${pr.bg};color:${pr.color}">${pr.label}</span>
        ${due ? `<span class="ws-badge" style="background:var(--ws-bg-3);color:${due.color}">📅 ${due.label}</span>` : ''}
      </div>
      ${tags ? `<div><div class="ws-detail-label">Labels</div><div class="ws-tags">${tags}</div></div>` : ''}
      <div><div class="ws-detail-label">Section</div><div class="ws-sec-btns">${secBtns}</div></div>
      <div><div class="ws-detail-label">Notes</div>
        <textarea class="ws-detail-notes" placeholder="Context, links, details…">${esc(t.notes||'')}</textarea>
      </div>
      <button class="ws-delete">Delete Task</button>`;
    d.querySelector('.ws-detail-close').addEventListener('click', () => setState({ selId:null }));
    d.querySelector('.ws-delete').addEventListener('click', () => deleteTask(t.id));
    d.querySelectorAll('.ws-sec-btn').forEach(b => b.addEventListener('click', () => moveSection(t.id, b.dataset.sec)));
    d.querySelector('.ws-detail-notes').addEventListener('input', e => {
      const v = e.target.value;
      S.tasks = S.tasks.map(x => x.id === t.id ? { ...x, notes: v } : x);
      Store.saveTasks(S.tasks);
      const cur = S.tasks.find(x => x.id === t.id);
      if (cur && cur.tdId && S.td.token) tdScheduleNotesUpdate(cur);
    });
  }

  function toggleDone(id) {
    const before = S.tasks.find(t => t.id === id);
    if (!before) return;
    const nextDone = !before.done;
    S.tasks = S.tasks.map(t => t.id === id ? { ...t, done: nextDone, section: nextDone ? 'Done' : 'To Do' } : t);
    Store.saveTasks(S.tasks); render();
    if (before.tdId && S.td.token) {
      (nextDone ? tdCloseTask(before.tdId) : tdReopenTask(before.tdId))
        .catch(e => { S.td.status = 'Sync error: ' + (e.message || e); S.td.statusKind = 'error'; render(); });
    }
  }
  function deleteTask(id) {
    const before = S.tasks.find(t => t.id === id);
    S.tasks = S.tasks.filter(t => t.id !== id);
    if (S.selId === id) S.selId = null;
    Store.saveTasks(S.tasks); render();
    if (before && before.tdId && S.td.token) {
      tdDeleteTask(before.tdId)
        .catch(e => { S.td.status = 'Delete sync error: ' + (e.message || e); S.td.statusKind = 'error'; render(); });
    }
  }
  function moveSection(id, sec) {
    const before = S.tasks.find(t => t.id === id);
    const wasDone = !!(before && before.done);
    S.tasks = S.tasks.map(t => t.id === id ? { ...t, section: sec, done: sec === 'Done' } : t);
    Store.saveTasks(S.tasks); render();
    if (before && before.tdId && S.td.token) tdMaybeMoveSection(before, sec, wasDone);
  }
  async function addTask() {
    const title = $('ws-add-title').value.trim();
    if (!title) return;
    let t = {
      id: Date.now(), title, project: S.project,
      priority: $('ws-add-priority').value, done: false,
      due: $('ws-add-due').value,
      labels: $('ws-add-labels').value.split(',').map(l=>l.trim()).filter(Boolean),
      section: 'To Do', notes: '',
    };
    // If the active project is synced, POST to Todoist first so we capture tdId.
    const proj = S.projects.find(p => p.id === S.project);
    if (proj && proj.tdId && S.td.token) {
      try {
        const created = await tdCreateTask(tdPayloadFromTask(t, proj.tdId));
        if (created && created.id) {
          t = { ...t, tdId: String(created.id), id: 'td_' + created.id };
        }
      } catch (e) {
        S.td.status = 'Add failed (saved local-only): ' + (e.message || e);
        S.td.statusKind = 'error';
      }
    }
    S.tasks.unshift(t);
    Store.saveTasks(S.tasks);
    $('ws-add-title').value = ''; $('ws-add-due').value = ''; $('ws-add-labels').value = '';
    setState({ addOpen:false });
  }

  // ─── ZOTERO ───
  const ZT_KEY = 'll_ws_zt';
  const ZT_CACHE_KEY = 'll_ws_zt_cache';

  function ztConfig() {
    try { return JSON.parse(localStorage.getItem(ZT_KEY)) || {}; } catch { return {}; }
  }
  function ztSaveConfig(c) { localStorage.setItem(ZT_KEY, JSON.stringify(c)); }

  // Format a Zotero item as an APA-ish citation string
  function ztCite(item) {
    const d = item.data;
    const authors = (d.creators||[]).filter(c=>c.creatorType==='author')
      .map(c => c.lastName ? `${c.lastName}, ${(c.firstName||'').slice(0,1)}.` : c.name||'')
      .join(', ');
    const year = (d.date||'').slice(0,4) || 'n.d.';
    const title = d.title || '';
    const journal = d.publicationTitle || d.bookTitle || d.publisher || '';
    const vol = d.volume ? `, ${d.volume}` : '';
    const pages = d.pages ? `, ${d.pages}` : '';
    return `${authors} (${year}). ${title}. ${journal}${vol}${pages}.`.replace(/\.{2,}/g,'.');
  }

  let ztState = { collections:[], items:[], filteredItems:[], collId:'all', search:'', loading:false };

  async function ztFetch(path, apiKey, useCache) {
    const cacheStore = JSON.parse(localStorage.getItem(ZT_CACHE_KEY)||'{}');
    if (useCache && cacheStore[path]) return cacheStore[path];
    const url = `https://api.zotero.org${path}`;
    const res = await fetch(url, { headers: { 'Zotero-API-Key': apiKey, 'Zotero-API-Version': '3' } });
    if (!res.ok) {
      // Translate the most common Zotero failures into actionable hints
      const hints = {
        401: 'API key missing or malformed. Re-paste it from zotero.org → Settings → Keys.',
        403: 'API key rejected (403). The key is likely revoked, lacks library read permission, or doesn\'t belong to the User ID you entered. Click Disconnect and create a fresh key with library access at zotero.org/settings/keys.',
        404: 'User ID not found. Double-check the numeric User ID shown at zotero.org/settings/keys.',
        429: 'Rate-limited by Zotero. Wait ~60s and try Sync again.',
      };
      const msg = hints[res.status] || `Zotero API ${res.status} ${res.statusText || ''}`.trim();
      throw new Error(msg);
    }
    const data = await res.json();
    cacheStore[path] = data;
    localStorage.setItem(ZT_CACHE_KEY, JSON.stringify(cacheStore));
    return data;
  }

  async function ztLoad(force) {
    const cfg = ztConfig();
    if (!cfg.apiKey || !cfg.userId) { renderZotero(); return; }
    ztState.loading = true;
    renderZotero();
    try {
      const [colls, items] = await Promise.all([
        ztFetch(`/users/${cfg.userId}/collections?limit=100`, cfg.apiKey, !force),
        ztFetch(`/users/${cfg.userId}/items?limit=100&itemType=-attachment&sort=dateModified&direction=desc`, cfg.apiKey, !force),
      ]);
      ztState.collections = colls;
      ztState.items = items;
      ztState.filteredItems = items;
    } catch(e) {
      ztState.error = e.message;
    }
    ztState.loading = false;
    renderZotero();
  }

  function ztFilterItems() {
    const q = ztState.search.toLowerCase();
    ztState.filteredItems = ztState.items.filter(it => {
      const title = (it.data.title||'').toLowerCase();
      const authors = (it.data.creators||[]).map(c=>(c.lastName||c.name||'')).join(' ').toLowerCase();
      const matchSearch = !q || title.includes(q) || authors.includes(q);
      const matchColl = ztState.collId === 'all' || (it.data.collections||[]).includes(ztState.collId);
      return matchSearch && matchColl;
    });
  }

  function renderZotero() {
    const el = $('ws-zotero-view');
    const cfg = ztConfig();
    if (!cfg.apiKey || !cfg.userId) {
      el.innerHTML = `<div class="ws-zt-setup">
        <div class="ws-zt-setup-card">
          <div class="ws-zt-setup-title">📚 Connect Zotero</div>
          <div class="ws-zt-setup-hint">Paste your Zotero User ID and a personal API key (<a href="https://www.zotero.org/settings/keys" target="_blank" style="color:var(--ws-primary)">zotero.org/settings/keys</a>). Credentials are stored only in your browser's localStorage.</div>
          <div class="ws-zt-field"><label>User ID</label><input id="zt-uid" type="text" placeholder="e.g. 1234567"></div>
          <div class="ws-zt-field"><label>API Key</label><input id="zt-key" type="password" placeholder="Your personal API key"></div>
          <button class="ws-zt-save-btn" id="zt-save">Save & Connect</button>
          <div class="ws-zt-msg" id="zt-msg"></div>
        </div>
      </div>`;
      $('zt-save').addEventListener('click', () => {
        const uid = $('zt-uid').value.trim();
        const key = $('zt-key').value.trim();
        if (!uid || !key) { $('zt-msg').textContent = 'Both fields are required.'; return; }
        ztSaveConfig({ userId: uid, apiKey: key });
        ztLoad(true);
      });
      return;
    }
    if (ztState.loading) {
      el.innerHTML = `<div class="ws-empty">Loading references…</div>`;
      return;
    }
    if (ztState.error) {
      el.innerHTML = `<div class="ws-empty">Error: ${esc(ztState.error)}<br><br><button class="ws-zt-setup-btn" id="zt-dis">Disconnect</button></div>`;
      el.querySelector('#zt-dis').addEventListener('click', () => { localStorage.removeItem(ZT_KEY); localStorage.removeItem(ZT_CACHE_KEY); ztState={collections:[],items:[],filteredItems:[],collId:'all',search:'',loading:false}; renderZotero(); });
      return;
    }
    ztFilterItems();
    const collsHTML = `<div class="ws-zt-coll${ztState.collId==='all'?' active':''}" data-coll="all">All References</div>`
      + ztState.collections.map(c=>`<div class="ws-zt-coll${ztState.collId===c.data.key?' active':''}" data-coll="${esc(c.data.key)}">${esc(c.data.name)}</div>`).join('');
    const itemsHTML = ztState.filteredItems.length
      ? ztState.filteredItems.map(it => {
          const d = it.data;
          const authors = (d.creators||[]).filter(c=>c.creatorType==='author')
            .map(c=>c.lastName||c.name||'').slice(0,3).join(', ');
          const year = (d.date||'').slice(0,4);
          return `<div class="ws-zt-item" data-key="${esc(it.data.key)}">
            <button class="ws-zt-copy" title="Copy citation" data-key="${esc(it.data.key)}">⧉</button>
            <div class="ws-zt-item-title">${esc(d.title||'(no title)')}</div>
            <div class="ws-zt-item-meta">
              <span class="ws-zt-item-type">${esc(d.itemType||'')}</span>
              ${authors ? `<span>${esc(authors)}${year?` · ${year}`:''}</span>` : ''}
              ${d.publicationTitle ? `<span>${esc(d.publicationTitle)}</span>` : ''}
            </div>
          </div>`;
        }).join('')
      : `<div class="ws-empty">No references found.</div>`;
    el.innerHTML = `
      <div class="ws-zt-toolbar">
        <input class="ws-zt-search" id="zt-search" placeholder="Search references…" value="${esc(ztState.search)}">
        <button class="ws-zt-reload-btn" id="zt-reload">↻ Sync</button>
        <button class="ws-zt-setup-btn" id="zt-settings">⚙ Settings</button>
      </div>
      <div class="ws-zt-body">
        <div class="ws-zt-collections" id="zt-colls">${collsHTML}</div>
        <div class="ws-zt-items">${itemsHTML}</div>
      </div>`;
    $('zt-search').addEventListener('input', e => { ztState.search = e.target.value; ztFilterItems(); renderZotero(); });
    $('zt-reload').addEventListener('click', () => ztLoad(true));
    $('zt-settings').addEventListener('click', () => { localStorage.removeItem(ZT_KEY); localStorage.removeItem(ZT_CACHE_KEY); ztState={collections:[],items:[],filteredItems:[],collId:'all',search:'',loading:false}; renderZotero(); });
    el.querySelectorAll('[data-coll]').forEach(c => c.addEventListener('click', () => { ztState.collId = c.dataset.coll; renderZotero(); }));
    el.querySelectorAll('.ws-zt-copy').forEach(btn => btn.addEventListener('click', e => {
      e.stopPropagation();
      const it = ztState.items.find(x => x.data.key === btn.dataset.key);
      if (it) { navigator.clipboard.writeText(ztCite(it)).then(() => { btn.textContent='✓'; setTimeout(()=>btn.textContent='⧉',1500); }); }
    }));
  }

  // ─── WEEK ───
  const WK_CACHE_KEY = 'll_ws_week';
  const WK_TOGGLE_KEY = 'll_ws_week_showCancelled';
  let wkState = { showCancelled: false, status: '', statusKind: '' };

  function wkStartOfWeek(d) {
    const x = new Date(d); x.setHours(0,0,0,0);
    const day = x.getDay();
    x.setDate(x.getDate() + (day === 0 ? -6 : 1 - day));
    return x;
  }
  function wkFmtDate(d) {
    return d.toLocaleDateString(undefined, { month:'long', day:'numeric', year:'numeric' });
  }
  function wkFmtTime(d) {
    return d.toLocaleTimeString(undefined, { hour:'numeric', minute:'2-digit' })
            .replace(':00','').toLowerCase().replace(' ','');
  }
  function wkParseRaw(raw) {
    let p = raw;
    if (raw && Array.isArray(raw.content)) {
      const text = raw.content.map(c => c.text || '').join('');
      try { p = JSON.parse(text); } catch { p = { items: [] }; }
    }
    if (typeof p === 'string') { try { p = JSON.parse(p); } catch { p = { items: [] }; } }
    return Array.isArray(p?.items) ? p.items : [];
  }
  function wkDedupe(events) {
    const seen = new Map();
    for (const e of events) {
      const k = (e.title||'').trim().toLowerCase() + '|' + e.startDate + '|' + (e.endDate||'');
      if (!seen.has(k)) seen.set(k, e);
    }
    return [...seen.values()];
  }
  function wkClassify(e) {
    const lower = (e.title||'').toLowerCase();
    const start = new Date(e.startDate);
    const end = new Date(e.endDate || e.startDate);
    const durMin = Math.max(0, Math.round((end - start) / 60000));
    return {
      cancelled: lower.startsWith('canceled:') || lower.startsWith('cancelled:'),
      focus: lower.includes('focus time'),
      lunch: lower.includes('lunch'),
      allDay: durMin >= 23*60 && start.getHours() === 0,
      durMin, start, end,
    };
  }
  function wkSummarize(dayEvents) {
    let meetings = 0, meetingMin = 0, earliest = null, latest = null, hasFocus = false;
    for (const { c } of dayEvents) {
      if (c.cancelled || c.allDay) continue;
      if (c.focus) { hasFocus = true; continue; }
      if (c.lunch) continue;
      meetings++; meetingMin += c.durMin;
      if (!earliest || c.start < earliest) earliest = c.start;
      if (!latest || c.end > latest) latest = c.end;
    }
    const stats = [];
    stats.push(`${meetings} meeting${meetings === 1 ? '' : 's'}`);
    if (meetingMin) stats.push(`${(meetingMin/60).toFixed(1)} hr`);
    if (earliest && latest) stats.push(`${wkFmtTime(earliest)}–${wkFmtTime(latest)}`);
    if (hasFocus) stats.push('focus held');
    let vibe;
    if (meetings === 0)       vibe = 'Wide open. Defend it.';
    else if (meetings <= 2)   vibe = 'Light load — deep work day.';
    else if (meetings <= 4)   vibe = 'Manageable. Buffer between back-to-backs.';
    else if (meetings <= 6)   vibe = 'Heavy meeting day. Lunch is non-negotiable.';
    else                       vibe = 'Calendar bloodbath. Decline what you can.';
    return { vibe, stats: stats.join(' · ') };
  }

  function wkLoadCache() {
    try {
      const raw = localStorage.getItem(WK_CACHE_KEY);
      return raw ? JSON.parse(raw) : null;
    } catch { return null; }
  }
  function wkSaveCache(items, weekStart) {
    try {
      localStorage.setItem(WK_CACHE_KEY, JSON.stringify({
        items, weekStart: weekStart.toISOString(), fetchedAt: new Date().toISOString(),
      }));
    } catch {}
  }

  function wkRender(items, weekStart) {
    const el = $('ws-week-view');
    const dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
    const buckets = new Map();
    for (let i = 0; i < 7; i++) {
      const d = new Date(weekStart); d.setDate(d.getDate() + i);
      buckets.set(d.toDateString(), []);
    }
    for (const e of items) {
      const c = wkClassify(e);
      const k = c.start.toDateString();
      if (buckets.has(k)) buckets.get(k).push({ e, c });
    }
    const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 6);
    const todayStr = new Date().toDateString();

    let statusHTML = '';
    if (wkState.status) {
      statusHTML = `<div class="ws-wk-status ${esc(wkState.statusKind)}">${esc(wkState.status)}</div>`;
    }

    let cards = '';
    for (const [key, list] of buckets) {
      const d = new Date(key);
      list.sort((a,b) => a.c.allDay !== b.c.allDay
        ? (a.c.allDay ? -1 : 1)
        : a.c.start - b.c.start);
      const visible = wkState.showCancelled ? list : list.filter(x => !x.c.cancelled);
      const sum = wkSummarize(visible);
      const isWeekend = d.getDay() === 0 || d.getDay() === 6;
      const isToday = key === todayStr;
      const evHTML = visible.length === 0
        ? `<div class="ws-wk-empty">Nothing scheduled.</div>`
        : visible.map(({ e, c }) => {
            const time = c.allDay ? 'all day' : wkFmtTime(c.start);
            const cls = ['ws-wk-ev',
              c.cancelled ? 'cancelled' : '',
              c.allDay ? 'allday' : '',
              c.focus ? 'focus' : '',
              c.lunch ? 'lunch' : ''].filter(Boolean).join(' ');
            const title = (e.title || '(untitled)').replace(/^canceled:\s*/i, '').trim();
            return `<div class="${cls}"><span class="t">${esc(time)}</span><span class="n">${esc(title)}</span></div>`;
          }).join('');
      cards += `<div class="ws-wk-day${isToday?' today':''}${isWeekend?' weekend':''}">
        <div class="ws-wk-dhead">
          <span class="ws-wk-dname">${dayNames[d.getDay()]}</span>
          <span class="ws-wk-ddate">${esc(d.toLocaleDateString(undefined,{month:'short',day:'numeric'}))}</span>
        </div>
        <div class="ws-wk-sum">
          <div>${esc(sum.vibe)}</div>
          <div class="ws-wk-sum-stats">${esc(sum.stats)}</div>
        </div>
        <div class="ws-wk-evs">${evHTML}</div>
      </div>`;
    }

    el.innerHTML = `
      <div class="ws-wk-head">
        <div>
          <div class="ws-wk-title">Week at a Glance</div>
          <div class="ws-wk-range">${esc(wkFmtDate(weekStart))} → ${esc(wkFmtDate(weekEnd))}</div>
        </div>
        <div class="ws-wk-actions">
          <label class="ws-wk-toggle">
            <input type="checkbox" id="ws-wk-showcancelled" ${wkState.showCancelled?'checked':''}>
            Show cancelled
          </label>
          <button class="ws-zt-reload-btn" id="ws-wk-reload">↻ Refresh</button>
        </div>
      </div>
      ${statusHTML}
      <div class="ws-wk-grid">${cards}</div>`;

    $('ws-wk-showcancelled').addEventListener('change', e => {
      wkState.showCancelled = e.target.checked;
      try { localStorage.setItem(WK_TOGGLE_KEY, e.target.checked ? '1' : '0'); } catch {}
      weekLoad(false);
    });
    $('ws-wk-reload').addEventListener('click', () => weekLoad(true));
  }

  function wkRenderBlank() {
    $('ws-week-view').innerHTML = `
      <div class="ws-wk-head">
        <div>
          <div class="ws-wk-title">Week at a Glance</div>
          <div class="ws-wk-range">No data yet</div>
        </div>
      </div>
      <div class="ws-wk-blank">
        This view fetches your calendar via Fantastical and only loads<br>
        inside the Cowork desktop app on your Mac.<br><br>
        Open this workspace in Cowork to populate the cache.
      </div>`;
  }

  async function weekLoad(force) {
    try { wkState.showCancelled = localStorage.getItem(WK_TOGGLE_KEY) === '1'; } catch {}
    const weekStart = wkStartOfWeek(new Date());
    const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 6);
    const bridge = window.cowork && typeof window.cowork.callMcpTool === 'function';

    if (!bridge) {
      const snap = wkLoadCache();
      if (!snap) { wkRenderBlank(); return; }
      const fetched = new Date(snap.fetchedAt);
      wkState.status = `Offline view — cached ${fetched.toLocaleString()}. Open in Cowork to refresh.`;
      wkState.statusKind = 'warn';
      wkRender(snap.items, new Date(snap.weekStart));
      return;
    }

    if (!force) {
      const snap = wkLoadCache();
      if (snap) {
        // Show cache immediately; we'll overwrite when the fetch resolves
        wkState.status = '';
        wkState.statusKind = '';
        wkRender(snap.items, new Date(snap.weekStart));
      }
    }

    try {
      const fmt = d => d.toLocaleDateString(undefined, { weekday:'long', month:'long', day:'numeric', year:'numeric' });
      const when = `${fmt(weekStart)} to ${fmt(weekEnd)}`;
      const raw = await window.cowork.callMcpTool('mcp__Fantastical__queryCalendarItems', { when });
      const items = wkDedupe(wkParseRaw(raw));
      wkSaveCache(items, weekStart);
      wkState.status = ''; wkState.statusKind = '';
      wkRender(items, weekStart);
    } catch (err) {
      const snap = wkLoadCache();
      wkState.status = 'Live fetch failed: ' + ((err && err.message) || err) + '.';
      wkState.statusKind = 'error';
      if (snap) wkRender(snap.items, new Date(snap.weekStart));
      else wkRenderBlank();
    }
  }

  // ─── TODOIST ───
  // Hybrid backend: REST API (default) + optional Cowork MCP bridge when available.
  // Tasks/projects coexist as "local-only" (no tdId) or "synced" (tdId present).

  const TD_API = 'https://api.todoist.com/rest/v2';
  const TD_PRIO     = { high: 4, medium: 3, low: 1 };          // our → Todoist (4=urgent, 1=default)
  const TD_PRIO_REV = { 4: 'high', 3: 'medium', 2: 'low', 1: 'low' };

  function tdHasBridge() {
    return !!(window.cowork && typeof window.cowork.callMcpTool === 'function');
  }

  async function tdFetch(path, opts = {}) {
    if (!S.td.token) throw new Error('Not connected to Todoist.');
    const res = await fetch(`${TD_API}${path}`, {
      method: opts.method || 'GET',
      headers: {
        'Authorization': `Bearer ${S.td.token}`,
        'Content-Type':  'application/json',
      },
      body: opts.body ? JSON.stringify(opts.body) : undefined,
    });
    if (!res.ok) {
      const txt = await res.text().catch(() => '');
      throw new Error(`Todoist ${res.status} ${res.statusText}${txt ? ': '+txt.slice(0,200) : ''}`);
    }
    if (res.status === 204) return null;
    const text = await res.text();
    if (!text) return null;
    try { return JSON.parse(text); } catch { return text; }
  }

  // === Read ops ===
  const tdLoadProjects     = () => tdFetch('/projects');
  const tdLoadSections     = projectId => tdFetch(`/sections?project_id=${encodeURIComponent(projectId)}`);
  const tdLoadActiveTasks  = () => tdFetch('/tasks');

  // === Write ops ===
  const tdCreateTask   = payload => tdFetch('/tasks', { method:'POST', body: payload });
  const tdUpdateTask   = (id, patch) => tdFetch(`/tasks/${encodeURIComponent(id)}`, { method:'POST', body: patch });
  const tdCloseTask    = id => tdFetch(`/tasks/${encodeURIComponent(id)}/close`,  { method:'POST' });
  const tdReopenTask   = id => tdFetch(`/tasks/${encodeURIComponent(id)}/reopen`, { method:'POST' });
  const tdDeleteTask   = id => tdFetch(`/tasks/${encodeURIComponent(id)}`,        { method:'DELETE' });
  const tdMoveTask     = (id, target) => tdFetch(`/tasks/${encodeURIComponent(id)}/move`, { method:'POST', body: target });
  const tdCreateSection = (name, projectId) => tdFetch('/sections', { method:'POST', body: { name, project_id: String(projectId) } });

  // Build a Todoist task payload from our task object
  function tdPayloadFromTask(t, projectTdId) {
    const p = {
      content:    t.title,
      project_id: String(projectTdId),
      priority:   TD_PRIO[t.priority] || 1,
    };
    if (t.due)                                 p.due_date    = t.due;
    if (t.labels && t.labels.length)           p.labels      = t.labels;
    if (t.notes)                               p.description = t.notes;
    return p;
  }

  // Map a Todoist API task → our task object. sectionMap: { tdSectionId: sectionName }
  function tdMapTask(tt, projectLocalId, sectionMap) {
    let section = 'To Do';
    if (tt.is_completed || tt.completed) {
      section = 'Done';
    } else if (tt.section_id && sectionMap[tt.section_id]) {
      if (/in[\s-]*progress/i.test(sectionMap[tt.section_id])) section = 'In Progress';
    }
    const due = tt.due
      ? (tt.due.date || (tt.due.datetime ? String(tt.due.datetime).slice(0,10) : ''))
      : '';
    return {
      id:           'td_' + tt.id,
      tdId:         String(tt.id),
      tdSectionId:  tt.section_id ? String(tt.section_id) : null,
      title:        tt.content || '',
      project:      projectLocalId,
      priority:     TD_PRIO_REV[tt.priority] || 'low',
      done:         section === 'Done',
      due:          due || '',
      labels:       Array.isArray(tt.labels) ? tt.labels.slice() : [],
      section,
      notes:        tt.description || '',
    };
  }

  // Ensure an "In Progress" section exists in the given Todoist project; return its id.
  async function tdEnsureInProgressSection(projectTdId) {
    const cache = Store.getTdCache() || {};
    const list  = (cache.sectionsByProject && cache.sectionsByProject[projectTdId]) || [];
    const found = list.find(s => /in[\s-]*progress/i.test(s.name));
    if (found) return String(found.id);
    const created = await tdCreateSection('In Progress', projectTdId);
    try {
      const fresh = await tdLoadSections(projectTdId);
      cache.sectionsByProject = cache.sectionsByProject || {};
      cache.sectionsByProject[projectTdId] = fresh;
      Store.saveTdCache(cache);
    } catch (_) {}
    return created ? String(created.id) : null;
  }

  async function tdSync(opts = {}) {
    if (!S.td.token) return;
    if (S.td.syncing && !opts.force) return;
    S.td.syncing = true;
    S.td.status = 'Syncing…';
    S.td.statusKind = '';
    render();
    try {
      const [projects, tasks] = await Promise.all([ tdLoadProjects(), tdLoadActiveTasks() ]);

      // Pull sections per project in parallel
      const sectionsByProject = {};
      await Promise.all(projects.map(async pr => {
        try { sectionsByProject[pr.id] = await tdLoadSections(pr.id); }
        catch { sectionsByProject[pr.id] = []; }
      }));

      // Section lookup for task mapping
      const sectionMap = {};
      Object.values(sectionsByProject).forEach(arr => {
        (arr || []).forEach(sec => { sectionMap[sec.id] = sec.name; });
      });

      // Merge local-only projects with Todoist projects
      const localOnly = (S.projects || []).filter(p => !p.tdId);
      const tdProjects = projects.map(pr => ({
        id:    'td_' + pr.id,
        tdId:  String(pr.id),
        name:  pr.name,
        icon:  '✓',
      }));
      const merged = [...localOnly, ...tdProjects];

      // Map tasks; keep local-only tasks (those without tdId) tied to local-only projects.
      // On first sync, untouched demo seed tasks are wiped (user pref: Todoist is source of truth).
      const mapped = tasks.map(tt => tdMapTask(tt, 'td_' + tt.project_id, sectionMap));
      const localProjectIds = new Set(localOnly.map(p => p.id));
      const isFirstSync = !Store.getTdCache();
      const wipeSeed = isFirstSync && tasksMatchSeed(S.tasks);
      const keptLocal = wipeSeed
        ? []
        : (S.tasks || []).filter(t => !t.tdId && localProjectIds.has(t.project));

      const fetchedAt = new Date().toISOString();
      Store.saveTdCache({ projects, sectionsByProject, fetchedAt });
      Store.saveProjects(merged);
      const newTasks = [...keptLocal, ...mapped];
      Store.saveTasks(newTasks);

      const stillExists = merged.find(p => p.id === S.project);
      S.projects   = merged;
      S.tasks      = newTasks;
      S.project    = stillExists ? S.project : (merged[0]?.id || S.project);
      S.td.connected  = true;
      S.td.lastSync   = fetchedAt;
      S.td.syncing    = false;
      S.td.status     = `Synced ${new Date(fetchedAt).toLocaleString()} — ${projects.length} project${projects.length===1?'':'s'}, ${mapped.length} task${mapped.length===1?'':'s'}.`;
      S.td.statusKind = 'ok';
      render();
    } catch (err) {
      S.td.syncing    = false;
      S.td.status     = 'Sync failed: ' + ((err && err.message) || err);
      S.td.statusKind = 'error';
      render();
    }
  }

  async function tdConnect(token, opts = {}) {
    const cfg = { token: token.trim(), useMcp: !!opts.useMcp, mcpPrefix: (opts.mcpPrefix || '').trim() };
    Store.saveTdCfg(cfg);
    S.td.token     = cfg.token;
    S.td.useMcp    = cfg.useMcp;
    S.td.mcpPrefix = cfg.mcpPrefix;
    await tdSync({ force: true });
    renderTodoist(); // refresh the panel after connect
  }

  function tdDisconnect() {
    Store.clearTdCfg();
    const localOnly  = (S.projects || []).filter(p => !p.tdId);
    const localTasks = (S.tasks    || []).filter(t => !t.tdId);
    Store.saveProjects(localOnly);
    Store.saveTasks(localTasks);
    S.projects = localOnly;
    S.tasks    = localTasks;
    S.td = { connected:false, token:'', useMcp:false, mcpPrefix:'', lastSync:null, status:'Disconnected.', statusKind:'', syncing:false };
    if (!S.projects.find(p => p.id === S.project)) S.project = S.projects[0]?.id || 'work';
    render();
    renderTodoist();
  }

  // Async writers for hooks below
  async function tdMaybeMoveSection(t, sec, wasDone) {
    if (!t.tdId) return;
    const proj = S.projects.find(p => p.id === t.project);
    if (!proj || !proj.tdId) return;
    try {
      if (sec === 'Done') { await tdCloseTask(t.tdId); return; }
      if (wasDone)        { await tdReopenTask(t.tdId); }
      if (sec === 'In Progress') {
        const secId = await tdEnsureInProgressSection(proj.tdId);
        if (secId) await tdMoveTask(t.tdId, { section_id: String(secId) });
      } else if (sec === 'To Do') {
        // Move to project root, clearing any section
        await tdMoveTask(t.tdId, { project_id: String(proj.tdId) });
      }
    } catch (e) {
      S.td.status = 'Section sync error: ' + (e.message || e);
      S.td.statusKind = 'error';
      render();
    }
  }

  // Debounced description sync per task
  const tdNotesTimers = {};
  function tdScheduleNotesUpdate(t) {
    if (!t.tdId || !S.td.token) return;
    clearTimeout(tdNotesTimers[t.tdId]);
    tdNotesTimers[t.tdId] = setTimeout(async () => {
      try { await tdUpdateTask(t.tdId, { description: t.notes || '' }); }
      catch (e) {
        S.td.status = 'Notes save failed: ' + (e.message || e);
        S.td.statusKind = 'error';
        render();
      }
    }, 800);
  }

  function renderTodoist() {
    const el = $('ws-todoist-view');
    if (!el) return;
    const cfg = Store.getTdCfg();
    const bridge = tdHasBridge();
    const connected = !!cfg.token;

    if (!connected) {
      el.innerHTML = `
        <div class="ws-td-panel-head">
          <span class="ws-td-panel-title">✓ Todoist</span>
          <span class="ws-td-panel-hint">connect once; credentials stay in your browser</span>
        </div>
        <div class="ws-zt-setup-card" style="max-width:520px">
          <div class="ws-zt-setup-title">Connect Todoist</div>
          <div class="ws-zt-setup-hint">
            Paste a personal API token from
            <a href="https://todoist.com/app/settings/integrations/developer" target="_blank" rel="noopener" style="color:var(--ws-primary)">todoist.com → Settings → Integrations → Developer</a>.
            Stored only in this browser's localStorage. You can disconnect any time.
          </div>
          <div class="ws-zt-field">
            <label>API token</label>
            <input id="td-token" type="password" placeholder="Personal Todoist API token" autocomplete="off">
          </div>
          ${bridge ? `
          <div class="ws-td-bridge-row">
            <input type="checkbox" id="td-usemcp">
            <label for="td-usemcp">Also configure the Cowork MCP bridge (optional)</label>
          </div>
          <div class="ws-zt-field" id="td-mcp-row" style="display:none;margin-top:6px">
            <label>MCP tool prefix</label>
            <input id="td-mcp-prefix" type="text" placeholder="mcp__&lt;todoist-server-uuid&gt;">
          </div>` : ''}
          <button class="ws-zt-save-btn" id="td-connect">Connect & Sync</button>
          <div class="ws-zt-msg" id="td-msg"></div>
        </div>`;
      const tokInput = $('td-token');
      tokInput && tokInput.focus();
      $('td-connect').addEventListener('click', async () => {
        const tk = ($('td-token').value || '').trim();
        if (!tk) { $('td-msg').textContent = 'API token is required.'; return; }
        const useMcp = bridge && $('td-usemcp') && $('td-usemcp').checked;
        const mcpPrefix = useMcp ? (($('td-mcp-prefix') && $('td-mcp-prefix').value) || '') : '';
        $('td-msg').textContent = 'Connecting…';
        try { await tdConnect(tk, { useMcp, mcpPrefix }); }
        catch (e) { $('td-msg').textContent = 'Connection failed: ' + (e.message || e); }
      });
      const mcpToggle = $('td-usemcp');
      if (mcpToggle) {
        mcpToggle.addEventListener('change', e => {
          const row = $('td-mcp-row');
          if (row) row.style.display = e.target.checked ? '' : 'none';
        });
      }
      return;
    }

    // Connected
    const cache        = Store.getTdCache();
    const projectCount = (cache && cache.projects) ? cache.projects.length : 0;
    const syncedCount  = S.tasks.filter(t => t.tdId).length;
    const localCount   = S.tasks.filter(t => !t.tdId).length;
    const projList = (cache && cache.projects && cache.projects.length)
      ? cache.projects.map(p => `<li>${esc(p.name)}</li>`).join('')
      : '<li><em style="color:var(--ws-text-3)">(none yet — click Sync now)</em></li>';

    const statusCls  = S.td.statusKind ? ' ' + S.td.statusKind : '';
    const statusText = S.td.status || (S.td.lastSync
      ? `Last synced ${new Date(S.td.lastSync).toLocaleString()}.`
      : 'Connected.');

    el.innerHTML = `
      <div class="ws-td-panel-head">
        <span class="ws-td-panel-title">✓ Todoist</span>
        <span class="ws-td-panel-hint">${projectCount} project${projectCount===1?'':'s'} · ${syncedCount} synced · ${localCount} local-only</span>
      </div>
      <div class="ws-td-status${statusCls}">${esc(statusText)}</div>
      <div class="ws-td-actions">
        <button class="ws-zt-reload-btn" id="td-sync">${S.td.syncing ? 'Syncing…' : '↻ Sync now'}</button>
        <button class="ws-zt-setup-btn" id="td-edit">⚙ Edit connection</button>
        <button class="ws-td-danger" id="td-disconnect">Disconnect</button>
      </div>
      <div class="ws-td-projects">
        <div style="color:var(--ws-text-3);font-size:9px;letter-spacing:0.1em;text-transform:uppercase">Projects pulled from Todoist</div>
        <ul>${projList}</ul>
      </div>
      <div style="margin-top:14px;font-size:11px;color:var(--ws-text-3);line-height:1.7;font-family:var(--ws-mono)">
        Writes go to Todoist when the active project shows a ✓ icon.<br>
        Sections — "To Do" = project root, "In Progress" = section created on first use, "Done" = closed task.<br>
        Priorities — high → P1, medium → P2, low → P4.
      </div>`;

    $('td-sync').addEventListener('click', () => tdSync({ force:true }));
    $('td-disconnect').addEventListener('click', () => {
      if (confirm('Disconnect Todoist?\n\nLocal-only tasks are kept on this device.\nSynced tasks are removed locally — they remain in Todoist.')) tdDisconnect();
    });
    $('td-edit').addEventListener('click', () => {
      Store.saveTdCfg({});
      S.td.connected = false;
      S.td.token = '';
      renderTodoist();
    });
  }

  // ─── AUTH ───
  let firstTime = false;

  function updateLockUI() {
    firstTime = !Store.getHash();
    $('ws-lock-title').textContent = firstTime ? 'Create Password' : 'Workspace';
    $('ws-lock-sub').textContent   = firstTime ? 'Set a password to protect this page.' : 'Personal productivity · private area';
    $('ws-lock-submit').textContent = firstTime ? 'Set Password →' : 'Unlock →';
    $('ws-lock-input').placeholder  = firstTime ? 'Choose a password…' : 'Enter password';
  }

  async function handleUnlock() {
    const v = $('ws-lock-input').value;
    if (!v) return;
    const h = await sha256(v);
    if (firstTime) {
      Store.setHash(h);
      sessionStorage.setItem('ll_ws_s', h.slice(0,16));
      unlock();
    } else if (h === Store.getHash()) {
      sessionStorage.setItem('ll_ws_s', h.slice(0,16));
      unlock();
    } else {
      const inp = $('ws-lock-input');
      $('ws-lock-err').textContent = 'Incorrect password.';
      inp.classList.add('err'); inp.value = '';
      setTimeout(() => inp.classList.remove('err'), 500);
    }
  }

  function unlock() {
    $('ws-lock-err').textContent = '';
    $('ws-lock').classList.add('dismissed');
    setTimeout(() => {
      $('ws-lock').style.display = 'none';
      $('ws-app').classList.add('visible');
      render();
    }, 350);
  }

  function relock() {
    sessionStorage.removeItem('ll_ws_s');
    $('ws-lock-input').value = '';
    updateLockUI();
    $('ws-lock').style.display = '';
    $('ws-lock').classList.remove('dismissed');
    $('ws-app').classList.remove('visible');
  }

  function handleReset() {
    if (!confirm('Clear password and all stored tasks & notes? This is permanent.')) return;
    Store.clearAll();
    sessionStorage.removeItem('ll_ws_s');
    location.reload();
  }

  async function checkSession() {
    const s = sessionStorage.getItem('ll_ws_s');
    const h = Store.getHash();
    if (s && h && h.startsWith(s)) { unlock(); return true; }
    return false;
  }

  function bind() {
    $('ws-lock-submit').addEventListener('click', handleUnlock);
    $('ws-lock-input').addEventListener('keydown', e => { if (e.key === 'Enter') handleUnlock(); });
    $('ws-reset').addEventListener('click', handleReset);

    // Tools nav (Zotero, Week, Todoist) — these hide the task views.
    const TOOL_NAVS = new Set(['zotero', 'week', 'todoist']);
    document.addEventListener('click', e => {
      const n = e.target.closest('[data-nav]');
      if (!n) return;
      const which = n.dataset.nav;
      if (TOOL_NAVS.has(which)) {
        $$('[data-nav]').forEach(el => el.classList.remove('active'));
        n.classList.add('active');
        // hide normal task views
        $('ws-list-view').classList.add('ws-view-hidden');
        $('ws-board-view').classList.add('ws-view-hidden');
        $('ws-notes-view').classList.add('ws-view-hidden');
        $('ws-detail').classList.add('ws-view-hidden');
        $('ws-filter-bar').classList.add('ws-view-hidden');
        $('ws-add-form').classList.remove('open');
        // toggle tool views
        $('ws-zotero-view').classList.toggle('ws-view-hidden',   which !== 'zotero');
        $('ws-week-view').classList.toggle('ws-view-hidden',     which !== 'week');
        $('ws-todoist-view').classList.toggle('ws-view-hidden',  which !== 'todoist');
        if (which === 'zotero')  ztLoad(false);
        if (which === 'week')    weekLoad(false);
        if (which === 'todoist') renderTodoist();
      } else {
        // Returning to a task view from a tool view
        $('ws-zotero-view').classList.add('ws-view-hidden');
        $('ws-week-view').classList.add('ws-view-hidden');
        $('ws-todoist-view').classList.add('ws-view-hidden');
        $('ws-filter-bar').classList.remove('ws-view-hidden');
      }
    });

    $$('.ws-vbtn').forEach(b => b.addEventListener('click', () => setState({ view: b.dataset.view, addOpen:false })));
    $$('.ws-chip').forEach(b => b.addEventListener('click', () => setState({ filter: b.dataset.filter })));
    $('ws-toggle-add').addEventListener('click', () => setState({ addOpen: !S.addOpen, view: S.view === 'notes' ? 'list' : S.view }));
    $('ws-submit-add').addEventListener('click', addTask);
    $('ws-cancel-add').addEventListener('click', () => setState({ addOpen:false }));
    $('ws-add-title').addEventListener('keydown', e => { if (e.key === 'Enter') addTask(); });
    $('ws-notes-area').addEventListener('input', e => {
      S.notes[S.project] = e.target.value;
      Store.saveNotes(S.notes);
    });
    $('ws-relock').addEventListener('click', relock);
  }

  async function init() {
    S.tasks    = Store.getTasks();
    S.notes    = Store.getNotes();
    S.projects = Store.getProjects();
    // Todoist config
    const cfg = Store.getTdCfg();
    if (cfg && cfg.token) {
      S.td.token     = cfg.token;
      S.td.useMcp    = !!cfg.useMcp;
      S.td.mcpPrefix = cfg.mcpPrefix || '';
      S.td.connected = true;
      const cache = Store.getTdCache();
      if (cache && cache.fetchedAt) S.td.lastSync = cache.fetchedAt;
    }
    // Make sure current project still exists
    if (!S.projects.find(p => p.id === S.project)) {
      S.project = (S.projects[0] && S.projects[0].id) || 'work';
    }
    bind();
    updateLockUI();
    await checkSession();
    // Background Todoist refresh once unlocked (non-blocking)
    if (S.td.connected) setTimeout(() => { tdSync().catch(()=>{}); }, 300);
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
  else init();
})();
</script>
```
