/* Ported from apps/web/src/features/home/:
 *   drawer/DrawerShell.tsx, drawer/drawer-close.ts, drawer/DrawerDeleteAction.tsx,
 *   drawer/DrawerRefLinks.tsx, drawer/drawer-ref-links.ts, drawer/OutreachSection.tsx,
 *   drawer/outreach-query.ts (over T.Data),
 *   signals/SignalDrawer.tsx, signals/signal-details.tsx, signals/AccountSigCard.tsx,
 *   signals/AccountObjectCard.tsx, signals/oneliner.ts, signals/signal-audience.ts,
 *   accounts/AccountDrawer.tsx, accounts/AccountSynthesis.tsx,
 *   people/PersonDrawer.tsx, people/PersonContactFacts.tsx, people/PersonSynthesis.tsx,
 *   people/EnrichedCell.tsx
 * plus apps/web/src/components/newsfeed/AddAccountSuggestion.tsx (SignalDrawer dep)
 * and the formatRevenue slice of apps/web/src/lib/formatters.ts (private).
 * drawer.css is loaded globally (css/drawer.css — verbatim copy).
 *
 * Owned elsewhere, referenced lazily inside render bodies:
 *   T.SignalTilePill (home-shared), T.PersonPickCard (people-tab),
 *   T.signalIdentity / T.signalHeadline / T.relativeWhen (signals-tab),
 *   T.useAudience / T.seniorityOf / T.buildEventContext / T.rankAccountPeople /
 *   T.whyFor (audience).
 */
