# Lead table

> A prospecting table that shows who is ready to buy: intent cards that filter in one click, bulk actions, a company drawer and Undo on every change.

A sales prospecting table for the companies that fit your customer profile. Three intent cards count the High, Medium and Low ones with a strip of dots, and pressing one filters the table to that level. Search, filter by status or industry, show your own accounts, pick the columns, sort and page. Select rows to delete, export, set a status, assign an owner or save them to a list; open a company for its firmographics, headcount, contacts and buying signals; hand one teammate’s open accounts to another, or add a company. Each change reports itself in a toast whose Undo puts it back.

A React component by Bruno Delorence: https://brunodelorence.com/components/lead-table. MIT License, Copyright (c) 2026 Bruno Delorence.

Category: Data.

## Anatomy

1. **Header**: The title and a line on who is in the table, with Hand Over and the dark Add Company.
2. **Intent card**: A level’s count, a strip of dots lit for its share, the percent, and how many lack an owner. Pressed, it filters the table to that level.
3. **Toolbar**: Search as you type, Filter by status and industry, My Accounts, Clear when anything filters, Columns and the view options.
4. **Company tile**: A small square with the company’s mark, beside its name. The name opens the drawer.
5. **Status chip**: New, Researching, Contacted, Qualified or Disqualified, each its colour with a dot. It opens the status menu.
6. **Owner**: An initials avatar and first name, or Assign for nobody. It opens the owner menu.
7. **Pages**: Rows per page, the range shown out of the companies the filters leave, and the page buttons.

## When to use

- A list of accounts a sales team works through, where the first question is which ones are ready to buy.
- Twenty to a few hundred companies, scored, with owners and a status each, paged ten to thirty at a time.
- Work done in batches: pick a few rows, then set their status, assign them, export them or save them to a list.
- Changes that should be easy to take back: each one reports itself in a toast with Undo.
- A main view, about 900 to 1,100 px wide; on a phone the table scrolls sideways inside its own box.

## When not to use

- Thousands of rows, loaded from a server as the reader scrolls. Use a virtualised grid with server sorting.
- Cells edited one by one like a spreadsheet. Use a data grid with inline editing.
- A single account’s full history. Use a record page with a timeline.
- A handful of items with nothing to score, assign or filter. Use a simple list.
- Reports and trends across the pipeline. Use charts and a dashboard.

## Installation

Install it with the shadcn CLI, into components/lead-table of any React project:

```sh
npx shadcn@latest add https://brunodelorence.com/r/lead-table.json
```

Or add the registry to components.json once, as a namespace that coding agents and the shadcn MCP server search by name:

```json
{
  "registries": {
    "@brunodelorence": "https://brunodelorence.com/r/{name}.json"
  }
}
```

```sh
npx shadcn@latest add @brunodelorence/lead-table
```

Or copy all the source files below into one folder of your project. Either way it needs React 19 and a bundler with CSS Modules (Next.js, Vite).

It has a dark colour scheme. Its colours are CSS light-dark() pairs, kept inside @supports so that builds which rewrite light-dark() for older browsers leave them alone: it follows your page’s color-scheme (light unless the page sets one) or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

## Usage

```tsx
'use client';

import { LeadTable, type Company } from './lead-table';

const companies: Company[] = [
  {
    id: 'acme',
    name: 'Northwind Parts',
    domain: 'northwindparts.com',
    icon: 'factory',
    industry: 'Manufacturing',
    city: 'Munich',
    country: 'DE',
    growth: 12,
    size: 420,
    intent: 81,
    contacts: 36,
    status: 'new',
    owner: null,
    funding: 'Series B',
    revenue: 58,
    founded: 2011,
  },
];

export function Prospects() {
  return (
    <LeadTable
      companies={companies}
      onStatusChange={(list, status) => console.log(list.map((c) => c.id), status)}
      onAssign={(list, owner) => console.log(list.map((c) => c.id), owner?.id)}
      autoplay={false}
    />
  );
}
```

## States

- **All companies**: Sorted by name, ten to a page, every intent card unpressed.
- **High intent only**: The High card is pressed: the table keeps the ten companies scored 70 and up, and Clear shows in the toolbar.
- **Rows selected**: Two rows checked: the bulk bar replaces the toolbar with their contacts, average intent and the batch actions.
- **Company drawer**: A company’s name opened its drawer over the blurred table: firmographics, headcount and outreach.
- **Hand over**: One owner’s open accounts, ready to move to another; closed ones stay put.
- **Add company**: The form for a new company. It lands in the table as New, lit for a moment.
- **No match**: A search nothing matches: the table says so and offers to clear the filters.

## How it works

- The table is one pipeline over the rows: the search (name, industry, city or domain), the intent level, My Accounts, and the status and industry filters, then the sort, then the page. The intent cards run the same pipeline without their own filter, so their counts, dots and percents always describe what the other filters leave.
- An intent score of 70 and up is High, 40 to 69 Medium, under 40 Low. A card’s strip has one dot per scored company (up to 30), lit for the level’s share, and it is a toggle button: pressed, the table keeps that level and Clear shows in the toolbar.
- Every change goes through one function that keeps the rows and lists as they were with the toast that reports it; Undo puts them back. Status, owner, delete, hand-over, saving to a list, adding and editing all work that way, and each calls its callback with the companies it touched.
- Selecting rows swaps the toolbar for a bulk bar with the count, the contacts and the average intent of the selection. Status, Assign and Save to List open menus with the same items as the cells’ own menus.
- Menus sit on the stage, or in the panel they open from, never inside the table: its sideways scroll would clip them. Each is placed under its trigger in the stage’s own px, so it stays right when the gallery scales the stage, and flips above it when there is no room below.
- The drawer and the dialogs are modal panels over the card, which blurs and goes inert behind them. The headcount chart is an SVG path through six month-end counts with a soft area under it; growth bars and intent rings are small SVGs coloured by tone.
- Under 720 px the cards and toolbar wrap; the table always scrolls inside its own box, with the checkbox and Company columns held at its left, so the page never scrolls sideways.
- The demo presses the High card, sets two companies to Contacted, gives the unassigned one an owner, opens its drawer, then hands accounts over, each with its toast, in a loop. It presses the real controls, so it runs the same code a reader does; any input of the reader’s stops it.

## API reference

- **companies** (`readonly Company[]`, default `COMPANIES`): The rows: name, domain, mark, industry, city and country, growth (% over six months, or null), size, intent score (0–100, or null), contacts, status, owner id (or null), funding, revenue and year founded.
- **owners** (`readonly Owner[]`, default `OWNERS`): Who can own an account: an id, a name and an avatar colour. The one with you: true is the reader, for My Accounts and Assign to me.
- **lists** (`readonly SavedList[]`, default `LISTS`): The saved lists Save to List offers, each with the ids saved to it.
- **title** (`string`, default `'Prospects'`): The header’s title.
- **subtitle** (`string`, default `'Companies that fit your customer profile'`): The line under the title.
- **pageSize** (`number`, default `10`): Rows per page at first: 10, 20 or 30.
- **columns** (`readonly ColumnId[]`): The columns shown at first besides Company; the reader changes them under Columns. Industry, Growth, Size, Intent, Contacts, Status and Owner by default; HQ, Funding, Revenue and Founded are there to add.
- **onStatusChange** (`(companies: Company[], status: Status) => void`): Called when one company or a selection gets a new status.
- **onAssign** (`(companies: Company[], owner: Owner | null) => void`): Called when companies get an owner, or none.
- **onHandOver** (`(companies: Company[], from: Owner | null, to: Owner | null) => void`): Called with the open accounts a hand-over moved.
- **onAdd** (`(company: Company) => void`): Called with the company Add Company created.
- **onDelete** (`(companies: Company[]) => void`): Called with the companies deleted.
- **onExport** (`(companies: Company[]) => void`): Called with the companies to export; writing the file is up to you.
- **onSaveToList** (`(companies: Company[], list: SavedList) => void`): Called when companies are saved to a list, new or not.
- **onOpen** (`(company: Company) => void`): Called when a company’s drawer opens.
- **autoplay** (`boolean`, default `true`): While on screen and untouched, a demo pointer shows the table at work; any input hands it to the reader.
- **colorScheme** (`'light' | 'dark' | 'system'`): Light or dark colours, or the reader’s; without it, the page’s color-scheme.
- **defaultIntent** (`'high' | 'medium' | 'low'`): Opens filtered to this intent level; turns the demo off.
- **defaultQuery** (`string`): Opens with this search typed in; turns the demo off.
- **defaultSelected** (`readonly string[]`): Opens with these rows selected, by id; turns the demo off.
- **defaultOpen** (`string`): Opens with this company’s drawer showing, by id; turns the demo off.
- **defaultDialog** (`'handover' | 'add'`): Opens with Hand Over or Add Company showing; turns the demo off.
- **className** (`string`): Classes for the stage.

## Accessibility

- The card is a section named by its title, and the table a real table with a caption that says how many companies show, the level filter and the sort. Sortable headers are buttons, with aria-sort on the sorted one.
- The intent cards are toggle buttons (aria-pressed) named with their count, share and how many are unassigned; My Accounts is a toggle too.
- Status chips, owners, row actions, Filter, Columns, the view options and the bulk bar’s Status, Assign and Save to List open menus: arrow keys, Home and End move through the items, which are menu items, radios or checkboxes with aria-checked; Escape closes and gives the focus back.
- The drawer and the dialogs are modal dialogs named by their titles: the focus moves in and stays inside, Escape closes, and the focus goes back to what opened them. The table behind them is inert. The drawer’s tabs are a tablist, moved through with the arrow keys.
- Every change is announced in a polite live region, as the toast says it; Undo is a plain button in the toast. The range of rows shown is announced as it changes.
- Every control has a visible focus ring. Under reduced motion the demo, the panels’ slide, the menus’ pop and the toast’s motion are off.
- The demo pointer is decorative and hidden from assistive technology.

## Source

### lead-table/lead-table.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

'use client';

import {
  useCallback,
  useEffect,
  useEffectEvent,
  useId,
  useRef,
  useState,
  type ReactNode,
} from 'react';
import {
  COMPANIES,
  LEVEL_WORD,
  LISTS,
  OWNERS,
  STATUSES,
  STATUS_WORD,
  count,
  firstName,
  isClosed,
  levelOf,
  type Company,
  type Level,
  type Owner,
  type SavedList,
  type Status,
} from './data';
import { DemoPointer, runDemo, type Point } from './demo-pointer';
import { CompanyDialog, HandOverDialog, NewListDialog, type CompanyDraft } from './dialogs';
import { Drawer, type DrawerTab } from './drawer';
import { Icon, Mark, type UiIcon } from './glyphs';
import { Menu, MenuItem, MenuLabel } from './layers';
import { Avatar, Dots, Flag, Growth, Intent, StatusChip } from './parts';
import { useAutoplay } from './use-autoplay';
import styles from './lead-table.module.css';

/*
 * Lead table: a prospecting table that shows which companies are ready to buy.
 *
 * - Three intent cards count the companies the filters leave, by level (an intent score of 70 and
 *   up is High, 40 to 69 Medium, under 40 Low), with a strip of dots, one per company, lit for the
 *   level's share. A card is a toggle: pressed, it filters the table to its level.
 * - The toolbar searches names, industries and cities as you type, and filters by status and
 *   industry, by the reader's own accounts, and by column; selecting rows swaps it for a bulk bar
 *   that deletes, exports, sets a status, assigns an owner or saves to a list.
 * - Every change is one step: the data before it is kept with the toast that reports it, and Undo
 *   puts it back.
 * - A company's name opens its drawer over the blurred table; Hand Over moves one owner's open
 *   accounts to another; Add Company puts a new row in the table, lit for a moment.
 * - While on screen and untouched, a demo pointer filters by an intent card, sets two companies'
 *   status, assigns an owner, opens a drawer and hands accounts over, in a loop. Any real input
 *   hands the table to the reader.
 */

export type { Company, CompanyIcon, Level, Owner, SavedList, Status } from './data';

export type ColumnId =
  | 'industry'
  | 'hq'
  | 'growth'
  | 'size'
  | 'funding'
  | 'revenue'
  | 'founded'
  | 'intent'
  | 'contacts'
  | 'status'
  | 'owner';

type SortKey = 'name' | ColumnId;
type Sort = { key: SortKey; dir: 'asc' | 'desc' };

const GROUPS = ['Firmographics', 'Buying Signals', 'Outreach'] as const;

const COLUMNS: {
  id: ColumnId;
  label: string;
  group: (typeof GROUPS)[number];
  numeric?: boolean;
}[] = [
  { id: 'industry', label: 'Industry', group: 'Firmographics' },
  { id: 'hq', label: 'HQ', group: 'Firmographics' },
  { id: 'growth', label: 'Growth', group: 'Buying Signals' },
  { id: 'size', label: 'Size', group: 'Firmographics', numeric: true },
  { id: 'funding', label: 'Funding', group: 'Firmographics' },
  { id: 'revenue', label: 'Revenue', group: 'Firmographics', numeric: true },
  { id: 'founded', label: 'Founded', group: 'Firmographics', numeric: true },
  { id: 'intent', label: 'Intent', group: 'Buying Signals' },
  { id: 'contacts', label: 'Contacts', group: 'Buying Signals', numeric: true },
  { id: 'status', label: 'Status', group: 'Outreach' },
  { id: 'owner', label: 'Owner', group: 'Outreach' },
];

const DEFAULT_COLUMNS: ColumnId[] = [
  'industry',
  'growth',
  'size',
  'intent',
  'contacts',
  'status',
  'owner',
];
const LEVELS: Level[] = ['high', 'medium', 'low'];
const PAGE_SIZES = [10, 20, 30];

type Filters = {
  query: string;
  level: Level | null;
  mine: boolean;
  statuses: Status[];
  industries: string[];
};
const NO_FILTERS: Filters = { query: '', level: null, mine: false, statuses: [], industries: [] };

type Snapshot = { rows: Company[]; lists: SavedList[] };
type Toast = { n: number; title: string; body: string; undo?: Snapshot };
type MenuState = { id: string; anchor: HTMLElement; where: 'stage' | 'drawer' };
type Dialog =
  | { kind: 'handover' }
  | { kind: 'add' }
  | { kind: 'edit'; id: string }
  | { kind: 'newlist'; ids: string[] };

const STATUS_ORDER: Record<Status, number> = {
  new: 0,
  researching: 1,
  contacted: 2,
  qualified: 3,
  disqualified: 4,
};

function sortValue(row: Company, key: SortKey, owners: Owner[]): string | number | null {
  switch (key) {
    case 'name':
      return row.name.toLowerCase();
    case 'industry':
      return row.industry;
    case 'hq':
      return row.city;
    case 'growth':
      return row.growth;
    case 'size':
      return row.size;
    case 'funding':
      return row.funding;
    case 'revenue':
      return row.revenue;
    case 'founded':
      return row.founded;
    case 'intent':
      return row.intent;
    case 'contacts':
      return row.contacts;
    case 'status':
      return STATUS_ORDER[row.status];
    case 'owner':
      return owners.find((owner) => owner.id === row.owner)?.name ?? null;
  }
}

/** The rows the filters leave, sorted. `skipLevel` leaves the intent card's filter out. */
function pipeline(
  rows: Company[],
  filters: Filters,
  sort: Sort,
  owners: Owner[],
  skipLevel = false,
) {
  const you = owners.find((owner) => owner.you)?.id;
  const query = filters.query.trim().toLowerCase();
  const kept = rows.filter((row) => {
    if (
      query &&
      !`${row.name} ${row.industry} ${row.city} ${row.domain}`.toLowerCase().includes(query)
    )
      return false;
    if (!skipLevel && filters.level && levelOf(row.intent) !== filters.level) return false;
    if (filters.mine && row.owner !== you) return false;
    if (filters.statuses.length && !filters.statuses.includes(row.status)) return false;
    if (filters.industries.length && !filters.industries.includes(row.industry)) return false;
    return true;
  });
  const sign = sort.dir === 'asc' ? 1 : -1;
  return kept.sort((a, b) => {
    const x = sortValue(a, sort.key, owners);
    const y = sortValue(b, sort.key, owners);
    // Empty values go last either way.
    if (x === null || y === null) return x === y ? 0 : x === null ? 1 : -1;
    const order =
      typeof x === 'number' && typeof y === 'number' ? x - y : String(x).localeCompare(String(y));
    return order * sign || a.name.localeCompare(b.name);
  });
}

const companiesWord = (n: number) => (n === 1 ? '1 company' : `${n} companies`);

