Productivity Toolkit

Local-first browser productivity suite with notes, snippets, focus blocking, timer, reports, highlights, shortcuts, site rules, and backup/restore.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Productivity Toolkit
// @namespace    local.productivity-toolkit
// @version      0.2.8
// @description  Local-first browser productivity suite with notes, snippets, focus blocking, timer, reports, highlights, shortcuts, site rules, and backup/restore.
// @license GNU GPLv3
// @author       Thomas
// @author       GitHub: https://github.com/Thomas2074
// @author       LinkedIn: https://www.linkedin.com/in/thomas-crutchfield-780957192/
// @match        http://*/*
// @match        https://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  if (window.__PRODUCTIVITY_TOOLKIT_BOOTED__) {
    return;
  }
  window.__PRODUCTIVITY_TOOLKIT_BOOTED__ = true;

  try {
    if (window.top !== window.self) {
      return;
    }
  } catch (error) {
    return;
  }

  const APP = {
    version: "0.2.8",
    storageKey: "productivity_toolkit_state_v1",
    styleId: "ptk-style",
    rootId: "ptk-root",
    disabledId: "ptk-disabled-launcher",
    focusOverlayId: "ptk-focus-overlay",
    highlightClass: "ptk-highlight",
    maxHighlights: 500,
    maxHighlightTextNodes: 12000
  };

  const FEATURE_LABELS = {
    textExpander: "Text Expander",
    quickNotes: "Quick Notes",
    focusLock: "FocusLock",
    pomodoro: "Pomodoro",
    timeTracker: "Time Tracker",
    highlights: "Highlights",
    shortcuts: "Keyboard Shortcuts"
  };

  const DEFAULT_SHORTCUTS = {
    togglePanel: "Alt+Shift+P",
    openNotes: "Alt+Shift+N",
    highlightSelection: "Alt+Shift+H",
    clearHighlights: "Alt+Shift+C",
    toggleFocus: "Alt+Shift+F"
  };

  const SHORTCUT_LABELS = {
    togglePanel: "Open or close panel",
    openNotes: "Open notes",
    highlightSelection: "Highlight selected text",
    clearHighlights: "Clear highlights",
    toggleFocus: "Start or end focus"
  };

  const DEFAULT_BLOCKLIST = [
    "youtube.com",
    "tiktok.com",
    "instagram.com",
    "facebook.com",
    "x.com",
    "twitter.com",
    "reddit.com"
  ];

  const DAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
  const TEXT_EXPANDER_KEYS = new Set([" ", "Enter", "Tab", ".", ",", "!", "?", ";", ":", ")", "]", "}"]);
  const SITE_RULE_FEATURES = Object.keys(FEATURE_LABELS);

  let state = normalizeState(readStoredState());
  let root = null;
  let panel = null;
  let edgeTab = null;
  let toolkitStarted = false;
  let listenersBound = false;
  let pomodoroInterval = null;
  let timeTrackerInterval = null;
  let focusInterval = null;
  let scheduleInterval = null;
  let shortcutCaptureAction = null;
  let currentDomain = getCurrentDomain();
  let lastTrackedAt = Date.now();
  let lastTimeSaveAt = Date.now();
  let dragState = null;
  let pendingSaveTimer = null;

  function createDefaultState() {
    return {
      appVersion: APP.version,
      ui: {
        open: false,
        activeTab: "notes",
        position: null,
        theme: "light"
      },
      snippets: [
        {
          id: "default-brb",
          shortcut: "brb",
          expansion: "Be right back.",
          category: "General",
          enabled: true
        },
        {
          id: "default-ty",
          shortcut: "ty",
          expansion: "Thank you, I appreciate it.",
          category: "General",
          enabled: true
        },
        {
          id: "default-sig",
          shortcut: "sig",
          expansion: "Best regards,\nYour Name",
          category: "General",
          enabled: true
        }
      ],
      snippetCategories: ["General"],
      snippetFilter: "all",
      notes: "",
      focus: {
        activeUntil: 0,
        activeSource: "manual",
        activeScheduleEnd: 0,
        schedulePausedUntil: 0,
        blocklist: DEFAULT_BLOCKLIST.slice(),
        schedules: []
      },
      pomodoro: {
        running: false,
        mode: "work",
        endsAt: 0,
        remainingMs: 25 * 60 * 1000,
        workMinutes: 25,
        breakMinutes: 5,
        sessionsCompleted: 0
      },
      timeLogs: {},
      shortcuts: Object.assign({}, DEFAULT_SHORTCUTS),
      siteRules: {}
    };
  }

  function normalizeState(raw) {
    const defaults = createDefaultState();
    if (!isPlainObject(raw)) {
      return defaults;
    }

    const next = defaults;

    if (isPlainObject(raw.ui)) {
      next.ui.open = Boolean(raw.ui.open);
      next.ui.activeTab = typeof raw.ui.activeTab === "string" ? raw.ui.activeTab : defaults.ui.activeTab;
      next.ui.theme = raw.ui.theme === "dark" ? "dark" : "light";
      if (isPlainObject(raw.ui.position) && Number.isFinite(raw.ui.position.left) && Number.isFinite(raw.ui.position.top)) {
        next.ui.position = {
          left: clamp(raw.ui.position.left, 0, Math.max(0, window.innerWidth - 80)),
          top: clamp(raw.ui.position.top, 0, Math.max(0, window.innerHeight - 40))
        };
      }
    }

    if (Array.isArray(raw.snippets)) {
      const snippets = raw.snippets
        .map((snippet) => ({
          id: stringOr(snippet && snippet.id, createId("snippet")),
          shortcut: stringOr(snippet && snippet.shortcut, "").trim(),
          expansion: stringOr(snippet && snippet.expansion, ""),
          category: stringOr(snippet && snippet.category, "General").trim() || "General",
          enabled: snippet && snippet.enabled !== false
        }))
        .filter((snippet) => snippet.shortcut && snippet.expansion);
      if (snippets.length) {
        next.snippets = snippets;
      }
    }

    if (Array.isArray(raw.snippetCategories)) {
      next.snippetCategories = uniqueStrings(raw.snippetCategories.concat(next.snippets.map((snippet) => snippet.category)));
    } else {
      next.snippetCategories = uniqueStrings(next.snippets.map((snippet) => snippet.category).concat(defaults.snippetCategories));
    }
    next.snippetFilter = typeof raw.snippetFilter === "string" ? raw.snippetFilter : defaults.snippetFilter;
    if (next.snippetFilter !== "all" && !next.snippetCategories.includes(next.snippetFilter)) {
      next.snippetFilter = "all";
    }

    next.notes = typeof raw.notes === "string" ? raw.notes : defaults.notes;

    if (isPlainObject(raw.focus)) {
      next.focus.activeUntil = positiveNumber(raw.focus.activeUntil, 0);
      next.focus.activeSource = raw.focus.activeSource === "schedule" ? "schedule" : "manual";
      next.focus.activeScheduleEnd = positiveNumber(raw.focus.activeScheduleEnd, 0);
      next.focus.schedulePausedUntil = positiveNumber(raw.focus.schedulePausedUntil, 0);
      if (Array.isArray(raw.focus.blocklist)) {
        const blocklist = uniqueStrings(raw.focus.blocklist.map(normalizeDomain).filter(Boolean));
        next.focus.blocklist = blocklist.length ? blocklist : DEFAULT_BLOCKLIST.slice();
      }
      if (Array.isArray(raw.focus.schedules)) {
        next.focus.schedules = raw.focus.schedules.map(normalizeSchedule).filter(Boolean);
      }
    }

    if (isPlainObject(raw.pomodoro)) {
      next.pomodoro.running = Boolean(raw.pomodoro.running && raw.pomodoro.endsAt > Date.now());
      next.pomodoro.mode = raw.pomodoro.mode === "break" ? "break" : "work";
      next.pomodoro.endsAt = positiveNumber(raw.pomodoro.endsAt, 0);
      next.pomodoro.workMinutes = clamp(positiveNumber(raw.pomodoro.workMinutes, defaults.pomodoro.workMinutes), 1, 180);
      next.pomodoro.breakMinutes = clamp(positiveNumber(raw.pomodoro.breakMinutes, defaults.pomodoro.breakMinutes), 1, 120);
      next.pomodoro.remainingMs = clamp(positiveNumber(raw.pomodoro.remainingMs, defaults.pomodoro.remainingMs), 1000, 12 * 60 * 60 * 1000);
      next.pomodoro.sessionsCompleted = Math.floor(positiveNumber(raw.pomodoro.sessionsCompleted, 0));
    }

    if (isPlainObject(raw.timeLogs)) {
      next.timeLogs = normalizeTimeLogs(raw.timeLogs);
    }

    if (isPlainObject(raw.shortcuts)) {
      next.shortcuts = Object.assign({}, DEFAULT_SHORTCUTS);
      Object.keys(DEFAULT_SHORTCUTS).forEach((action) => {
        const combo = normalizeComboString(raw.shortcuts[action]);
        if (combo) {
          next.shortcuts[action] = combo;
        }
      });
    }

    if (isPlainObject(raw.siteRules)) {
      next.siteRules = normalizeSiteRules(raw.siteRules);
    }

    next.appVersion = APP.version;
    return next;
  }

  function normalizeSchedule(schedule) {
    if (!isPlainObject(schedule)) {
      return null;
    }
    const days = Array.isArray(schedule.days)
      ? uniqueNumbers(schedule.days.map((day) => Number(day)).filter((day) => day >= 0 && day <= 6))
      : [];
    const start = isTimeString(schedule.start) ? schedule.start : "";
    const end = isTimeString(schedule.end) ? schedule.end : "";
    if (!days.length || !start || !end) {
      return null;
    }
    return {
      id: stringOr(schedule.id, createId("schedule")),
      name: stringOr(schedule.name, "Focus window").trim() || "Focus window",
      days,
      start,
      end,
      enabled: schedule.enabled !== false
    };
  }

  function normalizeTimeLogs(logs) {
    const next = {};
    Object.keys(logs).forEach((dateKey) => {
      if (!/^\d{4}-\d{2}-\d{2}$/.test(dateKey) || !isPlainObject(logs[dateKey])) {
        return;
      }
      const day = {};
      Object.keys(logs[dateKey]).forEach((domain) => {
        const cleanDomain = normalizeDomain(domain);
        const seconds = Math.floor(positiveNumber(logs[dateKey][domain], 0));
        if (cleanDomain && seconds > 0) {
          day[cleanDomain] = seconds;
        }
      });
      if (Object.keys(day).length) {
        next[dateKey] = day;
      }
    });
    return next;
  }

  function normalizeSiteRules(rules) {
    const next = {};
    Object.keys(rules).forEach((domain) => {
      const cleanDomain = normalizeDomain(domain);
      const rule = rules[domain];
      if (!cleanDomain || !isPlainObject(rule)) {
        return;
      }
      const disabledFeatures = {};
      if (isPlainObject(rule.disabledFeatures)) {
        SITE_RULE_FEATURES.forEach((feature) => {
          if (rule.disabledFeatures[feature]) {
            disabledFeatures[feature] = true;
          }
        });
      }
      next[cleanDomain] = {
        toolkitDisabled: Boolean(rule.toolkitDisabled),
        disabledFeatures
      };
    });
    return next;
  }

  function readStoredState() {
    const fallback = null;
    try {
      if (typeof GM_getValue === "function") {
        return GM_getValue(APP.storageKey, fallback);
      }
    } catch (error) {
      console.warn("Productivity Toolkit: GM_getValue failed", error);
    }
    try {
      const raw = window.localStorage && window.localStorage.getItem(APP.storageKey);
      return raw ? JSON.parse(raw) : fallback;
    } catch (error) {
      console.warn("Productivity Toolkit: localStorage read failed", error);
      return fallback;
    }
  }

  function saveState() {
    if (pendingSaveTimer) {
      window.clearTimeout(pendingSaveTimer);
      pendingSaveTimer = null;
    }
    state.appVersion = APP.version;
    try {
      if (typeof GM_setValue === "function") {
        GM_setValue(APP.storageKey, state);
        return;
      }
    } catch (error) {
      console.warn("Productivity Toolkit: GM_setValue failed", error);
    }
    try {
      if (window.localStorage) {
        window.localStorage.setItem(APP.storageKey, JSON.stringify(state));
      }
    } catch (error) {
      console.warn("Productivity Toolkit: localStorage write failed", error);
    }
  }

  function saveStateSoon() {
    if (pendingSaveTimer) {
      window.clearTimeout(pendingSaveTimer);
    }
    pendingSaveTimer = window.setTimeout(() => {
      pendingSaveTimer = null;
      saveState();
    }, 350);
  }

  function bootWhenReady() {
    if (!document.documentElement) {
      return;
    }
    if (!document.body) {
      window.setTimeout(bootWhenReady, 50);
      return;
    }
    addStyles();
    bindGlobalListeners();
    currentDomain = getCurrentDomain();

    if (isToolkitDisabledForCurrentDomain()) {
      createDisabledLauncher();
      return;
    }

    startToolkit();
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", bootWhenReady, { once: true });
  } else {
    bootWhenReady();
  }

  function startToolkit() {
    if (toolkitStarted || isToolkitDisabledForCurrentDomain()) {
      return;
    }
    if (!createRoot()) {
      window.setTimeout(startToolkit, 50);
      return;
    }
    toolkitStarted = true;
    applyPanelPosition();
    renderPanel();
    startIntervals();
    checkSchedules();
    updateFocusOverlay();
    updateLauncher();
  }

  function stopToolkitUiOnly() {
    flushTimeTracker();
    stopIntervals();
    removeFocusOverlay();
    if (root) {
      root.remove();
    }
    root = null;
    panel = null;
    edgeTab = null;
    toolkitStarted = false;
  }

  function createDisabledLauncher() {
    if (document.getElementById(APP.disabledId) || !document.body) {
      return;
    }
    const button = document.createElement("button");
    button.id = APP.disabledId;
    button.type = "button";
    button.innerHTML = renderToolboxIcon() + "<small>Off</small>";
    button.setAttribute("aria-label", "Re-enable Productivity Toolkit on this site");
    button.title = "Productivity Toolkit is disabled on this site. Click to re-enable.";
    button.addEventListener("click", () => {
      const rule = ensureSiteRule(currentDomain);
      rule.toolkitDisabled = false;
      saveState();
      button.remove();
      startToolkit();
      toast("Toolkit re-enabled for this site.");
    });
    document.body.appendChild(button);
  }

  function createRoot() {
    if (!document.body) {
      return false;
    }
    const existingRoot = document.getElementById(APP.rootId);
    if (existingRoot) {
      existingRoot.remove();
    }

    root = document.createElement("div");
    root.id = APP.rootId;
    root.dataset.theme = getTheme();
    root.innerHTML = [
      '<button type="button" class="ptk-edge-tab" data-action="toggle-panel" aria-label="Open Productivity Toolkit">',
      renderToolboxIcon(),
      '<small>Toolkit</small>',
      "</button>",
      '<section class="ptk-panel" role="dialog" aria-label="Productivity Toolkit"></section>',
      '<div class="ptk-toast" aria-live="polite"></div>'
    ].join("");
    document.body.appendChild(root);

    edgeTab = root.querySelector(".ptk-edge-tab");
    panel = root.querySelector(".ptk-panel");

    root.addEventListener("click", handleRootClick);
    root.addEventListener("input", handleRootInput);
    root.addEventListener("change", handleRootChange);
    root.addEventListener("pointerdown", handleDragStart);
    return true;
  }

  function renderToolboxIcon() {
    return [
      '<svg class="ptk-toolbox-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">',
      '<path d="M9 6V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1"></path>',
      '<rect x="3" y="6" width="18" height="14" rx="2"></rect>',
      '<path d="M3 11h18"></path>',
      '<path d="M10 11v2h4v-2"></path>',
      "</svg>"
    ].join("");
  }

  function renderPanel() {
    if (!panel) {
      return;
    }

    if (edgeTab) {
      edgeTab.setAttribute("aria-expanded", state.ui.open ? "true" : "false");
    }
    panel.hidden = !state.ui.open;
    updateLauncher();
    if (!state.ui.open) {
      return;
    }

    const tabs = [
      ["notes", "Notes"],
      ["snippets", "Snippets"],
      ["focus", "Focus"],
      ["timer", "Timer"],
      ["reports", "Reports"],
      ["highlight", "Highlight"],
      ["shortcuts", "Shortcuts"],
      ["site", "Site"],
      ["backup", "Backup"]
    ];
    const theme = getTheme();
    const themeButtonLabel = theme === "dark" ? "Light" : "Dark";

    panel.innerHTML = [
      '<header class="ptk-header" data-drag-handle>',
      '<div><strong>Productivity Toolkit</strong><span>v' + escapeHtml(APP.version) + "</span></div>",
      '<div class="ptk-header-actions">',
      '<button type="button" class="ptk-icon-button" data-action="toggle-theme" title="Switch to ' + escapeAttr(themeButtonLabel.toLowerCase()) + ' mode">' + escapeHtml(themeButtonLabel) + "</button>",
      '<button type="button" class="ptk-icon-button" data-action="reset-position" title="Reset panel position">Reset</button>',
      '<button type="button" class="ptk-icon-button" data-action="close-panel" title="Close panel">Close</button>',
      "</div>",
      "</header>",
      '<nav class="ptk-tabs" aria-label="Toolkit sections">',
      tabs.map(([tab, label]) => {
        const active = state.ui.activeTab === tab ? " aria-current=\"page\"" : "";
        return '<button type="button" data-action="select-tab" data-tab="' + tab + '"' + active + ">" + escapeHtml(label) + "</button>";
      }).join(""),
      "</nav>",
      '<main class="ptk-body">',
      renderActiveTab(),
      "</main>"
    ].join("");
  }

  function renderActiveTab() {
    switch (state.ui.activeTab) {
      case "snippets":
        return renderSnippetsTab();
      case "focus":
        return renderFocusTab();
      case "timer":
        return renderTimerTab();
      case "reports":
        return renderReportsTab();
      case "highlight":
        return renderHighlightTab();
      case "shortcuts":
        return renderShortcutsTab();
      case "site":
        return renderSiteTab();
      case "backup":
        return renderBackupTab();
      case "notes":
      default:
        return renderNotesTab();
    }
  }

  function renderNotesTab() {
    return [
      '<section class="ptk-section">',
      '<div class="ptk-section-title"><h2>Quick Notes</h2><span>Autosaved locally</span></div>',
      '<textarea class="ptk-notes" data-field="notes" spellcheck="true" placeholder="Capture quick notes here...">' + escapeHtml(state.notes) + "</textarea>",
      '<div class="ptk-row">',
      '<button type="button" data-action="export-notes">Export .txt</button>',
      '<button type="button" data-action="clear-notes">Clear notes</button>',
      "</div>",
      "</section>"
    ].join("");
  }

  function renderSnippetsTab() {
    const filter = state.snippetFilter || "all";
    const categories = uniqueStrings(state.snippetCategories.concat(state.snippets.map((snippet) => snippet.category)));
    const snippets = state.snippets.filter((snippet) => filter === "all" || snippet.category === filter);
    const categoryOptions = ['<option value="all">All categories</option>']
      .concat(categories.map((category) => {
        const selected = filter === category ? " selected" : "";
        return '<option value="' + escapeAttr(category) + '"' + selected + ">" + escapeHtml(category) + "</option>";
      }))
      .join("");
    const datalist = '<datalist id="ptk-category-list">' + categories.map((category) => '<option value="' + escapeAttr(category) + '"></option>').join("") + "</datalist>";

    return [
      '<section class="ptk-section">',
      '<div class="ptk-section-title"><h2>Text Expander</h2><span>' + state.snippets.length + " snippets</span></div>",
      '<label class="ptk-label">Filter<select data-change="snippet-filter">' + categoryOptions + "</select></label>",
      datalist,
      '<div class="ptk-subpanel">',
      '<h3>Add snippet</h3>',
      '<div class="ptk-grid-2">',
      '<label class="ptk-label">Shortcut<input data-field="new-snippet-shortcut" autocomplete="off" placeholder="omw"></label>',
      '<label class="ptk-label">Category<input data-field="new-snippet-category" list="ptk-category-list" placeholder="General"></label>',
      "</div>",
      '<label class="ptk-label">Expansion<textarea data-field="new-snippet-expansion" rows="3" placeholder="On my way."></textarea></label>',
      '<button type="button" data-action="add-snippet">Add snippet</button>',
      "</div>",
      '<div class="ptk-subpanel">',
      '<h3>Categories</h3>',
      '<div class="ptk-inline-form">',
      '<input data-field="new-category" placeholder="New category">',
      '<button type="button" data-action="add-category">Add</button>',
      "</div>",
      "</div>",
      '<div class="ptk-list">',
      snippets.length ? snippets.map(renderSnippetItem).join("") : '<p class="ptk-empty">No snippets in this category.</p>',
      "</div>",
      "</section>"
    ].join("");
  }

  function renderSnippetItem(snippet) {
    const checked = snippet.enabled ? " checked" : "";
    return [
      '<article class="ptk-list-item" data-snippet-id="' + escapeAttr(snippet.id) + '">',
      '<div class="ptk-list-heading">',
      '<label class="ptk-checkbox"><input type="checkbox" data-change="snippet-enabled" data-id="' + escapeAttr(snippet.id) + '"' + checked + "> Enabled</label>",
      '<button type="button" data-action="delete-snippet" data-id="' + escapeAttr(snippet.id) + '">Delete</button>',
      "</div>",
      '<div class="ptk-grid-2">',
      '<label class="ptk-label">Shortcut<input data-snippet-field="shortcut" data-id="' + escapeAttr(snippet.id) + '" value="' + escapeAttr(snippet.shortcut) + '"></label>',
      '<label class="ptk-label">Category<input data-snippet-field="category" data-id="' + escapeAttr(snippet.id) + '" list="ptk-category-list" value="' + escapeAttr(snippet.category) + '"></label>',
      "</div>",
      '<label class="ptk-label">Expansion<textarea data-snippet-field="expansion" data-id="' + escapeAttr(snippet.id) + '" rows="3">' + escapeHtml(snippet.expansion) + "</textarea></label>",
      "</article>"
    ].join("");
  }

  function renderFocusTab() {
    const active = isFocusActive();
    const remaining = active ? formatDuration(Math.ceil((state.focus.activeUntil - Date.now()) / 1000)) : "Inactive";
    const blocklistText = state.focus.blocklist.join("\n");
    return [
      '<section class="ptk-section">',
      '<div class="ptk-section-title"><h2>FocusLock</h2><span>' + escapeHtml(remaining) + "</span></div>",
      '<div class="ptk-status-row">',
      '<strong>Status</strong>',
      '<span class="' + (active ? "ptk-pill ptk-pill-active" : "ptk-pill") + '">' + (active ? "Active" : "Inactive") + "</span>",
      "</div>",
      '<div class="ptk-grid-2">',
      '<button type="button" data-action="start-focus-default">Start 25 minutes</button>',
      '<button type="button" data-action="stop-focus">Stop focus</button>',
      "</div>",
      '<div class="ptk-inline-form">',
      '<input data-field="custom-focus-minutes" type="number" min="1" max="720" value="25" aria-label="Custom focus minutes">',
      '<button type="button" data-action="start-focus-custom">Start custom</button>',
      "</div>",
      '<label class="ptk-label">Blocklist<textarea data-field="focus-blocklist" rows="7">' + escapeHtml(blocklistText) + "</textarea></label>",
      '<button type="button" data-action="add-current-site-blocklist">Add current site</button>',
      '<div class="ptk-subpanel">',
      '<h3>Schedules</h3>',
      renderScheduleList(),
      '<div class="ptk-schedule-add">',
      '<input data-field="new-schedule-name" placeholder="Deep work">',
      '<div class="ptk-day-row">' + DAY_LABELS.map((label, index) => '<label><input type="checkbox" data-schedule-day="' + index + '"' + (index >= 1 && index <= 5 ? " checked" : "") + "> " + label + "</label>").join("") + "</div>",
      '<div class="ptk-grid-2">',
      '<label class="ptk-label">Start<input data-field="new-schedule-start" type="time" value="09:00"></label>',
      '<label class="ptk-label">End<input data-field="new-schedule-end" type="time" value="11:00"></label>',
      "</div>",
      '<button type="button" data-action="add-schedule">Add schedule</button>',
      "</div>",
      "</div>",
      "</section>"
    ].join("");
  }

  function renderScheduleList() {
    if (!state.focus.schedules.length) {
      return '<p class="ptk-empty">No schedules yet.</p>';
    }
    return state.focus.schedules.map((schedule) => {
      const days = DAY_LABELS.map((label, index) => {
        const checked = schedule.days.includes(index) ? " checked" : "";
        return '<label><input type="checkbox" data-change="schedule-day" data-id="' + escapeAttr(schedule.id) + '" data-day="' + index + '"' + checked + "> " + label + "</label>";
      }).join("");
      return [
        '<article class="ptk-list-item">',
        '<div class="ptk-list-heading">',
        '<label class="ptk-checkbox"><input type="checkbox" data-change="schedule-enabled" data-id="' + escapeAttr(schedule.id) + '"' + (schedule.enabled ? " checked" : "") + "> Enabled</label>",
        '<button type="button" data-action="delete-schedule" data-id="' + escapeAttr(schedule.id) + '">Delete</button>',
        "</div>",
        '<label class="ptk-label">Name<input data-schedule-field="name" data-id="' + escapeAttr(schedule.id) + '" value="' + escapeAttr(schedule.name) + '"></label>',
        '<div class="ptk-day-row">' + days + "</div>",
        '<div class="ptk-grid-2">',
        '<label class="ptk-label">Start<input type="time" data-change="schedule-time" data-field-name="start" data-id="' + escapeAttr(schedule.id) + '" value="' + escapeAttr(schedule.start) + '"></label>',
        '<label class="ptk-label">End<input type="time" data-change="schedule-time" data-field-name="end" data-id="' + escapeAttr(schedule.id) + '" value="' + escapeAttr(schedule.end) + '"></label>',
        "</div>",
        "</article>"
      ].join("");
    }).join("");
  }

  function renderTimerTab() {
    const remainingMs = getPomodoroRemainingMs();
    return [
      '<section class="ptk-section">',
      '<div class="ptk-section-title"><h2>Pomodoro</h2><span>' + escapeHtml(capitalize(state.pomodoro.mode)) + "</span></div>",
      '<div class="ptk-timer-display">' + escapeHtml(formatCountdown(remainingMs)) + "</div>",
      '<div class="ptk-status-row">',
      '<strong>Completed work sessions</strong>',
      '<span class="ptk-pill">' + state.pomodoro.sessionsCompleted + "</span>",
      "</div>",
      '<div class="ptk-grid-2">',
      '<label class="ptk-label">Work minutes<input type="number" min="1" max="180" data-change="pomodoro-work" value="' + state.pomodoro.workMinutes + '"></label>',
      '<label class="ptk-label">Break minutes<input type="number" min="1" max="120" data-change="pomodoro-break" value="' + state.pomodoro.breakMinutes + '"></label>',
      "</div>",
      '<div class="ptk-grid-2">',
      '<button type="button" data-action="start-pomodoro-work">Start work</button>',
      '<button type="button" data-action="start-pomodoro-break">Start break</button>',
      '<button type="button" data-action="toggle-pomodoro">' + (state.pomodoro.running ? "Pause" : "Resume") + "</button>",
      '<button type="button" data-action="reset-pomodoro">Reset</button>',
      "</div>",
      "</section>"
    ].join("");
  }

  function renderReportsTab() {
    return [
      '<section class="ptk-section">',
      '<div class="ptk-section-title"><h2>Reports</h2><span>Domain-level browsing time</span></div>',
      '<div class="ptk-subpanel">',
      '<h3>Today</h3>',
      renderReportTable(getReportForDates([todayKey()])),
      '<div class="ptk-row">',
      '<button type="button" data-action="export-today-csv">Export today CSV</button>',
      '<button type="button" data-action="clear-today-report">Clear today</button>',
      "</div>",
      "</div>",
      '<div class="ptk-subpanel">',
      '<h3>Current week</h3>',
      renderReportTable(getReportForDates(currentWeekKeys())),
      '<button type="button" data-action="export-week-csv">Export week CSV</button>',
      "</div>",
      "</section>"
    ].join("");
  }

  function renderReportTable(rows) {
    if (!rows.length) {
      return '<p class="ptk-empty">No tracked time yet.</p>';
    }
    return [
      '<table class="ptk-table"><thead><tr><th>Domain</th><th>Time</th></tr></thead><tbody>',
      rows.map((row) => '<tr><td>' + escapeHtml(row.domain) + "</td><td>" + escapeHtml(formatDuration(row.seconds)) + "</td></tr>").join(""),
      "</tbody></table>"
    ].join("");
  }

  function renderHighlightTab() {
    return [
      '<section class="ptk-section">',
      '<div class="ptk-section-title"><h2>Highlights</h2><span>Current page only</span></div>',
      '<button type="button" data-action="highlight-selection">Highlight selected text</button>',
      '<label class="ptk-label">Phrase<input data-field="highlight-phrase" placeholder="Phrase to highlight"></label>',
      '<label class="ptk-checkbox"><input type="checkbox" data-field="highlight-case-sensitive"> Case sensitive</label>',
      '<div class="ptk-grid-2">',
      '<button type="button" data-action="highlight-phrase">Highlight phrase</button>',
      '<button type="button" data-action="clear-highlights">Clear highlights</button>',
      "</div>",
      "</section>"
    ].join("");
  }

  function renderShortcutsTab() {
    return [
      '<section class="ptk-section">',
      '<div class="ptk-section-title"><h2>Keyboard Shortcuts</h2><span>' + (shortcutCaptureAction ? "Capturing..." : "Editable") + "</span></div>",
      '<div class="ptk-list">',
      Object.keys(DEFAULT_SHORTCUTS).map((action) => [
        '<article class="ptk-list-item">',
        '<div class="ptk-list-heading">',
        '<strong>' + escapeHtml(SHORTCUT_LABELS[action]) + "</strong>",
        '<span class="ptk-kbd">' + escapeHtml(state.shortcuts[action] || DEFAULT_SHORTCUTS[action]) + "</span>",
        "</div>",
        '<button type="button" data-action="capture-shortcut" data-shortcut-action="' + escapeAttr(action) + '">' + (shortcutCaptureAction === action ? "Press keys..." : "Capture") + "</button>",
        "</article>"
      ].join("")).join(""),
      "</div>",
      '<button type="button" data-action="reset-shortcuts">Restore defaults</button>',
      "</section>"
    ].join("");
  }

  function renderSiteTab() {
    const domain = currentDomain || "no-hostname";
    const rule = getSiteRule(currentDomain);
    return [
      '<section class="ptk-section">',
      '<div class="ptk-section-title"><h2>Site Rules</h2><span>' + escapeHtml(domain) + "</span></div>",
      '<label class="ptk-checkbox ptk-danger-check"><input type="checkbox" data-change="site-toolkit-disabled"' + (rule.toolkitDisabled ? " checked" : "") + "> Disable entire toolkit on this site</label>",
      '<div class="ptk-list">',
      SITE_RULE_FEATURES.map((feature) => {
        const disabled = Boolean(rule.disabledFeatures[feature]);
        return [
          '<label class="ptk-checkbox ptk-list-item">',
          '<input type="checkbox" data-change="site-feature-disabled" data-feature="' + escapeAttr(feature) + '"' + (disabled ? " checked" : "") + ">",
          " Disable " + escapeHtml(FEATURE_LABELS[feature]),
          "</label>"
        ].join("");
      }).join(""),
      "</div>",
      "</section>"
    ].join("");
  }

  function renderBackupTab() {
    return [
      '<section class="ptk-section">',
      '<div class="ptk-section-title"><h2>Backup / Restore</h2><span>JSON only</span></div>',
      '<div class="ptk-grid-2">',
      '<button type="button" data-action="export-backup">Download backup</button>',
      '<button type="button" data-action="copy-backup">Copy JSON</button>',
      "</div>",
      '<label class="ptk-label">Restore JSON<input type="file" data-change="backup-file" accept="application/json,.json"></label>',
      '<p class="ptk-muted">Restore validates the JSON before replacing local settings.</p>',
      "</section>"
    ].join("");
  }

  function handleRootClick(event) {
    const button = event.target.closest("[data-action]");
    if (!button || !root || !root.contains(button)) {
      return;
    }
    if (button.matches('input[type="checkbox"]')) {
      return;
    }
    event.preventDefault();
    event.stopPropagation();

    const action = button.dataset.action;
    switch (action) {
      case "toggle-panel":
        togglePanel();
        break;
      case "close-panel":
        closePanel();
        break;
      case "select-tab":
        openPanel(button.dataset.tab);
        break;
      case "reset-position":
        resetPanelPosition();
        break;
      case "toggle-theme":
        toggleTheme();
        break;
      case "export-notes":
        exportNotes();
        break;
      case "clear-notes":
        clearNotes();
        break;
      case "add-snippet":
        addSnippetFromForm();
        break;
      case "delete-snippet":
        deleteSnippet(button.dataset.id);
        break;
      case "add-category":
        addCategoryFromForm();
        break;
      case "start-focus-default":
        startFocusSession(25, "manual");
        break;
      case "start-focus-custom":
        startCustomFocusSession();
        break;
      case "stop-focus":
        stopFocusSession();
        break;
      case "add-current-site-blocklist":
        addCurrentSiteToBlocklist();
        break;
      case "add-schedule":
        addScheduleFromForm();
        break;
      case "delete-schedule":
        deleteSchedule(button.dataset.id);
        break;
      case "start-pomodoro-work":
        startPomodoro("work");
        break;
      case "start-pomodoro-break":
        startPomodoro("break");
        break;
      case "toggle-pomodoro":
        togglePomodoroPause();
        break;
      case "reset-pomodoro":
        resetPomodoro();
        break;
      case "export-today-csv":
        exportReportCsv([todayKey()], "productivity-toolkit-today.csv");
        break;
      case "export-week-csv":
        exportReportCsv(currentWeekKeys(), "productivity-toolkit-week.csv");
        break;
      case "clear-today-report":
        clearTodayReport();
        break;
      case "highlight-selection":
        highlightSelectedText();
        break;
      case "highlight-phrase":
        highlightPhraseFromForm();
        break;
      case "clear-highlights":
        clearHighlights();
        break;
      case "capture-shortcut":
        captureShortcut(button.dataset.shortcutAction);
        break;
      case "reset-shortcuts":
        resetShortcuts();
        break;
      case "export-backup":
        exportBackup();
        break;
      case "copy-backup":
        copyBackup();
        break;
      default:
        break;
    }
  }

  function handleRootInput(event) {
    const target = event.target;
    if (!target) {
      return;
    }

    if (target.dataset.field === "notes") {
      state.notes = target.value;
      saveStateSoon();
      return;
    }

    if (target.dataset.field === "focus-blocklist") {
      state.focus.blocklist = parseBlocklist(target.value);
      saveState();
      updateFocusOverlay();
      return;
    }

    if (target.dataset.snippetField) {
      updateSnippetField(target.dataset.id, target.dataset.snippetField, target.value);
      return;
    }

    if (target.dataset.scheduleField) {
      updateScheduleField(target.dataset.id, target.dataset.scheduleField, target.value);
    }
  }

  function handleRootChange(event) {
    const target = event.target;
    if (!target) {
      return;
    }

    const change = target.dataset.change;
    switch (change) {
      case "snippet-enabled":
        setSnippetEnabled(target.dataset.id, target.checked);
        break;
      case "snippet-filter":
        state.snippetFilter = target.value;
        saveState();
        renderPanel();
        break;
      case "schedule-enabled":
        updateScheduleEnabled(target.dataset.id, target.checked);
        break;
      case "schedule-day":
        updateScheduleDay(target.dataset.id, Number(target.dataset.day), target.checked);
        break;
      case "schedule-time":
        updateScheduleField(target.dataset.id, target.dataset.fieldName, target.value);
        break;
      case "pomodoro-work":
        state.pomodoro.workMinutes = clamp(Number(target.value) || 25, 1, 180);
        if (!state.pomodoro.running && state.pomodoro.mode === "work") {
          state.pomodoro.remainingMs = state.pomodoro.workMinutes * 60 * 1000;
        }
        saveState();
        renderPanel();
        break;
      case "pomodoro-break":
        state.pomodoro.breakMinutes = clamp(Number(target.value) || 5, 1, 120);
        if (!state.pomodoro.running && state.pomodoro.mode === "break") {
          state.pomodoro.remainingMs = state.pomodoro.breakMinutes * 60 * 1000;
        }
        saveState();
        renderPanel();
        break;
      case "site-toolkit-disabled":
        setToolkitDisabledForCurrentSite(target.checked);
        break;
      case "site-feature-disabled":
        setFeatureDisabledForCurrentSite(target.dataset.feature, target.checked);
        break;
      case "backup-file":
        importBackupFile(target.files && target.files[0]);
        target.value = "";
        break;
      default:
        break;
    }
  }

  function bindGlobalListeners() {
    if (listenersBound) {
      return;
    }
    listenersBound = true;
    document.addEventListener("keydown", handleDocumentKeydown, true);
    document.addEventListener("visibilitychange", handleVisibilityChange);
    window.addEventListener("beforeunload", flushTimeTracker);
    window.addEventListener("resize", keepPanelInViewport);
    window.addEventListener("pointermove", handleDragMove);
    window.addEventListener("pointerup", handleDragEnd);
  }

  function handleDocumentKeydown(event) {
    if (shortcutCaptureAction) {
      const combo = eventToCombo(event);
      if (combo && !isModifierOnly(event)) {
        event.preventDefault();
        event.stopPropagation();
        state.shortcuts[shortcutCaptureAction] = combo;
        shortcutCaptureAction = null;
        saveState();
        renderPanel();
        toast("Shortcut saved.");
      }
      return;
    }

    if (isFeatureEnabled("shortcuts") && handleShortcut(event)) {
      return;
    }

    if (isFeatureEnabled("textExpander")) {
      handleTextExpansion(event);
    }
  }

  function handleShortcut(event) {
    const combo = eventToCombo(event);
    if (!combo || isTypingTarget(event.target)) {
      return false;
    }

    const shortcuts = state.shortcuts || DEFAULT_SHORTCUTS;
    if (combo === shortcuts.togglePanel) {
      event.preventDefault();
      togglePanel();
      return true;
    }
    if (combo === shortcuts.openNotes) {
      event.preventDefault();
      openPanel("notes");
      return true;
    }
    if (combo === shortcuts.highlightSelection && isFeatureEnabled("highlights")) {
      event.preventDefault();
      highlightSelectedText();
      return true;
    }
    if (combo === shortcuts.clearHighlights && isFeatureEnabled("highlights")) {
      event.preventDefault();
      clearHighlights();
      return true;
    }
    if (combo === shortcuts.toggleFocus && isFeatureEnabled("focusLock")) {
      event.preventDefault();
      if (isFocusActive()) {
        stopFocusSession();
      } else {
        startFocusSession(25, "manual");
      }
      return true;
    }
    return false;
  }

  function handleTextExpansion(event) {
    if (!TEXT_EXPANDER_KEYS.has(event.key) || event.ctrlKey || event.metaKey || event.altKey) {
      return;
    }
    const target = event.target;
    if (!target || isInsideToolkit(target) || isPasswordField(target)) {
      return;
    }

    if (isTextControl(target)) {
      expandInTextControl(event, target);
      return;
    }

    const editable = target.closest && target.closest("[contenteditable=''], [contenteditable='true']");
    if (editable) {
      expandInContentEditable(event, editable);
    }
  }

  function expandInTextControl(event, target) {
    const start = target.selectionStart;
    const end = target.selectionEnd;
    if (!Number.isInteger(start) || !Number.isInteger(end) || start !== end) {
      return;
    }
    const before = target.value.slice(0, start);
    const token = getLastToken(before);
    if (!token) {
      return;
    }
    const snippet = findEnabledSnippet(token);
    if (!snippet) {
      return;
    }

    event.preventDefault();
    const delimiter = delimiterForEvent(event, target);
    target.setRangeText(buildExpansionReplacement(snippet.expansion, delimiter), start - token.length, end, "end");
    target.dispatchEvent(new Event("input", { bubbles: true }));
  }

  function expandInContentEditable(event, editable) {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0 || !selection.isCollapsed) {
      return;
    }
    const range = selection.getRangeAt(0);
    const caret = getEditableCaretTextNode(range, editable);
    if (!caret) {
      return;
    }
    const before = caret.node.nodeValue.slice(0, caret.offset);
    const token = getLastToken(before);
    const snippet = token ? findEnabledSnippet(token) : null;
    if (!snippet) {
      return;
    }

    event.preventDefault();
    const delimiter = delimiterForEvent(event, null);
    const replacement = buildExpansionReplacement(snippet.expansion, delimiter);
    const replaceRange = document.createRange();
    replaceRange.setStart(caret.node, caret.offset - token.length);
    replaceRange.setEnd(caret.node, caret.offset);
    selection.removeAllRanges();
    selection.addRange(replaceRange);

    if (!document.execCommand || !document.execCommand("insertText", false, replacement)) {
      replaceRange.deleteContents();
      const textNode = document.createTextNode(replacement);
      replaceRange.insertNode(textNode);
      replaceRange.setStartAfter(textNode);
      replaceRange.collapse(true);
      selection.removeAllRanges();
      selection.addRange(replaceRange);
    }
  }

  function getEditableCaretTextNode(range, editable) {
    if (range.startContainer && range.startContainer.nodeType === Node.TEXT_NODE) {
      return {
        node: range.startContainer,
        offset: range.startOffset
      };
    }
    if (!editable || !range.startContainer || range.startContainer.nodeType !== Node.ELEMENT_NODE) {
      return null;
    }
    let node = range.startContainer.childNodes[Math.max(0, range.startOffset - 1)];
    while (node && node.lastChild) {
      node = node.lastChild;
    }
    if (node && node.nodeType === Node.TEXT_NODE) {
      return {
        node,
        offset: node.nodeValue.length
      };
    }
    return null;
  }

  function getLastToken(text) {
    const match = text.match(/([^\s.,!?;:()[\]{}"'`]+)$/);
    return match ? match[1] : "";
  }

  function findEnabledSnippet(shortcut) {
    return state.snippets.find((snippet) => snippet.enabled && snippet.shortcut === shortcut);
  }

  function delimiterForEvent(event, target) {
    if (event.key === "Enter") {
      return target && target.tagName === "INPUT" ? " " : "\n";
    }
    if (event.key === "Tab") {
      return " ";
    }
    return event.key === " " ? " " : event.key;
  }

  function buildExpansionReplacement(expansion, delimiter) {
    const text = String(expansion || "");
    if (delimiter && /[.,!?;:)\]}]/.test(delimiter) && text.endsWith(delimiter)) {
      return text;
    }
    return text + delimiter;
  }

  function addSnippetFromForm() {
    const shortcut = getFieldValue("new-snippet-shortcut").trim();
    const expansion = getFieldValue("new-snippet-expansion");
    const category = getFieldValue("new-snippet-category").trim() || "General";
    if (!shortcut || !expansion) {
      toast("Shortcut and expansion are required.");
      return;
    }
    if (state.snippets.some((snippet) => snippet.shortcut === shortcut)) {
      toast("That shortcut already exists.");
      return;
    }
    state.snippets.push({
      id: createId("snippet"),
      shortcut,
      expansion,
      category,
      enabled: true
    });
    state.snippetCategories = uniqueStrings(state.snippetCategories.concat(category));
    state.snippetFilter = "all";
    saveState();
    renderPanel();
    toast("Snippet added.");
  }

  function deleteSnippet(id) {
    state.snippets = state.snippets.filter((snippet) => snippet.id !== id);
    state.snippetCategories = uniqueStrings(["General"].concat(state.snippets.map((snippet) => snippet.category), state.snippetCategories));
    saveState();
    renderPanel();
    toast("Snippet deleted.");
  }

  function updateSnippetField(id, field, value) {
    const snippet = state.snippets.find((item) => item.id === id);
    if (!snippet || !["shortcut", "expansion", "category"].includes(field)) {
      return;
    }
    snippet[field] = field === "category" ? (value.trim() || "General") : value;
    if (field === "category") {
      state.snippetCategories = uniqueStrings(state.snippetCategories.concat(snippet.category));
    }
    saveState();
  }

  function setSnippetEnabled(id, enabled) {
    const snippet = state.snippets.find((item) => item.id === id);
    if (!snippet) {
      return;
    }
    snippet.enabled = Boolean(enabled);
    saveState();
  }

  function addCategoryFromForm() {
    const category = getFieldValue("new-category").trim();
    if (!category) {
      return;
    }
    state.snippetCategories = uniqueStrings(state.snippetCategories.concat(category));
    saveState();
    renderPanel();
  }

  function exportNotes() {
    downloadTextFile("productivity-toolkit-notes.txt", state.notes || "");
  }

  function clearNotes() {
    if (state.notes && !window.confirm("Clear all notes?")) {
      return;
    }
    state.notes = "";
    saveState();
    renderPanel();
    toast("Notes cleared.");
  }

  function startFocusSession(minutes, source, scheduleEnd) {
    if (!isFeatureEnabled("focusLock")) {
      return;
    }
    const duration = clamp(Number(minutes) || 25, 1, 720);
    state.focus.activeUntil = Date.now() + duration * 60 * 1000;
    state.focus.activeSource = source === "schedule" ? "schedule" : "manual";
    state.focus.activeScheduleEnd = positiveNumber(scheduleEnd, 0);
    if (source !== "schedule") {
      state.focus.schedulePausedUntil = 0;
    }
    saveState();
    updateFocusOverlay();
    renderPanel();
    toast("FocusLock started.");
  }

  function startCustomFocusSession() {
    const minutes = clamp(Number(getFieldValue("custom-focus-minutes")) || 25, 1, 720);
    startFocusSession(minutes, "manual");
  }

  function stopFocusSession() {
    if (state.focus.activeSource === "schedule" && state.focus.activeScheduleEnd > Date.now()) {
      state.focus.schedulePausedUntil = state.focus.activeScheduleEnd;
    }
    state.focus.activeUntil = 0;
    state.focus.activeSource = "manual";
    state.focus.activeScheduleEnd = 0;
    saveState();
    removeFocusOverlay();
    renderPanel();
    toast("FocusLock stopped.");
  }

  function isFocusActive() {
    if (state.focus.activeUntil > Date.now()) {
      return true;
    }
    if (state.focus.activeUntil) {
      state.focus.activeUntil = 0;
      state.focus.activeSource = "manual";
      state.focus.activeScheduleEnd = 0;
      saveState();
      removeFocusOverlay();
    }
    return false;
  }

  function parseBlocklist(value) {
    return uniqueStrings(String(value || "")
      .split(/\r?\n|,/)
      .map(normalizeDomain)
      .filter(Boolean));
  }

  function addCurrentSiteToBlocklist() {
    if (!currentDomain) {
      toast("This page does not have a normal hostname.");
      return;
    }
    state.focus.blocklist = uniqueStrings(state.focus.blocklist.concat(currentDomain));
    saveState();
    renderPanel();
    updateFocusOverlay();
    toast("Current site added to blocklist.");
  }

  function updateFocusOverlay() {
    if (!isFeatureEnabled("focusLock") || !isFocusActive() || !isBlockedDomain(currentDomain)) {
      removeFocusOverlay();
      return;
    }
    if (!document.body) {
      return;
    }
    let overlay = document.getElementById(APP.focusOverlayId);
    if (!overlay) {
      overlay = document.createElement("div");
      overlay.id = APP.focusOverlayId;
      overlay.addEventListener("click", (event) => {
        const button = event.target.closest("[data-focus-action]");
        if (button && button.dataset.focusAction === "stop") {
          event.preventDefault();
          stopFocusSession();
        }
      });
      document.body.appendChild(overlay);
    }
    overlay.innerHTML = [
      '<div class="ptk-focus-card">',
      "<h1>FocusLock is active</h1>",
      "<p>This site is on your blocklist during the current focus session.</p>",
      '<strong>' + escapeHtml(formatDuration(Math.ceil((state.focus.activeUntil - Date.now()) / 1000))) + " remaining</strong>",
      '<button type="button" data-focus-action="stop">End focus session</button>',
      "</div>"
    ].join("");
  }

  function removeFocusOverlay() {
    const overlay = document.getElementById(APP.focusOverlayId);
    if (overlay) {
      overlay.remove();
    }
  }

  function isBlockedDomain(domain) {
    const cleanDomain = normalizeDomain(domain);
    if (!cleanDomain) {
      return false;
    }
    return state.focus.blocklist.some((blocked) => {
      const cleanBlocked = normalizeDomain(blocked);
      return cleanDomain === cleanBlocked || cleanDomain.endsWith("." + cleanBlocked);
    });
  }

  function addScheduleFromForm() {
    const days = Array.from(root.querySelectorAll("[data-schedule-day]:checked")).map((input) => Number(input.dataset.scheduleDay));
    const start = getFieldValue("new-schedule-start");
    const end = getFieldValue("new-schedule-end");
    const name = getFieldValue("new-schedule-name").trim() || "Focus window";
    if (!days.length || !isTimeString(start) || !isTimeString(end)) {
      toast("Choose days, start time, and end time.");
      return;
    }
    state.focus.schedules.push({
      id: createId("schedule"),
      name,
      days: uniqueNumbers(days),
      start,
      end,
      enabled: true
    });
    saveState();
    renderPanel();
    checkSchedules();
  }

  function deleteSchedule(id) {
    state.focus.schedules = state.focus.schedules.filter((schedule) => schedule.id !== id);
    saveState();
    renderPanel();
  }

  function updateScheduleEnabled(id, enabled) {
    const schedule = findSchedule(id);
    if (!schedule) {
      return;
    }
    schedule.enabled = Boolean(enabled);
    saveState();
    checkSchedules();
  }

  function updateScheduleDay(id, day, checked) {
    const schedule = findSchedule(id);
    if (!schedule || day < 0 || day > 6) {
      return;
    }
    if (checked) {
      schedule.days = uniqueNumbers(schedule.days.concat(day));
    } else {
      schedule.days = schedule.days.filter((item) => item !== day);
    }
    saveState();
    checkSchedules();
  }

  function updateScheduleField(id, field, value) {
    const schedule = findSchedule(id);
    if (!schedule || !["name", "start", "end"].includes(field)) {
      return;
    }
    if ((field === "start" || field === "end") && !isTimeString(value)) {
      return;
    }
    schedule[field] = field === "name" ? (value.trim() || "Focus window") : value;
    saveState();
    checkSchedules();
  }

  function findSchedule(id) {
    return state.focus.schedules.find((schedule) => schedule.id === id);
  }

  function checkSchedules() {
    if (!isFeatureEnabled("focusLock")) {
      return;
    }
    const now = Date.now();
    if (state.focus.schedulePausedUntil > now) {
      return;
    }
    const activeWindow = getActiveScheduleWindow(new Date(now));
    if (!activeWindow) {
      if (state.focus.activeSource === "schedule" && state.focus.activeUntil) {
        state.focus.activeUntil = 0;
        state.focus.activeSource = "manual";
        state.focus.activeScheduleEnd = 0;
        saveState();
        removeFocusOverlay();
      }
      return;
    }
    if (isFocusActive() && state.focus.activeUntil >= activeWindow.end.getTime()) {
      return;
    }
    const minutes = Math.max(1, Math.ceil((activeWindow.end.getTime() - now) / 60000));
    startFocusSession(minutes, "schedule", activeWindow.end.getTime());
  }

  function getActiveScheduleWindow(now) {
    const windows = [];
    state.focus.schedules.forEach((schedule) => {
      if (!schedule.enabled) {
        return;
      }
      const today = scheduleWindowForDate(schedule, now);
      const yesterday = new Date(now);
      yesterday.setDate(yesterday.getDate() - 1);
      const previous = scheduleWindowForDate(schedule, yesterday);
      [today, previous].forEach((windowInfo) => {
        if (windowInfo && now >= windowInfo.start && now < windowInfo.end) {
          windows.push(windowInfo);
        }
      });
    });
    windows.sort((a, b) => a.end - b.end);
    return windows[0] || null;
  }

  function scheduleWindowForDate(schedule, date) {
    const day = date.getDay();
    if (!schedule.days.includes(day)) {
      return null;
    }
    const [startHour, startMinute] = schedule.start.split(":").map(Number);
    const [endHour, endMinute] = schedule.end.split(":").map(Number);
    const start = new Date(date);
    start.setHours(startHour, startMinute, 0, 0);
    const end = new Date(date);
    end.setHours(endHour, endMinute, 0, 0);
    if (end <= start) {
      end.setDate(end.getDate() + 1);
    }
    return { schedule, start, end };
  }

  function startPomodoro(mode) {
    if (!isFeatureEnabled("pomodoro")) {
      return;
    }
    state.pomodoro.mode = mode === "break" ? "break" : "work";
    state.pomodoro.running = true;
    state.pomodoro.remainingMs = (state.pomodoro.mode === "work" ? state.pomodoro.workMinutes : state.pomodoro.breakMinutes) * 60 * 1000;
    state.pomodoro.endsAt = Date.now() + state.pomodoro.remainingMs;
    saveState();
    renderPanel();
    updateLauncher();
  }

  function togglePomodoroPause() {
    if (!isFeatureEnabled("pomodoro")) {
      return;
    }
    if (state.pomodoro.running) {
      state.pomodoro.remainingMs = getPomodoroRemainingMs();
      state.pomodoro.running = false;
      state.pomodoro.endsAt = 0;
    } else {
      state.pomodoro.running = true;
      state.pomodoro.endsAt = Date.now() + state.pomodoro.remainingMs;
    }
    saveState();
    renderPanel();
    updateLauncher();
  }

  function resetPomodoro() {
    state.pomodoro.running = false;
    state.pomodoro.endsAt = 0;
    state.pomodoro.mode = "work";
    state.pomodoro.remainingMs = state.pomodoro.workMinutes * 60 * 1000;
    saveState();
    renderPanel();
    updateLauncher();
  }

  function updatePomodoro() {
    if (!isFeatureEnabled("pomodoro")) {
      return;
    }
    if (state.pomodoro.running && Date.now() >= state.pomodoro.endsAt) {
      const completedMode = state.pomodoro.mode;
      state.pomodoro.running = false;
      state.pomodoro.mode = completedMode === "work" ? "break" : "work";
      state.pomodoro.remainingMs = (state.pomodoro.mode === "work" ? state.pomodoro.workMinutes : state.pomodoro.breakMinutes) * 60 * 1000;
      state.pomodoro.endsAt = 0;
      if (completedMode === "work") {
        state.pomodoro.sessionsCompleted += 1;
      }
      saveState();
      playBeep();
      toast(completedMode === "work" ? "Work session complete." : "Break complete.");
      renderPanel();
    }
    updateLauncher();
  }

  function getPomodoroRemainingMs() {
    if (state.pomodoro.running) {
      return Math.max(0, state.pomodoro.endsAt - Date.now());
    }
    return Math.max(0, state.pomodoro.remainingMs);
  }

  function playBeep() {
    try {
      const AudioContextClass = window.AudioContext || window.webkitAudioContext;
      if (!AudioContextClass) {
        return;
      }
      const context = new AudioContextClass();
      const oscillator = context.createOscillator();
      const gain = context.createGain();
      oscillator.type = "sine";
      oscillator.frequency.value = 880;
      gain.gain.value = 0.06;
      oscillator.connect(gain);
      gain.connect(context.destination);
      oscillator.start();
      oscillator.stop(context.currentTime + 0.18);
      oscillator.addEventListener("ended", () => context.close());
    } catch (error) {
      console.warn("Productivity Toolkit: notification beep unavailable", error);
    }
  }

  function trackVisibleTime() {
    const now = Date.now();
    if (!isFeatureEnabled("timeTracker") || document.hidden || !currentDomain) {
      lastTrackedAt = now;
      return;
    }
    const deltaSeconds = Math.floor((now - lastTrackedAt) / 1000);
    lastTrackedAt = now;
    if (deltaSeconds <= 0 || deltaSeconds > 120) {
      return;
    }
    const day = todayKey();
    if (!state.timeLogs[day]) {
      state.timeLogs[day] = {};
    }
    state.timeLogs[day][currentDomain] = Math.floor((state.timeLogs[day][currentDomain] || 0) + deltaSeconds);
    if (now - lastTimeSaveAt > 30000) {
      lastTimeSaveAt = now;
      saveState();
      if (state.ui.activeTab === "reports") {
        renderPanel();
      }
    }
  }

  function flushTimeTracker() {
    trackVisibleTime();
    lastTimeSaveAt = Date.now();
    saveState();
  }

  function handleVisibilityChange() {
    lastTrackedAt = Date.now();
    if (document.hidden) {
      flushTimeTracker();
    }
  }

  function getReportForDates(dateKeys) {
    const totals = {};
    dateKeys.forEach((dateKey) => {
      const day = state.timeLogs[dateKey] || {};
      Object.keys(day).forEach((domain) => {
        totals[domain] = (totals[domain] || 0) + Math.floor(day[domain]);
      });
    });
    return Object.keys(totals)
      .map((domain) => ({ domain, seconds: totals[domain] }))
      .sort((a, b) => b.seconds - a.seconds);
  }

  function exportReportCsv(dateKeys, filename) {
    const rows = [["date", "domain", "seconds", "duration"]];
    dateKeys.forEach((dateKey) => {
      const day = state.timeLogs[dateKey] || {};
      Object.keys(day).sort().forEach((domain) => {
        rows.push([dateKey, domain, String(Math.floor(day[domain])), formatDuration(day[domain])]);
      });
    });
    downloadTextFile(filename, rows.map((row) => row.map(csvCell).join(",")).join("\n"), "text/csv");
  }

  function clearTodayReport() {
    if (!window.confirm("Clear today's time report?")) {
      return;
    }
    delete state.timeLogs[todayKey()];
    saveState();
    renderPanel();
  }

  function highlightSelectedText() {
    if (!isFeatureEnabled("highlights")) {
      return;
    }
    const selection = window.getSelection();
    const text = selection ? selection.toString().trim() : "";
    if (!text) {
      toast("Select text to highlight.");
      return;
    }
    const count = highlightPhrase(text, false);
    toast(count ? "Highlighted " + count + " match" + (count === 1 ? "." : "es.") : "No matches found.");
  }

  function highlightPhraseFromForm() {
    if (!isFeatureEnabled("highlights")) {
      return;
    }
    const phrase = getFieldValue("highlight-phrase").trim();
    const caseSensitive = Boolean(root.querySelector('[data-field="highlight-case-sensitive"]') && root.querySelector('[data-field="highlight-case-sensitive"]').checked);
    if (!phrase) {
      toast("Enter a phrase to highlight.");
      return;
    }
    const count = highlightPhrase(phrase, caseSensitive);
    toast(count ? "Highlighted " + count + " match" + (count === 1 ? "." : "es.") : "No matches found.");
  }

  function highlightPhrase(phrase, caseSensitive) {
    if (!phrase || !document.body) {
      return 0;
    }
    const regex = new RegExp(escapeRegExp(phrase), caseSensitive ? "g" : "gi");
    const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
    const nodes = [];
    let scannedTextNodes = 0;
    while (nodes.length < APP.maxHighlights && scannedTextNodes < APP.maxHighlightTextNodes) {
      const node = walker.nextNode();
      if (!node) {
        break;
      }
      scannedTextNodes += 1;
      if (!node.nodeValue || !node.nodeValue.trim()) {
        continue;
      }
      const parent = node.parentElement;
      if (!parent || shouldSkipHighlightParent(parent)) {
        continue;
      }
      regex.lastIndex = 0;
      if (!regex.test(node.nodeValue)) {
        continue;
      }
      nodes.push(node);
    }

    let count = 0;
    nodes.forEach((node) => {
      const fragment = document.createDocumentFragment();
      const text = node.nodeValue;
      regex.lastIndex = 0;
      let lastIndex = 0;
      let match;
      while ((match = regex.exec(text)) && count < APP.maxHighlights) {
        if (match.index > lastIndex) {
          fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
        }
        const mark = document.createElement("mark");
        mark.className = APP.highlightClass;
        mark.textContent = match[0];
        fragment.appendChild(mark);
        count += 1;
        lastIndex = match.index + match[0].length;
      }
      if (lastIndex < text.length) {
        fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
      }
      if (count > 0 && node.parentNode) {
        node.parentNode.replaceChild(fragment, node);
      }
    });

    return count;
  }

  function shouldSkipHighlightParent(parent) {
    if (isInsideToolkit(parent) || parent.closest("#" + APP.focusOverlayId)) {
      return true;
    }
    if (parent.closest("script, style, noscript, textarea, input, select, option, mark." + APP.highlightClass)) {
      return true;
    }
    return false;
  }

  function clearHighlights() {
    const marks = Array.from(document.querySelectorAll("mark." + APP.highlightClass));
    marks.forEach((mark) => {
      const text = document.createTextNode(mark.textContent || "");
      mark.replaceWith(text);
      if (text.parentNode) {
        text.parentNode.normalize();
      }
    });
    toast("Highlights cleared.");
  }

  function captureShortcut(action) {
    if (!DEFAULT_SHORTCUTS[action]) {
      return;
    }
    shortcutCaptureAction = action;
    renderPanel();
  }

  function resetShortcuts() {
    state.shortcuts = Object.assign({}, DEFAULT_SHORTCUTS);
    shortcutCaptureAction = null;
    saveState();
    renderPanel();
  }

  function setToolkitDisabledForCurrentSite(disabled) {
    if (!currentDomain) {
      toast("This page does not have a normal hostname.");
      renderPanel();
      return;
    }
    const rule = ensureSiteRule(currentDomain);
    rule.toolkitDisabled = Boolean(disabled);
    saveState();
    if (disabled) {
      stopToolkitUiOnly();
      createDisabledLauncher();
      return;
    }
    renderPanel();
  }

  function setFeatureDisabledForCurrentSite(feature, disabled) {
    if (!SITE_RULE_FEATURES.includes(feature) || !currentDomain) {
      return;
    }
    const rule = ensureSiteRule(currentDomain);
    if (disabled) {
      rule.disabledFeatures[feature] = true;
    } else {
      delete rule.disabledFeatures[feature];
    }
    saveState();
    updateFocusOverlay();
    updateLauncher();
  }

  function exportBackup() {
    downloadTextFile("productivity-toolkit-backup.json", buildBackupJson(), "application/json");
  }

  function copyBackup() {
    const json = buildBackupJson();
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(json)
        .then(() => toast("Backup JSON copied."))
        .catch(() => fallbackCopy(json));
      return;
    }
    fallbackCopy(json);
  }

  function fallbackCopy(text) {
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.setAttribute("readonly", "");
    textarea.style.position = "fixed";
    textarea.style.left = "-9999px";
    document.body.appendChild(textarea);
    textarea.select();
    try {
      document.execCommand("copy");
      toast("Backup JSON copied.");
    } catch (error) {
      toast("Copy failed; download the backup instead.");
    }
    textarea.remove();
  }

  function buildBackupJson() {
    return JSON.stringify({
      product: "Productivity Toolkit",
      version: APP.version,
      exportedAt: new Date().toISOString(),
      settings: state
    }, null, 2);
  }

  function importBackupFile(file) {
    if (!file) {
      return;
    }
    const reader = new FileReader();
    reader.onload = () => {
      try {
        restoreBackupText(String(reader.result || ""));
        toast("Backup restored.");
      } catch (error) {
        console.warn("Productivity Toolkit: backup restore failed", error);
        toast("Invalid backup JSON.");
      }
    };
    reader.onerror = () => toast("Could not read backup file.");
    reader.readAsText(file);
  }

  function restoreBackupText(text) {
    const parsed = JSON.parse(text);
    const candidate = parsed && isPlainObject(parsed.settings) ? parsed.settings : parsed;
    if (!isPlainObject(candidate) || !hasKnownSettingsShape(candidate)) {
      throw new Error("Backup is not a Productivity Toolkit settings object.");
    }
    state = normalizeState(candidate);
    saveState();
    currentDomain = getCurrentDomain();
    if (isToolkitDisabledForCurrentDomain()) {
      stopToolkitUiOnly();
      createDisabledLauncher();
    } else {
      const disabledLauncher = document.getElementById(APP.disabledId);
      if (disabledLauncher) {
        disabledLauncher.remove();
      }
      if (!toolkitStarted) {
        startToolkit();
      }
      applyTheme();
      renderPanel();
      startIntervals();
      checkSchedules();
      updateFocusOverlay();
    }
  }

  function hasKnownSettingsShape(candidate) {
    return [
      "snippets",
      "snippetCategories",
      "notes",
      "focus",
      "pomodoro",
      "timeLogs",
      "shortcuts",
      "siteRules",
      "ui"
    ].some((key) => Object.prototype.hasOwnProperty.call(candidate, key));
  }

  function togglePanel() {
    if (!toolkitStarted) {
      return;
    }
    state.ui.open = !state.ui.open;
    saveState();
    renderPanel();
  }

  function getTheme() {
    return state.ui && state.ui.theme === "dark" ? "dark" : "light";
  }

  function applyTheme() {
    if (root) {
      root.dataset.theme = getTheme();
    }
  }

  function toggleTheme() {
    state.ui.theme = getTheme() === "dark" ? "light" : "dark";
    applyTheme();
    saveState();
    renderPanel();
    toast("Switched to " + getTheme() + " mode.");
  }

  function openPanel(tab) {
    if (!toolkitStarted) {
      return;
    }
    if (tab) {
      state.ui.activeTab = tab;
    }
    state.ui.open = true;
    saveState();
    renderPanel();
  }

  function closePanel() {
    state.ui.open = false;
    saveState();
    renderPanel();
  }

  function resetPanelPosition() {
    state.ui.position = null;
    saveState();
    applyPanelPosition();
    toast("Panel position reset.");
  }

  function applyPanelPosition() {
    if (!panel) {
      return;
    }
    if (state.ui.position) {
      panel.style.left = state.ui.position.left + "px";
      panel.style.top = state.ui.position.top + "px";
      panel.style.right = "auto";
      panel.style.bottom = "auto";
    } else {
      panel.style.left = "auto";
      panel.style.top = "auto";
      panel.style.right = "18px";
      panel.style.bottom = "82px";
    }
  }

  function keepPanelInViewport() {
    if (!panel || !state.ui.position) {
      return;
    }
    state.ui.position.left = clamp(state.ui.position.left, 0, Math.max(0, window.innerWidth - 80));
    state.ui.position.top = clamp(state.ui.position.top, 0, Math.max(0, window.innerHeight - 40));
    applyPanelPosition();
    saveState();
  }

  function handleDragStart(event) {
    if (!panel || !event.target.closest("[data-drag-handle]")) {
      return;
    }
    if (event.target.closest("button, input, textarea, select, label, a")) {
      return;
    }
    const rect = panel.getBoundingClientRect();
    dragState = {
      offsetX: event.clientX - rect.left,
      offsetY: event.clientY - rect.top
    };
    document.documentElement.classList.add("ptk-dragging");
    event.preventDefault();
  }

  function handleDragMove(event) {
    if (!dragState || !panel) {
      return;
    }
    const rect = panel.getBoundingClientRect();
    const left = clamp(event.clientX - dragState.offsetX, 0, Math.max(0, window.innerWidth - rect.width));
    const top = clamp(event.clientY - dragState.offsetY, 0, Math.max(0, window.innerHeight - rect.height));
    state.ui.position = { left, top };
    applyPanelPosition();
  }

  function handleDragEnd() {
    if (!dragState) {
      return;
    }
    dragState = null;
    document.documentElement.classList.remove("ptk-dragging");
    saveState();
  }

  function startIntervals() {
    if (!pomodoroInterval) {
      pomodoroInterval = window.setInterval(updatePomodoro, 1000);
    }
    if (!timeTrackerInterval) {
      timeTrackerInterval = window.setInterval(trackVisibleTime, 5000);
    }
    if (!focusInterval) {
      focusInterval = window.setInterval(updateFocusOverlay, 1000);
    }
    if (!scheduleInterval) {
      scheduleInterval = window.setInterval(checkSchedules, 30000);
    }
  }

  function stopIntervals() {
    if (pomodoroInterval) {
      window.clearInterval(pomodoroInterval);
      pomodoroInterval = null;
    }
    if (timeTrackerInterval) {
      window.clearInterval(timeTrackerInterval);
      timeTrackerInterval = null;
    }
    if (focusInterval) {
      window.clearInterval(focusInterval);
      focusInterval = null;
    }
    if (scheduleInterval) {
      window.clearInterval(scheduleInterval);
      scheduleInterval = null;
    }
  }

  function updateLauncher() {
    if (!edgeTab) {
      return;
    }
    const label = edgeTab.querySelector("small");
    if (!label) {
      return;
    }
    if (state.pomodoro.running && isFeatureEnabled("pomodoro")) {
      label.textContent = formatCountdown(getPomodoroRemainingMs());
    } else {
      label.textContent = "Toolkit";
    }
  }

  function toast(message) {
    if (!root) {
      return;
    }
    const toastNode = root.querySelector(".ptk-toast");
    if (!toastNode) {
      return;
    }
    toastNode.textContent = message;
    toastNode.classList.add("ptk-toast-visible");
    window.clearTimeout(toastNode._ptkTimer);
    toastNode._ptkTimer = window.setTimeout(() => {
      toastNode.classList.remove("ptk-toast-visible");
    }, 2200);
  }

  function getFieldValue(field) {
    const input = root && root.querySelector('[data-field="' + cssEscape(field) + '"]');
    return input ? input.value || "" : "";
  }

  function getSiteRule(domain) {
    const cleanDomain = normalizeDomain(domain);
    if (!cleanDomain || !state.siteRules[cleanDomain]) {
      return { toolkitDisabled: false, disabledFeatures: {} };
    }
    return state.siteRules[cleanDomain];
  }

  function ensureSiteRule(domain) {
    const cleanDomain = normalizeDomain(domain);
    if (!state.siteRules[cleanDomain]) {
      state.siteRules[cleanDomain] = { toolkitDisabled: false, disabledFeatures: {} };
    }
    return state.siteRules[cleanDomain];
  }

  function isToolkitDisabledForCurrentDomain() {
    return Boolean(currentDomain && getSiteRule(currentDomain).toolkitDisabled);
  }

  function isFeatureEnabled(feature) {
    if (isToolkitDisabledForCurrentDomain()) {
      return false;
    }
    const rule = getSiteRule(currentDomain);
    return !rule.disabledFeatures[feature];
  }

  function getCurrentDomain() {
    try {
      return normalizeDomain(window.location && window.location.hostname ? window.location.hostname : "");
    } catch (error) {
      return "";
    }
  }

  function normalizeDomain(value) {
    return String(value || "")
      .trim()
      .toLowerCase()
      .replace(/^https?:\/\//, "")
      .replace(/\/.*$/, "")
      .replace(/:\d+$/, "")
      .replace(/^www\./, "");
  }

  function eventToCombo(event) {
    const parts = [];
    if (event.ctrlKey) {
      parts.push("Ctrl");
    }
    if (event.altKey) {
      parts.push("Alt");
    }
    if (event.shiftKey) {
      parts.push("Shift");
    }
    if (event.metaKey) {
      parts.push("Meta");
    }
    const key = normalizeKey(event.key);
    if (!key) {
      return "";
    }
    if (!["Control", "Alt", "Shift", "Meta"].includes(key)) {
      parts.push(key);
    }
    return parts.join("+");
  }

  function normalizeComboString(combo) {
    if (typeof combo !== "string" || !combo.trim()) {
      return "";
    }
    const parts = combo.split("+").map((part) => part.trim()).filter(Boolean);
    const modifiers = [];
    let key = "";
    parts.forEach((part) => {
      const lower = part.toLowerCase();
      if (lower === "ctrl" || lower === "control") {
        modifiers.push("Ctrl");
      } else if (lower === "alt" || lower === "option") {
        modifiers.push("Alt");
      } else if (lower === "shift") {
        modifiers.push("Shift");
      } else if (lower === "meta" || lower === "cmd" || lower === "command") {
        modifiers.push("Meta");
      } else {
        key = normalizeKey(part);
      }
    });
    return uniqueStrings(modifiers).concat(key ? [key] : []).join("+");
  }

  function normalizeKey(key) {
    if (!key) {
      return "";
    }
    const aliases = {
      " ": "Space",
      Spacebar: "Space",
      Esc: "Escape",
      Del: "Delete",
      Up: "ArrowUp",
      Down: "ArrowDown",
      Left: "ArrowLeft",
      Right: "ArrowRight"
    };
    if (aliases[key]) {
      return aliases[key];
    }
    return key.length === 1 ? key.toUpperCase() : key;
  }

  function isModifierOnly(event) {
    return ["Control", "Shift", "Alt", "Meta"].includes(event.key);
  }

  function isTypingTarget(target) {
    if (!target || isInsideToolkit(target)) {
      return Boolean(target && target.closest && target.closest("input, textarea, select, [contenteditable=''], [contenteditable='true']"));
    }
    return isTextControl(target) || Boolean(target.closest && target.closest("[contenteditable=''], [contenteditable='true']"));
  }

  function isTextControl(target) {
    if (!target || !target.tagName) {
      return false;
    }
    const tag = target.tagName.toLowerCase();
    if (tag === "textarea") {
      return true;
    }
    if (tag !== "input") {
      return false;
    }
    const type = String(target.type || "text").toLowerCase();
    return ["text", "search", "url", "tel", "email"].includes(type);
  }

  function isPasswordField(target) {
    return target && target.tagName && target.tagName.toLowerCase() === "input" && String(target.type || "").toLowerCase() === "password";
  }

  function isInsideToolkit(target) {
    return Boolean(target && target.closest && (target.closest("#" + APP.rootId) || target.closest("#" + APP.disabledId)));
  }

  function currentWeekKeys() {
    const today = new Date();
    const day = today.getDay();
    const mondayOffset = day === 0 ? -6 : 1 - day;
    const monday = new Date(today);
    monday.setDate(today.getDate() + mondayOffset);
    monday.setHours(0, 0, 0, 0);
    return Array.from({ length: 7 }, (_, index) => {
      const date = new Date(monday);
      date.setDate(monday.getDate() + index);
      return dateKey(date);
    });
  }

  function todayKey() {
    return dateKey(new Date());
  }

  function dateKey(date) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    return year + "-" + month + "-" + day;
  }

  function formatDuration(seconds) {
    const total = Math.max(0, Math.floor(seconds));
    const hours = Math.floor(total / 3600);
    const minutes = Math.floor((total % 3600) / 60);
    const secs = total % 60;
    if (hours) {
      return hours + "h " + String(minutes).padStart(2, "0") + "m";
    }
    if (minutes) {
      return minutes + "m " + String(secs).padStart(2, "0") + "s";
    }
    return secs + "s";
  }

  function formatCountdown(ms) {
    const total = Math.max(0, Math.ceil(ms / 1000));
    const minutes = Math.floor(total / 60);
    const seconds = total % 60;
    return String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0");
  }

  function downloadTextFile(filename, text, mimeType) {
    const blob = new Blob([text], { type: mimeType || "text/plain" });
    const canUseObjectUrl = window.URL && typeof URL.createObjectURL === "function";
    const url = canUseObjectUrl
      ? URL.createObjectURL(blob)
      : "data:" + encodeURIComponent(mimeType || "text/plain") + ";charset=utf-8," + encodeURIComponent(text);
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    link.remove();
    if (canUseObjectUrl && typeof URL.revokeObjectURL === "function") {
      window.setTimeout(() => URL.revokeObjectURL(url), 1000);
    }
  }

  function csvCell(value) {
    const text = String(value == null ? "" : value);
    return /[",\n]/.test(text) ? '"' + text.replace(/"/g, '""') + '"' : text;
  }

  function escapeHtml(value) {
    return String(value == null ? "" : value)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }

  function escapeAttr(value) {
    return escapeHtml(value);
  }

  function escapeRegExp(value) {
    return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }

  function cssEscape(value) {
    if (window.CSS && typeof window.CSS.escape === "function") {
      return window.CSS.escape(value);
    }
    return String(value).replace(/"/g, '\\"');
  }

  function createId(prefix) {
    return prefix + "-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
  }

  function isPlainObject(value) {
    return Boolean(value && typeof value === "object" && !Array.isArray(value));
  }

  function stringOr(value, fallback) {
    return typeof value === "string" ? value : fallback;
  }

  function positiveNumber(value, fallback) {
    const number = Number(value);
    return Number.isFinite(number) && number >= 0 ? number : fallback;
  }

  function clamp(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }

  function uniqueStrings(values) {
    const seen = new Set();
    const result = [];
    values.forEach((value) => {
      const text = String(value || "").trim();
      if (text && !seen.has(text)) {
        seen.add(text);
        result.push(text);
      }
    });
    return result;
  }

  function uniqueNumbers(values) {
    return Array.from(new Set(values.map(Number))).filter((value) => Number.isFinite(value)).sort((a, b) => a - b);
  }

  function isTimeString(value) {
    return typeof value === "string" && /^([01]\d|2[0-3]):[0-5]\d$/.test(value);
  }

  function capitalize(value) {
    const text = String(value || "");
    return text ? text.charAt(0).toUpperCase() + text.slice(1) : "";
  }

  function addStyles() {
    if (document.getElementById(APP.styleId)) {
      return;
    }
    const css = `
#${APP.rootId}, #${APP.rootId} *, #${APP.focusOverlayId}, #${APP.focusOverlayId} *, #${APP.disabledId} {
  box-sizing: border-box;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  letter-spacing: 0;
}
#${APP.rootId} {
  position: fixed;
  z-index: 2147483000;
  inset: auto 0 0 auto;
  color: #17202a;
}
#${APP.rootId} button, #${APP.disabledId} {
  border: 1px solid #8aa0b8;
  background: #ffffff;
  color: #17202a;
  min-height: 34px;
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 12px;
  line-height: 1.2;
  cursor: pointer;
}
#${APP.rootId} button:hover, #${APP.disabledId}:hover {
  background: #eef4fb;
}
.ptk-edge-tab {
  position: fixed;
  top: 50%;
  right: -76px;
  transform: translateY(-50%);
  width: 112px;
  min-height: 46px !important;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px 8px 12px !important;
  border-radius: 8px 0 0 8px !important;
  border: 1px solid #5f7f9d !important;
  background: #e9edf2 !important;
  box-shadow: 0 10px 26px rgba(23, 32, 42, 0.22);
  transition: right 0.18s ease, box-shadow 0.18s ease;
}
.ptk-edge-tab:hover, .ptk-edge-tab:focus, .ptk-edge-tab:focus-visible {
  background: #dde5ee !important;
  right: 0;
  box-shadow: 0 12px 30px rgba(23, 32, 42, 0.28);
}
.ptk-toolbox-icon {
  width: 18px;
  height: 18px;
  flex: 0 0 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ptk-edge-tab small {
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
}
.ptk-panel {
  position: fixed;
  right: 18px;
  bottom: 82px;
  width: min(420px, calc(100vw - 24px));
  max-height: min(760px, calc(100vh - 110px));
  background: #f8fafc;
  border: 1px solid #9cafbf;
  border-radius: 8px;
  box-shadow: 0 18px 48px rgba(23, 32, 42, 0.28);
  overflow: hidden;
}
.ptk-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: #243447;
  color: #ffffff;
  padding: 10px 12px;
  cursor: move;
  user-select: none;
}
.ptk-header strong {
  display: block;
  font-size: 14px;
}
.ptk-header span {
  display: block;
  margin-top: 2px;
  color: #dbe6ef;
  font-size: 11px;
}
.ptk-header-actions {
  display: flex;
  gap: 6px;
}
#${APP.rootId} .ptk-header .ptk-icon-button {
  background: #ffffff;
  border-color: #d5e0ea;
  min-height: 28px;
  padding: 4px 8px;
}
.ptk-tabs {
  display: flex;
  gap: 4px;
  padding: 8px;
  overflow-x: auto;
  background: #e8eef4;
  border-bottom: 1px solid #c5d1dc;
}
#${APP.rootId} .ptk-tabs button {
  min-height: 30px;
  white-space: nowrap;
  background: transparent;
  border-color: transparent;
}
#${APP.rootId} .ptk-tabs button[aria-current="page"] {
  background: #ffffff;
  border-color: #9cafbf;
  font-weight: 700;
}
.ptk-body {
  max-height: calc(min(760px, 100vh - 110px) - 94px);
  overflow: auto;
  padding: 12px;
}
.ptk-section {
  display: grid;
  gap: 12px;
}
.ptk-section-title {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
}
.ptk-section-title h2 {
  margin: 0;
  font-size: 16px;
  line-height: 1.25;
}
.ptk-section-title span, .ptk-muted {
  color: #5f6f7f;
  font-size: 12px;
}
.ptk-subpanel, .ptk-list-item {
  border: 1px solid #d2dce6;
  border-radius: 8px;
  background: #ffffff;
  padding: 10px;
}
.ptk-subpanel h3 {
  margin: 0 0 8px;
  font-size: 13px;
}
.ptk-list {
  display: grid;
  gap: 8px;
}
.ptk-list-heading, .ptk-status-row, .ptk-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.ptk-grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.ptk-inline-form {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  align-items: end;
}
.ptk-label {
  display: grid;
  gap: 5px;
  color: #405163;
  font-size: 12px;
}
#${APP.rootId} input, #${APP.rootId} textarea, #${APP.rootId} select {
  width: 100%;
  min-height: 34px;
  border: 1px solid #aebccd;
  border-radius: 6px;
  background: #ffffff;
  color: #17202a;
  padding: 7px 8px;
  font-size: 13px;
  line-height: 1.3;
}
#${APP.rootId} textarea {
  resize: vertical;
}
.ptk-notes {
  min-height: 230px;
}
.ptk-checkbox {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  color: #17202a;
}
#${APP.rootId} .ptk-checkbox input {
  width: auto;
  min-height: auto;
}
.ptk-danger-check {
  border: 1px solid #e0b1a7;
  border-radius: 8px;
  background: #fff7f5;
  padding: 10px;
}
.ptk-day-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  font-size: 12px;
}
.ptk-day-row input {
  width: auto !important;
  min-height: auto !important;
}
.ptk-schedule-add {
  display: grid;
  gap: 8px;
}
.ptk-pill, .ptk-kbd {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  border-radius: 999px;
  background: #e8eef4;
  color: #27384a;
  padding: 3px 8px;
  font-size: 12px;
  font-weight: 700;
}
.ptk-pill-active {
  background: #daf2e3;
  color: #1d6537;
}
.ptk-timer-display {
  font-variant-numeric: tabular-nums;
  font-size: 42px;
  font-weight: 800;
  text-align: center;
  color: #223244;
  padding: 16px;
  background: #ffffff;
  border: 1px solid #d2dce6;
  border-radius: 8px;
}
.ptk-table {
  width: 100%;
  border-collapse: collapse;
  background: #ffffff;
  font-size: 12px;
}
.ptk-table th, .ptk-table td {
  border-bottom: 1px solid #d8e1ea;
  padding: 7px;
  text-align: left;
}
.ptk-table th:last-child, .ptk-table td:last-child {
  text-align: right;
}
.ptk-empty {
  margin: 0;
  color: #6b7c8f;
  font-size: 12px;
}
.${APP.highlightClass} {
  background: #fff08a !important;
  color: inherit !important;
  padding: 0 1px;
}
.ptk-toast {
  position: fixed;
  right: 18px;
  bottom: 84px;
  max-width: min(360px, calc(100vw - 24px));
  background: #17202a;
  color: #ffffff;
  border-radius: 8px;
  padding: 9px 11px;
  font-size: 12px;
  box-shadow: 0 10px 26px rgba(23, 32, 42, 0.24);
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px);
  transition: opacity 0.16s ease, transform 0.16s ease;
}
.ptk-toast-visible {
  opacity: 1;
  transform: translateY(0);
}
#${APP.focusOverlayId} {
  position: fixed;
  z-index: 2147482999;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(16, 24, 32, 0.94);
  color: #ffffff;
  padding: 24px;
}
.ptk-focus-card {
  width: min(520px, 100%);
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 8px;
  background: #233242;
  padding: 24px;
  text-align: center;
  box-shadow: 0 18px 54px rgba(0, 0, 0, 0.35);
}
.ptk-focus-card h1 {
  margin: 0 0 10px;
  font-size: 24px;
}
.ptk-focus-card p {
  margin: 0 0 16px;
  color: #d8e3ed;
}
.ptk-focus-card strong {
  display: block;
  margin-bottom: 18px;
}
.ptk-focus-card button {
  min-height: 38px;
  border: 1px solid #ffffff;
  border-radius: 6px;
  background: #ffffff;
  color: #17202a;
  padding: 8px 12px;
  cursor: pointer;
}
#${APP.disabledId} {
  position: fixed;
  z-index: 2147483000;
  top: 50%;
  right: -70px;
  bottom: auto;
  transform: translateY(-50%);
  width: 106px;
  min-height: 46px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px 8px 12px;
  border-radius: 8px 0 0 8px;
  border-color: #b56c5b;
  background: #ece7e5;
  box-shadow: 0 10px 24px rgba(23, 32, 42, 0.2);
  transition: right 0.18s ease, box-shadow 0.18s ease;
}
#${APP.disabledId}:hover, #${APP.disabledId}:focus, #${APP.disabledId}:focus-visible {
  background: #e3d8d4;
  right: 0;
  box-shadow: 0 12px 30px rgba(23, 32, 42, 0.26);
}
#${APP.disabledId} small {
  font-size: 11px;
  font-weight: 700;
}
.ptk-dragging, .ptk-dragging * {
  user-select: none !important;
}
#${APP.rootId}[data-theme="dark"] {
  color: #edf3f8;
}
#${APP.rootId}[data-theme="dark"] button {
  border-color: #53677c;
  background: #1b2633;
  color: #edf3f8;
}
#${APP.rootId}[data-theme="dark"] button:hover {
  background: #263648;
}
#${APP.rootId}[data-theme="dark"] .ptk-edge-tab {
  border-color: #6c8aae !important;
  background: #1b2633 !important;
  color: #f8fafc;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.38);
}
#${APP.rootId}[data-theme="dark"] .ptk-panel {
  background: #101820;
  border-color: #43566b;
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.48);
}
#${APP.rootId}[data-theme="dark"] .ptk-header {
  background: #0b1118;
  color: #f8fafc;
}
#${APP.rootId}[data-theme="dark"] .ptk-header span {
  color: #b8c7d7;
}
#${APP.rootId}[data-theme="dark"] .ptk-header .ptk-icon-button {
  background: #1b2633;
  border-color: #53677c;
  color: #edf3f8;
}
#${APP.rootId}[data-theme="dark"] .ptk-tabs {
  background: #172231;
  border-color: #34475c;
}
#${APP.rootId}[data-theme="dark"] .ptk-tabs button {
  color: #dbe5ee;
}
#${APP.rootId}[data-theme="dark"] .ptk-tabs button[aria-current="page"] {
  background: #243247;
  border-color: #526a85;
  color: #ffffff;
}
#${APP.rootId}[data-theme="dark"] .ptk-section-title span,
#${APP.rootId}[data-theme="dark"] .ptk-muted,
#${APP.rootId}[data-theme="dark"] .ptk-empty {
  color: #aab8c7;
}
#${APP.rootId}[data-theme="dark"] .ptk-subpanel,
#${APP.rootId}[data-theme="dark"] .ptk-list-item,
#${APP.rootId}[data-theme="dark"] .ptk-timer-display,
#${APP.rootId}[data-theme="dark"] .ptk-table {
  background: #151f2b;
  border-color: #34475c;
  color: #edf3f8;
}
#${APP.rootId}[data-theme="dark"] .ptk-label,
#${APP.rootId}[data-theme="dark"] .ptk-checkbox {
  color: #edf3f8;
}
#${APP.rootId}[data-theme="dark"] input,
#${APP.rootId}[data-theme="dark"] textarea,
#${APP.rootId}[data-theme="dark"] select {
  background: #0f1720;
  border-color: #52667d;
  color: #f8fafc;
}
#${APP.rootId}[data-theme="dark"] input::placeholder,
#${APP.rootId}[data-theme="dark"] textarea::placeholder {
  color: #8fa1b4;
}
#${APP.rootId}[data-theme="dark"] .ptk-danger-check {
  background: #291819;
  border-color: #76524f;
}
#${APP.rootId}[data-theme="dark"] .ptk-pill,
#${APP.rootId}[data-theme="dark"] .ptk-kbd {
  background: #27384c;
  color: #edf3f8;
}
#${APP.rootId}[data-theme="dark"] .ptk-pill-active {
  background: #123d2a;
  color: #9ee8b8;
}
#${APP.rootId}[data-theme="dark"] .ptk-table th,
#${APP.rootId}[data-theme="dark"] .ptk-table td {
  border-color: #34475c;
}
@media (max-width: 520px) {
  .ptk-panel {
    width: calc(100vw - 16px);
    right: 8px;
    bottom: 76px;
  }
  .ptk-grid-2 {
    grid-template-columns: 1fr;
  }
  .ptk-section-title, .ptk-list-heading, .ptk-status-row, .ptk-row {
    align-items: stretch;
    flex-direction: column;
  }
}
`;
    try {
      if (typeof GM_addStyle === "function") {
        const styleNode = GM_addStyle(css);
        if (styleNode && styleNode.setAttribute) {
          styleNode.setAttribute("id", APP.styleId);
        }
        return;
      }
    } catch (error) {
      console.warn("Productivity Toolkit: GM_addStyle failed", error);
    }
    const style = document.createElement("style");
    style.id = APP.styleId;
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);
  }
})();