<script lang="ts">
  // @ts-nocheck
  import { onDestroy } from "svelte";
  import { invoke } from "@tauri-apps/api/core";
  import {
    history,
    saveTranscriptMeta,
    deleteFromHistory,
    deleteFromHistoryById,
    renameHistoryEntry,
  } from "$lib/stores/page.svelte.js";
  import { toasts } from "$lib/stores/toasts.svelte.js";
  import { convertFileSrc } from "@tauri-apps/api/core";
  import {
    deriveAutoTags, buildFrontmatter, buildMarkdown, normaliseTag,
  } from "$lib/utils/frontmatter.js";
  import {
    saveTranscriptAsMarkdown,
    exportTranscriptsToDir,
  } from "$lib/utils/saveMarkdown";
  import { getPreferences } from "$lib/stores/preferences.svelte.js";
  import { clampTextLines, measurePreWrap } from "$lib/utils/textMeasure.js";
  import { bodyPretextLineHeight, pretextFontShorthand } from "$lib/utils/accessibilityTypography.js";
  import { buildCumulativeOffsets, findVisibleRange } from "$lib/utils/virtualList.js";
  import LumotiaCard from "$lib/ui/LumotiaCard.svelte";
  import LumotiaEmptyState from "$lib/ui/LumotiaEmptyState.svelte";
  import LumotiaButton from "$lib/ui/LumotiaButton.svelte";
  import LumotiaNotice from "$lib/ui/LumotiaNotice.svelte";
  // v0.3 Phase 5f — quietware History layout.
  import LumotiaPageSkeleton from "$lib/ui/LumotiaPageSkeleton.svelte";
  import LumotiaMenu from "$lib/ui/LumotiaMenu.svelte";
  import LumotiaIconButton from "$lib/ui/LumotiaIconButton.svelte";
  import { onMount } from "svelte";
  import { formatTime, formatDuration, formatTimestamp } from "$lib/utils/time.js";
  import { PLAYBACK_SPEEDS } from "$lib/utils/constants.js";
  import { Search, Clock, Play, Pause, FileText, Mic, ChevronDown, ExternalLink, Star, Tag, MoreHorizontal, Copy, Trash2, FileDown } from 'lucide-svelte';

  const prefs = getPreferences();
  const COLLAPSED_ROW_MIN_HEIGHT = 54;
  const COLLAPSED_ROW_VERTICAL_PADDING = 24;
  const EXPANDED_BASE_HEIGHT = 92;
  const EXPANDED_TITLE_INPUT_HEIGHT = 48;
  const EXPANDED_TAGS_ROW_HEIGHT = 40;
  const AUDIO_PLAYER_HEIGHT = 54;
  const HISTORY_PREVIEW_LINES = 2;
  const HISTORY_DURATION_WIDTH = 48;
  const HISTORY_DATE_WIDTH = 90;
  const HISTORY_ICON_WIDTH = 24;
  const HISTORY_SOURCE_WIDTH = 14;
  const HISTORY_CHEVRON_WIDTH = 14;
  const HISTORY_ROW_GAP = 12;
  const HISTORY_ROW_HORIZONTAL_PADDING = 32;
  const HISTORY_PREVIEW_MIN_WIDTH = 120;
  const BUFFER_COUNT = 6;

  let searchQuery = $state("");
  let showStarredOnly = $state(false);
  let activeTagFilter = $state(null); // null = all; string = tag value
  let expandedId = $state(null);

  // View toggle: "live" shows current transcripts (default), "trash"
  // shows soft-deleted transcripts. Backed by `list_trashed_transcripts`
  // and `restore_transcript` Tauri commands. Permanent-delete-from-trash
  // is intentionally deferred — the 30-day startup purge
  // (TRANSCRIPT_TRASH_RETENTION_DAYS in src-tauri/src/lib.rs) handles
  // hard-removal until the UI for it lands.
  let viewMode: "live" | "trash" = $state("live");
  let trashItems = $state([]);
  let trashLoading = $state(false);
  let trashError = $state("");
  let restoringId = $state(null);

  async function loadTrash() {
    trashLoading = true;
    trashError = "";
    try {
      trashItems = await invoke("list_trashed_transcripts", { limit: 500, offset: 0 });
    } catch (err) {
      trashError = typeof err === "string" ? err : String(err);
      toasts.error("Failed to load Trash", trashError);
    } finally {
      trashLoading = false;
    }
  }

  async function restoreFromTrash(id) {
    if (restoringId) return;
    restoringId = id;
    try {
      await invoke("restore_transcript", { id: String(id) });
      // Drop the row from the trash list. The live list will pick the
      // restored row up on next refresh; we deliberately do not splice
      // it into `history` directly here, because that store is loaded
      // by the page-store init path and inserting a partial DTO would
      // drift the in-memory shape from the SQLite source of truth.
      trashItems = trashItems.filter((t) => t.id !== id);
    } catch (err) {
      toasts.error(
        "Restore failed",
        typeof err === "string" ? err : String(err),
      );
    } finally {
      restoringId = null;
    }
  }

  // Formats an ISO timestamp for display in the Trash list. We surface
  // the transcript's `createdAt` rather than the deletion timestamp
  // because `deleted_at` is not currently in the TranscriptDto shape;
  // adding it is out of scope for this fix. Falls back to the raw
  // string if Date can't parse it so we never display "NaN".
  function formatTrashTimestamp(iso) {
    if (!iso) return "";
    try {
      const d = new Date(iso);
      if (Number.isNaN(d.getTime())) return iso;
      return d.toLocaleString();
    } catch {
      return iso;
    }
  }

  // Switching to trash view loads the list once. Switching back to live
  // discards the trash data so a stale list doesn't reappear on toggle.
  $effect(() => {
    if (viewMode === "trash") {
      void loadTrash();
    } else {
      trashItems = [];
      trashError = "";
    }
  });
  // Phase 9 bulk selection. Selection state is frontend-only and resets
  // on page refresh by design.
  let selected = $state(new Set());
  let playingId = $state(null);
  let audioEl = $state(null);
  let currentTime = $state(0);
  let duration = $state(0);
  let playbackRate = $state(1);
  let animFrameId = null;
  let listEl = $state(null);
  let containerHeight = $state(0);
  let containerWidth = $state(0);
  let scrollTop = $state(0);

  // Inline two-click confirmation state for destructive bulk actions.
  // First click arms the button (morphs into "Confirm" + "Cancel"), a
  // second click confirms. Auto-cancels after CONFIRM_TIMEOUT_MS so a
  // user who walked away doesn't return to a primed delete button.
  // Replaces the native window.confirm() dialog, which broke the warm
  // UI tone with an OS-styled modal.
  //
  // NOTE: clearAll (delete-everything) is gated by a stronger
  // type-the-word-DELETE modal — see CLEAR_ALL_CONFIRM_WORD below.
  // The arm-confirm pattern is retained for bulkDelete (selection
  // subset) where the failure mode is smaller.
  const CONFIRM_TIMEOUT_MS = 4000;
  let bulkDeleteArmed = $state(false);
  let bulkDeleteArmTimer: ReturnType<typeof setTimeout> | null = null;

  // Type-the-word modal state for clearAll. Replaces the 4-second
  // arm-confirm because Clear All wipes every transcript at once;
  // mis-clicks under the prior pattern soft-deleted the entire
  // history. The user must type the word DELETE exactly
  // (case-sensitive) before the Confirm button activates.
  const CLEAR_ALL_CONFIRM_WORD = "DELETE";
  let clearAllModalOpen = $state(false);
  let clearAllConfirmInput = $state("");
  let clearAllInputEl = $state(null);
  let clearAllInProgress = $state(false);
  let clearAllConfirmValid = $derived(
    clearAllConfirmInput === CLEAR_ALL_CONFIRM_WORD,
  );

  function openClearAllModal() {
    clearAllConfirmInput = "";
    clearAllModalOpen = true;
    // Focus the input on the next tick — Svelte 5 binds run after the
    // {#if} branch mounts, so a microtask is enough.
    queueMicrotask(() => {
      if (clearAllInputEl) clearAllInputEl.focus();
    });
  }

  function closeClearAllModal() {
    if (clearAllInProgress) return;
    clearAllModalOpen = false;
    clearAllConfirmInput = "";
  }

  function armBulkDelete() {
    bulkDeleteArmed = true;
    if (bulkDeleteArmTimer) clearTimeout(bulkDeleteArmTimer);
    bulkDeleteArmTimer = setTimeout(() => { bulkDeleteArmed = false; }, CONFIRM_TIMEOUT_MS);
  }
  function disarmBulkDelete() {
    bulkDeleteArmed = false;
    if (bulkDeleteArmTimer) { clearTimeout(bulkDeleteArmTimer); bulkDeleteArmTimer = null; }
  }

  // Screen-reader announcement for the inline-confirm flows. The button
  // morphs into Confirm/Cancel pills, which is silent to assistive tech;
  // this derived feeds an aria-live region near the page root so the
  // armed state is announced as it arms and disarms.
  let confirmAnnouncement = $derived(
    bulkDeleteArmed
      ? `Delete ${selected.size} ${selected.size === 1 ? 'transcript' : 'transcripts'} armed. Click confirm or cancel to dismiss.`
      : ""
  );

  // v0.3 Phase 5f — quietware layout switch. Same observer pattern
  // as Dictation / Files. v0.2 surface unchanged without quietware.
  let isQuietware = $state(false);
  let quietwareObserver: MutationObserver | null = null;

  onMount(() => {
    if (typeof document === "undefined") return;
    const sync = () => {
      isQuietware = document.documentElement.dataset.design === "quietware";
    };
    sync();
    quietwareObserver = new MutationObserver(sync);
    quietwareObserver.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["data-design"],
    });
  });

  onDestroy(() => {
    quietwareObserver?.disconnect();
    quietwareObserver = null;
    stopPlayback();
    if (bulkDeleteArmTimer) clearTimeout(bulkDeleteArmTimer);
  });

  function itemHasStar(h) {
    if (Array.isArray(h?.segments)) {
      return h.segments.some((s) => s?.starred);
    }
    return Boolean(h?.starred);
  }

  function itemAllTags(h) {
    const auto = deriveAutoTags(h);
    const manual = Array.isArray(h?.manualTags) ? h.manualTags : [];
    return [...auto, ...manual];
  }

  function parseTagFilter(q) {
    // Matches `tag:value` anywhere in the query; returns { tag, rest }.
    const match = q.match(/(?:^|\s)tag:([^\s]+)/i);
    if (!match) return { tag: null, rest: q };
    const rest = (q.slice(0, match.index) + " " + q.slice(match.index + match[0].length)).trim();
    return { tag: match[1].toLowerCase(), rest };
  }

  let searchParsed = $derived(parseTagFilter(searchQuery || ""));

  let allTags = $derived.by(() => {
    const counts = new Map();
    for (const h of history) {
      for (const tag of itemAllTags(h)) {
        const t = tag.toLowerCase();
        counts.set(t, (counts.get(t) || 0) + 1);
      }
    }
    return Array.from(counts.entries())
      .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
      .map(([tag, count]) => ({ tag, count }));
  });

  let filtered = $derived.by(() => {
    let items = history;
    if (showStarredOnly) items = items.filter(itemHasStar);
    if (activeTagFilter) {
      items = items.filter((h) => itemAllTags(h).some((t) => t.toLowerCase() === activeTagFilter));
    }
    if (searchParsed.tag) {
      items = items.filter((h) => itemAllTags(h).some((t) => t.toLowerCase() === searchParsed.tag));
    }
    const q = searchParsed.rest.trim().toLowerCase();
    if (q) {
      items = items.filter((h) => (
        (h.text && h.text.toLowerCase().includes(q)) ||
        (h.preview && h.preview.toLowerCase().includes(q)) ||
        (h.source && h.source.toLowerCase().includes(q)) ||
        (h.title && h.title.toLowerCase().includes(q))
      ));
    }
    return items;
  });

  let historyTextFont = $derived(pretextFontShorthand(prefs.accessibility, 13));
  let historyLineHeight = $derived(bodyPretextLineHeight(prefs.accessibility, 13));

  function compactPreviewText(item) {
    // The compact row shows the title (or a placeholder). The transcript
    // body lives in the expanded drawer so the two are visually distinct.
    return item.title?.trim() || "Untitled";
  }

  // v0.3 Phase 5f polish — quietware row helpers. Kept separate from
  // compactPreviewText so the v0.2 fallback's row-height measurement
  // (which reads compactPreviews) is untouched.

  /** Two-line transcript snippet for the quietware row sub-line. Falls
   *  through item.text → item.preview → empty string ("No preview…"
   *  rendering is the template's job, not this helper's). Stripped of
   *  leading whitespace so previews don't start mid-pause. */
  function quietRowPreview(item) {
    const raw = (item?.text || item?.preview || "").replace(/\s+/g, " ").trim();
    return raw;
  }

  /** Source label for the quietware row meta-line. "Dictation" for
   *  mic-captured items, "File" for imports. Mirrors the icon column
   *  but lives in the meta-line so the row scans without colour cues. */
  function quietRowSourceLabel(item) {
    const s = (item?.source || "").toLowerCase();
    return s.includes("file") ? "File" : "Dictation";
  }

  /** Just the HH:MM portion of a saved-at timestamp. The date sits in
   *  the group header above the row, so repeating it here would waste
   *  hierarchy. Uses en-GB 24h to match formatTimestamp. */
  function quietRowTime(iso) {
    if (!iso) return "";
    const d = new Date(iso);
    if (Number.isNaN(d.getTime())) return "";
    return d.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
  }

  /** Calendar-day label for date-group headers. Formula:
   *    same local-calendar day as now → "Today"
   *    previous local-calendar day    → "Yesterday"
   *    same local-calendar year       → "D MMM" (e.g. "12 May")
   *    older                          → "D MMM YYYY" (e.g. "12 May 2025")
   *  Day boundaries are strictly local midnight, not 24-hour windows. */
  function dateGroupLabel(iso) {
    if (!iso) return "Undated";
    const d = new Date(iso);
    if (Number.isNaN(d.getTime())) return "Undated";
    const now = new Date();
    const sameDay = (a, b) =>
      a.getFullYear() === b.getFullYear() &&
      a.getMonth() === b.getMonth() &&
      a.getDate() === b.getDate();
    if (sameDay(d, now)) return "Today";
    const y = new Date(now);
    y.setDate(now.getDate() - 1);
    if (sameDay(d, y)) return "Yesterday";
    const fmt = d.toLocaleDateString("en-GB", {
      day: "numeric",
      month: "short",
      year: d.getFullYear() === now.getFullYear() ? undefined : "numeric",
    });
    return fmt;
  }

  /** Stable bucket key for grouping. Local-calendar YYYY-MM-DD so the
   *  groups break at midnight, not on UTC date math. Items with no
   *  savedAt fall into an "undated" bucket that surfaces last. */
  function dateGroupKey(iso) {
    if (!iso) return "undated";
    const d = new Date(iso);
    if (Number.isNaN(d.getTime())) return "undated";
    const y = d.getFullYear();
    const m = (d.getMonth() + 1).toString().padStart(2, "0");
    const day = d.getDate().toString().padStart(2, "0");
    return `${y}-${m}-${day}`;
  }

  /** Walk `filtered` in order, breaking at calendar-day boundaries.
   *  Preserves the underlying sort so newest-first stays newest-first;
   *  groups appear in the order their first item appears. */
  let liveGroups = $derived.by(() => {
    const out = [];
    let current = null;
    for (const item of filtered) {
      const iso = item.savedAt || item.timestamp;
      const key = dateGroupKey(iso);
      if (!current || current.key !== key) {
        current = { key, label: dateGroupLabel(iso), items: [] };
        out.push(current);
      }
      current.items.push(item);
    }
    return out;
  });

  let compactPreviews = $derived.by(() => {
    return filtered.map((item) => {
      const text = compactPreviewText(item);
      if (!text) {
        return {
          text: "",
          fullText: "",
          height: historyLineHeight,
        };
      }

      if (containerWidth <= 0) {
        const fallbackText = text.length > 80 ? `${text.slice(0, 79)}…` : text;
        return {
          text: fallbackText,
          fullText: text,
          height: historyLineHeight,
        };
      }

      const reservedWidth =
        HISTORY_ROW_HORIZONTAL_PADDING +
        HISTORY_ICON_WIDTH +
        (item.duration ? HISTORY_DURATION_WIDTH : 0) +
        HISTORY_SOURCE_WIDTH +
        HISTORY_DATE_WIDTH +
        HISTORY_CHEVRON_WIDTH +
        HISTORY_ROW_GAP * (item.duration ? 5 : 4);
      const textWidth = Math.max(HISTORY_PREVIEW_MIN_WIDTH, containerWidth - reservedWidth);
      const preview = clampTextLines(
        text,
        historyTextFont,
        textWidth,
        historyLineHeight,
        HISTORY_PREVIEW_LINES,
      );

      return {
        text: preview.text,
        fullText: text,
        height: preview.height,
      };
    });
  });

  let itemHeights = $derived.by(() => {
    if (!filtered.length) return [];
    const textWidth = Math.max(0, containerWidth - 32);
    return filtered.map((item, index) => {
      const compactHeight = Math.max(
        COLLAPSED_ROW_MIN_HEIGHT,
        (compactPreviews[index]?.height || historyLineHeight) + COLLAPSED_ROW_VERTICAL_PADDING,
      );
      let height = compactHeight;
      if (expandedId === item.id) {
        height += EXPANDED_BASE_HEIGHT + EXPANDED_TITLE_INPUT_HEIGHT + EXPANDED_TAGS_ROW_HEIGHT;
        const transcriptHeight = item.text && textWidth > 0
          ? measurePreWrap(item.text, historyTextFont, textWidth, historyLineHeight).height
          : historyLineHeight;
        height += transcriptHeight;
        if (item.audioPath && playingId === item.id) {
          height += AUDIO_PLAYER_HEIGHT;
        }
      }
      return height;
    });
  });

  let cumulativeOffsets = $derived(buildCumulativeOffsets(itemHeights));
  let totalHeight = $derived(
    cumulativeOffsets.length > 0 ? cumulativeOffsets[cumulativeOffsets.length - 1] : 0
  );
  let visibleRange = $derived.by(() =>
    findVisibleRange(
      cumulativeOffsets,
      filtered.length,
      scrollTop,
      containerHeight,
      BUFFER_COUNT,
    )
  );
  let visibleItems = $derived.by(() => {
    const items = [];
    for (let i = visibleRange.start; i < visibleRange.end; i++) {
      items.push({
        item: filtered[i],
        top: cumulativeOffsets[i],
        height: itemHeights[i],
        preview: compactPreviews[i],
      });
    }
    return items;
  });

  // Wipe every live transcript. Soft-delete via the SQLite backend so
  // the user has a 30-day window to restore from Trash. Gated behind
  // the type-the-word-DELETE modal — the prior 4-second arm-confirm
  // let a single mis-click destroy the whole history.
  async function clearAll() {
    if (!clearAllConfirmValid || clearAllInProgress) return;
    clearAllInProgress = true;
    const ids = history.map((entry) => entry.id);
    history.splice(0);
    expandedId = null;
    stopPlayback();
    let failed = 0;
    for (const id of ids) {
      try {
        await invoke("delete_transcript", { id: String(id) });
      } catch (err) {
        failed++;
        console.warn("clearAll: SQLite delete failed", id, err);
      }
    }
    if (failed > 0) {
      toasts.error(
        "Some history entries could not be deleted",
        `${failed} of ${ids.length} failed to delete from disk and may reappear after restart.`,
      );
    }
    clearAllInProgress = false;
    clearAllModalOpen = false;
    clearAllConfirmInput = "";
  }

  function toggleExpand(id) {
    expandedId = expandedId === id ? null : id;
  }

  function onListScroll(e) {
    scrollTop = e.target.scrollTop;
  }

  function copyItem(item) {
    navigator.clipboard.writeText(item.text).catch(() => {
      invoke("copy_to_clipboard", { text: item.text }).catch(() => {});
    });
  }

  function removeItem(item) {
    const idx = history.indexOf(item);
    if (idx !== -1) {
      deleteFromHistory(idx);
      if (expandedId === item.id) expandedId = null;
      if (playingId === item.id) stopPlayback();
    }
  }

  function togglePlay(item) {
    if (playingId === item.id) {
      if (audioEl && !audioEl.paused) {
        audioEl.pause();
        cancelAnimationFrame(animFrameId);
      } else if (audioEl) {
        audioEl.play();
        tickTime();
      }
      return;
    }
    stopPlayback();
    try {
      const src = convertFileSrc(item.audioPath);
      const audio = new Audio(src);
      audio.playbackRate = playbackRate;
      audio.onloadedmetadata = () => { duration = audio.duration; };
      audio.onended = () => { stopPlayback(); };
      audio.onerror = () => { stopPlayback(); };
      audio.play();
      audioEl = audio;
      playingId = item.id;
      tickTime();
    } catch {
      stopPlayback();
    }
  }

  function stopPlayback() {
    if (audioEl) { audioEl.pause(); audioEl.src = ""; audioEl = null; }
    playingId = null;
    currentTime = 0;
    duration = 0;
    cancelAnimationFrame(animFrameId);
  }

  function tickTime() {
    if (audioEl) {
      currentTime = audioEl.currentTime;
      if (!audioEl.paused) {
        animFrameId = requestAnimationFrame(tickTime);
      }
    }
  }

  let seekTimeout = null;
  function seekTo(e) {
    const val = parseFloat(e.target.value);
    currentTime = val; // update UI immediately
    clearTimeout(seekTimeout);
    seekTimeout = setTimeout(() => {
      if (audioEl) audioEl.currentTime = val;
    }, 50);
  }

  function setSpeed(speed) {
    playbackRate = speed;
    if (audioEl) audioEl.playbackRate = speed;
  }

  async function openViewer(item) {
    // Hand off the transcript ID only — the viewer window fetches the full
    // row from SQLite via `get_transcript`. Previously the entire DTO
    // (text + segments + tags) was stuffed into localStorage, putting
    // potentially MB-scale PII in a place readable by any same-origin
    // script.
    const handoff = JSON.stringify({ id: String(item.id) });
    try {
      localStorage.setItem("lumotia_viewer_item", handoff);
      localStorage.setItem("lumotia_viewer_mode", "view");
      await invoke("open_viewer_window");
    } catch {
      localStorage.setItem("lumotia_viewer_item", handoff);
      localStorage.setItem("lumotia_viewer_mode", "view");
      window.open("/viewer", "_blank", "width=600,height=700");
    }
  }

  function handleAddTagKey(e, item) {
    if (e.key !== "Enter" && e.key !== ",") return;
    e.preventDefault();
    const raw = e.target.value || "";
    const next = normaliseTag(raw);
    if (!next) return;
    const existing = new Set((item.manualTags || []).map((t) => normaliseTag(t)));
    if (existing.has(next)) {
      e.target.value = "";
      return;
    }
    item.manualTags = [...(item.manualTags || []), next];
    saveTranscriptMeta(item.id, { manualTags: item.manualTags });
    e.target.value = "";
  }

  function removeManualTag(item, tag) {
    const t = normaliseTag(tag);
    item.manualTags = (item.manualTags || []).filter((x) => normaliseTag(x) !== t);
    saveTranscriptMeta(item.id, { manualTags: item.manualTags });
  }

  // Phase 9 LLM content tagging.
  let tagging = $state(new Set());
  let bulkTagging = $state(false);
  let bulkTaggingProgress = $state("");

  async function tagRow(item) {
    if (tagging.has(item.id)) return;
    const next = new Set(tagging);
    next.add(item.id);
    tagging = next;
    try {
      const result = await invoke("extract_content_tags_cmd", {
        transcript: item.text || "",
      });
      const r = result;
      item.llmTags = [`topic:${r.topic}`, `intent:${r.intent}`];
      await saveTranscriptMeta(item.id, { llmTags: item.llmTags });
    } catch (err) {
      toasts.error("Tagging failed", String(err));
    } finally {
      const rest = new Set(tagging);
      rest.delete(item.id);
      tagging = rest;
    }
  }

  function promoteLlmTag(item, tag) {
    const normalised = normaliseTag(tag);
    const manual = new Set((item.manualTags || []).map(normaliseTag));
    manual.add(normalised);
    item.manualTags = [...manual];
    item.llmTags = (item.llmTags || []).filter((t) => normaliseTag(t) !== normalised);
    saveTranscriptMeta(item.id, {
      manualTags: item.manualTags,
      llmTags: item.llmTags,
    });
  }

  async function tagAllUntagged() {
    if (bulkTagging) return;
    bulkTagging = true;
    try {
      const untagged = history.filter((i) => !i.llmTags || i.llmTags.length === 0);
      let done = 0;
      for (const item of untagged) {
        done += 1;
        bulkTaggingProgress = `${done} / ${untagged.length}`;
        try {
          const result = await invoke("extract_content_tags_cmd", {
            transcript: item.text || "",
          });
          const r = result;
          item.llmTags = [`topic:${r.topic}`, `intent:${r.intent}`];
          await saveTranscriptMeta(item.id, { llmTags: item.llmTags });
        } catch (err) {
          console.error("bulk tag failed for", item.id, err);
        }
      }
      toasts.success(`Tagged ${untagged.length} transcript${untagged.length === 1 ? "" : "s"}`);
    } finally {
      bulkTagging = false;
      bulkTaggingProgress = "";
    }
  }

  async function exportMarkdown(item) {
    await saveTranscriptAsMarkdown(item);
  }

  // Phase 9 bulk selection handlers.
  function toggleSelected(id) {
    const next = new Set(selected);
    if (next.has(id)) next.delete(id);
    else next.add(id);
    selected = next;
  }

  function clearSelection() {
    selected = new Set();
  }

  function selectAllVisible() {
    selected = new Set(filtered.map((i) => i.id));
  }

  let selectedItems = $derived(history.filter((i) => selected.has(i.id)));

  async function bulkExport() {
    const items = selectedItems;
    if (items.length === 0) return;
    await exportTranscriptsToDir(items);
    clearSelection();
  }

  async function bulkDelete() {
    if (selected.size === 0) return;
    disarmBulkDelete();
    for (const id of selected) deleteFromHistoryById(id);
    clearSelection();
  }

  // Phase 9 keyboard shortcuts: Escape clears selection, Cmd/Ctrl+A
  // selects all visible when the History list owns focus. The
  // listEl?.contains check prevents hijacking Cmd+A inside text inputs
  // outside the list (e.g. the search box, inline title input).
  $effect(() => {
    function onKey(e) {
      if (e.key === "Escape" && selected.size > 0) {
        clearSelection();
        e.preventDefault();
        return;
      }
      if ((e.key === "a" || e.key === "A") && (e.metaKey || e.ctrlKey)) {
        const target = document.activeElement;
        const inText =
          target &&
          (target.tagName === "INPUT" || target.tagName === "TEXTAREA");
        if (!inText && listEl && listEl.contains(target)) {
          selectAllVisible();
          e.preventDefault();
        }
      }
    }
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  });

  async function openEditor(item) {
    // Hand off the transcript ID only; the viewer hydrates from SQLite
    // (see openViewer above for the rationale).
    const handoff = JSON.stringify({ id: String(item.id) });
    try {
      localStorage.setItem("lumotia_viewer_item", handoff);
      localStorage.setItem("lumotia_viewer_mode", "edit");
      await invoke("open_viewer_window");
    } catch {
      localStorage.setItem("lumotia_viewer_item", handoff);
      localStorage.setItem("lumotia_viewer_mode", "edit");
      window.open("/viewer", "_blank", "width=600,height=700");
    }
  }

  $effect(() => {
    if (!listEl) return;
    const ro = new ResizeObserver((entries) => {
      for (const entry of entries) {
        containerHeight = entry.contentRect.height;
        containerWidth = entry.contentRect.width;
      }
    });
    ro.observe(listEl);
    return () => ro.disconnect();
  });

  $effect(() => {
    if (expandedId && !filtered.some((item) => item.id === expandedId)) {
      expandedId = null;
    }
  });
