diff --git a/CLAUDE.md b/CLAUDE.md
index 67de04b..a5926ea 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -756,12 +756,27 @@ far the largest file.
average* over `time_stride` samples, so a number between two stored samples is a second
estimate laid over the first that nothing on screen or in an export holds; and past the sample
spacing the polyline is a line the renderer drew, so a point on it reports the drawing. The
- change is in `deps/plotter` and unconditional — no setting, because the library had no business
- offering a readout the data does not contain. `CrosshairRow::marker` is placed from the same y,
- so the dot lands on the quoted sample and **sits beside the polyline at a deep zoom, by
- design**: one y feeding both is what keeps the mark and the number from disagreeing. The guards
- are the library's `crosshair_value_is_the_nearest_sample` and `[cursors-probe]`'s `first mark at
- x=.. y=..` agreeing with the pane's first value.
+ change is in `deps/plotter`, which keeps the blend as `CrosshairValue::Interpolated` for a host
+ whose x is a quantity it is willing to interpolate — the nearest sample is the **default** and
+ **udpscope never states the other**, so a `set_crosshair_value_mode()` call appearing here is a
+ regression, not a feature.
+ **Both the mark and the number come from one call, `Plot::crosshair_point()`**, which returns
+ the quoted sample's own **x** as well as its y — so every mark this app draws for a readout is
+ *snapped to the sample it quotes*. The library's `CrosshairRow::marker` and the cursors pane's
+ `mix` crossing annotations (`place_crossings()`, which must use `crosshair_point()` and not
+ `crosshair_value()`) are both placed from it. Two consequences that look like defects and are
+ not: the mark **sits beside the polyline at a deep zoom** (one y feeding both the number and the
+ dot is what keeps them from disagreeing), and a crossing dot **sits a little off its own
+ vertical cursor line** — up to half a sample period, invisible at a whole-window view and the
+ width of the panel at a few samples across, which is the only zoom at which anyone is asking
+ which point it is. The line says where the *marker* is; the dot says which *datum* is quoted.
+ The hover mark is also deliberately *larger* than a trace's own sample dots
+ (`Style::crosshair_marker_px` 9 against `marker_px` 3), because a snapped mark lands among them
+ and at their size reads as one more sample. The guards are the library's
+ `crosshair_value_is_the_nearest_sample`, `the_crosshair_marker_snaps_to_the_sample_it_reports`,
+ `crosshair_interpolated_mode_blends_the_two_samples` and
+ `the_crosshair_mark_is_bigger_than_a_sample_dot`, plus `[cursors-probe]`'s `first mark at x=..
+ y=..` agreeing with the pane's first value.
- **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x`
is the *free* pointer position while a drag runs and the snapped value otherwise -- which is
what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on
diff --git a/README.md b/README.md
index eb2a9a2..83cea19 100644
--- a/README.md
+++ b/README.md
@@ -1153,10 +1153,25 @@ second, undeclared estimate laid over the first, and nothing on screen or in an
holds it. And at a zoom past the sample spacing it is visibly not the datum the pointer
is over: the polyline between two samples is a straight line the renderer drew, and
quoting a point on it reports the drawing rather than the data. A tie reads the earlier
-sample. The fix is upstream and unconditional (`deps/plotter`, `crosshair_span()`'s `t`
-is now a selector rather than a blend factor), so the library's own hover crosshair
-changed with the pane — which is the point, since both are the same lookup and a user
-comparing them must not see two numbers.
+sample. The fix is upstream (`deps/plotter`, `crosshair_span()`'s `t` is a selector rather
+than a blend factor), so the library's own hover crosshair changed with the pane — which is
+the point, since both are the same lookup and a user comparing them must not see two
+numbers. Upstream keeps the blend as `CrosshairValue::Interpolated` for hosts whose x is a
+quantity they are willing to interpolate; the nearest sample is the default and udpscope
+never states the other.
+
+**The crossing dot is on the sample, not on the marker's column.** The dot is placed
+through `Plot::crosshair_point()` — the same lookup as the value, returning the quoted
+sample's own x as well as its y — so the mark the eye follows and the number in the pane
+are the same datum. The two x's differ by up to half a sample period: nothing at a
+whole-window view, and the whole width of the panel once a few samples span the axes, which
+is exactly the state in which a reader is asking "which point is that?". It therefore leaves
+the dot a little off its own vertical line by design — the line says where the *marker* is
+and the dot says which *datum* is being quoted, and when the marker is between two samples
+those are two places. The library's hover crosshair snaps the same way and draws its mark
+`Style::crosshair_marker_px` across (9 logical px against a trace's own 3 px sample dots),
+since a snapped mark lands among those dots and at their size would read as one more sample
+instead of as the answer to the question the hover asks.
The readouts are a **table**: a header naming the columns, then one row per instrument —
`| who | A | B | delta |`. The first row is the two times and their difference, and the
diff --git a/deps/plotter b/deps/plotter
index c028f48..c960dec 160000
--- a/deps/plotter
+++ b/deps/plotter
@@ -1 +1 @@
-Subproject commit c028f48fa0e2499e3e0774b00297297b028378a5
+Subproject commit c960decaea6ec1ba00abd1d4d10c2a29c1d0a511
diff --git a/src/help/09-cursors.txt b/src/help/09-cursors.txt
index e7960ef..ea706e1 100644
--- a/src/help/09-cursors.txt
+++ b/src/help/09-cursors.txt
@@ -25,7 +25,10 @@ owns a Reset button. Drag a marker to move it. The mode dropdown offers f
• A marker past the newest sample reads --. With only one marker over data the row
reads partial: a difference between a number and nothing is not a number.
• In mix each marker also places a crossing annotation per trace, which is what lets
- you see on the plot which sample a number came from.
+ you see on the plot which sample a number came from. It is on that sample, so when the
+ marker falls between two it sits a little off its own line: the line is where you put the
+ marker, the dot is the datum being quoted. The hover crosshair's mark snaps the same way, and
+ is drawn larger than a trace's sample dots so it does not read as one more sample.
• The time row exists only in the modes that show the vertical pair. horizontal has no
time row, by design.
diff --git a/src/plot_host.cpp b/src/plot_host.cpp
index fc878aa..b5040bf 100644
--- a/src/plot_host.cpp
+++ b/src/plot_host.cpp
@@ -396,18 +396,30 @@ void RowSet::place_crossings() {
if (ida >= r.plot->annotation_count() || idb >= r.plot->annotation_count()) continue;
plot::Annotation a = r.plot->annotation(ida);
plot::Annotation b = r.plot->annotation(idb);
+ double xa = 0.0;
double ya = 0.0;
+ double xb = 0.0;
double yb = 0.0;
- const bool has_a = mix && r.plot->crosshair_value(static_cast(s), cursor_a_x_, ya);
- const bool has_b = mix && r.plot->crosshair_value(static_cast(s), cursor_b_x_, yb);
+ // `crosshair_point()`, not `crosshair_value()`: the number beside this dot is a *stored
+ // sample's*, so the dot belongs on that sample and not on the marker's own column. The
+ // two differ by up to half a sample period, which is nothing at a whole-window view and
+ // the whole width of the panel once the view is zoomed to a handful of samples -- the
+ // state in which a reader is asking "which point is that?" in the first place. It leaves
+ // the dot a little off its vertical line by design: the line says *where the marker is*
+ // and the dot says *which datum is being quoted*, and when the marker is between two
+ // samples those are not the same place.
+ const bool has_a =
+ mix && r.plot->crosshair_point(static_cast(s), cursor_a_x_, xa, ya);
+ const bool has_b =
+ mix && r.plot->crosshair_point(static_cast(s), cursor_b_x_, xb, yb);
a.visible = has_a;
b.visible = has_b;
if (has_a) {
- a.x = cursor_a_x_;
+ a.x = xa;
a.y = ya;
}
if (has_b) {
- b.x = cursor_b_x_;
+ b.x = xb;
b.y = yb;
}
r.plot->set_annotation(ida, a);