Project

General

Profile

Task #46 » nlos_diagnosis.html

knight8241, 08/07/2026 18:32

 
<title>Aether mesh map diagnosis — the fixed cross-room stretch on A3</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{
--bg:#e9eef4; --panel:#ffffff; --panel2:#f3f7fb; --line:#d3dde8;
--ink:#15212f; --muted:#54677e; --faint:#7f90a6;
--accent:#0e9f92; --accent-ink:#0b7d73;
--good:#1f9e57; --good-bg:#e6f5ec;
--bad:#dd382c; --bad-bg:#fceae8;
--warn:#bd7d1f; --warn-bg:#faf0da;
--true:#1391a6;
--shadow:0 1px 2px rgba(20,40,70,.06),0 8px 24px rgba(20,40,70,.06);
--radius:14px;
--font-sans:"Segoe UI",system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif;
--font-mono:ui-monospace,"SF Mono","Cascadia Code","JetBrains Mono","Roboto Mono",Menlo,Consolas,monospace;
}
@media (prefers-color-scheme:dark){
:root{
--bg:#080e17; --panel:#0f1928; --panel2:#152238; --line:#243549;
--ink:#e9eff7; --muted:#95a7c0; --faint:#6d7f98;
--accent:#33d6c6; --accent-ink:#7ff0e4;
--good:#40cf79; --good-bg:rgba(64,207,121,.13);
--bad:#ff5d50; --bad-bg:rgba(255,93,80,.14);
--warn:#f4ab41; --warn-bg:rgba(244,171,65,.14);
--true:#5fd6e8;
--shadow:0 1px 2px rgba(0,0,0,.4),0 10px 30px rgba(0,0,0,.35);
}
}
:root[data-theme="light"]{
--bg:#e9eef4; --panel:#ffffff; --panel2:#f3f7fb; --line:#d3dde8;
--ink:#15212f; --muted:#54677e; --faint:#7f90a6;
--accent:#0e9f92; --accent-ink:#0b7d73;
--good:#1f9e57; --good-bg:#e6f5ec; --bad:#dd382c; --bad-bg:#fceae8;
--warn:#bd7d1f; --warn-bg:#faf0da; --true:#1391a6;
--shadow:0 1px 2px rgba(20,40,70,.06),0 8px 24px rgba(20,40,70,.06);
}
:root[data-theme="dark"]{
--bg:#080e17; --panel:#0f1928; --panel2:#152238; --line:#243549;
--ink:#e9eff7; --muted:#95a7c0; --faint:#6d7f98;
--accent:#33d6c6; --accent-ink:#7ff0e4;
--good:#40cf79; --good-bg:rgba(64,207,121,.13); --bad:#ff5d50; --bad-bg:rgba(255,93,80,.14);
--warn:#f4ab41; --warn-bg:rgba(244,171,65,.14); --true:#5fd6e8;
--shadow:0 1px 2px rgba(0,0,0,.4),0 10px 30px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0; background:var(--bg); color:var(--ink);
font-family:var(--font-sans); line-height:1.65; font-size:17px; letter-spacing:.005em}
.wrap{max-width:1000px; margin:0 auto; padding:0 22px}
.measure{max-width:66ch}
.eyebrow{font-family:var(--font-mono); font-size:.72rem; text-transform:uppercase;
letter-spacing:.16em; color:var(--accent-ink); font-weight:600}
h1{font-size:clamp(1.9rem,4.6vw,2.9rem); line-height:1.08; font-weight:800;
letter-spacing:-.02em; text-wrap:balance; margin:.35em 0 .3em}
h2{font-size:clamp(1.3rem,2.6vw,1.7rem); font-weight:750; letter-spacing:-.015em;
text-wrap:balance; margin:0 0 .1em}
h3{font-size:1.02rem; font-weight:700; margin:0 0 .3em; letter-spacing:-.01em}
p{margin:.55em 0}
a{color:var(--accent-ink)}
.lead{font-size:1.12rem}
.mono{font-family:var(--font-mono)}
.num{font-family:var(--font-mono); font-variant-numeric:tabular-nums}
section{padding:44px 0; border-top:1px solid var(--line)}
section:first-of-type{border-top:0}
.stack{display:flex; flex-direction:column; gap:14px}
header{padding:54px 0 40px; position:relative; overflow:hidden}
.scope{position:absolute; inset:0; z-index:0; opacity:.45; pointer-events:none; width:100%; height:100%}
header .wrap{position:relative; z-index:1}
.kicker-row{display:flex; align-items:center; gap:12px; flex-wrap:wrap}
.dot{width:9px;height:9px;border-radius:50%;background:var(--bad);box-shadow:0 0 0 4px var(--bad-bg)}
.verdict{background:var(--panel); border:1px solid var(--line); border-left:5px solid var(--bad);
border-radius:var(--radius); padding:20px 22px; box-shadow:var(--shadow); margin-top:8px}
.verdict .big{font-size:1.22rem; font-weight:700; letter-spacing:-.01em}
figure{margin:0}
.fig{background:var(--panel); border:1px solid var(--line); border-radius:var(--radius);
box-shadow:var(--shadow); padding:18px; overflow:hidden}
.fig svg{display:block; width:100%; height:auto}
figcaption{color:var(--muted); font-size:.9rem; margin-top:12px; padding-top:12px; border-top:1px dashed var(--line)}
figcaption b{color:var(--ink)}
.legend{display:flex; gap:8px 18px; flex-wrap:wrap; font-family:var(--font-mono);
font-size:.76rem; letter-spacing:.02em; margin-top:4px}
.legend span{display:inline-flex; align-items:center; gap:7px; color:var(--muted)}
.swatch{width:22px; height:0; border-top-width:3px; border-top-style:solid; border-radius:2px}
.tablewrap{overflow-x:auto; border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); background:var(--panel)}
table{border-collapse:collapse; width:100%; min-width:660px; font-size:.95rem}
th,td{padding:12px 14px; text-align:left; border-bottom:1px solid var(--line); white-space:nowrap}
thead th{font-family:var(--font-mono); font-size:.68rem; text-transform:uppercase;
letter-spacing:.11em; color:var(--muted); font-weight:600; background:var(--panel2)}
tbody tr:last-child td{border-bottom:0}
td.n{font-family:var(--font-mono); font-variant-numeric:tabular-nums}
.pill{display:inline-flex; align-items:center; gap:6px; font-family:var(--font-mono);
font-size:.72rem; font-weight:600; padding:3px 9px; border-radius:999px; letter-spacing:.02em}
.pill.good{background:var(--good-bg); color:var(--good)}
.pill.bad{background:var(--bad-bg); color:var(--bad)}
.pill.warn{background:var(--warn-bg); color:var(--warn)}
.rowmark{width:4px; padding:0}
tr.good .rowmark{background:var(--good)} tr.bad .rowmark{background:var(--bad)} tr.warn .rowmark{background:var(--warn)}
.delta-bad{color:var(--bad); font-weight:700} .delta-warn{color:var(--warn); font-weight:700} .delta-ok{color:var(--muted)}
.grid{display:grid; gap:16px}
@media(min-width:640px){ .grid.cols3{grid-template-columns:repeat(3,1fr)} }
.card{background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); padding:18px; box-shadow:var(--shadow)}
.card .tag{font-family:var(--font-mono); font-size:.68rem; letter-spacing:.12em; text-transform:uppercase; color:var(--accent-ink); font-weight:600}
.rank{font-family:var(--font-mono); font-size:.75rem; color:var(--faint)}
ul.check{list-style:none; padding:0; margin:0; display:flex; flex-direction:column; gap:12px}
ul.check li{display:flex; gap:12px; align-items:flex-start; background:var(--panel);
border:1px solid var(--line); border-radius:12px; padding:14px 16px; box-shadow:var(--shadow)}
ul.check .box{flex:0 0 auto; width:20px; height:20px; border-radius:6px; border:2px solid var(--accent); margin-top:2px}
ul.check .q{font-weight:700; letter-spacing:-.01em}
ul.check small{color:var(--muted)}
.note{color:var(--muted); font-size:.95rem}
.kbd{font-family:var(--font-mono); font-size:.85em; background:var(--panel2); border:1px solid var(--line); border-radius:6px; padding:1px 6px}
footer{padding:34px 0 60px; color:var(--faint); font-size:.85rem}
hr.soft{border:0; border-top:1px dashed var(--line); margin:26px 0}
/* SVG color utilities (var() is not honored in SVG presentation attributes) */
svg text{font-family:var(--font-mono); fill:var(--ink)}
.st-accent{stroke:var(--accent)} .fl-accent{fill:var(--accent)}
.st-good{stroke:var(--good)} .fl-good{fill:var(--good)}
.st-bad{stroke:var(--bad)} .fl-bad{fill:var(--bad)}
.st-warn{stroke:var(--warn)} .fl-warn{fill:var(--warn)}
.st-true{stroke:var(--true)} .fl-true{fill:var(--true)}
.st-muted{stroke:var(--muted)} .fl-muted{fill:var(--muted)}
.st-line{stroke:var(--line)} .st-faint{stroke:var(--faint)} .fl-faint{fill:var(--faint)}
.fl-panel{fill:var(--panel)} .fl-ink{fill:var(--ink)} .fl-badbg{fill:var(--bad-bg)}
@media (prefers-reduced-motion:reduce){*{animation:none!important; transition:none!important}}
</style>