</script>

<div class="flex flex-col h-full {isQuietware ? '' : 'overflow-y-auto'} animate-fade-in">
  <!-- Off-screen live region for inline-confirm announcements. Sits at
       the page root so it persists while the morphed Confirm/Cancel
       pills mount and unmount in the header and bulk toolbar. -->
  <div
    class="sr-only"
    aria-live="polite"
    aria-atomic="true"
  >{confirmAnnouncement}</div>

  {#if isQuietware}
    <!-- =================================================================
         v0.3 Phase 5f — quietware History layout. Calm local archive.

           Header           "History" + Live / Trash tabs with counts
           Primary surface  search + date-grouped list / empty state
           Row              icon · title · 2-line preview · meta-line
                            Open (visible) + ⋯ overflow (Copy / Export
                            / Move to Trash). Trash rows show Restore
                            visible; permanent-delete UI is deferred
                            (the 30-day purge handles hard-removal,
                            see TRANSCRIPT_TRASH_RETENTION_DAYS).
           Mono footer      saved count · trash count · local only

         Reuses every existing state and handler: history (store),
         trashItems, viewMode, searchQuery, filtered (derived),
         copyItem, exportMarkdown, removeItem, openViewer,
         restoreFromTrash. Bulk actions, audio playback, tag chips
         and ClearAll modal stay accessible via the v0.2 fallback —
         the quietware view focuses on the core archive flow.
         ================================================================= -->
    <LumotiaPageSkeleton>
      {#snippet header()}
        <div class="flex items-center gap-4">
          <div class="flex-1 min-w-0">
            <h1 class="text-[18px] font-medium text-text leading-tight">History</h1>
            <p class="text-[12px] text-text-secondary mt-0.5">
              {viewMode === "trash" ? "Recover transcripts within 30 days." : "Search and reopen saved transcripts."}
            </p>
          </div>
          <div
            class="inline-flex items-center rounded-lg border border-border-subtle p-0.5 bg-bg-elevated text-[12px]"
            role="tablist"
            aria-label="History view"
          >
            <button
              role="tab"
              aria-selected={viewMode === "live"}
              class="px-3 py-1 rounded-md transition-colors
                     {viewMode === 'live' ? 'bg-bg-card text-text' : 'text-text-secondary hover:text-text'}"
              onclick={() => (viewMode = "live")}
            >Live <span class="text-text-tertiary font-mono">{history.length}</span></button>
            <button
              role="tab"
              aria-selected={viewMode === "trash"}
              class="px-3 py-1 rounded-md transition-colors
                     {viewMode === 'trash' ? 'bg-bg-card text-text' : 'text-text-secondary hover:text-text'}"
              onclick={() => { viewMode = "trash"; loadTrash(); }}
            >Trash <span class="text-text-tertiary font-mono">{trashItems.length}</span></button>
          </div>
        </div>
      {/snippet}

      {#snippet primary()}
        <div class="h-full flex flex-col">
          {#if trashError && viewMode === "trash"}
            <div class="px-6 pt-4">
              <LumotiaNotice tone="danger" slim dismissible onDismiss={() => trashError = ""}>
                {trashError}
              </LumotiaNotice>
            </div>
          {/if}

          <!-- Search input. No max-width: spans the same content gutter
               as the rows below so the right edge aligns with where the
               row actions column begins. Intentional, not arbitrary. -->
          <div class="px-6 pt-5 pb-3 border-b border-border-subtle">
            <div class="relative">
              <Search size={14} class="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary pointer-events-none" aria-hidden="true" />
              <input
                type="search"
                placeholder={viewMode === "trash" ? "Search trash" : "Search transcripts, #tags"}
                bind:value={searchQuery}
                class="w-full bg-bg-input border border-border-subtle rounded-lg pl-9 pr-9 py-2 text-[13px] text-text placeholder:text-text-tertiary
                       focus:outline-none focus:border-[var(--button-primary-bg)] focus:ring-[var(--wire-width-focus,2px)] focus:ring-[var(--button-primary-bg)]"
                aria-label="Search history"
              />
            </div>
          </div>

          <!-- Archive list. Reuses the existing derived `filtered` for
               live mode and `trashItems` for trash mode. Live mode
               renders date-bucketed groups (Today / Yesterday / D MMM)
               so the surface scans as an archive rather than a flat
               table; trash stays ungrouped because the relevant axis
               there is days-until-purge, not save date. -->
          <div class="flex-1 overflow-auto min-h-0">
            {#if viewMode === "live"}
              {#if filtered.length === 0}
                <div class="h-full flex flex-col items-center justify-center text-center gap-3 text-text-tertiary px-12">
                  <FileText size={48} class="opacity-50" aria-hidden="true" />
                  <p class="font-display italic text-[24px] text-text">
                    {searchQuery ? "Nothing matched." : "Nothing saved yet."}
                  </p>
                  <p class="text-[13px] text-text-secondary">
                    {searchQuery ? "Try different keywords or clear the search." : "Saved dictations and file transcripts will appear here."}
                  </p>
                </div>
              {:else}
                <ul class="pb-2">
                  {#each liveGroups as group (group.key)}
                    <li
                      class="px-6 pt-5 pb-2 text-[11px] uppercase tracking-wider text-text-tertiary font-mono"
                      aria-hidden="true"
                    >
                      {group.label}
                    </li>
                    <li>
                      <ul class="divide-y divide-border-subtle border-y border-border-subtle">
                        {#each group.items as item (item.id)}
                          {@const previewText = quietRowPreview(item)}
                          {@const sourceLabel = quietRowSourceLabel(item)}
                          <li class="group/row flex items-start gap-3 px-6 py-3 hover:bg-hover transition-colors">
                            <div class="mt-0.5 text-text-tertiary shrink-0">
                              {#if sourceLabel === "File"}
                                <FileText size={16} aria-hidden="true" />
                              {:else}
                                <Mic size={16} aria-hidden="true" />
                              {/if}
                            </div>
                            <div class="flex-1 min-w-0">
                              <p class="text-[13px] font-medium text-text truncate">
                                {item.title?.trim() || "Untitled"}
                              </p>
                              <p class="text-[12px] text-text-secondary mt-0.5 line-clamp-2">
                                {previewText || "No preview available"}
                              </p>
                              <p class="text-[11px] text-text-tertiary mt-1 font-mono">
                                {quietRowTime(item.savedAt || item.timestamp)}
                                {#if item.duration}
                                  &nbsp;·&nbsp;{formatDuration(item.duration)}
                                {/if}
                                &nbsp;·&nbsp;{sourceLabel}
                              </p>
                            </div>
                            <div class="flex items-center gap-1 shrink-0">
                              <LumotiaButton size="sm" variant="tertiary" onclick={() => openViewer(item)}>Open</LumotiaButton>
                              <LumotiaMenu
                                align="end"
                                items={[
                                  { value: "copy", label: "Copy transcript", icon: Copy, onSelect: () => copyItem(item) },
                                  { value: "export", label: "Export markdown", icon: FileDown, onSelect: () => exportMarkdown(item) },
                                  { kind: "separator" as const },
                                  { value: "delete", label: "Move to Trash", icon: Trash2, destructive: true, onSelect: () => removeItem(item) },
                                ]}
                              >
                                {#snippet trigger()}
                                  <LumotiaIconButton icon={MoreHorizontal} label="More actions" size="sm" />
                                {/snippet}
                              </LumotiaMenu>
                            </div>
                          </li>
                        {/each}
                      </ul>
                    </li>
                  {/each}
                </ul>
              {/if}
            {:else}
              <!-- Trash view -->
              {#if trashLoading && trashItems.length === 0}
                <div class="h-full flex items-center justify-center text-text-tertiary text-[13px]">Loading trash…</div>
              {:else if trashItems.length === 0}
                <div class="h-full flex flex-col items-center justify-center text-center gap-3 text-text-tertiary px-12">
                  <Clock size={48} class="opacity-50" aria-hidden="true" />
                  <p class="font-display italic text-[24px] text-text">Trash is empty.</p>
                  <p class="text-[13px] text-text-secondary">Deleted transcripts appear here for 30 days.</p>
                </div>
              {:else}
                <ul class="divide-y divide-border-subtle">
                  {#each trashItems as item (item.id)}
                    {@const previewText = quietRowPreview(item)}
                    {@const sourceLabel = quietRowSourceLabel(item)}
                    <li class="flex items-start gap-3 px-6 py-3 hover:bg-hover transition-colors">
                      <div class="mt-0.5 text-text-tertiary shrink-0">
                        {#if sourceLabel === "File"}
                          <FileText size={16} aria-hidden="true" />
                        {:else}
                          <Mic size={16} aria-hidden="true" />
                        {/if}
                      </div>
                      <div class="flex-1 min-w-0">
                        <p class="text-[13px] font-medium text-text truncate">
                          {item.title?.trim() || "Untitled"}
                        </p>
                        <p class="text-[12px] text-text-secondary mt-0.5 line-clamp-2">
                          {previewText || "No preview available"}
                        </p>
                        <p class="text-[11px] text-text-tertiary mt-1 font-mono">
                          Deleted {formatTrashTimestamp(item.deleted_at || item.createdAt)} · Auto-purges after 30 days
                        </p>
                      </div>
                      <div class="flex items-center gap-1 shrink-0">
                        <LumotiaButton
                          size="sm"
                          variant={restoringId === item.id ? "secondary" : "tertiary"}
                          disabled={restoringId === item.id}
                          onclick={() => restoreFromTrash(item.id)}
                        >{restoringId === item.id ? "Restoring…" : "Restore"}</LumotiaButton>
                      </div>
                    </li>
                  {/each}
                </ul>
              {/if}
            {/if}
          </div>
        </div>
      {/snippet}

      {#snippet metadata()}
        <div class="flex items-center justify-end">
          {history.length} saved · {trashItems.length} in trash · Local only
        </div>
      {/snippet}
    </LumotiaPageSkeleton>
  {:else}
  <!-- Header -->
  <div class="flex items-center gap-4 px-7 pt-6 pb-4">
    <h2 class="font-display text-[26px] italic text-text">History</h2>
    <span class="text-[12px] text-text-secondary mt-1">
      {viewMode === "trash" ? `${trashItems.length} in Trash` : `${history.length} saved`}
    </span>
    <div
      class="inline-flex items-center rounded-lg border border-border-subtle p-0.5 bg-bg-elevated"
      role="tablist"
      aria-label="History view"
    >
      <button
        class="text-[12px] px-2.5 py-1 rounded-md
               {viewMode === 'live' ? 'bg-bg-card text-text' : 'text-text-secondary hover:text-text'}"
        role="tab"
        aria-selected={viewMode === "live"}
        onclick={() => (viewMode = "live")}
      >Live</button>
      <button
        class="text-[12px] px-2.5 py-1 rounded-md
               {viewMode === 'trash' ? 'bg-bg-card text-text' : 'text-text-secondary hover:text-text'}"
        role="tab"
        aria-selected={viewMode === "trash"}
        onclick={() => (viewMode = "trash")}
      >Trash</button>
    </div>
    <div class="flex-1"></div>
    {#if viewMode === "live"}
      <button
        class="inline-flex items-center gap-1.5 btn-md rounded-lg
          {showStarredOnly ? 'text-accent bg-accent/10' : 'text-text-tertiary hover:text-text-secondary hover:bg-hover'}"
        onclick={() => (showStarredOnly = !showStarredOnly)}
        aria-pressed={showStarredOnly}
        title={showStarredOnly ? "Showing starred only (click to show all)" : "Show starred only"}
      >
        <Star size={14} aria-hidden="true" />
        <span class="text-[12px]">Starred</span>
      </button>
      {#if history.some((i) => !i.llmTags || i.llmTags.length === 0)}
        <button
          class="btn-md rounded-lg text-text-tertiary hover:text-text hover:bg-hover disabled:opacity-50 inline-flex items-center gap-1.5"
          onclick={tagAllUntagged}
          disabled={bulkTagging}
          title="Run AI content tagging across every untagged transcript"
        >
          <Tag size={13} aria-hidden="true" />
          <span class="text-[12px]">{bulkTagging ? `Tagging ${bulkTaggingProgress}` : "Tag all untagged"}</span>
        </button>
      {/if}
      {#if history.length > 0}
        <button
          class="btn-md rounded-lg text-text-tertiary hover:text-danger hover:bg-hover"
          onclick={openClearAllModal}
        >
          Clear All
        </button>
      {/if}
    {:else}
      <button
        class="btn-md rounded-lg text-text-tertiary hover:text-text hover:bg-hover disabled:opacity-50"
        onclick={loadTrash}
        disabled={trashLoading}
        title="Reload Trash"
      >
        <span class="text-[12px]">{trashLoading ? "Refreshing…" : "Refresh"}</span>
      </button>
    {/if}
  </div>

  <!-- Type-the-word DELETE modal for clearAll. Replaces the 4-second
       inline arm-confirm; an absent-minded double-tap on the prior
       Confirm pill wiped every transcript at once. Backed by the
       soft-delete path so a successful Clear All still leaves a 30-day
       restore window in the Trash view. -->
  {#if clearAllModalOpen}
    <!-- Backdrop. A keyboard user reaches the modal via Tab; the
         escape-to-close handler lives on the inner card. The
         non-interactive role and presentation role on the backdrop
         keep it out of the assistive-tech tree as a clickable
         element while still allowing the click-outside-to-close
         affordance for sighted users. -->
    <div
      class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in"
      role="presentation"
      onclick={closeClearAllModal}
      onkeydown={(e) => { if (e.key === 'Escape') closeClearAllModal(); }}
    >
      <div
        class="bg-bg-card border border-border-subtle rounded-xl shadow-2xl max-w-md w-[92%] p-6"
        role="dialog"
        tabindex="-1"
        aria-modal="true"
        aria-labelledby="clear-all-title"
        aria-describedby="clear-all-description"
        onclick={(e) => e.stopPropagation()}
        onkeydown={(e) => { if (e.key === 'Escape') closeClearAllModal(); }}
      >
        <h3 id="clear-all-title" class="font-display text-[18px] text-text mb-2">
          Clear all history?
        </h3>
        <p id="clear-all-description" class="text-[13px] text-text-secondary mb-4">
          This will move every transcript ({history.length}) to Trash. Items in Trash
          are kept for 30 days, then permanently deleted.
        </p>
        <label for="clear-all-confirm-input" class="block text-[12px] text-text-secondary mb-1.5">
          Type <span class="font-mono text-text">DELETE</span> to confirm.
        </label>
        <input
          id="clear-all-confirm-input"
          bind:this={clearAllInputEl}
          bind:value={clearAllConfirmInput}
          class="w-full bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px] text-text font-mono
                 placeholder:text-text-tertiary focus:outline-none focus:border-accent"
          placeholder="DELETE"
          autocomplete="off"
          spellcheck="false"
          data-no-transition
          onkeydown={(e) => {
            if (e.key === 'Enter' && clearAllConfirmValid && !clearAllInProgress) {
              clearAll();
            }
          }}
        />
        <div class="mt-5 flex items-center justify-end gap-2">
          <button
            class="btn-md rounded-lg text-text-secondary hover:text-text hover:bg-hover disabled:opacity-50"
            onclick={closeClearAllModal}
            disabled={clearAllInProgress}
          >Cancel</button>
          <button
            class="btn-md rounded-lg bg-danger/10 border border-danger/30 text-danger hover:bg-danger/20
                   disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-danger/10"
            onclick={clearAll}
            disabled={!clearAllConfirmValid || clearAllInProgress}
          >{clearAllInProgress ? "Clearing…" : "Clear all"}</button>
        </div>
      </div>
    </div>
  {/if}

  {#if viewMode === "live"}
  <!-- Search -->
  <div class="px-7 pb-3">
    <LumotiaCard tone="subtle">
      <div class="flex items-center gap-3 px-4 py-2.5">
        <Search size={16} class="text-text-tertiary flex-shrink-0" aria-hidden="true" />
        <input
          class="flex-1 bg-transparent text-text text-[13px] placeholder:text-text-tertiary"
          placeholder="Search all transcripts... (try tag:meetings)"
          bind:value={searchQuery}
          data-no-transition
        />
        {#if searchQuery}
          <span class="text-[12px] text-text-secondary mr-2">{filtered.length} results</span>
          <button
            class="text-[12px] text-text-secondary hover:text-text"
            onclick={() => searchQuery = ""}
          >Clear</button>
        {/if}
      </div>
    </LumotiaCard>
  </div>

  <!-- Tag chip filter bar -->
  {#if allTags.length > 0}
    <div class="px-7 pb-3 flex items-center gap-1.5 flex-wrap">
      <span class="text-[12px] uppercase tracking-wider text-text-secondary mr-1">Tags</span>
      {#if activeTagFilter}
        <button
          class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-accent text-[12px] text-accent bg-accent/10"
          onclick={() => (activeTagFilter = null)}
          aria-pressed="true"
          title="Clear tag filter"
        >
          {activeTagFilter}
          <span class="text-[12px] leading-none">×</span>
        </button>
      {:else}
        {#each allTags.slice(0, 7) as t (t.tag)}
          <button
            class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-border-subtle text-[12px] text-text-secondary hover:border-accent hover:text-accent"
            onclick={() => (activeTagFilter = t.tag)}
            aria-pressed={activeTagFilter === t.tag}
          >
            {t.tag}
            <span class="text-[12px] text-text-secondary">{t.count}</span>
          </button>
        {/each}
      {/if}
    </div>
  {/if}

  <!-- Phase 9 bulk-action toolbar. Surfaces only when selection is
       non-empty, so the unselected default state is unchanged. -->
  {#if selected.size > 0}
    <div
      class="mx-7 mb-3 flex items-center gap-3 px-4 py-2 rounded-lg border border-border-subtle bg-bg-elevated text-[12px]"
      role="toolbar"
      aria-label="Bulk actions for selected transcripts"
    >
      <span class="text-text-tertiary">{selected.size} selected</span>
      <button
        class="text-text hover:underline"
        onclick={selectAllVisible}
      >Select all</button>
      <button
        class="text-text-tertiary hover:text-text"
        onclick={clearSelection}
      >Clear</button>
      <div class="ml-auto flex items-center gap-4">
        <button
          class="text-text hover:underline"
          onclick={bulkExport}
        >Export selected</button>
        {#if bulkDeleteArmed}
          <span class="text-text-secondary">Delete {selected.size} {selected.size === 1 ? 'transcript' : 'transcripts'}?</span>
          <button
            class="text-danger font-medium hover:underline"
            onclick={bulkDelete}
          >Confirm</button>
          <button
            class="text-text-tertiary hover:text-text hover:underline"
            onclick={disarmBulkDelete}
          >Cancel</button>
        {:else}
          <button
            class="text-danger hover:underline"
            onclick={armBulkDelete}
          >Delete selected</button>
        {/if}
      </div>
    </div>
  {/if}

  <!-- History list -->
  <div class="flex-1 px-7 pb-4 min-h-0">
    <LumotiaCard classes="h-full flex flex-col overflow-hidden">
      {#if filtered.length === 0}
        <LumotiaEmptyState
          icon={Clock}
          message={searchQuery ? "No matching transcripts" : "Your transcriptions will be saved here"}
        />
      {:else}
        <div bind:this={listEl} class="flex-1 overflow-y-auto" onscroll={onListScroll}>
          <div class="relative" style="height: {totalHeight}px">
            {#each visibleItems as { item, top, height, preview } (item.id)}
              <div class="absolute left-0 right-0" style="top: {top}px; min-height: {height}px">
                <!-- Compact row -->
                <div
                  class="flex items-center gap-3 px-4 py-3 border-b border-border-subtle bg-bg-card hover:bg-hover cursor-pointer {selected.has(item.id) ? 'bg-accent/5' : ''}"
                  style="transition-duration: var(--duration-ui)"
                  onclick={() => toggleExpand(item.id)}
                  onkeydown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleExpand(item.id); } }}
                  role="button"
                  tabindex="0"
                  aria-expanded={expandedId === item.id}
                >
                  <!-- Phase 9 bulk-select checkbox. Stays unobtrusive
                       until checked or hovered; e.stopPropagation keeps
                       the click off the row-expand toggle. -->
                  <input
                    type="checkbox"
                    class="w-3.5 h-3.5 flex-shrink-0 accent-accent cursor-pointer opacity-70 hover:opacity-100 {selected.has(item.id) ? 'opacity-100' : ''}"
                    aria-label={`Select transcript ${item.title || item.id}`}
                    checked={selected.has(item.id)}
                    onclick={(e) => { e.stopPropagation(); toggleSelected(item.id); }}
                    onkeydown={(e) => { if (e.key === " " || e.key === "Enter") e.stopPropagation(); }}
                  />

                  <!-- Play button (if audio exists) -->
                  {#if item.audioPath}
                    <button
                      class="w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0
                        {playingId === item.id ? 'bg-accent/20 text-accent' : 'bg-accent/10 text-accent hover:bg-accent/20'}"
                      onclick={(e) => { e.stopPropagation(); togglePlay(item); }}
                      aria-label={playingId === item.id && audioEl && !audioEl.paused ? "Pause" : "Play"}
                    >
                      {#if playingId === item.id && audioEl && !audioEl.paused}
                        <Pause size={10} aria-hidden="true" />
                      {:else}
                        <Play size={10} class="ml-0.5" aria-hidden="true" />
                      {/if}
                    </button>
                  {:else}
                    <div class="w-6 h-6 flex-shrink-0"></div>
                  {/if}

                  <!-- Title / preview text (truncated) -->
                  <div class="flex-1 min-w-0">
                    <p
                      class="text-[13px] text-text whitespace-pre-wrap break-words"
                      style="line-height: {historyLineHeight}px"
                      title={preview?.fullText || ""}
                    >
                      {preview?.text || compactPreviewText(item)}
                    </p>
                  </div>

                  <!-- Duration -->
                  {#if item.duration}
                    <span class="text-[12px] text-text-secondary flex-shrink-0 tabular-nums">
                      {formatDuration(item.duration)}
                    </span>
                  {/if}

                  <!-- Source icon -->
                  <span class="flex-shrink-0 text-text-tertiary" title={item.source}>
                    {#if item.source && item.source.toLowerCase().includes("file")}
                      <FileText size={14} aria-hidden="true" />
                    {:else}
                      <Mic size={14} aria-hidden="true" />
                    {/if}
                  </span>

                  <!-- Date (right-aligned) -->
                  <span class="text-[12px] text-text-secondary flex-shrink-0 text-right min-w-[90px]">
                    {item.date}
                  </span>

                  <!-- Expand chevron -->
                  <ChevronDown
                    size={14}
                    class="text-text-tertiary flex-shrink-0 {expandedId === item.id ? 'rotate-180' : ''}"
                    style="transition: transform var(--duration-ui)"
                    aria-hidden="true"
                  />
                </div>

                <!-- Expanded detail -->
                {#if expandedId === item.id}
                  <div class="px-4 py-4 bg-bg-elevated border-b border-border-subtle">
                    <!-- Audio player (if playing this item) -->
                    {#if item.audioPath && playingId === item.id}
                      <div class="flex items-center gap-3 mb-4 px-1">
                        <span class="text-[12px] text-text-secondary tabular-nums w-[80px]">
                          {formatTime(currentTime)} / {formatTime(duration)}
                        </span>
                        <input
                          type="range"
                          min="0"
                          max={duration || 0}
                          step="0.1"
                          value={currentTime}
                          oninput={seekTo}
                          class="flex-1 accent-accent h-1"
                          data-no-transition
                          onclick={(e) => e.stopPropagation()}
                        />
                        <div class="flex gap-0.5">
                          {#each PLAYBACK_SPEEDS as speed}
                            <button
                              class="text-[12px] px-1.5 py-0.5 rounded-full
                                {playbackRate === speed
                                  ? 'bg-accent/15 text-accent font-medium'
                                  : 'text-text-tertiary hover:text-text-secondary'}"
                              onclick={(e) => { e.stopPropagation(); setSpeed(speed); }}
                            >{speed}x</button>
                          {/each}
                        </div>
                      </div>
                    {/if}

                    <!-- Inline title input -->
                    <input
                      type="text"
                      class="w-full bg-bg-input border border-border rounded-lg px-3 py-2 text-[12px]
                             text-text placeholder:text-text-tertiary focus:border-accent mb-3"
                      placeholder="Name this transcript..."
                      value={item.title || ""}
                      oninput={(e) => { item.title = e.target.value; }}
                      onblur={() => renameHistoryEntry(item.id, { title: (item.title || "").trim() }).catch(() => {})}
                      onclick={(e) => e.stopPropagation()}
                      data-no-transition
                    />

                    <!-- Tags: auto + manual -->
                    <div class="flex items-center gap-1.5 flex-wrap mb-3" onclick={(e) => e.stopPropagation()} role="presentation">
                      {#each deriveAutoTags(item) as t (t)}
                        <span
                          class="inline-flex items-center px-2 py-0.5 rounded-full text-[12px] bg-bg-input text-text-secondary border border-border-subtle"
                          title="Auto-generated tag"
                        >{t}</span>
                      {/each}
                      {#each (item.manualTags || []) as t (t)}
                        <span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[12px] bg-accent/10 text-accent border border-accent/30">
                          {t}
                          <button
                            class="text-[12px] leading-none hover:text-danger"
                            onclick={() => removeManualTag(item, t)}
                            title="Remove tag"
                            aria-label="Remove tag {t}"
                          >×</button>
                        </span>
                      {/each}
                      <!-- Phase 9 LLM-generated tags. Dashed border + italic
                           distinguishes from manual chips. Click to promote
                           into manualTags. -->
                      {#each (item.llmTags || []) as t (t)}
                        <button
                          class="inline-flex items-center px-2 py-0.5 rounded-full text-[12px] italic border border-dashed border-border-subtle text-text-secondary hover:text-text hover:border-accent"
                          onclick={() => promoteLlmTag(item, t)}
                          title="Click to keep as a manual tag"
                          aria-label={`Promote ${t} to manual tag`}
                        >{t}</button>
                      {/each}
                      <input
                        type="text"
                        class="bg-bg-input border border-border rounded-full px-2 py-0.5 text-[12px]
                               text-text placeholder:text-text-tertiary focus:border-accent w-[110px]"
                        placeholder="+ add tag"
                        onkeydown={(e) => handleAddTagKey(e, item)}
                        data-no-transition
                      />
                    </div>

                    <!-- Full transcript -->
                    <p class="text-[13px] text-text whitespace-pre-wrap mb-4" style="line-height: {historyLineHeight}px">{item.text}</p>

                    <!-- Action buttons -->
                    <div class="flex items-center gap-2 flex-wrap">
                      <button
                        class="text-[12px] px-3 py-1.5 rounded-lg bg-hover text-text-secondary hover:text-text"
                        style="transition-duration: var(--duration-ui)"
                        onclick={(e) => { e.stopPropagation(); copyItem(item); }}
                      >Copy</button>
                      <button
                        class="inline-flex items-center gap-1.5 text-[12px] px-3 py-1.5 rounded-lg bg-hover text-text-secondary hover:text-text"
                        style="transition-duration: var(--duration-ui)"
                        onclick={(e) => { e.stopPropagation(); openEditor(item); }}
                        title="Open transcript in a popout editor"
                      >
                        Edit
                        <ExternalLink size={11} aria-hidden="true" />
                      </button>
                      <button
                        class="text-[12px] px-3 py-1.5 rounded-lg bg-hover text-text-secondary hover:text-text"
                        style="transition-duration: var(--duration-ui)"
                        onclick={(e) => { e.stopPropagation(); exportMarkdown(item); }}
                        title="Export this transcript as a Markdown file with YAML frontmatter"
                      >Export .md</button>
                      <button
                        class="inline-flex items-center gap-1.5 text-[12px] px-3 py-1.5 rounded-lg bg-hover text-text-secondary hover:text-text disabled:opacity-50"
                        style="transition-duration: var(--duration-ui)"
                        onclick={(e) => { e.stopPropagation(); tagRow(item); }}
                        disabled={tagging.has(item.id)}
                        title="Generate AI content tags (topic and intent)"
                        aria-label="Generate content tags with AI"
                      >
                        <Tag size={11} aria-hidden="true" />
                        {tagging.has(item.id) ? "Tagging…" : "Tag"}
                      </button>
                      {#if item.audioPath && item.segments && item.segments.length > 0}
                        <button
                          class="text-[12px] px-3 py-1.5 rounded-lg bg-hover text-accent hover:text-accent-hover"
                          style="transition-duration: var(--duration-ui)"
                          onclick={(e) => { e.stopPropagation(); openViewer(item); }}
                        >Open viewer</button>
                      {/if}
                      <div class="flex-1"></div>
                      <button
                        class="text-[12px] px-3 py-1.5 rounded-lg bg-hover text-text-secondary hover:text-danger"
                        style="transition-duration: var(--duration-ui)"
                        onclick={(e) => { e.stopPropagation(); removeItem(item); }}
                      >Delete</button>
                    </div>
                  </div>
                {/if}
              </div>
            {/each}
          </div>
        </div>
      {/if}
    </LumotiaCard>
  </div>
  {:else}
  <!-- Trash view. Lists soft-deleted transcripts (deleted_at IS NOT
       NULL) with a Restore button per row. Permanently-delete-from-
       trash is intentionally deferred — the 30-day startup purge
       handles hard-removal until the UI lands. Audio files may
       already have been removed by delete_transcript's best-effort
       filesystem cleanup, so restored text + metadata may surface
       without playable audio. -->
  <div class="px-7 pb-3">
    <div class="text-[12px] text-text-secondary italic">
      Trashed items are kept for 30 days, then permanently deleted on next app start.
    </div>
  </div>
  <div class="flex-1 px-7 pb-4 min-h-0">
    <LumotiaCard classes="h-full flex flex-col overflow-hidden">
      {#if trashLoading && trashItems.length === 0}
        <LumotiaEmptyState
          icon={Clock}
          message="Loading Trash…"
        />
      {:else if trashError && trashItems.length === 0}
        <LumotiaEmptyState
          icon={Clock}
          message={`Failed to load Trash: ${trashError}`}
        />
      {:else if trashItems.length === 0}
        <LumotiaEmptyState
          icon={Clock}
          message="Trash is empty"
        />
      {:else}
        <div class="flex-1 overflow-y-auto">
          {#each trashItems as item (item.id)}
            <div class="flex items-center gap-3 px-4 py-3 border-b border-border-subtle bg-bg-card">
              <div class="flex-1 min-w-0">
                <div class="text-[13px] text-text truncate">
                  {item.title || (item.text ? item.text.slice(0, 80) : "Untitled")}
                </div>
                <div class="text-[12px] text-text-tertiary mt-0.5">
                  Created {formatTrashTimestamp(item.createdAt)}
                </div>
              </div>
              <button
                class="text-[12px] px-3 py-1.5 rounded-lg bg-accent/10 border border-accent/30 text-accent
                       hover:bg-accent/20 disabled:opacity-50 disabled:cursor-not-allowed"
                onclick={() => restoreFromTrash(item.id)}
                disabled={restoringId === item.id}
                title="Restore this transcript to the Live list"
              >{restoringId === item.id ? "Restoring…" : "Restore"}</button>
            </div>
          {/each}
        </div>
      {/if}
    </LumotiaCard>
  </div>
  {/if}
  {/if}
</div>
