https://floooh.github.io/2021/12/17/cycle-stepped-z80.html
One important note: at the start of the post I'm speculating about why I was seeing some minor differences to a 'real' Z80, it turned out that this speculation was wrong and instead the differences were caused by 'incomplete' netlist simulation code which worked fine for the 6502 but required some tweaks for the Z80, see the comments of this GH issue for details: https://github.com/floooh/v6502r/issues/2.
As far as I'm aware the netlist simulation now behaves correctly like a Zilog Z80 (but note that some reverse engineered Z80 clones like the East German U880 are known to have slightly different undocumented behaviour - which barely matters in practice though). The Z80 and 6502 emulators in https://github.com/floooh/chips are tested against their respective netlist simulations for correct behaviour and timing (all being based on the netlists from http://www.visual6502.org/ and the simulation code from https://github.com/mist64/perfect6502 - which is the simulation from visual6502.org ported from JS to C).