diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx
new file mode 100644
index 0000000..cea585f
--- /dev/null
+++ b/src/components/CopyButton.tsx
@@ -0,0 +1,37 @@
+import { useState, useCallback } from "react";
+import { VARIANT_CLASSES } from "./badge-utils";
+
+// Copy-to-clipboard pill, styled to match the analyzer's "Copy Link" button: flips to a green
+// "Copied" state for 1.5s after a click. aria-label defaults to the visible label.
+export function CopyButton({
+ value,
+ label = "Copy",
+ copiedLabel = "Copied",
+ ariaLabel,
+ className,
+}: {
+ value: string;
+ label?: string;
+ copiedLabel?: string;
+ ariaLabel?: string;
+ className?: string;
+}) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = useCallback(() => {
+ navigator.clipboard.writeText(value);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ }, [value]);
+
+ return (
+
+ );
+}
diff --git a/src/features/nodes/NodeDetailPanel.tsx b/src/features/nodes/NodeDetailPanel.tsx
index 7dba588..57a62e0 100644
--- a/src/features/nodes/NodeDetailPanel.tsx
+++ b/src/features/nodes/NodeDetailPanel.tsx
@@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { getNode, getNodeObservations, getNodeNeighbors } from "../../api/client";
import { Badge } from "../../components/Badge";
import { DetailPanel, Section, Field } from "../../components/DetailPanel";
+import { CopyButton } from "../../components/CopyButton";
import { IataChip } from "../../components/IataChip";
import { formatHex, formatSnr, snrLevel, formatRadio, SIGNAL_LEVEL_CLASSES } from "../../lib/formatters";
import { Timestamp } from "../../components/Timestamp";
@@ -116,8 +117,11 @@ export function NodeDetailPanel({ nodeId, onClose, onViewObserver, onViewNode, o