main moved ~60 commits while this branch was in progress, and one of them matters
here: PR #88 (the phone-gyro mirror) landed, touching the same motion path.
One conflicted file, `GamepadCapture.swift`, in three places — all of them the two
changes meeting rather than disagreeing:
- **Slot fields.** #88 added `motionSent` + `lastAccel` for its flush-parks-motion fix;
this branch removed `lastMotionNs` with the 4 ms drop-throttle. Kept both decisions:
the parking state stays, the throttle field goes.
- **forwardMotion's head.** #88 added the mirror stand-down (`pad 0` yields while the
phone speaks for it); this branch deleted the throttle guard. Kept the stand-down,
dropped the guard.
- **The send.** This branch converts into the DualSense report frame; #88 records what
went out so `flush` can replay it beside a zero gyro. Both, with the recording placed
AFTER the conversion — `flush` replays `lastAccel`, so it has to be the vector that
actually went on the wire, or a still pad's gravity gets parked in the wrong axis.
The two features compose exactly, which is worth stating because it is not luck: this
branch gates motion capture on `hasRotationRate`, and #88 engages the phone mirror when
`hasRotationRate != true`. They are complements — a pad either drives its own gyro or the
phone mirrors for it, never both and never neither.
Everything else auto-merged. Note `DeviceGyroRemapTests` is `#if os(iOS)`, so the macOS
suite reports the same 215 as before the merge rather than gaining #88's six — checked,
not assumed.
Gates re-run against the merged tree rather than trusting either side's: Linux fmt +
build + `clippy --locked --all-targets -D warnings` + punktfunk-core and pf-inject
suites; Apple 215 tests and the iOS-triple typecheck; Android kit + app compile and
tests. All green.