diff --git a/CLAUDE.md b/CLAUDE.md
index f2915cc..67de04b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -749,6 +749,19 @@ far the largest file.
again until the next latch. `PlotArea::create()` is handed `App::placeholder_plot`,
never a row: the area keeps a reference to the plot it was created with, and a rebuild
destroys every row.
+- **A readout quotes a stored sample, never a value between two of them.** The hover
+ crosshair and the cursors pane's `mix` values are one call, `Plot::crosshair_value()`, and it
+ reports the **nearer** of the two samples bracketing the x (a tie reads the earlier). Upstream
+ interpolated linearly, and that is wrong twice over here: ingest decimation is a *boxcar
+ 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.
- **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 66988ef..eb2a9a2 100644
--- a/README.md
+++ b/README.md
@@ -1139,10 +1139,24 @@ is what made a dragged cursor spring to where it had been. A ruler is per row: e
two different values, and the pane names the row.
`mix` needs no rulers: it reports the value the trace actually has at each marker, read
-through the library's `Plot::crosshair_value()` — the two bracketing samples,
-interpolated — so it is the answer to "what is this signal at these two times" rather
-than "drag a ruler until it crosses the trace". A readout is `--` when the trace has
-nothing at that time and `partial` when only one of the two markers is over samples.
+through the library's `Plot::crosshair_value()` — **the nearer of the two bracketing
+samples**, never a number between them — so it is the answer to "which datum of this
+signal is at these two times" rather than "drag a ruler until it crosses the trace". A
+readout is `--` when the trace has nothing at that time and `partial` when only one of
+the two markers is over samples.
+
+The value used to be a linear interpolation between the brackets, which is upstream's
+old convention and was wrong for this app in two ways at once. The samples here are
+**already averaged** — ingest decimation is a boxcar over `time_stride` samples (see
+"Decimation at ingest is an average") — so a number computed *between* two of them is a
+second, undeclared estimate laid over the first, and nothing on screen or in an export
+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.
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 26a7c82..c028f48 160000
--- a/deps/plotter
+++ b/deps/plotter
@@ -1 +1 @@
-Subproject commit 26a7c82a8050fa5446b0ca73b21203e38e47a916
+Subproject commit c028f48fa0e2499e3e0774b00297297b028378a5
diff --git a/src/channel.hpp b/src/channel.hpp
index cf22750..d301fc4 100644
--- a/src/channel.hpp
+++ b/src/channel.hpp
@@ -73,8 +73,8 @@ class ChannelBuffer final : public plot::data::Series {
/// datagram leaves a gap, so index times must come from the time ring.
f64 x_at(u64 i) const override;
/// Bracketing samples by binary search over the time ring, which is monotonic,
- /// so a crosshair value is interpolated between the two samples actually either
- /// side of the pointer -- a grid walk would land on the wrong pair after a gap.
+ /// so a crosshair reads whichever of the two samples actually either side of the
+ /// pointer is nearer -- a grid walk would land on the wrong pair after a gap.
bool neighbours(f64 x, u64& i0, u64& i1) const override;
u64 snap_index(f64 x) const override;
plot::Range x_bounds() const override;
diff --git a/src/help/09-cursors.txt b/src/help/09-cursors.txt
index b2a6a20..e7960ef 100644
--- a/src/help/09-cursors.txt
+++ b/src/help/09-cursors.txt
@@ -19,6 +19,9 @@ owns a Reset button. Drag a marker to move it. The mode dropdown offers f
• mix is the reading mode: one line per bound trace, with the trace's own values
at each marker and their difference. A slot with a name but no channel behind it is not a
trace and gets no line.
+• A value is the nearest stored sample's, never a number between two samples. Zoom in past
+ the sample spacing and the line between two samples is something the plot drew, not data —
+ so a reading off it would be the drawing. The hover crosshair reads the same way.
• 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