<header>
<svg class="scope" viewBox="0 0 1000 300" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<path class="st-accent" fill="none" stroke-width="1.5" d="M0 210 H360 l14 -20 l16 44 l12 -128 l16 150 l14 -46 H520"/>
<path class="st-bad" fill="none" stroke-width="1.5" opacity="0.8" d="M520 210 l18 -8 l22 34 l30 -150 l30 150 l22 -34 l18 8 H1000"/>
</svg>
<div class="wrap">
<div class="kicker-row">
<span class="dot"></span>
<span class="eyebrow">Field diagnosis · shared-truth review · build de2f0c7 · capture&nbsp;4 folded in</span>
</div>
<h1>The variable echoes are gone — a <em>fixed</em> cross-room stretch is what's left</h1>
<p class="lead measure">You re-aimed the anchors and cleared the sight lines, and the capture proves it
worked: the jittery echo on A3's long links <strong>collapsed</strong>. But those two cross-room links
still read a steady <span class="num">~0.6&nbsp;m</span> too long — a fixed stretch, not a jitter — and
that is what still breaks the map. Here's the updated shared story, and the fix that doesn't depend on
the room getting any cleaner.</p>
</div>
</header>

<div class="wrap">

<section id="verdict" style="border-top:0; padding-top:8px">
<div class="verdict stack">
<div><span class="eyebrow" style="color:var(--good)">Capture&nbsp;4 result — the re-aim test you ran</span></div>
<div class="big">Re-aiming the anchors <strong>eliminated the variable echo</strong>: A3's long-link jitter
collapsed (tails <span class="num">0.70→0.11</span> and <span class="num">0.54→0.15&nbsp;m</span>). What
remains is a <em>steady</em> over-range on the same two cross-room links
(<span class="num">A1–A3 +0.57</span>, <span class="num">A2–A3 +0.75&nbsp;m</span>) that
<strong>no calibration can remove</strong> — so the fix is to stop each phone guessing its own map and
have them all <strong>agree on one shared, self-solved map</strong>, made robust so it doesn't hinge on
those two ranges.</div>
<p class="note" style="margin:0">Two things changed our shared picture: (1) your orientation fix
<strong>worked</strong> — orientation is now <em>sufficient</em>, don't chase more angling; (2) the
leftover error is <em>fixed, not jittery</em>, and it lives on those specific long links — A3 ranges fine
to the phone (<span class="num">+0.10</span>) and to A4 (<span class="num">−0.15&nbsp;m</span>), so it's
the <em>path</em>, not A3's radio.</p>
</div>
</section>

<section id="c4" style="border-top:0; padding-top:0">
<div class="card measure" style="border-left:5px solid var(--good)">
<p style="margin:.1em 0"><strong>And on glass (you had the Pixel in first-person the whole time):</strong>
the phone now <em>localizes itself</em> and the anchors move <strong>smoothly</strong> — the client-side
fixes work. But the view still <em>points the wrong way and slowly slides the anchors in and out of
frame</em>, from two separate causes: <strong>(a)</strong> the map is warped by that fixed stretch, so
bearings are off; and <strong>(b)</strong> the camera has no compass lock — the AoA heading was disabled
because it read ±40° garbage, so it drifts on raw gyro. Two faults, two fixes — and neither is "more
smoothing."</p>
</div>
</section>

<section id="echo">
<div class="stack" style="gap:22px">
<div class="measure">
<span class="eyebrow">First, the mechanism</span>
<h2>A radio ranges by timing a pulse — and an echo takes the long way</h2>
<p>Each anchor measures distance to another by timing how long a radio pulse takes to travel between
them. If the straight path is clear, the timing is the true distance. But if something blocks or
weakens the straight path, the radio locks onto the <strong>next thing it hears</strong> — a
reflection off a wall, the floor, or a metal object. That bounce travelled <strong>farther</strong>,
so the anchor reports a distance that is <strong>too long</strong>, and it jitters as the reflection
shifts. Engineers call this "non-line-of-sight." I'll just call it an <strong>echo</strong>.</p>
</div>
<figure class="fig">
<svg viewBox="0 0 720 236" role="img" aria-label="A straight radio path is blocked, so the receiver hears a longer bounced echo and reports a distance that is too long.">
<defs>
<marker id="ah" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="fl-true" d="M0 0 L10 5 L0 10 z"/></marker>
<marker id="ahb" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="fl-bad" d="M0 0 L10 5 L0 10 z"/></marker>
</defs>
<circle class="fl-accent" cx="70" cy="150" r="9"/>
<text x="70" y="182" text-anchor="middle" font-size="13">Anchor</text>
<circle class="fl-accent" cx="650" cy="150" r="9"/>
<text x="650" y="182" text-anchor="middle" font-size="13">Anchor</text>
<line class="st-true" x1="82" y1="150" x2="638" y2="150" stroke-width="2.5" stroke-dasharray="7 6" marker-end="url(#ah)"/>
<text class="fl-true" x="360" y="142" text-anchor="middle" font-size="12.5">true distance — the straight path</text>
<rect class="fl-badbg st-bad" x="330" y="120" width="60" height="60" rx="6" stroke-width="2"/>
<text class="fl-bad" x="360" y="157" text-anchor="middle" font-size="22"></text>
<text class="fl-bad" x="360" y="205" text-anchor="middle" font-size="12">blocked / weak</text>
<path class="st-bad" d="M82 145 Q 360 20 638 145" fill="none" stroke-width="3" marker-end="url(#ahb)"/>
<text class="fl-bad" x="360" y="46" text-anchor="middle" font-size="12.5">echo — a longer bounced path → reads too far &nbsp;))))</text>
</svg>
<figcaption><b>Why an echo always reads long.</b> The reflected pulse literally travels a greater
distance than the straight line, so the measured range is inflated — never shortened. That single
fact is the fingerprint we look for in the data.</figcaption>
</figure>
</div>
</section>

