{/* Column headers. The empty first cell is the row-label gutter — it
draws no leading edge, so the header lines up with the body's
first hairline rather than boxing the gutter in. */}
{cols.map((col, i) => (
))}
{rows.map((row, r) => (
{/* No `border-r` any more: the first cell of the row draws that
line as its own `border-inline-start`, which is what keeps it
continuous with the hairlines between the cells beside it. */}
{cols.map((col, i) => {
const where = cellTitle?.(row, col);
return (
{/* One cell throwing must cost one cell. Without this a
component broken at a single axis value takes the whole
grid — and the page you were reading it on — with it. */}
{cells[r][i]}
{/* Revealed on hover so the grid still reads as a grid, but
focusable all the same — a keyboard reaches every cell.
Icon-only because a cell has room for a 32px square and
not for the word; the label survives as its accessible
name. */}
textOf(row, col, r, i)}
title={where}
iconOnly
className="absolute top-1 right-1 size-6 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
/>
);
})}
))}