export function LeadTable({
  companies = COMPANIES,
  owners = OWNERS,
  lists: initialLists = LISTS,
  title = 'Prospects',
  subtitle = 'Companies that fit your customer profile',
  pageSize = 10,
  columns: initialColumns = DEFAULT_COLUMNS,
  onStatusChange,
  onAssign,
  onHandOver,
  onAdd,
  onDelete,
  onExport,
  onSaveToList,
  onOpen,
  autoplay = true,
  colorScheme,
  defaultIntent,
  defaultQuery,
  defaultSelected,
  defaultOpen,
  defaultDialog,
  className = '',
}: {
  /** The companies, one row each. */
  companies?: readonly Company[];
  /** The people who can own an account; the one with `you` is the reader. */
  owners?: readonly Owner[];
  /** The saved lists offered by Save to List. */
  lists?: readonly SavedList[];
  title?: string;
  subtitle?: string;
  /** Rows per page at first: 10, 20 or 30. */
  pageSize?: number;
  /** The columns shown at first, besides Company. */
  columns?: readonly ColumnId[];
  onStatusChange?: (companies: Company[], status: Status) => void;
  onAssign?: (companies: Company[], owner: Owner | null) => void;
  onHandOver?: (companies: Company[], from: Owner | null, to: Owner | null) => void;
  onAdd?: (company: Company) => void;
  onDelete?: (companies: Company[]) => void;
  onExport?: (companies: Company[]) => void;
  onSaveToList?: (companies: Company[], list: SavedList) => void;
  /** A company's drawer opened. */
  onOpen?: (company: Company) => void;
  /** While on screen and untouched, a demo pointer shows the table at work. */
  autoplay?: boolean;
  /** Light or dark colours, or the reader's ('system'); without it, the page's color-scheme. */
  colorScheme?: 'light' | 'dark' | 'system';
  /** Opens filtered to this intent level; turns the demo off. */
  defaultIntent?: Level;
  /** Opens with this search typed in; turns the demo off. */
  defaultQuery?: string;
  /** Opens with these rows selected (ids); turns the demo off. */
  defaultSelected?: readonly string[];
  /** Opens with this company's drawer showing (its id); turns the demo off. */
  defaultOpen?: string;
  /** Opens with this dialog showing; turns the demo off. */
  defaultDialog?: 'handover' | 'add';
  className?: string;
}) {
  const { ref: root, playing, take } = useAutoplay<HTMLDivElement>(6000);
  const locked = !!(
    defaultIntent ||
    defaultQuery ||
    defaultSelected?.length ||
    defaultOpen ||
    defaultDialog
  );
  const demo = autoplay && playing && !locked;
  const uid = useId();
  const team = owners as Owner[];
  const you = team.find((owner) => owner.you);

  const [rows, setRows] = useState<Company[]>(() => [...companies]);
  const [lists, setLists] = useState<SavedList[]>(() => initialLists.map((list) => ({ ...list })));
  const [filters, setFilters] = useState<Filters>(() => ({
    ...NO_FILTERS,
    query: defaultQuery ?? '',
    level: defaultIntent ?? null,
  }));
  const [sort, setSort] = useState<Sort>({ key: 'name', dir: 'asc' });
  const [page, setPage] = useState(0);
  const [perPage, setPerPage] = useState(PAGE_SIZES.includes(pageSize) ? pageSize : 10);
  const [shown, setShown] = useState<ColumnId[]>(() => [...initialColumns]);
  const [compact, setCompact] = useState(false);
  const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set(defaultSelected));
  const [menu, setMenu] = useState<MenuState | null>(null);
  const [drawer, setDrawer] = useState<{ id: string; tab: DrawerTab } | null>(() =>
    defaultOpen ? { id: defaultOpen, tab: 'overview' } : null,
  );
  const [dialog, setDialog] = useState<Dialog | null>(() =>
    defaultDialog ? { kind: defaultDialog } : null,
  );
  const [toast, setToast] = useState<Toast | null>(null);
  const [fresh, setFresh] = useState<string | null>(null);
  const [fading, setFading] = useState(false);
  const [message, setMessage] = useState('');
  const pointer = useRef<HTMLSpanElement>(null);
  /** What had the focus when a panel opened: it gets it back. */
  const opener = useRef<HTMLElement | null>(null);
  const toastN = useRef(0);

  // New data from the props: start over. Compared by content, so arrays written inline in the
  // caller's JSX do not reset it.
  const givenKey = JSON.stringify([companies, initialLists]);
  const [given, setGiven] = useState(givenKey);
  if (given !== givenKey) {
    setGiven(givenKey);
    setRows([...companies]);
    setLists(initialLists.map((list) => ({ ...list })));
    setSelected(new Set());
    setToast(null);
  }

  const ownerOf = (id: string | null) => team.find((owner) => owner.id === id);
  const byId = (id: string) => rows.find((row) => row.id === id);

  // ---- What shows ------------------------------------------------------------------------------

  const visible = pipeline(rows, filters, sort, team);
  const base = pipeline(rows, filters, sort, team, true);
  const scored = base.filter((row) => row.intent !== null);
  const pages = Math.max(1, Math.ceil(visible.length / perPage));
  const at = Math.min(page, pages - 1);
  const onPage = visible.slice(at * perPage, at * perPage + perPage);
  const chosen = rows.filter((row) => selected.has(row.id));
  const filtered =
    filters.query !== '' ||
    filters.level !== null ||
    filters.mine ||
    filters.statuses.length > 0 ||
    filters.industries.length > 0;
  const columns = COLUMNS.filter((column) => shown.includes(column.id));
  const drawerRow = drawer ? byId(drawer.id) : undefined;
  const overlay = !!(drawerRow || dialog);

  // ---- Changes, each one a step Undo can take back ---------------------------------------------

  const announce = (text: string) => setMessage((now) => (now === text ? `${text} ` : text));
  const report = (title: string, body: string, undo?: Snapshot) => {
    toastN.current += 1;
    setToast({ n: toastN.current, title, body, undo });
    announce(`${title}. ${body}`);
  };
  const commit = (nextRows: Company[], nextLists: SavedList[], title: string, body: string) => {
    report(title, body, { rows, lists });
    setRows(nextRows);
    setLists(nextLists);
  };
  const undo = () => {
    if (!toast?.undo) return;
    setRows(toast.undo.rows);
    setLists(toast.undo.lists);
    setFresh(null);
    report('Undone', 'Back to how it was.');
  };

  const setStatus = (ids: string[], status: Status) => {
    const changed = rows.filter((row) => ids.includes(row.id));
    if (!changed.length) return;
    const one = changed[0];
    const next = rows.map((row) => (ids.includes(row.id) ? { ...row, status } : row));
    if (changed.length === 1)
      commit(
        next,
        lists,
        `${one.name} is ${STATUS_WORD[status]}`,
        `Was ${STATUS_WORD[one.status]}.`,
      );
    else
      commit(
        next,
        lists,
        `${companiesWord(changed.length)} set to ${STATUS_WORD[status]}`,
        'Each keeps its lists and contacts.',
      );
    onStatusChange?.(
      changed.map((row) => ({ ...row, status })),
      status,
    );
  };

  const assign = (ids: string[], ownerId: string | null) => {
    const changed = rows.filter((row) => ids.includes(row.id));
    if (!changed.length) return;
    const owner = ownerOf(ownerId) ?? null;
    const next = rows.map((row) => (ids.includes(row.id) ? { ...row, owner: ownerId } : row));
    const was = ownerOf(changed[0].owner);
    if (changed.length === 1)
      commit(
        next,
        lists,
        owner ? `${changed[0].name} assigned to ${owner.name}` : `${changed[0].name} is unassigned`,
        was ? `Was with ${was.name}.` : 'Was unassigned.',
      );
    else
      commit(
        next,
        lists,
        owner
          ? `${companiesWord(changed.length)} assigned to ${owner.name}`
          : `${companiesWord(changed.length)} unassigned`,
        'Status and lists stay as they were.',
      );
    onAssign?.(
      changed.map((row) => ({ ...row, owner: ownerId })),
      owner,
    );
  };

  const remove = (ids: string[]) => {
    const gone = rows.filter((row) => ids.includes(row.id));
    if (!gone.length) return;
    commit(
      rows.filter((row) => !ids.includes(row.id)),
      lists,
      gone.length === 1 ? `${gone[0].name} deleted` : `${companiesWord(gone.length)} deleted`,
      'Undo brings them back with their lists.',
    );
    setSelected(new Set());
    if (drawer && ids.includes(drawer.id)) setDrawer(null);
    onDelete?.(gone);
  };

  const exportRows = (ids: string[]) => {
    const picked = rows.filter((row) => ids.includes(row.id));
    const people = picked.reduce((sum, row) => sum + (row.contacts ?? 0), 0);
    report(
      `${companiesWord(picked.length)} exported`,
      `A file with ${count(people)} contacts, ready for the CRM.`,
    );
    onExport?.(picked);
  };

  const saveTo = (ids: string[], listId: string) => {
    const list = lists.find((each) => each.id === listId);
    if (!list) return;
    const adding = ids.filter((id) => !list.ids.includes(id));
    const next = lists.map((each) =>
      each.id === listId ? { ...each, ids: [...each.ids, ...adding] } : each,
    );
    commit(
      rows,
      next,
      `Saved to ${list.name}`,
      adding.length ? `${companiesWord(adding.length)} added to the list.` : 'Already in it.',
    );
    onSaveToList?.(
      rows.filter((row) => ids.includes(row.id)),
      next.find((each) => each.id === listId)!,
    );
  };

  const createList = (name: string, ids: string[]) => {
    const list: SavedList = { id: `list-${Date.now().toString(36)}`, name, ids: [...ids] };
    commit(
      rows,
      [...lists, list],
      `Saved to ${name}`,
      `${companiesWord(ids.length)} in the new list.`,
    );
    onSaveToList?.(
      rows.filter((row) => ids.includes(row.id)),
      list,
    );
  };

  const handOver = (from: string | null, to: string | null) => {
    const moving = rows.filter((row) => row.owner === from && !isClosed(row.status));
    const target = ownerOf(to);
    const source = ownerOf(from);
    commit(
      rows.map((row) => (moving.includes(row) ? { ...row, owner: to } : row)),
      lists,
      `${moving.length} ${moving.length === 1 ? 'account' : 'accounts'} handed to ${target?.name ?? 'nobody'}`,
      `${source ? `From ${source.name}` : 'From the unassigned pile'}; status and lists stay as they were.`,
    );
    onHandOver?.(
      moving.map((row) => ({ ...row, owner: to })),
      source ?? null,
      target ?? null,
    );
  };

  const add = (draft: CompanyDraft) => {
    const company: Company = {
      ...draft,
      id: `new-${Date.now().toString(36)}`,
      icon: 'box',
      growth: null,
      intent: null,
      contacts: null,
      revenue: 0,
      founded: new Date().getFullYear(),
    };
    const next = [...rows, company];
    commit(
      next,
      lists,
      'Company added',
      `${company.name} is in the table as ${STATUS_WORD[company.status]}.`,
    );
    // Show it: no filters, and the page it lands on.
    const order = pipeline(next, NO_FILTERS, sort, team);
    setFilters(NO_FILTERS);
    setPage(Math.floor(order.indexOf(company) / perPage));
    setFresh(company.id);
    onAdd?.(company);
  };

  const edit = (id: string, draft: CompanyDraft) => {
    commit(
      rows.map((row) => (row.id === id ? { ...row, ...draft } : row)),
      lists,
      `${draft.name} updated`,
      'The table shows the changes.',
    );
  };

  // ---- Small state helpers ---------------------------------------------------------------------

  const filter = (change: Partial<Filters>) => {
    setFilters((now) => ({ ...now, ...change }));
    setPage(0);
  };
  const toggleIn = <T,>(list: T[], value: T) =>
    list.includes(value) ? list.filter((each) => each !== value) : [...list, value];

  const openMenu = (id: string, anchor: HTMLElement, where: MenuState['where'] = 'stage') =>
    setMenu((now) => (now?.id === id && now.anchor === anchor ? null : { id, anchor, where }));
  const menuAnchor = menu?.anchor;
  const closeMenu = useCallback(
    (back = false) => {
      if (back) menuAnchor?.focus({ preventScroll: true });
      setMenu(null);
    },
    [menuAnchor],
  );

  const openDrawer = (id: string) => {
    opener.current = document.activeElement as HTMLElement | null;
    setMenu(null);
    setDrawer({ id, tab: 'overview' });
    const row = byId(id);
    if (row) onOpen?.(row);
  };
  const openDialog = (next: Dialog) => {
    if (!drawer) opener.current = document.activeElement as HTMLElement | null;
    setMenu(null);
    setDialog(next);
  };
  const giveFocusBack = () => {
    const element = opener.current;
    opener.current = null;
    if (element && !demo)
      window.requestAnimationFrame(() => element.focus({ preventScroll: true }));
  };
  const closeDrawer = () => {
    setDrawer(null);
    setMenu(null);
    giveFocusBack();
  };
  const closeDialog = () => {
    setDialog(null);
    if (!drawer) giveFocusBack();
  };

  // A toast stays five seconds.
  useEffect(() => {
    if (!toast) return;
    const timer = window.setTimeout(() => setToast(null), 5000);
    return () => window.clearTimeout(timer);
  }, [toast]);
  // A new row stays lit a few seconds.
  useEffect(() => {
    if (!fresh) return;
    const timer = window.setTimeout(() => setFresh(null), 4200);
    return () => window.clearTimeout(timer);
  }, [fresh]);

  // ---- Menus -----------------------------------------------------------------------------------

  const listItems = (ids: string[]) => (
    <>
      <MenuLabel>Lists</MenuLabel>
      {lists.map((list) => {
        const n = list.ids.filter((id) => rows.some((row) => row.id === id)).length;
        return (
          <MenuItem
            key={list.id}
            trail={n ? `${n} saved` : undefined}
            demo={`list-${list.id}`}
            onSelect={() => {
              closeMenu(true);
              saveTo(ids, list.id);
            }}
          >
            <span className={styles.optionLine}>
              <Icon name="bookmark" className={styles.menuIcon} />
              {list.name}
            </span>
          </MenuItem>
        );
      })}
      <hr className={styles.menuRule} />
      <MenuItem
        demo="list-new"
        onSelect={() => {
          closeMenu();
          openDialog({ kind: 'newlist', ids });
        }}
      >
        <span className={styles.optionLine}>
          <Icon name="plus" className={styles.menuIcon} />
          New list
        </span>
      </MenuItem>
    </>
  );

  const statusItems = (current: Status | null, pick: (status: Status) => void) => (
    <>
      <MenuLabel>Status</MenuLabel>
      {STATUSES.map((status) => (
        <MenuItem
          key={status}
          kind="radio"
          checked={status === current}
          demo={`status-${status}`}
          onSelect={() => {
            closeMenu(true);
            if (status !== current) pick(status);
          }}
        >
          <StatusChip status={status} />
        </MenuItem>
      ))}
    </>
  );

  const ownerItems = (current: string | null | undefined, pick: (owner: string | null) => void) => (
    <>
      <MenuLabel>Owner</MenuLabel>
      {[...team, null].map((owner) => (
        <MenuItem
          key={owner?.id ?? 'none'}
          kind="radio"
          checked={current !== undefined && (owner?.id ?? null) === current}
          trail={owner?.you ? 'You' : undefined}
          demo={`owner-${owner?.id ?? 'none'}`}
          onSelect={() => {
            closeMenu(true);
            if ((owner?.id ?? null) !== current) pick(owner?.id ?? null);
          }}
        >
          <span className={styles.optionLine} data-muted={!owner || undefined}>
            <Avatar owner={owner} />
            {owner?.name ?? 'Unassigned'}
          </span>
        </MenuItem>
      ))}
    </>
  );

  const menuBody = (
    id: string,
  ): { label: string; body: ReactNode; align?: 'end'; wide?: boolean } | null => {
    const [kind, rowId] = id.split(':');
    const row = rowId ? byId(rowId) : drawerRow;
    const ids = [...selected];
    switch (kind) {
      case 'status':
        return row
          ? { label: 'Status', body: statusItems(row.status, (s) => setStatus([row.id], s)) }
          : null;
      case 'owner':
        return row
          ? { label: 'Owner', body: ownerItems(row.owner, (o) => assign([row.id], o)) }
          : null;
      case 'row':
        return row
          ? {
              label: `Actions for ${row.name}`,
              align: 'end',
              body: (
                <>
                  <MenuItem
                    onSelect={() => {
                      closeMenu();
                      openDrawer(row.id);
                    }}
                  >
                    <span className={styles.optionLine}>
                      <Icon name="open" className={styles.menuIcon} />
                      Open details
                    </span>
                  </MenuItem>
                  {you && row.owner !== you.id && (
                    <MenuItem
                      onSelect={() => {
                        closeMenu(true);
                        assign([row.id], you.id);
                      }}
                    >
                      <span className={styles.optionLine}>
                        <Icon name="userPlus" className={styles.menuIcon} />
                        Assign to me
                      </span>
                    </MenuItem>
                  )}
                  <MenuItem
                    onSelect={() => {
                      closeMenu(true);
                      exportRows([row.id]);
                    }}
                  >
                    <span className={styles.optionLine}>
                      <Icon name="download" className={styles.menuIcon} />
                      Export
                    </span>
                  </MenuItem>
                  <hr className={styles.menuRule} />
                  <MenuItem
                    tone="danger"
                    onSelect={() => {
                      closeMenu();
                      remove([row.id]);
                    }}
                  >
                    <span className={styles.optionLine}>
                      <Icon name="trash" className={styles.menuIcon} />
                      Delete
                    </span>
                  </MenuItem>
                </>
              ),
            }
          : null;
      case 'bulk-status': {
        const same = chosen.every((each) => each.status === chosen[0]?.status)
          ? chosen[0]?.status
          : null;
        return { label: 'Status', body: statusItems(same ?? null, (s) => setStatus(ids, s)) };
      }
      case 'bulk-assign': {
        const same = chosen.every((each) => each.owner === chosen[0]?.owner)
          ? chosen[0]?.owner
          : undefined;
        return { label: 'Owner', body: ownerItems(same, (o) => assign(ids, o)) };
      }
      case 'bulk-list':
        return { label: 'Save to list', align: 'end', body: listItems(ids), wide: true };
      case 'drawer-status':
        return row
          ? { label: 'Status', body: statusItems(row.status, (s) => setStatus([row.id], s)) }
          : null;
      case 'drawer-list':
        return row
          ? { label: 'Save to list', align: 'end', body: listItems([row.id]), wide: true }
          : null;
      case 'drawer-more':
        return row
          ? {
              label: 'More actions',
              align: 'end',
              body: (
                <>
                  {you && row.owner !== you.id && (
                    <MenuItem
                      onSelect={() => {
                        closeMenu(true);
                        assign([row.id], you.id);
                      }}
                    >
                      <span className={styles.optionLine}>
                        <Icon name="userPlus" className={styles.menuIcon} />
                        Assign to me
                      </span>
                    </MenuItem>
                  )}
                  <MenuItem
                    tone="danger"
                    onSelect={() => {
                      closeMenu();
                      remove([row.id]);
                    }}
                  >
                    <span className={styles.optionLine}>
                      <Icon name="trash" className={styles.menuIcon} />
                      Delete
                    </span>
                  </MenuItem>
                </>
              ),
            }
          : null;
      case 'filter': {
        const industries = [...new Set(rows.map((each) => each.industry))].sort();
        return {
          label: 'Filter',
          body: (
            <>
              <MenuLabel>Status</MenuLabel>
              {STATUSES.map((status) => (
                <MenuItem
                  key={status}
                  kind="check"
                  checked={filters.statuses.includes(status)}
                  onSelect={() => filter({ statuses: toggleIn(filters.statuses, status) })}
                >
                  <StatusChip status={status} />
                </MenuItem>
              ))}
              <hr className={styles.menuRule} />
              <MenuLabel>Industry</MenuLabel>
              {industries.map((industry) => (
                <MenuItem
                  key={industry}
                  kind="check"
                  checked={filters.industries.includes(industry)}
                  onSelect={() => filter({ industries: toggleIn(filters.industries, industry) })}
                >
                  {industry}
                </MenuItem>
              ))}
            </>
          ),
        };
      }
      case 'columns':
        return {
          label: 'Columns',
          align: 'end',
          wide: true,
          body: (
            <ColumnsMenu
              shown={shown}
              onToggle={(column) => setShown((now) => toggleIn(now, column))}
            />
          ),
        };
      case 'view':
        return {
          label: 'View options',
          align: 'end',
          body: (
            <>
              <MenuLabel>Row height</MenuLabel>
              <MenuItem kind="radio" checked={!compact} onSelect={() => setCompact(false)}>
                Comfortable
              </MenuItem>
              <MenuItem kind="radio" checked={compact} onSelect={() => setCompact(true)}>
                Compact
              </MenuItem>
            </>
          ),
        };
      case 'perpage':
        return {
          label: 'Rows per page',
          body: (
            <>
              {PAGE_SIZES.map((size) => (
                <MenuItem
                  key={size}
                  kind="radio"
                  checked={perPage === size}
                  onSelect={() => {
                    closeMenu(true);
                    setPerPage(size);
                    setPage(0);
                  }}
                >
                  {size}
                </MenuItem>
              ))}
            </>
          ),
        };
    }
    return null;
  };

  const menuView = (() => {
    if (!menu) return null;
    const content = menuBody(menu.id);
    if (!content) return null;
    return (
      <Menu
        key={menu.id}
        anchor={menu.anchor}
        label={content.label}
        align={content.align}
        wide={content.wide}
        takeFocus={!demo}
        onClose={closeMenu}
      >
        {content.body}
      </Menu>
    );
  })();

  // ---- The demo --------------------------------------------------------------------------------

  const demoStep = useEffectEvent((step: 'reset' | 'fade-in' | 'fade-out') => {
    if (step === 'fade-in') setFading(true);
    else if (step === 'fade-out') setFading(false);
    else {
      setRows([...companies]);
      setLists(initialLists.map((list) => ({ ...list })));
      setFilters(NO_FILTERS);
      setSort({ key: 'name', dir: 'asc' });
      setPage(0);
      setSelected(new Set());
      setMenu(null);
      setDrawer(null);
      setDialog(null);
      setToast(null);
      setFresh(null);
    }
  });
  const dirty = useEffectEvent(
    () =>
      rows.length !== companies.length ||
      rows.some((row, i) => row !== companies[i]) ||
      lists.some((list, i) => list.ids.length !== initialLists[i]?.ids.length) ||
      filtered ||
      page !== 0 ||
      selected.size > 0 ||
      !!drawer ||
      !!dialog,
  );
  const demoClear = useEffectEvent(() => {
    setMenu(null);
  });

  useEffect(() => {
    const stage = root.current;
    const arrow = pointer.current;
    if (!demo || !stage || !arrow) return;
    const find = (name: string) => stage.querySelector<HTMLElement>(`[data-demo="${name}"]`);
    const on = (element: Element | null, fx = 0.5, fy = 0.5): Point => {
      const frame = stage.getBoundingClientRect();
      const scale = frame.width / stage.offsetWidth || 1;
      const rect = element?.getBoundingClientRect() ?? frame;
      return {
        x: (rect.left + rect.width * fx - frame.left) / scale,
        y: (rect.top + rect.height * fy - frame.top) / scale,
      };
    };
    let over: HTMLElement | null = null;
    const stop = runDemo(stage, arrow, async ({ wait, glide, glyph }) => {
      /** Glides onto an element, presses it, and waits `after` ms. */
      const press = async (name: string, ms: number, after: number, fx = 0.5, fy = 0.5) => {
        const element = find(name);
        if (!element) return;
        // On a narrow stage, bring a cell the table has scrolled away into its box first.
        const box = element.closest<HTMLElement>('[data-part="table"]');
        if (box) {
          const b = box.getBoundingClientRect();
          const r = element.getBoundingClientRect();
          if (r.right > b.right || r.left < b.left) {
            box.scrollBy({ left: r.left - b.left - b.width / 2 + r.width / 2, behavior: 'smooth' });
            await wait(420);
          }
        }
        glyph('arrow');
        await glide(on(element, fx, fy), ms);
        glyph('hand');
        over = element;
        element.dataset.over = '';
        await wait(240);
        // Found again: a re-render may have replaced it.
        const live = find(name) ?? element;
        delete element.dataset.over;
        over = null;
        live.click();
        await wait(after);
      };
      for (;;) {
        if (dirty()) {
          demoStep('fade-in');
          await wait(320);
          demoStep('reset');
          await wait(80);
          demoStep('fade-out');
        }
        await wait(900);
        // One click: only the companies ready to buy.
        await press('card-high', 700, 1200, 0.3, 0.55);
        // Two of them are in conversation now.
        await press('check-c9', 650, 280);
        await press('check-c10', 380, 700);
        await press('bulk-status', 600, 520);
        await press('status-contacted', 520, 1700, 0.35);
        await press('bulk-clear', 600, 600);
        // The unassigned one gets an owner.
        await press('owner-c1', 650, 520, 0.4);
        await press('owner-maya', 480, 1700, 0.35);
        // Its drawer: firmographics, then what it did lately.
        await press('open-c1', 650, 1500, 0.35);
        await press('tab-signals', 650, 1600);
        await press('tab-contacts', 480, 1300);
        await press('drawer-close', 600, 700);
        // Back to everyone, and a hand-over.
        await press('card-high', 650, 800, 0.3, 0.55);
        await press('handover', 700, 1500);
        await press('hand-submit', 650, 2400, 0.5);
        glyph('arrow');
        await glide(on(stage, 0.85, 0.95), 800);
        await wait(1800);
      }
    });
    return () => {
      stop();
      if (over) delete over.dataset.over;
      demoClear();
      setFading(false);
    };
  }, [demo, root]);

  // ---- Drawing ---------------------------------------------------------------------------------

  const pageIds = onPage.map((row) => row.id);
  const allOnPage = pageIds.length > 0 && pageIds.every((id) => selected.has(id));
  const someOnPage = pageIds.some((id) => selected.has(id));
  const contacts = chosen.reduce((sum, row) => sum + (row.contacts ?? 0), 0);
  const chosenScores = chosen.filter((row) => row.intent !== null);
  const avgIntent = chosenScores.length
    ? Math.round(
        chosenScores.reduce((sum, row) => sum + (row.intent ?? 0), 0) / chosenScores.length,
      )
    : null;
  const toggleRow = (id: string) =>
    setSelected((now) => {
      const next = new Set(now);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });

  const sortBy = (key: SortKey) => {
    setSort((now) =>
      now.key === key ? { key, dir: now.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' },
    );
    setPage(0);
  };

  const header = (key: SortKey, label: string, className?: string) => {
    const active = sort.key === key;
    return (
      <th
        key={key}
        scope="col"
        className={className}
        aria-sort={active ? (sort.dir === 'asc' ? 'ascending' : 'descending') : undefined}
      >
        <button type="button" className={styles.sortButton} onClick={() => sortBy(key)}>
          {label}
          <Icon
            name={active ? (sort.dir === 'asc' ? 'arrowUp' : 'arrowDown') : 'sort'}
            className={styles.sortIcon}
            width={active ? 2.2 : 2}
          />
        </button>
      </th>
    );
  };

  const cell = (row: Company, column: ColumnId): ReactNode => {
    switch (column) {
      case 'industry':
        return row.industry;
      case 'hq':
        return (
          <span className={styles.city}>
            <Flag country={row.country} />
            {row.city}
          </span>
        );
      case 'growth':
        return <Growth company={row} />;
      case 'size':
        return count(row.size);
      case 'funding':
        return row.funding;
      case 'revenue':
        return row.revenue ? (
          `$${count(row.revenue)}M`
        ) : (
          <span className={styles.none}>Unknown</span>
        );
      case 'founded':
        return row.founded;
      case 'intent':
        return <Intent score={row.intent} />;
      case 'contacts':
        return row.contacts === null ? (
          <span className={styles.none}>None yet</span>
        ) : (
          count(row.contacts)
        );
      case 'status':
        return (
          <button
            type="button"
            className={styles.chipButton}
            aria-haspopup="menu"
            aria-expanded={menu?.id === `status:${row.id}`}
            aria-label={`Status of ${row.name}: ${STATUS_WORD[row.status]}`}
            data-demo={`status-${row.id}`}
            onClick={(event) => openMenu(`status:${row.id}`, event.currentTarget)}
          >
            <StatusChip status={row.status} />
          </button>
        );
      case 'owner': {
        const owner = ownerOf(row.owner);
        return (
          <button
            type="button"
            className={styles.ownerButton}
            aria-haspopup="menu"
            aria-expanded={menu?.id === `owner:${row.id}`}
            aria-label={owner ? `Owner of ${row.name}: ${owner.name}` : `Assign ${row.name}`}
            data-empty={!owner || undefined}
            data-demo={`owner-${row.id}`}
            data-part="owner"
            onClick={(event) => openMenu(`owner:${row.id}`, event.currentTarget)}
          >
            <Avatar owner={owner} />
            {owner ? firstName(owner) : 'Assign'}
            {!owner && <Icon name="chevronDown" className={styles.chevronSmall} />}
          </button>
        );
      }
    }
  };

  const toolButton = (icon: UiIcon, label: string, props: Record<string, unknown> = {}) => (
    <button type="button" className={styles.tool} {...props}>
      <Icon name={icon} />
      {label}
    </button>
  );

  const pageList = (() => {
    const n = Math.min(5, pages);
    const first = Math.max(0, Math.min(at - 2, pages - n));
    return Array.from({ length: n }, (_, i) => first + i);
  })();

  return (
    <div
      ref={root}
      className={`${styles.root} ${className}`}
      data-part="stage"
      data-color-scheme={colorScheme}
      data-compact={compact || undefined}
      onPointerDownCapture={take}
      onKeyDownCapture={take}
      onWheel={take}
      onPointerMove={(event) => {
        // The reader's own pointer on the table takes over from the demo's.
        if (event.pointerType !== 'touch') take();
      }}
    >
      <section
        className={styles.card}
        data-part="card"
        data-fading={fading || undefined}
        data-covered={overlay || undefined}
        aria-labelledby={`${uid}-title`}
        inert={overlay}
      >
        <header className={styles.head} data-part="header">
          <div className={styles.headText}>
            <h2 id={`${uid}-title`} className={styles.title}>
              {title}
            </h2>
            <p className={styles.subtitle}>{subtitle}</p>
          </div>
          <div className={styles.headActions}>
            <button
              type="button"
              className={styles.button}
              data-demo="handover"
              onClick={() => openDialog({ kind: 'handover' })}
            >
              <Icon name="swap" />
              Hand Over
            </button>
            <button
              type="button"
              className={styles.buttonDark}
              data-demo="add"
              onClick={() => openDialog({ kind: 'add' })}
            >
              <Icon name="plus" />
              Add Company
            </button>
          </div>
        </header>

        <div className={styles.sheet}>
          <div
            className={styles.cards}
            role="group"
            aria-label="Filter by intent"
            data-part="intent-cards"
          >
            {LEVELS.map((level) => {
              const ofLevel = scored.filter((row) => levelOf(row.intent) === level);
              const unassigned = ofLevel.filter((row) => !row.owner).length;
              const share = scored.length ? Math.round((ofLevel.length / scored.length) * 100) : 0;
              const pressed = filters.level === level;
              return (
                <button
                  key={level}
                  type="button"
                  className={styles.intentCard}
                  data-level={level}
                  data-part="intent-card"
                  data-demo={`card-${level}`}
                  aria-pressed={pressed}
                  aria-label={`${LEVEL_WORD[level]} intent: ${companiesWord(ofLevel.length)}, ${share}%, ${
                    unassigned ? `${unassigned} unassigned` : 'all assigned'
                  }`}
                  onClick={() => filter({ level: pressed ? null : level })}
                >
                  <span className={styles.cardTop}>
                    <span className={styles.levelChip} data-level={level}>
                      <span className={styles.chipDot} />
                      {LEVEL_WORD[level]} Intent
                    </span>
                    <span className={styles.cardNote}>
                      {unassigned ? `${unassigned} unassigned` : 'All assigned'}
                    </span>
                  </span>
                  <span className={styles.cardBottom}>
                    <span className={styles.cardCount}>{ofLevel.length}</span>
                    <Dots filled={ofLevel.length} total={scored.length} />
                    <span className={styles.cardShare}>{share}%</span>
                  </span>
                </button>
              );
            })}
          </div>

          {chosen.length > 0 ? (
            <div
              className={styles.toolbar}
              role="toolbar"
              aria-label="Selected companies"
              data-part="bulk-bar"
            >
              <p className={styles.bulkFacts}>
                <strong>{chosen.length} selected</strong>
                <span aria-hidden="true">·</span>
                <span>{count(contacts)} contacts</span>
                {avgIntent !== null && (
                  <>
                    <span aria-hidden="true">·</span>
                    <span>Avg intent {avgIntent}</span>
                  </>
                )}
              </p>
              <span className={styles.grow} />
              <div className={styles.toolGroup}>
                <button
                  type="button"
                  className={styles.tool}
                  data-demo="bulk-clear"
                  onClick={() => setSelected(new Set())}
                >
                  Clear
                </button>
                {toolButton('trash', 'Delete', {
                  'data-tone': 'danger',
                  onClick: () => remove([...selected]),
                })}
                {toolButton('download', 'Export', { onClick: () => exportRows([...selected]) })}
                {toolButton('status', 'Status', {
                  'aria-haspopup': 'menu',
                  'data-demo': 'bulk-status',
                  onClick: (event: { currentTarget: HTMLElement }) =>
                    openMenu('bulk-status', event.currentTarget),
                })}
                {toolButton('userPlus', 'Assign', {
                  'aria-haspopup': 'menu',
                  onClick: (event: { currentTarget: HTMLElement }) =>
                    openMenu('bulk-assign', event.currentTarget),
                })}
                <button
                  type="button"
                  className={styles.toolDark}
                  aria-haspopup="menu"
                  data-demo="bulk-list"
                  onClick={(event) => openMenu('bulk-list', event.currentTarget)}
                >
                  <Icon name="bookmark" />
                  Save to List
                </button>
              </div>
            </div>
          ) : (
            <div
              className={styles.toolbar}
              role="toolbar"
              aria-label="Table tools"
              data-part="toolbar"
            >
              <label className={styles.search}>
                <Icon name="search" className={styles.searchIcon} />
                <input
                  type="search"
                  className={styles.searchInput}
                  placeholder="Search companies..."
                  aria-label="Search companies"
                  value={filters.query}
                  onChange={(event) => filter({ query: event.target.value })}
                  onKeyDown={(event) => {
                    if (event.key === 'Escape' && filters.query) {
                      event.preventDefault();
                      filter({ query: '' });
                    }
                  }}
                />
                {filters.query && (
                  <button
                    type="button"
                    className={styles.searchClear}
                    aria-label="Clear the search"
                    onClick={(event) => {
                      filter({ query: '' });
                      (event.currentTarget.previousElementSibling as HTMLElement | null)?.focus();
                    }}
                  >
                    <Icon name="x" />
                  </button>
                )}
              </label>
              <div className={styles.toolGroup}>
                <button
                  type="button"
                  className={styles.tool}
                  aria-haspopup="menu"
                  aria-expanded={menu?.id === 'filter'}
                  data-on={filters.statuses.length + filters.industries.length > 0 || undefined}
                  onClick={(event) => openMenu('filter', event.currentTarget)}
                >
                  <Icon name="filter" />
                  Filter
                  {filters.statuses.length + filters.industries.length > 0 && (
                    <span className={styles.toolCount}>
                      {filters.statuses.length + filters.industries.length}
                    </span>
                  )}
                </button>
                {you && (
                  <button
                    type="button"
                    className={styles.tool}
                    aria-pressed={filters.mine}
                    data-on={filters.mine || undefined}
                    onClick={() => filter({ mine: !filters.mine })}
                  >
                    <Avatar owner={you} size={16} />
                    My Accounts
                  </button>
                )}
                {filtered && (
                  <button
                    type="button"
                    className={styles.tool}
                    data-demo="clear"
                    onClick={() => filter({ ...NO_FILTERS })}
                  >
                    <Icon name="filter" />
                    Clear
                  </button>
                )}
              </div>
              <span className={styles.grow} />
              <div className={styles.toolGroup}>
                <button
                  type="button"
                  className={styles.tool}
                  aria-haspopup="menu"
                  aria-expanded={menu?.id === 'columns'}
                  data-demo="columns"
                  onClick={(event) => openMenu('columns', event.currentTarget)}
                >
                  <Icon name="columns" />
                  Columns
                </button>
                <button
                  type="button"
                  className={styles.toolIcon}
                  aria-haspopup="menu"
                  aria-expanded={menu?.id === 'view'}
                  aria-label="View options"
                  onClick={(event) => openMenu('view', event.currentTarget)}
                >
                  <Icon name="sliders" />
                </button>
              </div>
            </div>
          )}

          <div className={styles.scroller} data-part="table" onScroll={() => menu && closeMenu()}>
            <table className={styles.table}>
              <caption className={styles.hidden}>
                {title}: {companiesWord(visible.length)}
                {filters.level ? `, ${LEVEL_WORD[filters.level]} intent` : ''}, sorted by{' '}
                {sort.key === 'name' ? 'company' : COLUMNS.find((c) => c.id === sort.key)?.label}
              </caption>
              <thead>
                <tr>
                  <th scope="col" className={styles.checkCol}>
                    <span className={styles.check}>
                      <input
                        type="checkbox"
                        aria-label="Select the companies on this page"
                        checked={allOnPage}
                        ref={(element) => {
                          if (element) element.indeterminate = someOnPage && !allOnPage;
                        }}
                        onChange={() =>
                          setSelected((now) => {
                            const next = new Set(now);
                            for (const id of pageIds) {
                              if (allOnPage) next.delete(id);
                              else next.add(id);
                            }
                            return next;
                          })
                        }
                      />
                      <Icon name={someOnPage && !allOnPage ? 'minus' : 'check'} width={3} />
                    </span>
                  </th>
                  {header('name', 'Company', styles.nameCol)}
                  {columns.map((column) =>
                    header(column.id, column.label, column.numeric ? styles.numCol : undefined),
                  )}
                  <th scope="col" className={styles.moreCol}>
                    <span className={styles.hidden}>Actions</span>
                  </th>
                </tr>
              </thead>
              <tbody>
                {onPage.map((row) => (
                  <tr
                    key={row.id}
                    data-selected={selected.has(row.id) || undefined}
                    data-fresh={fresh === row.id || undefined}
                    data-part="row"
                  >
                    <td className={styles.checkCol}>
                      <span className={styles.check}>
                        <input
                          type="checkbox"
                          aria-label={`Select ${row.name}`}
                          checked={selected.has(row.id)}
                          data-demo={`check-${row.id}`}
                          onChange={() => toggleRow(row.id)}
                        />
                        <Icon name="check" width={3} />
                      </span>
                    </td>
                    <th scope="row" className={styles.nameCol}>
                      <button
                        type="button"
                        className={styles.nameButton}
                        data-demo={`open-${row.id}`}
                        onClick={() => openDrawer(row.id)}
                      >
                        <span className={styles.tile} aria-hidden="true" data-part="tile">
                          <Mark name={row.icon} />
                        </span>
                        <span className={styles.name}>{row.name}</span>
                      </button>
                    </th>
                    {columns.map((column) => (
                      <td key={column.id} className={column.numeric ? styles.numCol : undefined}>
                        {cell(row, column.id)}
                      </td>
                    ))}
                    <td className={styles.moreCol}>
                      <button
                        type="button"
                        className={styles.iconButton}
                        aria-haspopup="menu"
                        aria-expanded={menu?.id === `row:${row.id}`}
                        aria-label={`Actions for ${row.name}`}
                        onClick={(event) => openMenu(`row:${row.id}`, event.currentTarget)}
                      >
                        <Icon name="more" />
                      </button>
                    </td>
                  </tr>
                ))}
                {onPage.length === 0 && (
                  <tr>
                    <td colSpan={columns.length + 3} className={styles.empty}>
                      No companies match.{' '}
                      {filtered && (
                        <button
                          type="button"
                          className={styles.linkButton}
                          onClick={() => filter({ ...NO_FILTERS })}
                        >
                          Clear the filters
                        </button>
                      )}
                    </td>
                  </tr>
                )}
              </tbody>
            </table>
          </div>

          <footer className={styles.foot} data-part="pagination">
            <span className={styles.perPage}>
              <span id={`${uid}-per`}>Rows per page</span>
              <button
                type="button"
                className={styles.perPageButton}
                aria-haspopup="menu"
                aria-labelledby={`${uid}-per ${uid}-per-value`}
                onClick={(event) => openMenu('perpage', event.currentTarget)}
              >
                <span id={`${uid}-per-value`}>{perPage}</span>
                <Icon name="chevronDown" className={styles.chevronSmall} />
              </button>
            </span>
            <span className={styles.grow} />
            <span className={styles.range} aria-live="polite">
              {visible.length
                ? `${at * perPage + 1}–${Math.min(visible.length, (at + 1) * perPage)} of ${companiesWord(visible.length)}`
                : '0 companies'}
            </span>
            {pages > 1 && (
              <nav className={styles.pages} aria-label="Pages">
                <button
                  type="button"
                  className={styles.pageButton}
                  aria-label="Previous page"
                  disabled={at === 0}
                  onClick={() => setPage(at - 1)}
                >
                  <Icon name="chevronLeft" />
                </button>
                {pageList.map((n) => (
                  <button
                    key={n}
                    type="button"
                    className={styles.pageButton}
                    aria-current={n === at ? 'page' : undefined}
                    aria-label={`Page ${n + 1}`}
                    onClick={() => setPage(n)}
                  >
                    {n + 1}
                  </button>
                ))}
                <button
                  type="button"
                  className={styles.pageButton}
                  aria-label="Next page"
                  disabled={at >= pages - 1}
                  onClick={() => setPage(at + 1)}
                >
                  <Icon name="chevronRight" />
                </button>
              </nav>
            )}
          </footer>
        </div>
      </section>

      {drawerRow && drawer && (
        <Drawer
          company={drawerRow}
          owner={ownerOf(drawerRow.owner)}
          lists={lists}
          tab={drawer.tab}
          takeFocus={!demo}
          onTab={(tab) => setDrawer({ id: drawerRow.id, tab })}
          onClose={closeDrawer}
          onMenu={(id, anchor) => openMenu(id, anchor, 'drawer')}
          onEdit={() => openDialog({ kind: 'edit', id: drawerRow.id })}
          menu={menu?.where === 'drawer' ? menuView : null}
        />
      )}
      {dialog?.kind === 'handover' && (
        <HandOverDialog
          rows={rows}
          owners={team}
          takeFocus={!demo}
          onClose={closeDialog}
          onHandOver={(from, to) => {
            handOver(from, to);
            closeDialog();
          }}
        />
      )}
      {(dialog?.kind === 'add' || dialog?.kind === 'edit') && (
        <CompanyDialog
          key={dialog.kind === 'edit' ? dialog.id : 'add'}
          company={dialog.kind === 'edit' ? byId(dialog.id) : undefined}
          owners={team}
          takeFocus={!demo}
          onClose={closeDialog}
          onSubmit={(draft) => {
            if (dialog.kind === 'edit') edit(dialog.id, draft);
            else add(draft);
            closeDialog();
          }}
        />
      )}
      {dialog?.kind === 'newlist' && (
        <NewListDialog
          n={dialog.ids.length}
          takeFocus={!demo}
          onClose={closeDialog}
          onCreate={(name) => {
            createList(name, dialog.ids);
            closeDialog();
          }}
        />
      )}

      {menu?.where !== 'drawer' && menuView}

      <div className={styles.toastLayer}>
        {toast && (
          <div key={toast.n} className={styles.toast} data-part="toast">
            <Icon name="checkCircle" className={styles.toastIcon} />
            <span className={styles.toastText}>
              <span className={styles.toastTitle}>{toast.title}</span>
              <span className={styles.toastBody}>{toast.body}</span>
            </span>
            {toast.undo && (
              <button type="button" className={styles.undo} onClick={undo}>
                Undo
              </button>
            )}
          </div>
        )}
      </div>

      <div className={styles.pointerLayer} aria-hidden="true">
        <DemoPointer
          ref={pointer}
          className={styles.pointer}
          arrowClass={styles.arrow}
          handClass={styles.hand}
        />
      </div>
      <p className={styles.hidden} role="status">
        {message}
      </p>
    </div>
  );
}