<section id="map">
<div class="stack" style="gap:20px">
<div class="measure">
<span class="eyebrow">The room, to scale</span>
<h2>Which measurements to trust — and the two that are broken</h2>
<p>Your four anchors and the six links between them, drawn to scale from the locations you gave me
(A4 dropped to the floor at about 20&nbsp;cm). Green links measure true. The two <strong>red</strong>
links — both belonging to <strong>A3</strong> — read about 0.6&nbsp;m too long. One link
(<strong>amber</strong>) reads oddly short.</p>
</div>
<figure class="fig">
<svg viewBox="0 0 720 560" role="img" aria-label="Top-down room map. Anchor A3's two long cross-room links to A1 and A2 read about 0.6 metres too long; its short link to A4 and the other links are clean; the A2 to A4 link reads short.">
<defs>
<pattern id="floor" width="26" height="26" patternUnits="userSpaceOnUse">
<path class="st-line" d="M0 26 L26 0 M-6 6 L6 -6 M20 32 L32 20" stroke-width="1"/>
</pattern>
<marker id="aim" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7.5" markerHeight="7.5" orient="auto"><path class="fl-accent" d="M0 0 L10 5 L0 10 z"/></marker>
</defs>
<rect x="250" y="130" width="210" height="340" fill="url(#floor)" class="st-line" stroke-width="1.5" rx="6" opacity="0.9"/>

<line class="st-good" x1="250" y1="470" x2="460" y2="470" stroke-width="4"/>
<line class="st-good" x1="250" y1="470" x2="460" y2="201" stroke-width="4"/>
<line class="st-good" x1="250" y1="130" x2="460" y2="201" stroke-width="4"/>
<line class="st-warn" x1="460" y1="470" x2="460" y2="201" stroke-width="4"/>
<line class="st-bad" x1="250" y1="470" x2="250" y2="130" stroke-width="5"/>
<line class="st-bad" x1="460" y1="470" x2="250" y2="130" stroke-width="5"/>
<text class="fl-bad" x="238" y="300" text-anchor="end" font-size="12">))))</text>
<text class="fl-bad" x="372" y="292" text-anchor="middle" font-size="12" transform="rotate(-59 372 292)">))))</text>

<g font-size="12.5" font-variant-numeric="tabular-nums">
<text class="fl-good" x="355" y="492" text-anchor="middle">2.44 m · true 2.29 · +0.15</text>
<text class="fl-good" x="372" y="352" text-anchor="middle" transform="rotate(-52 372 352)">3.88 · true 3.85 · ok</text>
<text class="fl-good" x="352" y="150" text-anchor="middle" transform="rotate(19 352 150)">2.72 · true 2.72 · ok</text>
<text class="fl-warn" x="470" y="335">2.85 · true 3.15</text>
<text class="fl-warn" x="470" y="352">−0.30 short ?</text>
<text class="fl-bad" x="242" y="300" text-anchor="end" font-weight="bold">4.29</text>
<text class="fl-bad" x="242" y="316" text-anchor="end">true 3.70</text>
<text class="fl-bad" x="242" y="332" text-anchor="end" font-weight="bold">+0.59 ✕</text>
<text class="fl-bad" x="388" y="278" text-anchor="middle" font-weight="bold" transform="rotate(-59 388 278)">5.02 · true 4.35 · +0.67 ✕</text>
</g>

<g class="st-accent" stroke-width="3" fill="none">
<line x1="259" y1="457" x2="283" y2="422" marker-end="url(#aim)"/>
<line x1="451" y1="457" x2="427" y2="422" marker-end="url(#aim)"/>
<line x1="258" y1="144" x2="278" y2="181" marker-end="url(#aim)"/>
<line x1="449" y1="213" x2="421" y2="244" marker-end="url(#aim)"/>
</g>
<circle class="fl-accent fl-panel" cx="250" cy="470" r="13" style="fill:var(--accent); stroke:var(--panel)" stroke-width="3"/>
<text class="fl-ink" x="234" y="500" text-anchor="end" font-size="14" font-weight="bold">A1</text>
<text class="fl-muted" x="234" y="517" text-anchor="end" font-size="11.5">root · 1.23 m high</text>
<circle cx="460" cy="470" r="12" style="fill:var(--accent); stroke:var(--panel)" stroke-width="3"/>
<text class="fl-ink" x="478" y="500" font-size="14" font-weight="bold">A2</text>
<text class="fl-muted" x="478" y="517" font-size="11.5">1.38 m high</text>
<circle cx="250" cy="130" r="16" style="fill:var(--bad); stroke:var(--panel)" stroke-width="3"/>
<circle class="st-bad" cx="250" cy="130" r="23" fill="none" stroke-width="1.5" opacity="0.5"/>
<text class="fl-ink" x="234" y="112" text-anchor="end" font-size="14" font-weight="bold">A3</text>
<text class="fl-muted" x="234" y="128" text-anchor="end" font-size="11.5">1.45 m high</text>
<text class="fl-bad" x="234" y="144" text-anchor="end" font-size="11">← the culprit</text>
<circle cx="460" cy="201" r="12" style="fill:var(--accent); stroke:var(--panel)" stroke-width="3"/>
<text class="fl-ink" x="478" y="196" font-size="14" font-weight="bold">A4</text>
<text class="fl-warn" x="478" y="213" font-size="11.5">0.20 m — on the floor</text>

<line class="st-muted" x1="250" y1="536" x2="342" y2="536" stroke-width="2"/>
<line class="st-muted" x1="250" y1="532" x2="250" y2="540" stroke-width="2"/>
<line class="st-muted" x1="342" y1="532" x2="342" y2="540" stroke-width="2"/>
<text class="fl-muted" x="296" y="552" text-anchor="middle" font-size="11">1 metre</text>
</svg>
<div class="legend" aria-hidden="true">
<span><span class="swatch" style="border-top-color:var(--good)"></span> measures true</span>
<span><span class="swatch" style="border-top-color:var(--bad)"></span> broken — reads ~0.6 m long</span>
<span><span class="swatch" style="border-top-color:var(--warn)"></span> suspicious — reads short</span>
<span><span class="swatch" style="border-top-color:var(--accent)"></span> anchor &nbsp;·&nbsp; ))) echo</span>
<span class="mono" style="color:var(--accent-ink)">▸ desired orientation — top edge to centre</span>
</div>
<figcaption><b>The pattern points straight at A3 — and the teal arrows are the fix.</b> A3's <b>short</b>
link (to the nearby floor anchor A4) is perfect; its two <b>long</b> cross-room links to A1 and A2 are
the broken ones, so the trouble travels with A3's far side, not the room. Set every device the same
deterministic way: <b>upright, screen to the room, top edge (the antenna) aimed at the mesh centre</b>
— the arrows — so each anchor has a clear shot at all the others.</figcaption>
</figure>
</div>
</section>

