You need to also:
NVIC_ClearPendingIRQ(TIM2_IRQn);
that isn't needed, clearing it in the timer is enough
the only obvious difference from something I know works is using &= rather than just write to the status register
True on both counts.
Just to make sure, I reproduced the code on a F103 (different core, but exactly the same timer 2), and it works as expected.
I also moved the port toggling within the for(;; ) loop, with a condition on
ticks, so the code is definitely not stuck in the ISR.
The code, as written, is a bit strange: the difference between the ARR and CCR1 register is just one, so by the time the ISR is entered and the if() is reached, also UIF is set (together with CC1F) and then both are reset.
The capture, then, has no real effect on this program.
Another hint: to simplify debugging, it is convenient to set DBG_TIM2_STOP bit in the DBGMCU_APB1_FZ register, so that the timer is stopped when the core is halted.