Work out the counting sequence for graycode counters for your stuff/fetch counters and you will observe that when the counter is exactly 1/4 full, the two two bits of the two counters will have one relationship, and when it's 3/4 full they'll have another. The 1/4-full signal will also be active at some other times when the queue isn't exactly 1/4 full but holds between 1 item and N/2-1, and may output some runt pulses, but it is guaranteed to be solidly high sometime during the interval when the number of items goes from N/4-1 to N/4+1, and never be high when the number of items is N/2 or greater. The 3/4-full signal behaves similarly. The status latch may sometimes go metastable, but will never be metastable any time its condition matters.
On the other hand, there is a 'gotcha' with a fully-async queue: the full/empty outputs must be externally synchronized in the time domains of the stuffer/fetcher. This will limit the maximum data throughput that can be achieved, since the stuffer must wait a synchronization delay after each put to see if there's room for more, and likewise with the fetcher. Having the stuffer keep a synchronized copy of the fetcher's pointer and vice versa, and having them do their comparisons based on those, would avoid that limitation.