<section id="evidence">
<div class="stack" style="gap:18px">
<div class="measure">
<span class="eyebrow">The measurements · all five devices agree</span>
<h2>The six links, side by side</h2>
<p>"Measured" is the steady value each pair reported over the whole recording; "true" is the distance
from your layout with A4 on the floor. "Spread" is the shape of the readings — a clean link sits on
one value; an echo has a <strong>long tail</strong> of even-longer readings.</p>
</div>
<div class="tablewrap">
<table>
<thead>
<tr><th class="rowmark" aria-hidden="true"></th><th>Link</th><th>Measured</th><th>True (A4=0.20)</th><th>Difference</th><th>Spread</th><th>Reading</th></tr>
</thead>
<tbody>
<tr class="good"><td class="rowmark"></td><td class="mono">A1 – A2</td><td class="n">2.40 m</td><td class="n">2.29 m</td><td class="n delta-ok">+0.11</td>
<td><svg width="66" height="22" viewBox="0 0 66 22" aria-hidden="true"><path class="st-good" d="M2 20 L28 20 L33 4 L38 20 L64 20" fill="none" stroke-width="2"/></svg></td>
<td><span class="pill good">tight · small offset</span></td></tr>
<tr class="bad"><td class="rowmark"></td><td class="mono">A1 – A3</td><td class="n">4.27 m</td><td class="n">3.70 m</td><td class="n delta-bad">+0.57</td>
<td><svg width="66" height="22" viewBox="0 0 66 22" aria-hidden="true"><path class="st-bad" d="M2 20 L28 20 L33 4 L38 20 L64 20" fill="none" stroke-width="2"/></svg></td>
<td><span class="pill bad">↔ tight · steady +0.57</span></td></tr>
<tr class="good"><td class="rowmark"></td><td class="mono">A1 – A4</td><td class="n">3.79 m</td><td class="n">3.85 m</td><td class="n delta-ok">−0.06</td>
<td><svg width="66" height="22" viewBox="0 0 66 22" aria-hidden="true"><path class="st-good" d="M2 20 L28 20 L33 4 L38 20 L64 20" fill="none" stroke-width="2"/></svg></td>
<td><span class="pill good">clean ✓</span></td></tr>
<tr class="bad"><td class="rowmark"></td><td class="mono">A2 – A3</td><td class="n">5.10 m</td><td class="n">4.35 m</td><td class="n delta-bad">+0.75</td>
<td><svg width="66" height="22" viewBox="0 0 66 22" aria-hidden="true"><path class="st-bad" d="M2 20 L28 20 L33 4 L38 20 L64 20" fill="none" stroke-width="2"/></svg></td>
<td><span class="pill bad">↔ tight · steady +0.75</span></td></tr>
<tr class="warn"><td class="rowmark"></td><td class="mono">A2 – A4</td><td class="n">2.82 m</td><td class="n">3.15 m</td><td class="n delta-warn">−0.33</td>
<td><svg width="66" height="22" viewBox="0 0 66 22" aria-hidden="true"><path class="st-warn" d="M2 20 L20 20 L25 5 L30 14 C40 15 52 18 64 19" fill="none" stroke-width="2"/></svg></td>
<td><span class="pill warn">? short · noisy tail (A4)</span></td></tr>
<tr class="good"><td class="rowmark"></td><td class="mono">A3 – A4</td><td class="n">2.57 m</td><td class="n">2.72 m</td><td class="n delta-ok">−0.15</td>
<td><svg width="66" height="22" viewBox="0 0 66 22" aria-hidden="true"><path class="st-good" d="M2 20 L28 20 L33 4 L38 20 L64 20" fill="none" stroke-width="2"/></svg></td>
<td><span class="pill good">clean ✓</span></td></tr>
</tbody>
</table>
</div>
<p class="note measure"><strong>Capture&nbsp;4 flipped one of these tells.</strong> Last capture the two
red links were inflated <em>and</em> carried a long tail — a jittery echo. After your re-aim the
<em>tails are gone</em> (they read tight now) but the <em>inflation stayed</em>: a <strong>fixed</strong>
+0.57 / +0.75&nbsp;m stretch. It still isn't a per-device miscalibration — that would shift <em>all</em>
of A3's links, yet A3–A4 is clean and the phone ranges A3 fine (+0.10&nbsp;m) — so the stretch is tied to
those two long cross-room <em>paths</em>, not to A3's radio. The one remaining tail is now on
<strong>A2–A4</strong>, the loose floor anchor.</p>
</div>
</section>

<section id="whybreaks">
<div class="stack" style="gap:20px">
<div class="measure">
<span class="eyebrow">Why two bad links wreck everything</span>
<h2>The anchors can't agree on the room's shape</h2>
<p>To pin four anchors into one rigid 3-D shape, the mesh needs <strong>all six</strong> distances to
be right — there's no slack. A3 is held in place by three "ropes" (its three links). Two of them are
<strong>too long</strong>, so they pull A3 to the wrong spot. Because every anchor's position is
measured relative to the others, one wrong corner <strong>twists the whole shape</strong> — and each
phone, solving from its own slightly different readings, lands on a <strong>different</strong> wrong
shape. That's the "every diagnostics screen shows a different mesh" you saw.</p>
</div>
<figure class="fig">
<svg viewBox="0 0 720 250" role="img" aria-label="Anchor A3 is pulled to a wrong position by its two overly-long links, twisting the whole solved shape away from the true rectangle.">
<defs><marker id="ah2" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fl-muted" d="M0 0 L10 5 L0 10 z"/></marker></defs>
<text class="fl-muted" x="150" y="26" text-anchor="middle" font-size="12" letter-spacing=".05em">TRUE SHAPE</text>
<polygon class="st-good" points="70,200 230,200 230,70 70,70" fill="none" stroke-width="2.5"/>
<circle class="fl-accent" cx="70" cy="200" r="6"/><text class="fl-muted" x="58" y="220" text-anchor="end" font-size="12">A1</text>
<circle class="fl-accent" cx="230" cy="200" r="6"/><text class="fl-muted" x="238" y="220" font-size="12">A2</text>
<circle class="fl-good" cx="70" cy="70" r="8"/><text class="fl-muted" x="58" y="60" text-anchor="end" font-size="12">A3</text>
<circle class="fl-accent" cx="230" cy="70" r="6"/><text class="fl-muted" x="238" y="66" font-size="12">A4</text>

<line class="st-muted" x1="300" y1="135" x2="382" y2="135" stroke-width="2" marker-end="url(#ah2)"/>

