# Records Table

> CRM-style AI spreadsheet with tags, sorting and configurable AI columns.

- **Slug:** `records-table`
- **Kind:** component · **Category:** Data
- **Source:** ported from [Beautiful UI](https://github.com/slev12397/beautiful-ui/blob/main/components/primitives/RecordsTable.tsx) (MIT)
- **Docs page:** https://viewership.ai/ui/components/records-table
- **All files in one bundle:** https://viewership.ai/ui/components/records-table.bundle.txt
- **Structured JSON:** https://viewership.ai/ui/components/records-table.json

## Install

1. Import the foundation tokens once (download: https://viewership.ai/ui/raw/foundation.css.txt):

```astro
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
---
```

2. Copy this file into `src/components/viewership/records-table/`:

   - `RecordsTable.astro` (raw: https://viewership.ai/ui/raw/records-table/RecordsTable.astro.txt)

3. Copy its registry dependencies, each into its own sibling folder under `src/components/viewership/`:

   - `glide-menu`: `GlideMenu.astro`, `glide-menu.ts` (docs: https://viewership.ai/ui/components/glide-menu.md)
   - `switch`: `Switch.astro` (docs: https://viewership.ai/ui/components/switch.md)

npm packages: none.

## Usage

```astro
---
import RecordsTable from "../components/viewership/records-table/RecordsTable.astro";
---

<RecordsTable />
```

## Example

The gallery demo, verbatim. Use the import path from Usage instead of `./`.

```astro
---
import RecordsTable, { type RecordRow } from "./RecordsTable.astro";

/* Sources that AI engines cite for Northwind Outdoor. All names are fictional. */
const accounts: RecordRow[] = [
  { id: "campinggear", name: "r/CampingGear · Reddit", tags: ["Reddit", "Cited"], last: "9 days ago", lastDays: 9, strength: "strong", website: "reddit.test", ai: "Ridgecrest, Summit & Pine" },
  { id: "trailnotes", name: "Trail Notes Weekly · Leeds", tags: ["Reddit", "YouTube", "Blog"], last: "3 weeks ago", lastDays: 21, strength: "strong", website: "trailnotes.test", ai: "Alder Peak, Two Rivers Co" },
  { id: "gearlab", name: "Gear Lab Reviews · Melbourne", tags: ["Forum", "Reviews"], last: "2 months ago", lastDays: 60, strength: "weak", website: "gearlab-reviews.test", ai: "Summit & Pine, Saltline Gear" },
  { id: "basecamp-pod", name: "Basecamp Podcast · Ghent", tags: ["Podcast", "Cited", "Seasonal"], last: "15 days ago", lastDays: 15, strength: "weak", website: "basecamp-pod.test", ai: "Two Rivers Co, Emberline" },
  { id: "outdoor-wire", name: "Outdoor Wire News · Denver", tags: ["News", "Newsletter"], last: "over 1 year ago", lastDays: 400, strength: "veryweak", website: "outdoor-wire.test", ai: "Ridgecrest, Pale Ridge" },
  { id: "packlight", name: "Packlight Shorts · Lisbon", tags: ["Shorts", "YouTube"], last: "5 months ago", lastDays: 150, strength: "veryweak", ai: "Saltline Gear, Alder Peak" },
  { id: "ul-hikers", name: "r/Ultralight · Reddit", tags: ["Reddit", "Guides", "Seasonal"], last: "No contact", strength: "none", website: "reddit.test", ai: "Pale Ridge, Emberline" },
  { id: "alpine-stay", name: "Alpine Stay Guide · Banff", tags: ["Travel", "Reviews"], last: "3 months ago", lastDays: 90, strength: "veryweak", website: "alpine-stay.test", ai: "Summit & Pine, Two Rivers Co" },
  { id: "hike-forum", name: "HikeTalk Forum · Chicago", tags: ["Forum", "Cited"], last: "over 1 year ago", lastDays: 420, strength: "veryweak", website: "hiketalk.test", ai: "Ridgecrest, Saltline Gear" },
  { id: "orchard-outfit", name: "Orchard Outfitters · Hobart", tags: ["Retailer", "Blog", "Newsletter"], last: "2 months ago", lastDays: 62, strength: "weak", website: "orchard-outfit.test", ai: "Alder Peak, Pale Ridge" },
  { id: "kiln-trails", name: "Kiln Trails Channel · Kyoto", tags: ["Reddit", "Reviews", "Seasonal"], last: "11 days ago", lastDays: 11, strength: "strong", website: "kiln-trails.test", ai: "Emberline, Summit & Pine" },
  { id: "saltgrass", name: "Saltgrass Daily · Oslo", tags: ["News", "YouTube"], last: "about 1 month ago", lastDays: 33, strength: "weak", website: "saltgrass.test", ai: "Two Rivers Co, Ridgecrest" },
  { id: "wren", name: "Wren & Wild Blog · Edinburgh", tags: ["Reddit", "Podcast"], last: "4 days ago", lastDays: 4, strength: "strong", website: "wren-wild.test", ai: "Saltline Gear, Pale Ridge" },
  { id: "dune", name: "Dune Road Surf Talk · San Diego", tags: ["Shorts", "Seasonal"], last: "8 months ago", lastDays: 240, strength: "weak", website: "dune-road.test", ai: "Alder Peak, Emberline" },
  { id: "foundry", name: "Foundry Expo Hall · Pittsburgh", tags: ["Events", "Cited", "Reviews"], last: "about 2 years ago", lastDays: 730, strength: "none", website: "foundry-expo.test", ai: "Summit & Pine, Ridgecrest" },
  { id: "parlour", name: "Parlour Nine Vlog · Montréal", tags: ["Reddit", "YouTube"], last: "about 1 month ago", lastDays: 30, strength: "veryweak", ai: "Two Rivers Co, Saltline Gear" },
  { id: "greenhouse", name: "Greenhouse Goods · Copenhagen", tags: ["Retailer", "Guides"], last: "15 days ago", lastDays: 15, strength: "weak", website: "greenhouse-goods.test", ai: "Pale Ridge, Alder Peak" },
  { id: "low-tide", name: "Low Tide Paddle Club · Halifax", tags: ["Forum", "Blog"], last: "9 days ago", lastDays: 9, strength: "strong", website: "low-tide.test", ai: "Emberline, Two Rivers Co" },
  { id: "sundial", name: "Sundial Books & Maps · Seville", tags: ["Reddit", "Retailer"], last: "over 2 years ago", lastDays: 800, strength: "none", website: "sundial-maps.test", ai: "Ridgecrest, Summit & Pine" },
  { id: "meridian", name: "Meridian Media · Singapore", tags: ["News", "Newsletter", "Cited"], last: "about 1 month ago", lastDays: 35, strength: "veryweak", website: "meridian-media.test", ai: "Saltline Gear, Emberline" },
  { id: "thistle", name: "Thistle Trail Guides · Dublin", tags: ["Reddit", "Guides"], last: "No contact", strength: "none", ai: "Alder Peak, Pale Ridge" },
  { id: "clay", name: "Clay Ridge Climbing · Naples", tags: ["Forum", "YouTube"], last: "almost 2 years ago", lastDays: 700, strength: "veryweak", website: "clay-ridge.test", ai: "Two Rivers Co, Summit & Pine" },
  { id: "cobalt", name: "Cobalt Climbing Blog · Boulder", tags: ["Shorts", "Newsletter"], last: "6 days ago", lastDays: 6, strength: "strong", website: "cobalt-climbing.test", ai: "Ridgecrest, Emberline" },
  { id: "verandah", name: "Verandah Travel Notes · Galle", tags: ["Travel", "Seasonal", "Reviews"], last: "18 days ago", lastDays: 18, strength: "weak", website: "verandah-travel.test", ai: "Pale Ridge, Saltline Gear" },
  { id: "loom", name: "Loom & Ledge · Glasgow", tags: ["Reddit", "Events", "Blog"], last: "2 days ago", lastDays: 2, strength: "strong", website: "loom-ledge.test", ai: "Summit & Pine, Alder Peak" },
  { id: "pressroom", name: "The Gear Pressroom · Wellington", tags: ["Reddit", "YouTube", "Cited"], last: "5 weeks ago", lastDays: 36, strength: "weak", website: "gear-pressroom.test", ai: "Two Rivers Co, Ridgecrest" },
  { id: "alder", name: "Alder Camp Forum · Tallinn", tags: ["Forum", "Reviews"], last: "No contact", strength: "none", ai: "Emberline, Pale Ridge" },
  { id: "bluebird", name: "Bluebird Bike Trails · Utrecht", tags: ["Reddit", "Blog", "Seasonal"], last: "7 days ago", lastDays: 7, strength: "strong", website: "bluebird-trails.test", ai: "Saltline Gear, Summit & Pine" },
];
---

<RecordsTable
  rows={accounts}
  caption="Cited sources"
  columns={{ company: "Source" }}
  tagColors={{
    Reddit: "red",
    Cited: "amber",
    YouTube: "orange",
    Reviews: "cyan",
    Blog: "green",
    Seasonal: "yellow",
    News: "purple",
    Newsletter: "lime",
    Forum: "magenta",
    Podcast: "pink",
    Guides: "cyan",
    Retailer: "green",
    Shorts: "orange",
    Travel: "purple",
    Events: "magenta",
  }}
  aiColumn={{ label: "Competitors cited", prompt: { before: "Find competitors cited by ", chip: "Source" } }}
/>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `rows` | `RecordRow[]` |  | { id, name, tags, last, lastDays?, strength, website?, ai? }. Omit `lastDays` for no contact. `ai` is the AI column value. |
| `caption` | `string` | `"Records"` | Hidden caption. Also names the scroll region and select-all. |
| `columns` | `{ company?, categories?, last?, strength?, links? }` |  | Header labels. |
| `columnConfig` | `Partial<Record<ColumnKey, Partial<ColumnConfig>>>` |  | Popover overrides: type, tool, toolKind, inputs, prompt. |
| `tagColors` | `Record<string, string>` |  | Tag → palette name (amber, lime, yellow, purple, orange, cyan, red, magenta, green, pink) or CSS color. |
| `strengthLabels` | `Partial<Record<Strength, string>>` |  | Strength level labels. |
| `models` | `string[]` | `["Atlas 5", "Atlas 4.2", "Atlas Mini"]` | Tool picker models. First is the default. |
| `aiColumn` | `{ label, type?, tool?, prompt? } \| null` | `{ label: "Competitors" }` | AI property added by the + menu. Cells resolve to each row's `ai`. |
| `sort` | `{ key: "name" \| "last" \| "strength"; dir?: 1 \| -1 }` | `{ key: "name", dir: 1 }` | Initial sort. |
| `fill` | `boolean` | `false` | Fill the parent card, no border or shadow. |
| `maxHeight` | `string` | `"438px"` | Scroll viewport max height. |
| `Events` | `CustomEvent` |  | vs-records-selection { ids }, vs-records-sort { key, dir }, vs-records-calculate { column, label }, vs-records-property { type }, vs-records-add-calculation. |

## Source

### `src/components/viewership/records-table/RecordsTable.astro`

```astro
---
/**
 * RecordsTable: CRM-style AI spreadsheet. Click a header to configure the
 * column and run "Go calculate". The + header adds an AI property.
 *
 * Ported from Beautiful UI `components/primitives/RecordsTable.tsx`
 * (MIT, © 2026 Shane Levine).
 *
 * - Real <table>; sortable headers expose `aria-sort`.
 * - Resize by dragging the header edge or ←/→ on the handle (Shift = bigger steps).
 * - Popovers use the top layer (Popover API), with a `position: fixed` fallback.
 * - Renders without JS (sorted by name); interaction needs the script.
 *
 * Events (bubbling, on the root):
 *   vs-records-selection   detail: { ids: string[] }
 *   vs-records-sort        detail: { key: "name" | "last" | "strength", dir: 1 | -1 }
 *   vs-records-calculate   detail: { column: ColumnKey, label: string }
 *   vs-records-property    detail: { type: string }
 *   vs-records-add-calculation
 *
 * @example
 * <RecordsTable rows={accounts} caption="Partner accounts" aiColumn={{ label: "Competing brands" }} />
 */
import type { HTMLAttributes } from "astro/types";
import GlideMenu from "../glide-menu/GlideMenu.astro";
import Switch from "../switch/Switch.astro";

export type Strength = "strong" | "weak" | "veryweak" | "none";
export type ColumnKey = "company" | "categories" | "last" | "strength" | "links" | "ai";

export interface RecordRow {
  id: string;
  name: string;
  tags: string[];
  /** Display text for the last interaction, e.g. "9 days ago" or "No contact". */
  last: string;
  /** Days since the last interaction, for sorting. Omit (or null) for no contact. */
  lastDays?: number | null;
  strength: Strength;
  /** Bare domain, e.g. "northwind.test". */
  website?: string;
  /** The value the AI column resolves to for this row. */
  ai?: string;
}

export interface RecordsPrompt {
  before: string;
  /** An @-mentioned input, rendered as a chip. */
  chip?: string;
  after?: string;
}

export interface ColumnConfig {
  label: string;
  /** Property type shown in the popover (one of the property types). */
  type: string;
  /** Tool label, e.g. a model name, "Web search" or "User input". */
  tool: string;
  toolKind: "model" | "web" | "user";
  inputs: string[];
  prompt?: RecordsPrompt;
}

export interface AiColumn {
  label: string;
  type?: string;
  tool?: string;
  prompt?: RecordsPrompt;
}

type Props = HTMLAttributes<"div"> & {
  rows: RecordRow[];
  /** Table caption (visually hidden) and scroll-region name. */
  caption?: string;
  /** Header labels. */
  columns?: Partial<Record<Exclude<ColumnKey, "ai">, string>>;
  /** Per-column popover configuration overrides. */
  columnConfig?: Partial<Record<ColumnKey, Partial<ColumnConfig>>>;
  /** Tag → palette name or CSS color. Unlisted tags get a stable color. */
  tagColors?: Record<string, string>;
  /** Labels for the four strength levels. */
  strengthLabels?: Partial<Record<Strength, string>>;
  /** Models offered in the Tool picker; the first is the default. */
  models?: string[];
  /** The AI property the + menu adds. `null` hides the + menu's effect. */
  aiColumn?: AiColumn | null;
  /** Initial sort. */
  sort?: { key: "name" | "last" | "strength"; dir?: 1 | -1 };
  /** Stretch to fill a parent card, dropping the border and shadow. */
  fill?: boolean;
  /** Max height of the scroll viewport (ignored with `fill`). */
  maxHeight?: string;
};

const {
  rows,
  caption = "Records",
  columns = {},
  columnConfig = {},
  tagColors = {},
  strengthLabels = {},
  models = ["Atlas 5", "Atlas 4.2", "Atlas Mini"],
  aiColumn = { label: "Competitors" },
  sort = { key: "name", dir: 1 },
  fill = false,
  maxHeight = "438px",
  class: className,
  style,
  ...rest
} = Astro.props;

const uid = `vs-rt-${Math.random().toString(36).slice(2, 8)}`;

/* ── labels ─────────────────────────────────────────────── */
const L = {
  company: columns.company ?? "Company",
  categories: columns.categories ?? "Categories",
  last: columns.last ?? "Last interaction",
  strength: columns.strength ?? "Connection strength",
  links: columns.links ?? "Links",
  ai: aiColumn?.label ?? "Insights",
};

const STRENGTH: Record<Strength, { label: string; color: string; rank: number }> = {
  strong: { label: strengthLabels.strong ?? "Very strong", color: "var(--vs-green)", rank: 3 },
  weak: { label: strengthLabels.weak ?? "Weak", color: "var(--vs-orange)", rank: 2 },
  veryweak: { label: strengthLabels.veryweak ?? "Very weak", color: "var(--vs-red)", rank: 1 },
  none: { label: strengthLabels.none ?? "No communication", color: "var(--vs-ink-3)", rank: 0 },
};

/* ── tag colors: one mid-lightness base hue per tag, mixed against the theme ── */
const PALETTE: Record<string, string> = {
  amber: "oklch(0.76 0.13 70)",
  lime: "oklch(0.77 0.16 122)",
  yellow: "oklch(0.80 0.15 101)",
  purple: "oklch(0.62 0.18 293)",
  orange: "oklch(0.71 0.16 48)",
  cyan: "oklch(0.72 0.10 221)",
  red: "oklch(0.64 0.19 27)",
  magenta: "oklch(0.66 0.21 323)",
  green: "oklch(0.70 0.13 162)",
  pink: "oklch(0.67 0.19 3)",
};
const paletteKeys = Object.keys(PALETTE);
const tagBase = (tag: string) => {
  const chosen = tagColors[tag];
  if (chosen) return PALETTE[chosen] ?? chosen;
  let h = 0;
  for (const ch of tag) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
  return PALETTE[paletteKeys[h % paletteKeys.length]];
};

/* ── glyphs (upstream-drawn, MIT) ───────────────────────── */
const TYPE_GLYPHS: Record<string, string> = {
  Text: '<path d="M4 6h16M4 12h10M4 18h7" />',
  File: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6" />',
  Collection: '<ellipse cx="12" cy="5" rx="8" ry="3" /><path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3" />',
  "Single select": '<circle cx="12" cy="12" r="9" /><path d="m8.5 12 2.4 2.4 4.6-4.9" />',
  "Multi select": '<path d="M11 6h9M11 12h9M11 18h9" /><path d="M4 6l1.5 1.5L8 5M4 12l1.5 1.5L8 11M4 18l1.5 1.5L8 17" />',
  URL: '<path d="M10 13a5 5 0 0 0 7.1.1l2-2a5 5 0 0 0-7.1-7.1l-1.1 1.1" /><path d="M14 11a5 5 0 0 0-7.1-.1l-2 2A5 5 0 0 0 12 20l1.1-1.1" />',
  Reference: '<path d="M7 17 17 7M9 7h8v8" />',
  JSON: '<path d="M8 4c-2 0-2 2-2 3s.5 3-2 3c2.5 0 2 2 2 3s0 3 2 3" /><path d="M16 4c2 0 2 2 2 3s-.5 3 2 3c-2.5 0-2 2-2 3s0 3-2 3" />',
  "File splitter": '<rect x="8" y="8" width="12" height="12" rx="2" /><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" />',
  Date: '<rect x="3" y="5" width="18" height="16" rx="2.5" /><path d="M8 3v4M16 3v4M3 10h18" />',
};
const TOOL_GLYPHS = {
  model: '<path d="M12 3l1.7 5.1a2 2 0 0 0 1.2 1.2L20 11l-5.1 1.7a2 2 0 0 0-1.2 1.2L12 19l-1.7-5.1a2 2 0 0 0-1.2-1.2L4 11l5.1-1.7a2 2 0 0 0 1.2-1.2z" />',
  web: '<circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3a13.5 13.5 0 0 1 3.5 9 13.5 13.5 0 0 1-3.5 9 13.5 13.5 0 0 1-3.5-9A13.5 13.5 0 0 1 12 3z" />',
  user: '<circle cx="12" cy="8" r="4" /><path d="M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1" />',
};
const PROPERTY_TYPES = ["Text", "File", "Collection", "Single select", "Multi select", "URL", "Reference", "JSON", "File splitter"];
const svg = (inner: string, size = 14, sw = 1.8) =>
  `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
const modelSvg = () =>
  `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">${TOOL_GLYPHS.model}</svg>`;
const I = {
  check: '<path d="m5 12 4 4L19 6" />',
  chevron: '<path d="M9 6l6 6-6 6" />',
  arrowDown: '<path d="M12 5v14M5 12l7 7 7-7" />',
  plus: '<path d="M12 5v14M5 12h14" />',
  external: '<path d="M14 5h5v5M19 5l-8 8" />',
  info: '<circle cx="12" cy="12" r="9" /><path d="M12 8h.01M11 12h1v4h1" />',
  refresh: '<path d="M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6" />',
  pin: '<path d="M12 17v5M8 3h8l-1 7 3 3H6l3-3-1-7z" />',
  gear: '<circle cx="12" cy="12" r="3" /><path d="M12 2v3M12 19v3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M2 12h3M19 12h3M4.9 19.1 7 17M17 7l2.1-2.1" />',
  eyeOff: '<path d="M10.6 5.1A9.8 9.8 0 0 1 12 5c7 0 10 7 10 7a16.3 16.3 0 0 1-2.1 3M6.6 6.6A16 16 0 0 0 2 12s3 7 10 7a9.7 9.7 0 0 0 5.4-1.6M3 3l18 18" /><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />',
  compact: '<path d="M4 8h16M7 4 3 8l4 4M17 4l4 4-4 4M4 16h16" />',
  reset: '<path d="M3 12a9 9 0 1 0 3-6.7M3 4v6h6" />',
  clear: '<path d="M5 5l14 14M19 5 5 19" />',
};

/* ── per-column popover configuration ───────────────────── */
const model0 = models[0] ?? "Model";
const baseConfig: Record<ColumnKey, ColumnConfig> = {
  company: { label: L.company, type: "Text", tool: "User input", toolKind: "user", inputs: [] },
  categories: {
    label: L.categories, type: "Multi select", tool: model0, toolKind: "model", inputs: [L.company],
    prompt: { before: "Tag each ", chip: L.company, after: " with its market categories." },
  },
  last: { label: L.last, type: "Date", tool: "User input", toolKind: "user", inputs: [] },
  strength: {
    label: L.strength, type: "Single select", tool: model0, toolKind: "model", inputs: [L.last],
    prompt: { before: "Score the relationship from ", chip: L.last, after: "." },
  },
  links: {
    label: L.links, type: "URL", tool: "Web search", toolKind: "web", inputs: [L.company],
    prompt: { before: "Find the website for ", chip: L.company, after: "." },
  },
  ai: {
    label: L.ai, type: aiColumn?.type ?? "Text", tool: aiColumn?.tool ?? "Web search", toolKind: "web", inputs: [L.company],
    prompt: aiColumn?.prompt ?? { before: `Find ${L.ai.toLowerCase()} for `, chip: L.company },
  },
};
const config = Object.fromEntries(
  (Object.keys(baseConfig) as ColumnKey[]).map((k) => [k, { ...baseConfig[k], ...columnConfig[k] }]),
) as Record<ColumnKey, ColumnConfig>;
const inputOptions = [L.company, L.categories, L.last, L.strength, L.links];

/* ── initial order (server render matches the script's sort) ── */
const rank = (s: Strength) => STRENGTH[s].rank;
const lastValue = (r: RecordRow) => (r.lastDays === undefined || r.lastDays === null ? Number.POSITIVE_INFINITY : r.lastDays);
const dir = sort.dir ?? 1;
const sorted = [...rows].sort((a, b) => {
  const v =
    sort.key === "name"
      ? a.name.localeCompare(b.name)
      : sort.key === "last"
        ? lastValue(a) - lastValue(b) || a.name.localeCompare(b.name)
        : rank(a.strength) - rank(b.strength);
  return (Number.isNaN(v) ? 0 : v) * dir;
});

const avg = rows.length ? Math.round((rows.reduce((sum, r) => sum + rank(r.strength), 0) / rows.length / 3) * 100) : 0;
const avgColor = avg >= 67 ? "var(--vs-green)" : avg >= 34 ? "var(--vs-orange)" : "var(--vs-red)";
const linkCount = rows.filter((r) => r.website).length;
const ariaSort = (key: string) => (sort.key === key ? (dir === 1 ? "ascending" : "descending") : undefined);

const clientConfig = {
  uid,
  rows: rows.map((r) => ({ id: r.id, name: r.name, lastDays: lastValue(r), last: r.last, rank: rank(r.strength), ai: r.ai ?? "" })),
  sort: { key: sort.key, dir },
  columns: config,
  typeGlyphs: TYPE_GLYPHS,
  toolGlyphs: TOOL_GLYPHS,
  aiEnabled: aiColumn !== null,
};

const rootStyle = [fill ? "" : `--rt-max-h: ${maxHeight}`, typeof style === "string" ? style : ""].filter(Boolean).join("; ") || undefined;

const headers: Array<{ key: Exclude<ColumnKey, "company" | "ai">; glyph: string; sortKey?: "last" | "strength" }> = [
  { key: "categories", glyph: TYPE_GLYPHS["Multi select"] },
  { key: "last", glyph: TYPE_GLYPHS.Date, sortKey: "last" },
  { key: "strength", glyph: TYPE_GLYPHS["Single select"], sortKey: "strength" },
  { key: "links", glyph: TYPE_GLYPHS.URL },
];
---

<div class:list={["vs-records-table", { "is-fill": fill }, className]} style={rootStyle} data-uid={uid} {...rest}>
  <script type="application/json" data-vs-config set:html={JSON.stringify(clientConfig)} />

  <div class="rt-scroll" tabindex="0" role="region" aria-label={`${caption}. Scroll horizontally and vertically to view all columns and records.`}>
    <table class="rt-table">
      <caption class="rt-sr">{caption}</caption>
      <colgroup>
        <col data-col="company" style="width: 270px" />
        <col data-col="categories" style="width: 275px" />
        <col data-col="last" style="width: 190px" />
        <col data-col="strength" style="width: 210px" />
        <col data-col="links" style="width: 175px" />
        <col data-col="actions" style="width: 100px" />
      </colgroup>
      <thead>
        <tr>
          <th scope="col" class="rt-th rt-sticky" data-col="company" aria-sort={ariaSort("name")}>
            <div class="rt-head rt-head--company">
              <label class="rt-checkbox">
                <input type="checkbox" data-select-all aria-label={`Select all ${caption.toLowerCase()}`} />
                <span class="rt-box" aria-hidden="true"><Fragment set:html={svg(I.check, 12)} /><span class="rt-dash"></span></span>
              </label>
              <button type="button" class="rt-head-btn" data-prop="company" aria-haspopup="dialog" aria-expanded="false">
                <span class="rt-trunc">{L.company}</span>
              </button>
              <button type="button" class="rt-sort" data-sort="name" aria-label={`Sort by ${L.company}`}>
                <Fragment set:html={svg(I.arrowDown, 12)} />
              </button>
            </div>
            <span class="rt-resize" role="separator" tabindex="0" aria-orientation="vertical" aria-label={`Resize ${L.company} column`} data-resize="company" data-min="180"></span>
          </th>
          {
            headers.map((h) => (
              <th scope="col" class="rt-th" data-col={h.key} aria-sort={h.sortKey ? ariaSort(h.sortKey) : undefined}>
                <div class="rt-head">
                  <button type="button" class="rt-head-btn" data-prop={h.key} aria-haspopup="dialog" aria-expanded="false">
                    <span class="rt-head-icon" set:html={svg(h.glyph, 15)} />
                    <span class="rt-trunc">{L[h.key]}</span>
                  </button>
                  {h.sortKey && (
                    <button type="button" class="rt-sort" data-sort={h.sortKey} aria-label={`Sort by ${L[h.key]}`}>
                      <Fragment set:html={svg(I.arrowDown, 12)} />
                    </button>
                  )}
                </div>
                <span class="rt-resize" role="separator" tabindex="0" aria-orientation="vertical" aria-label={`Resize ${L[h.key]} column`} data-resize={h.key} data-min="120"></span>
              </th>
            ))
          }
          <th scope="col" class="rt-th" data-col="ai" hidden>
            <div class="rt-head">
              <button type="button" class="rt-head-btn" data-prop="ai" aria-haspopup="dialog" aria-expanded="false">
                <span class="rt-head-icon" data-ai-icon set:html={svg(TYPE_GLYPHS[config.ai.type] ?? TYPE_GLYPHS.Text, 15)} />
                <span class="rt-trunc">{L.ai}</span>
              </button>
            </div>
            <span class="rt-resize" role="separator" tabindex="0" aria-orientation="vertical" aria-label={`Resize ${L.ai} column`} data-resize="ai" data-min="120"></span>
          </th>
          <th scope="col" class="rt-th" data-col="actions">
            <span class="rt-sr">Table actions</span>
            <div class="rt-actions">
              <button type="button" class="rt-icon-btn" data-open-add aria-label="New property" aria-haspopup="menu" aria-expanded="false" data-recpop>
                <Fragment set:html={svg(I.plus, 15, 2)} />
              </button>
              <button type="button" class="rt-icon-btn rt-icon-btn--quiet" data-open-options aria-label="Table options" aria-haspopup="menu" aria-expanded="false" data-recpop>
                <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6" /><circle cx="12" cy="12" r="1.6" /><circle cx="19" cy="12" r="1.6" /></svg>
              </button>
            </div>
          </th>
        </tr>
      </thead>
      <tbody>
        {
          sorted.map((row, index) => {
            const s = STRENGTH[row.strength];
            return (
              <tr class="rt-row" data-id={row.id}>
                <td class="rt-cell rt-sticky rt-company" data-col="company">
                  <div class="rt-company-inner">
                  <span class="rt-gutter">
                    <span class="rt-rownum" aria-hidden="true">{index + 1}</span>
                    <label class="rt-checkbox">
                      <input type="checkbox" data-select={row.id} aria-label={`Select ${row.name}`} />
                      <span class="rt-box" aria-hidden="true"><Fragment set:html={svg(I.check, 12)} /><span class="rt-dash"></span></span>
                    </label>
                  </span>
                  <span class="rt-mark" aria-hidden="true">{row.name.slice(0, 1).toUpperCase()}</span>
                  {row.website ? (
                    <a class="rt-name has-link" href={`https://${row.website}`} title={row.name}>{row.name}</a>
                  ) : (
                    <span class="rt-name" title={row.name}>{row.name}</span>
                  )}
                  </div>
                </td>
                <td class="rt-cell" data-col="categories">
                  <span class="rt-val">
                    <span class="rt-tags" title={row.tags.join(", ")}>
                      {row.tags.map((tag) => (
                        <span class="rt-tag" style={`--rt-tag: ${tagBase(tag)}`}>{tag}</span>
                      ))}
                      <span class="rt-more" hidden aria-hidden="true"></span>
                    </span>
                  </span>
                </td>
                <td class:list={["rt-cell", { "rt-muted": row.lastDays === undefined || row.lastDays === null }]} data-col="last">
                  <span class="rt-val">{row.last}</span>
                </td>
                <td class="rt-cell" data-col="strength">
                  <span class="rt-val rt-strength">
                    <span class="rt-dot" style={`background: ${s.color}`} aria-hidden="true"></span>
                    {s.label}
                  </span>
                </td>
                <td class="rt-cell" data-col="links">
                  <span class="rt-val">
                    {row.website ? (
                      <a class="rt-link" href={`https://${row.website}`} title={row.website} target="_blank" rel="noreferrer">
                        <span class="rt-link-label">{row.website}</span>
                        <Fragment set:html={svg(I.external, 12)} />
                        <span class="rt-sr">(opens in a new tab)</span>
                      </a>
                    ) : (
                      <span class="rt-muted" aria-label="No link">–</span>
                    )}
                  </span>
                </td>
                <td class="rt-cell" data-col="ai" hidden>
                  <span class="rt-val"><span class="rt-muted">–</span></span>
                </td>
                <td class="rt-cell" data-col="actions"></td>
              </tr>
            );
          })
        }
      </tbody>
      <tfoot>
        <tr>
          <td class="rt-cell rt-sticky" data-col="company">
            <span class="rt-foot rt-foot-label"><span class="rt-foot-num">{rows.length}</span> count</span>
          </td>
          <td class="rt-cell" data-col="categories">
            <button type="button" class="rt-add-calc" data-add-calc><Fragment set:html={svg(I.plus, 15)} />Add calculation</button>
          </td>
          <td class="rt-cell rt-muted" data-col="last"><span class="rt-foot">–</span></td>
          <td class="rt-cell" data-col="strength">
            <span class="rt-foot rt-average"><span class="rt-dot" style={`background: ${avgColor}`} aria-hidden="true"></span>{avg}% average</span>
          </td>
          <td class="rt-cell" data-col="links"><span class="rt-foot rt-muted">{linkCount} links</span></td>
          <td class="rt-cell rt-muted" data-col="ai" hidden><span class="rt-foot" data-ai-foot>–</span></td>
          <td class="rt-cell" data-col="actions"></td>
        </tr>
      </tfoot>
    </table>
  </div>

  <p class="rt-sr" aria-live="polite" data-live></p>

  {/* ── property configuration popover ─────────────────── */}
  <div class="rt-pop rt-prop" id={`${uid}-prop`} popover="manual" role="dialog" aria-labelledby={`${uid}-prop-title`} data-recpop data-pop="prop">
    <div class="rt-prop-title" id={`${uid}-prop-title`} data-prop-title>{L.company}</div>

    <div class="rt-config-row">
      <span class="rt-config-label" id={`${uid}-type-label`}>Type</span>
      <button type="button" class="rt-config-btn" data-picker-open="type" aria-haspopup="menu" aria-expanded="false" aria-labelledby={`${uid}-type-label ${uid}-type-value`}>
        <span class="rt-ink-2" data-type-icon set:html={svg(TYPE_GLYPHS.Text)} />
        <span id={`${uid}-type-value`} data-type-label>Text</span>
        <span class="rt-ink-3" set:html={svg(I.chevron, 12, 2.2)} />
      </button>
      <div class="rt-picker" data-picker="type" hidden>
        <div class="rt-picker-label" aria-hidden="true">Property type</div>
        <GlideMenu class="rt-picker-list" role="menu" aria-label="Property type" arrowKeys>
          {
            PROPERTY_TYPES.map((type) => (
              <button type="button" class="rt-menu-row" data-menu-row role="menuitemradio" aria-checked="false" data-value={type}>
                <span class="rt-row-icon rt-ink-2" set:html={svg(TYPE_GLYPHS[type], 15)} />
                <span class="rt-row-label">{type}</span>
                <span class="rt-row-check" set:html={svg(I.check, 14, 2.2)} />
              </button>
            ))
          }
        </GlideMenu>
      </div>
    </div>

    <div class="rt-config-row">
      <span class="rt-config-label" id={`${uid}-tool-label`}>Tool</span>
      <button type="button" class="rt-config-btn" data-picker-open="tool" aria-haspopup="menu" aria-expanded="false" aria-labelledby={`${uid}-tool-label ${uid}-tool-value`}>
        <span class="rt-ink-2" data-tool-icon set:html={svg(TOOL_GLYPHS.user)} />
        <span id={`${uid}-tool-value`} data-tool-label>User input</span>
        <span class="rt-ink-3" set:html={svg(I.chevron, 12, 2.2)} />
      </button>
      <div class="rt-picker" data-picker="tool" hidden>
        <div class="rt-picker-label" aria-hidden="true">Model</div>
        <GlideMenu class="rt-picker-list" role="menu" aria-label="Model" arrowKeys>
          {
            models.map((m) => (
              <button type="button" class="rt-menu-row" data-menu-row role="menuitemradio" aria-checked="false" data-value={m}>
                <span class="rt-row-icon rt-accent" set:html={modelSvg()} />
                <span class="rt-row-label">{m}</span>
                <span class="rt-row-check" set:html={svg(I.check, 14, 2.2)} />
              </button>
            ))
          }
        </GlideMenu>
      </div>
    </div>

    <div class="rt-config-row">
      <span class="rt-config-label">Grounding</span>
      <span class="rt-config-group">
        <Switch size="sm" tone="accent" label="Grounding" />
        <button type="button" class="rt-help-btn" data-help aria-label="About grounding" aria-expanded="false" aria-controls={`${uid}-help`}>
          <Fragment set:html={svg(I.info, 13)} />
        </button>
      </span>
      <div class="rt-help" id={`${uid}-help`} hidden>Grounding lets the model verify generated values against connected sources.</div>
    </div>

    <div class="rt-config-row">
      <span class="rt-config-label" id={`${uid}-inputs-label`}>Inputs</span>
      <button type="button" class="rt-config-btn rt-config-btn--inputs" data-picker-open="inputs" aria-haspopup="menu" aria-expanded="false" aria-labelledby={`${uid}-inputs-label ${uid}-inputs-value`}>
        <span class="rt-input-chips" id={`${uid}-inputs-value`} data-inputs-value>Select inputs</span>
        <span class="rt-ink-3" set:html={svg(I.chevron, 12, 2.2)} />
      </button>
      <div class="rt-picker rt-picker--wide" data-picker="inputs" hidden>
        <div class="rt-picker-label" aria-hidden="true">Use values from</div>
        <GlideMenu class="rt-picker-list" role="menu" aria-label="Calculation inputs" arrowKeys>
          {
            inputOptions.map((input) => (
              <button type="button" class="rt-menu-row" data-menu-row role="menuitemcheckbox" aria-checked="false" data-value={input}>
                <span class="rt-mini-box" aria-hidden="true" set:html={svg(I.check, 11, 2.4)} />
                <span class="rt-row-label">{input}</span>
              </button>
            ))
          }
        </GlideMenu>
      </div>
    </div>

    <div class="rt-prompt" contenteditable="true" role="textbox" aria-multiline="true" spellcheck="true" aria-label="Calculation prompt" data-placeholder="Set a prompt (press @ to mention an input)" data-prompt></div>

    <button type="button" class="rt-go" data-calc>
      <Fragment set:html={svg(I.refresh, 14, 1.9)} />
      Go calculate
    </button>

    <GlideMenu class="rt-prop-actions" highlightInset="-6px">
      <button type="button" class="rt-action-row" data-menu-row data-pin aria-pressed="false">
        <span class="rt-ink-2" data-pin-icon set:html={svg(I.pin, 15)} />
        <span data-pin-label>Pin</span>
      </button>
      <button type="button" class="rt-action-row" data-menu-row data-more aria-expanded="false" aria-controls={`${uid}-more`}>
        <span class="rt-ink-2" set:html={svg(I.gear, 15)} />
        <span class="rt-grow">More settings</span>
        <span class="rt-ink-3 rt-more-chevron" set:html={svg(I.chevron, 12, 2.2)} />
      </button>
      <button type="button" class="rt-action-row" data-menu-row data-hide hidden>
        <span class="rt-ink-2" set:html={svg(I.eyeOff, 15)} />
        Hide from view
      </button>
    </GlideMenu>

    <div class="rt-more-settings" id={`${uid}-more`} hidden>
      <div class="rt-subhead">Behavior</div>
      <div class="rt-config-row"><span class="rt-config-label">Required value</span><Switch size="sm" tone="accent" label="Required value" /></div>
      <div class="rt-config-row"><span class="rt-config-label">Allow empty results</span><Switch size="sm" tone="accent" label="Allow empty results" checked /></div>
      <div class="rt-config-row"><span class="rt-config-label">Show confidence</span><Switch size="sm" tone="accent" label="Show confidence" /></div>
    </div>
  </div>

  {/* ── new property menu ─────────────────────────────── */}
  <div class="rt-pop rt-menu rt-menu--add" popover="manual" data-recpop data-pop="add">
    <div class="rt-menu-label" aria-hidden="true">New property</div>
    <GlideMenu class="rt-menu-list" role="menu" aria-label="New property" arrowKeys>
      {
        PROPERTY_TYPES.map((type) => (
          <button type="button" class="rt-menu-item" data-menu-row role="menuitem" data-add-type={type}>
            <span class="rt-ink-2" set:html={svg(TYPE_GLYPHS[type], 15)} />
            {type}
          </button>
        ))
      }
    </GlideMenu>
  </div>

  {/* ── table options menu ─────────────────────────────── */}
  <div class="rt-pop rt-menu rt-menu--options" popover="manual" data-recpop data-pop="options">
    <div class="rt-menu-label" aria-hidden="true">Table options</div>
    <GlideMenu class="rt-menu-list" role="menu" aria-label="Table options" arrowKeys>
      <button type="button" class="rt-menu-item" data-menu-row role="menuitem" data-option="add"><span class="rt-ink-2" set:html={svg(I.plus, 15, 2)} />Add property</button>
      <button type="button" class="rt-menu-item" data-menu-row role="menuitem" data-option="compact"><span class="rt-ink-2" set:html={svg(I.compact, 15)} />Compact columns</button>
      <button type="button" class="rt-menu-item" data-menu-row role="menuitem" data-option="reset"><span class="rt-ink-2" set:html={svg(I.reset, 15)} />Reset column widths</button>
      <div class="rt-menu-sep" role="separator"></div>
      <button type="button" class="rt-menu-item" data-menu-row role="menuitem" data-option="clear"><span class="rt-ink-2" set:html={svg(I.clear, 15)} />Clear selection</button>
    </GlideMenu>
  </div>
</div>

<script>
  import { initGlideMenu } from "../glide-menu/glide-menu";

  type ColumnKey = "company" | "categories" | "last" | "strength" | "links" | "ai";
  type SortKey = "name" | "last" | "strength";
  interface Prompt {
    before: string;
    chip?: string;
    after?: string;
  }
  interface ColumnConfig {
    label: string;
    type: string;
    tool: string;
    toolKind: "model" | "web" | "user";
    inputs: string[];
    prompt?: Prompt;
  }
  interface Config {
    uid: string;
    rows: { id: string; name: string; lastDays: number | null; last: string; rank: number; ai: string }[];
    sort: { key: SortKey; dir: 1 | -1 };
    columns: Record<ColumnKey, ColumnConfig>;
    typeGlyphs: Record<string, string>;
    toolGlyphs: Record<"model" | "web" | "user", string>;
    aiEnabled: boolean;
  }

  const DEFAULT_WIDTHS: Record<string, number> = { company: 270, categories: 275, last: 190, strength: 210, links: 175, ai: 240, actions: 100 };
  const COMPACT_WIDTHS: Record<string, number> = { company: 220, categories: 220, last: 155, strength: 180, links: 160, ai: 200 };
  const STEP_MS = 110;

  const svg = (inner: string, size = 14, sw = 1.8) =>
    `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
  const escapeHtml = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);

  function init(root: HTMLElement) {
    const configEl = root.querySelector<HTMLScriptElement>("script[data-vs-config]");
    if (!configEl) return;
    const cfg = JSON.parse(configEl.textContent || "{}") as Config;
    // JSON turns Infinity into null; null means "no contact" (sorts last).
    const rowsById = new Map(cfg.rows.map((r) => [r.id, { ...r, lastDays: r.lastDays ?? Number.POSITIVE_INFINITY }]));
    const columns = cfg.columns;

    const scroller = root.querySelector<HTMLElement>(".rt-scroll")!;
    const table = root.querySelector<HTMLTableElement>(".rt-table")!;
    const colgroup = table.querySelector("colgroup")!;
    const tbody = table.tBodies[0];
    const live = root.querySelector<HTMLElement>("[data-live]")!;
    const selectAll = root.querySelector<HTMLInputElement>("[data-select-all]")!;
    const propPop = root.querySelector<HTMLElement>('[data-pop="prop"]')!;
    const addPop = root.querySelector<HTMLElement>('[data-pop="add"]')!;
    const optionsPop = root.querySelector<HTMLElement>('[data-pop="options"]')!;
    const addBtn = root.querySelector<HTMLButtonElement>("[data-open-add]")!;
    const optionsBtn = root.querySelector<HTMLButtonElement>("[data-open-options]")!;
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)");

    const cleanups: Array<() => void> = [];
    const rowEls = () => Array.from(tbody.rows) as HTMLTableRowElement[];
    const colEl = (key: string) => colgroup.querySelector<HTMLTableColElement>(`col[data-col="${key}"]`);
    const cellsOf = (key: string) => Array.from(table.querySelectorAll<HTMLTableCellElement>(`th[data-col="${key}"], td[data-col="${key}"]`));

    /* ── selection ─────────────────────────────────────── */
    const selected = new Set<string>();
    const syncSelection = (announce = true) => {
      const all = rowEls();
      for (const tr of all) {
        const id = tr.dataset.id!;
        const on = selected.has(id);
        tr.classList.toggle("is-selected", on);
        const box = tr.querySelector<HTMLInputElement>("[data-select]");
        if (box) box.checked = on;
      }
      const count = all.filter((tr) => selected.has(tr.dataset.id!)).length;
      selectAll.checked = count > 0 && count === all.length;
      selectAll.indeterminate = count > 0 && count < all.length;
      if (announce) root.dispatchEvent(new CustomEvent("vs-records-selection", { bubbles: true, detail: { ids: [...selected] } }));
    };
    tbody.addEventListener("change", (event) => {
      const box = (event.target as Element).closest<HTMLInputElement>("[data-select]");
      if (!box) return;
      const id = box.dataset.select!;
      if (box.checked) selected.add(id);
      else selected.delete(id);
      syncSelection();
    });
    selectAll.addEventListener("change", () => {
      const ids = rowEls().map((tr) => tr.dataset.id!);
      if (selectAll.checked) ids.forEach((id) => selected.add(id));
      else ids.forEach((id) => selected.delete(id));
      syncSelection();
    });

    /* ── sorting ───────────────────────────────────────── */
    let sort = { ...cfg.sort };
    const sortHeaders: Record<SortKey, HTMLTableCellElement | null> = {
      name: table.querySelector('th[data-col="company"]'),
      last: table.querySelector('th[data-col="last"]'),
      strength: table.querySelector('th[data-col="strength"]'),
    };
    const applySort = () => {
      const trs = rowEls();
      trs.sort((a, b) => {
        const ra = rowsById.get(a.dataset.id!)!;
        const rb = rowsById.get(b.dataset.id!)!;
        const v =
          sort.key === "name"
            ? ra.name.localeCompare(rb.name)
            : sort.key === "last"
              ? ra.lastDays - rb.lastDays || ra.name.localeCompare(rb.name)
              : ra.rank - rb.rank;
        return (Number.isNaN(v) ? 0 : v) * sort.dir;
      });
      trs.forEach((tr, i) => {
        tbody.appendChild(tr);
        const num = tr.querySelector(".rt-rownum");
        if (num) num.textContent = String(i + 1);
      });
      for (const [key, th] of Object.entries(sortHeaders) as [SortKey, HTMLTableCellElement | null][]) {
        if (!th) continue;
        if (key === sort.key) th.setAttribute("aria-sort", sort.dir === 1 ? "ascending" : "descending");
        else th.removeAttribute("aria-sort");
        th.querySelector(".rt-sort")?.classList.toggle("is-visible", key === sort.key);
        th.querySelector(".rt-sort")?.classList.toggle("is-desc", key === sort.key && sort.dir === -1);
      }
    };
    root.querySelectorAll<HTMLButtonElement>("[data-sort]").forEach((btn) => {
      btn.addEventListener("click", () => {
        const key = btn.dataset.sort as SortKey;
        sort = sort.key === key ? { key, dir: (sort.dir * -1) as 1 | -1 } : { key, dir: 1 };
        closeAll();
        applySort();
        paintCalc();
        root.dispatchEvent(new CustomEvent("vs-records-sort", { bubbles: true, detail: { ...sort } }));
      });
    });

    /* ── tags: fit as many as the cell allows, then "+N" ── */
    const tagWidths = new WeakMap<HTMLElement, { tags: number[]; more: number }>();
    const measureTags = (box: HTMLElement) => {
      const tags = Array.from(box.querySelectorAll<HTMLElement>(".rt-tag"));
      const more = box.querySelector<HTMLElement>(".rt-more")!;
      tags.forEach((t) => (t.hidden = false));
      more.textContent = `+${tags.length}`;
      more.hidden = false;
      const m = { tags: tags.map((t) => t.offsetWidth), more: more.offsetWidth };
      tagWidths.set(box, m);
      return m;
    };
    const fitTags = (box: HTMLElement) => {
      if (!box.isConnected || box.getClientRects().length === 0) return;
      const m = tagWidths.get(box) ?? measureTags(box);
      if (!m.tags.length || m.tags[0] === 0) return;
      const available = box.clientWidth;
      let used = 0;
      let count = 0;
      for (let i = 0; i < m.tags.length; i += 1) {
        const next = used + (count > 0 ? 4 : 0) + m.tags[i];
        const hiddenAfter = m.tags.length - (i + 1);
        if (next + (hiddenAfter > 0 ? 4 + m.more : 0) > available) break;
        used = next;
        count += 1;
      }
      const tags = box.querySelectorAll<HTMLElement>(".rt-tag");
      tags.forEach((t, i) => (t.hidden = i >= count));
      const more = box.querySelector<HTMLElement>(".rt-more")!;
      const hidden = tags.length - count;
      more.hidden = hidden <= 0;
      more.textContent = `+${hidden}`;
    };
    const tagBoxes = Array.from(root.querySelectorAll<HTMLElement>(".rt-tags"));
    const tagObserver = new ResizeObserver((entries) => entries.forEach((e) => fitTags(e.target as HTMLElement)));
    tagBoxes.forEach((box) => tagObserver.observe(box));
    document.fonts?.ready.then(() => tagBoxes.forEach((box) => (tagWidths.delete(box), fitTags(box))));
    cleanups.push(() => tagObserver.disconnect());

    /* ── column widths: fill once, then lock every column explicitly ── */
    const widths: Record<string, number> = { ...DEFAULT_WIDTHS };
    let initialWidths: Record<string, number> = { ...DEFAULT_WIDTHS };
    let aiAdded = false;
    const applyWidths = () => {
      const keys = ["company", "categories", "last", "strength", "links", ...(aiAdded ? ["ai"] : []), "actions"];
      for (const key of keys) {
        const col = colEl(key);
        if (col) col.style.width = `${widths[key]}px`;
      }
      const total = keys.reduce((sum, k) => sum + widths[k], 0);
      table.style.width = `${total}px`;
      table.style.minWidth = `${total}px`;
      root.querySelectorAll<HTMLElement>("[data-resize]").forEach((h) => {
        const key = h.dataset.resize!;
        h.setAttribute("aria-valuenow", String(Math.round(widths[key])));
        h.setAttribute("aria-valuemin", h.dataset.min ?? "120");
        h.setAttribute("aria-valuemax", "800");
        h.setAttribute("aria-valuetext", `${Math.round(widths[key])} pixels`);
      });
    };
    const lockWidths = () => {
      if (scroller.getClientRects().length === 0) return false;
      const ths = Array.from(table.tHead!.rows[0].cells).filter((th) => !th.hidden);
      for (const th of ths) {
        const key = th.dataset.col!;
        widths[key] = th.getBoundingClientRect().width;
      }
      initialWidths = { ...widths };
      applyWidths();
      return true;
    };
    // Measure once visible (the gallery may render this inside a hidden variant panel).
    if (!lockWidths()) {
      const vis = new ResizeObserver(() => {
        if (lockWidths()) vis.disconnect();
      });
      vis.observe(scroller);
      cleanups.push(() => vis.disconnect());
    }

    const startResize = (handle: HTMLElement, event: PointerEvent) => {
      event.preventDefault();
      event.stopPropagation();
      closeAll();
      const key = handle.dataset.resize!;
      const min = Number(handle.dataset.min ?? 120);
      const startX = event.clientX;
      const startWidth = widths[key];
      const prevCursor = document.body.style.cursor;
      const prevSelect = document.body.style.userSelect;
      document.body.style.cursor = "col-resize";
      document.body.style.userSelect = "none";
      handle.classList.add("is-resizing");
      const move = (e: PointerEvent) => {
        widths[key] = Math.max(min, startWidth + e.clientX - startX);
        applyWidths();
      };
      const finish = () => {
        window.removeEventListener("pointermove", move);
        window.removeEventListener("pointerup", finish);
        window.removeEventListener("pointercancel", finish);
        document.body.style.cursor = prevCursor;
        document.body.style.userSelect = prevSelect;
        handle.classList.remove("is-resizing");
      };
      window.addEventListener("pointermove", move);
      window.addEventListener("pointerup", finish);
      window.addEventListener("pointercancel", finish);
    };
    root.querySelectorAll<HTMLElement>("[data-resize]").forEach((handle) => {
      handle.addEventListener("pointerdown", (e) => startResize(handle, e));
      handle.addEventListener("keydown", (e) => {
        if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
        e.preventDefault();
        const key = handle.dataset.resize!;
        const step = e.shiftKey ? 40 : 10;
        widths[key] = Math.min(800, Math.max(Number(handle.dataset.min ?? 120), widths[key] + (e.key === "ArrowRight" ? step : -step)));
        applyWidths();
      });
    });

    /* ── popovers (top layer when supported) ───────────── */
    const supportsPopover = typeof HTMLElement.prototype.showPopover === "function";
    let openPop: HTMLElement | null = null;
    let openTrigger: HTMLElement | null = null;
    let propCol: ColumnKey | null = null;
    let ignoreScroll = false;

    let placement: { pop: HTMLElement; anchorEl: HTMLElement; width: number; align: "left" | "right" } | null = null;
    const positionPop = (pop: HTMLElement, anchor: DOMRect, width: number, align: "left" | "right") => {
      const h = pop.offsetHeight;
      const left =
        align === "right"
          ? Math.max(8, Math.min(anchor.right - width, window.innerWidth - width - 8))
          : Math.max(8, Math.min(anchor.left, window.innerWidth - width - 8));
      let top = anchor.bottom + 6;
      if (top + h > window.innerHeight - 8 && anchor.top - h - 6 > 8) top = anchor.top - h - 6;
      pop.style.left = `${left}px`;
      pop.style.top = `${top}px`;
      pop.style.transformOrigin = align === "right" ? "top right" : "top left";
    };
    const showPop = (pop: HTMLElement, anchor: DOMRect, width: number, align: "left" | "right" = "left", anchorEl?: HTMLElement) => {
      pop.classList.add("is-open");
      if (supportsPopover) pop.showPopover();
      positionPop(pop, anchor, width, align);
      placement = anchorEl ? { pop, anchorEl, width, align } : null;
      pop.classList.remove("is-anim");
      void pop.offsetWidth;
      pop.classList.add("is-anim");
    };
    const hidePop = (pop: HTMLElement) => {
      pop.classList.remove("is-open", "is-anim");
      if (supportsPopover && pop.matches(":popover-open")) pop.hidePopover();
    };
    const closeAll = (restoreFocus = false) => {
      closePickers();
      setHelp(false);
      if (openPop) hidePop(openPop);
      openPop = null;
      placement = null;
      root.querySelectorAll('[data-prop][aria-expanded="true"], [data-open-add], [data-open-options]').forEach((el) => el.setAttribute("aria-expanded", "false"));
      table.querySelectorAll(".is-colsel").forEach((el) => el.classList.remove("is-colsel"));
      const trigger = openTrigger;
      openTrigger = null;
      propCol = null;
      if (restoreFocus && trigger?.isConnected) trigger.focus();
    };
    const focusFirst = (container: HTMLElement) => {
      const target = container.querySelector<HTMLElement>('[aria-checked="true"][data-menu-row]') ?? container.querySelector<HTMLElement>("[data-menu-row]:not([hidden])");
      target?.focus({ preventScroll: true });
    };

    const onDocPointerDown = (event: PointerEvent) => {
      if (!openPop) return;
      const target = event.target as Element;
      if (target.closest("[data-recpop]") || (openTrigger && openTrigger.contains(target))) return;
      closeAll();
    };
    const onDocKeyDown = (event: KeyboardEvent) => {
      if (event.key !== "Escape" || !openPop) return;
      if (closePickers(true)) return;
      event.preventDefault();
      closeAll(true);
    };
    document.addEventListener("pointerdown", onDocPointerDown);
    document.addEventListener("keydown", onDocKeyDown);
    // Re-anchor rather than close: on-screen keyboards fire resize while typing in the popover.
    const onWinResize = () => {
      if (placement?.anchorEl.isConnected) positionPop(placement.pop, placement.anchorEl.getBoundingClientRect(), placement.width, placement.align);
    };
    window.addEventListener("resize", onWinResize);
    window.addEventListener("scroll", onWinResize, { passive: true });
    cleanups.push(() => {
      document.removeEventListener("pointerdown", onDocPointerDown);
      document.removeEventListener("keydown", onDocKeyDown);
      window.removeEventListener("resize", onWinResize);
      window.removeEventListener("scroll", onWinResize);
    });
    scroller.addEventListener("scroll", () => {
      if (ignoreScroll) {
        ignoreScroll = false;
        return;
      }
      if (openPop) closeAll();
    });

    /* ── property popover ──────────────────────────────── */
    const overrides: Partial<Record<ColumnKey, Partial<ColumnConfig>>> = {};
    const pinned = new Set<ColumnKey>();
    const metaOf = (col: ColumnKey): ColumnConfig => ({ ...columns[col], ...overrides[col] });
    const q = <T extends HTMLElement = HTMLElement>(sel: string) => propPop.querySelector<T>(sel)!;
    const titleEl = q("[data-prop-title]");
    const typeIcon = q("[data-type-icon]");
    const typeLabel = q("[data-type-label]");
    const toolIcon = q("[data-tool-icon]");
    const toolLabel = q("[data-tool-label]");
    const inputsValue = q("[data-inputs-value]");
    const promptEl = q("[data-prompt]");
    const calcBtn = q<HTMLButtonElement>("[data-calc]");
    const pinBtn = q<HTMLButtonElement>("[data-pin]");
    const moreBtn = q<HTMLButtonElement>("[data-more]");
    const hideBtn = q<HTMLButtonElement>("[data-hide]");
    const moreSettings = q("#" + CSS.escape(`${cfg.uid}-more`));
    const helpBtn = q<HTMLButtonElement>("[data-help]");
    const helpEl = q("#" + CSS.escape(`${cfg.uid}-help`));

    const setHelp = (open: boolean) => {
      helpBtn.setAttribute("aria-expanded", String(open));
      helpEl.hidden = !open;
    };
    helpBtn.addEventListener("click", () => setHelp(Boolean(helpEl.hidden)));

    const toolSvg = (kind: ColumnConfig["toolKind"]) =>
      kind === "model"
        ? `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">${cfg.toolGlyphs.model}</svg>`
        : svg(cfg.toolGlyphs[kind]);

    const renderInputs = (list: string[]) => {
      if (!list.length) {
        inputsValue.textContent = "Select inputs";
        return;
      }
      inputsValue.innerHTML =
        list
          .slice(0, 2)
          .map((i) => `<span class="rt-input-chip">${escapeHtml(i)}</span>`)
          .join("") + (list.length > 2 ? `<span class="rt-input-more">+${list.length - 2}</span>` : "");
    };

    const renderProp = (col: ColumnKey) => {
      const meta = metaOf(col);
      titleEl.textContent = meta.label;
      typeIcon.innerHTML = svg(cfg.typeGlyphs[meta.type] ?? cfg.typeGlyphs.Text);
      typeLabel.textContent = meta.type;
      toolIcon.innerHTML = toolSvg(meta.toolKind);
      toolIcon.className = meta.toolKind === "model" ? "rt-accent" : "rt-ink-2";
      toolLabel.textContent = meta.tool;
      renderInputs(meta.inputs);
      propPop.querySelectorAll<HTMLElement>('[data-picker="inputs"] [data-value]').forEach((row) => {
        row.hidden = row.dataset.value === meta.label;
        row.setAttribute("aria-checked", String(meta.inputs.includes(row.dataset.value!)));
      });
      propPop.querySelectorAll<HTMLElement>('[data-picker="type"] [data-value]').forEach((row) => row.setAttribute("aria-checked", String(row.dataset.value === meta.type)));
      propPop.querySelectorAll<HTMLElement>('[data-picker="tool"] [data-value]').forEach((row) => row.setAttribute("aria-checked", String(row.dataset.value === meta.tool)));
      promptEl.setAttribute("aria-label", `${meta.label} calculation prompt`);
      if (meta.prompt) {
        promptEl.innerHTML =
          escapeHtml(meta.prompt.before) +
          (meta.prompt.chip ? `<span class="rt-prompt-chip" contenteditable="false">${escapeHtml(meta.prompt.chip)}</span>` : "") +
          escapeHtml(meta.prompt.after ?? "");
      } else promptEl.innerHTML = "";
      calcBtn.disabled = col === "company" || calc !== null;
      renderPin(col);
      hideBtn.hidden = col !== "ai";
      setMore(false);
      setHelp(false);
    };

    const renderPin = (col: ColumnKey) => {
      const isPinned = pinned.has(col);
      pinBtn.setAttribute("aria-pressed", String(isPinned));
      pinBtn.classList.toggle("is-pinned", isPinned);
      q("[data-pin-label]").textContent = isPinned ? "Unpin" : "Pin";
    };
    const setMore = (open: boolean) => {
      moreBtn.setAttribute("aria-expanded", String(open));
      moreBtn.classList.toggle("is-open", open);
      moreSettings.hidden = !open;
    };
    moreBtn.addEventListener("click", () => setMore(Boolean(moreSettings.hidden)));
    pinBtn.addEventListener("click", () => {
      if (!propCol) return;
      if (pinned.has(propCol)) pinned.delete(propCol);
      else pinned.add(propCol);
      renderPin(propCol);
    });

    const openProp = (col: ColumnKey, trigger: HTMLElement, viaKeyboard: boolean) => {
      const reopenSame = openPop === propPop && propCol === col;
      closeAll();
      if (reopenSame) return;
      const th = trigger.closest("th")!;
      propCol = col;
      openTrigger = trigger;
      openPop = propPop;
      trigger.setAttribute("aria-expanded", "true");
      cellsOf(col).forEach((c) => c.classList.add("is-colsel"));
      renderProp(col);
      showPop(propPop, th.getBoundingClientRect(), 320, "left", th);
      if (viaKeyboard) q<HTMLButtonElement>('[data-picker-open="type"]').focus({ preventScroll: true });
    };
    root.querySelectorAll<HTMLButtonElement>("[data-prop]").forEach((btn) => {
      btn.addEventListener("click", (e) => openProp(btn.dataset.prop as ColumnKey, btn, e.detail === 0));
    });

    const setAiIcon = (type: string) => {
      root.querySelector("[data-ai-icon]")!.innerHTML = svg(cfg.typeGlyphs[type] ?? cfg.typeGlyphs.Text, 15);
    };

    /* pickers inside the popover */
    let openPicker: HTMLElement | null = null;
    const closePickers = (restoreFocus = false) => {
      if (!openPicker) return false;
      const trigger = propPop.querySelector<HTMLElement>(`[data-picker-open="${openPicker.dataset.picker}"]`);
      openPicker.hidden = true;
      trigger?.setAttribute("aria-expanded", "false");
      openPicker = null;
      if (restoreFocus) trigger?.focus();
      return true;
    };
    propPop.querySelectorAll<HTMLButtonElement>("[data-picker-open]").forEach((btn) => {
      btn.addEventListener("click", (e) => {
        const name = btn.dataset.pickerOpen!;
        const picker = propPop.querySelector<HTMLElement>(`[data-picker="${name}"]`)!;
        const wasOpen = openPicker === picker;
        closePickers();
        setHelp(false);
        if (wasOpen) return;
        picker.hidden = false;
        openPicker = picker;
        btn.setAttribute("aria-expanded", "true");
        // Flip to the left when the right side has no room.
        const popRect = propPop.getBoundingClientRect();
        picker.classList.toggle("is-flipped", popRect.right + 20 + picker.offsetWidth > window.innerWidth - 8);
        picker.classList.remove("is-anim");
        void picker.offsetWidth;
        picker.classList.add("is-anim");
        if (e.detail === 0) focusFirst(picker);
      });
    });
    propPop.querySelector('[data-picker="type"]')!.addEventListener("click", (e) => {
      const row = (e.target as Element).closest<HTMLElement>("[data-value]");
      if (!row || !propCol) return;
      overrides[propCol] = { ...overrides[propCol], type: row.dataset.value! };
      if (propCol === "ai") setAiIcon(row.dataset.value!);
      renderProp(propCol);
      closePickers(true);
    });
    propPop.querySelector('[data-picker="tool"]')!.addEventListener("click", (e) => {
      const row = (e.target as Element).closest<HTMLElement>("[data-value]");
      if (!row || !propCol) return;
      overrides[propCol] = { ...overrides[propCol], tool: row.dataset.value!, toolKind: "model" };
      renderProp(propCol);
      closePickers(true);
    });
    propPop.querySelector('[data-picker="inputs"]')!.addEventListener("click", (e) => {
      const row = (e.target as Element).closest<HTMLElement>("[data-value]");
      if (!row || !propCol) return;
      const current = metaOf(propCol).inputs;
      const value = row.dataset.value!;
      const next = current.includes(value) ? current.filter((i) => i !== value) : [...current, value];
      overrides[propCol] = { ...overrides[propCol], inputs: next };
      row.setAttribute("aria-checked", String(next.includes(value)));
      renderInputs(next);
    });

    /* ── calculation: resolve one row per beat ─────────── */
    let calc: { col: ColumnKey; resolved: number; timer: number } | null = null;
    let aiDone = false;
    const aiFoot = root.querySelector<HTMLElement>("[data-ai-foot]")!;
    const calcMarkup = '<span class="rt-calc"><span class="rt-muted">Calculating…</span><span class="rt-pulse"></span></span>';
    const renderAiCell = (td: HTMLTableCellElement) => {
      const id = td.parentElement!.dataset.id!;
      const value = rowsById.get(id)?.ai ?? "";
      td.querySelector(".rt-val")!.innerHTML = aiDone && value ? escapeHtml(value) : '<span class="rt-muted">–</span>';
      td.title = aiDone ? value : "";
    };
    const paintCalc = () => {
      if (!calc) return;
      const { col, resolved } = calc;
      rowEls().forEach((tr, i) => {
        const td = tr.querySelector<HTMLTableCellElement>(`td[data-col="${col}"]`);
        if (!td) return;
        const pending = i >= resolved;
        if (pending && !td.classList.contains("is-calc")) {
          td.classList.add("is-calc");
          td.insertAdjacentHTML("beforeend", calcMarkup);
        } else if (!pending && td.classList.contains("is-calc")) {
          td.classList.remove("is-calc");
          td.querySelector(".rt-calc")?.remove();
          if (col === "ai") {
            const value = rowsById.get(tr.dataset.id!)?.ai ?? "";
            td.querySelector(".rt-val")!.innerHTML = value ? escapeHtml(value) : '<span class="rt-muted">–</span>';
            td.title = value;
          }
          if (col === "categories") {
            const box = td.querySelector<HTMLElement>(".rt-tags");
            if (box) fitTags(box);
          }
        }
      });
    };
    const finishCalc = () => {
      if (!calc) return;
      const { col } = calc;
      window.clearInterval(calc.timer);
      calc.resolved = Number.POSITIVE_INFINITY;
      paintCalc();
      calc = null;
      if (col === "ai") {
        aiDone = true;
        aiFoot.textContent = `${cfg.rows.length} filled`;
      }
      live.textContent = `${metaOf(col).label} calculated for ${cfg.rows.length} rows.`;
    };
    const tick = () => {
      if (!calc) return;
      calc.resolved += 1;
      if (calc.resolved > cfg.rows.length) finishCalc();
      else paintCalc();
    };
    const runCalc = (col: ColumnKey) => {
      if (calc || col === "company") return;
      calc = { col, resolved: 0, timer: 0 };
      live.textContent = `Calculating ${metaOf(col).label}…`;
      root.dispatchEvent(new CustomEvent("vs-records-calculate", { bubbles: true, detail: { column: col, label: metaOf(col).label } }));
      if (reduce.matches) {
        finishCalc();
        return;
      }
      paintCalc();
      calc.timer = window.setInterval(tick, STEP_MS);
    };
    cleanups.push(() => calc && window.clearInterval(calc.timer));
    calcBtn.addEventListener("click", () => {
      if (!propCol) return;
      const col = propCol;
      closeAll(true);
      runCalc(col);
    });

    /* ── the AI column ─────────────────────────────────── */
    const setAiVisible = (on: boolean) => {
      aiAdded = on;
      cellsOf("ai").forEach((cell) => (cell.hidden = !on));
      const existing = colEl("ai");
      if (on && !existing) {
        const col = document.createElement("col");
        col.dataset.col = "ai";
        colEl("actions")!.before(col);
      } else if (!on && existing) existing.remove();
      applyWidths();
    };
    const addAiColumn = (type: string) => {
      if (!cfg.aiEnabled) return;
      if (calc?.col === "ai") {
        window.clearInterval(calc.timer);
        calc = null;
      }
      aiDone = false;
      overrides.ai = { ...overrides.ai, type };
      setAiIcon(type);
      aiFoot.textContent = "–";
      table.querySelectorAll<HTMLTableCellElement>('td[data-col="ai"]').forEach((td) => {
        if (td.closest("tbody")) {
          td.classList.remove("is-calc");
          td.querySelector(".rt-calc")?.remove();
          renderAiCell(td);
        }
      });
      setAiVisible(true);
      root.dispatchEvent(new CustomEvent("vs-records-property", { bubbles: true, detail: { type } }));
      live.textContent = `Added ${metaOf("ai").label} column.`;
      // Reveal the new column, then open its configuration anchored to it.
      requestAnimationFrame(() => {
        ignoreScroll = true;
        scroller.scrollLeft = scroller.scrollWidth;
        const btn = root.querySelector<HTMLButtonElement>('[data-prop="ai"]')!;
        requestAnimationFrame(() => openProp("ai", btn, true));
      });
    };
    hideBtn.addEventListener("click", () => {
      if (calc?.col === "ai") {
        window.clearInterval(calc.timer);
        calc = null;
      }
      aiDone = false;
      closeAll();
      setAiVisible(false);
      live.textContent = `${metaOf("ai").label} column hidden.`;
      addBtn.focus();
    });

    /* + menu and options menu */
    const toggleMenu = (pop: HTMLElement, btn: HTMLButtonElement, align: "left" | "right", viaKeyboard: boolean, width: number) => {
      const wasOpen = openPop === pop;
      closeAll();
      if (wasOpen) return;
      openPop = pop;
      openTrigger = btn;
      btn.setAttribute("aria-expanded", "true");
      showPop(pop, btn.getBoundingClientRect(), width, align, btn);
      if (viaKeyboard) focusFirst(pop);
    };
    addBtn.addEventListener("click", (e) => toggleMenu(addPop, addBtn, "left", e.detail === 0, 260));
    optionsBtn.addEventListener("click", (e) => toggleMenu(optionsPop, optionsBtn, "right", e.detail === 0, 220));
    addPop.addEventListener("click", (e) => {
      const item = (e.target as Element).closest<HTMLElement>("[data-add-type]");
      if (!item) return;
      closeAll();
      addAiColumn(item.dataset.addType!);
    });
    optionsPop.addEventListener("click", (e) => {
      const item = (e.target as Element).closest<HTMLElement>("[data-option]");
      if (!item) return;
      const option = item.dataset.option;
      if (option === "add") {
        const anchor = optionsBtn.getBoundingClientRect();
        closeAll();
        openPop = addPop;
        openTrigger = addBtn;
        addBtn.setAttribute("aria-expanded", "true");
        showPop(addPop, anchor, 260, "right", optionsBtn);
        if (e.detail === 0) focusFirst(addPop);
        return;
      }
      if (option === "compact") Object.assign(widths, COMPACT_WIDTHS);
      if (option === "reset") Object.assign(widths, initialWidths);
      if (option === "clear") {
        selected.clear();
        syncSelection();
      }
      applyWidths();
      closeAll(true);
    });

    root.querySelector("[data-add-calc]")?.addEventListener("click", () => {
      root.dispatchEvent(new CustomEvent("vs-records-add-calculation", { bubbles: true }));
    });

    // Light up GlideMenus inside popovers that GlideMenu's own boot may have missed.
    root.querySelectorAll<HTMLElement>(".vs-glide-menu:not([data-vs-ready])").forEach((menu) => {
      menu.dataset.vsReady = "";
      cleanups.push(initGlideMenu(menu));
    });

    applySort();
    syncSelection(false);

    return () => {
      closeAll();
      cleanups.forEach((fn) => fn());
    };
  }

  const teardowns = new Set<() => void>();
  const boot = () =>
    document.querySelectorAll<HTMLElement>(".vs-records-table:not([data-vs-ready])").forEach((root) => {
      root.dataset.vsReady = "";
      const stop = init(root);
      if (stop) teardowns.add(stop);
    });
  boot();
  document.addEventListener("astro:page-load", boot);
  document.addEventListener("astro:before-swap", () => {
    teardowns.forEach((fn) => fn());
    teardowns.clear();
  });
</script>

<style>
  .vs-records-table {
    --rt-line: color-mix(in srgb, var(--vs-line) 78%, transparent);
    position: relative;
    width: 100%;
    min-width: 0;
    overflow: hidden;
    border: 1px solid var(--vs-line);
    border-radius: var(--vs-radius-card);
    background: var(--vs-surface);
    box-shadow: 0 1px 2px oklch(0 0 0 / 0.06);
    color: var(--vs-ink);
    font-family: var(--vs-font-sans);
    text-align: left;
  }
  .vs-records-table.is-fill {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-height: 0;
    height: 100%;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }
  .is-fill .rt-scroll {
    max-height: none;
    flex: 1;
  }

  .rt-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .rt-ink-2 {
    display: inline-flex;
    color: var(--vs-ink-2);
  }
  .rt-ink-3 {
    display: inline-flex;
    color: var(--vs-ink-3);
  }
  .rt-accent {
    display: inline-flex;
    color: var(--vs-accent-ink);
  }
  .rt-muted {
    color: var(--vs-ink-3);
  }
  .rt-trunc {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  button {
    font: inherit;
    color: inherit;
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
  }
  button:focus-visible,
  a:focus-visible,
  [tabindex]:focus-visible {
    outline: none;
    box-shadow: var(--vs-focus-ring);
  }

  /* ── scroll viewport + grid ─────────────────────────── */
  .rt-scroll {
    max-height: var(--rt-max-h, 438px);
    overflow: auto;
    overscroll-behavior: none;
    scrollbar-color: var(--vs-line-strong) transparent;
    scrollbar-gutter: stable;
  }
  .rt-scroll:focus-visible {
    box-shadow: inset 0 0 0 2px var(--vs-accent);
  }
  .rt-table {
    width: 100%;
    min-width: 1220px;
    border-collapse: separate;
    border-spacing: 0;
    color: var(--vs-ink);
    font-size: 13px;
    font-weight: 500;
    table-layout: fixed;
  }
  .rt-table th,
  .rt-table td {
    border-right: 1px solid var(--rt-line);
    border-bottom: 1px solid var(--rt-line);
    text-align: left;
    vertical-align: middle;
  }
  .rt-table tr > :last-child {
    border-right: 0;
  }
  .rt-table thead th {
    position: sticky;
    z-index: 5;
    top: 0;
    height: 35px;
    padding: 0;
    background: var(--vs-surface);
    color: var(--vs-ink-2);
    font-size: 12.5px;
    font-weight: 500;
  }
  .rt-table thead th.rt-sticky {
    z-index: 7;
  }

  /* header: property button + sort arrow */
  .rt-head {
    display: flex;
    height: 35px;
    align-items: center;
    transition: background-color 120ms ease-out, color 120ms ease-out;
  }
  .rt-head--company {
    gap: 8px;
    padding-left: 6px;
  }
  .rt-th:hover .rt-head {
    background: var(--vs-hover);
  }
  .rt-head-btn {
    display: flex;
    min-width: 0;
    height: 35px;
    flex: 1;
    align-items: center;
    gap: 8px;
    padding: 0 12px;
    color: var(--vs-ink-2);
    text-align: left;
    transition: color 120ms ease-out;
  }
  .rt-head--company .rt-head-btn {
    padding-left: 0;
  }
  .rt-th:hover .rt-head-btn {
    color: var(--vs-ink);
  }
  .rt-head-btn:focus-visible {
    box-shadow: inset 0 0 0 2px var(--vs-accent);
  }
  .rt-head-icon {
    display: inline-flex;
    flex-shrink: 0;
    color: var(--vs-ink-3);
  }
  .rt-sort {
    display: inline-flex;
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    margin-right: 8px;
    margin-left: -8px;
    border-radius: var(--vs-radius-chip);
    color: var(--vs-ink-2);
    opacity: 0;
    transition: opacity 120ms ease-out, transform 160ms var(--vs-ease-out), background-color 120ms ease-out;
  }
  .rt-sort:hover {
    background: var(--vs-hover-2);
    color: var(--vs-ink);
  }
  .rt-th:hover .rt-sort,
  .rt-sort:focus-visible,
  .rt-sort.is-visible {
    opacity: 1;
  }
  .rt-sort.is-desc {
    transform: rotate(180deg);
  }

  .rt-resize {
    position: absolute;
    z-index: 12;
    top: 0;
    right: -6px;
    width: 12px;
    height: 100%;
    cursor: col-resize;
    touch-action: none;
  }
  .rt-resize::after {
    position: absolute;
    top: 7px;
    bottom: 7px;
    left: 5px;
    width: 2px;
    border-radius: 99px;
    background: var(--vs-accent);
    content: "";
    opacity: 0;
    transform: scaleY(0.65);
    transition: opacity 120ms ease-out, transform 150ms var(--vs-ease-out);
  }
  .rt-resize:hover::after,
  .rt-resize:focus-visible::after,
  .rt-resize.is-resizing::after {
    opacity: 1;
    transform: scaleY(1);
  }
  .rt-resize:focus-visible {
    box-shadow: none;
  }

  .rt-actions {
    display: flex;
    height: 35px;
    align-items: center;
    gap: 4px;
    padding: 0 8px;
  }
  .rt-icon-btn {
    display: flex;
    width: 28px;
    height: 28px;
    align-items: center;
    justify-content: center;
    border-radius: 7px;
    color: var(--vs-ink-2);
    transition: background-color 100ms ease-out, color 100ms ease-out;
  }
  .rt-icon-btn--quiet {
    color: var(--vs-ink-3);
  }
  .rt-icon-btn:hover,
  .rt-icon-btn[aria-expanded="true"] {
    background: var(--vs-hover);
    color: var(--vs-ink);
  }

  /* ── checkbox ───────────────────────────────────────── */
  .rt-checkbox {
    position: relative;
    display: inline-flex;
    width: 24px;
    height: 24px;
    flex: 0 0 24px;
    align-items: center;
    justify-content: center;
    border-radius: var(--vs-radius-chip);
    cursor: pointer;
  }
  .rt-checkbox input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
  }
  .rt-box {
    display: inline-flex;
    width: 18px;
    height: 18px;
    align-items: center;
    justify-content: center;
    border: 1px solid color-mix(in oklab, var(--vs-ink-3) 55%, var(--vs-surface));
    border-radius: 6px;
    background: color-mix(in oklab, var(--vs-ink) 5%, var(--vs-surface));
    color: var(--vs-on-accent);
    pointer-events: none;
    transition: border-color 140ms ease-out, background-color 140ms ease-out, transform 140ms var(--vs-ease-out);
  }
  .rt-box :global(svg),
  .rt-dash {
    display: none;
  }
  .rt-dash {
    width: 8px;
    height: 1.5px;
    border-radius: 99px;
    background: var(--vs-on-accent);
  }
  .rt-checkbox:hover .rt-box {
    border-color: color-mix(in oklab, var(--vs-ink-3) 80%, var(--vs-surface));
    background: var(--vs-hover);
  }
  .rt-checkbox:active .rt-box {
    transform: scale(0.96);
  }
  .rt-checkbox input:focus-visible + .rt-box {
    box-shadow: var(--vs-focus-ring);
  }
  .rt-checkbox input:checked + .rt-box,
  .rt-checkbox input:indeterminate + .rt-box {
    border-color: var(--vs-accent);
    background: var(--vs-accent);
  }
  .rt-checkbox input:checked + .rt-box :global(svg) {
    display: block;
  }
  .rt-checkbox input:indeterminate + .rt-box .rt-dash {
    display: block;
  }

  /* ── cells ──────────────────────────────────────────── */
  .rt-cell {
    height: 35px;
    padding: 0 8px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .rt-sticky {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--vs-surface);
    box-shadow: 5px 0 8px -10px oklch(0 0 0 / 0.4);
  }
  .rt-company {
    padding-left: 6px;
  }
  .rt-company-inner {
    display: flex;
    min-width: 0;
    align-items: center;
  }
  .rt-row > .rt-cell {
    transition: background-color 120ms ease-out, color 120ms ease-out;
  }
  .rt-row:hover > .rt-cell {
    background: var(--vs-hover);
  }
  .rt-row.is-selected > .rt-cell {
    background: color-mix(in srgb, var(--vs-accent) 7%, var(--vs-surface));
  }

  /* gutter: the row number gives way to the checkbox on hover, focus or selection */
  .rt-gutter {
    position: relative;
    display: inline-flex;
    width: 24px;
    height: 24px;
    margin-right: 4px;
  }
  .rt-rownum {
    display: inline-flex;
    width: 24px;
    height: 24px;
    align-items: center;
    justify-content: center;
    color: var(--vs-ink-3);
    font-size: 11.5px;
    font-variant-numeric: tabular-nums;
  }
  .rt-gutter .rt-checkbox {
    position: absolute;
    inset: 0;
    opacity: 0;
  }
  .rt-row:hover .rt-gutter .rt-checkbox,
  .rt-row.is-selected .rt-gutter .rt-checkbox,
  .rt-gutter:focus-within .rt-checkbox {
    opacity: 1;
  }
  .rt-row:hover .rt-rownum,
  .rt-row.is-selected .rt-rownum,
  .rt-gutter:focus-within .rt-rownum {
    visibility: hidden;
  }

  .rt-mark {
    display: inline-flex;
    width: 20px;
    height: 20px;
    margin-right: 4px;
    align-items: center;
    justify-content: center;
    border-radius: 6px;
    color: var(--vs-ink-2);
    background: var(--vs-field);
    font-size: 10px;
    font-weight: 500;
  }
  .rt-name {
    min-width: 0;
    overflow: hidden;
    color: var(--vs-ink);
    font-size: 13px;
    font-weight: 500;
    text-decoration: none;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .rt-name.has-link:hover,
  .rt-name.has-link:focus-visible {
    color: var(--vs-accent-ink);
    text-decoration: underline;
    text-underline-offset: 3px;
  }
  .rt-val {
    display: contents;
  }

  /* tags */
  .rt-tags {
    position: relative;
    display: flex;
    width: 100%;
    min-width: 0;
    align-items: center;
    gap: 4px;
    overflow: hidden;
  }
  .rt-tag {
    display: inline-flex;
    height: 23px;
    max-width: 115px;
    flex-shrink: 0;
    align-items: center;
    overflow: hidden;
    border: 1px solid color-mix(in srgb, var(--rt-tag) 34%, var(--vs-surface));
    border-radius: 8px;
    padding: 0 7px;
    /* Text leans on --vs-ink, which flips with the mode: AA (4.5:1+) on the tint in paper and ink. */
    color: color-mix(in srgb, var(--rt-tag) 45%, var(--vs-ink));
    background: color-mix(in srgb, var(--rt-tag) 22%, var(--vs-surface));
    font-size: 13px;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .rt-tag[hidden],
  .rt-more[hidden] {
    display: none;
  }
  .rt-more {
    display: inline-flex;
    height: 23px;
    flex-shrink: 0;
    align-items: center;
    border: 1px solid var(--vs-line-strong);
    border-radius: 6px;
    padding: 0 7px;
    color: var(--vs-ink-3);
    background: var(--vs-inset);
    font-size: 11px;
    font-weight: 500;
  }

  .rt-strength {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--vs-ink-2);
  }
  .rt-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    flex: 0 0 8px;
    border-radius: 50%;
  }
  .rt-link {
    display: inline-flex;
    max-width: 100%;
    align-items: center;
    gap: 5px;
    overflow: hidden;
    color: var(--vs-accent-ink);
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
    text-underline-offset: 3px;
    transition: color 120ms ease-out, text-decoration-color 120ms ease-out;
  }
  .rt-link-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .rt-link :global(svg) {
    flex-shrink: 0;
  }
  .rt-link:hover,
  .rt-link:focus-visible {
    color: var(--vs-ink);
    text-decoration-color: currentColor;
  }

  /* selected column: the property being configured */
  .rt-table th:global(.is-colsel) {
    background: color-mix(in srgb, var(--vs-accent) 8%, var(--vs-surface));
    box-shadow: inset 0 2px 0 var(--vs-accent);
  }
  .rt-table th:global(.is-colsel) .rt-head-btn,
  .rt-table th:global(.is-colsel) .rt-head-icon {
    color: var(--vs-accent-ink);
  }
  .rt-table td:global(.is-colsel) {
    background: color-mix(in srgb, var(--vs-accent) 4%, var(--vs-surface));
  }
  .rt-row:hover > td:global(.is-colsel) {
    background: color-mix(in srgb, var(--vs-accent) 8%, var(--vs-surface));
  }
  .rt-row.is-selected > td:global(.is-colsel) {
    background: color-mix(in srgb, var(--vs-accent) 10%, var(--vs-surface));
  }

  /* calculating state */
  .rt-cell:global(.is-calc) > .rt-val {
    display: none;
  }
  .vs-records-table :global(.rt-calc) {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
  }
  .vs-records-table :global(.rt-pulse) {
    width: 8px;
    height: 8px;
    flex: 0 0 8px;
    border-radius: 50%;
    background: var(--vs-accent);
    animation: vs-pulse 1.1s ease-in-out infinite;
  }

  /* footer calculations */
  .rt-table tfoot td {
    position: sticky;
    z-index: 4;
    bottom: 0;
    height: 35px;
    background: var(--vs-inset);
    color: var(--vs-ink-2);
    font-size: 14px;
    font-weight: 500;
    line-height: 1;
  }
  .rt-table tfoot .rt-sticky {
    z-index: 6;
    background: var(--vs-inset);
  }
  .rt-foot {
    display: inline-flex;
    height: 35px;
    align-items: center;
    vertical-align: middle;
  }
  .rt-foot-label {
    color: var(--vs-ink-2);
  }
  .rt-foot-num {
    margin-right: 3px;
    color: var(--vs-ink);
    font-variant-numeric: tabular-nums;
  }
  .rt-average {
    gap: 7px;
    color: var(--vs-ink-2);
  }
  .rt-add-calc {
    display: inline-flex;
    height: 35px;
    align-items: center;
    gap: 6px;
    color: var(--vs-ink-3);
    font-size: 14px;
    font-weight: 500;
    line-height: 1;
    transition: color 120ms ease-out, transform 140ms var(--vs-ease-out);
  }
  .rt-add-calc:hover {
    color: var(--vs-ink);
  }
  .rt-add-calc:active {
    transform: scale(0.96);
  }

  /* ── popovers ───────────────────────────────────────── */
  .rt-pop {
    position: fixed;
    inset: auto;
    z-index: 50;
    display: none;
    margin: 0;
    border: 0;
    overflow: visible;
    background: var(--vs-surface);
    color: var(--vs-ink);
    box-shadow: var(--vs-shadow-overlay);
    font-family: var(--vs-font-sans);
    font-size: 13px;
    text-align: left;
  }
  .rt-pop.is-open {
    display: block;
  }
  .rt-pop.is-anim {
    animation: vs-pop-in 160ms var(--vs-ease-out) both;
  }
  .rt-prop {
    width: 320px;
    padding: 12px 12px 6px;
    border-radius: 14px;
  }
  .rt-menu {
    padding: 6px;
    border-radius: 14px;
  }
  .rt-menu--add {
    width: 260px;
  }
  .rt-menu--options {
    width: 220px;
  }
  .rt-prop-title {
    padding-bottom: 8px;
    font-size: 13.5px;
    font-weight: 500;
    color: var(--vs-ink);
  }
  .rt-config-row {
    position: relative;
    display: flex;
    height: 32px;
    align-items: center;
    justify-content: space-between;
  }
  .rt-config-label {
    font-size: 13px;
    color: var(--vs-ink-3);
  }
  .rt-config-group {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .rt-config-btn {
    display: flex;
    max-width: 220px;
    align-items: center;
    gap: 6px;
    padding: 4px 6px;
    border-radius: 6px;
    font-size: 13px;
    font-weight: 500;
    color: var(--vs-ink);
    transition: background-color 100ms ease-out;
  }
  .rt-config-btn:hover,
  .rt-config-btn[aria-expanded="true"] {
    background: var(--vs-hover);
  }
  .rt-config-btn--inputs {
    font-weight: 400;
    color: var(--vs-ink-2);
  }
  .rt-input-chips {
    display: flex;
    min-width: 0;
    align-items: center;
    gap: 4px;
  }
  .vs-records-table :global(.rt-input-chip),
  .vs-records-table :global(.rt-prompt-chip) {
    max-width: 92px;
    overflow: hidden;
    border-radius: 5px;
    padding: 2px 6px;
    background: var(--vs-accent-tint);
    color: var(--vs-accent-ink);
    font-size: 12px;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .vs-records-table :global(.rt-prompt-chip) {
    max-width: none;
  }
  .vs-records-table :global(.rt-input-more) {
    font-size: 11px;
    font-weight: 500;
    color: var(--vs-ink-3);
  }
  .rt-help-btn {
    display: flex;
    width: 24px;
    height: 24px;
    align-items: center;
    justify-content: center;
    border-radius: 6px;
    color: var(--vs-ink-3);
    transition: background-color 100ms ease-out, color 100ms ease-out;
  }
  .rt-help-btn:hover {
    background: var(--vs-hover);
    color: var(--vs-ink);
  }
  .rt-help {
    position: absolute;
    z-index: 30;
    top: 30px;
    right: 0;
    width: 230px;
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--vs-tooltip-bg);
    color: var(--vs-tooltip-fg);
    box-shadow: var(--vs-shadow-overlay);
    font-size: 12px;
    line-height: 1.6;
  }

  /* side pickers */
  .rt-picker {
    position: absolute;
    z-index: 30;
    top: 0;
    left: calc(100% + 20px);
    width: 210px;
    padding: 6px;
    border-radius: 12px;
    background: var(--vs-surface);
    box-shadow: var(--vs-shadow-overlay);
    transform-origin: top left;
  }
  .rt-picker--wide {
    width: 220px;
  }
  .rt-picker.is-flipped {
    right: calc(100% + 20px);
    left: auto;
    transform-origin: top right;
  }
  .rt-picker:global(.is-anim) {
    animation: vs-pop-in 140ms var(--vs-ease-out) both;
  }
  .rt-picker[hidden],
  .rt-help[hidden],
  .rt-more-settings[hidden] {
    display: none;
  }
  .rt-picker-label,
  .rt-menu-label {
    padding: 2px 8px 4px;
    font-size: 11.5px;
    font-weight: 500;
    color: var(--vs-ink-3);
  }
  .rt-menu-label {
    padding-top: 4px;
    font-size: 12px;
  }
  .rt-picker-list,
  .rt-menu-list {
    display: flex;
    flex-direction: column;
    gap: 1px;
  }
  .rt-menu-row {
    display: flex;
    width: 100%;
    height: 32px;
    align-items: center;
    gap: 6px;
    padding: 0 6px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 500;
    color: var(--vs-ink);
    text-align: left;
  }
  .rt-menu-row[hidden] {
    display: none;
  }
  .rt-row-icon {
    display: flex;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
  }
  .rt-row-label {
    min-width: 0;
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .rt-row-check {
    display: inline-flex;
    visibility: hidden;
    color: var(--vs-ink);
  }
  .rt-menu-row[aria-checked="true"] .rt-row-check {
    visibility: visible;
  }
  .rt-mini-box {
    display: flex;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--vs-line-strong);
    border-radius: 5px;
    color: transparent;
  }
  .rt-menu-row[aria-checked="true"] .rt-mini-box {
    border-color: var(--vs-accent);
    background: var(--vs-accent);
    color: var(--vs-on-accent);
  }
  .rt-menu-item {
    display: flex;
    width: 100%;
    height: 36px;
    align-items: center;
    gap: 10px;
    padding: 0 8px;
    border-radius: 8px;
    font-size: 13px;
    color: var(--vs-ink);
    text-align: left;
  }
  .rt-menu-sep {
    height: 1px;
    margin: 4px 0;
    background: var(--vs-line);
  }
  .rt-menu-row:focus-visible,
  .rt-menu-item:focus-visible,
  .rt-action-row:focus-visible {
    box-shadow: none;
    outline: none;
  }

  /* prompt */
  .rt-prompt {
    min-height: 88px;
    margin-top: 8px;
    padding: 12px;
    border-radius: 10px;
    background: var(--vs-inset);
    box-shadow: var(--vs-shadow-hairline);
    color: var(--vs-ink);
    font-size: 13px;
    line-height: 1.625;
    cursor: text;
    outline: none;
    transition: box-shadow 150ms ease-out;
  }
  .rt-prompt:focus {
    box-shadow: 0 0 0 2px var(--vs-accent);
  }
  .rt-prompt:empty::before {
    content: attr(data-placeholder);
    color: var(--vs-ink-3);
  }

  .rt-go {
    display: flex;
    width: 100%;
    height: 36px;
    margin-top: 10px;
    align-items: center;
    justify-content: center;
    gap: 8px;
    border-radius: 9px;
    color: var(--vs-ink);
    box-shadow: var(--vs-shadow-btn);
    font-size: 12.5px;
    font-weight: 500;
    transition: background-color 150ms ease-out, transform 150ms ease-out;
  }
  .rt-go:hover {
    background: var(--vs-hover);
  }
  .rt-go:active {
    transform: scale(0.98);
  }
  .rt-go:disabled {
    opacity: 0.6;
    cursor: default;
  }

  .rt-prop-actions {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-top: 12px;
    padding-top: 8px;
    border-top: 1px solid var(--vs-line);
  }
  .rt-action-row {
    display: flex;
    height: 32px;
    align-items: center;
    gap: 10px;
    margin: 0 -6px;
    padding: 0 6px;
    border-radius: 8px;
    color: var(--vs-ink);
    font-size: 13px;
    line-height: 1;
    text-align: left;
    transition: transform 150ms ease-out;
  }
  .rt-action-row[hidden] {
    display: none;
  }
  .rt-action-row:active {
    transform: scale(0.96);
  }
  .rt-action-row.is-pinned > :first-child {
    color: var(--vs-accent-ink);
  }
  .rt-grow {
    flex: 1;
  }
  .rt-more-chevron {
    transition: transform 150ms ease-out;
  }
  .rt-action-row.is-open .rt-more-chevron {
    transform: rotate(90deg);
  }
  .rt-more-settings {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--vs-line);
    animation: vs-fade-up 160ms var(--vs-ease-out) both;
  }
  .rt-subhead {
    padding-bottom: 4px;
    font-size: 11.5px;
    font-weight: 500;
    color: var(--vs-ink-3);
  }

  /* touch: :hover sticks after a tap, so neutralize row/header hovers there */
  @media (hover: none) {
    .rt-row:hover > .rt-cell {
      background: transparent;
    }
    .rt-row:hover > td:global(.is-colsel) {
      background: color-mix(in srgb, var(--vs-accent) 4%, var(--vs-surface));
    }
    .rt-th:hover .rt-head {
      background: transparent;
    }
    .rt-th:hover .rt-head-btn {
      color: var(--vs-ink-2);
    }
    .rt-gutter .rt-checkbox {
      opacity: 1;
    }
    .rt-rownum {
      visibility: hidden;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .rt-pop.is-anim,
    .rt-picker:global(.is-anim),
    .rt-more-settings {
      animation: none;
    }
    .vs-records-table :global(.rt-pulse) {
      animation: none;
      opacity: 1;
    }
    .rt-sort,
    .rt-row > .rt-cell,
    .rt-box {
      transition: none;
    }
  }

  @media (forced-colors: active) {
    .rt-pop {
      outline: 1px solid CanvasText;
    }
    .rt-box {
      forced-color-adjust: none;
    }
  }
</style>
```