(() => {
  const T = window.T;
  const {
    createContext,
    useCallback,
    useContext,
    useEffect,
    useMemo,
    useRef,
    useState,
  } = React;
  const { cn, toast, homeAnalytics } = T;
  const {
    Button,
    ConfirmDialog,
    DropdownMenu,
    DropdownMenuContent,
    DropdownMenuItem,
    DropdownMenuTrigger,
    LogoAvatar,
    PersonAvatar,
    getInitials,
    useFindEmail,
    useFindPhone,
  } = T.UI;
  const {
    Building2,
    Check,
    ChevronDown,
    ChevronLeft,
    ExternalLink,
    Globe,
    Linkedin,
    Loader2,
    Mail,
    MoreHorizontal,
    Phone,
    Plus,
    Star,
    Trash2,
    UserRound,
    UserPlus,
    X,
  } = T.Icons;

  /* PostHog is not ported — keep the call sites (lib/analytics captureEvent). */
  const captureEvent = () => {};

  /* ======================================================================
   * drawer/drawer-close.ts
   * ==================================================================== */

  // The consumer owns a drawer's mount/unmount, so to animate the LEAVE the shell
  // intercepts every close path: it plays the exit keyframe first, then calls the
  // real onClose once the animation ends. Any child that closes the drawer (the
  // header X, a footer "clear all") reads this context so it eases out too rather
  // than vanishing instantly.
  const DrawerCloseContext = createContext(null);

  /** The shell's animated close. Falls back to the given handler when used
   *  outside a DrawerShell. */
  function useDrawerClose(fallback) {
    return useContext(DrawerCloseContext) ?? fallback;
  }

  /* ======================================================================
   * drawer/DrawerShell.tsx
   * ==================================================================== */

  // Inset drawer that lives INSIDE the Home content card (not a viewport-edge
  // Sheet): an absolutely-positioned panel over a local scrim, so the tables stay
  // visible behind it. The parent card must be `relative` + clip overflow.
  //
  // `screen` (prototype divergence) re-parents the SAME markup into the layout's
  // content column instead, so the panel meets the right edge of the SCREEN and
  // runs its full height, with the scrim stopping at the sidebar. That column is
  // already `relative` and already has exactly that geometry, so the absolute
  // insets below need no change — only their containing block does. Opt-in
  // because Lists deliberately keeps its drawer inside the table card.
  function DrawerShell({
    label,
    onClose,
    onCloseStart,
    wide = false,
    screen = false,
    children,
  }) {
    const [closing, setClosing] = useState(false);
    // Ref so close paths stay identity-stable even if the callback churns.
    const onCloseStartRef = useRef(onCloseStart);
    useEffect(() => {
      onCloseStartRef.current = onCloseStart;
    }, [onCloseStart]);
    const requestClose = useCallback(() => {
      setClosing(true);
      onCloseStartRef.current?.();
    }, []);

    // Escape closes the drawer (standard dialog affordance; the scrim handles
    // click-away). Both route through the animated close.
    useEffect(() => {
      const onKey = (e) => {
        if (e.key === 'Escape') requestClose();
      };
      document.addEventListener('keydown', onKey);
      return () => document.removeEventListener('keydown', onKey);
    }, [requestClose]);

    // Once the leave animation finishes, actually unmount via the parent handler.
    const handleAnimationEnd = (e) => {
      if (closing && e.animationName === 'hdw-out') onClose();
    };

    // Safety net: if animationend never fires (reduced-motion, a background tab),
    // still unmount so the drawer can't get stuck open.
    useEffect(() => {
      if (!closing) return;
      const t = setTimeout(onClose, 300);
      return () => clearTimeout(t);
    }, [closing, onClose]);

    const shell = (
      <DrawerCloseContext.Provider value={requestClose}>
        <div
          className={cn('hdw-scrim absolute inset-0 z-20', closing && 'is-closing')}
          onClick={requestClose}
          aria-hidden
        />
        <aside
          role="dialog"
          aria-label={label}
          onAnimationEnd={handleAnimationEnd}
          className={cn(
            'hdw absolute inset-y-0 right-0 z-30 flex w-full flex-col border-l border-border-strong bg-surface-card',
            wide && 'max-w-[880px]',
            closing && 'is-closing',
          )}
          // 516px, up from the app's 480. `max-w-[516px]` is NOT in the
          // compiled theme.css — the build only emits arbitrary values the app
          // source actually uses — so per CONVENTIONS rule 1 this is an inline
          // style rather than an invented utility class. `wide` keeps its
          // class: max-w-[880px] does exist.
          style={wide ? undefined : { maxWidth: '516px' }}
        >
          {children}
        </aside>
      </DrawerCloseContext.Provider>
    );

    if (!screen) return shell;
    // Resolved per render rather than cached: the column outlives every drawer,
    // but reading it at mount time would pin a stale node if the layout ever
    // remounts. If it is missing (a page rendered outside MainLayout), fall
    // back to rendering in place — degraded to the in-card drawer, never blank.
    const column = document.getElementById(T.CONTENT_COLUMN_ID);
    return column ? <T.Portal container={column}>{shell}</T.Portal> : shell;
  }

  /** Drawer header: the mock's `dw-head` - optional back pill, identity slot,
   *  then a ghost close. */
  function DrawerHeader({ back, children, actions, onClose }) {
    const close = useDrawerClose(onClose);
    return (
      <div className="hdw-head">
        {back && (
          <button type="button" className="hdw-back" onClick={back.onClick}>
            <ChevronLeft />
            {/* Wrapped so it can actually truncate: a bare text node is not an
                element, so the stylesheet's ellipsis rule was landing on the
                chevron and the label was cut off mid-word with no "…". */}
            <span className="min-w-0 truncate">{back.label}</span>
          </button>
        )}
        {children}
        <span className="flex-1" />
        {actions}
        {/* Same shape as the drawer's delete/remove control — bare 28px circle,
            muted glyph, resolving on hover — in the neutral `quiet` reading
            rather than the destructive one. */}
        <Button
          variant="quiet"
          size="xs"
          className="size-7 p-0"
          onClick={close}
          title="Close"
        >
          <span className="sr-only">Close</span>
          <X />
        </Button>
      </div>
    );
  }

  // `px-4` gave way to the `.hdw-body` class: the gutter is now `--hdw-gutter`
  // (16px, 24px from md up) so the header, footer and the full-bleed section
  // bands can all read one value, and a Tailwind utility cannot read a custom
  // property. See css/drawer-overrides.css.
  function DrawerBody({ children, className }) {
    return (
      <div className={cn('hdw-body min-h-0 flex-1 overflow-y-auto py-3.5', className)}>
        {children}
      </div>
    );
  }

  function DrawerFooter({ children }) {
    return (
      <div className="flex items-center gap-2 border-t border-border-subtle px-5 py-3">
        {children}
      </div>
    );
  }

  /* ======================================================================
   * drawer/drawer-ref-links.ts
   * ==================================================================== */

  /**
   * Absolute href for a stored site URL. `Account.url` is sometimes a bare domain
   * ("acme.com"), which the browser would resolve relative to the app origin.
   */
  function absoluteUrl(url) {
    const value = url?.trim();
    if (!value) return null;
    if (/^https?:\/\//i.test(value)) return value;
    if (value.startsWith('//')) return `https:${value}`;
    // Anything that isn't host-like (a path, a mailto:, junk) is not linkable.
    if (!/^[\w-]+(\.[\w-]+)+/.test(value)) return null;
    return `https://${value}`;
  }

  /** Bare host for a website link label — "acme.com", not the full href. */
  function websiteLabel(href) {
    try {
      return new URL(href).hostname.replace(/^www\./i, '');
    } catch {
      return href;
    }
  }

  /**
   * Profile URL from `Person.linkedinUsername`, which stores a bare slug —
   * tolerant of a full URL slipping in via import.
   */
  function linkedinUrl(slug, path) {
    const value = slug?.trim();
    if (!value) return null;
    if (
      /^(https?:)?\/\//i.test(value) ||
      /^(www\.)?linkedin\.com\//i.test(value)
    ) {
      return absoluteUrl(value);
    }
    const clean = value.replace(/^\/+|\/+$/g, '');
    if (!clean) return null;
    return `https://www.linkedin.com/${path}/${encodeURIComponent(clean)}`;
  }

  const linkedinPersonUrl = (slug) => linkedinUrl(slug, 'in');

  /* ======================================================================
   * drawer/DrawerRefLinks.tsx
   * ==================================================================== */

  /**
   * Outbound entity links for a drawer header (LinkedIn for a person, LinkedIn +
   * website for an account). Icon-only so they trail the name without competing
   * with it, and `shrink-0` so a long name ellipsizes before they drop.
   */
  function DrawerRefLinks({ children }) {
    return (
      // `-ml-1` is optical, not layout: each link is a 28px hit target around a
      // 14px glyph, so it carries 7px of its own padding on top of `.hdw-head`'s
      // 9px gap — putting the glyph 16px from the name it belongs to. This claws
      // back half of that padding (glyph lands 12px out) while the tap target
      // keeps its full size. Cancelling all of it reads too tight.
      <span className="-ml-1 flex shrink-0 items-center gap-0.5">{children}</span>
    );
  }

  function DrawerRefLink({ href, icon: Icon, label }) {
    return (
      // Same primitive, variant and box as the header's close button
      // (DrawerShell) so both controls in the `.hdw-head` row read as one family
      // — minus the hover ring. These sit next to the name rather than at the
      // row's edge, where an outlined chip appearing mid-title reads as chrome;
      // the well fill alone is enough to show the target.
      <Button
        asChild
        variant="quiet"
        size="xs"
        className="size-7 p-0 hover:border-transparent"
      >
        <a
          href={href}
          target="_blank"
          rel="noreferrer"
          title={label}
          aria-label={label}
        >
          <Icon />
        </a>
      </Button>
    );
  }

  /* ======================================================================
   * lib/formatters.ts (formatRevenue slice — private to this file)
   * ==================================================================== */

  function formatUSD(value) {
    if (value >= 1000000000) return `$${(value / 1000000000).toFixed(1)}B`;
    if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`;
    if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`;
    return `$${value.toLocaleString()}`;
  }

  function formatRevenue(revenue) {
    if (!revenue || revenue.from === undefined || revenue.from === null) {
      return null;
    }
    if (
      revenue.to === undefined ||
      revenue.to === null ||
      revenue.from === revenue.to
    ) {
      return formatUSD(revenue.from);
    }
    return `${formatUSD(revenue.from)} - ${formatUSD(revenue.to)}`;
  }

  /* ======================================================================
   * drawer/DrawerDeleteAction.tsx
   * ==================================================================== */

  /* Starred accounts/contacts. The app keeps `starred` on the Account row and
     flips it server-side; nothing else in the skeleton reads the flag, so it
     lives here as a local persisted set rather than a field on the mock rows.
     Key follows CONVENTIONS rule 6 (t-skeleton-*). */
  const STARRED_KEY = 't-skeleton-starred';
  const readStarred = () => {
    try {
      const raw = JSON.parse(localStorage.getItem(STARRED_KEY) || '[]');
      return new Set(Array.isArray(raw) ? raw : []);
    } catch {
      return new Set();
    }
  };
  const starKey = (entity, id) => `${entity}:${id}`;
  const toggleStarred = (entity, id) => {
    const set = readStarred();
    const key = starKey(entity, id);
    const next = !set.has(key);
    if (next) set.add(key);
    else set.delete(key);
    localStorage.setItem(STARRED_KEY, JSON.stringify([...set]));
    return next;
  };

  const DELETE_COPY = {
    account: {
      permission: 'accounts:delete',
      typename: 'Account',
      noun: 'account',
      title: 'Delete this account?',
      menuLabel: 'Delete account',
      confirmText: 'Delete account',
      // Honest about the blast radius: the server soft-deletes the account row
      // only — its contacts and signals are not cascaded today.
      detail:
        'It leaves your accounts everywhere in Trayo. Contacts and signals already discovered stay in your data. This cannot be undone.',
      failure: 'Could not delete this account',
    },
    person: {
      permission: 'people:delete',
      typename: 'Person',
      noun: 'contact',
      title: 'Delete this contact?',
      menuLabel: 'Delete contact',
      confirmText: 'Delete contact',
      detail:
        'They leave your contacts everywhere in Trayo, including any list they are on. This cannot be undone.',
      failure: 'Could not delete this contact',
    },
  };

  /**
   * Footer-left destructive action for the account / person drawers: confirm,
   * delete, drop the row from the dataset and the audience, then close the
   * drawer. Permission-gated in the app (`accounts:delete` / `people:delete`);
   * the skeleton's fixture user can always delete.
   */
  function DrawerDeleteAction({ entity, id, name, onClose }) {
    // usePermissions → fixture user holds every permission (auth not ported).
    const has = () => true;
    const pb = T.useAudience();
    const close = useDrawerClose(onClose);
    const [open, setOpen] = useState(false);
    const [deleting, setDeleting] = useState(false);
    const copy = DELETE_COPY[entity];
    const [starred, setStarred] = useState(() =>
      readStarred().has(starKey(entity, id)),
    );

    if (!has(copy.permission)) return null;

    const confirm = async () => {
      setDeleting(true);
      try {
        if (entity === 'account') {
          await T.Data.deleteAccount(id);
          pb.removeGroup(id);
        } else {
          await T.Data.deletePerson(id);
          pb.removePerson(id);
        }
        // Apollo cache evict + gc → the mock dataset splice already drops the
        // row from every table on the same version bump.
        homeAnalytics.entityDeleted({ entity, id });
        toast.success(`${name} deleted`);
        setOpen(false);
        close();
      } catch (error) {
        toast.error(
          error instanceof Error && error.message ? error.message : copy.failure,
        );
      } finally {
        setDeleting(false);
      }
    };

    return (
      <>
        {/* The footer actions sit behind a … menu rather than a standing Delete
            button: the footer's real CTA is "Add <name>" on the right, and a
            permanent destructive button competed with it. Item set and order
            mirror the accounts table's row menu (account-columns.tsx).

            Deliberately NOT the shared OverflowMenu: that component's trigger is
            a borderless … (`border-0 hover:bg-accent-soft`) tuned for table rows,
            where a neutral well would vanish against the hovered row. Here the
            trigger has to read as the Delete button it replaces, so it's the same
            `tertiary` Button — bordered pill, `hover:bg-surface-well` — at the
            icon-only `icon-sm` size, which is the same 32px height the old
            `size="sm"` Delete had.

            `align="start"` because this trigger is footer-LEFT (the table's rows
            are right-aligned); the menu flips above the trigger on its own, since
            the drawer footer is pinned to the viewport bottom. */}
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button
              type="button"
              variant="tertiary"
              size="icon-sm"
              aria-label="Drawer actions"
              disabled={deleting}
            >
              <MoreHorizontal />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="start" className="min-w-[200px]">
            {/* No owner model or user picker is ported (auth is out of scope per
                CONVENTIONS rule 8), so this is a prototype stub. */}
            <DropdownMenuItem
              onSelect={() => T.toast('Not part of this prototype')}
            >
              <UserRound size={14} />
              Change owner
            </DropdownMenuItem>
            <DropdownMenuItem
              onSelect={() => {
                const next = toggleStarred(entity, id);
                setStarred(next);
                toast.success(
                  next ? `${name} starred` : `${name} unstarred`,
                );
              }}
            >
              <Star
                size={14}
                fill={starred ? 'currentColor' : 'none'}
                className={starred ? 'text-amber-400' : ''}
              />
              {starred ? `Unstar ${copy.noun}` : `Star ${copy.noun}`}
            </DropdownMenuItem>
            <DropdownMenuItem
              variant="destructive"
              disabled={deleting}
              onSelect={() => setOpen(true)}
            >
              <Trash2 size={14} />
              {copy.menuLabel}
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
        <ConfirmDialog
          open={open}
          onOpenChange={(next) => !deleting && setOpen(next)}
          title={copy.title}
          desc={`${name}: ${copy.detail}`}
          confirmText={copy.confirmText}
          destructive
          isLoading={deleting}
          handleConfirm={() => void confirm()}
        />
      </>
    );
  }

  /* ======================================================================
   * drawer/outreach-query.ts + OutreachSection.tsx
   * ==================================================================== */

  /** First fold before the "Show N more" toggle — mirrors the signals sections. */
  const OUTREACH_VISIBLE = 5;

  /**
   * Outreach section for the person / account drawers: emails Trayo has sent,
   * newest first. Read-only and deliberately narrow — subject, who sent it, when
   * (plus who it went to on the account variant). It is NOT a full activity feed:
   * mail a rep sends from their own inbox never reaches us, so the empty state
   * says "through Trayo" rather than implying the record is complete.
   */
  function OutreachSection({ entity, id }) {
    const [showAll, setShowAll] = useState(false);

    // Collapse when the drawer swaps to a different person or account, so an
    // expanded list does not carry over to someone else. Done here rather than
    // with a `key` at the call site — this component returns a FRAGMENT (a
    // heading and a card list as sibling children of the drawer body), and a
    // keyed fragment does not reliably replace its predecessor's DOM.
    const [watchedId, setWatchedId] = useState(id);
    if (watchedId !== id) {
      setWatchedId(id);
      setShowAll(false);
    }

    // Mock hooks cannot skip; the non-matching entity's query just returns [].
    const personQuery = T.Data.usePersonOutreach({ personId: id });
    const accountQuery = T.Data.useAccountOutreach({ accountId: id, take: 50 });

    const rows =
      entity === 'person'
        ? (personQuery.data?.personOutreachHistory ?? [])
        : (accountQuery.data?.accountOutreachHistory ?? []);
    const loading =
      entity === 'person' ? personQuery.loading : accountQuery.loading;

    // Nothing sent yet is the common case; render the header + a quiet line
    // rather than hiding the section, so the drawer's shape stays stable and the
    // absence of outreach is itself the answer the user came for.
    const visible = showAll ? rows : rows.slice(0, OUTREACH_VISIBLE);
    const hidden = Math.max(0, rows.length - OUTREACH_VISIBLE);

    return (
      <>
        <div className="hdw-ph">
          {/* Named for tests. "Outreach" as bare text is ambiguous here: with no
              rows there is no count badge, so this wrapper's OWN text is also
              exactly "Outreach", and so is its parent's. */}
          <span data-testid="drawer-outreach-heading">Outreach</span>
          {rows.length > 0 && <span className="hdw-ct">{rows.length}</span>}
        </div>
        <div className="hdw-cards">
          {rows.length === 0 ? (
            <p className="hdw-ranknote">
              {loading
                ? 'Loading outreach…'
                : entity === 'person'
                  ? 'No emails sent to this contact through Trayo yet.'
                  : 'No emails sent to anyone here through Trayo yet.'}
            </p>
          ) : (
            visible.map((row) => <OutreachRowItem key={row.id} row={row} />)
          )}
        </div>
        {hidden > 0 && (
          <>
            <button
              type="button"
              className="hdw-more mt-1"
              onClick={() => {
                homeAnalytics.drawerSectionToggled({
                  entity,
                  section: 'outreach',
                  expanded: !showAll,
                  hidden_count: hidden,
                });
                setShowAll((s) => !s);
              }}
            >
              {showAll ? 'Hide earlier outreach' : `Show ${hidden} more`}
              <span className={`hdw-twist${showAll ? ' rot' : ''}`}>
                <ChevronDown />
              </span>
            </button>
          </>
        )}
      </>
    );
  }

  function OutreachRowItem({ row }) {
    const when = T.relativeWhen(row.sentAt ?? null);
    const by = row.byMe ? 'You' : (row.byName ?? 'Someone on your team');
    // "You → Jane Doe · 3d ago" on the account variant; the person drawer already
    // names the recipient in its header, so it stays "You · 3d ago".
    const meta = [row.toName ? `${by} → ${row.toName}` : by, when]
      .filter(Boolean)
      .join(' · ');

    // Same card vocabulary as the signal cards above it (`hdw-sc-hd` headline +
    // `hdw-sc-when` meta), so a subject line never reads bigger than a signal
    // headline; `is-static` drops the click affordances these rows do not have.
    return (
      <div className="hdw-sigcard is-static">
        <span className="hdw-sc-tx">
          <span className="hdw-sc-hd" title={row.subject}>
            {row.subject || '(no subject)'}
          </span>
          <span className="hdw-sc-mt">
            <span className="hdw-sc-when">{meta}</span>
          </span>
        </span>
      </div>
    );
  }

  /* ======================================================================
   * signals/oneliner.ts
   * ==================================================================== */

  /** Escape a string for literal use inside a RegExp. */
  function escapeRegExp(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  /**
   * Presentational cleanup for a company one-line description shown after the
   * account name on the signal-drawer account card: strip a leading
   * account-name prefix and the connector that follows it, then re-capitalize.
   */
  function displayOneLiner(oneLiner, name) {
    const original = (oneLiner ?? '').trim();
    const n = (name ?? '').trim();
    if (!original || !n) return original;

    // Whole-word, case-insensitive leading name match (so "Meta" doesn't strip
    // from "Metallica").
    const nameRe = new RegExp(`^${escapeRegExp(n)}\\b`, 'i');
    if (!nameRe.test(original)) return original;

    const stripped = original
      .replace(nameRe, '')
      .replace(/^[\s,:;.—–-]+/, '')
      .replace(/^(?:is|are|was|were)\s+(?:a|an|the)\s+/i, '')
      .replace(/^(?:is|are|was|were)\s+/i, '')
      .replace(/^(?:a|an|the)\s+/i, '')
      // Drop a contentless org-noun filler ("company that is building …" →
      // "building …").
      .replace(
        /^(?:company|business|firm|organi[sz]ation|corporation)\s+(?:that|which)\s+(?:is\s+)?/i,
        '',
      )
      .trim();

    if (!stripped) return original;
    return stripped.charAt(0).toUpperCase() + stripped.slice(1);
  }

  /* ======================================================================
   * signals/AccountObjectCard.tsx
   * ==================================================================== */

  /** The linked ACCOUNT as a compact, clickable object card at the top of a
   *  drawer body (TRA-1359). Two lines: the account name (+ optional one-liner),
   *  and a signals-on-this-account hint. Clicking hops to the AccountDrawer. */
  function AccountObjectCard({
    account,
    oneLiner,
    otherSignals,
    signalCountMode = 'other',
    onClick,
  }) {
    const signalsLine =
      otherSignals > 0
        ? signalCountMode === 'total'
          ? `${otherSignals} ${otherSignals === 1 ? 'signal' : 'signals'} on this account`
          : `${otherSignals} other ${otherSignals === 1 ? 'signal' : 'signals'} on this account`
        : null;

    return (
      <div
        role="button"
        tabIndex={0}
        onClick={onClick}
        onKeyDown={(e) => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            onClick();
          }
        }}
        className="hdw-acctcard"
      >
        <LogoAvatar
          src={account.logoUrl}
          domain={account.url}
          alt={account.name}
          fallbackText={getInitials(account.name)}
          size="sm"
          className="size-[34px] rounded-[8px] shrink-0"
        />
        <span className="hdw-ac-tx">
          <span className="hdw-ac-line1">
            <span className="hdw-ac-name">{account.name}</span>
            {(() => {
              const desc = displayOneLiner(oneLiner, account.name);
              return desc ? <span className="hdw-ac-desc">{desc}</span> : null;
            })()}
          </span>
          {signalsLine && <span className="hdw-ac-more">{signalsLine}</span>}
        </span>
        <span className="hdw-ac-chev">
          <ChevronDown />
        </span>
      </div>
    );
  }

  /* ======================================================================
   * signals/AccountSigCard.tsx
   * ==================================================================== */

  /** Compact event card for the account / person drawers' "signals" lists: the
   *  headline on line 1, then the signal's icon + tinted name pill and the when ·
   *  source on line 2 (the same signal representation as the table row). Opens the
   *  full signal drawer on click. */
  function AccountSigCard({ event, onClick }) {
    const SignalTilePill = T.SignalTilePill;
    const sig = T.signalIdentity(event.data, event.signalType);
    return (
      <div
        role="button"
        tabIndex={0}
        onClick={onClick}
        onKeyDown={(e) => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            onClick();
          }
        }}
        className="hdw-sigcard"
      >
        <span className="hdw-sc-tx">
          <span className="hdw-sc-hd">{T.signalHeadline(event.title, event.signalType, event.account?.name)}</span>
          <span className="hdw-sc-mt">
            <SignalTilePill sig={sig} />
            <span className="hdw-sc-when">
              {T.relativeWhen(event.eventDate)}
              {event.publication ? ` · ${event.publication}` : ''}
            </span>
          </span>
        </span>
        <span className="hdw-sc-chev">
          <ChevronDown />
        </span>
      </div>
    );
  }

  /* ======================================================================
   * signals/signal-details.tsx
   * ==================================================================== */

  /** why_it_matters can carry sanitized HTML (the newsfeed renders it as such);
   *  the table shows plain text, so strip tags + decode the few common entities
   *  rather than risk dangerouslySetInnerHTML inside a table cell. */
  function stripHtml(s) {
    return s
      .replace(/<[^>]*>/g, ' ')
      .replace(/&nbsp;/g, ' ')
      .replace(/&amp;/g, '&')
      .replace(/&#39;|&rsquo;|&lsquo;/g, "'")
      .replace(/&quot;|&ldquo;|&rdquo;/g, '"')
      .replace(/\s+/g, ' ')
      .trim();
  }

  const confRank = (c) => ({ high: 3, medium: 2, low: 1 })[(c ?? '').toLowerCase()] ?? 0;

  function signalRichFacts(event) {
    const d = event.data ?? {};
    const matched = Array.isArray(d.matched_signals)
      ? d.matched_signals.filter((m) => m.signal_key || m.signal_name)
      : [];
    // Highest-confidence matched signal supplies the evidence sentence.
    const best = matched.reduce(
      (acc, m) => (confRank(m.confidence) > confRank(acc?.confidence) ? m : acc),
      undefined,
    );
    // "Where {company} fits": the first offering that carries a pitch.
    const off = Array.isArray(d.offerings)
      ? d.offerings.find((o) => o?.offering && o.offering.trim())
      : undefined;
    const fit = off
      ? { name: (off.name ?? '').trim(), body: stripHtml(off.offering ?? '') }
      : null;
    return {
      why: stripHtml(d.why_it_matters ?? ''),
      summary: (d.event_summary ?? '').trim(),
      evidence: (best?.evidence ?? '').trim(),
      fit,
    };
  }

  const shortDate = (d) => {
    if (!d) return '';
    const t = new Date(d).getTime();
    if (Number.isNaN(t)) return '';
    return new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
  };

  function SourceMeta({ event }) {
    const when = shortDate(event.eventDate);
    const label = [event.publication, when].filter(Boolean).join(' · ');
    if (!label && !event.url) return null;
    return (
      <span className="htbl-src-meta">
        {label}
        {event.url && (
          <a
            className="htbl-src-link"
            href={event.url}
            target="_blank"
            rel="noreferrer"
            onClick={(e) => e.stopPropagation()}
          >
            Source
            <ExternalLink />
          </a>
        )}
      </span>
    );
  }

  /** Story's right-hand column: "Where {company} fits" - the account's offering
   *  name for this signal (When-styled). Falls back to the source line when
   *  there's no offering, so the column is never empty. */
  function SignalFitCell({ event, fit }) {
    if (!fit?.name) return <SourceMeta event={event} />;
    return <span className="htbl-fit-name">{fit.name}</span>;
  }

  /** The Details cell: the event summary (line 1), the 2-line "why it matters",
   *  then the row of suggested-contact name pills. */
  function SignalDetailsCell({ event, details, facts }) {
    const line1 = (
      <span className="htbl-headline htbl-l1" title={details}>
        {details}
      </span>
    );
    const people = event.people.filter((ep) => ep.person);

    return (
      <div className="htbl-det">
        {line1}
        {facts.why && <span className="htbl-det-why">{facts.why}</span>}
        {people.length > 0 && (
          <span className="htbl-facechips">
            {people.slice(0, 4).map((ep) => (
              <span key={ep.id} className="htbl-facechip">
                <PersonAvatar
                  src={ep.person.profileImageUrl}
                  personId={ep.person.id}
                  name={ep.person.fullName}
                  className="size-[20px]"
                />
                <span className="htbl-facechip-n">{ep.person.fullName}</span>
              </span>
            ))}
            {people.length > 4 && (
              <span className="htbl-contact-more">+{people.length - 4}</span>
            )}
          </span>
        )}
      </div>
    );
  }

  /* ======================================================================
   * signals/signal-audience.ts (pure row -> audience add decision)
   * ==================================================================== */

  const signalRowFacts = (event) => ({
    eventId: event.id,
    accountId: event.account?.id ?? event.accountId ?? '',
    suggestedPersonIds: event.people
      .filter((ep) => ep.person)
      .slice(0, 3)
      .map((ep) => ep.person.id),
  });

  /** What the row's add does: pin the signal's suggested contacts, or fall back
   *  to the whole account when the signal has none. */
  function signalRowAdd(row) {
    if (!row.accountId) return { kind: 'skip' };
    if (row.suggestedPersonIds.length === 0) {
      return { kind: 'account', accountId: row.accountId };
    }
    return {
      kind: 'people',
      accountId: row.accountId,
      personIds: row.suggestedPersonIds,
    };
  }

  /** True when the row is already selected — the bulk planner's skip test. */
  function signalRowFullyAdded(row, audience) {
    if (!row.accountId) return true; // nothing addable
    return signalRowCheckState(row, audience);
  }

  /**
   * Checkbox / row selection for a signal — what the USER picked this session.
   * Session-scoped by design — see `AudienceState.selectedEventIds`.
   */
  function signalRowCheckState(row, audience) {
    if (!row.accountId) return false;
    return !!audience.selectedEventIds?.[row.eventId];
  }

  /** Row lavender tint: on exactly when the row is checked. */
  function signalRowSelected(row, audience) {
    return signalRowCheckState(row, audience);
  }

  /**
   * Which of this row's contacts should leave the audience when it is unchecked:
   * the ones no OTHER still-selected row also suggests.
   */
  function signalRowRemovablePersonIds(row, audience, allRows) {
    return signalRowPersonOwnershipReleases(row, audience, allRows)
      .filter((release) => !release.replacementEventId)
      .map((release) => release.personId);
  }

  /**
   * Release the contacts this signal owns. A still-selected sibling that claims
   * the same contact takes ownership, allowing the final sibling to remove it.
   */
  function signalRowPersonOwnershipReleases(row, audience, allRows) {
    const stillSelected = allRows.filter(
      (candidate) =>
        candidate.eventId !== row.eventId &&
        signalRowCheckState(candidate, audience),
    );
    return row.suggestedPersonIds.flatMap((personId) => {
      const entry = audience.people[personId];
      const signalOwned =
        entry?.via === 'signal-table' || entry?.via === 'signal';
      if (!entry || !signalOwned || entry.eventId !== row.eventId) return [];
      const replacement = stillSelected.find((candidate) =>
        candidate.suggestedPersonIds.includes(personId),
      );
      return [
        {
          personId,
          ...(replacement ? { replacementEventId: replacement.eventId } : {}),
        },
      ];
    });
  }

  /** Bulk plan: the row's add, or skip when the row is already selected. */
  function planSignalRowAdd(row, audience) {
    if (signalRowFullyAdded(row, audience)) return { kind: 'skip' };
    return signalRowAdd(row);
  }

  /** Fold signal rows into one audience commit and explicitly record every row
   * selected by the bulk action. Membership is deduplicated; row selection is
   * not, because two signals sharing a contact must both remain checked. */
  function eventsToAudienceBatch(events, membership) {
    const accounts = {};
    const people = {};
    const meta = {};
    const peopleMeta = {};
    const selectedEventIds = {};

    for (const event of events) {
      const facts = signalRowFacts(event);
      if (!facts.accountId) continue;
      selectedEventIds[event.id] = true;

      const add = planSignalRowAdd(facts, membership);
      if (add.kind === 'skip') continue;
      const acctMeta = event.account
        ? {
            name: event.account.name,
            url: event.account.url ?? null,
            logoUrl: event.account.logoUrl ?? null,
          }
        : null;
      if (acctMeta && !meta[add.accountId]) meta[add.accountId] = acctMeta;
      if (add.kind === 'account') {
        accounts[add.accountId] ??= {
          via: 'signal-table',
          per: 1,
          seq: 0,
          eventId: event.id,
        };
        continue;
      }

      for (const id of add.personIds) {
        if (people[id]) continue;
        const person = event.people.find((ep) => ep.person?.id === id)?.person;
        people[id] = {
          via: 'signal',
          accountId: add.accountId,
          eventId: event.id,
          seq: 0,
        };
        if (person) {
          peopleMeta[id] = {
            fullName: person.fullName,
            title: person.title ?? null,
            profileImageUrl: person.profileImageUrl ?? null,
          };
        }
      }
    }

    return {
      accounts,
      people,
      meta,
      peopleMeta,
      selectedEventIds,
    };
  }

  /* ======================================================================
   * components/newsfeed/AddAccountSuggestion.tsx (SignalDrawer dependency)
   * ==================================================================== */

  /**
   * TRA-1065 — "add this company as an account" CTA on a champion-move card.
   * One click creates it as a fully monitored account.
   */
  function AddAccountSuggestion({ name, coresignalCompanyId, eventId }) {
    const [status, setStatus] = useState('idle');

    const handleAdd = async () => {
      setStatus('adding');
      try {
        await T.Data.createAccount({
          name,
          url: '',
          triggerCoresignal: true,
          triggerInitialDiscovery: true,
        });
        setStatus('added');
        toast.success(`Added ${name}`, {
          description: 'Resolving the company and starting discovery.',
        });
        captureEvent('client.champion.add_account', {
          event_id: eventId,
          company_name: name,
          coresignal_company_id: coresignalCompanyId ?? null,
        });
      } catch (err) {
        setStatus('idle');
        toast.error('Could not add account', {
          description: err instanceof Error ? err.message : String(err),
        });
      }
    };

    return (
      <div className="border-border-subtle mt-3 flex items-center justify-between gap-3 rounded-lg border border-dashed px-3 py-2">
        <div className="text-text-secondary flex min-w-0 items-center gap-2 text-sm">
          <Building2 className="text-text-muted size-4 shrink-0" aria-hidden />
          <span className="truncate">
            <span className="text-text-primary font-medium">{name}</span> isn’t an account yet
          </span>
        </div>
        {status === 'added' ? (
          <span className="text-accent-text inline-flex shrink-0 items-center gap-1 text-sm font-medium">
            <Check className="size-4" aria-hidden /> Added
          </span>
        ) : (
          <Button
            size="sm"
            variant="secondary"
            onClick={handleAdd}
            disabled={status === 'adding'}
            className="shrink-0"
            data-testid="champion-add-account"
          >
            {status === 'adding' ? (
              <Loader2 className="animate-spin" aria-hidden />
            ) : (
              <Plus aria-hidden />
            )}
            Add as account
          </Button>
        )}
      </div>
    );
  }

  /* ======================================================================
   * signals/SignalDrawer.tsx
   * ==================================================================== */

  function SignalDrawer({
    event,
    onClose,
    onBack,
    backLabel,
    onOpenPerson,
    onOpenAccount,
  }) {
    const SignalTilePill = T.SignalTilePill;
    const PersonPickCard = T.PersonPickCard;
    // "More stakeholders" ranked client-side by the seniority heuristic.
    const SENIORITY_RANK = (title) =>
      ['Exec', 'VP / Head', 'Director', 'Other'].indexOf(T.seniorityOf(title));

    const pb = T.useAudience();
    const accountId = event.account?.id ?? event.accountId ?? '';
    const account = event.account;
    const [showMore, setShowMore] = useState(false);
    // The expander sits at the bottom of the scroll area, so the roster it
    // reveals opens out of view - scroll the button to the top so the list
    // shows under it.
    const moreRef = useRef(null);
    useEffect(() => {
      if (showMore)
        moreRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }, [showMore]);

    const data = event.data ?? {};
    // TRA-1065 champion move: the company on the other side of the move that
    // isn't one of the tenant's accounts yet — offered as a one-click add.
    const suggestedAccount = data.suggested_account?.name ? data.suggested_account : null;
    const why = data.why_it_matters || data.event_summary || '';
    const summary =
      data.event_summary && data.event_summary !== why ? data.event_summary : '';
    const discoveredOn = event.eventDate
      ? new Date(event.eventDate).toLocaleDateString('en-US', {
          month: 'short',
          day: 'numeric',
          year: 'numeric',
        })
      : '';

    // The header renders the SAME signal representation as the table row: the
    // type's icon + tinted name pill.
    const sig = T.signalIdentity(event.data, event.signalType);

    const suggested = useMemo(
      () => event.people.filter((ep) => ep.person),
      [event.people],
    );
    const suggestedIds = useMemo(
      () => new Set(suggested.map((ep) => ep.person.id)),
      [suggested],
    );
    const allSuggestedIn =
      suggested.length > 0 && suggested.every((ep) => pb.hasPerson(ep.person.id));

    // Eager: the expander label needs the count up front (mock: "Show more
    // stakeholders at Acme (3)").
    const { data: peopleData } = T.Data.useAccountPeople({ accountId });
    // "N other signals on this account": total completed signals for the account
    // minus this one.
    const { data: sigCountData } = T.Data.useAccountSignalCount({
      accountId,
      accountIdId: accountId,
    });
    const otherSignals = Math.max(0, (sigCountData?.eventsCount ?? 0) - 1);
    // One-line company description (research_info-derived, cheap scalar), fetched
    // here drawer-scoped by accountId - NOT on the events list.
    const accountOneLiner = sigCountData?.account?.oneLiner ?? null;
    const rest = useMemo(() => {
      const rows = (peopleData?.people ?? [])
        .filter((p) => !suggestedIds.has(p.id))
        .map((p) => ({
          id: p.id,
          fullName: p.fullName,
          title: p.title ?? null,
          profileImageUrl: p.profileImageUrl ?? null,
        }));
      return rows.sort(
        (a, b) =>
          SENIORITY_RANK(a.title) - SENIORITY_RANK(b.title) ||
          a.fullName.localeCompare(b.fullName),
      );
    }, [peopleData, suggestedIds]);

    return (
      <>
        <DrawerHeader
          onClose={onClose}
          back={onBack ? { label: backLabel ?? 'Back', onClick: onBack } : undefined}
        >
          <SignalTilePill sig={sig} />
          {discoveredOn && (
            <span className="hdw-when">discovered {discoveredOn}</span>
          )}
        </DrawerHeader>

        <DrawerBody>
          {account && (
            <AccountObjectCard
              account={account}
              oneLiner={accountOneLiner}
              otherSignals={otherSignals}
              onClick={() => onOpenAccount?.(account)}
            />
          )}

          {/* Signal details: the table-style title, the source link, and the
              side-lined "why it matters" grouped under one section header. */}
          <div className="hdw-ph">
            <span>Signal details</span>
          </div>

          <p className="hdw-sig-title">
            {T.signalHeadline(event.title, event.signalType, account?.name)}
            {event.url && (
              <a
                className="hdw-sig-src"
                href={event.url}
                target="_blank"
                rel="noreferrer"
              >
                View source
                <ExternalLink size={11} />
              </a>
            )}
          </p>

          {why && (
            <div className="hdw-sigbody">
              <div className="hdw-why">
                <b>Why it matters</b>
                <p>{why}</p>
                {summary && <p className="hdw-sum">{summary}</p>}
              </div>
            </div>
          )}

          {/* TRA-1065 champion move: the company on the OTHER side of the move
              (the one that isn't a tenant account yet) — one-click add, mirroring
              the newsfeed card's footer. */}
          {suggestedAccount && (
            <>
              <div className="hdw-ph">
                <span>Suggested account</span>
              </div>
              <AddAccountSuggestion
                name={suggestedAccount.name}
                coresignalCompanyId={suggestedAccount.coresignalCompanyId}
                eventId={event.id}
              />
            </>
          )}

          {suggested.length > 0 && (
            <>
              <div className="hdw-ph">
                <span>Suggested contacts</span>
                <span className="hdw-ct">{suggested.length}</span>
                <span className="flex-1" />
                <button
                  type="button"
                  className={`hdw-fcadd${allSuggestedIn ? ' is-added' : ''}`}
                  onClick={() =>
                    suggested.forEach((ep) =>
                      allSuggestedIn
                        ? pb.removePerson(ep.person.id)
                        : pb.addPerson(
                            accountId,
                            ep.person.id,
                            'signal',
                            event.id,
                            account
                              ? {
                                  name: account.name,
                                  url: account.url ?? null,
                                  logoUrl: account.logoUrl ?? null,
                                }
                              : undefined,
                            {
                              // Event already carries the person's display data —
                              // pass it so the bar paints the real face/name on
                              // the first frame (TRA-1359).
                              fullName: ep.person.fullName,
                              title: ep.person.title ?? null,
                              profileImageUrl: ep.person.profileImageUrl ?? null,
                            },
                          ),
                    )
                  }
                >
                  {allSuggestedIn ? (
                    <>
                      <Check />
                      All added
                    </>
                  ) : (
                    <>
                      <UserPlus />
                      {suggested.length === 2 ? 'Add both' : `Add all ${suggested.length}`}
                    </>
                  )}
                </button>
              </div>
              <div className="hdw-cards">
                {suggested.map((ep) => (
                  <PersonPickCard
                    key={ep.person.id}
                    accountId={accountId}
                    person={{
                      id: ep.person.id,
                      fullName: ep.person.fullName,
                      title: ep.person.title ?? null,
                      profileImageUrl: ep.person.profileImageUrl ?? null,
                    }}
                    why={ep.reasoning || `Suggested for this signal at ${account?.name ?? ''}`}
                    via="signal"
                    eventId={event.id}
                    onOpen={onOpenPerson ? (p) => onOpenPerson(p, event) : undefined}
                  />
                ))}
              </div>
            </>
          )}

          {accountId && rest.length > 0 && (
            <>
              <button
                ref={moreRef}
                type="button"
                className="hdw-more mt-3"
                onClick={() => {
                  homeAnalytics.drawerSectionToggled({
                    entity: 'signal',
                    section: 'stakeholders',
                    expanded: !showMore,
                    hidden_count: rest.length,
                  });
                  setShowMore((s) => !s);
                }}
              >
                {showMore
                  ? 'Hide other stakeholders'
                  : `Show more stakeholders at ${account?.name ?? 'this account'} (${rest.length})`}
                <span className={`hdw-twist${showMore ? ' rot' : ''}`}>
                  <ChevronDown />
                </span>
              </button>
              {showMore && rest.length > 0 && (
                <div className="mt-1 flex flex-col gap-2">
                  {rest.map((person) => (
                    <PersonPickCard
                      key={person.id}
                      accountId={accountId}
                      person={person}
                      why={person.title ?? ''}
                      via="manual"
                      onOpen={onOpenPerson ? (p) => onOpenPerson(p, event) : undefined}
                    />
                  ))}
                </div>
              )}
            </>
          )}

          {suggested.length === 0 && (
            <p className="hdw-ranknote mt-4">
              No people linked yet. Trayo is researching stakeholders.
            </p>
          )}
        </DrawerBody>
      </>
    );
  }

  /* ======================================================================
   * Simulated quick-synthesis "mutation" (GENERATE_QUICK_SYNTHESIS /
   * GENERATE_ACCOUNT_QUICK_SYNTHESIS). Cache-first like the server: the first
   * run for an id "generates" (~1.5–2s behind a setTimeout); reopening resolves
   * instantly from the cache. Research is synthesized deterministically from
   * the mock dataset.
   * ==================================================================== */

  const synthCache = new Map();

  function useSimulatedSynthesis(resultKey, keyOf, build) {
    const [state, setState] = useState({ data: null, loading: false, error: null });
    const alive = useRef(true);
    useEffect(() => {
      alive.current = true;
      return () => {
        alive.current = false;
      };
    }, []);
    const run = useCallback(
      (opts) =>
        new Promise((resolve, reject) => {
          const vars = (opts && opts.variables) || {};
          const key = keyOf(vars);
          const finish = (research) => {
            const data = { [resultKey]: { research } };
            if (alive.current) setState({ data, loading: false, error: null });
            resolve({ data });
          };
          if (synthCache.has(key)) {
            finish(synthCache.get(key));
            return;
          }
          if (alive.current) setState({ data: null, loading: true, error: null });
          setTimeout(() => {
            let research = null;
            try {
              research = build(vars);
            } catch (e) {
              /* fall through to null */
            }
            synthCache.set(key, research);
            if (research == null) {
              const error = new Error('NOT_FOUND');
              if (alive.current) setState({ data: null, loading: false, error });
              reject(error);
              return;
            }
            finish(research);
          }, 1500 + Math.random() * 600);
        }),
      [resultKey, keyOf, build],
    );
    return [run, state];
  }

  /** Account research built from the mock rows (server: AccountQuickSynthesis). */
  function buildAccountResearch(vars) {
    const a = T.Data.raw.acctById(vars.accountId);
    if (!a) return null;
    const evts = T.Data.raw.eventsForAccount(a.id);
    const emojiFor = (type) =>
      type === 'jobs' ? '🧑‍💻' : type === 'job_change' ? '🔄' : '📰';
    const why_now = evts.slice(0, 3).map((e) => ({
      emoji: emojiFor(e.signalType),
      label: e.title,
      text:
        (e.data && (e.data.why_it_matters || e.data.event_summary)) ||
        'Recent activity on this account.',
      source_url: e.url || undefined,
    }));
    const fit = evts
      .map((e) => (e.data && Array.isArray(e.data.offerings) ? e.data.offerings[0] : null))
      .filter((o) => o && o.offering)
      .slice(0, 2)
      .map((o) => ({ emoji: '🎯', label: o.name, text: o.offering }));
    return {
      display_name: a.name,
      one_liner: a.oneLiner || undefined,
      industry: undefined,
      hq_location: a.hqLocation || undefined,
      employee_count:
        a.employeeCount != null ? `${a.employeeCount.toLocaleString()} employees` : undefined,
      founding_year: undefined,
      why_now,
      fit,
    };
  }

  /** Person research built from the mock rows (server: QuickSynthesisResearch). */
  function buildPersonResearch(vars) {
    const p = T.Data.raw.personById(vars.personId);
    if (!p) return null;
    const co = p.account ? p.account.name : '';
    const role = p.title || 'Contact';
    const senior = /chief|cto|ciso|cfo|ceo|vp|head|director|founder/i.test(role);
    const hash = [...p.id].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7);
    const priorPool = [
      ['Google', 'Senior roles across infrastructure and product'],
      ['Amazon Web Services', 'Led platform and operations teams'],
      ['Microsoft', 'Engineering and program leadership'],
      ['Salesforce', 'Scaled go-to-market platform teams'],
      ['Oracle', 'Enterprise systems and data platforms'],
      ['Cisco', 'Security and network engineering'],
    ];
    const prior1 = priorPool[hash % priorPool.length];
    // Offset 3 in a 6-slot pool never lands on the same company as prior1.
    const prior2 = priorPool[(hash + 3) % priorPool.length];
    return {
      full_name: p.fullName,
      title: p.title || undefined,
      company: co || undefined,
      career_progression: [
        `${prior2[1]} at ${prior2[0]} (2012 – 2016)`,
        `${prior1[1]} at ${prior1[0]} (2016 – 2021)`,
        `${role} at ${co || 'their current company'} (2021 – present)`,
      ],
      professional_insights: [
        {
          emoji: '💼',
          label: 'Current focus',
          text: `${role}${co ? ` at ${co}` : ''} — owns the initiatives Trayo flagged in their signals.`,
        },
        {
          emoji: '🗣️',
          label: 'Voice',
          text: senior
            ? 'Speaks publicly about scaling their function; responsive to peer-level, outcome-first outreach.'
            : 'Hands-on practitioner; responds best to concrete, tactical value in the first line.',
        },
      ],
      challenges: [
        {
          emoji: '⚙️',
          label: 'Likely challenge',
          text: senior
            ? 'Balancing budget scrutiny against pressure to ship AI-era capabilities this fiscal year.'
            : 'Doing more with a flat headcount while new platform initiatives land on their team.',
        },
      ],
      personal_insights: [],
      company_intel: [],
    };
  }

  /* ======================================================================
   * accounts/AccountSynthesis.tsx + people/PersonSynthesis.tsx shared bits
   * (InsightRow / InsightSection / OverviewHead are byte-identical in the two
   *  source files, so they are defined once here.)
   * ==================================================================== */

  function InsightRow({ i }) {
    return (
      <li className="hdw-ai-item">
        {i.emoji && <span className="hdw-ai-emoji">{i.emoji}</span>}
        <span className="hdw-ai-body">
          {i.label && <b>{i.label}: </b>}
          {i.text}
          {i.source_url && (
            <a
              href={i.source_url}
              target="_blank"
              rel="noopener noreferrer"
              className="hdw-ai-src inline-flex"
              onClick={(e) => e.stopPropagation()}
            >
              <ExternalLink size={12} />
            </a>
          )}
        </span>
      </li>
    );
  }

  function InsightSection({ title, items }) {
    if (!items?.length) return null;
    return (
      <>
        <p className="hdw-ai-sec">{title}</p>
        <ul className="hdw-ai-list">
          {items.map((i, k) => (
            <InsightRow key={k} i={i} />
          ))}
        </ul>
      </>
    );
  }

  /** The native "Research" section header (mirrors the drawer's .hdw-ph section
   *  heads), with the expand/collapse control in the trailing slot. */
  function OverviewHead({ controls }) {
    return (
      <div className="hdw-ph">
        <span>Research</span>
        {controls}
      </div>
    );
  }

  /* ---------- accounts/AccountSynthesis.tsx ---------- */

  /** The full overview: a compact firmographic line, then the "why now" hooks and
   *  the "how they fit" angles. */
  function AccountSnapshotBody({ r }) {
    const facts = [
      r.industry,
      r.employee_count,
      r.hq_location,
      r.founding_year ? `Founded ${r.founding_year}` : '',
    ]
      .filter(Boolean)
      .join(' · ');
    return (
      <div>
        {facts && <p className="hdw-ai-facts">{facts}</p>}
        <InsightSection title="Why now" items={r.why_now} />
        <InsightSection title="How they fit" items={r.fit} />
      </div>
    );
  }

  const acctSynthKey = (vars) => `acct:${vars.accountId}`;

  /**
   * On-demand company overview of an account (account quick synthesis), rendered
   * as a native section at the bottom of the account drawer. Cache-first (~2s
   * cold); a quiet line covers the wait. Minimized by default: a clipped peek of
   * the SAME body that expands in full.
   */
  function AccountSynthesis({ accountId }) {
    const [expanded, setExpanded] = useState(false);
    const [run, { data, loading, error }] = useSimulatedSynthesis(
      'generateAccountQuickSynthesis',
      acctSynthKey,
      buildAccountResearch,
    );
    // Last account this component reported research for. StrictMode mounts an
    // effect twice in dev, and the mutation itself is cache-first, so without
    // this the request/resolve pair double-counts a paid LLM call.
    const reportedFor = useRef(null);
    useEffect(() => {
      if (!accountId) return;
      const alreadyReported = reportedFor.current === accountId;
      reportedFor.current = accountId;
      const startedAt = Date.now();
      if (!alreadyReported) {
        homeAnalytics.researchRequested({ entity: 'account', entity_id: accountId });
      }
      void run({ variables: { accountId, force: false } })
        .then(() => {
          if (alreadyReported) return;
          homeAnalytics.researchResolved({
            entity: 'account',
            entity_id: accountId,
            ok: true,
            duration_ms: Date.now() - startedAt,
          });
        })
        .catch(() => {
          if (alreadyReported) return;
          homeAnalytics.researchResolved({
            entity: 'account',
            entity_id: accountId,
            ok: false,
            duration_ms: Date.now() - startedAt,
          });
        });
    }, [accountId, run]);

    const research = data?.generateAccountQuickSynthesis?.research ?? null;

    // Unresolvable / errored with nothing to show → hide the whole section.
    if (error && !research) return null;

    if (loading && !research) {
      return (
        <div className="hdw-ai">
          <OverviewHead />
          <div className="hdw-sec-body">
            <p className="hdw-ai-note">
              <Loader2 size={13} className="animate-spin" />
              Researching…
            </p>
          </div>
        </div>
      );
    }
    if (!research) return null;

    return (
      <div className="hdw-ai">
        <OverviewHead
          controls={
            <button
              type="button"
              onClick={() => {
                homeAnalytics.drawerSectionToggled({
                  entity: 'account',
                  section: 'research',
                  expanded: !expanded,
                });
                setExpanded((x) => !x);
              }}
              className="hdw-ai-ctl"
              aria-expanded={expanded}
              aria-label={expanded ? 'Collapse research' : 'Expand research'}
            >
              <ChevronDown
                size={16}
                className={cn('transition-transform', expanded && 'rotate-180')}
              />
            </button>
          }
        />

        <div className="hdw-sec-body">
          {/* Same body minimized and expanded — peek only clips height. */}
          <div className={cn('hdw-ai-snapshot', !expanded && 'is-peek')}>
            <AccountSnapshotBody r={research} />
          </div>
          {/* Always mount the control so expand/collapse doesn't drop a line of
              height under the peek and nudge scroll anchoring. */}
          <button
            type="button"
            onClick={() => {
              homeAnalytics.drawerSectionToggled({
                entity: 'account',
                section: 'research',
                expanded: !expanded,
              });
              setExpanded((x) => !x);
            }}
            className="hdw-ai-more"
            aria-expanded={expanded}
          >
            {expanded ? 'Show less' : 'Show more'}
          </button>
        </div>
      </div>
    );
  }

  /* ---------- people/PersonSynthesis.tsx ---------- */

  /** "Role at Company (Jan 2020 – present)" → parts, for structured rendering.
   *  Tolerates either " at " or " @ " as the role/company separator, with an
   *  optional trailing "(dates)". */
  function parseStep(s) {
    const m = /^(.*?)\s+(?:at|@)\s+(.*?)(?:\s*\(([^)]*)\))?$/.exec(s.trim());
    if (!m) return { role: s, company: '', dates: '' };
    return { role: m[1] ?? s, company: m[2] ?? '', dates: m[3] ?? '' };
  }

  /** The full overview: professional / company / challenges / personal insight
   *  sections, then a compact career list (most-recent first). */
  function PersonSnapshotBody({ r }) {
    const steps = [...(r.career_progression ?? [])].reverse();
    return (
      <div>
        <InsightSection title="Professional" items={r.professional_insights} />
        <InsightSection title="Company" items={r.company_intel} />
        <InsightSection title="Likely challenges" items={r.challenges} />
        <InsightSection title="Personal" items={r.personal_insights} />
        {!!steps.length && (
          <>
            <p className="hdw-ai-sec">Career</p>
            <ul className="hdw-ai-career">
              {steps.map((s, k) => {
                const p = parseStep(s);
                return (
                  <li key={k} className="hdw-ai-job">
                    <span className="hdw-ai-job-top">
                      <span className="hdw-ai-job-co" title={p.company || p.role}>
                        {p.company || p.role}
                      </span>
                      {p.dates && (
                        <span className="hdw-ai-job-dates">{p.dates}</span>
                      )}
                    </span>
                    {p.company && p.role && (
                      <span className="hdw-ai-job-role" title={p.role}>
                        {p.role}
                      </span>
                    )}
                  </li>
                );
              })}
            </ul>
          </>
        )}
      </div>
    );
  }

  const personSynthKey = (vars) => `person:${vars.personId}`;

  /**
   * On-demand "quick synthesis" overview of a person, rendered as a native
   * section at the bottom of the person drawer — same section head, type, and
   * colors as the rest of the drawer (no card). Cache-first; minimized by
   * default with a clipped peek of the SAME SnapshotBody that expands in full.
   */
  function PersonSynthesis({ personId }) {
    const [expanded, setExpanded] = useState(false);
    const [run, { data, loading, error }] = useSimulatedSynthesis(
      'generateQuickSynthesis',
      personSynthKey,
      buildPersonResearch,
    );
    // See AccountSynthesis: StrictMode runs this effect twice in dev, which would
    // double-count a paid LLM call.
    const reportedFor = useRef(null);
    useEffect(() => {
      if (!personId) return;
      const alreadyReported = reportedFor.current === personId;
      reportedFor.current = personId;
      const startedAt = Date.now();
      if (!alreadyReported) {
        homeAnalytics.researchRequested({ entity: 'person', entity_id: personId });
      }
      void run({ variables: { personId, grounded: false, force: false } })
        .then(() => {
          if (alreadyReported) return;
          homeAnalytics.researchResolved({
            entity: 'person',
            entity_id: personId,
            ok: true,
            duration_ms: Date.now() - startedAt,
          });
        })
        .catch(() => {
          if (alreadyReported) return;
          homeAnalytics.researchResolved({
            entity: 'person',
            entity_id: personId,
            ok: false,
            duration_ms: Date.now() - startedAt,
          });
        });
    }, [personId, run]);

    const research = data?.generateQuickSynthesis?.research ?? null;

    // Graceful degrade for contacts not yet enriched (no member id). Only take
    // over when we have nothing to show.
    if (error && !research) {
      const noId = /no CoreSignal member id/i.test(error.message);
      return (
        <div className="hdw-ai">
          <OverviewHead />
          <div className="hdw-sec-body">
            <p className="hdw-ai-note">
              {noId
                ? 'Research isn’t available for this contact yet.'
                : 'Research failed to load.'}
            </p>
          </div>
        </div>
      );
    }

    if (loading && !research) {
      return (
        <div className="hdw-ai">
          <OverviewHead />
          <div className="hdw-sec-body">
            <p className="hdw-ai-note">
              <Loader2 size={13} className="animate-spin" />
              Researching…
            </p>
          </div>
        </div>
      );
    }
    if (!research) return null;

    return (
      <div className="hdw-ai">
        <OverviewHead
          controls={
            <button
              type="button"
              onClick={() => {
                homeAnalytics.drawerSectionToggled({
                  entity: 'person',
                  section: 'research',
                  expanded: !expanded,
                });
                setExpanded((x) => !x);
              }}
              className="hdw-ai-ctl"
              aria-expanded={expanded}
              aria-label={expanded ? 'Collapse research' : 'Expand research'}
            >
              <ChevronDown
                size={16}
                className={cn('transition-transform', expanded && 'rotate-180')}
              />
            </button>
          }
        />

        <div className="hdw-sec-body">
          {/* Same body minimized and expanded — peek only clips height. */}
          <div className={cn('hdw-ai-snapshot', !expanded && 'is-peek')}>
            <PersonSnapshotBody r={research} />
          </div>
          {/* Always mount the control so expand/collapse doesn't drop a line of
              height under the peek and nudge scroll anchoring. */}
          <button
            type="button"
            onClick={() => {
              homeAnalytics.drawerSectionToggled({
                entity: 'person',
                section: 'research',
                expanded: !expanded,
              });
              setExpanded((x) => !x);
            }}
            className="hdw-ai-more"
            aria-expanded={expanded}
          >
            {expanded ? 'Show less' : 'Show more'}
          </button>
        </div>
      </div>
    );
  }

  /* ======================================================================
   * accounts/AccountDrawer.tsx
   * ==================================================================== */

  function researchDescription(researchInfo) {
    if (!researchInfo || typeof researchInfo !== 'object') {
      // The mock dataset stores researchInfo as a plain paragraph string —
      // accept it too so the drawer's About section renders it.
      if (typeof researchInfo === 'string' && researchInfo.trim()) {
        return researchInfo.trim();
      }
      return null;
    }
    const r = researchInfo;
    const company =
      r.company && typeof r.company === 'object' ? r.company : null;
    // Top-level keys (full agent research) win; then the CoreSignal pre-fill's
    // NESTED research_info.company.{description,one_liner}.
    const candidates = [
      r.company_description,
      r.description,
      r.overview,
      r.summary,
      company?.description,
      company?.one_liner,
    ];
    for (const v of candidates) {
      if (typeof v === 'string' && v.trim()) return v.trim();
    }
    return null;
  }

  function AccountDrawer({
    account,
    onClose,
    onBack,
    backLabel,
    onOpenSignal,
    onOpenPerson,
  }) {
    const PersonPickCard = T.PersonPickCard;
    const pb = T.useAudience();
    const [showAll, setShowAll] = useState(false);
    const [showAllSignals, setShowAllSignals] = useState(false);
    const { data, refetch: refetchDrawer } = T.Data.useHomeAccountDrawer({
      id: account.id,
    });

    // TRA-1359: per-account discovery-stage flags, polled while either stage is
    // still running so the drawer's People / signals sections resolve live.
    const {
      data: countsData,
      startPolling: startCountsPolling,
      stopPolling: stopCountsPolling,
    } = T.Data.useHomeAccountCounts({ ids: [account.id] });
    const status = countsData?.accountCounts?.[0];
    const contactsDiscovering = status?.contactsDiscovering ?? false;
    const signalsDiscovering = status?.signalsDiscovering ?? false;
    const discovering = contactsDiscovering || signalsDiscovering;
    useEffect(() => {
      if (discovering) startCountsPolling(5000);
      else stopCountsPolling();
      return () => stopCountsPolling();
    }, [discovering, startCountsPolling, stopCountsPolling]);
    // When a stage completes, pull the drawer payload once more so the newly
    // landed people/events replace the discovering note without a reopen.
    useEffect(() => {
      if (!discovering) return;
      return () => void refetchDrawer();
    }, [discovering, refetchDrawer]);

    const detail = data?.account ?? account;
    const events = useMemo(() => data?.events ?? [], [data?.events]);
    const people = useMemo(
      () =>
        (data?.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 ?? account.id,
          // TRA-1359 step 8: threads the contacted rollup to the pick cards.
          lastContactedAt: p.lastContactedAt ?? null,
          lastContactedByName: p.lastContactedByName ?? null,
          contactedByMe: p.contactedByMe ?? false,
        })),
      [data?.people, account.id],
    );

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

    const ranked = useMemo(
      () => T.rankAccountPeople(people, eventCtx),
      [people, eventCtx],
    );
    const top = ranked.slice(0, 4);
    const rest = ranked.slice(4);
    // The header badge counts every signal, so the list must be able to reach all
    // of them; cap the first fold at 3 and put the remainder behind a toggle.
    const topEvents = events.slice(0, 3);
    const restEvents = events.slice(3);

    const acctIn = pb.hasAccount(account.id);
    const pickedN = pb.pickedCount(account.id);
    const allIn = people.length > 0 && people.every((p) => pb.hasPerson(p.id));

    const facts = [
      ['Employees', detail.employeeCount != null ? detail.employeeCount.toLocaleString() : '-'],
      [
        'Revenue',
        formatRevenue(
          detail.annualRevenueFrom != null
            ? { from: detail.annualRevenueFrom, to: detail.annualRevenueTo ?? null }
            : null,
        ) || '-',
      ],
      ['Type', detail.ownership || '-'],
      ['HQ', detail.hqLocation || '-'],
    ];
    const about =
      researchDescription(detail.researchInfo) || detail.oneLiner || null;
    // Website only: `accounts.linkedin_handle` is null for nearly every account,
    // so a LinkedIn icon here would mostly resolve to a search page.
    const website = absoluteUrl(detail.url);

    const toPick = (p) => ({
      id: p.id,
      fullName: p.fullName,
      title: p.title,
      profileImageUrl: p.profileImageUrl,
      // TRA-1359 step 8: lets the card show the recent-outreach chip.
      lastContactedAt: p.lastContactedAt,
      lastContactedByName: p.lastContactedByName,
      contactedByMe: p.contactedByMe,
    });

    return (
      <>
        <DrawerHeader
          onClose={onClose}
          back={onBack ? { label: backLabel ?? 'Back', onClick: onBack } : undefined}
          actions={
            detail.timingScore != null && detail.timingScore > 8 ? (
              <span className="hdw-score">Score {detail.timingScore}</span>
            ) : undefined
          }
        >
          <LogoAvatar
            src={detail.logoUrl}
            domain={detail.url}
            alt={detail.name}
            fallbackText={getInitials(detail.name)}
            size="md"
            className="size-[32px] rounded-[8px]"
          />
          <span className="hdw-title">
            <span className="hdw-title-name">{detail.name}</span>
          </span>
          {website && (
            <DrawerRefLinks>
              <DrawerRefLink
                href={website}
                icon={Globe}
                label={`${detail.name} website (${websiteLabel(website)})`}
              />
            </DrawerRefLinks>
          )}
        </DrawerHeader>

        <DrawerBody>
          <div className="hdw-sigbody">
            <div className="hdw-why">
              <b>About</b>
              {about && <p>{about}</p>}
              <span className="hdw-facts">
                {facts
                  .filter(([, v]) => v && v !== '-')
                  .map(([k, v]) => (
                    <span key={k}>
                      <i>{k}</i>
                      <b>{v}</b>
                    </span>
                  ))}
              </span>
            </div>
          </div>

          <div className="hdw-ph">
            <span>People</span>
            <span className="hdw-ct">{people.length}</span>
            {contactsDiscovering && (
              <Loader2
                className="size-3 shrink-0 animate-spin text-text-muted"
                aria-label="Finding stakeholders"
              />
            )}
            <span className="flex-1" />
            {people.length > 1 && (
              <button
                type="button"
                className={`hdw-fcadd${allIn ? ' is-added' : ''}`}
                onClick={() =>
                  people.forEach((p) =>
                    allIn
                      ? pb.removePerson(p.id)
                      : pb.addPerson(account.id, p.id, 'manual', undefined, undefined, {
                          // Roster already has the person's display data — pass it
                          // so the bar paints the real face/name first (TRA-1359).
                          fullName: p.fullName,
                          title: p.title,
                          profileImageUrl: p.profileImageUrl,
                        }),
                  )
                }
              >
                {allIn ? (
                  <>
                    <Check />
                    All added
                  </>
                ) : (
                  <>
                    <UserPlus />
                    {people.length === 2 ? 'Add both' : `Add all ${people.length}`}
                  </>
                )}
              </button>
            )}
          </div>
          <div className="hdw-cards">
            {top.map((p) => (
              <PersonPickCard
                key={p.id}
                accountId={account.id}
                person={toPick(p)}
                why={T.whyFor(p, detail.name, eventCtx)}
                via="manual"
                onOpen={(person) => onOpenPerson(person, account.id)}
              />
            ))}
            {people.length === 0 && (
              <p className="hdw-ranknote">
                {contactsDiscovering
                  ? 'Trayo is finding stakeholders. They appear here as they land.'
                  : 'No people yet. Trayo is researching stakeholders.'}
              </p>
            )}
          </div>
          {rest.length > 0 && (
            <>
              <button
                type="button"
                className="hdw-more mt-1"
                onClick={() => {
                  homeAnalytics.drawerSectionToggled({
                    entity: 'account',
                    section: 'stakeholders',
                    expanded: !showAll,
                    hidden_count: rest.length,
                  });
                  setShowAll((s) => !s);
                }}
              >
                {showAll ? 'Hide other stakeholders' : `Show ${rest.length} more`}
                <span className={`hdw-twist${showAll ? ' rot' : ''}`}>
                  <ChevronDown />
                </span>
              </button>
              {showAll && (
                <div className="hdw-cards mt-2">
                  {rest.map((p) => (
                    <PersonPickCard
                      key={p.id}
                      accountId={account.id}
                      person={toPick(p)}
                      why={T.whyFor(p, detail.name, eventCtx)}
                      via="manual"
                      onOpen={(person) => onOpenPerson(person, account.id)}
                    />
                  ))}
                </div>
              )}
            </>
          )}

          <div className="hdw-ph">
            <span>Recent signals</span>
            <span className="hdw-ct">{events.length}</span>
            {signalsDiscovering && (
              <Loader2
                className="size-3 shrink-0 animate-spin text-text-muted"
                aria-label="Discovery running"
              />
            )}
          </div>
          <div className="hdw-cards">
            {topEvents.map((ev) => (
              <AccountSigCard key={ev.id} event={ev} onClick={() => onOpenSignal(ev)} />
            ))}
            {events.length === 0 && (
              <p className="hdw-ranknote">
                {signalsDiscovering
                  ? 'Initial discovery is running, signals appear here as they complete.'
                  : 'No signals yet. Trayo is monitoring this account.'}
              </p>
            )}
          </div>
          {restEvents.length > 0 && (
            <>
              <button
                type="button"
                className="hdw-more mt-1"
                onClick={() => {
                  homeAnalytics.drawerSectionToggled({
                    entity: 'account',
                    section: 'signals',
                    expanded: !showAllSignals,
                    hidden_count: restEvents.length,
                  });
                  setShowAllSignals((s) => !s);
                }}
              >
                {showAllSignals
                  ? 'Hide other signals'
                  : `Show ${restEvents.length} more`}
                <span className={`hdw-twist${showAllSignals ? ' rot' : ''}`}>
                  <ChevronDown />
                </span>
              </button>
              {showAllSignals && (
                <div className="hdw-cards mt-2">
                  {restEvents.map((ev) => (
                    <AccountSigCard
                      key={ev.id}
                      event={ev}
                      onClick={() => onOpenSignal(ev)}
                    />
                  ))}
                </div>
              )}
            </>
          )}

          <OutreachSection entity="account" id={account.id} />

          {/* On-demand research — bottom-most so About/people/signals lead.
              Minimized shows a clipped peek of the SAME body as expanded.
              `key` remounts on account switch so a stale brief can't linger. */}
          <AccountSynthesis key={account.id} accountId={account.id} />
        </DrawerBody>

        <div className="hdw-foot">
          <DrawerDeleteAction
            entity="account"
            id={account.id}
            name={detail.name}
            onClose={onClose}
          />
          <span className="flex-1" />
          <button
            type="button"
            className={`hdw-cta sm${acctIn ? ' ghost is-added' : ' second'}`}
            onClick={() =>
              acctIn
                ? pb.removeGroup(account.id)
                : pb.addAccount(account.id, 'account-drawer', {
                    name: detail.name,
                    url: detail.url ?? null,
                    logoUrl: detail.logoUrl ?? null,
                  })
            }
            title={
              acctIn
                ? 'Remove this account and its picks from the list'
                : 'Adds the whole account: Trayo picks the contacts, adjust anytime'
            }
          >
            {acctIn ? (
              <>
                <Check />
                {detail.name} added · {pickedN} {pickedN === 1 ? 'contact' : 'contacts'}
              </>
            ) : (
              <>
                <UserPlus />
                Add {detail.name}
              </>
            )}
          </button>
        </div>
      </>
    );
  }

  /* ======================================================================
   * people/PersonContactFacts.tsx
   * ==================================================================== */

  // Billing / entitlements are not ported: the fixture tenant is unlimited, so
  // the upgrade-modal branches below are kept but unreachable.
  const FIXTURE_ENTITLEMENTS = {
    canGetPersonEmail: true,
    findEmailLimit: null,
    findPhoneLimit: null,
  };
  const FIXTURE_USAGE = { emailsLookedUp: 0, phonesLookedUp: 0 };
  const showUpgradeModal = () => {};

  const extractCapErrorCode = (error) => {
    const m =
      /\b(EMAIL_LOOKUP_CAP_REACHED|PHONE_LOOKUP_CAP_REACHED|EMAIL_LOOKUP_DISABLED|PHONE_LOOKUP_DISABLED)\b/.exec(
        (error && error.message) || '',
      );
    return m ? m[1] : null;
  };
  const capErrorToTrigger = (code) =>
    code === 'EMAIL_LOOKUP_CAP_REACHED'
      ? 'findEmailLimit'
      : code === 'PHONE_LOOKUP_CAP_REACHED'
        ? 'findPhoneLimit'
        : code === 'EMAIL_LOOKUP_DISABLED'
          ? 'canGetPersonEmail'
          : code === 'PHONE_LOOKUP_DISABLED'
            ? 'canGetPersonPhone'
            : null;
  const shouldSuppressPaywallError = () => false;

  function PersonContactFacts({ personId, title }) {
    // usePermissions → fixture user holds every permission.
    const has = () => true;
    const entitlements = FIXTURE_ENTITLEMENTS;
    const usage = FIXTURE_USAGE;
    const [emailIssue, setEmailIssue] = useState(null);
    const [phoneIssue, setPhoneIssue] = useState(null);

    const refreshUsage = useCallback(() => {}, []);

    const onEmailCompleted = useCallback(
      (outcome, latencyMs, hasEmail) => {
        setEmailIssue(null);
        captureEvent('client.find_email.completed', {
          personId,
          entryPoint: 'home_person_drawer',
          outcome,
          latencyMs,
          hasEmail,
        });
        refreshUsage();
      },
      [personId, refreshUsage],
    );
    const onPhoneCompleted = useCallback(
      (outcome, latencyMs, hasPhone) => {
        setPhoneIssue(null);
        captureEvent('client.find_phone.completed', {
          personId,
          entryPoint: 'home_person_drawer',
          outcome,
          latencyMs,
          hasPhone,
        });
        refreshUsage();
      },
      [personId, refreshUsage],
    );

    const showMutationIssue = useCallback(
      (kind, error) => {
        const code = extractCapErrorCode(error);
        const trigger = capErrorToTrigger(code);
        captureEvent(`client.find_${kind}.mutation_failed`, {
          personId,
          entryPoint: 'home_person_drawer',
          errorCode: code,
          errorMessage: error.message,
        });
        if (shouldSuppressPaywallError(error)) return;
        const label = friendlyMutationIssue(kind, error);
        if (kind === 'email') setEmailIssue(label);
        else setPhoneIssue(label);

        if (
          trigger === 'findEmailLimit' ||
          trigger === 'canGetPersonEmail' ||
          trigger === 'findPhoneLimit' ||
          trigger === 'canGetPersonPhone'
        ) {
          showUpgradeModal({ trigger });
          return;
        }
        toast.error(label);
      },
      [personId],
    );

    const onEmailError = useCallback(
      (error) => showMutationIssue('email', error),
      [showMutationIssue],
    );
    const onPhoneError = useCallback(
      (error) => showMutationIssue('phone', error),
      [showMutationIssue],
    );

    const email = useFindEmail(personId, {
      onCompleted: onEmailCompleted,
      onError: onEmailError,
    });
    const phone = useFindPhone(personId, {
      onCompleted: onPhoneCompleted,
      onError: onPhoneError,
    });

    const emailAllowed = has('people:enrich_email');
    const phoneAllowed = has('people:enrich_phone');
    const capabilityDisabled = entitlements?.canGetPersonEmail === false;
    const emailLimit = entitlements?.findEmailLimit ?? null;
    const phoneLimit = entitlements?.findPhoneLimit ?? null;
    const emailsUsed = usage?.emailsLookedUp ?? 0;
    const phonesUsed = usage?.phonesLookedUp ?? 0;

    const triggerEmail = useCallback(() => {
      if (!emailAllowed) return;
      setEmailIssue(null);
      captureEvent('client.find_email.pill_clicked', {
        personId,
        entryPoint: 'home_person_drawer',
        priorState: email.state,
      });
      if (capabilityDisabled) {
        showUpgradeModal({ trigger: 'canGetPersonEmail' });
        return;
      }
      if (emailLimit !== null && emailsUsed >= emailLimit) {
        setEmailIssue('Email lookup limit reached');
        showUpgradeModal({
          trigger: 'findEmailLimit',
          context: { used: emailsUsed, limit: emailLimit },
        });
        return;
      }
      if (email.canTrigger) void email.trigger('manual_profile');
    }, [emailAllowed, personId, email, capabilityDisabled, emailLimit, emailsUsed]);

    const triggerPhone = useCallback(() => {
      if (!phoneAllowed) return;
      setPhoneIssue(null);
      captureEvent('client.find_phone.pill_clicked', {
        personId,
        entryPoint: 'home_person_drawer',
        priorState: phone.state,
      });
      if (capabilityDisabled) {
        showUpgradeModal({ trigger: 'canGetPersonPhone' });
        return;
      }
      if (phoneLimit !== null && phonesUsed >= phoneLimit) {
        setPhoneIssue('Phone lookup limit reached');
        showUpgradeModal({
          trigger: 'findPhoneLimit',
          context: { used: phonesUsed, limit: phoneLimit },
        });
        return;
      }
      if (phone.canTrigger) void phone.trigger('manual_profile');
    }, [phoneAllowed, personId, phone, capabilityDisabled, phoneLimit, phonesUsed]);

    return (
      <span className="hdw-facts">
        <span>
          <i>Role</i>
          <b>{title || '-'}</b>
        </span>
        <ContactFact
          kind="email"
          value={email.email}
          state={email.state}
          retryAvailableAt={email.retryAvailableAt}
          issue={emailIssue}
          loading={email.loading}
          allowed={emailAllowed}
          canTrigger={email.canTrigger}
          onTrigger={triggerEmail}
        />
        <ContactFact
          kind="phone"
          value={phone.phone}
          state={phone.state}
          retryAvailableAt={phone.retryAvailableAt}
          issue={phoneIssue}
          loading={phone.loading}
          allowed={phoneAllowed}
          canTrigger={phone.canTrigger}
          onTrigger={triggerPhone}
        />
      </span>
    );
  }

  function ContactFact({
    kind,
    value,
    state,
    retryAvailableAt,
    issue,
    loading,
    allowed,
    canTrigger,
    onTrigger,
  }) {
    const noun = kind === 'email' ? 'Email' : 'Phone';

    if (value) {
      return (
        <span>
          <i>{noun}</i>
          <b className="ok">
            <Check />
            <span className="max-w-48 truncate" title={value}>
              {value}
            </span>
          </b>
        </span>
      );
    }

    const status = contactStatus(kind, state, issue);
    const showAction =
      allowed &&
      (state === 'not_requested' ||
        state === 'in_progress' ||
        state === 'found' ||
        state === 'error' ||
        state === 'bounced' ||
        (state === 'not_found' && canTrigger));

    return (
      <span>
        <i>{noun}</i>
        {status && (
          <span
            className={
              issue || state === 'error' || state === 'bounced'
                ? 'text-meta text-destructive'
                : 'text-meta text-text-muted'
            }
            title={
              state === 'not_found' && retryAvailableAt
                ? `Retry available ${retryAvailableAt.toLocaleString()}`
                : undefined
            }
          >
            {status}
          </span>
        )}
        {showAction ? (
          <Button
            type="button"
            size="xs"
            variant="secondary"
            className="w-28 justify-center"
            loading={state === 'in_progress'}
            disabled={loading || !canTrigger}
            onClick={onTrigger}
          >
            Enrich {kind}
          </Button>
        ) : (
          !status && (
            <span className="text-meta text-text-muted">
              {allowed ? `No ${kind} yet` : 'Not available'}
            </span>
          )
        )}
      </span>
    );
  }

  function contactStatus(kind, state, issue) {
    if (issue) return issue;
    const noun = kind === 'email' ? 'Email' : 'Phone';
    if (state === 'not_found') return `${noun} not found`;
    if (state === 'error') return `Couldn’t enrich ${kind}`;
    if (state === 'bounced') {
      return kind === 'email' ? 'Email bounced' : 'Phone unavailable';
    }
    if (state === 'found') return `Couldn’t enrich ${kind}`;
    return null;
  }

  function friendlyMutationIssue(kind, error) {
    const noun = kind === 'email' ? 'Email' : 'Phone';
    const code = extractCapErrorCode(error);
    if (code === 'EMAIL_LOOKUP_CAP_REACHED') return 'Email lookup limit reached';
    if (code === 'PHONE_LOOKUP_CAP_REACHED') return 'Phone lookup limit reached';
    if (code === 'EMAIL_LOOKUP_DISABLED') {
      return 'Email lookup isn’t available on this plan';
    }
    if (code === 'PHONE_LOOKUP_DISABLED') {
      return 'Phone lookup isn’t available on this plan';
    }
    if (/Missing permission/i.test(error.message)) {
      return `You don’t have permission to enrich ${kind}`;
    }
    if (/\bRETRY_TOO_SOON\b/.test(error.message)) {
      return `${noun} can’t be retried yet`;
    }
    return `Couldn’t start ${kind} enrichment`;
  }

  /* The ui.jsx useFindEmail/useFindPhone shims resolve through
   * T.Data.resolveFindEmail / resolveFindPhone when present. data.jsx does not
   * define them, so provide them here: peek seeds the pill from the person row's
   * stored enrichment state; a trigger enriches the row in place (and bumps the
   * dataset so the tables' Enriched cells update too). */
  if (T.Data && !T.Data.resolveFindEmail) {
    T.Data.resolveFindEmail = (personId, opts = {}) => {
      const p = T.Data.raw.personById(personId);
      if (!p) return null;
      if (opts.peek) {
        return {
          initialState: p.email ? 'found' : p.enrichmentState || 'not_requested',
          email: p.email || p.deliverableEmail || null,
        };
      }
      if (p.enrichmentState === 'not_found') return { email: null, outcome: 'not_found' };
      if (!p.email) {
        const domain = p.account
          ? p.account.url.replace('https://', '')
          : 'example.com';
        p.email = `${p.fullName.toLowerCase().replace(/[^a-z ]/g, '').trim().replace(/ +/g, '.')}@${domain}`;
      }
      p.deliverableEmail = p.email;
      p.enrichmentState = 'found';
      T.Data.raw.bump();
      return { email: p.email, outcome: 'found' };
    };
  }
  if (T.Data && !T.Data.resolveFindPhone) {
    T.Data.resolveFindPhone = (personId, opts = {}) => {
      const p = T.Data.raw.personById(personId);
      if (!p) return null;
      if (opts.peek) {
        return {
          initialState: p.deliverablePhone
            ? 'found'
            : p.phoneEnrichmentState || 'not_requested',
          phone: p.deliverablePhone || null,
        };
      }
      if (p.phoneEnrichmentState === 'not_found') return { phone: null, outcome: 'not_found' };
      if (!p.deliverablePhone) {
        const hash = [...p.id].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7);
        p.deliverablePhone = `+1 415 555 0${100 + (hash % 900)}`;
      }
      p.phoneEnrichmentState = 'found';
      T.Data.raw.bump();
      return { phone: p.deliverablePhone, outcome: 'found' };
    };
  }

  /* ======================================================================
   * people/EnrichedCell.tsx
   * ==================================================================== */

  /**
   * The `Enriched` column: do we hold this person's email and phone?
   * Both icons always render — the absence IS the answer here.
   */
  function EnrichedCell({ hasEmail, hasPhone }) {
    return (
      <span className="flex items-center gap-1">
        <EnrichedChip
          on={hasEmail}
          label={hasEmail ? 'Email found' : 'No email yet'}
        >
          <Mail className="size-3.5" />
        </EnrichedChip>
        <EnrichedChip
          on={hasPhone}
          label={hasPhone ? 'Phone found' : 'No phone yet'}
        >
          <Phone className="size-3.5" />
        </EnrichedChip>
      </span>
    );
  }

  function EnrichedChip({ on, label, children }) {
    return (
      <span
        role="img"
        aria-label={label}
        title={label}
        className={cn(
          'inline-flex size-5 shrink-0 items-center justify-center',
          on ? 'text-accent-text' : 'text-text-muted opacity-40',
        )}
      >
        {children}
      </span>
    );
  }

  /* ======================================================================
   * people/PersonDrawer.tsx
   * ==================================================================== */

  function PersonDrawer({
    person,
    onClose,
    onBack,
    backLabel,
    onOpenSignal,
    onOpenAccount,
  }) {
    const pb = T.useAudience();
    const { data } = T.Data.useHomePersonDrawer({ id: person.id });
    const detail = data?.person;
    const account = detail?.account;
    const accountId = detail?.accountId ?? account?.id ?? '';
    const on = pb.hasPerson(person.id);
    const linkedin = linkedinPersonUrl(detail?.linkedinUsername);

    // Person.eventPeople.event is a reduced type (no account/signalType/people),
    // so fetch the FULL events by id - the cards then show the right pill and
    // framing, and the person→signal hop opens a complete drawer.
    const eventIds = useMemo(
      () =>
        (detail?.eventPeople ?? [])
          .filter((ep) => ep.event && ep.event.status === 'completed')
          .slice(0, 5)
          .map((ep) => ep.event.id),
      [detail?.eventPeople],
    );
    // Mock hook has no `skip` — an empty eventIds filter would return every
    // event, so gate the result instead.
    const { data: eventsData } = T.Data.useHomeSignalsByIds({ eventIds });
    const signals = useMemo(
      () => (eventIds.length === 0 ? [] : (eventsData?.events ?? [])),
      [eventsData, eventIds],
    );

    // Same lightweight account card query the SignalDrawer uses — one-liner +
    // completed-signal count for the "N signals on this account" line.
    const { data: acctCardData } = T.Data.useAccountSignalCount({
      accountId,
      accountIdId: accountId,
    });
    const accountOneLiner = acctCardData?.account?.oneLiner ?? null;
    const accountSignalCount = acctCardData?.eventsCount ?? 0;

    const why = useMemo(() => {
      const reasoning = (detail?.eventPeople ?? []).find(
        (ep) => ep.reasoning,
      )?.reasoning;
      if (reasoning) return reasoning;
      // No real event reason: just the title (or nothing) — no "senior contact
      // at {account}" filler, which restated the account for every contact.
      return person.title ?? '';
    }, [detail?.eventPeople, person.title]);

    return (
      <>
        <DrawerHeader
          onClose={onClose}
          back={
            onBack ? { label: backLabel ?? 'Back', onClick: onBack } : undefined
          }
        >
          <PersonAvatar
            src={person.profileImageUrl}
            personId={person.id}
            name={person.fullName}
            className="size-[32px]"
          />
          <span className="hdw-title">
            <span className="hdw-title-name">{person.fullName}</span>
          </span>
          {linkedin && (
            <DrawerRefLinks>
              <DrawerRefLink
                href={linkedin}
                icon={Linkedin}
                label={`${person.fullName} on LinkedIn`}
              />
            </DrawerRefLinks>
          )}
        </DrawerHeader>

        <DrawerBody>
          {account && (
            <AccountObjectCard
              account={account}
              oneLiner={accountOneLiner}
              otherSignals={accountSignalCount}
              signalCountMode="total"
              onClick={() => onOpenAccount?.(account)}
            />
          )}

          {/* Native section header so the signal-derived reason to reach out
              reads as a distinct section. This is the drawer's hero section — it
              keeps the accent rail (`is-hero`); Research sits at the bottom. */}
          <div className="hdw-ph">
            <span>Why reach out</span>
          </div>
          <div className="hdw-why hdw-sec-body is-hero">
            <p>{why}</p>
            <PersonContactFacts personId={person.id} title={person.title} />
          </div>

          {signals.length > 0 && (
            <>
              <div className="hdw-ph">
                <span>Their signals</span>
                <span className="hdw-ct">{signals.length}</span>
              </div>
              <div className="hdw-cards hdw-sec-body">
                {signals.map((ev) => (
                  <AccountSigCard
                    key={ev.id}
                    event={ev}
                    onClick={() => onOpenSignal(ev)}
                  />
                ))}
              </div>
            </>
          )}

          <OutreachSection entity="person" id={person.id} />

          {/* On-demand research — bottom-most so why/signals lead. `key`
              remounts on person switch so stale research can't linger under the
              new person. */}
          <PersonSynthesis key={person.id} personId={person.id} />
        </DrawerBody>

        <div className="hdw-foot">
          <DrawerDeleteAction
            entity="person"
            id={person.id}
            name={person.fullName}
            onClose={onClose}
          />
          <span className="flex-1" />
          <button
            type="button"
            className={`hdw-cta sm${on ? ' ghost is-added' : ' second'}`}
            disabled={!on && !accountId}
            onClick={() =>
              on
                ? pb.removePerson(person.id)
                : pb.addPerson(
                    accountId,
                    person.id,
                    'person-drawer',
                    undefined,
                    undefined,
                    {
                      // Drawer already has the person's display data — pass it so
                      // the bar paints the real face/name first frame (TRA-1359).
                      fullName: person.fullName,
                      title: person.title,
                      profileImageUrl: person.profileImageUrl,
                    },
                  )
            }
          >
            {on ? (
              <>
                <Check />
                {person.fullName.split(' ')[0]} added
              </>
            ) : (
              <>
                <UserPlus />
                Add {person.fullName.split(' ')[0]}
              </>
            )}
          </button>
        </div>
      </>
    );
  }

  /* ======================================================================
   * exports
   * ==================================================================== */

  Object.assign(T, {
    // drawer shell + chrome
    DrawerCloseContext,
    useDrawerClose,
    DrawerShell,
    DrawerHeader,
    DrawerBody,
    DrawerFooter,
    DrawerDeleteAction,
    DrawerRefLinks,
    DrawerRefLink,
    absoluteUrl,
    websiteLabel,
    linkedinPersonUrl,
    // outreach
    OutreachSection,
    OUTREACH_VISIBLE,
    // signals
    SignalDrawer,
    signalRichFacts,
    SignalFitCell,
    SignalDetailsCell,
    AccountSigCard,
    AccountObjectCard,
    displayOneLiner,
    AddAccountSuggestion,
    signalRowFacts,
    signalRowAdd,
    signalRowFullyAdded,
    signalRowCheckState,
    signalRowSelected,
    signalRowRemovablePersonIds,
    signalRowPersonOwnershipReleases,
    planSignalRowAdd,
    eventsToAudienceBatch,
    // accounts
    AccountDrawer,
    AccountSynthesis,
    // people
    PersonDrawer,
    PersonContactFacts,
    PersonSynthesis,
    EnrichedCell,
  });
})();