<text class="fl-muted" x="558" y="26" text-anchor="middle" font-size="12" letter-spacing=".05em">WHAT THE MESH SOLVES</text>
<polygon class="st-faint" points="470,200 630,200 630,70 470,70" fill="none" stroke-width="1.5" stroke-dasharray="4 5" opacity="0.8"/>
<polygon class="st-bad" points="470,200 630,200 640,120 435,32" fill="none" stroke-width="2.5"/>
<line class="st-bad" x1="470" y1="200" x2="435" y2="32" stroke-width="2" opacity="0.55"/>
<line class="st-bad" x1="630" y1="200" x2="435" y2="32" stroke-width="2" opacity="0.55"/>
<circle class="fl-accent" cx="470" cy="200" r="6"/><text class="fl-muted" x="458" y="220" text-anchor="end" font-size="12">A1</text>
<circle class="fl-accent" cx="630" cy="200" r="6"/><text class="fl-muted" x="638" y="220" font-size="12">A2</text>
<circle class="fl-accent" cx="640" cy="120" r="6"/><text class="fl-muted" x="650" y="120" font-size="12">A4</text>
<circle class="st-faint" cx="470" cy="70" r="5" fill="none" stroke-width="1.5"/>
<line class="st-bad" x1="470" y1="70" x2="435" y2="32" stroke-width="1.5" stroke-dasharray="3 3"/>
<circle class="fl-bad" cx="435" cy="32" r="8"/>
<text class="fl-bad" x="415" y="26" text-anchor="end" font-size="12">A3 pulled here</text>
</svg>
<figcaption><b>One wrong corner twists the whole room.</b> A3's two long links drag it off its true
spot (grey ring → red). The rigid shape rotates and flattens to absorb the error, the vertical
collapses so nothing looks "raised," and it never settles — so it never locks, and no two phones
agree.</figcaption>
</figure>
</div>
</section>

<section id="hypothesis">
<div class="stack" style="gap:18px">
<div class="measure">
<span class="eyebrow">What specifically — updated after capture&nbsp;4</span>
<h2>What's left on A3's two long links — now that the jitter is gone</h2>
<p>The data still says <em>A3's far side is the problem</em> (short link clean, both long links off by a
similar amount) — but capture&nbsp;4 changed <em>which</em> cause is in play. The variable echo cleared,
so a switching-reflection story no longer fits; a <strong>fixed</strong> stretch does. One candidate is
ruled out and two remain, and a single A/B test tells them apart.</p>
</div>
<div class="grid cols3">
<div class="card stack" style="gap:8px; opacity:.72">
<div style="display:flex; justify-content:space-between; align-items:baseline"><span class="tag" style="color:var(--bad)">Ruled out</span><span class="rank">A</span></div>
<h3><s>Something blocks A3's view</s></h3>
<p class="note">Was my first guess — an obstruction (TV/shelf/metal/person) on the cross-room lines.
<strong>You confirmed the sight line is clear, and capture&nbsp;4 agrees:</strong> a blocked path would
have kept a jittery tail, but the tails <em>collapsed</em> while the offset stayed. A clear line that
still reads long isn't a blockage. <strong>Dropped.</strong></p>
</div>
<div class="card stack" style="gap:8px">
<div style="display:flex; justify-content:space-between; align-items:baseline"><span class="tag" style="color:var(--good)">Leading</span><span class="rank">B</span></div>
<h3>The direct pulse is simply weak over the long span</h3>
<p class="note">Across 3.7–4.3&nbsp;m the straight signal is faint (but present), and the radio's
first-arrival detector consistently latches a hair late → a <em>fixed</em> over-range. Fits the
evidence: short/near links are clean, only the long ones stretch, and it's the same in both
directions. This is <strong>orientation-independent</strong> — pinning bypasses it; removing it at
source needs first-path-aware ranging inside the phone's radio, which we don't control.</p>
</div>
<div class="card stack" style="gap:8px">
<div style="display:flex; justify-content:space-between; align-items:baseline"><span class="tag" style="color:var(--warn)">Still possible</span><span class="rank">C</span></div>
<h3>A3's antenna adds a fixed delay in that direction</h3>
<p class="note">UWB antennas delay the pulse by a small amount that varies with the <em>angle</em> you
present them at (group delay). If A1/A2↔A3's mutual bearing sits in a slow part of A3's pattern, every
reading on those links gains a constant offset. This is <strong>orientation-dependent</strong> — so
the A/B rotation test (turn A3 90°) separates it from&nbsp;B: bias moves ⇒ C, bias holds ⇒ B.</p>
</div>
</div>
<div class="card" style="border-left:5px solid var(--warn)">
<div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:2px"><span class="tag" style="color:var(--warn)">Separate, smaller puzzle</span><span class="rank">A2 – A4</span></div>
<h3>One link reads <em>short</em>, which an echo can't explain</h3>
<p class="note measure">A2–A4 comes in ~30&nbsp;cm <strong>shorter</strong> than it should — in fact
slightly shorter than the straight-line floor distance, which isn't physically possible if both
anchors are exactly where we think. Echoes only make links longer, so this one is different: most
likely <strong>A2 or the floor-anchor A4 isn't quite at its surveyed spot</strong> (A4 is loose on
the floor at 20&nbsp;cm and easy to nudge), or a strong floor-bounce is confusing that one low link.
Minor next to the A3 echoes, but worth a glance while you're there.</p>
</div>
</div>
</section>