/** The Columns menu: a search, then the three groups, each opening its columns with a box each. */
function ColumnsMenu({
  shown,
  onToggle,
}: {
  shown: ColumnId[];
  onToggle: (column: ColumnId) => void;
}) {
  const [query, setQuery] = useState('');
  const [group, setGroup] = useState<(typeof GROUPS)[number] | null>(null);
  const q = query.trim().toLowerCase();
  const list = q
    ? COLUMNS.filter((column) => column.label.toLowerCase().includes(q))
    : group
      ? COLUMNS.filter((column) => column.group === group)
      : null;
  return (
    <>
      <div className={styles.menuSearch}>
        {group && !q ? (
          <button
            type="button"
            className={styles.menuBack}
            aria-label="Back to the groups"
            data-menu-field
            onClick={() => setGroup(null)}
          >
            <Icon name="chevronLeft" />
          </button>
        ) : (
          <Icon name="search" className={styles.menuSearchIcon} />
        )}
        <input
          className={styles.menuSearchInput}
          placeholder="Search columns"
          aria-label="Search columns"
          value={query}
          data-menu-field
          onChange={(event) => setQuery(event.target.value)}
        />
      </div>
      {list ? (
        <>
          {!q && group && <MenuLabel>{group}</MenuLabel>}
          {list.map((column) => (
            <MenuItem
              key={column.id}
              kind="check"
              checked={shown.includes(column.id)}
              onSelect={() => onToggle(column.id)}
            >
              {column.label}
            </MenuItem>
          ))}
          {list.length === 0 && <p className={styles.menuEmpty}>No column by that name</p>}
        </>
      ) : (
        GROUPS.map((name) => {
          const on = COLUMNS.filter(
            (column) => column.group === name && shown.includes(column.id),
          ).length;
          return (
            <MenuItem key={name} trail={on || undefined} onSelect={() => setGroup(name)}>
              <span className={styles.optionLine}>
                {name}
                <Icon name="chevronRight" className={styles.menuGo} />
              </span>
            </MenuItem>
          );
        })
      )}
    </>
  );
}
```

### lead-table/lead-table.module.css

```css
/* Lead table (lead-table.tsx): a light, crisp prospecting table on a white stage, or at night a
   near-black one. The card is a pale frame with the title, holding a white sheet: intent cards,
   the toolbar, the table and its pages. Type is small (13 px in the table, 11.5 px in chips).

   The drawer and the dialogs are layers on the stage, over the card, which blurs behind a grey
   veil. Menus sit on the stage too (or in the panel they open from), placed by the script.
   Under 720 px of stage, the header and toolbar wrap and the cards stack; the table always
   scrolls inside its own box, with the checkbox and Company columns held at its left.

   Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes
   the color-scheme it inherits from the page (light unless the page says otherwise), or the one
   its colorScheme prop sets (data-color-scheme). */

