/* Ported from apps/web/src/features/home/ — shared Home-table chrome, selection
 * machinery, signal identity/pills, list mapping, save/filter list controls,
 * deep links, and the external-directory seam. Sources noted per section. */
(() => {
  const T = window.T;
  const { useState, useEffect, useLayoutEffect, useMemo, useRef } = React;
  const cn = T.cn;
  const {
    Button,
    Input,
    searchFieldClassName,
    Checkbox,
    Popover,
    PopoverTrigger,
    PopoverContent,
    SearchableFilterPopover,
    CommandGroup,
    CommandItem,
    LogoAvatar,
    getInitials,
    PersonAvatar,
    SIGNAL_TYPES,
    toSignalType,
  } = T.UI;
  const {
    Zap,
    Building2,
    Users,
    Loader2,
    Search,
    X,
    ChevronDown,
    Bookmark,
    BookmarkFilled,
    BookmarkPlus,
    Check,
    Pencil,
    Sparkles,
    UserPlus,
  } = T.Icons;
  const { useNavigate, useSearchParams } = T.Router;
  const toast = T.toast;
  const homeAnalytics = T.homeAnalytics;

  /* ================= compact-count.ts ================= */

  /**
   * Phone-tier count format for the Home tab strip: 2,700 → 2.7k, 1,000 → 1k,
   * 4,900 → 4.9k.
   *
   * Under 1,000 the value is returned unchanged, so the everyday case (21, 16,
   * 22) reads exactly as it does on desktop and only genuinely long counts
   * change shape.
   *
   * Intl's DEFAULT compact rounding is deliberate — it gives two significant
   * digits below 10k and drops the decimal above it (12,300 → 12k, not 12.3k).
   * Lowercased because Intl emits "K"/"M" and a capital reads as another word
   * beside a sentence-case label rather than as a unit.
   */
  function compactCount(count) {
    return new Intl.NumberFormat('en-US', { notation: 'compact' })
      .format(count)
      .toLowerCase();
  }

  /* ================= HomeTabs.tsx ================= */

  const TABS = [
    { id: 'signals', label: 'Signals', icon: Zap },
    { id: 'accounts', label: 'Accounts', icon: Building2 },
    { id: 'people', label: 'People', icon: Users },
  ];

  /** Folder-style tabs that seat on the top edge of the content card (mock
   *  `.p4-tabs.v-folder`): the active tab is white with a top accent bar and
   *  connects to the card; the rest recede into the cream shell. Metrics live in
   *  home-tables.css. Bespoke chrome, so the tab buttons take a scoped
   *  eslint-disable rather than the pill-shaped shared <Button>. */
  function HomeTabs({ value, onChange, counts }) {
    return (
      <div className="htbl-tabs" role="tablist" aria-label="Home sources">
        {TABS.map(({ id, label, icon: Icon }) => {
          const count = counts[id];
          const on = value === id;
          return (
            // bespoke folder tab (top-only radius, seated on the card edge, top accent bar)
            <button
              key={id}
              type="button"
              role="tab"
              aria-selected={on}
              className={`htbl-tab${on ? ' is-on' : ''}`}
              onClick={() => onChange(id)}
            >
              <Icon />
              {label}
              {count != null && (
                // Both forms are rendered and one is hidden, rather than reading
                // the viewport in JS: a matchMedia hook would re-render the whole
                // strip on resize and flash the wrong form on first paint, and
                // every other phone rule on this surface is already CSS.
                <span className="htbl-tab-count">
                  <span className="max-[480px]:hidden">{count}</span>
                  <span className="hidden max-[480px]:inline">
                    {compactCount(count)}
                  </span>
                </span>
              )}
            </button>
          );
        })}
      </div>
    );
  }

  /* ================= HomeTableSearch.tsx ================= */

  /** Toolbar search for Home's Accounts / People / Signals tables. */
  function HomeTableSearch({
    value,
    onChange,
    placeholder,
    searching = false,
    trailing,
  }) {
    const wrapRef = useRef(null);

    useEffect(() => {
      wrapRef.current?.querySelector('input')?.focus();
    }, []);

    const showClear = value.length > 0 && !searching;
    const hasTrail = Boolean(trailing) || searching || showClear;

    const focusInput = () => {
      wrapRef.current?.querySelector('input')?.focus();
    };

    return (
      <div ref={wrapRef} className="htbl-search">
        <Input
          value={value}
          onChange={(e) => onChange(e.target.value)}
          icon={<Search />}
          placeholder={placeholder}
          className={cn(
            searchFieldClassName,
            'w-full',
            hasTrail && 'htbl-search-pad',
            showClear && 'htbl-search-pad-clear',
          )}
        />
        {(trailing || searching || showClear) && (
          <div className="htbl-search-trail">
            {trailing}
            {searching && (
              <Loader2
                className="size-3.5 shrink-0 animate-spin text-text-muted"
                aria-hidden
              />
            )}
            {showClear && (
              <Button
                type="button"
                variant="tertiary"
                size="icon-sm"
                aria-label="Clear search"
                className="size-6 shrink-0 border-transparent bg-transparent shadow-none hover:bg-surface-well"
                onClick={() => {
                  onChange('');
                  focusInput();
                }}
              >
                <X />
              </Button>
            )}
          </div>
        )}
      </div>
    );
  }

  /* ================= HomeSelectCell.tsx ================= */

  /**
   * The select column on the Home tables (TRA-1429). The 16px checkbox alone was
   * too small a target, so the whole cell is the hit area and `home-tables.css`
   * restates `cursor: pointer` there. The three tabs each hand-rolled this cell;
   * this is the single place the click semantics live.
   */
  function HomeSelectCell({
    checked,
    disabled = false,
    busy = false,
    ariaLabel,
    onToggle,
  }) {
    const onCellClick = (e) => {
      // Load-bearing: the row's onClick opens the drawer. Selecting never opens.
      e.stopPropagation();
      if (disabled || busy) return;
      // The Checkbox is a <button>: a click on it already fired onCheckedChange
      // and then bubbles here. Toggling again would undo it — and on the
      // directory rows, where onToggle CREATES a record, it silently created two.
      if (e.target.closest('[data-slot="checkbox"]')) return;
      onToggle();
    };

    return (
      <td
        className="htbl-cell-select"
        data-testid="home-select-cell"
        onClick={onCellClick}
      >
        {busy ? (
          <span
            className="htbl-ext-pending"
            data-testid="home-external-pending"
            // The checkbox is unmounted while this runs, so without a role and a
            // name nothing announces that the add is in progress.
            role="status"
            aria-live="polite"
            aria-label={ariaLabel}
            aria-busy="true"
          >
            <Loader2 className="size-4 animate-spin" />
          </span>
        ) : (
          <Checkbox
            checked={checked}
            disabled={disabled}
            aria-label={ariaLabel}
            // Drop the CheckedState arg — every caller's handler derives the
            // next state from its own audience data, not from the event.
            onCheckedChange={() => onToggle()}
          />
        )}
      </td>
    );
  }

  /* ================= HomeTableSortTh.tsx ================= */

  /**
   * A sortable column header for the Home tables. The active column shows a
   * single chevron that flips with the direction; inactive columns show
   * nothing, so a row of headers stays quiet until the user actually sorts.
   */
  function HomeTableSortTh({ label, field, sortField, sortDir, onSort, className }) {
    const active = sortField === field;
    return (
      <th className={cn('htbl-sortable', className)} onClick={() => onSort(field)}>
        <span className="inline-flex items-center gap-1">
          {label}
          {active && (
            <ChevronDown
              className={cn('size-3', sortDir === 'asc' && 'rotate-180')}
            />
          )}
        </span>
      </th>
    );
  }

  /**
   * Next direction for a sort click: flip when the column is already active,
   * otherwise open on the direction that column reads best in. Text columns
   * open A→Z; counts, scores and dates open with the biggest or most recent
   * first, which is what someone clicking them is looking for.
   */
  function nextSortDir({ field, sortField, sortDir, ascFirst }) {
    if (sortField === field) return sortDir === 'asc' ? 'desc' : 'asc';
    return ascFirst.includes(field) ? 'asc' : 'desc';
  }

  /* ================= HomeTableResultCount.tsx ================= */

  /** Soft chip for partial Home search results — `3 of 209`. Hidden when unfiltered. */
  function HomeTableResultCount({
    count,
    baseline,
    className,
    'data-testid': testId = 'home-table-result-count',
  }) {
    if (count === undefined || baseline === undefined || baseline <= count) {
      return null;
    }

    const label = `${count.toLocaleString()} of ${baseline.toLocaleString()}`;

    return (
      <span
        data-testid={testId}
        aria-live="polite"
        aria-label={label}
        title={label}
        className={cn('htbl-result-count', className)}
      >
        {label}
      </span>
    );
  }

  /* ================= home-table-selection.ts ================= */

  const DEFAULT_BULK_PAGE_SIZE = 100;

  /** Pace server-side audience growth in deliberate, reviewable batches. */
  const HOME_TABLE_BULK_SELECTION_LIMIT = 100;
  /**
   * Bulk enumeration is a read-only snapshot used to build audience membership.
   * It must not normalize its pages into the live cache (kept for call-site
   * fidelity; the mock data layer has no normalized cache).
   */
  const HOME_TABLE_BULK_FETCH_POLICY = 'no-cache';

  function homeTableBulkSelectionState(selection, scopeKey, contextKey) {
    if (!selection) return 'none';
    if (selection.scopeKey === scopeKey) return 'active';
    return selection.contextKey === contextKey ? 'parked' : 'stale';
  }

  function homeTableLoadedSelectionState(
    selectionScopeKey,
    currentScopeKey,
    hasTemporaryFilter,
  ) {
    if (!selectionScopeKey) return 'none';
    // The unfiltered view contains every row from an account-filtered selection,
    // so loaded selections can safely expand after returning to that broad view.
    if (selectionScopeKey === currentScopeKey || !hasTemporaryFilter) {
      return 'active';
    }
    return 'parked';
  }

  /**
   * Keep the scope row visible while any part of its original selection remains.
   * Without the explicit loaded-selection count, unchecking one row makes
   * `allLoadedSelected` false and incorrectly hides the controls.
   */
  function shouldShowHomeTableSelectionScope({
    selectedBulkMatchingCount,
    selectedLoadedSelectionCount,
    allLoadedSelected,
    totalCount,
    loadedCount,
  }) {
    return (
      (selectedBulkMatchingCount !== null && selectedBulkMatchingCount > 0) ||
      (selectedLoadedSelectionCount !== null &&
        selectedLoadedSelectionCount > 0) ||
      (allLoadedSelected && totalCount > loadedCount)
    );
  }

  function clearHomeTableSelection({
    focusedListId,
    keepFocusedList = false,
    clearFocusedList,
    clearBulkSelection,
    clearAudience,
  }) {
    // A focused-list effect can rehydrate a cleared audience. Unfocus first,
    // then clear the session-only bulk scope and finally the audience itself.
    if (focusedListId && !keepFocusedList) clearFocusedList();
    clearBulkSelection();
    clearAudience();
  }

  function throwIfAborted(signal) {
    if (!signal?.aborted) return;
    const error = new Error('Selection cancelled');
    error.name = 'AbortError';
    throw error;
  }

  /**
   * Fetch a safely capped server-side result without turning one selection into
   * one enormous response. The total is a snapshot hint: a short page ends the
   * walk early when rows disappear while the selection is being built. Rejected
   * rows do not consume the limit, so Signals can skip accountless rows while
   * still selecting up to the advertised cap.
   */
  async function fetchAllMatchingRows({
    totalCount,
    fetchPage,
    getRowId,
    startAt = 0,
    pageSize = DEFAULT_BULK_PAGE_SIZE,
    maxRows = HOME_TABLE_BULK_SELECTION_LIMIT,
    excludeIds = [],
    isSelectable = () => true,
    signal,
    onProgress,
  }) {
    const initialOffset = Math.max(0, Math.min(startAt, totalCount));
    if (totalCount <= 0 || maxRows <= 0 || initialOffset >= totalCount) {
      return {
        rows: [],
        scannedCount: 0,
        nextOffset: initialOffset,
        outcome: 'all',
      };
    }

    const rows = [];
    const previouslySelected = new Set(excludeIds);
    const seen = new Set(previouslySelected);
    let scannedCount = 0;
    let uncoveredCount = 0;
    let exhausted = false;
    let nextOffset = initialOffset;
    while (nextOffset < totalCount && rows.length < maxRows) {
      throwIfAborted(signal);
      const take = Math.min(pageSize, totalCount - nextOffset);
      const page = await fetchPage(nextOffset, take, signal);
      throwIfAborted(signal);
      let consumedFromPage = 0;
      for (const row of page) {
        consumedFromPage += 1;
        scannedCount += 1;
        nextOffset += 1;
        const id = getRowId(row);
        if (seen.has(id)) {
          if (!previouslySelected.has(id)) uncoveredCount += 1;
          continue;
        }
        seen.add(id);
        if (!isSelectable(row)) {
          uncoveredCount += 1;
          continue;
        }
        rows.push(row);
        if (rows.length === maxRows) break;
      }
      onProgress?.(rows.length);
      const consumedWholePage = consumedFromPage === page.length;
      if ((consumedWholePage && page.length < take) || nextOffset >= totalCount) {
        exhausted = true;
        break;
      }
    }

    const outcome =
      exhausted && nextOffset >= totalCount && uncoveredCount === 0
        ? 'all'
        : !exhausted
          ? 'capped'
          : 'partial';
    return { rows, scannedCount, nextOffset, outcome };
  }

  function hasAudienceSelection(audience) {
    return (
      Object.keys(audience.accounts).length > 0 ||
      Object.keys(audience.people).length > 0 ||
      Object.keys(audience.fresh).length > 0 ||
      Object.keys(audience.selectedEventIds ?? {}).length > 0
    );
  }

  function countSelectedMatchingIds(matchingIds, isSelected) {
    if (matchingIds === null) return null;
    return matchingIds.reduce((count, id) => count + (isSelected(id) ? 1 : 0), 0);
  }

  function accountsToAudienceBatch(accounts, per) {
    const batchAccounts = {};
    const meta = {};

    for (const account of accounts) {
      batchAccounts[account.id] = {
        via: 'accounts-table',
        per,
        seq: 0,
      };
      meta[account.id] = {
        name: account.name,
        url: account.url ?? null,
        logoUrl: account.logoUrl ?? null,
      };
    }

    return { accounts: batchAccounts, meta };
  }

  function peopleToAudienceBatch(people) {
    const batchPeople = {};
    const peopleMeta = {};
    const meta = {};

    for (const person of people) {
      const accountId = person.accountId ?? person.account?.id;
      if (!accountId) continue;
      batchPeople[person.id] = {
        via: 'people-table',
        accountId,
        seq: 0,
      };
      peopleMeta[person.id] = {
        fullName: person.fullName,
        title: person.title ?? null,
        profileImageUrl: person.profileImageUrl ?? null,
      };
      if (person.account) {
        meta[accountId] = {
          name: person.account.name,
          url: person.account.url ?? null,
          logoUrl: person.account.logoUrl ?? null,
        };
      }
    }

    return { people: batchPeople, peopleMeta, meta };
  }

  /* ================= HomeTableSelection.tsx ================= */

  function HomeTableSelectAllCheckbox({
    checked,
    busy,
    disabled,
    entityName,
    entityPlural = `${entityName}s`,
    loadedCount,
    onToggle,
  }) {
    const clearing = checked !== false;
    const actionLabel = clearing
      ? 'Clear selection'
      : `Select all ${loadedCount.toLocaleString()} loaded ${loadedCount === 1 ? entityName : entityPlural}`;
    return (
      <Checkbox
        checked={checked}
        disabled={busy || disabled}
        aria-label={actionLabel}
        title={actionLabel}
        onCheckedChange={onToggle}
      />
    );
  }

  function HomeTableSelectionScopeRow({
    colSpan,
    selectedCount,
    totalCount,
    remainingCount,
    entityName,
    entityPlural = `${entityName}s`,
    bulkOutcome,
    bulkSelectionComplete,
    loadedSelectionComplete = true,
    busy,
    progressCount,
    allowServerSelection = true,
    onSelectAll,
    onClear,
  }) {
    const totalLabel = totalCount === 1 ? entityName : entityPlural;
    const selectedLabel = selectedCount === 1 ? entityName : entityPlural;
    const serverRemaining = Math.max(
      0,
      remainingCount ?? totalCount - selectedCount
    );
    const batchCount = Math.min(HOME_TABLE_BULK_SELECTION_LIMIT, serverRemaining);
    const continuingBulkSelection = bulkOutcome === 'capped';
    const canSelectMatching =
      allowServerSelection &&
      serverRemaining > 0 &&
      (bulkOutcome === null || continuingBulkSelection);
    const selectLabel = continuingBulkSelection
      ? `Select next ${batchCount.toLocaleString()}`
      : `Select ${batchCount.toLocaleString()} more`;
    const selectedCountLabel = selectedCount.toLocaleString();
    const totalCountLabel = totalCount.toLocaleString();
    const statusEmphasis =
      bulkSelectionComplete && bulkOutcome === 'all'
        ? `All ${totalCountLabel}`
        : bulkOutcome === 'capped'
          ? `${selectedCountLabel} of ${totalCountLabel}`
          : null;
    const statusDetail = statusEmphasis ? `${totalLabel} selected` : null;
    const status =
      busy && progressCount !== undefined
        ? `Selecting… ${progressCount.toLocaleString()} selected`
        : bulkSelectionComplete && bulkOutcome === 'all'
          ? `${statusEmphasis} ${statusDetail}`
          : bulkOutcome === 'capped'
            ? `${statusEmphasis} ${statusDetail}`
            : bulkSelectionComplete && bulkOutcome === 'partial'
              ? `${selectedCountLabel} selectable ${selectedLabel} selected`
              : !loadedSelectionComplete
                ? `${selectedCountLabel} ${selectedLabel} selected`
                : `All ${selectedCountLabel} loaded ${selectedLabel} ${selectedCount === 1 ? 'is' : 'are'} selected`;
    return (
      <tr className="htbl-selection-scope" aria-busy={busy}>
        <td colSpan={colSpan}>
          <span
            role="status"
            aria-live="polite"
            className="htbl-selection-status"
          >
            {statusEmphasis && statusDetail && !busy ? (
              <>
                <span className="htbl-selection-status-count">
                  {bulkOutcome === 'capped' ? (
                    <>
                      {selectedCountLabel}{' '}
                      <span className="htbl-selection-status-separator">of</span>{' '}
                      {totalCountLabel}
                    </>
                  ) : (
                    statusEmphasis
                  )}
                </span>{' '}
                <span className="htbl-selection-status-detail">
                  {statusDetail}
                </span>
              </>
            ) : (
              status
            )}
          </span>
          {canSelectMatching && (
            <Button
              type="button"
              variant="secondary"
              size="xs"
              loading={busy}
              onClick={onSelectAll}
              aria-label={`${selectLabel} ${totalLabel}`}
            >
              {selectLabel}
            </Button>
          )}
          <Button
            type="button"
            variant="tertiary"
            size="xs"
            disabled={busy}
            onClick={onClear}
          >
            Clear
          </Button>
        </td>
      </tr>
    );
  }

  function HomeTableFilteredSelectionNoticeRow({ colSpan, viewLabel, onClear }) {
    return (
      <tr className="htbl-selection-scope">
        <td colSpan={colSpan}>
          <span role="status">Selection kept · {viewLabel}</span>
          <Button type="button" variant="tertiary" size="xs" onClick={onClear}>
            Clear selection
          </Button>
        </td>
      </tr>
    );
  }

  /* ================= email-status.ts ================= */

  /** Product rule (TRA-1298): only surface a "finding email" indicator while an
   *  enrichment run is genuinely in flight (`enrichmentState === 'in_progress'`).
   *  Every other emailless state is a quiet neutral "no email yet", never a fake
   *  spinner. */
  function emailStatus(email, enrichmentState) {
    if (email) return 'ready';
    if (enrichmentState === 'in_progress') return 'pending';
    return 'none';
  }

  /* ================= use-search-param.ts ================= */

  /** Query-string key each Home tab seeds its search box from. */
  const HOME_SEARCH_PARAM = 'q';

  /**
   * Search state for a Home table, seeded once from `?q=`.
   *
   * The tabs own their search locally; this only lets one surface hand a query
   * to another. Seeding happens in the state initializer, so the table's FIRST
   * fetch already carries the query. The param is then dropped from the URL, so
   * what the user types next is never contradicted by a stale address bar.
   */
  function useSeededSearch() {
    const [params, setParams] = useSearchParams();
    const [query, setQuery] = useState(
      () => params.get(HOME_SEARCH_PARAM) ?? '',
    );

    useEffect(() => {
      if (!params.has(HOME_SEARCH_PARAM)) return;
      const next = new URLSearchParams(params);
      next.delete(HOME_SEARCH_PARAM);
      setParams(next, { replace: true });
    }, [params, setParams]);

    return [query, setQuery];
  }

  /* ================= useWorkingSetPin.ts ================= */

  const EMPTY_NON_PINNING_IDS = [];

  /**
   * The "working set pinned to the top" mechanism, shared by the Home People
   * and Accounts tabs. Three rules: SNAPSHOT (selected ids frozen at
   * view-entry / bump), LIVE ARRIVALS (selected ids that appeared after the
   * snapshot and aren't paged rows stream into the pin), NON-PINNING MEMBERS
   * (bulk-selected ids never promote above the page being viewed).
   */
  function useWorkingSetPin({
    audienceIds,
    loadedIds,
    viewKey,
    resetKey = 0,
    disabled = false,
    nonPinningIds = EMPTY_NON_PINNING_IDS,
  }) {
    const [version, setVersion] = useState(0);
    const bump = useMemo(() => () => setVersion((v) => v + 1), []);
    const nonPinning = useMemo(() => new Set(nonPinningIds), [nonPinningIds]);

    // Frozen working set: recomputes only on viewKey / version / reset change.
    // It is monotonic within that epoch: live arrivals may be added, but a later
    // deselection or bulk classification cannot remove an already-painted row.
    const snapshotKey = `${viewKey}|${version}|${resetKey}|${disabled ? 1 : 0}`;
    const workingSet = useMemo(
      () =>
        new Set(disabled ? [] : audienceIds.filter((id) => !nonPinning.has(id))),
      // eslint-disable-next-line react-hooks/exhaustive-deps -- frozen per view/bump/reset
      [snapshotKey]
    );

    if (!disabled) {
      for (const id of audienceIds) {
        if (!loadedIds.has(id) && !nonPinning.has(id)) workingSet.add(id);
      }
    }

    return { pinnedIds: [...workingSet], bump };
  }

  /* ================= use-home-deep-link.ts ================= */

  /** Query param → drawer, one per entity. Mutually exclusive; `signal` wins. */
  const HOME_DEEP_LINK_PARAMS = ['signal', 'account', 'person'];

  /**
   * Build `/user/home/<view>?<param>=<id>`, PRESERVING the caller's query
   * string. Any deep-link param already present is dropped first, so a redirect
   * can never stack two drawers.
   */
  function homeDeepLink(view, param, id, search) {
    const next = new URLSearchParams(search);
    for (const key of HOME_DEEP_LINK_PARAMS) next.delete(key);
    if (id) next.set(param, id);
    const qs = next.toString();
    return `/user/home/${view}${qs ? `?${qs}` : ''}`;
  }

  /**
   * Drop Home's deep-link params from a query string, keeping everything else.
   * `utm_*` is deliberately kept: attribution has to survive every hop.
   */
  function stripHomeDeepLink(search) {
    const next = new URLSearchParams(search);
    for (const key of HOME_DEEP_LINK_PARAMS) next.delete(key);
    const qs = next.toString();
    return qs ? `?${qs}` : '';
  }

  /**
   * Where the report email's "Reach out →" CTA lands a redesign user. It lands
   * on the PERSON DRAWER, not the composer. No `personId` means there is nobody
   * to open — fall back to the signal.
   */
  function legacyComposeDeepLink(eventId, search) {
    const params = new URLSearchParams(search);
    const personId = params.get('personId') ?? undefined;
    // `personId` is the LEGACY composer's spelling of the instruction. Home
    // reads `person`, so leaving it would park a dead param in the address bar.
    params.delete('personId');
    const rest = params.toString();
    return personId
      ? homeDeepLink('people', 'person', personId, rest)
      : homeDeepLink('signals', 'signal', eventId, rest);
  }

  function useHomeDeepLink() {
    const [searchParams, setSearchParams] = useSearchParams();

    // One param wins, so a hand-edited `?signal=..&person=..` opens one drawer
    // rather than racing two. Order matches HOME_DEEP_LINK_PARAMS.
    const signalId = searchParams.get('signal');
    const accountId = !signalId ? searchParams.get('account') : null;
    const personId = !signalId && !accountId ? searchParams.get('person') : null;

    // The app skips these with Apollo `skip:`; the mock hooks are synchronous,
    // so an empty-id lookup simply resolves to nothing.
    const signal = T.Data.useHomeSignalsByIds({ eventIds: [signalId ?? ''] });
    const account = T.Data.useHomeAccountDrawer({ id: accountId ?? '' });
    const person = T.Data.useHomePersonDrawer({ id: personId ?? '' });

    const clear = () => {
      setSearchParams(
        (prev) => {
          const next = new URLSearchParams(prev);
          for (const key of HOME_DEEP_LINK_PARAMS) next.delete(key);
          return next;
        },
        { replace: true }
      );
    };

    const idle = {
      target: null,
      key: null,
      pending: false,
      missing: false,
      clear,
    };

    if (signalId) {
      // `events` is filtered by tenant server-side, so an id from another tenant
      // comes back as an empty list — indistinguishable from deleted, and both
      // want the same "strip it and stay put" handling.
      const event = signal.data?.events?.[0];
      if (event) {
        return {
          ...idle,
          key: `signal:${signalId}`,
          target: { type: 'signal', event },
        };
      }
      return resolving(idle, signal.loading, 'signal', signalId);
    }

    if (accountId) {
      const row = account.data?.account;
      if (row) {
        return {
          ...idle,
          key: `account:${accountId}`,
          target: {
            type: 'account',
            // AccountDrawer refetches this same document by id; what the ROW
            // seeds is the drawer shell's title, which is why name/logo are
            // worth carrying.
            account: {
              id: row.id,
              name: row.name,
              url: row.url ?? null,
              logoUrl: row.logoUrl ?? null,
              oneLiner: row.oneLiner ?? null,
              employeeCount: row.employeeCount ?? null,
              annualRevenueFrom: row.annualRevenueFrom ?? null,
              annualRevenueTo: row.annualRevenueTo ?? null,
              ownership: row.ownership ?? null,
              hqLocation: row.hqLocation ?? null,
              timingScore: row.timingScore ?? null,
              // Not selected by the drawer document and not read by the drawer;
              // inventing a date here would put a fake "added on" in front of
              // the user.
              createdAt: '',
            },
          },
        };
      }
      return resolving(idle, account.loading, 'account', accountId);
    }

    if (personId) {
      const row = person.data?.person;
      if (row) {
        return {
          ...idle,
          key: `person:${personId}`,
          target: {
            type: 'person',
            person: {
              id: row.id,
              fullName: row.fullName,
              title: row.title ?? null,
              profileImageUrl: row.profileImageUrl ?? null,
              lastContactedAt: row.lastContactedAt ?? null,
            },
          },
        };
      }
      return resolving(idle, person.loading, 'person', personId);
    }

    return idle;
  }

  /** No record yet: still loading, or genuinely unresolvable. The key carries
   *  the id, not a bare 'missing', so a second dead link in a session still
   *  gets its param stripped. */
  function resolving(idle, loading, param, id) {
    return loading
      ? { ...idle, pending: true }
      : { ...idle, missing: true, key: `missing:${param}:${id}` };
  }

  /**
   * Carry out the deep-link instruction exactly once, then strip it. Gated on
   * the last CONSUMED key: `target` is a fresh object every render, and under
   * StrictMode the mount effect double-invokes with the key already set —
   * opening twice would make the drawer its own back target and double-count
   * `drawer_opened`.
   */
  function useHomeDeepLinkConsumer(onOpen) {
    const { key, target, clear } = useHomeDeepLink();
    /** Last key actually carried out. Written only inside the effect. */
    const consumed = useRef(null);

    // Honest dependencies: `target`, `clear` and `onOpen` are all rebuilt every
    // render, and that is fine, because the consumed-key guard makes the effect
    // idempotent.
    useEffect(() => {
      if (!key) {
        // Disarm once the instruction is gone, so the SAME link can be followed
        // again later in the same mount.
        consumed.current = null;
        return;
      }
      if (consumed.current === key) return;
      consumed.current = key;
      // A dead id (deleted, other tenant, mangled link) leaves `target` null and
      // drops the reader on the plain table rather than an error page.
      if (target) onOpen(target);
      clear();
    }, [key, target, clear, onOpen]);
  }

  /* ================= signals/signal-identity.ts ================= */

  // The row's signal IDENTITY: a display NAME (the tenant's matched signal,
  // e.g. "Security Hiring") plus the coarse TYPE that drives every visual.
  // Colour and icon come from the type via the shared SIGNAL_TYPES table.

  const tokensOf = (s) => s.toLowerCase().split(/[-_\s]+/).filter(Boolean);

  /** Tokens that read as acronyms/brands, not Title Case words. */
  const ACRONYMS = {
    ai: 'AI', soc: 'SOC', siem: 'SIEM', gtm: 'GTM', it: 'IT', ciso: 'CISO',
    cto: 'CTO', cio: 'CIO', api: 'API', ipo: 'IPO', ma: 'M&A',
    revops: 'RevOps', devops: 'DevOps', devsecops: 'DevSecOps', saas: 'SaaS',
  };

  /** "soc-automation-hiring" / "ai_search_expansion" -> "SOC Automation Hiring" / "AI Search Expansion". */
  function humanizeKey(key) {
    return tokensOf(key)
      .map((t) => ACRONYMS[t] ?? t.charAt(0).toUpperCase() + t.slice(1))
      .join(' ');
  }

  /** Identity for an event: its matched signal's key, else the type's label.
   *
   * The key is the ONLY display source. `matched_signals[].signal_name` is not
   * read: the v1 enrichment path wrote the signal's `detects` text there — the
   * LLM detection criteria, routinely 400+ characters — and the pill rendered
   * that paragraph verbatim. */
  function signalIdentity(data, signalType) {
    const type = toSignalType(signalType);
    const matched = data?.matched_signals;
    const key = Array.isArray(matched)
      ? matched.find((m) => m.signal_key)?.signal_key
      : undefined;
    const name = key ? humanizeKey(key) : SIGNAL_TYPES[type].label;
    return { name, type };
  }

  /** Identity for a bare signal key (server facet rows carry no display name). */
  function signalIdentityForKey(key, signalType) {
    return { name: humanizeKey(key), type: toSignalType(signalType) };
  }

  /* ================= signals/signal-time.ts ================= */

  // Recency buckets + short "when" labels for the Signals table (TRA-1298).
  // Signals are discovered in daily sweeps, so day granularity is the floor.

  const DAY_BUCKETS = ['New', 'This week', 'This month', 'Earlier'];

  const DAY_MS = 24 * 60 * 60 * 1000;

  /** CALENDAR days between `date` and `now` (local midnights, never negative):
   *  an event from yesterday evening is 1 ("Yesterday"), not 0 ("Today"). */
  function daysAgo(date, now = Date.now()) {
    if (!date) return Number.MAX_SAFE_INTEGER;
    const t = date instanceof Date ? date.getTime() : new Date(date).getTime();
    if (Number.isNaN(t)) return Number.MAX_SAFE_INTEGER;
    const startOfDay = (ms) => {
      const d = new Date(ms);
      d.setHours(0, 0, 0, 0);
      return d.getTime();
    };
    return Math.max(0, Math.round((startOfDay(now) - startOfDay(t)) / DAY_MS));
  }

  function dayBucket(date, now = Date.now()) {
    const d = daysAgo(date, now);
    if (d < 2) return 'New';
    if (d < 7) return 'This week';
    if (d < 31) return 'This month';
    return 'Earlier';
  }

  /** Short label for the "When" column: relative for recent, absolute otherwise. */
  function relativeWhen(date, now = Date.now()) {
    if (!date) return '';
    const t = date instanceof Date ? date.getTime() : new Date(date).getTime();
    if (Number.isNaN(t)) return '';
    const d = daysAgo(date, now);
    if (d === 0) return 'Today';
    if (d === 1) return 'Yesterday';
    if (d < 7) return `${d}d ago`;
    if (d < 31) return `${Math.floor(d / 7)}w ago`;
    return new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
  }

  /** Drawer/mini-card headline: job listings need framing — "<Company> is
   *  hiring <title>", or "Hiring: <title>" without a company. News headlines
   *  and career events carry themselves. */
  function signalHeadline(title, signalType, company) {
    const t = (signalType ?? '').toLowerCase();
    const isJobs = t === 'jobs' || t === 'job_listing';
    if (!isJobs || /hiring/i.test(title)) return title;
    return company ? `${company} is hiring ${title}` : `Hiring: ${title}`;
  }

  /* ================= signals/SignalTilePill.tsx ================= */

  /**
   * The one signal representation: the TYPE's icon + the signal's name in a
   * single tinted pill. Colour and icon come from the shared SIGNAL_TYPES
   * table, so the signals table row, the filter dropdown, the drawer header and
   * the account drawer's cards have nothing per-signal left to disagree about.
   */
  function SignalNamePill({ sig, className }) {
    const meta = SIGNAL_TYPES[sig.type];
    const Icon = meta.icon;
    return (
      <span
        className={`htbl-pill htbl-sig-pill${className ? ` ${className}` : ''}`}
        // The tint is applied in home-tables.css off this one variable. Utility
        // classes can't do it: `.htbl-pill` sets background/border/color as
        // unlayered CSS, which beats Tailwind's utility layer regardless of
        // order.
        style={{ '--sig-hue': meta.hueVar }}
        data-signal-type={sig.type}
        title={sig.name}
      >
        {/* Size comes from `.htbl-pill svg`; the ink is the type's hue. */}
        <Icon />
        <span className="htbl-sig-pill-t">{sig.name}</span>
      </span>
    );
  }

  /** Alias for the call sites that read as "the tile" (drawer header, cards). */
  const SignalTilePill = SignalNamePill;

  /* ================= signals/SignalChip.tsx ================= */

  // The one signal pill for the whole home surface — the mock's nv-sig
  // (accent-tinted, icon + label). Table rows, drawers, and the mini-cards all
  // render THIS so the pill can't drift between surfaces.
  function SignalChip({ type }) {
    const t = toSignalType(type);
    const { icon: Icon, label } = SIGNAL_TYPES[t];
    return (
      <span className="htbl-pill">
        <Icon />
        {label}
      </span>
    );
  }

  /* ================= signals/SignalCell.tsx ================= */

  function AccountLine({ account }) {
    return (
      <span className="htbl-sig-account" title={account.name}>
        <LogoAvatar
          src={account.logoUrl}
          domain={account.url ?? undefined}
          alt={account.name}
          fallbackText={getInitials(account.name)}
          size="xs"
          className="size-4 shrink-0 rounded"
        />
        <span className="htbl-sig-account-name">{account.name}</span>
      </span>
    );
  }

  function SignalCell({ sig, account }) {
    return (
      <span className="htbl-sig-cell">
        <SignalNamePill sig={sig} className="htbl-sig-cell-pill" />
        {account && <AccountLine account={account} />}
      </span>
    );
  }

  /* ================= lists/list-mapping.ts ================= */

  // Pure mappings between the working audience state, the server List
  // membership, and the Home focus scope.
  //
  // TRA-1359: a list stores members at the altitude the user chose them —
  // dynamic account entries (with a per-account contacts lever), pinned people
  // with provenance, and exclusions ("not this person"). Save/load are
  // symmetric, so "Acme, top 3, minus Bob" survives a round-trip instead of
  // flattening to three frozen names.

  const ORIGIN_LABEL = {
    saved: 'Saved from Home',
    import: 'CSV import',
    play: 'From a play',
  };

  /** The known via values; anything else (future additions) maps to manual so
   *  the server enum never rejects a stale client. */
  const VIA_VALUES = new Set(['manual', 'account', 'signal', 'find', 'import']);
  function toVia(via) {
    return VIA_VALUES.has(via) ? via : 'manual';
  }

  /** Working audience -> full membership input (TRA-1359, save side). */
  function membersInputFromAudience(a) {
    const out = [];
    for (const [accountId, entry] of Object.entries(a.accounts)) {
      out.push({
        kind: 'account',
        accountId,
        via: toVia(entry.via),
        per: entry.per,
      });
    }
    for (const [personId, entry] of Object.entries(a.people)) {
      // An explicit add that was then unchecked lives in `people` AND `excl`
      // (kept so the drawer renders the row in place) — persist it as excluded.
      out.push({
        personId,
        via: toVia(entry.via),
        accountId: entry.accountId || null,
        eventId: entry.eventId ?? null,
        excluded: !!a.excl[personId],
      });
    }
    // Bare exclusions (no people entry) are unchecked auto-picks of an account
    // entry. `via: 'account'` is the load-side discriminator for "exclusion
    // only".
    for (const personId of Object.keys(a.excl)) {
      if (a.people[personId]) continue; // already emitted above
      out.push({ personId, via: 'account', excluded: true });
    }
    return out;
  }

  /** List members -> working audience state (TRA-1359, load side). */
  function membersToAudience(members) {
    const accounts = {};
    const people = {};
    const excl = {};
    let seq = 1;
    for (const m of members) {
      if (m.kind === 'account') {
        if (!m.accountId) continue; // account deleted since save
        accounts[m.accountId] = {
          via: m.via,
          per: m.per ?? 1,
          seq: seq++,
        };
        continue;
      }
      if (!m.person) continue;
      if (m.excluded) {
        excl[m.person.id] = true;
        // `via` is the discriminator (set at save): 'account' marks a bare
        // exclusion that restores as exclusion only; any other via is an
        // excluded EXPLICIT add, which keeps its person entry so the drawer
        // renders the row unchecked in place.
        if (m.via === 'account') continue;
      }
      people[m.person.id] = {
        via: m.via,
        accountId: m.accountId ?? m.person.accountId ?? '',
        ...(m.eventId ? { eventId: m.eventId } : {}),
        seq: seq++,
      };
    }
    return { ...T.EMPTY_AUDIENCE, accounts, people, excl };
  }

  /** Accounts + people a list scopes to (Home focus filter). Account members
   *  scope directly; excluded people never widen the scope. */
  function listScope(members) {
    const accountIds = new Set();
    const personIds = new Set();
    const accountScopeIds = new Set();
    const excludePersonIds = new Set();
    for (const m of members) {
      if (m.kind === 'account') {
        const id = m.accountId ?? m.account?.id;
        if (id) {
          accountIds.add(id);
          accountScopeIds.add(id);
        }
        continue;
      }
      if (!m.person) continue;
      if (m.excluded) {
        // "not this person" — must be subtracted from the focused People view
        // even when an account member would otherwise cover them (TRA-1359).
        excludePersonIds.add(m.person.id);
        continue;
      }
      const acctId = m.accountId ?? m.person.accountId;
      if (acctId) accountIds.add(acctId);
      personIds.add(m.person.id);
    }
    return {
      accountIds: [...accountIds],
      personIds: [...personIds],
      accountScopeIds: [...accountScopeIds],
      excludePersonIds: [...excludePersonIds],
    };
  }

  /**
   * A focused list that resolves to NO members must scope to an impossible id,
   * never to "no filter" — an empty scope would silently show the whole tenant
   * under a list filter.
   */
  const IMPOSSIBLE_ID = '00000000-0000-0000-0000-000000000000';

  /** Accounts/Signals tabs: the list's account members (or the impossible id). */
  function scopeToAccountIds(scope) {
    return scope.accountIds.length ? scope.accountIds : [IMPOSSIBLE_ID];
  }

  /** People tab: OR-composed person-pinned / account-member scope, minus the
   *  list's exclusions. Empty branches are omitted so `[]` never reaches the
   *  server meaning "no filter". */
  function scopeToPeopleOr(scope) {
    const personIds = scope.personIds.length ? scope.personIds : undefined;
    const accountIds = scope.accountScopeIds.length ? scope.accountScopeIds : undefined;
    const excludePersonIds = scope.excludePersonIds.length ? scope.excludePersonIds : undefined;
    if (personIds || accountIds) return { personIds, accountIds, excludePersonIds };
    return { personIds: [IMPOSSIBLE_ID] };
  }

  /**
   * A dynamic list's People view must use its resolved recipients, not its raw
   * account members. Account scoping would expand "Acme · Top 3" into every
   * known Acme contact and make the focused table disagree with the list count.
   */
  function resolvedPeopleScope(personIds) {
    const ids = [...new Set(personIds)];
    return { personIds: ids.length ? ids : [IMPOSSIBLE_ID] };
  }

  /** Client-side list size: distinct COMPANIES REPRESENTED + pinned people.
   *  `people` stays the raw pinned count; callers pass the RESOLVED people
   *  count separately for the displayed label. */
  function listComposition(members) {
    const companyIds = new Set();
    let people = 0;
    for (const m of members) {
      if (m.excluded) continue;
      const companyId =
        m.kind === 'account'
          ? m.accountId ?? m.account?.id ?? null
          : m.person?.accountId ?? m.person?.account?.id ?? null;
      if (companyId) companyIds.add(companyId);
      if (m.kind === 'person' && m.person) people++;
    }
    return { accounts: companyIds.size, people };
  }

  /** "N accounts · M people" (empty parts dropped; both empty -> "Empty"). */
  function compositionLabel(accounts, people) {
    const parts = [];
    if (accounts > 0) parts.push(`${accounts} ${accounts === 1 ? 'account' : 'accounts'}`);
    if (people > 0) parts.push(`${people} ${people === 1 ? 'person' : 'people'}`);
    return parts.length ? parts.join(' · ') : 'Empty';
  }

  /** "N accounts · M people" from the raw membership (people = pinned only). */
  function listCompositionLabel(members) {
    const { accounts, people } = listComposition(members);
    return compositionLabel(accounts, people);
  }

  /** Every list (by name) a person appears in — pinned directly, or covered by
   *  an account member (minus exclusions). Order follows `lists`. */
  function listsOf(lists, personId, accountId) {
    const out = [];
    for (const l of lists) {
      const pinned = l.members.some(
        (m) => !m.excluded && m.person?.id === personId,
      );
      // An exclusion row vetoes account coverage: "Acme minus Bob" does not
      // list Bob even though it lists Acme.
      const excludedHere = l.members.some(
        (m) => m.excluded && m.person?.id === personId,
      );
      // Match on either id field — an account member may carry its id as
      // `accountId` or only inside the `account` relation.
      const covered =
        !!accountId &&
        l.members.some(
          (m) =>
            m.kind === 'account' && (m.accountId ?? m.account?.id) === accountId,
        );
      if (pinned || (covered && !excludedHere)) out.push(l.name);
    }
    return out;
  }

  function firstListOf(lists, personId, accountId) {
    return listsOf(lists, personId, accountId)[0] ?? null;
  }

  /** Default name for the save popover: the dominant account among recipients,
   *  else "New list". */
  function suggestedListName(recipients) {
    if (recipients.length === 0) return 'New list';
    const tally = new Map();
    for (const r of recipients) {
      tally.set(r.account.name, (tally.get(r.account.name) ?? 0) + 1);
    }
    const top = [...tally.entries()].sort((a, b) => b[1] - a[1])[0];
    return top ? top[0] : 'New list';
  }

  /* ================= lists/useListResolutions.ts ================= */

  /** Seed the load-side audience with the person/account DISPLAY meta the lists
   *  query already carries, so pinned people paint their real name/avatar on
   *  the first frame instead of a raw id while the roster query is in flight. */
  function audienceForList(list) {
    const base = membersToAudience(list.members);
    const peopleMeta = { ...(base.peopleMeta ?? {}) };
    const meta = { ...base.meta };
    for (const m of list.members) {
      if (m.person && base.people[m.person.id]) {
        peopleMeta[m.person.id] = {
          fullName: m.person.fullName,
          title: m.person.title,
          profileImageUrl: m.person.profileImageUrl,
        };
      }
      const a = m.kind === 'account' ? m.account : m.person?.account;
      if (a) meta[a.id] = { name: a.name, url: a.url, logoUrl: a.logoUrl };
    }
    return { ...base, peopleMeta, meta };
  }

  /** Resolve every list's people preview from ONE batched roster + events
   *  fetch, reusing the audience machinery (membersToAudience +
   *  resolveAudience) so the Lists page and the list drawer can never disagree
   *  about who a list resolves to (TRA-1359 step 5). */
  function useListResolutions(lists, enabled = true) {
    // Per-list load-side audience (accounts + pinned people + exclusions),
    // seeded with display meta so pinned faces don't flash raw ids.
    const audiences = useMemo(() => {
      const map = new Map();
      for (const l of lists) map.set(l.id, audienceForList(l));
      return map;
    }, [lists]);

    // Distinct accounts to fetch = every whole-account entry ∪ every pinned
    // person's account, across ALL lists — mirrors the provider's accountIds.
    const accountIds = useMemo(() => {
      const ids = new Set();
      for (const a of audiences.values()) {
        for (const id of Object.keys(a.accounts)) ids.add(id);
        for (const entry of Object.values(a.people)) {
          if (entry.accountId) ids.add(entry.accountId);
        }
      }
      return [...ids];
    }, [audiences]);

    const skip = !enabled || accountIds.length === 0;

    const { data: peopleRaw, loading: peopleLoading } = T.Data.useListPeople({
      accountIds: skip ? [] : accountIds,
    });
    const { data: eventsRaw, loading: eventsLoading } = T.Data.useListEvents({
      accountIds: skip ? [] : accountIds,
    });

    const roster = useMemo(
      () =>
        (peopleRaw?.people ?? []).map((p) => ({
          id: p.id,
          fullName: p.fullName,
          title: p.title ?? null,
          profileImageUrl: p.profileImageUrl ?? null,
          email: p.email ?? null,
          enrichmentState: p.enrichmentState ?? null,
          accountId: p.accountId ?? p.account?.id ?? '',
          lastContactedAt: p.lastContactedAt ?? null,
          lastContactedByName: p.lastContactedByName ?? null,
          contactedByMe: p.contactedByMe ?? false,
        })),
      [peopleRaw],
    );

    const accountsMeta = useMemo(() => {
      const map = new Map();
      for (const p of peopleRaw?.people ?? []) {
        if (p.account?.id) {
          map.set(p.account.id, {
            id: p.account.id,
            name: p.account.name,
            url: p.account.url ?? null,
            logoUrl: p.account.logoUrl ?? null,
          });
        }
      }
      for (const e of eventsRaw?.events ?? []) {
        if (e.account?.id && !map.has(e.account.id)) {
          map.set(e.account.id, {
            id: e.account.id,
            name: e.account.name,
            url: e.account.url ?? null,
            logoUrl: e.account.logoUrl ?? null,
          });
        }
      }
      return map;
    }, [peopleRaw, eventsRaw]);

    const eventCtx = useMemo(() => {
      const events = (eventsRaw?.events ?? []).map((e) => ({
        id: e.id,
        accountId: e.accountId ?? e.account?.id ?? null,
        eventDate: e.eventDate ?? null,
        people: (e.people ?? [])
          .filter((ep) => !!ep.person)
          .map((ep) => ({ personId: ep.person.id, reasoning: ep.reasoning ?? null })),
      }));
      return T.buildEventContext(events);
    }, [eventsRaw]);

    return useMemo(() => {
      const byList = new Map();
      for (const l of lists) {
        const audience = audiences.get(l.id);
        if (!audience) continue;
        const res = T.resolveAudience({
          audience,
          lever: 'best',
          roster,
          accountsMeta,
          eventCtx,
        });
        byList.set(l.id, {
          recipients: res.recipients.map((r) => ({
            personId: r.personId,
            fullName: r.fullName,
            profileImageUrl: r.profileImageUrl,
          })),
          peopleCount: res.counts.people,
        });
      }
      return {
        byList,
        resolving:
          !skip &&
          ((peopleLoading && peopleRaw == null) ||
            (eventsLoading && eventsRaw == null)),
      };
    }, [
      lists,
      audiences,
      roster,
      accountsMeta,
      eventCtx,
      skip,
      peopleLoading,
      peopleRaw,
      eventsLoading,
      eventsRaw,
    ]);
  }

  /* ================= lists/ListFilterChip.tsx ================= */

  /** Home toolbar chip that scopes all three tabs to a saved list. Built on the
   *  canonical SearchableFilterPopover pill like every other toolbar filter.
   *  Value is the list id (mirrored in the URL by the page). */
  function ListFilterChip({ value, onChange }) {
    const { lists } = T.Data.useLists();
    const [open, setOpen] = useState(false);
    const active = value ? lists.find((l) => l.id === value) : null;
    // Resolve dynamic account members into people (same as the Lists page + the
    // drawer) so the subtitle shows "N accounts · M people" honestly, not the
    // pinned-only count. Gated on `open` so the roster fetch only runs when the
    // menu is actually shown.
    const { byList: resolutions, resolving } = useListResolutions(lists, open);

    return (
      <SearchableFilterPopover
        open={open}
        onOpenChange={setOpen}
        active={!!active}
        triggerContent={
          <span className="flex min-w-0 items-center gap-1.5">
            {active ? (
              <BookmarkFilled className="size-3.5 shrink-0 text-accent-text" />
            ) : (
              <Bookmark className="size-3.5 shrink-0 text-text-muted" />
            )}
            <span
              className={
                active
                  ? 'max-w-[160px] truncate text-xs text-text-secondary'
                  : 'max-w-[160px] truncate text-xs font-normal text-text-muted'
              }
            >
              {/* Not "All lists": nothing is filtered here, and the table shows
                  records that are on no list at all. The empty state is an
                  invitation, matching the aria-label and the search placeholder. */}
              {active ? active.name : 'Filter by list'}
            </span>
          </span>
        }
        triggerAriaLabel="Filter by list"
        onClear={(e) => {
          e.stopPropagation();
          onChange(null);
        }}
        placeholder="Filter by list"
        emptyText="No lists yet"
        showSearch={false}
      >
        <CommandGroup>
          {lists.map((l) => (
            <CommandItem
              key={l.id}
              value={l.id}
              onSelect={() => {
                onChange(l.id === value ? null : l.id);
                setOpen(false);
              }}
            >
              <Bookmark className="text-text-muted" />
              <span className="flex-1 truncate">{l.name}</span>
              {/* One number, not a row of glyphs: picking a list is one
                  question — how many people does it reach — so that is the only
                  figure, right-aligned into a clean numeric edge. RESOLVED
                  people (dynamic account members included, matching the
                  drawer), with the pinned count as the fallback while the
                  roster resolves. */}
              {(() => {
                const comp = listComposition(l.members);
                const r = resolutions.get(l.id);
                const people = !resolving && r ? r.peopleCount : comp.people;
                if (!people && !comp.accounts) {
                  return (
                    <span className="shrink-0 text-meta text-text-muted">
                      Empty
                    </span>
                  );
                }
                // The unit is a glyph, not a word: one icon reads faster than
                // "people" repeated down the list, and the number keeps the
                // weight. Accounts only stand in when a list reaches nobody yet.
                const Icon = people ? Users : Building2;
                return (
                  <span
                    // The unit is a glyph, and icons are aria-hidden — so
                    // without a name this reads as a bare "12" with no way to
                    // tell people from accounts.
                    role="img"
                    aria-label={compositionLabel(comp.accounts, people)}
                    className="inline-flex shrink-0 items-center gap-1 text-meta tabular-nums text-text-muted"
                    title={compositionLabel(comp.accounts, people)}
                  >
                    {people || comp.accounts}
                    <Icon className="size-3 shrink-0 opacity-70" />
                  </span>
                );
              })()}
              {value === l.id && <Check className="text-accent-text" />}
            </CommandItem>
          ))}
        </CommandGroup>
      </SearchableFilterPopover>
    );
  }

  /* ================= lists/SaveListControl.tsx ================= */

  /** Save / update the current audience as a List. Bar variant is a labeled
   *  secondary button, drawer variant is a quiet header button. Manual save
   *  only.
   *
   *  Icons signal the *action/state*, not list identity:
   *    Save list  → BookmarkPlus (create)
   *    Edited     → Pencil (dirty, needs update)
   *    Saved      → Check (clean confirmation) */
  function SaveListControl({ variant }) {
    const pb = T.useAudience();
    const { lists, createList, updateList } = T.Data.useLists();
    // Linked state comes from the audience alone: right after a save the lists
    // refetch may not have landed yet, and the control must still read Saved
    // (not fall back to the Save list CTA).
    const linkedId = pb.linkedListId;
    const linkedList = linkedId
      ? (lists.find((l) => l.id === linkedId) ?? null)
      : null;
    const dirty = pb.isDirty;
    const recipients = pb.res.recipients;

    const [open, setOpen] = useState(false);
    const [name, setName] = useState('');
    const [saving, setSaving] = useState(false);

    // An empty audience has nothing to name and save, so the create path stays
    // hidden. A LINKED list is different (TRA-1431): unchecking the only
    // contact of a per=1 account legitimately resolves to zero recipients, and
    // that is an edit the user must be able to persist.
    if (recipients.length === 0 && !linkedId) return null;

    const openPopover = () => {
      setName(suggestedListName(recipients));
      setOpen(true);
    };

    const doCreate = async () => {
      const finalName = name.trim() || 'New list';
      setSaving(true);
      try {
        // TRA-1359: persist the audience STATE (account entries, pinned people,
        // exclusions), not the resolved roster. Snapshot the exact audience we
        // send so markSaved records THAT as the saved baseline; an edit during
        // the in-flight save then stays dirty instead of being silently marked
        // saved (and lost).
        const sent = pb.audience;
        const list = await createList(finalName, membersInputFromAudience(sent));
        if (list) {
          pb.markSaved(list.id, sent);
          homeAnalytics.listSaved({
            list_id: list.id,
            audience_people: pb.res.counts.people,
            audience_accounts: pb.res.counts.accounts,
            is_update: false,
          });
        }
        toast(`Saved to Lists · “${finalName}”`);
        setOpen(false);
      } finally {
        setSaving(false);
      }
    };

    const doUpdate = async () => {
      if (!linkedId) return;
      setSaving(true);
      try {
        const sent = pb.audience;
        await updateList(linkedId, {
          members: membersInputFromAudience(sent),
        });
        pb.markSaved(linkedId, sent);
        homeAnalytics.listSaved({
          list_id: linkedId,
          audience_people: pb.res.counts.people,
          audience_accounts: pb.res.counts.accounts,
          is_update: true,
        });
        toast(`“${linkedList?.name ?? 'List'}” updated.`);
        setOpen(false);
      } finally {
        setSaving(false);
      }
    };

    // Linked and clean: same footprint as the save CTA so the bar doesn't
    // collapse, but disabled/ghost so it reads as status, not an action.
    if (linkedId && !dirty) {
      const savedTitle = linkedList
        ? `Saved to “${linkedList.name}”`
        : 'Saved to Lists';
      if (variant === 'bar') {
        return (
          // mock pl-btn ghost sm; matches Save/Edited footprint
          <button
            type="button"
            className="hdw-cta sm ghost"
            disabled
            title={savedTitle}
          >
            <Check />
            {/* Wrapped so the phone bar can drop it (TRA-1512): the control
                collapses to its glyph, and CSS cannot hide a bare text node.
                The visible copy is aria-hidden and an sr-only twin carries the
                name. */}
            <span aria-hidden className="hdw-cta-label">
              Saved
            </span>
            <span className="sr-only">Saved</span>
          </button>
        );
      }
      return (
        // mock pl-btn quiet sm
        <button type="button" className="hdw-quiet" disabled title={savedTitle}>
          <Check />
          Saved
        </button>
      );
    }

    const triggerLabel = linkedId ? 'Edited' : 'Save list';
    const TriggerIcon = linkedId ? Pencil : BookmarkPlus;

    return (
      <Popover
        open={open}
        onOpenChange={(o) => (o ? openPopover() : setOpen(false))}
      >
        <PopoverTrigger asChild>
          {variant === 'bar' ? (
            // mock pl-btn second sm; no Button variant matches
            <button
              type="button"
              className="hdw-cta sm second"
              title={
                linkedId
                  ? 'Edited since last save'
                  : 'Name and save this list to reuse and filter by'
              }
            >
              <TriggerIcon />
              {/* See the Saved button above — wrapped so the phone bar can
                  collapse this to its glyph, with an sr-only twin keeping the
                  accessible name (TRA-1512). */}
              <span aria-hidden className="hdw-cta-label">
                {triggerLabel}
              </span>
              <span className="sr-only">{triggerLabel}</span>
            </button>
          ) : (
            // mock pl-btn quiet sm; no Button variant matches
            <button
              type="button"
              className="hdw-quiet"
              title={
                linkedId
                  ? 'Edited since last save'
                  : 'Name and save this list to reuse and filter by'
              }
            >
              <TriggerIcon />
              {triggerLabel}
            </button>
          )}
        </PopoverTrigger>
        <PopoverContent align="end" className="w-[264px]">
          {linkedId && dirty ? (
            <div className="flex flex-col gap-2">
              <Button size="sm" loading={saving} onClick={doUpdate}>
                <Check />
                Update “{linkedList?.name ?? 'list'}”
              </Button>
              <span className="text-eyebrow text-text-muted">or save as new</span>
              <Input
                value={name}
                onChange={(e) => setName(e.target.value)}
                onKeyDown={(e) => e.key === 'Enter' && void doCreate()}
                placeholder="List name"
              />
              <Button
                variant="tertiary"
                size="sm"
                loading={saving}
                onClick={doCreate}
              >
                Save as new list
              </Button>
            </div>
          ) : (
            <div className="flex flex-col gap-2">
              <span className="text-eyebrow text-text-muted">List name</span>
              <Input
                autoFocus
                value={name}
                onChange={(e) => setName(e.target.value)}
                onKeyDown={(e) => e.key === 'Enter' && void doCreate()}
                onFocus={(e) => e.target.select()}
              />
              <div className="flex items-center justify-between gap-2">
                <span className="text-meta text-text-muted">
                  Lives in Lists. Keep building here.
                </span>
                <Button size="sm" loading={saving} onClick={doCreate}>
                  Save
                </Button>
              </div>
            </div>
          )}
        </PopoverContent>
      </Popover>
    );
  }

  /* ================= ExternalGroup.tsx ================= */

  /**
   * The seam between records the tenant owns and directory matches it does not
   * (TRA-1382 discovery rows), plus the two bits of copy that describe it.
   * Shared by People and Accounts so the copy and the seam can't drift apart.
   */

  /** Cap each directory search asks for; an exact hit means "at least this
   *  many". The two tables ask for different amounts, so the cap travels with
   *  the count rather than being a single shared constant. */
  const PEOPLE_SEARCH_LIMIT = 25;
  const ACCOUNTS_SEARCH_LIMIT = 5;

  /**
   * Park the sticky band directly under whatever is sticky above it: the column
   * header, plus the selection/notice row when one is showing. Measured rather
   * than declared, so it is immune to either height changing later.
   */
  function useExternalBandOffset(tableRef, selectionRowShowing) {
    useLayoutEffect(() => {
      const table = tableRef.current;
      if (!table) return;
      const measure = () => {
        const head = table.querySelector('thead');
        const selRow = table.querySelector('.htbl-selection-scope');
        const top =
          (head?.getBoundingClientRect().height ?? 0) +
          (selRow?.getBoundingClientRect().height ?? 0);
        table.style.setProperty('--htbl-band-top', `${Math.round(top)}px`);
      };
      measure();
      // Re-measure on reflow: a longer selection message wraps to two lines,
      // and the header can change height with the window.
      const ro = new ResizeObserver(measure);
      const head = table.querySelector('thead');
      const selRow = table.querySelector('.htbl-selection-scope');
      if (head) ro.observe(head);
      if (selRow) ro.observe(selRow);
      return () => ro.disconnect();
    }, [tableRef, selectionRowShowing]);
  }

  /**
   * "12 people available to add" / "25+ accounts available to add".
   *
   * At the cap we say `25+` rather than `25`: the search asked for 25 and got
   * 25, so the true total is unknown, and printing an exact number we cannot
   * stand behind is how the old "1 of 730" ended up contradicting the screen.
   */
  function availableToAddLabel(count, noun, limit) {
    const atCap = count >= limit;
    const n = atCap ? `${limit}+` : String(count);
    const unit = !atCap && count === 1 ? (noun === 'people' ? 'person' : 'account') : noun;
    return `${n} ${unit} available to add`;
  }

  /**
   * The sticky seam row. `colSpan` differs per table. The air above the seam is
   * the band's own top padding, not a spacer row.
   */
  function ExternalGroupBand({ colSpan, count, noun, limit }) {
    // Nothing to announce yet — a count of zero is never a useful seam label.
    if (count <= 0) return null;
    return (
      <tr className="htbl-ext-band">
        <td colSpan={colSpan} data-testid="home-external-band">
          {availableToAddLabel(count, noun, limit)}
        </td>
      </tr>
    );
  }

  /**
   * "1 yours · 12 to add" — replaces `N of M`, which counted a scope the user
   * could not see and contradicted the rows on screen. Rendered only while a
   * search is actually narrowing something, and the to-add half only while the
   * directory search has results.
   */
  function HomeSearchSplitCount({
    owned,
    external,
    limit,
    className,
    'data-testid': testId = 'home-search-split-count',
  }) {
    if (owned === undefined) return null;
    const parts = [`${owned.toLocaleString()} yours`];
    if (external !== undefined && external > 0) {
      const atCap = external >= limit;
      parts.push(`${atCap ? `${limit}+` : external} to add`);
    }
    const label = parts.join(' · ');
    return (
      <span
        data-testid={testId}
        aria-live="polite"
        aria-label={label}
        title={label}
        className={cn('htbl-result-count', className)}
      >
        {label}
      </span>
    );
  }

  /* ================= SearchInAccountsLink.tsx ================= */

  /**
   * Escape hatch for a Signals search that found nothing. A signal search term
   * is nearly always a company, and the Accounts tab already answers every
   * version of "does Trayo know this company" — so this hands the query over
   * rather than rebuilding a slice of that answer here.
   */
  function SearchInAccountsLink({ query }) {
    const navigate = useNavigate();
    const q = query.trim();
    if (q.length < 2) return null;
    return (
      <span className="inline-flex items-center gap-1.5 text-meta text-text-muted">
        <Building2 className="size-3.5" />
        <span>
          Search <span className="font-medium">“{q}”</span> in{' '}
          {/* inline text link, mirrors SearchInFindLink */}
          <button
            type="button"
            onClick={() =>
              navigate(
                `/user/home/accounts?${HOME_SEARCH_PARAM}=${encodeURIComponent(q)}`,
              )
            }
            className="font-medium text-accent-text underline-offset-2 hover:underline"
          >
            Accounts
          </button>
        </span>
      </span>
    );
  }

  /* ================= SearchInFindLink.tsx ================= */

  /**
   * Escape hatch shown at the bottom of the Home Accounts/People search
   * results: the Home search bar only does exact name/domain lookups, so a
   * descriptive or ICP-style query belongs on the Find page. One muted line
   * where only "Find" is the link, which deep-links to Find with the same query
   * pre-run (Find reads `?q=`).
   */
  function SearchInFindLink({ query, prefix }) {
    const navigate = useNavigate();
    const q = query.trim();
    if (q.length < 3) return null;
    return (
      <span className="inline-flex items-center gap-1.5 text-meta text-text-muted">
        <Sparkles className="size-3.5" />
        <span>
          {prefix ? `${prefix} ` : ''}Search <span className="font-medium">“{q}”</span> in{' '}
          {/* inline text link, intentionally not a <Button> */}
          <button
            type="button"
            onClick={() => navigate(`/user/find?q=${encodeURIComponent(q)}`)}
            className="font-medium text-accent-text underline-offset-2 transition-colors hover:underline"
          >
            Find
          </button>
        </span>
      </span>
    );
  }

  /* ================= cards/PersonPickCard.tsx ================= */

  /** A person suggestion card (signal / account / person drawers). The body
   *  opens the person when `onOpen` is given; the pill is always the audience
   *  toggle. Markup mirrors the mock's `pt-ex-p` card + `pcard-add` pill. */
  function PersonPickCard({ accountId, person, why, via, eventId, onOpen }) {
    const pb = T.useAudience();
    const on = pb.hasPerson(person.id);
    const toggle = () =>
      on
        ? pb.removePerson(person.id)
        : pb.addPerson(accountId, person.id, via, eventId, undefined, {
            // Card already has the person's display data — pass it so the bar
            // paints the real face/name on the first frame (TRA-1359).
            fullName: person.fullName,
            title: person.title,
            profileImageUrl: person.profileImageUrl,
          });
    const open = onOpen ? () => onOpen(person) : toggle;

    return (
      <div
        className={`hdw-pcard${on ? ' on' : ''}${onOpen ? ' opens' : ''}`}
        onClick={open}
        role={onOpen ? 'button' : 'checkbox'}
        aria-checked={onOpen ? undefined : on}
        tabIndex={0}
        onKeyDown={(e) => {
          if (e.key === 'Enter') {
            e.preventDefault();
            open();
          } else if (e.key === ' ') {
            e.preventDefault();
            toggle();
          }
        }}
      >
        <PersonAvatar
          src={person.profileImageUrl}
          personId={person.id}
          name={person.fullName}
          className="size-8 shrink-0"
        />
        <span className="hdw-pc-t">
          <span className="hdw-pc-n">
            {person.fullName}
            {person.title && <i>{person.title}</i>}
          </span>
          <span className="hdw-pc-w" title={why}>
            {why}
          </span>
          {T.isRecentlyContacted(person.lastContactedAt, T.RECENT_ROW_DAYS) && (
            <span
              className="hdw-pc-w text-text-muted"
              data-testid="pick-card-outreach-marker"
            >
              {T.outreachMarker({
                lastContactedAt: person.lastContactedAt ?? null,
                lastContactedByName: person.lastContactedByName ?? null,
                contactedByMe: person.contactedByMe ?? false,
              })}
            </span>
          )}
        </span>
        {/* mock pcard-add pill; no Button variant matches (74x28, accent-soft→solid) */}
        <button
          type="button"
          className={`hdw-pcard-add${on ? ' on' : ''}`}
          aria-label={on ? 'Remove from list' : 'Add to list'}
          onClick={(e) => {
            e.stopPropagation();
            toggle();
          }}
        >
          {on ? (
            <>
              <Check />
              Added
            </>
          ) : (
            <>
              <UserPlus />
              Add
            </>
          )}
        </button>
      </div>
    );
  }

  Object.assign(T, {
    // compact-count.ts
    compactCount,
    // HomeTabs.tsx
    HomeTabs,
    // HomeTableSearch.tsx
    HomeTableSearch,
    // HomeSelectCell.tsx
    HomeSelectCell,
    // HomeTableSortTh.tsx
    HomeTableSortTh,
    nextSortDir,
    // HomeTableResultCount.tsx
    HomeTableResultCount,
    // home-table-selection.ts
    HOME_TABLE_BULK_SELECTION_LIMIT,
    HOME_TABLE_BULK_FETCH_POLICY,
    homeTableBulkSelectionState,
    homeTableLoadedSelectionState,
    shouldShowHomeTableSelectionScope,
    clearHomeTableSelection,
    fetchAllMatchingRows,
    hasAudienceSelection,
    countSelectedMatchingIds,
    accountsToAudienceBatch,
    peopleToAudienceBatch,
    // HomeTableSelection.tsx
    HomeTableSelectAllCheckbox,
    HomeTableSelectionScopeRow,
    HomeTableFilteredSelectionNoticeRow,
    // email-status.ts
    emailStatus,
    // use-search-param.ts
    HOME_SEARCH_PARAM,
    useSeededSearch,
    // useWorkingSetPin.ts
    useWorkingSetPin,
    // use-home-deep-link.ts
    HOME_DEEP_LINK_PARAMS,
    homeDeepLink,
    stripHomeDeepLink,
    legacyComposeDeepLink,
    useHomeDeepLink,
    useHomeDeepLinkConsumer,
    // signals/signal-identity.ts
    signalIdentity,
    signalIdentityForKey,
    // signals/signal-time.ts
    DAY_BUCKETS,
    daysAgo,
    dayBucket,
    relativeWhen,
    signalHeadline,
    // signals/SignalTilePill.tsx
    SignalNamePill,
    SignalTilePill,
    // signals/SignalChip.tsx
    SignalChip,
    // signals/SignalCell.tsx
    SignalCell,
    // lists/list-mapping.ts
    ORIGIN_LABEL,
    membersInputFromAudience,
    membersToAudience,
    listScope,
    IMPOSSIBLE_ID,
    scopeToAccountIds,
    scopeToPeopleOr,
    resolvedPeopleScope,
    listComposition,
    compositionLabel,
    listCompositionLabel,
    listsOf,
    firstListOf,
    suggestedListName,
    // lists/useListResolutions.ts
    useListResolutions,
    // lists/ListFilterChip.tsx
    ListFilterChip,
    // lists/SaveListControl.tsx
    SaveListControl,
    // ExternalGroup.tsx
    PEOPLE_SEARCH_LIMIT,
    ACCOUNTS_SEARCH_LIMIT,
    useExternalBandOffset,
    availableToAddLabel,
    ExternalGroupBand,
    HomeSearchSplitCount,
    // SearchInAccountsLink.tsx / SearchInFindLink.tsx
    SearchInAccountsLink,
    SearchInFindLink,
    // cards/PersonPickCard.tsx
    PersonPickCard,
  });
})();