<section id="placement">
<div class="stack" style="gap:20px">
<div class="measure">
<span class="eyebrow">Fix #1 · point the radios at the mesh</span>
<h2>These exact phones have a one-sided antenna — aim it</h2>
<p>Your four anchors are <strong>Samsung Galaxy&nbsp;S21+</strong> phones; the moving observer is a
<strong>Pixel&nbsp;8&nbsp;Pro</strong>. On all of them the ultra-wideband antenna sits near the
<strong>top edge</strong> and is <strong>directional</strong> — strong out one face, weak out the
others. Aim a weak side at an anchor and its straight signal loses to a reflection: that is the echo
on A3's far links. Since you can set orientation exactly, use <strong>one deterministic rule for all
four</strong>: <strong>upright, screen toward the room, top edge (the antenna) aimed at the mesh
centre</strong> — the teal arrows on the map above. That gives every anchor a clear shot at every
other, and it's repeatable test to test.</p>
</div>
<figure class="fig">
<svg viewBox="0 0 720 240" role="img" aria-label="Stand each anchor phone upright with its top edge toward the other anchors. A phone lying flat on the floor sends its signal into the floor and hears echoes.">
<line class="st-line" x1="352" y1="18" x2="352" y2="226" stroke-width="1" stroke-dasharray="4 5"/>
<text class="fl-good" x="40" y="32" font-size="13" font-weight="bold">DO — stand it upright</text>
<rect x="76" y="64" width="56" height="118" rx="9" style="fill:var(--panel2);stroke:var(--good)" stroke-width="3"/>
<rect class="fl-good" x="88" y="70" width="32" height="6" rx="3"/>
<text class="fl-muted" x="104" y="200" text-anchor="middle" font-size="11">top edge</text>
<path class="st-good" d="M132 92 L252 74 M132 104 L252 130" fill="none" stroke-width="2"/>
<circle class="fl-accent" cx="268" cy="74" r="6"/><text class="fl-muted" x="280" y="78" font-size="12">A1</text>
<circle class="fl-accent" cx="268" cy="130" r="6"/><text class="fl-muted" x="280" y="134" font-size="12">A2</text>
<text class="fl-good" x="178" y="150" text-anchor="middle" font-size="12">straight signal reaches them ✓</text>
<text class="fl-bad" x="400" y="32" font-size="13" font-weight="bold">DON'T — lie it flat</text>
<line class="st-muted" x1="396" y1="188" x2="700" y2="188" stroke-width="2"/>
<text class="fl-muted" x="400" y="204" font-size="11">floor</text>
<rect x="432" y="170" width="96" height="18" rx="6" style="fill:var(--panel2);stroke:var(--bad)" stroke-width="3"/>
<path class="st-bad" d="M480 188 L524 222 L582 150" fill="none" stroke-width="2" stroke-dasharray="6 4"/>
<text class="fl-bad" x="612" y="126" text-anchor="middle" font-size="12">into the floor → echo ✗</text>
<circle class="fl-accent" cx="662" cy="82" r="6"/><text class="fl-muted" x="648" y="86" text-anchor="end" font-size="12">A1/A2</text>
</svg>
<figcaption><b>Upright, top edge toward the mesh — the deterministic rule.</b> A phone lying flat fires
its pattern into the floor and ceiling and hears the bounce instead of the straight path. A4 isn't flat
— you have it propped at an angle in a cup — but it's the only anchor <em>not</em> on a proper stand,
which is the likeliest reason its A2–A4 link is the one that's still noisy.</figcaption>
</figure>
<div class="grid cols3">
<div class="card stack" style="gap:6px">
<span class="tag" style="color:var(--bad)">A3 · re-aim it</span>
<p class="note">Turn A3 so its <strong>top edge points at the mesh centre</strong> (which faces it back
across the room at A1/A2), stand it upright, and leave a few centimetres between it and the wall or any
metal behind it. Its clean link to A4 proves the radio works — it's just pointed the wrong way.</p>
</div>
<div class="card stack" style="gap:6px">
<span class="tag" style="color:var(--warn)">A4 · put it on a stand</span>
<p class="note">A4 is the only anchor <strong>not on a tripod</strong> — propped at an angle in a cup at
~20&nbsp;cm. Stand it upright on a proper mount, top edge toward the room. This should clear the noisy,
slightly-short A2–A4 reading. (With pinning it won't block the map either way.)</p>
</div>
<div class="card stack" style="gap:6px">
<span class="tag" style="color:var(--good)">All four · basics</span>
<p class="note">Upright; top edge toward the middle of the mesh; out of any case with metal or magnets;
not held in a hand (a body absorbs the signal); clear line of sight to every other anchor.</p>
</div>
</div>
</div>
</section>

<section id="engine">
<div class="stack" style="gap:20px">
<div class="measure">
<span class="eyebrow">Fix #2 · the software should have flagged this</span>
<h2>Should the engine have caught the echo? Today it doesn't</h2>
<p>The engine <em>does</em> have echo defenses — but they're built to catch a sudden <strong>blip</strong>,
not a <strong>steady</strong> echo, and the check that could catch a steady one doesn't run on the
anchor-to-anchor measurements that build the map. So a link that is quietly 0.6&nbsp;m too long on
every single reading sails straight through.</p>
</div>
<figure class="fig">
<svg viewBox="0 0 720 210" role="img" aria-label="The measurement passes through several software guards, but none catch a steady echo on a four-anchor mesh. The fix is to use the radio's own echo signal and flag lopsided links.">
<defs>
<marker id="ahm" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fl-muted" d="M0 0 L10 5 L0 10 z"/></marker>
<marker id="ahg" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fl-good" d="M0 0 L10 5 L0 10 z"/></marker>
</defs>
<g font-size="12.5" text-anchor="middle">
<rect x="10" y="34" width="150" height="54" rx="10" style="fill:var(--panel2);stroke:var(--line)" stroke-width="1.5"/>
<text class="fl-ink" x="85" y="58">anchor ↔ anchor</text><text class="fl-ink" x="85" y="75">range</text>
<rect x="196" y="34" width="150" height="54" rx="10" style="fill:var(--bad-bg);stroke:var(--bad)" stroke-width="1.5"/>
<text class="fl-ink" x="271" y="58">smooth +</text><text class="fl-ink" x="271" y="75">blip gate</text>
<text class="fl-bad" x="333" y="49" font-size="14"></text>
<rect x="382" y="34" width="150" height="54" rx="10" style="fill:var(--bad-bg);stroke:var(--bad)" stroke-width="1.5"/>
<text class="fl-ink" x="457" y="58">average into</text><text class="fl-ink" x="457" y="75">a shape</text>
<text class="fl-bad" x="519" y="49" font-size="14"></text>
<rect x="568" y="34" width="142" height="54" rx="10" style="fill:var(--bad-bg);stroke:var(--bad)" stroke-width="2.5"/>
<text class="fl-bad" x="639" y="58" font-weight="bold">wrong room</text><text class="fl-bad" x="639" y="75" font-weight="bold">shape</text>
</g>
<line class="st-muted" x1="162" y1="61" x2="194" y2="61" stroke-width="2" marker-end="url(#ahm)"/>
<line class="st-muted" x1="348" y1="61" x2="380" y2="61" stroke-width="2" marker-end="url(#ahm)"/>
<line class="st-muted" x1="534" y1="61" x2="566" y2="61" stroke-width="2" marker-end="url(#ahm)"/>
<text class="fl-muted" x="271" y="104" text-anchor="middle" font-size="11">steady echo slips through</text>
<text class="fl-muted" x="457" y="104" text-anchor="middle" font-size="11">(this check is skipped here)</text>
<rect x="150" y="150" width="360" height="46" rx="10" style="fill:var(--good-bg);stroke:var(--good)" stroke-width="1.5"/>
<text class="fl-good" x="330" y="171" text-anchor="middle" font-size="12.5" font-weight="bold">ADD — use the radio's own echo signal,</text>
<text class="fl-good" x="330" y="188" text-anchor="middle" font-size="12.5">flag lopsided links, and share one robust solve</text>
<line class="st-good" x1="271" y1="150" x2="271" y2="90" stroke-width="2" marker-end="url(#ahg)"/>
</svg>
<figcaption><b>Every current guard misses a steady echo — and the radio's own echo signal is thrown
away.</b> These phones report a signal-strength / first-path indicator that flags a reflection, but the
app passes a constant "perfect" value, so the guard meant to use it never fires.</figcaption>
</figure>
<div class="grid cols2" style="grid-template-columns:1fr 1fr">
<div class="card stack" style="gap:8px">
<span class="tag">What exists — and why it misses this</span>
<ul class="note" style="margin:.1em 0 0; padding-left:1.1em; line-height:1.75">
<li><b>Blip gate</b> (3-sigma): the smoother settles onto the biased value, so every steady-long reading then looks normal.</li>
<li><b>Signal-quality gate:</b> present, but <b>fed a constant</b> — the real echo indicator (signal strength / first-path) is discarded in the phone layer before it arrives.</li>
<li><b>Triangle check:</b> the biased distances still form a valid (distorted) triangle, so nothing trips — and it doesn't run on the anchor-to-anchor path anyway.</li>
<li><b>Link-health report:</b> calls the bad links "strong," because the shape is fit <em>to</em> those biased numbers, so they look self-consistent.</li>
<li><b>Four anchors = no spare:</b> with exactly four, there's no extra measurement to cross-check, so one bad link can't be caught by maths alone.</li>
</ul>
</div>
<div class="card stack" style="gap:8px">
<span class="tag" style="color:var(--good)">The update I recommend</span>
<ul class="note" style="margin:.1em 0 0; padding-left:1.1em; line-height:1.75">
<li><b>Capture the radio's echo signal</b> (signal strength + first-path) instead of a constant, and let a poor value distrust that reading — this revives the dead guard.</li>
<li><b>Guard the anchor-to-anchor path:</b> flag a link whose readings are <b>lopsided</b> (a long high tail) as echo-suspect and trust it less when solving the shape.</li>
<li><b>With only four anchors there's no spare</b> to cross-check by maths (a fifth isn't available), so that radio echo signal is the <em>essential</em> guard, not a nice-to-have — and it makes the deterministic orientation above matter even more.</li>
<li><b>Tell the operator:</b> say "A3's links look like echoes," not "strong."</li>
<li class="note" style="opacity:.85"><b>The real fix:</b> share ONE robust self-solved map (every phone uses the lead phone's solve, made tough with direction readings + the moving phone's clean ranges) — no hand-entered locations. I use your surveyed layout only offline, to check the answer.</li>
</ul>
</div>
</div>
<p class="note measure">Bottom line: the engine can't <em>remove</em> a real echo — clean signal has to come
from the room — but it should <strong>detect it, distrust it, and say so</strong>, and it should be able to
stand the map up on the survey when the raw measurements can't be trusted. Worth doing; I can deliver the
detect-and-flag parts as tested patches.</p>
</div>
</section>

