, so they contribute nothing to the
+ * width distribution, and the global `.data-table td { max-width: 0 }` above
+ * zeroes their max-content contribution too — the auto table algorithm then
+ * hands them whatever is left over, which is routinely narrower than the
+ * button they contain, and `overflow: hidden` silently clips it. Measured on
+ * the node Neighbours table: 40px of column for a 53px button at a 1440px
+ * viewport, 12px in the 420px split panel — clipped on every row at every
+ * desktop width below ~2000px.
+ *
+ * `width: 1%` is the shrink-to-content idiom for auto-layout tables: the
+ * column takes its min-content width and the data columns absorb the rest.
+ * `max-width: none` opts back out of the zeroing; `overflow: visible` means a
+ * future miscalculation shows as a visible overhang rather than a silent crop.
+ */
+.data-table th.col-action,
+.data-table td.col-action {
+ width: 1%;
+ max-width: none;
+ overflow: visible;
+ white-space: nowrap;
+ text-align: right;
+}
+
/* #1415 / #1458 / #1461 — mobile packets UX overhaul.
* NOTE: hiding the first dt/dd pair below assumes Payload Type is the
* first
in .detail-meta (per packets.js renderDetail). Keep that
diff --git a/test-neighbor-map-btn-clip-e2e.js b/test-neighbor-map-btn-clip-e2e.js
new file mode 100644
index 00000000..59d04cee
--- /dev/null
+++ b/test-neighbor-map-btn-clip-e2e.js
@@ -0,0 +1,210 @@
+#!/usr/bin/env node
+/* Trailing action-column clip — node Neighbours "Map" button, analytics
+ * "View on map" button.
+ *
+ * Symptom (operator report, 2026-08-30): on the node detail page the
+ * Neighbours table's "Map" button rendered as a cropped "◎ M" on every row.
+ * Unaffected by browser, browser version, zoom level or incognito — because
+ * it is a pure layout bug, not a rendering one.
+ *
+ * Root cause: the action column carries an empty
, so it contributes
+ * nothing to the table's width distribution, and the global
+ * .data-table td { max-width: 0; overflow: hidden; }
+ * zeroes its max-content contribution as well. The auto table algorithm
+ * therefore hands that column only whatever space is left over, which is
+ * narrower than the button it holds, and `overflow: hidden` crops the
+ * remainder without any scrollbar to reveal it. Measured before the fix:
+ *
+ * viewport column button rows clipped
+ * 1024px 28px 53px all
+ * 1440px 40px 53px all
+ * 1920px 54px 53px all (padding pushes it over)
+ * 420px split panel: 12px column — 47 of 53px cropped
+ *
+ * Fix: `.data-table th.col-action, .data-table td.col-action` opts the
+ * column out of the zeroing (`max-width: none`), sizes it to its content
+ * (`width: 1%`) and stops it cropping (`overflow: visible`); nodes.js and
+ * analytics.js tag their action
/
with it.
+ *
+ * This test does not need a running server: the bug lives entirely in
+ * public/style.css plus the markup contract, so it renders the real
+ * stylesheet against the real emitted row markup via setContent().
+ *
+ * Mutation guard: dropping the `.col-action` class from either the
or
+ * the
, or removing `max-width: none` / `width: 1%` from the rule, must
+ * make the geometry assertions below fail.
+ */
+'use strict';
+
+const fs = require('node:fs');
+const path = require('node:path');
+const assert = require('node:assert');
+const { chromium } = require('playwright');
+
+const CSS = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
+const NODES_JS = fs.readFileSync(path.join(__dirname, 'public', 'nodes.js'), 'utf8');
+const ANALYTICS_JS = fs.readFileSync(path.join(__dirname, 'public', 'analytics.js'), 'utf8');
+
+const HEIGHT = 900;
+// The bug's window: below ~640px the table finally overflows and the
+// container scrolls (so nothing is cropped); above ~2000px the leftover
+// space happens to exceed the button. Everything between was broken.
+const WIDTHS = [1024, 1280, 1366, 1440, 1600, 1920];
+
+// A neighbour row exactly as renderNeighborRows() emits it (nodes.js).
+function neighborRow(name, role, score, count, seen, dist) {
+ return '