This always happens. I post immediately before I figure it out. Maybe the act of posting gets me thinking in a different way..
So in my question the sample was a little bit over-simplified. It's actually more like this:
struct Base {};
struct C1 : Base { int i; }
struct C2 : Base { C1 c; int i; }
sizeof(C1) is correctly 4 on all platforms, but sizeof(C2) is 9 instead of 8 on GCC. And... apparently GCC is the only thing that gets it right, according to the last bit of the article I linked to in the original question. I'll quote it (from Nathan Meyers) here:
A whole family of related "empty subobject" optimizations are possible, subject to the ABI specifications a compiler must observe. (Jason Merrill pointed some of these out to me, years back.) For example, consider three struct members of (empty) types A, B, and C, and a fourth non-empty. They may, conformingly, all occupy the same address, as long as they don't have any bases in common with one another or with the containing class. A common gotcha in practice is to have the first (or only) member of a class derived from the same empty base as the class. The compiler has to insert padding so that they two subobjects have different addresses. This actually occurs in iterator adapters that have an interator member, both derived from std::iterator. An incautiously-implemented standard std::reverse_iterator might exhibit this problem.
So, the inconsistency I was seeing was only in cases where I had the above pattern. Every other place I was deriving from an empty struct was ok.
Easy enough to work around. Thanks all for the comments and answers.