<section id="confirm">
<div class="stack" style="gap:18px">
<div class="measure">
<span class="eyebrow">You already answered — here's what we now agree on</span>
<h2>What you confirmed, and what it ruled out</h2>
<p>You corrected the room for me last round. Two of your answers <em>changed</em> the diagnosis — flagged
below — and that is exactly what this loop is for.</p>
</div>
<ul class="check">
<li><span class="box" style="background:var(--good);border-color:var(--good)"></span><div><div class="q">Layout confirmed (A4 height corrected to 0.20&nbsp;m)</div>
<small>A1&nbsp;(root)+A2 on stands against the wall behind them, upright, screens to the room; A3 across
the room ~1&nbsp;m off the far wall; A4 loose in a cup on the floor, top ~20&nbsp;cm out, angled toward
centre. The map above is drawn to these.</small></div></li>
<li><span class="box" style="background:var(--bad);border-color:var(--bad)"></span><div><div class="q">Nothing blocks the sight line A3↔A1/A2 — this ruled out my first guess</div>
<small>My original "most likely" cause was an obstruction on those two lines; <strong>you refuted it.</strong>
That's decisive: a <em>clear</em> line of sight that still reads long cannot be a blockage echo — it's a
fixed path-length or antenna effect. That single correction is why the fix moved to <strong>pinning</strong>.</small></div></li>
<li><span class="box" style="background:var(--good);border-color:var(--good)"></span><div><div class="q">Orientation is deterministic — and sufficient</div>
<small>A1/A2 screens face +y, A3 faces −y, all upright; A4 the only one off a tripod. Capture&nbsp;4 proves
this orientation is enough: the jitter cleared. No more angling needed.</small></div></li>
<li><span class="box" style="background:var(--warn);border-color:var(--warn)"></span><div><div class="q">A4 is the loose one — and it shows</div>
<small>The A2↔A4 link is now the one noisy link (fat tail, reads a bit short) — consistent with A4 low and
angled in a cup. Standing it on a tripod would clean that link; pinning makes it non-blocking for the
map either way.</small></div></li>
</ul>
</div>
</section>

