SELECT ... FOR UPDATE returns old value after waiting for lock
I have figured out the explanation for the behavior, by doing some experiments. I have used connection 1 to perform some other changes rather than insert a new row. The combined results reveal how the process works.

If connection 1 updates the description for the id, connection 2 will see the updated description. (The index scan has halted before the locked row.)
If connection 1 updates the id to a smaller  value, e.g. from 10 to 9, connection 2 will return the new value, 9. (The index scan has halted before the locked row, sees it disappear, and continues in a descending manner, thus arriving at 9.)
If connection 1 updates the id to a greater value, e.g. from 10 to 11, connection 2 will skip that value and instead return the greatest id that is smaller than what its original value was. In this example, it returns id 9 if that exists. (The index scan has halted before the locked row, sees it disappear, and continues in a descending manner, thus never passing 11, but rather arriving at 9.)
If connection 1 updates the id to a greater value, e.g. from 10 to 11, and then inserts a new row in place of the old one, e.g. with id 10, connection 2 will select the newly inserted row. (The index scan has halted before the locked row, and will see something inserted in its place.)
TODO: If connection 1 updates the id to a greater value, e.g. from 10 to 20, and then inserts a new row in place of the old one, but with a greater value than the original, e.g. 11, will connection 2 see it? Most likely not, but this should be tested.
If connection 1 locks a MAX(id) of 10, and connection 2 selects a hard-coded id of 9, then the locks do not overlap, and both can proceed independently.

We can conclude that the MAX(id) is determined using a reverse scan, and that the locks  are on actual (index) rows. This makes sense. Our isolation level of READ COMMITTED does not acquire gap locks. REPEATABLE READ might, but it causes the transaction of connection 2 to fail entirely, so there is no exploring that scenario any deeper.
Concluding the exact workings

Connection 2 starts reverse scanning until it hits a lock on a certain row.
It will wait for that lock to be released, and continue its scan from (and including) whatever id it left off at. Not whatever row, but whatever id.


As a result, if there is no longer a row at that id, the scan continues beyond it, to smaller ids.
As a result, if the row has been updated, the updated values are visible (if selecting more than just the id).
As a result, if the row has been moved and a new one inserted for its id, that new row is selected.