.root {
  /* The stage, the card and its sheet, their lines. */
  --stage: #ffffff;
  --card: #fafafa;
  --surface: #ffffff;
  --surface-2: #f7f7f8;
  --line: #ececee;
  --edge: #e4e4e7;
  --shadow: rgb(0 0 0 / 0.06);
  --shadow-strong: rgb(0 0 0 / 0.12);
  /* Text. */
  --ink: #18181b;
  --ink-2: #6b6b73;
  --ink-3: #a1a1aa;
  /* Hover, a selected row, a pressed tool. */
  --hover: #f4f4f5;
  --row-selected: #fafafa;
  --pressed: #f4f4f5;
  /* The dark button and its text. */
  --accent: #18181b;
  --accent-hover: #2e2e33;
  --accent-ink: #ffffff;
  /* The danger tone (Delete). */
  --danger: #dc2626;
  /* Levels: dots, rings, card chips. */
  --high: #10b981;
  --high-dim: #c9f1e1;
  --high-bg: #e3f7ee;
  --high-ink: #047852;
  --medium: #e4a90a;
  --medium-dim: #f6e7b4;
  --medium-bg: #fdf3d6;
  --medium-ink: #8a5d00;
  --low: #e11d48;
  --low-dim: #f8cdd6;
  --low-bg: #fde6ea;
  --low-ink: #b4123a;
  /* Growth bars. */
  --strong: #10b981;
  --steady: #2f6fed;
  --flat: #e4a90a;
  --down: #e6476b;
  /* Status chips: fill, edge, text, dot. */
  --new-bg: #fdf5dc;
  --new-edge: #f5e2a4;
  --new-ink: #7a5200;
  --new-dot: #e4a90a;
  --research-bg: #f3ecff;
  --research-edge: #e1d2fd;
  --research-ink: #6232c2;
  --research-dot: #8b5cf6;
  --contact-bg: #f1f1f2;
  --contact-edge: #e1e1e4;
  --contact-ink: #27272a;
  --contact-dot: #27272a;
  --qualified-bg: #e5f7ee;
  --qualified-edge: #c3ecd8;
  --qualified-ink: #067a4d;
  --qualified-dot: #10b981;
  --disq-bg: #fde8ec;
  --disq-edge: #f9cbd5;
  --disq-ink: #b4123a;
  --disq-dot: #e11d48;
  /* A new row, lit for a moment. */
  --fresh: #ecfaf3;
  /* The layers: veil, panels, menus, the toast. */
  --veil: rgb(120 120 125 / 0.16);
  --panel: #ffffff;
  --menu: #ffffff;
  --toast: #ffffff;
  /* The headcount chart. */
  --chart: #10b981;
  --chart-fill: rgb(16 185 129 / 0.14);
  /* The same on both schemes: the focus ring, and the demo's pointer, as the system draws it. */
  --focus: #387bff;
  --pointer: #1c1c1e;
  --pointer-edge: #fff;
  --pointer-shadow: rgb(0 0 0 / 0.25);
  --sans: var(--body, Inter, ui-sans-serif, system-ui, sans-serif);

  position: relative;
  container-type: inline-size;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
  width: 100%;
  min-height: 680px;
  padding: 40px 12px 32px;
  overflow: hidden;
  overflow: clip;
  border-radius: 18px;
  background: var(--stage);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.35;
  -webkit-tap-highlight-color: transparent;
}
@supports (color: light-dark(#fff, #000)) {
  .root {
    --stage: light-dark(#ffffff, #0e0e10);
    --card: light-dark(#fafafa, #141416);
    --surface: light-dark(#ffffff, #19191c);
    --surface-2: light-dark(#f7f7f8, #1f1f23);
    --line: light-dark(#ececee, #29292d);
    --edge: light-dark(#e4e4e7, #313136);
    --shadow: light-dark(rgb(0 0 0 / 0.06), rgb(0 0 0 / 0.4));
    --shadow-strong: light-dark(rgb(0 0 0 / 0.12), rgb(0 0 0 / 0.6));
    --ink: light-dark(#18181b, #ededf0);
    --ink-2: light-dark(#6b6b73, #9f9fa8);
    --ink-3: light-dark(#a1a1aa, #686870);
    --hover: light-dark(#f4f4f5, #25252a);
    --row-selected: light-dark(#fafafa, #1e1e22);
    --pressed: light-dark(#f4f4f5, #26262b);
    --accent: light-dark(#18181b, #f2f2f4);
    --accent-hover: light-dark(#2e2e33, #ffffff);
    --accent-ink: light-dark(#ffffff, #141416);
    --danger: light-dark(#dc2626, #f87171);
    --high: light-dark(#10b981, #34d399);
    --high-dim: light-dark(#c9f1e1, #164536);
    --high-bg: light-dark(#e3f7ee, #12332a);
    --high-ink: light-dark(#047852, #6ee7b7);
    --medium: light-dark(#e4a90a, #f5c24a);
    --medium-dim: light-dark(#f6e7b4, #4a3c13);
    --medium-bg: light-dark(#fdf3d6, #352b10);
    --medium-ink: light-dark(#8a5d00, #f5cf73);
    --low: light-dark(#e11d48, #fb6f8d);
    --low-dim: light-dark(#f8cdd6, #4d1b28);
    --low-bg: light-dark(#fde6ea, #3a1620);
    --low-ink: light-dark(#b4123a, #fda4b8);
    --strong: light-dark(#10b981, #34d399);
    --steady: light-dark(#2f6fed, #6b9bff);
    --flat: light-dark(#e4a90a, #f5c24a);
    --down: light-dark(#e6476b, #fb7f9a);
    --new-bg: light-dark(#fdf5dc, #2f2710);
    --new-edge: light-dark(#f5e2a4, #4a3d15);
    --new-ink: light-dark(#7a5200, #f3cd6f);
    --new-dot: light-dark(#e4a90a, #f5c24a);
    --research-bg: light-dark(#f3ecff, #261c3d);
    --research-edge: light-dark(#e1d2fd, #3b2c61);
    --research-ink: light-dark(#6232c2, #c4a9ff);
    --research-dot: light-dark(#8b5cf6, #a78bfa);
    --contact-bg: light-dark(#f1f1f2, #26262b);
    --contact-edge: light-dark(#e1e1e4, #36363c);
    --contact-ink: light-dark(#27272a, #dcdce0);
    --contact-dot: light-dark(#27272a, #dcdce0);
    --qualified-bg: light-dark(#e5f7ee, #11302a);
    --qualified-edge: light-dark(#c3ecd8, #1c4a3c);
    --qualified-ink: light-dark(#067a4d, #6ee7b7);
    --qualified-dot: light-dark(#10b981, #34d399);
    --disq-bg: light-dark(#fde8ec, #36141e);
    --disq-edge: light-dark(#f9cbd5, #56202f);
    --disq-ink: light-dark(#b4123a, #fda4b8);
    --disq-dot: light-dark(#e11d48, #fb6f8d);
    --fresh: light-dark(#ecfaf3, #14302a);
    --veil: light-dark(rgb(120 120 125 / 0.16), rgb(0 0 0 / 0.4));
    --panel: light-dark(#ffffff, #1a1a1d);
    --menu: light-dark(#ffffff, #1f1f23);
    --toast: light-dark(#ffffff, #222226);
    --chart: light-dark(#10b981, #34d399);
    --chart-fill: light-dark(rgb(16 185 129 / 0.14), rgb(52 211 153 / 0.14));
  }
}
/* The colorScheme prop. Without it, nothing is set: color-scheme is inherited from the page. */
.root[data-color-scheme='light'] {
  color-scheme: light;
}
.root[data-color-scheme='dark'] {
  color-scheme: dark;
}
.root[data-color-scheme='system'] {
  color-scheme: light dark;
}

.root button,
.root input {
  font: inherit;
  color: inherit;
}
.root button {
  cursor: pointer;
}
.root :focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}
.root svg {
  display: block;
  flex: none;
}
.root h2,
.root h3,
.root h4,
.root p,
.root dl,
.root dd,
.root ol,
.root ul,
.root figure {
  margin: 0;
  padding: 0;
}
.root ol,
.root ul {
  list-style: none;
}

.hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.grow {
  flex: 1;
}
.none {
  color: var(--ink-3);
  white-space: nowrap;
}

/* ---- The card --------------------------------------------------------------------------------- */

.card {
  width: 100%;
  max-width: 1080px;
  margin: 0 auto;
  border: 1px solid var(--edge);
  border-radius: 12px;
  background: var(--card);
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    filter 0.25s ease,
    opacity 0.3s ease;
}
.card[data-covered] {
  filter: blur(3px);
}
.card[data-fading] {
  opacity: 0;
}

.head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  padding: 10px 14px 9px;
}
.headText {
  flex: 1 1 200px;
  min-width: 0;
}
.title {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.005em;
}
.subtitle {
  margin-top: 1px;
  color: var(--ink-2);
  font-size: 11.5px;
}
.headActions {
  display: flex;
  gap: 8px;
}

/* Buttons: plain, dark, wide (a dialog's one action), and the toolbar's tools. */
.button,
.buttonDark,
.buttonWide,
.tool,
.toolDark,
.toolIcon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 27px;
  padding: 0 10px;
  border: 1px solid var(--edge);
  border-radius: 7px;
  background: var(--surface);
  font-size: 12.5px;
  font-weight: 500;
  white-space: nowrap;
  box-shadow: 0 1px 1px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease,
    opacity 0.15s ease;
}
.button svg,
.buttonDark svg,
.buttonWide svg,
.tool svg,
.toolDark svg,
.toolIcon svg {
  width: 14px;
  height: 14px;
}
.button:hover,
.tool:hover,
.toolIcon:hover,
.button[data-over],
.tool[data-over],
.toolIcon[data-over] {
  background: var(--hover);
}
.buttonDark,
.toolDark,
.buttonWide {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-ink) !important;
}
.buttonDark:hover,
.toolDark:hover,
.buttonWide:hover,
.buttonDark[data-over],
.buttonWide[data-over] {
  border-color: var(--accent-hover);
  background: var(--accent-hover);
}
.buttonDark:disabled,
.buttonWide:disabled {
  opacity: 0.4;
  cursor: default;
}
.buttonWide {
  width: 100%;
  height: 32px;
  font-size: 13px;
}
.tool[data-tone='danger'] {
  color: var(--danger);
}
.tool[data-on],
.tool[aria-pressed='true'],
.tool[aria-expanded='true'],
.toolIcon[aria-expanded='true'] {
  border-color: var(--ink-3);
  background: var(--pressed);
}
.toolIcon {
  width: 27px;
  padding: 0;
}
.toolCount {
  display: grid;
  place-items: center;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 10.5px;
  font-weight: 600;
}

.iconButton {
  display: inline-grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--ink);
}
.iconButton svg {
  width: 15px;
  height: 15px;
}
.iconButton:hover,
.iconButton[data-over],
.iconButton[aria-expanded='true'] {
  background: var(--hover);
}

/* ---- The sheet: cards, toolbar, table, pages --------------------------------------------------- */

.sheet {
  border-top: 1px solid var(--line);
  border-radius: 0 0 11px 11px;
  background: var(--surface);
}

.cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  padding: 8px 14px 0;
}
.intentCard {
  display: grid;
  gap: 9px;
  min-width: 0;
  padding: 9px 10px 8px 11px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--surface);
  text-align: left;
  transition:
    background-color 0.18s ease,
    border-color 0.18s ease,
    box-shadow 0.18s ease;
}
.intentCard:hover,
.intentCard[data-over] {
  border-color: var(--edge);
  background: var(--surface-2);
}
.intentCard[aria-pressed='true'] {
  border-color: var(--ink-3);
  background: var(--surface-2);
  box-shadow: inset 0 1px 2px var(--shadow);
}
.cardTop,
.cardBottom {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.cardTop {
  justify-content: space-between;
}
.cardNote,
.cardShare {
  color: var(--ink-2);
  font-size: 11.5px;
  white-space: nowrap;
}
.cardCount {
  min-width: 16px;
  font-size: 17px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.levelChip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 19px;
  padding: 0 7px 0 6px;
  border-radius: 5px;
  font-size: 11.5px;
  font-weight: 500;
  white-space: nowrap;
}
.levelChip[data-level='high'] {
  background: var(--high-bg);
  color: var(--high-ink);
  --dot: var(--high);
}
.levelChip[data-level='medium'] {
  background: var(--medium-bg);
  color: var(--medium-ink);
  --dot: var(--medium);
}
.levelChip[data-level='low'] {
  background: var(--low-bg);
  color: var(--low-ink);
  --dot: var(--low);
}

.dots {
  display: flex;
  flex: 1;
  gap: 3px;
  min-width: 0;
  overflow: hidden;
}
.dot {
  flex: none;
  width: 6.5px;
  height: 6.5px;
  border-radius: 50%;
  background: var(--dim);
  transition: background-color 0.35s ease;
}
.dot[data-lit] {
  background: var(--lit);
}
.intentCard[data-level='high'] {
  --lit: var(--high);
  --dim: var(--high-dim);
}
.intentCard[data-level='medium'] {
  --lit: var(--medium);
  --dim: var(--medium-dim);
}
.intentCard[data-level='low'] {
  --lit: var(--low);
  --dim: var(--low-dim);
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  min-height: 45px;
  padding: 9px 14px;
}
.toolGroup {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.bulkFacts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  color: var(--ink-2);
  font-size: 12.5px;
  animation: rise 0.22s ease both;
}
.bulkFacts strong {
  color: var(--ink);
  font-weight: 600;
}

.search {
  position: relative;
  display: flex;
  align-items: center;
  flex: 0 1 220px;
  min-width: 150px;
  height: 27px;
  border: 1px solid var(--edge);
  border-radius: 7px;
  background: var(--surface);
  transition: border-color 0.15s ease;
}
.search:focus-within {
  border-color: var(--ink-3);
  box-shadow: 0 0 0 3px var(--hover);
}
.searchIcon {
  position: absolute;
  left: 8px;
  width: 14px;
  height: 14px;
  color: var(--ink-2);
  pointer-events: none;
}
.searchInput {
  flex: 1;
  min-width: 0;
  height: 100%;
  padding: 0 26px 0 28px;
  border: 0;
  border-radius: 7px;
  outline: 0 !important;
  background: none;
  font-size: 12.5px !important;
  appearance: none;
}
.searchInput::placeholder {
  color: var(--ink-3);
}
.searchInput::-webkit-search-cancel-button {
  display: none;
}
.searchClear {
  position: absolute;
  right: 4px;
  display: grid;
  place-items: center;
  width: 19px;
  height: 19px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--ink-2);
}
.searchClear:hover {
  background: var(--hover);
}
.searchClear svg {
  width: 12px;
  height: 12px;
}

/* The table scrolls inside its box; its first two columns stay at its left. */
.scroller {
  overflow-x: auto;
  border-top: 1px solid var(--line);
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}
.table {
  width: 100%;
  min-width: max-content;
  border-collapse: separate;
  border-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.table th,
.table td {
  height: 36px;
  padding: 0 8px;
  border-bottom: 1px solid var(--line);
  border-left: 1px solid var(--line);
  font-weight: 400;
  text-align: left;
  white-space: nowrap;
  vertical-align: middle;
}
.root[data-compact] .table td,
.root[data-compact] tbody th {
  height: 30px;
}
.table thead th {
  height: 27px;
  color: var(--ink-2);
  font-size: 12.5px;
}
.table tr > :first-child {
  border-left: 0;
}
.table tbody tr:last-child > * {
  border-bottom: 0;
}
.table tbody tr {
  transition: background-color 0.2s ease;
}
.table tbody tr:hover > * {
  background: var(--row-selected);
}
.table tbody tr[data-selected] > * {
  background: var(--row-selected);
}
.table tbody tr[data-fresh] > * {
  animation: fresh 4.2s ease both;
}
@keyframes fresh {
  0%,
  70% {
    background: var(--fresh);
  }
  100% {
    background: var(--surface);
  }
}
.checkCol {
  position: sticky;
  left: 0;
  z-index: 1;
  width: 36px;
  padding: 0 0 0 14px !important;
  background: var(--surface);
}
.nameCol {
  position: sticky;
  left: 36px;
  z-index: 1;
  min-width: 158px;
  background: var(--surface);
  border-left: 1px solid var(--line) !important;
}
.numCol {
  min-width: 52px;
}
.moreCol {
  width: 34px;
  padding: 0 5px !important;
  text-align: center !important;
}
.sortButton {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 22px;
  margin-left: -4px;
  padding: 0 4px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--ink-2) !important;
  white-space: nowrap;
}
.sortButton:hover {
  background: var(--hover);
  color: var(--ink) !important;
}
th[aria-sort] .sortButton {
  color: var(--ink) !important;
}
.sortIcon {
  width: 11px;
  height: 11px;
}

.check {
  position: relative;
  display: grid;
  width: 15px;
  height: 15px;
}
.check input {
  width: 15px;
  height: 15px;
  margin: 0;
  border: 1px solid var(--edge);
  border-radius: 4px;
  background: var(--surface);
  appearance: none;
  cursor: pointer;
  transition:
    background-color 0.12s ease,
    border-color 0.12s ease;
}
.check input:hover,
.check input[data-over] {
  border-color: var(--ink-3);
}
.check input:checked,
.check input:indeterminate {
  border-color: var(--accent);
  background: var(--accent);
}
.check svg {
  position: absolute;
  inset: 2.5px;
  width: 10px;
  height: 10px;
  color: var(--accent-ink);
  opacity: 0;
  pointer-events: none;
}
.check input:checked + svg,
.check input:indeterminate + svg {
  opacity: 1;
}

.nameButton {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  max-width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font-weight: 500;
  text-align: left;
}
.name {
  overflow: hidden;
  text-overflow: ellipsis;
  text-underline-offset: 3px;
}
.nameButton:hover .name,
.nameButton[data-over] .name {
  text-decoration: underline;
  text-decoration-color: var(--ink-3);
}
.tile,
.tileLarge {
  display: grid;
  place-items: center;
  flex: none;
  width: 20px;
  height: 20px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--surface);
  color: var(--ink);
}
.tile svg {
  width: 12px;
  height: 12px;
}
.tileLarge {
  width: 32px;
  height: 32px;
  border-radius: 8px;
}
.tileLarge svg {
  width: 17px;
  height: 17px;
}

.city {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.flag {
  flex: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 0.5px rgb(0 0 0 / 0.18);
}

.growth {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.spark {
  width: 34px;
  height: 16px;
  fill: var(--tone);
}
.growthValue {
  min-width: 34px;
  color: var(--ink-2);
  font-size: 11.5px;
}
.growth[data-tone='strong'] {
  --tone: var(--strong);
}
.growth[data-tone='steady'] {
  --tone: var(--steady);
}
.growth[data-tone='flat'] {
  --tone: var(--flat);
}
.growth[data-tone='down'] {
  --tone: var(--down);
}

.intent {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.ring {
  width: 15px;
  height: 15px;
}
.ringTrack {
  fill: none;
  stroke: var(--line);
  stroke-width: 2.4;
}
.ringArc {
  fill: none;
  stroke: var(--level);
  stroke-width: 2.4;
  stroke-linecap: round;
  transition: stroke-dasharray 0.4s ease;
}
.intentScore {
  min-width: 15px;
  font-weight: 600;
}
.intentWord {
  color: var(--ink-3);
}
.intent[data-level='high'] {
  --level: var(--high);
}
.intent[data-level='medium'] {
  --level: var(--medium);
}
.intent[data-level='low'] {
  --level: var(--low);
}

/* Status chips. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 19px;
  padding: 0 6px 0 5px;
  border: 1px solid var(--chip-edge);
  border-radius: 5px;
  background: var(--chip-bg);
  color: var(--chip-ink);
  font-size: 11.5px;
  font-weight: 500;
  white-space: nowrap;
  --dot: var(--chip-dot);
}
.chipDot {
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--dot);
}
.chip[data-status='new'] {
  --chip-bg: var(--new-bg);
  --chip-edge: var(--new-edge);
  --chip-ink: var(--new-ink);
  --chip-dot: var(--new-dot);
}
.chip[data-status='researching'] {
  --chip-bg: var(--research-bg);
  --chip-edge: var(--research-edge);
  --chip-ink: var(--research-ink);
  --chip-dot: var(--research-dot);
}
.chip[data-status='contacted'] {
  --chip-bg: var(--contact-bg);
  --chip-edge: var(--contact-edge);
  --chip-ink: var(--contact-ink);
  --chip-dot: var(--contact-dot);
}
.chip[data-status='qualified'] {
  --chip-bg: var(--qualified-bg);
  --chip-edge: var(--qualified-edge);
  --chip-ink: var(--qualified-ink);
  --chip-dot: var(--qualified-dot);
}
.chip[data-status='disqualified'] {
  --chip-bg: var(--disq-bg);
  --chip-edge: var(--disq-edge);
  --chip-ink: var(--disq-ink);
  --chip-dot: var(--disq-dot);
}
.chipButton {
  display: inline-flex;
  margin: 0 -3px;
  padding: 3px;
  border: 0;
  border-radius: 7px;
  background: none;
}
.chipButton:hover,
.chipButton[data-over],
.chipButton[aria-expanded='true'] {
  background: var(--hover);
}

.ownerButton {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 24px;
  margin-left: -5px;
  padding: 0 6px 0 5px;
  border: 0;
  border-radius: 6px;
  background: none;
}
.ownerButton[data-empty] {
  color: var(--ink-3);
}
.ownerButton:hover,
.ownerButton[data-over],
.ownerButton[aria-expanded='true'] {
  background: var(--hover);
}
.chevron {
  width: 14px;
  height: 14px;
  color: var(--ink-2);
}
.chevronSmall {
  width: 12px;
  height: 12px;
  color: var(--ink-3);
}

.avatar,
.avatarNone {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: var(--size, 18px);
  height: var(--size, 18px);
  border-radius: 50%;
}
.avatar {
  background: var(--tint);
  color: #fff;
  font-size: calc(var(--size, 18px) * 0.42);
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1;
}
.avatarNone {
  border: 1px dashed var(--ink-3);
  color: var(--ink-3);
}
.avatarNone svg {
  width: 72%;
  height: 72%;
}

.empty {
  height: 96px !important;
  color: var(--ink-2);
  text-align: center !important;
}
.linkButton {
  padding: 0;
  border: 0;
  background: none;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  min-height: 40px;
  padding: 6px 14px;
  border-top: 1px solid var(--line);
  color: var(--ink-2);
  font-size: 12.5px;
}
.perPage {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.perPageButton {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 25px;
  padding: 0 7px 0 9px;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: var(--surface);
  color: var(--ink) !important;
}
.range {
  white-space: nowrap;
}
.pages {
  display: flex;
  gap: 2px;
}
.pageButton {
  display: grid;
  place-items: center;
  min-width: 25px;
  height: 25px;
  padding: 0 4px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  color: var(--ink) !important;
  font-size: 12.5px;
}
.pageButton svg {
  width: 14px;
  height: 14px;
}
.pageButton:hover:not(:disabled) {
  background: var(--hover);
}
.pageButton[aria-current='page'] {
  border-color: var(--edge);
  background: var(--surface-2);
  font-weight: 500;
}
.pageButton:disabled {
  opacity: 0.35;
  cursor: default;
}

/* ---- Menus ------------------------------------------------------------------------------------ */

.menu {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 6;
  display: grid;
  min-width: 168px;
  max-width: min(280px, calc(100% - 16px));
  max-height: 330px;
  padding: 4px;
  overflow-y: auto;
  border: 1px solid var(--edge);
  border-radius: 9px;
  background: var(--menu);
  box-shadow:
    0 6px 20px var(--shadow-strong),
    0 1px 2px var(--shadow);
  font-size: 12.5px;
  animation: pop 0.14s ease-out both;
}
.menu[data-wide] {
  min-width: 210px;
}
@keyframes pop {
  from {
    opacity: 0;
    scale: 0.97;
  }
}
.menuLabel {
  padding: 5px 7px 3px;
  color: var(--ink-2);
  font-size: 11px;
}
.menuItem {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 27px;
  padding: 3px 7px;
  border: 0;
  border-radius: 6px;
  background: none;
  text-align: left;
  white-space: nowrap;
}
.menuItem:hover,
.menuItem:focus-visible,
.menuItem[data-over] {
  outline: 0;
  background: var(--hover);
}
.menuItem[data-tone='danger'] {
  color: var(--danger) !important;
}
.menuText {
  flex: 1;
  min-width: 0;
}
.menuTrail {
  color: var(--ink-3);
  font-size: 11.5px;
}
.menuCheck {
  width: 14px;
  height: 14px;
}
.menuBox {
  display: grid;
  place-items: center;
  width: 14px;
  height: 14px;
  border: 1px solid var(--edge);
  border-radius: 4px;
}
.menuBox[data-on] {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-ink);
}
.menuBox svg {
  width: 10px;
  height: 10px;
}
.menuRule {
  height: 1px;
  margin: 4px -4px;
  border: 0;
  background: var(--line);
}
.menuIcon {
  width: 14px;
  height: 14px;
  color: var(--ink-2);
}
.menuItem[data-tone='danger'] .menuIcon {
  color: inherit;
}
.optionLine {
  display: flex;
  align-items: center;
  gap: 8px;
}
.optionLine[data-muted] {
  color: var(--ink-2);
}
.menuSearch {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: -4px -4px 4px;
  padding: 0 8px;
  border-bottom: 1px solid var(--line);
}
.menuSearchIcon {
  width: 14px;
  height: 14px;
  color: var(--ink-3);
}
.menuBack {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  margin-left: -3px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--ink-2) !important;
}
.menuBack:hover {
  background: var(--hover);
}
.menuBack svg {
  width: 14px;
  height: 14px;
}
.menuSearchInput {
  flex: 1;
  min-width: 0;
  height: 32px;
  border: 0;
  outline: 0 !important;
  background: none;
  font-size: 12.5px !important;
}
.menuSearchInput::placeholder {
  color: var(--ink-3);
}
.menuGo {
  width: 13px;
  height: 13px;
  margin-left: auto;
  color: var(--ink-3);
  order: 3;
}
.menuEmpty {
  padding: 8px 7px;
  color: var(--ink-3);
}

/* ---- Panels: the drawer and the dialogs -------------------------------------------------------- */

.layer {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: grid;
}
.veil {
  position: absolute;
  inset: 0;
  background: var(--veil);
  animation: fade 0.22s ease both;
}
@keyframes fade {
  from {
    opacity: 0;
  }
}
.panel {
  position: absolute;
  display: flex;
  flex-direction: column;
  overflow: clip;
  border: 1px solid var(--edge);
  border-radius: 12px;
  background: var(--panel);
  box-shadow:
    0 18px 50px var(--shadow-strong),
    0 2px 6px var(--shadow);
}
.panel[data-kind='drawer'],
.panel[data-kind='side'] {
  top: 12px;
  right: 12px;
  bottom: 12px;
  width: min(480px, calc(100% - 24px));
  animation: slide 0.32s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.panel[data-kind='center'] {
  top: 50%;
  left: 50%;
  width: min(430px, calc(100% - 24px));
  max-height: calc(100% - 24px);
  translate: -50% -50%;
  animation: rise 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes slide {
  from {
    opacity: 0;
    transform: translateX(28px);
  }
}
@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.985);
  }
}

.drawerHead {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 14px 10px 16px;
}
.drawerTitle {
  flex: 1;
  min-width: 0;
}
.drawerNameLine {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
}
.drawerName {
  font-size: 14px;
  font-weight: 600;
}
.drawerSub {
  margin-top: 2px;
  color: var(--ink-2);
  font-size: 11.5px;
}
.drawerTools {
  display: flex;
  align-self: flex-start;
  gap: 2px;
}
.tabs {
  display: flex;
  gap: 18px;
  padding: 0 16px;
  border-bottom: 1px solid var(--line);
}
.tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-2) !important;
  font-size: 12.5px;
}
.tab:hover,
.tab[data-over] {
  color: var(--ink) !important;
}
.tab[aria-selected='true'] {
  color: var(--ink) !important;
  font-weight: 500;
}
.tab[aria-selected='true']::after {
  content: '';
  position: absolute;
  right: 0;
  bottom: -1px;
  left: 0;
  height: 2px;
  border-radius: 1px;
  background: var(--accent);
}
.tabCount {
  display: grid;
  place-items: center;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--ink-2);
  font-size: 10.5px;
  font-weight: 500;
}
.drawerBody {
  display: grid;
  flex: 1;
  align-content: start;
  gap: 10px;
  min-height: 0;
  padding: 12px 14px;
  overflow-y: auto;
  outline-offset: -2px !important;
  overscroll-behavior: contain;
}
.drawerBody > * {
  animation: fade 0.25s ease both;
}
.section {
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
}
.sectionHead {
  padding: 8px 11px 7px;
}
.sectionTitle {
  font-size: 12.5px;
  font-weight: 600;
}
.sectionNote {
  color: var(--ink-2);
  font-size: 11px;
}
.sectionBody {
  margin: 0 -1px -1px;
  padding: 8px 11px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
}
.pairs {
  display: grid;
  gap: 2px;
}
.pair {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 24px;
  font-size: 12.5px;
}
.pair dt {
  color: var(--ink-2);
}
.pair dd {
  text-align: right;
}
.trend {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.trend svg {
  width: 13px;
  height: 13px;
  color: var(--strong);
}
.trend[data-down] svg {
  color: var(--down);
}
.ownerInline {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.chart {
  position: relative;
  padding-top: 4px;
}
.chartSvg {
  width: 100%;
  height: 64px;
  overflow: visible;
}
.chartArea {
  fill: var(--chart-fill);
}
.chartLine {
  fill: none;
  stroke: var(--chart);
  stroke-width: 1.6;
}
.chartEnd {
  position: absolute;
  right: -2.5px;
  width: 5px;
  height: 5px;
  margin-top: 1.5px;
  border-radius: 50%;
  background: var(--chart);
}
.chartAxis {
  display: flex;
  justify-content: space-between;
  margin-top: 6px;
  color: var(--ink-2);
  font-size: 11px;
}

.people,
.signals {
  display: grid;
  gap: 2px;
}
.person,
.signal {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
}
.signal {
  align-items: flex-start;
  padding: 6px 0;
}
.personMark,
.signalIcon {
  display: grid;
  place-items: center;
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--ink-2);
  font-size: 10px;
  font-weight: 600;
}
.signalIcon {
  border-radius: 7px;
}
.signalIcon svg {
  width: 14px;
  height: 14px;
}
.personText {
  display: grid;
  flex: 1;
  min-width: 0;
}
.personName {
  font-size: 12.5px;
  font-weight: 500;
}
.personRole {
  color: var(--ink-2);
  font-size: 11.5px;
}
.signalDate {
  color: var(--ink-3);
  font-size: 11px;
}
.emptyNote {
  padding: 6px 0;
  color: var(--ink-2);
}

.drawerFoot {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}
.selectButton,
.picker {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 28px;
  padding: 0 7px 0 6px;
  border: 1px solid var(--edge);
  border-radius: 7px;
  background: var(--surface);
  box-shadow: 0 1px 1px var(--shadow);
}
.selectButton:hover,
.picker:hover,
.picker[data-over] {
  background: var(--hover);
}
.selectButton .chevron {
  margin-left: 10px;
}

/* Dialogs. */
.dialogHead {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 14px 10px 16px;
}
.dialogHead > div {
  flex: 1;
}
.dialogHead[data-ruled] {
  border-bottom: 1px solid var(--line);
}
.dialogTitle {
  font-size: 14px;
  font-weight: 600;
}
.dialogNote {
  margin-top: 3px;
  color: var(--ink-2);
  font-size: 12px;
}
.dialogBody {
  display: grid;
  gap: 0;
  min-height: 0;
  padding: 2px 16px 12px;
  overflow-y: auto;
}
.dialogFoot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 10px 14px 14px;
}
.dialogFoot[data-ruled] {
  padding: 10px 14px;
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}

.handBox {
  display: grid;
  gap: 4px;
  padding: 9px 12px 10px;
  border-radius: 9px;
  background: var(--surface-2);
}
.handTop {
  display: flex;
  justify-content: space-between;
  color: var(--ink-2);
  font-size: 11.5px;
}
.handRow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.handRow .picker {
  width: auto;
  min-width: 158px;
  max-width: 60%;
}
.handBig {
  display: grid;
  justify-items: end;
}
.handBig strong {
  font-size: 22px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}
.handBig span {
  color: var(--ink-2);
  white-space: nowrap;
  font-size: 11.5px;
}
.swap {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  justify-self: center;
  width: 26px;
  height: 26px;
  margin: -9px 0;
  padding: 0;
  border: 1px solid var(--edge);
  border-radius: 7px;
  background: var(--surface);
  box-shadow: 0 1px 2px var(--shadow);
}
.swap:hover {
  background: var(--hover);
}
.swap svg {
  width: 14px;
  height: 14px;
}
.handHot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 14px;
}
.handHot > span:first-child {
  display: grid;
}
.handHot strong {
  font-size: 13px;
  font-weight: 600;
}
.handHot > span:first-child > span {
  color: var(--ink-2);
  font-size: 11.5px;
}
.handDots {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 3px;
  max-width: 50%;
}
.handDot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink-3);
}
.handDot[data-level='high'] {
  background: var(--high);
}
.handDot[data-level='medium'] {
  background: var(--medium);
}
.handDot[data-level='low'] {
  background: var(--low);
}
.handFacts {
  display: grid;
  gap: 6px;
  margin-top: 12px;
  padding-bottom: 4px;
  font-size: 12.5px;
}
.handFacts > div {
  display: flex;
  justify-content: space-between;
  gap: 12px;
}
.handFacts dt {
  color: var(--ink-2);
}

.form {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-height: 0;
}
.formBody {
  display: grid;
  flex: 1;
  align-content: start;
  gap: 12px;
  min-height: 0;
  padding: 14px 16px;
  overflow-y: auto;
}
.formGrid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px 14px;
}
.field {
  display: grid;
  gap: 5px;
  min-width: 0;
}
.fieldLabel {
  font-size: 12px;
  font-weight: 500;
}
.fieldNote {
  color: var(--ink-2);
  font-size: 11.5px;
}
.input {
  width: 100%;
  height: 28px;
  padding: 0 9px;
  border: 1px solid var(--edge);
  border-radius: 7px;
  outline: 0 !important;
  background: var(--surface);
  font-size: 12.5px !important;
  transition:
    border-color 0.15s ease,
    box-shadow 0.15s ease;
}
.input::placeholder {
  color: var(--ink-3);
}
.input:focus {
  border-color: var(--ink-3);
  box-shadow: 0 0 0 3px var(--hover);
}
.picker {
  width: 100%;
  font-size: 12.5px;
  text-align: left;
}
.pickerText {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.stepper {
  display: flex;
  align-items: center;
  height: 28px;
  border: 1px solid var(--edge);
  border-radius: 7px;
  background: var(--surface);
}
.stepButton {
  display: grid;
  place-items: center;
  width: 28px;
  height: 100%;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
}
.stepButton:hover {
  background: var(--hover);
}
.stepButton svg {
  width: 13px;
  height: 13px;
}
.stepInput {
  flex: 1;
  min-width: 0;
  height: 100%;
  border: 0;
  outline-offset: -2px;
  background: none;
  font-size: 12.5px !important;
  text-align: center;
}

/* ---- The toast ---------------------------------------------------------------------------------- */

.toastLayer {
  position: absolute;
  top: 10px;
  left: 50%;
  z-index: 7;
  width: min(320px, calc(100% - 24px));
  translate: -50% 0;
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 10px 10px 10px 11px;
  border: 1px solid var(--edge);
  border-radius: 10px;
  background: var(--toast);
  box-shadow:
    0 10px 28px var(--shadow-strong),
    0 1px 2px var(--shadow);
  pointer-events: auto;
  animation:
    toast-in 0.34s cubic-bezier(0.2, 0.9, 0.25, 1.15) both,
    toast-out 0.3s ease 4.7s forwards;
}
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(-14px) scale(0.97);
  }
}
@keyframes toast-out {
  to {
    opacity: 0;
    transform: translateY(-8px);
  }
}
.toastIcon {
  width: 15px;
  height: 15px;
  margin-top: 1px;
  color: var(--high);
}
.toastText {
  display: grid;
  flex: 1;
  min-width: 0;
  gap: 1px;
}
.toastTitle {
  font-size: 12.5px;
  font-weight: 500;
}
.toastBody {
  color: var(--ink-2);
  font-size: 11.5px;
}
.undo {
  height: 21px;
  padding: 0 7px;
  border: 0;
  border-radius: 5px;
  background: var(--accent);
  color: var(--accent-ink) !important;
  font-size: 11px !important;
  font-weight: 500;
}
.undo:hover {
  background: var(--accent-hover);
}

/* ---- Narrow ------------------------------------------------------------------------------------- */

@container (max-width: 720px) {
  .cards {
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  }
  .search {
    flex: 1 1 100%;
  }
  .toolbar > .grow {
    display: none;
  }
}
@container (max-width: 480px) {
  .root {
    padding: 28px 8px 16px;
  }
  .head,
  .toolbar,
  .foot {
    padding-right: 10px;
    padding-left: 10px;
  }
  .cards {
    padding: 8px 10px 0;
  }
  .headActions {
    width: 100%;
  }
  .headActions > * {
    flex: 1;
  }
  .nameCol {
    min-width: 150px;
  }
  .formGrid {
    grid-template-columns: minmax(0, 1fr);
  }
  .panel[data-kind='drawer'],
  .panel[data-kind='side'] {
    top: 8px;
    right: 8px;
    bottom: auto;
    width: calc(100% - 16px);
    height: min(calc(100% - 16px), 720px);
  }
  /* Near the top, where Hand Over is, on a tall phone stage. */
  .panel[data-kind='center'] {
    top: 88px;
    max-height: calc(100% - 96px);
    translate: -50% 0;
  }
}

/* ---- The demo's pointer ---------------------------------------------------------------------------- */

.pointerLayer {
  position: absolute;
  inset: 0;
  z-index: 8;
  pointer-events: none;
}
.pointer {
  width: 20px;
  height: 20px;
}
.hand {
  visibility: hidden;
}
.pointer[data-glyph='hand'] .arrow {
  visibility: hidden;
}
.pointer[data-glyph='hand'] .hand {
  visibility: visible;
}

@media (prefers-reduced-motion: reduce) {
  .card,
  .dot,
  .ringArc,
  .intentCard,
  .table tbody tr {
    transition: none;
  }
  .menu,
  .veil,
  .panel,
  .bulkFacts,
  .drawerBody > *,
  .toast,
  .table tbody tr[data-fresh] > * {
    animation: none;
  }
}
```

### lead-table/data.ts

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

/*
 * The lead table's data: the companies, the four owners, the saved lists, and the small rules that
 * read them (an intent score's level, a status that closes an account, the numbers a row shows).
 * Every company and person here is made up.
 */

export type Level = 'high' | 'medium' | 'low';
export type Status = 'new' | 'researching' | 'contacted' | 'qualified' | 'disqualified';

/** The monochrome mark on a company's tile. */
export type CompanyIcon =
  | 'chart'
  | 'flask'
  | 'route'
  | 'robot'
  | 'database'
  | 'layers'
  | 'cloud'
  | 'shield'
  | 'card'
  | 'wallet'
  | 'target'
  | 'cart'
  | 'cross'
  | 'store'
  | 'book'
  | 'cap'
  | 'wrench'
  | 'megaphone'
  | 'flame'
  | 'palette'
  | 'umbrella'
  | 'factory'
  | 'video'
  | 'bank'
  | 'anchor'
  | 'truck'
  | 'shirt'
  | 'box';

export type Owner = {
  id: string;
  name: string;
  /** The avatar's fill: initials sit on it. */
  color: string;
  /** The reader: "You" in the owner menu, and My Accounts filters by them. */
  you?: boolean;
};

export type Company = {
  id: string;
  name: string;
  domain: string;
  icon: CompanyIcon;
  industry: string;
  city: string;
  /** Two letters: the colours of the round mark beside the city. */
  country: string;
  /** Headcount growth over six months, in %; null for a company with no history yet. */
  growth: number | null;
  /** Employees. */
  size: number;
  /** Intent score, 0–100; null until it is scored. */
  intent: number | null;
  /** People at the company in the database; null when none are matched yet. */
  contacts: number | null;
  status: Status;
  /** An owner's id, or null when unassigned. */
  owner: string | null;
  funding: string;
  /** Yearly revenue, in millions of dollars. */
  revenue: number;
  founded: number;
};

/** A saved list: the companies saved to it, by id. */
export type SavedList = { id: string; name: string; ids: string[] };

export const OWNERS: Owner[] = [
  { id: 'maya', name: 'Maya Chen', color: '#c9837a', you: true },
  { id: 'theo', name: 'Theo Park', color: '#7d93b8' },
  { id: 'iris', name: 'Iris Novak', color: '#b59a6a' },
  { id: 'sam', name: 'Sam Okafor', color: '#7fa58c' },
];

export const STATUSES: Status[] = ['new', 'researching', 'contacted', 'qualified', 'disqualified'];

export const STATUS_WORD: Record<Status, string> = {
  new: 'New',
  researching: 'Researching',
  contacted: 'Contacted',
  qualified: 'Qualified',
  disqualified: 'Disqualified',
};

export const LEVEL_WORD: Record<Level, string> = { high: 'High', medium: 'Medium', low: 'Low' };

export const COUNTRIES: Record<string, { name: string; stripes: string[]; vertical?: boolean }> = {
  DE: { name: 'Germany', stripes: ['#1a1a1a', '#dd0000', '#ffce00'] },
  NL: { name: 'Netherlands', stripes: ['#ae1c28', '#ffffff', '#21468b'] },
  FR: { name: 'France', stripes: ['#0055a4', '#ffffff', '#ef4135'], vertical: true },
  PL: { name: 'Poland', stripes: ['#ffffff', '#dc143c'] },
  AT: { name: 'Austria', stripes: ['#ed2939', '#ffffff', '#ed2939'] },
  IT: { name: 'Italy', stripes: ['#009246', '#ffffff', '#ce2b37'], vertical: true },
  BE: { name: 'Belgium', stripes: ['#1a1a1a', '#fdda24', '#ef3340'], vertical: true },
  IE: { name: 'Ireland', stripes: ['#169b62', '#ffffff', '#ff883e'], vertical: true },
  ES: { name: 'Spain', stripes: ['#aa151b', '#f1bf00', '#f1bf00', '#aa151b'] },
  EE: { name: 'Estonia', stripes: ['#0072ce', '#1a1a1a', '#ffffff'] },
  LT: { name: 'Lithuania', stripes: ['#fdb913', '#006a44', '#c1272d'] },
};

/** The cities the Add Company form offers, with their country. */
export const CITIES: [string, string][] = [
  ['Amsterdam', 'NL'],
  ['Antwerp', 'BE'],
  ['Berlin', 'DE'],
  ['Dublin', 'IE'],
  ['Hamburg', 'DE'],
  ['Krakow', 'PL'],
  ['Lyon', 'FR'],
  ['Madrid', 'ES'],
  ['Milan', 'IT'],
  ['Munich', 'DE'],
  ['Tallinn', 'EE'],
  ['Vienna', 'AT'],
  ['Vilnius', 'LT'],
  ['Warsaw', 'PL'],
];

export const INDUSTRIES = [
  'Education',
  'Fintech',
  'Healthcare',
  'Logistics',
  'Manufacturing',
  'Marketing',
  'Retail',
  'Software',
];

export const FUNDING = ['Bootstrapped', 'Seed', 'Series A', 'Series B', 'Series C', 'Public'];

type Row = [
  name: string,
  icon: CompanyIcon,
  industry: string,
  city: string,
  growth: number,
  size: number,
  intent: number,
  contacts: number,
  status: Status,
  owner: string | null,
  funding: string,
  revenue: number,
  founded: number,
];

const ROWS: Row[] = [
  // High intent: 70 and up.
  [
    'Quillstone Analytics',
    'chart',
    'Software',
    'Berlin',
    22,
    240,
    86,
    42,
    'new',
    null,
    'Series B',
    38,
    2017,
  ],
  [
    'Brightwater Bio',
    'flask',
    'Healthcare',
    'Munich',
    38,
    88,
    72,
    10,
    'new',
    'iris',
    'Series A',
    9,
    2020,
  ],
  [
    'Copperline Logistics',
    'route',
    'Logistics',
    'Hamburg',
    13,
    340,
    70,
    31,
    'contacted',
    'sam',
    'Series B',
    46,
    2014,
  ],
  [
    'Halvard Robotics',
    'robot',
    'Manufacturing',
    'Munich',
    11,
    520,
    81,
    57,
    'qualified',
    'theo',
    'Series C',
    74,
    2012,
  ],
  [
    'Larkfield Data',
    'database',
    'Software',
    'Dublin',
    15,
    380,
    83,
    38,
    'contacted',
    'iris',
    'Series B',
    41,
    2016,
  ],
  ['Mossgate', 'layers', 'Software', 'Warsaw', 36, 95, 91, 12, 'new', 'maya', 'Series A', 11, 2021],
  [
    'Northpeak Cloud',
    'cloud',
    'Software',
    'Vienna',
    16,
    1900,
    73,
    96,
    'contacted',
    'maya',
    'Series C',
    210,
    2011,
  ],
  [
    'Riverly',
    'shield',
    'Fintech',
    'Vilnius',
    17,
    560,
    76,
    49,
    'qualified',
    'maya',
    'Series B',
    63,
    2015,
  ],
  [
    'Saltmarsh Pay',
    'card',
    'Fintech',
    'Tallinn',
    29,
    130,
    88,
    19,
    'researching',
    'maya',
    'Series A',
    14,
    2019,
  ],
  [
    'Tidemark Pay',
    'wallet',
    'Fintech',
    'Amsterdam',
    16,
    410,
    78,
    64,
    'researching',
    'maya',
    'Series B',
    52,
    2016,
  ],
  // Medium intent: 40 to 69.
  [
    'Ashcombe Systems',
    'target',
    'Manufacturing',
    'Hamburg',
    5,
    1450,
    64,
    74,
    'contacted',
    'theo',
    'Public',
    320,
    1998,
  ],
  ['Bellmoor Commerce', 'cart', 'Retail', 'Berlin', 12, 58, 52, 8, 'new', null, 'Seed', 4, 2022],
  [
    'Corvane Health',
    'cross',
    'Healthcare',
    'Amsterdam',
    2,
    130,
    54,
    23,
    'contacted',
    'iris',
    'Series A',
    16,
    2018,
  ],
  [
    'Fernway Retail',
    'store',
    'Retail',
    'Antwerp',
    1,
    1200,
    44,
    120,
    'disqualified',
    'maya',
    'Public',
    180,
    2003,
  ],
  [
    'Orchard Lane Learning',
    'book',
    'Education',
    'Milan',
    29,
    75,
    67,
    14,
    'contacted',
    'maya',
    'Seed',
    6,
    2021,
  ],
  [
    'Junebrook Academy',
    'cap',
    'Education',
    'Lyon',
    19,
    140,
    46,
    16,
    'new',
    null,
    'Series A',
    12,
    2017,
  ],
  [
    'Ironvale Tools',
    'wrench',
    'Manufacturing',
    'Krakow',
    -1,
    680,
    48,
    46,
    'new',
    null,
    'Series C',
    95,
    2008,
  ],
  [
    'Pinecrest Media',
    'megaphone',
    'Marketing',
    'Madrid',
    4,
    160,
    58,
    17,
    'researching',
    'sam',
    'Series A',
    18,
    2015,
  ],
  [
    'Wrenfield Foods',
    'box',
    'Retail',
    'Vienna',
    3,
    900,
    61,
    52,
    'researching',
    'sam',
    'Series B',
    120,
    2009,
  ],
  [
    'Kilnworks Studio',
    'palette',
    'Marketing',
    'Antwerp',
    6,
    45,
    42,
    9,
    'new',
    null,
    'Seed',
    3,
    2020,
  ],
  [
    'Harborline Mutual',
    'umbrella',
    'Fintech',
    'Dublin',
    8,
    2200,
    57,
    88,
    'new',
    null,
    'Public',
    640,
    1987,
  ],
  // Low intent: under 40.
  [
    'Graniteford Metals',
    'factory',
    'Manufacturing',
    'Warsaw',
    -6,
    2100,
    33,
    95,
    'researching',
    'maya',
    'Public',
    410,
    1994,
  ],
  [
    'Emberwick Foods',
    'flame',
    'Retail',
    'Lyon',
    3,
    900,
    37,
    52,
    'researching',
    'sam',
    'Series B',
    110,
    2010,
  ],
  [
    'Prismhall Studio',
    'palette',
    'Marketing',
    'Berlin',
    -3,
    32,
    29,
    6,
    'new',
    null,
    'Seed',
    2,
    2021,
  ],
  [
    'Lanternfield Media',
    'video',
    'Marketing',
    'Vienna',
    -7,
    160,
    27,
    17,
    'new',
    null,
    'Series A',
    15,
    2013,
  ],
  [
    'Pebblecourt Bank',
    'bank',
    'Fintech',
    'Madrid',
    -4,
    2600,
    22,
    140,
    'disqualified',
    'sam',
    'Public',
    900,
    1979,
  ],
  [
    'Quayside Shipping',
    'anchor',
    'Logistics',
    'Antwerp',
    -5,
    860,
    31,
    40,
    'new',
    null,
    'Series C',
    130,
    2006,
  ],
  [
    'Westerly Haul',
    'truck',
    'Logistics',
    'Krakow',
    -5,
    270,
    18,
    21,
    'disqualified',
    'theo',
    'Series A',
    27,
    2012,
  ],
  [
    'Velvet Row Apparel',
    'shirt',
    'Retail',
    'Milan',
    1,
    210,
    35,
    28,
    'contacted',
    null,
    'Series A',
    24,
    2014,
  ],
  ['Duskwood Labs', 'flask', 'Healthcare', 'Tallinn', -2, 45, 26, 11, 'new', null, 'Seed', 3, 2022],
];

const countryOf = (city: string) => CITIES.find(([name]) => name === city)?.[1] ?? 'DE';

/** A name as a domain: lower case, no spaces, .io for software, .com otherwise. */
export function domainOf(name: string, industry = '') {
  const stem = name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '')
    .slice(0, 24);
  return `${stem || 'company'}${industry === 'Software' ? '.io' : '.com'}`;
}

export const COMPANIES: Company[] = ROWS.map(
  (
    [
      name,
      icon,
      industry,
      city,
      growth,
      size,
      intent,
      contacts,
      status,
      owner,
      funding,
      revenue,
      founded,
    ],
    i,
  ) => ({
    id: `c${i + 1}`,
    name,
    domain: domainOf(name, industry),
    icon,
    industry,
    city,
    country: countryOf(city),
    growth,
    size,
    intent,
    contacts,
    status,
    owner,
    funding,
    revenue,
    founded,
  }),
);

export const LISTS: SavedList[] = [
  { id: 'eu', name: 'EU Expansion Q4', ids: ['c1'] },
  { id: 'seriesb', name: 'Series B and Up', ids: ['c7'] },
  { id: 'industrial', name: 'Industrial Targets', ids: [] },
];

// ---- Rules ------------------------------------------------------------------------------------

export function levelOf(score: number | null): Level | null {
  if (score === null) return null;
  return score >= 70 ? 'high' : score >= 40 ? 'medium' : 'low';
}

/** Qualified and disqualified accounts are closed: a hand-over leaves them where they are. */
export const isClosed = (status: Status) => status === 'qualified' || status === 'disqualified';

export const firstName = (owner: Owner) => owner.name.split(' ')[0];
export const initialsOf = (name: string) =>
  name
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2)
    .map((part) => part[0]?.toUpperCase())
    .join('');

const NUMBER = new Intl.NumberFormat('en-US');
export const count = (value: number) => NUMBER.format(value);

/** How a growth reads, and its colour: strong, steady, flat or shrinking. */
export function growthTone(growth: number) {
  return growth >= 20 ? 'strong' : growth >= 5 ? 'steady' : growth >= -5 ? 'flat' : 'down';
}

/** A small pseudo-random number from a string and a salt, the same on every render. */
function noise(seed: string, salt: number) {
  let hash = 2166136261 ^ salt;
  for (let i = 0; i < seed.length; i += 1) hash = Math.imul(hash ^ seed.charCodeAt(i), 16777619);
  return ((hash >>> 0) % 1000) / 1000;
}

/** Seven bars for the growth cell: monthly hires, rising with growth, falling when it shrinks. */
export function sparkOf(company: Company) {
  const growth = company.growth ?? 0;
  const bars: number[] = [];
  for (let i = 0; i < 7; i += 1) {
    const t = i / 6;
    const trend = growth >= 0 ? 0.25 + t * 0.75 : 1 - t * 0.75;
    const wobble = (noise(company.id, i) - 0.5) * 0.22;
    bars.push(Math.max(0.12, Math.min(1, trend + wobble * (1 - t))));
  }
  bars[6] = growth >= 0 ? 1 : 0.22;
  return bars;
}

/** Month-end headcounts over six months, ending on today's size. */
export function headcountOf(company: Company) {
  const end = company.size;
  const start = end / (1 + (company.growth ?? 0) / 100);
  const points: number[] = [];
  for (let i = 0; i < 6; i += 1) {
    const t = i / 5;
    // Most of the change comes late, as hiring picks up.
    const eased = t * t * (0.6 + 0.4 * t) + (noise(company.id, 40 + i) - 0.5) * 0.06 * (1 - t);
    points.push(i === 5 ? end : Math.round(start + (end - start) * Math.max(0, eased)));
  }
  return points;
}

export const MONTHS = ['May', 'Jun', 'Jul', 'Aug', 'Sep', 'Now'];

const FIRST = [
  'Clara',
  'Rafael',
  'Hannah',
  'Jonas',
  'Priya',
  'Lukas',
  'Elena',
  'Tomas',
  'Amira',
  'Felix',
];
const LAST = [
  'Hartley',
  'Navarro',
  'Ashby',
  'Lindqvist',
  'Rao',
  'Brenner',
  'Costa',
  'Varga',
  'Haddad',
  'Moreau',
];
const ROLES = [
  'Chief Revenue Officer',
  'Sales Director',
  'Head of Operations',
  'Account Executive Lead',
  'VP Marketing',
];

/** Up to three key people at a company. */
export function contactsOf(company: Company) {
  const n = Math.min(3, company.contacts ?? 0);
  return Array.from({ length: n }, (_, i) => {
    const first = FIRST[Math.floor(noise(company.id, 60 + i) * FIRST.length)];
    const last = LAST[Math.floor(noise(company.id, 70 + i) * LAST.length)];
    return {
      name: `${first} ${last}`,
      role: ROLES[(i + Math.floor(noise(company.id, 80) * 5)) % ROLES.length],
      email: `${first.toLowerCase()}@${company.domain}`,
    };
  });
}

export type Signal = {
  kind: 'visit' | 'hiring' | 'content' | 'funding';
  title: string;
  body: string;
  date: string;
};

/** What the company did lately that says it may buy. */
export function signalsOf(company: Company): Signal[] {
  if (company.intent === null) return [];
  const people = 2 + Math.floor(noise(company.id, 90) * 4);
  const hires = 2 + Math.floor(noise(company.id, 91) * 6);
  const all: Signal[] = [
    {
      kind: 'visit',
      title: 'Viewed the pricing page',
      body: `${people} visits from ${people - 1} people this week`,
      date: 'Oct 19',
    },
    {
      kind: 'hiring',
      title: `Hiring ${hires} sales roles`,
      body: `${count(Math.round(company.size * 0.08))} people joined in six months`,
      date: 'Oct 12',
    },
    {
      kind: 'content',
      title: 'Read the pipeline reporting guide',
      body: 'Downloaded from a paid search ad',
      date: 'Oct 7',
    },
    {
      kind: 'funding',
      title:
        company.funding === 'Public' || company.funding === 'Bootstrapped'
          ? 'Posted a plan to grow sales'
          : `Raised a ${company.funding} round`,
      body: 'Announced with plans to grow the sales team',
      date: 'Sep 18',
    },
  ];
  const n = company.intent >= 70 ? 4 : company.intent >= 40 ? 3 : 1;
  return all.slice(0, n);
}
```

### lead-table/parts.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

import type { CSSProperties } from 'react';
import {
  COUNTRIES,
  LEVEL_WORD,
  MONTHS,
  STATUS_WORD,
  count,
  growthTone,
  headcountOf,
  initialsOf,
  levelOf,
  sparkOf,
  type Company,
  type Owner,
  type Status,
} from './data';
import { Icon } from './glyphs';
import styles from './lead-table.module.css';

/*
 * The small pieces the table, the drawer and the dialogs share: avatars, the round colour mark
 * beside a city, the growth bars, the intent ring, status chips, the dotted strip of an intent card
 * and the headcount chart.
 */

/** An owner's initials on their colour; a dashed ring for nobody. */
export function Avatar({ owner, size = 18 }: { owner: Owner | null | undefined; size?: number }) {
  if (!owner)
    return (
      <span
        className={styles.avatarNone}
        style={{ '--size': `${size}px` } as CSSProperties}
        aria-hidden="true"
      >
        <Icon name="unassigned" />
      </span>
    );
  return (
    <span
      className={styles.avatar}
      style={{ '--size': `${size}px`, '--tint': owner.color } as CSSProperties}
      aria-hidden="true"
    >
      {initialsOf(owner.name)}
    </span>
  );
}

/** The round mark of a country's colours, in stripes. */
export function Flag({ country }: { country: string }) {
  const info = COUNTRIES[country];
  if (!info) return null;
  const share = 100 / info.stripes.length;
  const stops = info.stripes
    .map((colour, i) => `${colour} ${(i * share).toFixed(2)}% ${((i + 1) * share).toFixed(2)}%`)
    .join(', ');
  return (
    <span
      className={styles.flag}
      style={{ background: `linear-gradient(${info.vertical ? '90deg' : '180deg'}, ${stops})` }}
      aria-hidden="true"
    />
  );
}

/** Seven bars of monthly hiring, then the six months' change in %. */
export function Growth({ company }: { company: Company }) {
  if (company.growth === null) return <span className={styles.none}>No history yet</span>;
  const bars = sparkOf(company);
  const tone = growthTone(company.growth);
  return (
    <span className={styles.growth} data-tone={tone}>
      <svg className={styles.spark} viewBox="0 0 34 16" aria-hidden="true">
        {bars.map((height, i) => (
          <rect
            key={i}
            x={i * 5}
            y={16 - height * 16}
            width="3"
            height={height * 16}
            rx="0.8"
            opacity={i >= 5 ? 1 : 0.32}
          />
        ))}
      </svg>
      <span className={styles.growthValue}>
        {company.growth > 0 ? '+' : company.growth < 0 ? '−' : ''}
        {Math.abs(company.growth)}%
      </span>
    </span>
  );
}

/** The intent score as an arc, the number, and its level in words. */
export function Intent({ score, withWord = true }: { score: number | null; withWord?: boolean }) {
  const level = levelOf(score);
  if (score === null || !level) return <span className={styles.none}>Not scored yet</span>;
  const r = 5.6;
  const length = 2 * Math.PI * r;
  return (
    <span className={styles.intent} data-level={level}>
      <svg className={styles.ring} viewBox="0 0 16 16" aria-hidden="true">
        <circle cx="8" cy="8" r={r} className={styles.ringTrack} />
        <circle
          cx="8"
          cy="8"
          r={r}
          className={styles.ringArc}
          strokeDasharray={`${((score / 100) * length).toFixed(2)} ${length.toFixed(2)}`}
          transform="rotate(-90 8 8)"
        />
      </svg>
      <span className={styles.intentScore}>{score}</span>
      {withWord && <span className={styles.intentWord}>{LEVEL_WORD[level]}</span>}
    </span>
  );
}

/** A status as a chip with its coloured dot. */
export function StatusChip({ status }: { status: Status }) {
  return (
    <span className={styles.chip} data-status={status} data-part="status-chip">
      <span className={styles.chipDot} aria-hidden="true" />
      {STATUS_WORD[status]}
    </span>
  );
}

/** A row of dots, `filled` of `total` lit: the intent card's strip. */
export function Dots({ filled, total }: { filled: number; total: number }) {
  // Past 30, each dot stands for more than one company.
  const shown = Math.min(total, 30);
  const lit = total ? Math.round((filled / total) * shown) : 0;
  return (
    <span className={styles.dots} aria-hidden="true">
      {Array.from({ length: shown }, (_, i) => (
        <span key={i} className={styles.dot} data-lit={i < lit || undefined} />
      ))}
    </span>
  );
}

/** Month-end headcount over six months: a line over a soft area, first and last labelled. */
export function HeadcountChart({ company }: { company: Company }) {
  const points = headcountOf(company);
  const min = Math.min(...points);
  const max = Math.max(...points);
  const span = Math.max(1, max - min);
  const w = 400;
  const h = 64;
  const xy = points.map((value, i) => [
    (i / (points.length - 1)) * w,
    h - 6 - ((value - min) / span) * (h - 16),
  ]);
  // A smooth line through the months: each bend's handles lie a third of the way to its neighbours.
  let line = `M${xy[0][0]} ${xy[0][1].toFixed(1)}`;
  for (let i = 1; i < xy.length; i += 1) {
    const [x0, y0] = xy[i - 1];
    const [x1, y1] = xy[i];
    const dx = (x1 - x0) / 3;
    line += ` C${(x0 + dx).toFixed(1)} ${y0.toFixed(1)} ${(x1 - dx).toFixed(1)} ${y1.toFixed(1)} ${x1.toFixed(1)} ${y1.toFixed(1)}`;
  }
  const area = `${line} L${w} ${h} L0 ${h}Z`;
  const last = xy[xy.length - 1];
  return (
    <figure className={styles.chart} data-part="headcount">
      <svg
        viewBox={`0 0 ${w} ${h}`}
        preserveAspectRatio="none"
        className={styles.chartSvg}
        role="img"
        aria-label={`Headcount from ${count(points[0])} in ${MONTHS[0]} to ${count(points[5])} now`}
      >
        <path d={area} className={styles.chartArea} />
        <path d={line} className={styles.chartLine} vectorEffect="non-scaling-stroke" />
      </svg>
      <span
        className={styles.chartEnd}
        style={{ top: `${((last[1] / h) * 100).toFixed(1)}%` }}
        aria-hidden="true"
      />
      <figcaption className={styles.chartAxis}>
        <span>
          {MONTHS[0]}, {count(points[0])}
        </span>
        <span>Now, {count(points[5])}</span>
      </figcaption>
    </figure>
  );
}
```

### lead-table/glyphs.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

import type { CompanyIcon } from './data';

/*
 * The table's line glyphs, on a 24-unit grid, drawn with the current text colour: the controls'
 * icons and the companies' marks. All decorative: the button or the name beside them says it.
 */

const UI = {
  search: 'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4',
  x: 'M6 6l12 12M18 6L6 18',
  filter: 'M4 5h16l-6.2 7.4V19l-3.6-1.8v-4.8z',
  columns: 'M4 5h16v14H4zM9.5 5v14M14.5 5v14',
  sliders: 'M4 7h9M17 7h3M4 17h3M11 17h9M15 5v4M9 15v4',
  chevronDown: 'M7 10l5 5 5-5',
  chevronLeft: 'M14.5 7l-5 5 5 5',
  chevronRight: 'M9.5 7l5 5-5 5',
  sort: 'M8 9.5l4-4 4 4M8 14.5l4 4 4-4',
  arrowUp: 'M12 19V5M6.5 10.5L12 5l5.5 5.5',
  arrowDown: 'M12 5v14M6.5 13.5L12 19l5.5-5.5',
  check: 'M5 12.5l4.5 4.5L19 7.5',
  plus: 'M12 5v14M5 12h14',
  minus: 'M5 12h14',
  swap: 'M4 8.5h15M15 4.5l4 4-4 4M20 15.5H5M9 11.5l-4 4 4 4',
  swapVertical: 'M8 4v15M4 8l4-4 4 4M16 20V5M12 16l4 4 4-4',
  trash: 'M4 7h16M10 11v6M14 11v6M6 7l1 12.5h10L18 7M9 7V4.5h6V7',
  download: 'M12 4v11M7 10.5l5 5 5-5M5 19.5h14',
  status: 'M10 6h10M10 12h10M10 18h10M4 6l1.3 1.3L7.8 5M4 12h2.8M4 18h2.8',
  userPlus:
    'M10 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM3 20c.8-3.4 3.6-5 7-5 1.4 0 2.6.3 3.7.8M18 14v6M15 17h6',
  user: 'M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4.5 20c.9-3.6 4-5.5 7.5-5.5s6.6 1.9 7.5 5.5',
  bookmark: 'M6.5 4h11v16l-5.5-4-5.5 4z',
  pencil: 'M4.5 19.5l1-4.2L15.8 5a2.1 2.1 0 0 1 3 3L8.7 18.3zM13.5 7.5l3 3',
  mail: 'M4 6h16v12H4zM4.5 6.5l7.5 6 7.5-6',
  trendUp: 'M3.5 16.5l6-6 4 4 7-7M15 7.5h5.5V13',
  trendDown: 'M3.5 7.5l6 6 4-4 7 7M15 16.5h5.5V11',
  more: 'M5.5 12h.01M12 12h.01M18.5 12h.01',
  open: 'M14 5h5v5M19 5l-8 8M17 14v5H5V7h5',
  eye: 'M2.5 12s3.5-6.5 9.5-6.5S21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12zM12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z',
  globe:
    'M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM3.5 12h17M12 3.5c2.4 2.4 3.4 5.3 3.4 8.5S14.4 18.1 12 20.5M12 3.5C9.6 5.9 8.6 8.8 8.6 12s1 6.1 3.4 8.5',
  users:
    'M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM2.5 20c.7-3.2 3.3-5 6.5-5s5.8 1.8 6.5 5M16 4.4a3.5 3.5 0 0 1 0 6.2M18 15.3c1.8.6 3 2.2 3.5 4.7',
  file: 'M7 3.5h7l4.5 4.5v12.5H7zM14 3.5V8h4.5M10 13h5M10 16.5h5',
  coins:
    'M9 9.5c3 0 5.5-1.1 5.5-2.5S12 4.5 9 4.5 3.5 5.6 3.5 7 6 9.5 9 9.5zM3.5 7v4.5C3.5 12.9 6 14 9 14M3.5 11.5V16c0 1.4 2.5 2.5 5.5 2.5M15 12.5c3 0 5.5-1.1 5.5-2.5M9.5 12.5v4.5c0 1.4 2.5 2.5 5.5 2.5s5.5-1.1 5.5-2.5V10c0-1.4-2.5-2.5-5.5-2.5S9.5 8.6 9.5 10v2.5c0 1.4 2.5 2.5 5.5 2.5s5.5-1.1 5.5-2.5',
  unassigned:
    'M12 12a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM5.5 19.5c.9-3.1 3.5-4.8 6.5-4.8s5.6 1.7 6.5 4.8',
  checkCircle: 'M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM8.3 12.3l2.5 2.5 4.9-5.1',
} as const;

export type UiIcon = keyof typeof UI;

const MARKS: Record<CompanyIcon, string> = {
  chart: 'M5 19.5V5M5 19.5h14.5M9 16v-5M13 16V8M17 16v-3',
  flask:
    'M9.5 3.5h5M10.5 3.5v5.5l-5 9a1.5 1.5 0 0 0 1.3 2.2h10.4a1.5 1.5 0 0 0 1.3-2.2l-5-9V3.5M8 14.5h8',
  route:
    'M6 7.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM18 21.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM8.5 5h7a3.5 3.5 0 0 1 0 7h-7a3.5 3.5 0 0 0 0 7h7',
  robot: 'M5.5 9h13v10h-13zM12 9V5.5M12 4.5h.01M9.5 13.5h.01M14.5 13.5h.01M3 13v3M21 13v3',
  database:
    'M12 9c4.1 0 7.5-1.3 7.5-2.8S16.1 3.5 12 3.5 4.5 4.7 4.5 6.2 7.9 9 12 9zM4.5 6.2v11.6c0 1.5 3.4 2.7 7.5 2.7s7.5-1.2 7.5-2.7V6.2M4.5 12c0 1.5 3.4 2.8 7.5 2.8s7.5-1.3 7.5-2.8',
  layers: 'M12 3.5l8.5 4.5-8.5 4.5L3.5 8zM3.5 12l8.5 4.5 8.5-4.5M3.5 16l8.5 4.5 8.5-4.5',
  cloud:
    'M7 18.5a4.5 4.5 0 0 1-.6-9 6 6 0 0 1 11.4 1.6A3.8 3.8 0 0 1 17.3 18.5zM12 11.5v5M9.5 14l2.5-2.5 2.5 2.5',
  shield: 'M12 3.5l7 2.8v5.4c0 4.3-2.9 7.6-7 8.8-4.1-1.2-7-4.5-7-8.8V6.3zM9 12l2 2 4-4',
  card: 'M3.5 6h17v12h-17zM3.5 10h17M7 14.5h3',
  wallet:
    'M4 7.5h14.5a1.5 1.5 0 0 1 1.5 1.5v9.5a1.5 1.5 0 0 1-1.5 1.5H5.5A1.5 1.5 0 0 1 4 18.5V6a1.5 1.5 0 0 1 1.5-1.5H16v3M16 14h.01',
  target:
    'M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM12 7.5a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9zM12 11.5a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1z',
  cart: 'M3.5 4.5h2.5l2.2 10.5h9.6l2-7.5H7M9.5 19.5h.01M17 19.5h.01',
  cross: 'M5 5h14v14H5zM12 8.5v7M8.5 12h7',
  store:
    'M4 9.5L5.5 4.5h13L20 9.5M4 9.5a2.7 2.7 0 0 0 5.3 0 2.7 2.7 0 0 0 5.4 0 2.7 2.7 0 0 0 5.3 0M5.5 12v7.5h13V12M10 19.5v-4.5h4v4.5',
  book: 'M12 6.5c-1.8-1.4-4.3-2-7.5-2v13c3.2 0 5.7.6 7.5 2 1.8-1.4 4.3-2 7.5-2v-13c-3.2 0-5.7.6-7.5 2zM12 6.5v13',
  cap: 'M12 5l9.5 4.5L12 14 2.5 9.5zM6.5 11.5v4.5c1.5 1.4 3.4 2 5.5 2s4-.6 5.5-2v-4.5M21.5 9.5v5',
  wrench:
    'M14.5 6.5a4 4 0 0 0 5 5L12 19a2.1 2.1 0 0 1-3-3l7.5-7.5zM14.5 6.5l2-2a4 4 0 0 1 3 3l-2 2',
  megaphone: 'M4 10v4h3l8 4.5v-13L7 10zM18 9.5a3.5 3.5 0 0 1 0 5M7 14l1.2 5h2.3L9.5 14',
  flame:
    'M12 21c3.6 0 6-2.5 6-5.8 0-3.8-3.2-6-4.3-10.2C11 7 10.2 9 9.8 10.8 8.9 9.9 8.5 9 8.4 8 6.8 9.9 6 12.6 6 15.2 6 18.5 8.4 21 12 21z',
  palette:
    'M12 3.5a8.5 8.5 0 0 0 0 17c1.2 0 1.8-.9 1.4-1.9-.5-1.3.3-2.4 1.6-2.4h1.8c2.1 0 3.7-1.6 3.7-3.7C20.5 7.6 16.7 3.5 12 3.5zM8 11h.01M10.5 7.5h.01M15 8h.01',
  umbrella: 'M3.5 12a8.5 8.5 0 0 1 17 0zM12 12v6a2 2 0 0 1-4 0',
  factory: 'M3.5 20.5V10l5 3V10l5 3V10l5 3V4.5h2v16zM7 17h2M12 17h2',
  video: 'M3.5 7h11.5v10H3.5zM15 10.5l5.5-3v9l-5.5-3',
  bank: 'M3.5 9.5L12 4.5l8.5 5M5 9.5h14M6.5 10v7M10 10v7M14 10v7M17.5 10v7M4 19.5h16',
  anchor:
    'M12 8a2.2 2.2 0 1 0 0-4.4A2.2 2.2 0 0 0 12 8zM12 8v12.5M8 11.5h8M4.5 13.5c0 4 3.4 7 7.5 7s7.5-3 7.5-7',
  truck:
    'M3 6h11v10H3zM14 9.5h4l3 3.5v3h-7M7 19a1.8 1.8 0 1 0 0-3.6A1.8 1.8 0 0 0 7 19zM17.5 19a1.8 1.8 0 1 0 0-3.6 1.8 1.8 0 0 0 0 3.6z',
  shirt: 'M8.5 4l-5 3 2 4 2-1v10h9V10l2 1 2-4-5-3c-.5 1.5-1.8 2.5-3.5 2.5S9 5.5 8.5 4z',
  box: 'M12 3.5l8 4v9l-8 4-8-4v-9zM4 7.5l8 4 8-4M12 11.5v9',
};

const DRAWN = {
  fill: 'none',
  stroke: 'currentColor',
  strokeLinecap: 'round',
  strokeLinejoin: 'round',
} as const;

/** One of the controls' icons. */
export function Icon({
  name,
  className,
  width = 2,
}: {
  name: UiIcon;
  className?: string;
  width?: number;
}) {
  return (
    <svg viewBox="0 0 24 24" className={className} aria-hidden="true" focusable="false">
      <path d={UI[name]} {...DRAWN} strokeWidth={name === 'more' ? 3.2 : width} />
    </svg>
  );
}

/** A company's mark, in its tile. */
export function Mark({ name, className }: { name: CompanyIcon; className?: string }) {
  return (
    <svg viewBox="0 0 24 24" className={className} aria-hidden="true" focusable="false">
      <path d={MARKS[name]} {...DRAWN} strokeWidth={1.9} />
    </svg>
  );
}

export const MARK_NAMES = Object.keys(MARKS) as CompanyIcon[];
```

### lead-table/layers.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

'use client';

import {
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
  type KeyboardEvent as ReactKeyboardEvent,
  type ReactNode,
} from 'react';
import { Icon } from './glyphs';
import styles from './lead-table.module.css';

/*
 * The layers over the table: menus, and the panels that take the screen (the company drawer, the
 * dialogs). The table's menus live on the stage, not inside the table, so its scrolling never clips
 * them; a panel's menus live in the panel. A menu is placed under its trigger in the px of the box
 * it sits in (the gallery may scale the whole stage), or over it when there is no room below.
 */

const ITEMS =
  '[role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [data-menu-field]';

export function Menu({
  anchor,
  label,
  align = 'start',
  takeFocus,
  onClose,
  children,
  wide,
}: {
  anchor: HTMLElement;
  label: string;
  align?: 'start' | 'end';
  /** Moves the focus to the first item: when the reader opened it, not the demo. */
  takeFocus: boolean;
  /** `back`: give the focus back to the trigger (Escape, a choice). */
  onClose: (back: boolean) => void;
  children: ReactNode;
  wide?: boolean;
}) {
  const box = useRef<HTMLDivElement>(null);
  const [at, setAt] = useState<{ x: number; y: number } | null>(null);

  useLayoutEffect(() => {
    const menu = box.current;
    // The box it is placed in: the stage, or the panel it opened from.
    const frame = menu?.offsetParent as HTMLElement | null;
    if (!frame || !menu) return;
    const place = () => {
      const s = frame.getBoundingClientRect();
      const scale = s.width / frame.offsetWidth || 1;
      const a = anchor.getBoundingClientRect();
      const left = (a.left - s.left) / scale - frame.clientLeft;
      const right = (a.right - s.left) / scale - frame.clientLeft;
      const top = (a.top - s.top) / scale - frame.clientTop;
      const bottom = (a.bottom - s.top) / scale - frame.clientTop;
      const w = menu.offsetWidth;
      const h = menu.offsetHeight;
      let x = align === 'end' ? right - w : left;
      x = Math.max(8, Math.min(x, frame.clientWidth - w - 8));
      let y = bottom + 4;
      if (y + h > frame.clientHeight - 6 && top - h - 4 > 6) y = top - h - 4;
      setAt({ x, y });
    };
    place();
    const observer = new ResizeObserver(place);
    observer.observe(menu);
    return () => observer.disconnect();
  }, [anchor, align]);

  useEffect(() => {
    if (!takeFocus || !at) return;
    const first = box.current?.querySelector<HTMLElement>(
      '[data-menu-field], [aria-checked="true"], [role^="menuitem"]',
    );
    first?.focus({ preventScroll: true });
    // Only when it first shows.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [takeFocus, at === null]);

  // A press anywhere else closes it; its trigger toggles it on its own.
  useEffect(() => {
    const away = (event: PointerEvent) => {
      const target = event.target as Node;
      if (box.current?.contains(target) || anchor.contains(target)) return;
      onClose(false);
    };
    document.addEventListener('pointerdown', away);
    return () => document.removeEventListener('pointerdown', away);
  }, [anchor, onClose]);

  const onKeyDown = (event: ReactKeyboardEvent) => {
    const items = [...(box.current?.querySelectorAll<HTMLElement>(ITEMS) ?? [])];
    const index = items.indexOf(document.activeElement as HTMLElement);
    const move = (to: number) => {
      event.preventDefault();
      items[(to + items.length) % items.length]?.focus();
    };
    const inField = (event.target as HTMLElement).matches('input');
    if (event.key === 'ArrowDown') move(index + 1);
    else if (event.key === 'ArrowUp') move(index - 1);
    else if (event.key === 'Home' && !inField) move(0);
    else if (event.key === 'End' && !inField) move(items.length - 1);
    else if (event.key === 'Escape') {
      event.preventDefault();
      event.stopPropagation();
      onClose(true);
    } else if (event.key === 'Tab') onClose(false);
  };

  return (
    <div
      ref={box}
      role="menu"
      tabIndex={-1}
      aria-label={label}
      className={styles.menu}
      data-wide={wide || undefined}
      data-part="menu"
      style={{
        transform: at ? `translate(${at.x}px, ${at.y}px)` : undefined,
        visibility: at ? undefined : 'hidden',
      }}
      onKeyDown={onKeyDown}
    >
      {children}
    </div>
  );
}

export function MenuLabel({ children }: { children: ReactNode }) {
  return (
    <div className={styles.menuLabel} role="presentation">
      {children}
    </div>
  );
}

export function MenuItem({
  onSelect,
  checked,
  kind = 'item',
  children,
  tone,
  demo,
  trail,
}: {
  onSelect: () => void;
  checked?: boolean;
  kind?: 'item' | 'radio' | 'check';
  children: ReactNode;
  tone?: 'danger';
  /** The demo finds the item by this. */
  demo?: string;
  /** Text at the right end: a count, "You". */
  trail?: ReactNode;
}) {
  return (
    <button
      type="button"
      role={kind === 'radio' ? 'menuitemradio' : kind === 'check' ? 'menuitemcheckbox' : 'menuitem'}
      aria-checked={kind === 'item' ? undefined : !!checked}
      tabIndex={-1}
      className={styles.menuItem}
      data-tone={tone}
      data-demo={demo}
      onClick={onSelect}
    >
      <span className={styles.menuText}>{children}</span>
      {trail !== undefined && <span className={styles.menuTrail}>{trail}</span>}
      {kind === 'radio' && checked && <Icon name="check" className={styles.menuCheck} />}
      {kind === 'check' && (
        <span className={styles.menuBox} data-on={checked || undefined} aria-hidden="true">
          {checked && <Icon name="check" width={3} />}
        </span>
      )}
    </button>
  );
}

const FOCUSABLE =
  'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';

/**
 * A panel that takes the stage: the drawer at the right, or a dialog in the middle. The table
 * behind it blurs and goes inert. Escape or a press on the veil closes it; Tab stays inside.
 */
export function Panel({
  kind,
  labelledBy,
  takeFocus,
  onClose,
  children,
  part,
}: {
  kind: 'drawer' | 'side' | 'center';
  labelledBy: string;
  takeFocus: boolean;
  onClose: () => void;
  children: ReactNode;
  part: string;
}) {
  const box = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (!takeFocus) return;
    const panel = box.current;
    const first =
      panel?.querySelector<HTMLElement>('[data-autofocus]') ??
      panel?.querySelector<HTMLElement>(FOCUSABLE);
    first?.focus({ preventScroll: true });
  }, [takeFocus]);

  const onKeyDown = (event: ReactKeyboardEvent) => {
    if (event.key === 'Escape') {
      // A menu inside closes first, on its own.
      if (event.defaultPrevented) return;
      event.preventDefault();
      onClose();
      return;
    }
    if (event.key !== 'Tab') return;
    const all = [...(box.current?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])].filter(
      (element) => element.offsetParent !== null,
    );
    if (!all.length) return;
    const first = all[0];
    const last = all[all.length - 1];
    if (event.shiftKey && document.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  };

  return (
    <div className={styles.layer} data-kind={kind}>
      <div className={styles.veil} onClick={onClose} aria-hidden="true" />
      <div
        ref={box}
        role="dialog"
        aria-modal="true"
        aria-labelledby={labelledBy}
        className={styles.panel}
        data-kind={kind}
        data-part={part}
        onKeyDown={onKeyDown}
      >
        {children}
      </div>
    </div>
  );
}
```

### lead-table/drawer.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

'use client';

import { useId, useRef, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from 'react';
import {
  COUNTRIES,
  contactsOf,
  count,
  initialsOf,
  signalsOf,
  type Company,
  type Owner,
  STATUS_WORD,
  type SavedList,
} from './data';
import { Icon, Mark, type UiIcon } from './glyphs';
import { Panel } from './layers';
import { Avatar, HeadcountChart, Intent, StatusChip } from './parts';
import styles from './lead-table.module.css';

/*
 * The company drawer: who the company is, its people and what it did lately, over the blurred
 * table. Overview holds the firmographics, a six-month headcount chart and where outreach stands;
 * Contacts and Signals list the rest. Its footer changes the status, edits the company, or saves it
 * to a list.
 */

export type DrawerTab = 'overview' | 'contacts' | 'signals';

const SIGNAL_ICON: Record<string, UiIcon> = {
  visit: 'globe',
  hiring: 'users',
  content: 'file',
  funding: 'coins',
};

function Section({ title, note, children }: { title: string; note: string; children: ReactNode }) {
  return (
    <section className={styles.section}>
      <header className={styles.sectionHead}>
        <h4 className={styles.sectionTitle}>{title}</h4>
        <p className={styles.sectionNote}>{note}</p>
      </header>
      <div className={styles.sectionBody}>{children}</div>
    </section>
  );
}

function Pair({ name, children }: { name: string; children: ReactNode }) {
  return (
    <div className={styles.pair}>
      <dt>{name}</dt>
      <dd>{children}</dd>
    </div>
  );
}

export function Drawer({
  company,
  owner,
  lists,
  tab,
  takeFocus,
  onTab,
  onClose,
  onMenu,
  onEdit,
  menu,
}: {
  company: Company;
  owner: Owner | undefined;
  lists: SavedList[];
  tab: DrawerTab;
  takeFocus: boolean;
  onTab: (tab: DrawerTab) => void;
  onClose: () => void;
  /** Opens one of the drawer's menus under `anchor`. */
  onMenu: (id: 'drawer-status' | 'drawer-list' | 'drawer-more', anchor: HTMLElement) => void;
  onEdit: () => void;
  /** The open menu, when it opened from the drawer: it sits in the drawer. */
  menu?: ReactNode;
}) {
  const uid = useId();
  const tabs = useRef(new Map<DrawerTab, HTMLButtonElement>());
  const people = contactsOf(company);
  const signals = signalsOf(company);
  const country = COUNTRIES[company.country]?.name;
  const inLists = lists.filter((list) => list.ids.includes(company.id));
  const order: DrawerTab[] = ['overview', 'contacts', 'signals'];

  const onTabKey = (event: ReactKeyboardEvent) => {
    const at = order.indexOf(tab);
    const to =
      event.key === 'ArrowRight'
        ? at + 1
        : event.key === 'ArrowLeft'
          ? at - 1
          : event.key === 'Home'
            ? 0
            : event.key === 'End'
              ? 2
              : null;
    if (to === null) return;
    event.preventDefault();
    const next = order[(to + 3) % 3];
    onTab(next);
    tabs.current.get(next)?.focus();
  };

  const tabButton = (id: DrawerTab, label: string, n?: number) => (
    <button
      key={id}
      ref={(element) => {
        if (element) tabs.current.set(id, element);
      }}
      type="button"
      role="tab"
      id={`${uid}-tab-${id}`}
      aria-selected={tab === id}
      aria-controls={`${uid}-panel`}
      tabIndex={tab === id ? 0 : -1}
      className={styles.tab}
      data-demo={`tab-${id}`}
      onClick={() => onTab(id)}
    >
      {label}
      {n !== undefined && <span className={styles.tabCount}>{n}</span>}
    </button>
  );

  return (
    <Panel
      kind="drawer"
      labelledBy={`${uid}-name`}
      takeFocus={takeFocus}
      onClose={onClose}
      part="drawer"
    >
      <header className={styles.drawerHead}>
        <span className={styles.tileLarge} aria-hidden="true">
          <Mark name={company.icon} />
        </span>
        <div className={styles.drawerTitle}>
          <div className={styles.drawerNameLine}>
            <h3 id={`${uid}-name`} className={styles.drawerName}>
              {company.name}
            </h3>
            <StatusChip status={company.status} />
          </div>
          <p className={styles.drawerSub}>
            {company.domain} <span aria-hidden="true">·</span> {company.city}
          </p>
        </div>
        <div className={styles.drawerTools}>
          <button
            type="button"
            className={styles.iconButton}
            aria-label="More actions"
            aria-haspopup="menu"
            onClick={(event) => onMenu('drawer-more', event.currentTarget)}
          >
            <Icon name="more" />
          </button>
          <button
            type="button"
            className={styles.iconButton}
            aria-label="Close"
            data-demo="drawer-close"
            onClick={onClose}
          >
            <Icon name="x" />
          </button>
        </div>
      </header>
      <div className={styles.tabs} role="tablist" aria-label="Company details" onKeyDown={onTabKey}>
        {tabButton('overview', 'Overview')}
        {tabButton('contacts', 'Contacts', people.length)}
        {tabButton('signals', 'Signals', signals.length)}
      </div>
      <div
        className={styles.drawerBody}
        role="tabpanel"
        id={`${uid}-panel`}
        aria-labelledby={`${uid}-tab-${tab}`}
        tabIndex={0}
      >
        {tab === 'overview' && (
          <>
            <Section title="Firmographics" note="Who they are and how big">
              <dl className={styles.pairs}>
                <Pair name="Industry">{company.industry}</Pair>
                <Pair name="Headquarters">
                  {company.city}
                  {country ? `, ${country}` : ''}
                </Pair>
                <Pair name="Employees">{count(company.size)}</Pair>
                <Pair name="Growth, 6 months">
                  {company.growth === null ? (
                    <span className={styles.none}>No history yet</span>
                  ) : (
                    <span className={styles.trend} data-down={company.growth < 0 || undefined}>
                      <Icon name={company.growth < 0 ? 'trendDown' : 'trendUp'} />
                      {company.growth > 0 ? '+' : company.growth < 0 ? '−' : ''}
                      {Math.abs(company.growth)}%
                    </span>
                  )}
                </Pair>
                <Pair name="Revenue">${count(company.revenue)}M</Pair>
                <Pair name="Funding">{company.funding}</Pair>
                <Pair name="Founded">{company.founded}</Pair>
              </dl>
            </Section>
            <Section title="Headcount" note="Month-end team size over six months">
              <HeadcountChart company={company} />
            </Section>
            <Section title="Outreach" note="Where this account stands">
              <dl className={styles.pairs}>
                <Pair name="Owner">
                  <span className={styles.ownerInline}>
                    <Avatar owner={owner} />
                    {owner?.name ?? 'Unassigned'}
                  </span>
                </Pair>
                <Pair name="Intent">
                  <Intent score={company.intent} />
                </Pair>
                <Pair name="Contacts">
                  {company.contacts === null ? 'None yet' : count(company.contacts)}
                </Pair>
                <Pair name="Lists">
                  {inLists.length ? inLists.map((list) => list.name).join(', ') : 'None yet'}
                </Pair>
              </dl>
            </Section>
          </>
        )}
        {tab === 'contacts' && (
          <Section
            title="Key Contacts"
            note={
              company.contacts
                ? `${people.length} of ${count(company.contacts)} people in the database`
                : 'No one matched on the domain yet'
            }
          >
            {people.length ? (
              <ul className={styles.people}>
                {people.map((person) => (
                  <li key={person.email} className={styles.person}>
                    <span className={styles.personMark} aria-hidden="true">
                      {initialsOf(person.name)}
                    </span>
                    <span className={styles.personText}>
                      <span className={styles.personName}>{person.name}</span>
                      <span className={styles.personRole}>{person.role}</span>
                    </span>
                    <a
                      className={styles.iconButton}
                      href={`mailto:${person.email}`}
                      aria-label={`Email ${person.name}`}
                    >
                      <Icon name="mail" />
                    </a>
                  </li>
                ))}
              </ul>
            ) : (
              <p className={styles.emptyNote}>Contacts show here once the domain is matched.</p>
            )}
          </Section>
        )}
        {tab === 'signals' && (
          <Section title="Buying Signals" note="Why this looks ready to buy">
            {signals.length ? (
              <ol className={styles.signals}>
                {signals.map((signal) => (
                  <li key={signal.title} className={styles.signal}>
                    <span className={styles.signalIcon} aria-hidden="true">
                      <Icon name={SIGNAL_ICON[signal.kind]} />
                    </span>
                    <span className={styles.personText}>
                      <span className={styles.personName}>{signal.title}</span>
                      <span className={styles.personRole}>{signal.body}</span>
                      <span className={styles.signalDate}>{signal.date}</span>
                    </span>
                  </li>
                ))}
              </ol>
            ) : (
              <p className={styles.emptyNote}>Signals show here once the company is scored.</p>
            )}
          </Section>
        )}
      </div>
      <footer className={styles.drawerFoot}>
        <button
          type="button"
          className={styles.selectButton}
          aria-haspopup="menu"
          aria-label={`Status: ${STATUS_WORD[company.status]}`}
          onClick={(event) => onMenu('drawer-status', event.currentTarget)}
        >
          <StatusChip status={company.status} />
          <Icon name="chevronDown" className={styles.chevron} />
        </button>
        <span className={styles.grow} />
        <button type="button" className={styles.button} onClick={onEdit}>
          <Icon name="pencil" />
          Edit
        </button>
        <button
          type="button"
          className={styles.buttonDark}
          aria-haspopup="menu"
          onClick={(event) => onMenu('drawer-list', event.currentTarget)}
        >
          <Icon name="bookmark" />
          Save to List
        </button>
      </footer>
      {menu}
    </Panel>
  );
}
```

### lead-table/dialogs.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

'use client';

import { useId, useRef, useState, type FormEvent, type ReactNode } from 'react';
import {
  CITIES,
  FUNDING,
  INDUSTRIES,
  STATUSES,
  STATUS_WORD,
  count,
  domainOf,
  isClosed,
  levelOf,
  type Company,
  type Owner,
  type Status,
} from './data';
import { Icon } from './glyphs';
import { Menu, MenuItem, Panel } from './layers';
import { Avatar, Flag, StatusChip } from './parts';
import styles from './lead-table.module.css';

/*
 * The dialogs: Hand Over (move one owner's open accounts to another), Add Company (and Edit, the
 * same form filled in) and New List. Their pickers are buttons that open a menu inside the dialog,
 * so they can show avatars, colour marks and chips the way the table does.
 */

type Option = { value: string; label: string; lead?: ReactNode; trail?: ReactNode };

/** A field's value as a button, and its choices as a menu under it. */
function Picker({
  label,
  value,
  options,
  onChange,
  takeFocus,
  demo,
  labelId,
}: {
  label: string;
  value: string;
  options: Option[];
  onChange: (value: string) => void;
  takeFocus: boolean;
  demo?: string;
  /** The id of a visible label, when there is one. */
  labelId?: string;
}) {
  const [anchor, setAnchor] = useState<HTMLButtonElement | null>(null);
  const button = useRef<HTMLButtonElement>(null);
  const open = anchor !== null;
  const current = options.find((option) => option.value === value);
  return (
    <>
      <button
        ref={button}
        type="button"
        className={styles.picker}
        aria-haspopup="menu"
        aria-expanded={open}
        aria-labelledby={labelId ? `${labelId} ${labelId}-value` : undefined}
        aria-label={labelId ? undefined : `${label}: ${current?.label ?? ''}`}
        data-demo={demo}
        onClick={(event) => setAnchor(open ? null : event.currentTarget)}
      >
        {current?.lead}
        <span id={labelId ? `${labelId}-value` : undefined} className={styles.pickerText}>
          {current?.label}
        </span>
        <Icon name="chevronDown" className={styles.chevron} />
      </button>
      {anchor && (
        <Menu
          anchor={anchor}
          label={label}
          takeFocus={takeFocus}
          onClose={(back) => {
            setAnchor(null);
            if (back) button.current?.focus();
          }}
        >
          {options.map((option) => (
            <MenuItem
              key={option.value}
              kind="radio"
              checked={option.value === value}
              trail={option.trail}
              demo={demo ? `${demo}-${option.value}` : undefined}
              onSelect={() => {
                onChange(option.value);
                setAnchor(null);
                button.current?.focus({ preventScroll: true });
              }}
            >
              <span className={styles.optionLine}>
                {option.lead}
                {option.label}
              </span>
            </MenuItem>
          ))}
        </Menu>
      )}
    </>
  );
}

const ownerOptions = (owners: Owner[], you = true): Option[] => [
  ...owners.map((owner) => ({
    value: owner.id,
    label: owner.name,
    lead: <Avatar owner={owner} />,
    trail: you && owner.you ? 'You' : undefined,
  })),
  { value: 'none', label: 'Unassigned', lead: <Avatar owner={null} /> },
];

// ---- Hand Over --------------------------------------------------------------------------------

export function HandOverDialog({
  rows,
  owners,
  takeFocus,
  onClose,
  onHandOver,
}: {
  rows: Company[];
  owners: Owner[];
  takeFocus: boolean;
  onClose: () => void;
  onHandOver: (from: string | null, to: string | null) => void;
}) {
  const uid = useId();
  const you = owners.find((owner) => owner.you) ?? owners[0];
  const [from, setFrom] = useState(you?.id ?? 'none');
  const [to, setTo] = useState(owners.find((owner) => owner.id !== from)?.id ?? 'none');
  const key = (id: string) => (id === 'none' ? null : id);
  const nameOf = (id: string) => owners.find((owner) => owner.id === id)?.name ?? 'Unassigned';
  const owned = (id: string) => rows.filter((row) => row.owner === key(id));
  const fromRows = owned(from);
  const moving = fromRows.filter((row) => !isClosed(row.status));
  const staying = fromRows.length - moving.length;
  const toCount = owned(to).length;
  const high = moving.filter((row) => levelOf(row.intent) === 'high').length;
  const talking = moving.filter(
    (row) => row.status === 'contacted' || row.status === 'researching',
  ).length;
  const contacts = moving.reduce((sum, row) => sum + (row.contacts ?? 0), 0);
  const order = { high: 0, medium: 1, low: 2 } as const;
  const dots = moving
    .map((row) => levelOf(row.intent))
    .sort((a, b) => (a ? order[a] : 3) - (b ? order[b] : 3));
  const same = from === to;
  const ownsWord = (id: string, n: number) => (id === 'none' ? `${n} unowned` : `Owns ${n}`);

  return (
    <Panel
      kind="center"
      labelledBy={`${uid}-title`}
      takeFocus={takeFocus}
      onClose={onClose}
      part="hand-over"
    >
      <header className={styles.dialogHead}>
        <div>
          <h3 id={`${uid}-title`} className={styles.dialogTitle}>
            Hand Over Accounts
          </h3>
          <p className={styles.dialogNote}>
            Open accounts move to a teammate; closed ones stay as history.
          </p>
        </div>
        <button type="button" className={styles.iconButton} aria-label="Close" onClick={onClose}>
          <Icon name="x" />
        </button>
      </header>
      <div className={styles.dialogBody}>
        <div className={styles.handBox}>
          <div className={styles.handTop}>
            <span id={`${uid}-from`}>From</span>
            <span>{ownsWord(from, fromRows.length)}</span>
          </div>
          <div className={styles.handRow}>
            <Picker
              label="From"
              labelId={`${uid}-from`}
              value={from}
              options={ownerOptions(owners)}
              onChange={setFrom}
              takeFocus={takeFocus}
              demo="hand-from"
            />
            <span className={styles.handBig}>
              <strong>{moving.length}</strong>
              <span>{count(contacts)} contacts</span>
            </span>
          </div>
        </div>
        <button
          type="button"
          className={styles.swap}
          aria-label="Swap the two owners"
          onClick={() => {
            setFrom(to);
            setTo(from);
          }}
        >
          <Icon name="swapVertical" />
        </button>
        <div className={styles.handBox}>
          <div className={styles.handTop}>
            <span id={`${uid}-to`}>To</span>
            <span>{ownsWord(to, toCount)}</span>
          </div>
          <div className={styles.handRow}>
            <Picker
              label="To"
              labelId={`${uid}-to`}
              value={to}
              options={ownerOptions(owners)}
              onChange={setTo}
              takeFocus={takeFocus}
              demo="hand-to"
            />
            <span className={styles.handBig}>
              <strong>{same ? toCount : toCount + moving.length}</strong>
              <span>{same ? 'Same person' : `+${moving.length} incoming`}</span>
            </span>
          </div>
        </div>
        <div className={styles.handHot}>
          <span>
            <strong>{high} High Intent</strong>
            <span>{high ? 'Worth a call first' : 'None of them is hot'}</span>
          </span>
          <span className={styles.handDots} aria-hidden="true">
            {dots.slice(0, 16).map((level, i) => (
              <span key={i} className={styles.handDot} data-level={level ?? undefined} />
            ))}
          </span>
        </div>
        <dl className={styles.handFacts}>
          <div>
            <dt>Mid conversation</dt>
            <dd>
              {talking} {talking === 1 ? 'account' : 'accounts'}
            </dd>
          </div>
          <div>
            <dt>{from === 'none' ? 'Stays unassigned' : `Stays with ${nameOf(from)}`}</dt>
            <dd>
              {staying} closed {staying === 1 ? 'account' : 'accounts'}
            </dd>
          </div>
          <div>
            <dt>Status and lists</dt>
            <dd>Kept as they are</dd>
          </div>
        </dl>
      </div>
      <footer className={styles.dialogFoot}>
        <button
          type="button"
          className={styles.buttonWide}
          disabled={same || moving.length === 0}
          data-demo="hand-submit"
          onClick={() => onHandOver(key(from), key(to))}
        >
          <Icon name="swap" />
          Hand Over {moving.length} {moving.length === 1 ? 'Account' : 'Accounts'}
        </button>
      </footer>
    </Panel>
  );
}

// ---- Add or edit a company --------------------------------------------------------------------

export type CompanyDraft = Pick<
  Company,
  'name' | 'domain' | 'industry' | 'city' | 'country' | 'size' | 'funding' | 'owner' | 'status'
>;

export function CompanyDialog({
  company,
  owners,
  takeFocus,
  onClose,
  onSubmit,
}: {
  /** The company to edit; a new one without it. */
  company?: Company;
  owners: Owner[];
  takeFocus: boolean;
  onClose: () => void;
  onSubmit: (draft: CompanyDraft) => void;
}) {
  const uid = useId();
  const you = owners.find((owner) => owner.you);
  const [name, setName] = useState(company?.name ?? '');
  const [domain, setDomain] = useState(company?.domain ?? '');
  const [industry, setIndustry] = useState(company?.industry ?? 'Software');
  const [city, setCity] = useState(company?.city ?? 'Berlin');
  const [size, setSize] = useState(company?.size ?? 50);
  const [funding, setFunding] = useState(company?.funding ?? 'Seed');
  const [owner, setOwner] = useState(company ? (company.owner ?? 'none') : (you?.id ?? 'none'));
  const [status, setStatus] = useState<Status>(company?.status ?? 'new');
  const guess = name.trim() ? domainOf(name, industry) : 'company.com';

  const submit = (event: FormEvent) => {
    event.preventDefault();
    if (!name.trim()) return;
    onSubmit({
      name: name.trim(),
      domain: domain.trim() || guess,
      industry,
      city,
      country: CITIES.find(([each]) => each === city)?.[1] ?? 'DE',
      size,
      funding,
      owner: owner === 'none' ? null : owner,
      status,
    });
  };

  const label = (id: string, text: string) => (
    <span id={`${uid}-${id}`} className={styles.fieldLabel}>
      {text}
    </span>
  );

  return (
    <Panel
      kind="side"
      labelledBy={`${uid}-title`}
      takeFocus={takeFocus}
      onClose={onClose}
      part="company-form"
    >
      <form className={styles.form} onSubmit={submit}>
        <header className={styles.dialogHead} data-ruled>
          <div>
            <h3 id={`${uid}-title`} className={styles.dialogTitle}>
              {company ? 'Edit Company' : 'Add Company'}
            </h3>
            <p className={styles.dialogNote}>
              {company
                ? 'Changes show in the table at once.'
                : 'A company to prospect, matched on its domain.'}
            </p>
          </div>
          <button type="button" className={styles.iconButton} aria-label="Close" onClick={onClose}>
            <Icon name="x" />
          </button>
        </header>
        <div className={styles.formBody}>
          <label className={styles.field}>
            <span className={styles.fieldLabel}>Company Name</span>
            <input
              className={styles.input}
              value={name}
              placeholder="Quillstone Analytics"
              autoComplete="off"
              required
              data-autofocus
              onChange={(event) => setName(event.target.value)}
            />
          </label>
          <label className={styles.field}>
            <span className={styles.fieldLabel}>Domain</span>
            <input
              className={styles.input}
              value={domain}
              placeholder={guess}
              autoComplete="off"
              aria-describedby={`${uid}-domain`}
              onChange={(event) => setDomain(event.target.value)}
            />
            <span id={`${uid}-domain`} className={styles.fieldNote}>
              Contacts are matched on it.
            </span>
          </label>
          <div className={styles.formGrid}>
            <div className={styles.field}>
              {label('industry', 'Industry')}
              <Picker
                label="Industry"
                labelId={`${uid}-industry`}
                value={industry}
                options={INDUSTRIES.map((each) => ({ value: each, label: each }))}
                onChange={setIndustry}
                takeFocus={takeFocus}
              />
            </div>
            <div className={styles.field}>
              {label('hq', 'Headquarters')}
              <Picker
                label="Headquarters"
                labelId={`${uid}-hq`}
                value={city}
                options={CITIES.map(([each, country]) => ({
                  value: each,
                  label: each,
                  lead: <Flag country={country} />,
                }))}
                onChange={setCity}
                takeFocus={takeFocus}
              />
            </div>
            <div className={styles.field}>
              {label('size', 'Employees')}
              <div className={styles.stepper} role="group" aria-labelledby={`${uid}-size`}>
                <button
                  type="button"
                  aria-label="Fewer employees"
                  className={styles.stepButton}
                  onClick={() => setSize((now) => Math.max(1, now - (now > 100 ? 50 : 10)))}
                >
                  <Icon name="minus" />
                </button>
                <input
                  className={styles.stepInput}
                  inputMode="numeric"
                  aria-labelledby={`${uid}-size`}
                  value={String(size)}
                  onChange={(event) => {
                    const value = Number(event.target.value.replace(/\D/g, ''));
                    setSize(Number.isFinite(value) ? Math.min(999999, value) : 0);
                  }}
                />
                <button
                  type="button"
                  aria-label="More employees"
                  className={styles.stepButton}
                  onClick={() => setSize((now) => now + (now >= 100 ? 50 : 10))}
                >
                  <Icon name="plus" />
                </button>
              </div>
            </div>
            <div className={styles.field}>
              {label('funding', 'Funding')}
              <Picker
                label="Funding"
                labelId={`${uid}-funding`}
                value={funding}
                options={FUNDING.map((each) => ({ value: each, label: each }))}
                onChange={setFunding}
                takeFocus={takeFocus}
              />
            </div>
            <div className={styles.field}>
              {label('owner', 'Owner')}
              <Picker
                label="Owner"
                labelId={`${uid}-owner`}
                value={owner}
                options={ownerOptions(owners)}
                onChange={setOwner}
                takeFocus={takeFocus}
              />
            </div>
            <div className={styles.field}>
              {label('status', 'Status')}
              <Picker
                label="Status"
                labelId={`${uid}-status`}
                value={status}
                options={STATUSES.map((each) => ({
                  value: each,
                  label: '',
                  lead: <StatusChip status={each} />,
                }))}
                onChange={(value) => setStatus(value as Status)}
                takeFocus={takeFocus}
              />
              <span className={styles.hidden}>{STATUS_WORD[status]}</span>
            </div>
          </div>
        </div>
        <footer className={styles.dialogFoot} data-ruled>
          <button type="button" className={styles.button} onClick={onClose}>
            Cancel
          </button>
          <button type="submit" className={styles.buttonDark} disabled={!name.trim()}>
            {company ? (
              'Save Changes'
            ) : (
              <>
                <Icon name="plus" />
                Add Company
              </>
            )}
          </button>
        </footer>
      </form>
    </Panel>
  );
}

// ---- New list ---------------------------------------------------------------------------------

export function NewListDialog({
  n,
  takeFocus,
  onClose,
  onCreate,
}: {
  /** How many companies go in. */
  n: number;
  takeFocus: boolean;
  onClose: () => void;
  onCreate: (name: string) => void;
}) {
  const uid = useId();
  const [name, setName] = useState('');
  return (
    <Panel
      kind="center"
      labelledBy={`${uid}-title`}
      takeFocus={takeFocus}
      onClose={onClose}
      part="new-list"
    >
      <form
        onSubmit={(event) => {
          event.preventDefault();
          if (name.trim()) onCreate(name.trim());
        }}
      >
        <header className={styles.dialogHead}>
          <div>
            <h3 id={`${uid}-title`} className={styles.dialogTitle}>
              New List
            </h3>
            <p className={styles.dialogNote}>
              {n === 1 ? 'The company goes' : `${n} companies go`} straight in; the list shows in
              every Save menu.
            </p>
          </div>
          <button type="button" className={styles.iconButton} aria-label="Close" onClick={onClose}>
            <Icon name="x" />
          </button>
        </header>
        <div className={styles.dialogBody}>
          <label className={styles.field}>
            <span className={styles.fieldLabel}>Name</span>
            <input
              className={styles.input}
              value={name}
              placeholder="Fintech Q1"
              autoComplete="off"
              data-autofocus
              onChange={(event) => setName(event.target.value)}
            />
          </label>
        </div>
        <footer className={styles.dialogFoot} data-end>
          <button type="button" className={styles.button} onClick={onClose}>
            Cancel
          </button>
          <button type="submit" className={styles.buttonDark} disabled={!name.trim()}>
            Create List
          </button>
        </footer>
      </form>
    </Panel>
  );
}
```

### lead-table/demo-pointer.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

'use client';

import type { CSSProperties, Ref } from 'react';

/*
 * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or
 * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends
 * the step in progress at once: its promise rejects, which ends the script right there, and the
 * pointer fades out.
 *
 * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame.
 * The points of the flight are handed to whoever wants to move along with it. The arrow and the
 * hand are HTML boxes
 * that swap by their visibility: a change of style inside an SVG would lay the SVG out again.
 */

export type Point = { x: number; y: number };

/** A glide's path and timing, for an animation that plays along with it. */
export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };

export type Demo = {
  /** Resolves after `ms`. */
  wait: (ms: number) => Promise<void>;
  /** Glides the pointer to `to`, in the stage's own px, in `ms`; `along` gets the flight. */
  glide: (to: Point, ms: number, along?: (flight: Flight) => void) => Promise<void>;
  /** Shows the arrow, or the hand a link shows. */
  glyph: (name: 'arrow' | 'hand') => void;
};

/** What a stopped demo's pending step rejects with. */
class Stopped extends Error {}

/** Where the arrow's tip is, as shares of its box: the point the pointer stands on. */
const TIP: Point = { x: 0.15, y: 0.1 };

/** How many points a flight is drawn through. */
const SAMPLES = 24;

/** Plays `script` with the pointer over `stage`; returns the function that stops it. */
export function runDemo(
  stage: HTMLElement,
  pointer: HTMLElement,
  script: (demo: Demo) => Promise<void>,
): () => void {
  let alive = true;
  let flight: Animation | null = null;
  // The stylesheet sizes the pointer from the stage's width; its tip is where it stands.
  const tip = { x: pointer.offsetWidth * TIP.x, y: pointer.offsetHeight * TIP.y };
  const place = ({ x, y }: Point) => `translate(${x - tip.x}px, ${y - tip.y}px)`;
  const timers = new Set<number>();
  const pending = new Set<() => void>();
  // The pointer comes in from the lower right.
  let at: Point = { x: stage.offsetWidth * 0.8, y: stage.offsetHeight * 0.9 };

  /** Settles like `promise`, or rejects as soon as the demo stops. */
  const step = (promise: Promise<unknown>) =>
    new Promise<void>((resolve, reject) => {
      if (!alive) return reject(new Stopped());
      const cancel = () => reject(new Stopped());
      pending.add(cancel);
      promise.then(
        () => {
          pending.delete(cancel);
          if (alive) resolve();
        },
        () => {
          pending.delete(cancel);
          reject(new Stopped());
        },
      );
    });

  const wait = (ms: number) =>
    step(
      new Promise<void>((resolve) => {
        const timer = window.setTimeout(() => {
          timers.delete(timer);
          resolve();
        }, ms);
        timers.add(timer);
      }),
    );

  const glide: Demo['glide'] = (to, ms, along) => {
    const from = at;
    at = to;
    const dx = to.x - from.x;
    const dy = to.y - from.y;
    // A gentle curve, as a hand moves: the control point sits off the straight line.
    const bend = { x: (from.x + to.x) / 2 - dy * 0.18, y: (from.y + to.y) / 2 + dx * 0.18 };
    const points: Point[] = [];
    for (let i = 0; i <= SAMPLES; i += 1) {
      const t = i / SAMPLES;
      const u = 1 - t;
      points.push({
        x: u * u * from.x + 2 * u * t * bend.x + t * t * to.x,
        y: u * u * from.y + 2 * u * t * bend.y + t * t * to.y,
      });
    }
    const timing: KeyframeAnimationOptions = {
      duration: ms,
      easing: 'cubic-bezier(0.45, 0, 0.25, 1)',
    };
    flight?.cancel();
    // Where it rests once the flight is over.
    pointer.style.transform = place(to);
    flight = pointer.animate(
      points.map((point) => ({ transform: place(point) })),
      timing,
    );
    along?.({ points, timing });
    return step(flight.finished);
  };

  const glyph: Demo['glyph'] = (name) => {
    pointer.dataset.glyph = name;
  };

  pointer.dataset.glyph = 'arrow';
  pointer.style.transform = place(at);
  pointer.style.opacity = '1';
  script({ wait, glide, glyph }).catch((error: unknown) => {
    if (!(error instanceof Stopped)) throw error;
  });

  return () => {
    if (!alive) return;
    alive = false;
    timers.forEach((timer) => window.clearTimeout(timer));
    flight?.cancel();
    pending.forEach((cancel) => cancel());
    pointer.style.opacity = '0';
  };
}

/**
 * The pointer's box: it moves, on a layer of its own. Its size comes from its class, its colours
 * from the stage's tokens (lead-table.module.css).
 */
const POINTER: CSSProperties = {
  position: 'absolute',
  top: 0,
  left: 0,
  zIndex: 2,
  display: 'block',
  pointerEvents: 'none',
  opacity: 0,
  transition: 'opacity 0.3s ease',
  filter: 'drop-shadow(0 1px 1.5px var(--pointer-shadow))',
  willChange: 'transform',
};

/** One glyph over the other, each filling the box. */
const GLYPH: CSSProperties = { position: 'absolute', inset: 0, display: 'block' };
const DRAWING: CSSProperties = {
  display: 'block',
  width: '100%',
  height: '100%',
  overflow: 'visible',
};
/** The arrow is dark on a white edge, the hand white on a dark one, as the system draws them. */
const ARROW: CSSProperties = { fill: 'var(--pointer)', stroke: 'var(--pointer-edge)' };
const HAND: CSSProperties = { fill: 'var(--pointer-edge)', stroke: 'var(--pointer)' };

/**
 * The pointer the demo moves around: an arrow, or a pointing hand over what it can press. It is
 * decorative: what it does shows on the table itself. The stylesheet shows one glyph at a time,
 * from the box's data-glyph.
 */
export function DemoPointer({
  ref,
  className,
  arrowClass,
  handClass,
}: {
  ref: Ref<HTMLSpanElement>;
  className?: string;
  arrowClass?: string;
  handClass?: string;
}) {
  return (
    <span ref={ref} className={className} style={POINTER} aria-hidden="true">
      <span className={arrowClass} style={GLYPH}>
        <svg viewBox="0 0 20 20" style={DRAWING}>
          <path
            d="M3 2v14.5l3.6-3.3 2.6 5.6 2.4-1.1-2.6-5.5h5.2z"
            style={ARROW}
            strokeWidth="1.3"
            strokeLinejoin="round"
          />
        </svg>
      </span>
      <span className={handClass} style={GLYPH}>
        <svg viewBox="0 0 20 20" style={DRAWING}>
          {/* The tip of the finger sits where the arrow's tip is. */}
          <path
            d="M6.4 3.3c0-.8.6-1.4 1.3-1.4S9 2.5 9 3.3v5.5c.2-.5.6-.8 1.2-.8.7 0 1.2.5 1.2 1.2v.5c.2-.5.6-.8 1.2-.8.7 0 1.2.5 1.2 1.2v.7c.2-.4.6-.7 1.1-.7.7 0 1.2.5 1.2 1.2v3.5c0 2.8-1.9 4.9-4.6 4.9h-1.2c-1.8 0-3.1-.8-4.1-2.3l-2.6-3.9c-.4-.6-.2-1.3.4-1.7.6-.3 1.3-.2 1.7.4l.6.9z"
            style={HAND}
            strokeWidth="1.1"
            strokeLinejoin="round"
            transform="translate(-4.7 0.1)"
          />
        </svg>
      </span>
    </span>
  );
}
```

### lead-table/use-autoplay.ts

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

'use client';

import { useCallback, useEffect, useRef, useState } from 'react';

/*
 * When a demo may play on its own: while part of it is on screen and the tab is visible, never
 * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it
 * takes back over `idle` ms after their last input.
 */
export function useAutoplay<T extends HTMLElement>(idle = 6000) {
  const ref = useRef<T>(null);
  const [visible, setVisible] = useState(false);
  const [reduced, setReduced] = useState(true);
  const [taken, setTaken] = useState(false);
  const timer = useRef(0);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;
    let onScreen = false;
    const update = () => setVisible(onScreen && !document.hidden);
    const observer = new IntersectionObserver(
      ([entry]) => {
        onScreen = entry.isIntersecting;
        update();
      },
      { threshold: 0.3 },
    );
    observer.observe(element);
    document.addEventListener('visibilitychange', update);
    const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
    const readMotion = () => setReduced(motion.matches);
    readMotion();
    motion.addEventListener('change', readMotion);
    return () => {
      observer.disconnect();
      document.removeEventListener('visibilitychange', update);
      motion.removeEventListener('change', readMotion);
    };
  }, []);

  const take = useCallback(() => {
    setTaken(true);
    window.clearTimeout(timer.current);
    timer.current = window.setTimeout(() => setTaken(false), idle);
  }, [idle]);
  useEffect(() => () => window.clearTimeout(timer.current), []);

  return { ref, take, visible, reduced, playing: visible && !reduced && !taken };
}
```