<section id="viewchain">
<div class="stack" style="gap:18px">
<div class="measure">
<span class="eyebrow">Your aim-and-view drawings → the fix sequence</span>
<h2>The picture is built in five steps — and they must be fixed in order</h2>
<p>You drew the goal: point the phone at A1, and A1 shows up in the right place and <em>stays</em> there
as you turn and orbit. The image on the screen is built as a <strong>chain</strong> — where the phone
is, then where the anchors are, then the direction to each, then which way you're looking, then where
that lands on the glass. Each link has to be right <em>before the next one means anything</em>. Two
links are broken, and that order is exactly why the fixes are sequenced the way they are.</p>
</div>
<figure class="fig">
<svg viewBox="0 0 740 250" role="img" aria-label="The on-screen picture is a five-step chain: where the phone is (done), where the anchors are (fix now, every phone agrees on one self-solved map), direction to each (automatic), which way you look (fix next, heading lock), where it lands on screen (done plus an aim-sign check). All five correct gives: point at A1 and A1 stays locked in the view.">
<defs>
<marker id="ahv" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fl-muted" d="M0 0 L10 5 L0 10 z"/></marker>
<marker id="ahvg" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path class="fl-good" d="M0 0 L10 5 L0 10 z"/></marker>
</defs>
<g font-size="11.5" text-anchor="middle">
<!-- 1 done -->
<rect x="8" y="30" width="128" height="66" rx="10" style="fill:var(--good-bg);stroke:var(--good)" stroke-width="1.5"/>
<text class="fl-ink" x="72" y="52">where the</text><text class="fl-ink" x="72" y="67">phone is</text>
<text class="fl-good" x="72" y="87" font-size="10.5" font-weight="bold">✓ done · self-seed</text>
<!-- 2 fix now -->
<rect x="156" y="30" width="128" height="66" rx="10" style="fill:var(--bad-bg);stroke:var(--bad)" stroke-width="2.5"/>
<text class="fl-ink" x="220" y="52">where the</text><text class="fl-ink" x="220" y="67">anchors are</text>
<text class="fl-bad" x="220" y="87" font-size="10.5" font-weight="bold">① FIX NOW · one shared solve</text>
<!-- 3 auto -->
<rect x="304" y="30" width="128" height="66" rx="10" style="fill:var(--panel2);stroke:var(--line)" stroke-width="1.5"/>
<text class="fl-ink" x="368" y="52">direction to</text><text class="fl-ink" x="368" y="67">each anchor</text>
<text class="fl-muted" x="368" y="87" font-size="10.5">auto · once 1+2 right</text>
<!-- 4 fix next -->
<rect x="452" y="30" width="128" height="66" rx="10" style="fill:var(--warn-bg);stroke:var(--warn)" stroke-width="2.5"/>
<text class="fl-ink" x="516" y="52">which way</text><text class="fl-ink" x="516" y="67">you're looking</text>
<text class="fl-warn" x="516" y="87" font-size="10.5" font-weight="bold">② THEN · heading lock</text>
<!-- 5 done+check -->
<rect x="600" y="30" width="128" height="66" rx="10" style="fill:var(--good-bg);stroke:var(--good)" stroke-width="1.5"/>
<text class="fl-ink" x="664" y="52">where it lands</text><text class="fl-ink" x="664" y="67">on the glass</text>
<text class="fl-good" x="664" y="87" font-size="10.5" font-weight="bold">✓ done · +aim-sign check</text>
</g>
<!-- chain arrows -->
<g>
<line class="st-muted" x1="138" y1="63" x2="154" y2="63" stroke-width="2" marker-end="url(#ahv)"/>
<line class="st-muted" x1="286" y1="63" x2="302" y2="63" stroke-width="2" marker-end="url(#ahv)"/>
<line class="st-muted" x1="434" y1="63" x2="450" y2="63" stroke-width="2" marker-end="url(#ahv)"/>
<line class="st-muted" x1="582" y1="63" x2="598" y2="63" stroke-width="2" marker-end="url(#ahv)"/>
</g>
<!-- result -->
<line class="st-good" x1="368" y1="96" x2="368" y2="150" stroke-width="2.5" marker-end="url(#ahvg)"/>
<rect x="150" y="152" width="440" height="72" rx="12" style="fill:var(--good-bg);stroke:var(--good)" stroke-width="2"/>
<text class="fl-good" x="370" y="180" text-anchor="middle" font-size="13.5" font-weight="bold">All five correct →</text>
<text class="fl-ink" x="370" y="201" text-anchor="middle" font-size="12.5">point the phone at A1, and A1 appears at its true spot</text>
<text class="fl-ink" x="370" y="217" text-anchor="middle" font-size="12.5">and <tspan font-weight="bold">stays locked</tspan> as you turn in place and orbit — exactly your drawings.</text>
</svg>
<figcaption><b>Why the order is forced.</b> Step ② (which way you're looking) is worked out from the
phone's own radio bearings to the anchors — so it can only be right once step ① has the anchors in the
<em>right places</em>. That's the whole reason the heading came out as garbage before and had to be
switched off: it was being computed against a warped map. Fix the map first, and the heading has a
correct thing to lock onto. The aim-sign check on step ⑤ is the last guard from your drawings — screen
toward A1 must show A1, never the far wall. <b>And step ① is mesh-wide:</b> right now the root and each
anchor each draw a <em>different</em> shape, because every phone solves the room from its own noisy
measurements and ignores the others. The mesh <em>already</em> computes one shared answer on the lead
phone and broadcasts it — the fix is to make every phone <em>use that one</em> instead of guessing its
own, and to make that shared solve tougher (leaning on the phones' direction sensors and the moving
phone's own clean distance readings). <b>No hand-entered locations</b> — it stays self-solving, works
for any sensible layout, and flags one that genuinely can't be solved. That shared agreement is what
"the map is locked" really means, and it must hold before steps ②–⑤ can.</figcaption>
</figure>
</div>
</section>

<section id="next">
<div class="stack" style="gap:14px">
<div class="measure">
<span class="eyebrow">The re-test is in — here's what each side does next</span>
<h2>Fix the map in software; the room is already good enough</h2>
<p>Capture&nbsp;4 was the clean re-test, and it half-confirmed the theory: the variable echo is gone,
a fixed stretch remains. That splits the way forward cleanly — I fix the map and the heading in code;
you only touch the room again if we decide to chase the stretch to its physical source.</p>
</div>
<ul class="check">
<li><span class="box" style="background:var(--good);border-color:var(--good)"></span><div><div class="q">On my side — make every phone agree on one self-solved map (the real fix)</div>
<small>The mesh already works out one shared map on the lead phone and sends it to the others — but each
phone currently <strong>ignores it and draws its own guess</strong>, which is why you get four
different shapes. I make every phone <strong>use the one shared map</strong>, and make that shared
solve tougher so it doesn't hinge on the two bad links: it leans on the phones' <em>direction</em>
readings between anchors and on the <em>moving</em> phone's own (clean) distance readings as it walks
the room. <strong>No hand-entered locations</strong> — it stays self-solving for any sensible layout,
and flags a layout that genuinely can't be solved rather than locking a wrong one.</small></div></li>
<li><span class="box" style="background:var(--good);border-color:var(--good)"></span><div><div class="q">On my side — give the camera a compass again</div>
<small>Once the map is correct, the phone's own bearings to the anchors become trustworthy, so the
heading lock I had to disable can be re-checked and re-enabled — that's what stops the anchors sliding
out of frame. If the bearings still won't settle, I fall back to the phone's magnetometer.</small></div></li>
<li><span class="box" style="background:var(--good);border-color:var(--good)"></span><div><div class="q">On my side — let the engine see the weak links</div>
<small>Right now the engine is blind to signal strength (every reading claims "perfect"), so it can't
down-weight the long links on its own. I wire the real first-path strength through so it can.</small></div></li>
<li><span class="box"></span><div><div class="q">On your side — <em>only if</em> we want to kill the stretch at its source</div>
<small>One optional A/B: rotate <strong>A3</strong> a known angle (say 90°), change nothing else, record
once. If the +0.6&nbsp;m moves, it's the antenna's aim and we can tune it out; if it holds, it's the
room's geometry and pinning is the only client-side answer. Skip it if pinning's accuracy is enough.</small></div></li>
</ul>
<p class="measure" style="margin-top:6px">What the re-test actually showed, against the precise, falsifiable
prediction I made last round:</p>
<div class="card measure" style="border-left:5px solid var(--warn)">
<p style="margin:.2em 0"><strong>Predicted:</strong> A3's two links drop from
<span class="num">+0.59</span> / <span class="num">+0.67 m</span> to near zero <em>and</em> their long
tails disappear. <strong>Actual:</strong> the long <em>tails</em> disappeared
(<span class="num">0.70→0.11</span>, <span class="num">0.54→0.15 m</span>) — the echo half was real and
your re-aim killed it — but the <em>offsets stayed</em> (<span class="num">+0.57</span> /
<span class="num">+0.75 m</span>). So the map still won't lock.</p>
<p class="note" style="margin:.4em 0 0">That's the useful half of "if they don't drop, I'm partly wrong":
a purely jittery echo would have cleared completely. A steady offset that survives a clean line of sight
is a <em>fixed</em> path-length or antenna effect — which is why the answer is now <strong>pin to the
survey</strong>, not chase the room further. The optional A/B above is the only thing that tells us which
of the two it physically is.</p>
</div>
<p class="note measure">One honest limit: I can prove from your data that these ranges are corrupted and
that no calibration can fix them — but I can't <em>un</em>-corrupt them from here. Clean measurements
have to come from the room. That's the only reason this needs a device, and it's a confirmation step,
not a diagnosis gap.</p>
</div>
</section>

<footer>
<hr class="soft">
<p>Updated from capture <span class="kbd">mofetrace · 1785969…</span> (capture&nbsp;4, the re-aim re-test) —
five devices, one build (<span class="kbd">de2f0c7</span>), all agreeing on the numbers above; supersedes
the first capture <span class="kbd">1785924…</span>. Distances are the steady (median) values over the
recording; before→after tails compare A3's long links across the two captures; "true" uses your layout with
A4 at 0.20 m. This is our converged picture — still offered to be corrected.</p>
</footer>
</div>
    (1-1/1)