Complementary filter at 0.98 and a timer-driven loop, and it stands. Tuned it with P=18, D=0.6, a tiny I, plus a 6 % deadband. The 1 message per second batches at 20 Hz work, no more rate_limit, and the angle chart on the booth board looks great. My mistake was publishing from inside the control loop, every publish stretched the loop and made the oscillation worse too.

