File size: 409,662 Bytes
adda942
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
4606
4607
4608
4609
4610
4611
4612
4613
4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
4679
4680
4681
4682
4683
4684
4685
4686
4687
4688
4689
4690
4691
4692
4693
4694
4695
4696
4697
4698
4699
4700
4701
4702
4703
4704
4705
4706
4707
4708
4709
4710
4711
4712
4713
4714
4715
4716
4717
4718
4719
4720
4721
4722
4723
4724
4725
4726
4727
4728
4729
4730
4731
4732
4733
4734
4735
4736
4737
4738
4739
4740
4741
4742
4743
4744
4745
4746
4747
4748
4749
4750
4751
4752
4753
4754
4755
4756
4757
4758
4759
4760
4761
4762
4763
4764
4765
4766
4767
4768
4769
4770
4771
4772
4773
4774
4775
4776
4777
4778
4779
4780
4781
4782
4783
4784
4785
4786
4787
4788
4789
4790
4791
4792
4793
4794
4795
4796
4797
4798
4799
4800
4801
4802
4803
4804
4805
4806
4807
4808
4809
4810
4811
4812
4813
4814
4815
4816
4817
4818
4819
4820
4821
4822
4823
4824
4825
4826
4827
4828
4829
4830
4831
4832
4833
4834
4835
4836
4837
4838
4839
4840
4841
4842
4843
4844
4845
4846
4847
4848
4849
4850
4851
4852
4853
4854
4855
4856
4857
4858
4859
4860
4861
4862
4863
4864
4865
4866
4867
4868
4869
4870
4871
4872
4873
4874
4875
4876
4877
4878
4879
4880
4881
4882
4883
4884
4885
4886
4887
4888
4889
4890
4891
4892
4893
4894
4895
4896
4897
4898
4899
4900
4901
4902
4903
4904
4905
4906
4907
4908
4909
4910
4911
4912
4913
4914
4915
4916
4917
4918
4919
4920
4921
4922
4923
4924
4925
4926
4927
4928
4929
4930
4931
4932
4933
4934
4935
4936
4937
4938
4939
4940
4941
4942
4943
4944
4945
4946
4947
4948
4949
4950
4951
4952
4953
4954
4955
4956
4957
4958
4959
4960
4961
4962
4963
4964
4965
4966
4967
4968
4969
4970
4971
4972
4973
4974
4975
4976
4977
4978
4979
4980
4981
4982
4983
4984
4985
4986
4987
4988
4989
4990
4991
4992
4993
4994
4995
4996
4997
4998
4999
5000
5001
5002
5003
5004
5005
5006
5007
5008
5009
5010
5011
5012
5013
5014
5015
5016
5017
5018
5019
5020
5021
5022
5023
5024
5025
5026
5027
5028
5029
5030
5031
5032
5033
5034
5035
5036
5037
5038
5039
5040
5041
5042
5043
5044
5045
5046
5047
5048
5049
5050
5051
5052
5053
5054
5055
5056
5057
5058
5059
5060
5061
5062
5063
5064
5065
5066
5067
5068
5069
5070
5071
5072
5073
5074
5075
5076
5077
5078
5079
5080
5081
5082
5083
5084
5085
5086
5087
5088
5089
5090
5091
5092
5093
5094
5095
5096
5097
5098
5099
5100
5101
5102
5103
5104
5105
5106
5107
5108
5109
5110
5111
5112
5113
5114
5115
5116
5117
5118
5119
5120
5121
5122
5123
5124
5125
5126
5127
5128
5129
5130
5131
5132
5133
5134
5135
5136
5137
5138
5139
5140
5141
5142
5143
5144
5145
5146
5147
5148
5149
5150
5151
5152
5153
5154
5155
5156
5157
5158
5159
5160
5161
5162
5163
5164
5165
5166
5167
5168
5169
5170
5171
5172
5173
5174
5175
5176
5177
5178
5179
5180
5181
5182
5183
5184
5185
5186
5187
5188
5189
5190
5191
5192
5193
5194
5195
5196
5197
5198
5199
5200
5201
5202
5203
5204
5205
5206
5207
5208
5209
5210
5211
5212
5213
5214
5215
5216
5217
5218
5219
5220
5221
5222
5223
5224
5225
5226
5227
5228
5229
5230
5231
5232
5233
5234
5235
5236
5237
5238
5239
5240
5241
5242
5243
5244
5245
5246
5247
5248
5249
5250
5251
5252
5253
5254
5255
5256
5257
5258
5259
5260
5261
5262
5263
5264
5265
5266
5267
5268
5269
5270
5271
5272
5273
5274
5275
5276
5277
5278
5279
5280
5281
5282
5283
5284
5285
5286
5287
5288
5289
5290
5291
5292
5293
5294
5295
5296
5297
5298
5299
5300
5301
5302
5303
5304
5305
5306
5307
5308
5309
5310
5311
5312
5313
5314
5315
5316
5317
5318
5319
5320
5321
5322
5323
5324
5325
5326
5327
5328
5329
5330
5331
5332
5333
5334
5335
5336
5337
5338
5339
5340
5341
5342
5343
5344
5345
5346
5347
5348
5349
5350
5351
5352
5353
5354
5355
5356
5357
5358
5359
5360
5361
5362
5363
5364
5365
5366
5367
5368
5369
5370
5371
5372
5373
5374
5375
5376
5377
5378
5379
5380
5381
5382
5383
5384
5385
5386
5387
5388
5389
5390
5391
5392
5393
5394
5395
5396
5397
5398
5399
5400
5401
5402
5403
5404
5405
5406
5407
5408
5409
5410
5411
5412
5413
5414
5415
5416
5417
5418
5419
5420
5421
5422
5423
5424
5425
5426
5427
5428
5429
5430
5431
5432
5433
5434
5435
5436
5437
5438
5439
5440
5441
5442
5443
5444
5445
5446
5447
5448
5449
5450
5451
5452
5453
5454
5455
5456
5457
5458
5459
5460
5461
5462
5463
5464
5465
5466
5467
5468
5469
5470
5471
5472
5473
5474
5475
5476
5477
5478
5479
5480
5481
5482
5483
5484
5485
5486
5487
5488
5489
5490
5491
5492
5493
5494
5495
5496
5497
5498
5499
5500
5501
5502
5503
5504
5505
5506
5507
5508
5509
5510
5511
5512
5513
5514
5515
5516
5517
5518
5519
5520
5521
5522
5523
5524
5525
5526
5527
5528
5529
5530
5531
5532
5533
5534
5535
5536
5537
5538
5539
5540
5541
5542
5543
5544
5545
5546
5547
5548
5549
5550
5551
5552
5553
5554
5555
5556
5557
5558
5559
5560
5561
5562
5563
5564
5565
5566
5567
5568
5569
5570
5571
5572
5573
5574
5575
5576
5577
5578
5579
5580
5581
5582
5583
5584
5585
5586
5587
5588
5589
5590
5591
5592
5593
5594
5595
5596
5597
5598
5599
5600
5601
5602
5603
5604
5605
5606
5607
5608
5609
5610
5611
5612
5613
5614
5615
5616
5617
5618
5619
5620
5621
5622
5623
5624
5625
5626
5627
5628
5629
5630
5631
5632
5633
5634
5635
5636
5637
5638
5639
5640
5641
5642
5643
5644
5645
5646
5647
5648
5649
5650
5651
5652
5653
5654
5655
5656
5657
5658
5659
5660
5661
5662
5663
5664
5665
5666
5667
5668
5669
5670
5671
5672
5673
5674
5675
5676
5677
5678
5679
5680
5681
5682
5683
5684
5685
5686
5687
5688
5689
5690
5691
5692
5693
5694
5695
5696
5697
5698
import gradio as gr
import gspread
import requests
import socket
import tempfile
import os
import json
from urllib.parse import quote as url_quote
import hashlib
import re
import cv2
import pytesseract
import threading
import time
from concurrent.futures import ThreadPoolExecutor
import concurrent.futures
from pyzbar.pyzbar import decode
from datetime import datetime, timedelta
from PIL import Image as PILImage
from fpdf import FPDF
import fitz  # PyMuPDF für PDF-Konvertierung

# Globale Absicherung gegen unendliches Hängenbleiben bei Netzwerk-Aufrufen (z.B.
# zu Google Sheets) - ohne das kann eine einzelne hängende Verbindung die GANZE
# App beim Start lahmlegen, noch bevor Python/Gradio überhaupt eine Zeile Log
# ausgibt (genau das ist am 2026-07-14 passiert: Space blieb bei "Restarting"
# stehen, Container-Log zeigte NICHTS außer "Application Startup" - kein Fehler,
# kein Absturz, einfach ewiges Warten auf eine Server-Antwort, die nie kam).
# 30 Sekunden sind großzügig genug für normale Google-API-Aufrufe, verhindern
# aber ein Hängenbleiben für immer - danach kommt wenigstens ein klarer Fehler
# im Log statt stiller Funkstille.
socket.setdefaulttimeout(30)

# ==========================================
# 1. AUTH, SETUP & CACHE
# ==========================================
# Google-Zugangsdaten: bevorzugt aus einem Secret (Umgebungsvariable) laden - das
# ist der sichere Weg, da Secrets nie im Repo landen und damit nicht öffentlich
# einsehbar sind (im Gegensatz zu einer hochgeladenen JSON-Datei, siehe der
# Sicherheitsvorfall mit der geleakten Datei früher in diesem Projekt). Fällt auf
# die alte datei-basierte Methode zurück, falls kein Secret gesetzt ist - dadurch
# funktioniert derselbe Code sowohl auf der alten als auch auf der neuen Space.
_google_creds_json = os.environ.get("GOOGLE_CREDENTIALS")
if _google_creds_json:
    gc = gspread.service_account_from_dict(json.loads(_google_creds_json))
else:
    gc = gspread.service_account(filename="carbide-theme-491608-t3-1223e08c6a66.json")

def _mit_wiederholung(fn, *args, **kwargs):
    """Führt einen Google-Sheets-Aufruf mit steigender Wartezeit erneut aus, falls
    Google kurzzeitig mit 503/500 (überlastet) oder 429 (Ratenlimit) antwortet -
    das ist ein bekanntes, rein vorübergehendes Google-seitiges Problem, kein
    Fehler in unserem Code (siehe v7.91.5, Absturz beim Start durch
    gspread.exceptions.APIError: [503] direkt bei sh.worksheet(...)). Ohne das
    stürzte die GESAMTE App schon beim Hochfahren ab, wenn Google in genau diesem
    Sekundenbruchteil mal kurz überlastet war - nicht behebbar durch den
    30-Sekunden-Timeout aus v7.88.4 allein, da das nur endloses Hängen verhindert,
    aber keine neuen Versuche unternimmt."""
    wartezeit = 2
    for versuch in range(5):
        try:
            return fn(*args, **kwargs)
        except gspread.exceptions.APIError as e:
            code = getattr(getattr(e, "response", None), "status_code", None)
            if code in (503, 500, 429) and versuch < 4:
                time.sleep(wartezeit)
                wartezeit *= 2
                continue
            raise

sh = _mit_wiederholung(gc.open_by_key, "10aL6G3CDr40I5ZREN-z8u4lnE8kenUVHQID0GdkAfzE")
ws = _mit_wiederholung(sh.worksheet, "Tier_Profile")
try:
    if ws.col_count < 40: ws.resize(cols=40)
except: pass
ws_archiv = _mit_wiederholung(sh.worksheet, "Archiv")
ws_inv = _mit_wiederholung(sh.worksheet, "Futter_Inventar")
try:
    if ws_inv.col_count < 23: ws_inv.resize(cols=23)
except: pass
ws_sorten = _mit_wiederholung(sh.worksheet, "Futter_Sorten")
ws_set = _mit_wiederholung(sh.worksheet, "Einstellungen")

try: ws_users = _mit_wiederholung(sh.worksheet, "Master_Nutzer")
except: ws_users = sh.add_worksheet(title="Master_Nutzer", rows="100", cols="8")

try:
    if ws_users.col_count < 8: ws_users.resize(cols=8)
except: pass

try: ws_blacklist = _mit_wiederholung(sh.worksheet, "Blacklist")
except:
    ws_blacklist = sh.add_worksheet(title="Blacklist", rows="200", cols="3")
    ws_blacklist.append_row(["Zoo_Name", "Email", "Datum"])

try: ws_emaillog = _mit_wiederholung(sh.worksheet, "EmailLog")
except:
    ws_emaillog = sh.add_worksheet(title="EmailLog", rows="1000", cols="4")
    ws_emaillog.append_row(["Datum", "Zoo_Name", "Empfänger", "Betreff"])

try: ws_futtererinnerung = _mit_wiederholung(sh.worksheet, "Futter_Erinnerungen_Log")
except:
    ws_futtererinnerung = sh.add_worksheet(title="Futter_Erinnerungen_Log", rows="1000", cols="5")
    ws_futtererinnerung.append_row(["Zoo_Name", "Sorte", "Letzte_Aktualisierung", "Schwelle", "Gesendet_Am"])

try: ws_tagebuch = _mit_wiederholung(sh.worksheet, "Tagebuch")
except:
    ws_tagebuch = sh.add_worksheet(title="Tagebuch", rows="2000", cols="6")
    ws_tagebuch.append_row(["Zoo_Name", "Tier_Name", "Datum", "Eintrag", "Foto_URL", "Titel"])

try: ws_apotheke = _mit_wiederholung(sh.worksheet, "Haustierapotheke")
except:
    ws_apotheke = sh.add_worksheet(title="Haustierapotheke", rows="1000", cols="4")
    ws_apotheke.append_row(["Kategorie", "Titel", "Inhalt", "Erstellt_Am"])

try: ws_app_einstellungen = _mit_wiederholung(sh.worksheet, "App_Einstellungen")
except:
    ws_app_einstellungen = sh.add_worksheet(title="App_Einstellungen", rows="20", cols="2")
    ws_app_einstellungen.append_row(["Key", "Value"])
    ws_app_einstellungen.append_row(["apotheke_banner_aktiv", "ja"])

try: ws_feedback = _mit_wiederholung(sh.worksheet, "Feedback")
except: 
    ws_feedback = sh.add_worksheet(title="Feedback", rows="100", cols="4")
    ws_feedback.append_row(["Datum", "Zoo_Name", "Thema", "Nachricht"])

try: ws_news = _mit_wiederholung(sh.worksheet, "News")
except: 
    ws_news = sh.add_worksheet(title="News", rows="100", cols="3")
    ws_news.append_row(["Datum", "Admin", "Nachricht"])

CACHE = {"inv": (None, []), "tier": (None, []), "set": (None, []), "users": (None, []), "archiv": (None, []), "news": (None, []), "blacklist": (None, []), "emaillog": (None, []), "futtererinnerung": (None, []), "tagebuch": (None, []), "feedback": (None, []), "apotheke": (None, [])}
CACHE_LOCK = threading.Lock()

def get_cached_data(sheet_name):
    with CACHE_LOCK:
        now = datetime.now()
        if CACHE[sheet_name][0] is None or (now - CACHE[sheet_name][0]).total_seconds() > 5:
            try:
                if sheet_name == "inv": CACHE["inv"] = (now, ws_inv.get_all_values())
                elif sheet_name == "tier": CACHE["tier"] = (now, ws.get_all_values())
                elif sheet_name == "set": CACHE["set"] = (now, ws_set.get_all_values())
                elif sheet_name == "users": CACHE["users"] = (now, ws_users.get_all_values())
                elif sheet_name == "archiv": CACHE["archiv"] = (now, ws_archiv.get_all_values())
                elif sheet_name == "news": CACHE["news"] = (now, ws_news.get_all_values())
                elif sheet_name == "blacklist": CACHE["blacklist"] = (now, ws_blacklist.get_all_values())
                elif sheet_name == "emaillog": CACHE["emaillog"] = (now, ws_emaillog.get_all_values())
                elif sheet_name == "futtererinnerung": CACHE["futtererinnerung"] = (now, ws_futtererinnerung.get_all_values())
                elif sheet_name == "tagebuch": CACHE["tagebuch"] = (now, ws_tagebuch.get_all_values())
                elif sheet_name == "feedback": CACHE["feedback"] = (now, ws_feedback.get_all_values())
                elif sheet_name == "apotheke": CACHE["apotheke"] = (now, ws_apotheke.get_all_values())
            except: pass
    return CACHE[sheet_name][1]

def invalidate_cache():
    with CACHE_LOCK:
        CACHE["inv"] = (None, []); CACHE["tier"] = (None, []); CACHE["set"] = (None, []); CACHE["users"] = (None, []); CACHE["archiv"] = (None, []); CACHE["news"] = (None, []); CACHE["blacklist"] = (None, []); CACHE["emaillog"] = (None, []); CACHE["futtererinnerung"] = (None, []); CACHE["tagebuch"] = (None, []); CACHE["feedback"] = (None, []); CACHE["apotheke"] = (None, [])

# ==========================================
# 2. ACCOUNT, LOGIN, NEWS & ADMIN STATS
# ==========================================
def is_zoo(val, current): return str(val).strip().lower() == str(current).strip().lower()

def als_text(wert):
    """Erzwingt Google-Sheets-Textformat (führendes Apostroph), damit führende 0 oder + bei
    Telefonnummern & Co. nicht von Sheets als Zahl/Formel fehlinterpretiert und verloren gehen."""
    wert = str(wert).strip() if wert else ""
    if not wert or wert.startswith("'"): return wert
    return "'" + wert if (wert[0] in ("0", "+")) else wert

def von_text(wert):
    """Entfernt ein evtl. führendes Apostroph beim Anzeigen/Weiterverarbeiten."""
    wert = str(wert) if wert else ""
    return wert[1:] if wert.startswith("'") else wert

def hash_pw(pw): return hashlib.sha256(str(pw).encode()).hexdigest()

def ist_admin(zoo_name):
    if not zoo_name: return False
    if str(zoo_name).strip().lower() == "petbase": return True
    for r in get_cached_data("users"):
        if len(r) > 0 and is_zoo(r[0], zoo_name):
            return len(r) > 7 and str(r[7]).strip().lower() == "ja"
    return False

def publish_news(admin_name, text, auch_per_email):
    if not ist_admin(admin_name): return "❌ Nur der Admin darf News posten!"
    if not text: return "❌ Bitte Text eingeben!"
    datum = datetime.now().strftime("%d.%m.%Y %H:%M")
    ws_news.append_row([datum, admin_name, text])
    invalidate_cache()
    if not auch_per_email:
        return "✅ News erfolgreich veröffentlicht!"
    if not email_konfiguriert():
        return "✅ News veröffentlicht. ⚠️ E-Mail-Versand ist nicht eingerichtet, daher keine E-Mails verschickt."
    versendet, fehler = 0, 0
    for r in get_cached_data("set"):
        if len(r) > 2 and r[2] and "@" in str(r[2]):
            ok, _ = sende_email(r[2], "🐾 PetBase Update", f"Hallo {r[0]},\n\nes gibt ein neues Update in PetBase:\n\n{text}\n\nDiese E-Mail wurde automatisch versendet.", r[0])
            versendet += 1 if ok else 0
            fehler += 0 if ok else 1
    return f"✅ News veröffentlicht! 📧 {versendet} E-Mail(s) versendet" + (f", {fehler} fehlgeschlagen." if fehler else ".")

def get_unread_news(user_name):
    rows = get_cached_data("users")
    last_login = None
    for r in rows:
        if r[0] == user_name:
            if len(r) >= 4 and r[3]: last_login = r[3]
            break
    news_rows = get_cached_data("news")[1:]
    if not last_login: return [n[2] for n in news_rows if len(n) > 2]
    
    try: last_dt = datetime.strptime(last_login, "%d.%m.%Y %H:%M")
    except: return [n[2] for n in news_rows if len(n) > 2]
    
    unread = []
    for n in news_rows:
        try:
            if len(n) > 2 and datetime.strptime(n[0], "%d.%m.%Y %H:%M") > last_dt:
                unread.append(n[2])
        except: pass
    return unread

def get_admin_stats():
    try:
        users = get_cached_data("users")
        start_idx = 1 if len(users) > 0 and "name" in str(users[0][0]).lower() else 0
        total_logins = sum([int(r[4]) for r in users[start_idx:] 
                            if len(r) > 4 and str(r[4]).isdigit() 
                            and str(r[0]).strip().lower() != "petbase"])
        zoo_count = len([r for r in users[start_idx:] 
                         if len(r) > 0 and str(r[0]).strip().lower() != "petbase"])
        return f"<div style='background:#FFFCF5; padding:15px; border-radius:12px; border-left: 5px solid #6D4C41; box-shadow: 0 1px 3px rgba(59,50,38,0.15); color:#3B3226;'><span style='color:#4E342E; font-size:1.2em;'>🐾 App-Reichweite</span><br><br><b>Registrierte Zoos:</b> {zoo_count}<br><b>Gesamt-Logins aller Nutzer:</b> {total_logins}<br><b>Versendete E-Mails gesamt:</b> {email_zaehler()}<br><br>{email_status_text()}</div>"
    except Exception as e: return f"Fehler Statistik: {e}"

def erstelle_feedback_liste():
    """Zeigt alle eingegangenen Feedback-Einträge (neueste zuerst), dieselben
    Angaben wie in der Benachrichtigungs-E-Mail: Von, Thema, Nachricht, Datum."""
    try:
        rows = get_cached_data("feedback")
        start_idx = 1 if len(rows) > 0 and "datum" in str(rows[0][0]).lower() else 0
        eintraege = [r for r in rows[start_idx:] if len(r) >= 4 and str(r[1]).strip()]
        if not eintraege:
            return "<i>Noch kein Feedback eingegangen.</i>"
        html = "<div style='display:flex; flex-direction:column; gap:8px;'>"
        for r in reversed(eintraege):
            datum, von, thema, text = r[0], r[1], r[2], r[3]
            text_html = str(text).replace("\n", "<br>")
            html += f"""<div style='background:#FFFCF5; border-radius:12px; padding:10px 14px; border-left:3px solid #6D4C41; box-shadow: 0 1px 3px rgba(59,50,38,0.12); color:#3B3226;'>
                <div style='display:flex; justify-content:space-between; align-items:baseline;'><b style='font-size:1.02em;'>{von}</b><span style='color:#7A6E5C; font-size:0.82em;'>{datum}</span></div>
                <div style='font-size:0.9em; color:#6D4C41; font-weight:bold; margin-top:2px;'>{thema}</div>
                <div style='font-size:0.9em; line-height:1.4; margin-top:4px;'>{text_html}</div>
                </div>"""
        html += "</div>"
        return html
    except Exception as e:
        return f"<i>Feedback-Liste konnte nicht geladen werden: {e}</i>"

def erstelle_feedback_dropdown_choices():
    """Baut die Auswahlliste für 'Als erledigt markieren' - jede Option beginnt
    mit der tatsächlichen Sheet-Zeilennummer, damit das Löschen auch bei
    inhaltlich identischen Einträgen eindeutig bleibt."""
    try:
        rows = ws_feedback.get_all_values()
        start_idx = 1 if len(rows) > 0 and "datum" in str(rows[0][0]).lower() else 0
        choices = []
        for i, r in enumerate(rows):
            if i < start_idx or len(r) < 4 or not str(r[1]).strip():
                continue
            kurztext = str(r[3])[:40] + ("…" if len(str(r[3])) > 40 else "")
            choices.append(f"Zeile {i + 1} | {r[0]}{r[1]}{r[2]}: {kurztext}")
        return list(reversed(choices))
    except Exception:
        return []

def feedback_als_erledigt(auswahl):
    """Löscht den ausgewählten Feedback-Eintrag aus dem Sheet - erscheint danach
    weder in der Liste noch in der Auswahl. Kein 'Papierkorb', wirklich gelöscht,
    wie vom Nutzer gewünscht."""
    if not auswahl or "Zeile " not in auswahl:
        return "❌ Bitte einen Eintrag auswählen.", gr.update(), gr.update()
    try:
        zeilen_nr = int(auswahl.split("Zeile ")[1].split(" |")[0])
        ws_feedback.delete_rows(zeilen_nr)
        invalidate_cache()
        return "✅ Als erledigt markiert und gelöscht.", erstelle_feedback_liste(), gr.update(choices=erstelle_feedback_dropdown_choices(), value=None)
    except Exception as e:
        return f"❌ Fehler beim Löschen: {str(e)[:80]}", gr.update(), gr.update()

def erstelle_nutzerliste():
    try:
        users = get_cached_data("users")
        start_idx = 1 if len(users) > 0 and "name" in str(users[0][0]).lower() else 0
        set_rows = get_cached_data("set")
        tier_rows = get_cached_data("tier")
        email_map = {}
        for r in set_rows:
            if len(r) > 2: email_map[str(r[0]).strip().lower()] = r[2]
        html = "<div style='display:flex; flex-direction:column; gap:8px;'>"
        for r in users[start_idx:]:
            if len(r) == 0 or not r[0].strip(): continue
            name = r[0].strip()
            if name.lower() == "petbase": continue
            email = email_map.get(name.lower(), "–")
            letzter_login = r[3] if len(r) > 3 and r[3] else "noch nie"
            anzahl_logins = r[4] if len(r) > 4 and r[4] else "0"
            anzahl_tiere = len([t for t in tier_rows if len(t) > 1 and is_zoo(t[0], name) and str(t[1]).strip().lower() != "name"])
            gesperrt = len(r) > 6 and str(r[6]).strip().lower() == "ja"
            admin = len(r) > 7 and str(r[7]).strip().lower() == "ja"
            status_farbe = "#D32F2F" if gesperrt else "#2E7D32"
            status_text = "🚫 Gesperrt" if gesperrt else "✓ Aktiv"
            admin_tag = " · 👑 Admin" if admin else ""
            html += f"""<div style='background:#FFFCF5; border-radius:12px; padding:10px 14px; border-left:3px solid {status_farbe}; box-shadow: 0 1px 3px rgba(59,50,38,0.12); color:#3B3226;'>
                <div style='display:flex; justify-content:space-between; align-items:baseline;'><b style='font-size:1.05em;'>{name}{admin_tag}</b><span style='color:{status_farbe}; font-size:0.9em;'>{status_text}</span></div>
                <div style='font-size:0.85em; color:#7A6E5C; margin-top:2px;'>📧 {email} &nbsp;·&nbsp; 🐾 {anzahl_tiere} Tier(e) &nbsp;·&nbsp; 🔑 {anzahl_logins}x eingeloggt &nbsp;·&nbsp; 🕐 zuletzt: {letzter_login}</div>
                </div>"""
        html += "</div>"
        if start_idx == len(users) or all(not r[0].strip() or r[0].strip().lower() == "petbase" for r in users[start_idx:] if len(r) > 0):
            return "<i>Noch keine registrierten Nutzer außer dir.</i>"
        return html
    except Exception as e:
        return f"<i>Nutzerliste konnte nicht geladen werden: {e}</i>"

def hole_nutzer_gruppen():
    """Gibt vier Namenslisten zurück: aktive (nicht gesperrte), gesperrte,
    nicht-Admin (können ernannt werden) und aktuelle Admins (können entzogen werden)."""
    try:
        users = get_cached_data("users")
        start_idx = 1 if len(users) > 0 and "name" in str(users[0][0]).lower() else 0
        aktive, gesperrte, nicht_admin, admins = [], [], [], []
        for r in users[start_idx:]:
            if len(r) == 0 or not r[0].strip(): continue
            name = r[0].strip()
            if name.lower() == "petbase": continue
            gesperrt = len(r) > 6 and str(r[6]).strip().lower() == "ja"
            admin = len(r) > 7 and str(r[7]).strip().lower() == "ja"
            (gesperrte if gesperrt else aktive).append(name)
            (admins if admin else nicht_admin).append(name)
        return aktive, gesperrte, nicht_admin, admins
    except Exception:
        return [], [], [], []

def nutzer_dropdowns_update():
    aktive, gesperrte, nicht_admin, admins = hole_nutzer_gruppen()
    return (gr.update(choices=aktive or ["Keine aktiven Nutzer"], value=None),
            gr.update(choices=gesperrte or ["Keine gesperrten Nutzer"], value=None),
            gr.update(choices=nicht_admin or ["Keine passenden Nutzer"], value=None),
            gr.update(choices=admins or ["Keine weiteren Admins"], value=None))

def _nutzer_sperr_aktion(admin_name, ziel_name, sperren):
    ungueltig = (None, "Keine aktiven Nutzer", "Keine gesperrten Nutzer")
    if not ist_admin(admin_name): return "❌ Keine Berechtigung.", erstelle_nutzerliste(), *nutzer_dropdowns_update()
    if not ziel_name or ziel_name in ungueltig: return "Bitte einen Nutzer auswählen.", erstelle_nutzerliste(), *nutzer_dropdowns_update()
    if ziel_name.strip().lower() == "petbase": return "❌ Der Hauptadmin kann nicht gesperrt werden.", erstelle_nutzerliste(), *nutzer_dropdowns_update()
    try:
        rows = ws_users.get_all_values()
        for i, r in enumerate(rows):
            if len(r) > 0 and is_zoo(r[0], ziel_name):
                ws_users.update_cell(i + 1, 7, "Ja" if sperren else "Nein")
                invalidate_cache()
                text = f"✅ {ziel_name} wurde {'gesperrt' if sperren else 'entsperrt'}."
                return (text, erstelle_nutzerliste()) + nutzer_dropdowns_update()
        return ("❌ Nutzer nicht gefunden.", erstelle_nutzerliste()) + nutzer_dropdowns_update()
    except Exception as e:
        return (f"❌ Fehler: {e}", erstelle_nutzerliste()) + nutzer_dropdowns_update()

def nutzer_sperren(admin_name, ziel_name): return _nutzer_sperr_aktion(admin_name, ziel_name, True)
def nutzer_entsperren(admin_name, ziel_name): return _nutzer_sperr_aktion(admin_name, ziel_name, False)

def _admin_aktion(admin_name, ziel_name, ernennen):
    ungueltig = (None, "Keine passenden Nutzer", "Keine weiteren Admins")
    if str(admin_name).strip().lower() != "petbase": return ("❌ Nur der Hauptadmin kann Admin-Rechte vergeben oder entziehen.", erstelle_nutzerliste()) + nutzer_dropdowns_update()
    if not ziel_name or ziel_name in ungueltig: return ("Bitte einen Nutzer auswählen.", erstelle_nutzerliste()) + nutzer_dropdowns_update()
    if ziel_name.strip().lower() == "petbase": return ("❌ Der Hauptadmin-Status kann nicht geändert werden.", erstelle_nutzerliste()) + nutzer_dropdowns_update()
    try:
        rows = ws_users.get_all_values()
        for i, r in enumerate(rows):
            if len(r) > 0 and is_zoo(r[0], ziel_name):
                while len(r) < 8: r.append("")
                ws_users.update_cell(i + 1, 8, "Ja" if ernennen else "Nein")
                invalidate_cache()
                text = f"✅ {ziel_name} ist jetzt {'Admin' if ernennen else 'kein Admin mehr'}."
                return (text, erstelle_nutzerliste()) + nutzer_dropdowns_update()
        return ("❌ Nutzer nicht gefunden.", erstelle_nutzerliste()) + nutzer_dropdowns_update()
    except Exception as e:
        return (f"❌ Fehler: {e}", erstelle_nutzerliste()) + nutzer_dropdowns_update()

def admin_ernennen(admin_name, ziel_name): return _admin_aktion(admin_name, ziel_name, True)
def admin_entziehen(admin_name, ziel_name): return _admin_aktion(admin_name, ziel_name, False)

def login_user(zoo_name, pw):
    if not zoo_name or not pw: return "Bitte alles ausfüllen!", ""
    users = get_cached_data("users")
    hashed = hash_pw(pw)
    for r in users:
        if len(r) >= 2 and is_zoo(r[0], zoo_name):
            if r[1] != hashed: return "Falsches Passwort!", ""
            if len(r) > 6 and str(r[6]).strip().lower() == "ja":
                return "🚫 Dieses Konto wurde gesperrt. Bitte wende dich an den Admin.", ""
            return "Login erfolgreich!", r[0].strip()
    return "Zoo-Name nicht gefunden!", ""

def ist_gesperrt(zoo_name, email):
    for r in get_cached_data("blacklist"):
        if len(r) > 0 and is_zoo(r[0], zoo_name): return True
        if len(r) > 1 and email and str(r[1]).strip().lower() == str(email).strip().lower(): return True
    return False

def register_user(zoo_name, pw, email):
    if not zoo_name or not pw: return "Bitte Name und Passwort ausfüllen!", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    if ist_gesperrt(zoo_name, email):
        return "❌ Registrierung nicht möglich. Bitte wende dich an den Admin.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    for r in get_cached_data("users"):
        if len(r) > 0 and is_zoo(r[0], zoo_name): return "Diesen Zoo-Namen gibt es leider schon!", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    try:
        rows = ws_users.get_all_values()
        naechste_zeile = len(rows) + 1
        ws_users.update(range_name=f"A{naechste_zeile}:E{naechste_zeile}", values=[[zoo_name.strip(), hash_pw(pw), "", "", "0"]])
        if email and email.strip():
            set_rows = ws_set.get_all_values()
            naechste_set_zeile = len(set_rows) + 1
            ws_set.update(range_name=f"A{naechste_set_zeile}:C{naechste_set_zeile}", values=[[zoo_name.strip(), "Nein", email.strip()]])
        invalidate_cache()
        # Nach erfolgreicher Registrierung direkt zurück zur Anmeldung springen, mit
        # bereits ausgefülltem Zoo-Namen - ein Klick weniger für den Nutzer.
        return ("✅ Erfolgreich registriert! Du kannst dich jetzt einloggen.", gr.update(value=zoo_name.strip()),
                gr.update(visible=True), gr.update(visible=False), gr.update(variant="primary"), gr.update(variant="secondary"))
    except Exception as e:
        return f"❌ Fehler bei der Registrierung: {str(e)[:80]}", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()

def change_password(zoo_name, old_pw, new_pw):
    if not zoo_name or not old_pw or not new_pw: return "Bitte alle Felder ausfüllen!"
    hashed_old = hash_pw(old_pw)
    rows = ws_users.get_all_values()
    for i, r in enumerate(rows):
        if len(r) >= 2 and is_zoo(r[0], zoo_name):
            if r[1] == hashed_old:
                ws_users.update_cell(i+1, 2, hash_pw(new_pw))
                invalidate_cache()
                return "Passwort erfolgreich geändert!"
            else: return "❌ Altes Passwort ist falsch!"
    return "Fehler beim Ändern."
def delete_account(zoo_name, pw):
    if not zoo_name or not pw: return "Passwort eingeben!", gr.update(), gr.update(), zoo_name
    hashed = hash_pw(pw)
    valid = False
    for r in get_cached_data("users"):
        if len(r) >= 2 and is_zoo(r[0], zoo_name) and r[1] == hashed: valid = True; break
    if not valid: return "Falsches Passwort!", gr.update(), gr.update(), zoo_name
    email = lade_einstellungen(zoo_name)
    try:
        ws_blacklist.append_row([zoo_name.strip(), email.strip() if email else "", datetime.now().strftime("%d.%m.%Y")])
    except: pass
    for sheet_obj in [ws_users, ws, ws_archiv, ws_inv, ws_set]:
        try:
            rows = sheet_obj.get_all_values()
            to_delete = [i+1 for i, r in enumerate(rows) if len(r) > 0 and is_zoo(r[0], zoo_name)]
            for idx in sorted(to_delete, reverse=True): sheet_obj.delete_rows(idx)
        except: pass
    invalidate_cache()
    return "Dein Konto und alle Daten wurden gelöscht.", gr.update(visible=True), gr.update(visible=False), ""

# ==========================================
# 3. SETTINGS, DASHBOARD & FEEDBACK
# ==========================================
def lade_einstellungen(zoo_name):
    if not zoo_name: return ""
    for r in get_cached_data("set"):
        if len(r) >= 3 and is_zoo(r[0], zoo_name): return r[2]
    return ""

def lade_modus(zoo_name):
    if not zoo_name: return False
    for r in get_cached_data("set"):
        if len(r) >= 2 and is_zoo(r[0], zoo_name): return True if str(r[1]).upper() == "JA" else False
    return False

def speichere_einstellungen(zoo_name, email):
    if not zoo_name: return "Fehler: Nicht eingeloggt!"
    rows = ws_set.get_all_values()
    for i, r in enumerate(rows):
        if len(r) > 0 and is_zoo(r[0], zoo_name):
            ws_set.update_cell(i+1, 3, email)
            invalidate_cache()
            return "Gespeichert!"
    ws_set.append_row([zoo_name, "Nein", email])
    invalidate_cache()
    return "Erfolgreich angelegt und gespeichert!"

def submit_feedback(zoo_name, thema, text):
    if not zoo_name: return "Fehler: Nicht eingeloggt!"
    if not thema or not text: return "Bitte Thema wählen und Nachricht schreiben!"
    try:
        rows = ws_feedback.get_all_values()
        naechste_zeile = len(rows) + 1
        ws_feedback.update(range_name=f"A{naechste_zeile}:D{naechste_zeile}", values=[[datetime.now().strftime("%d.%m.%Y %H:%M"), zoo_name, thema, text]])
        invalidate_cache()
        # Admin (PetBase) per E-Mail benachrichtigen, damit Feedback nicht erst beim
        # manuellen Nachschauen im Sheet auffällt. Fehlschlag hier (z.B. keine
        # E-Mail hinterlegt) soll das Feedback selbst nicht verhindern - daher
        # eigener try/except, der die Rückgabe nicht beeinflusst.
        try:
            admin_email = lade_einstellungen("PetBase")
            if admin_email and str(zoo_name).strip().lower() != "petbase":
                betreff = f"🐞 Neues Feedback von {zoo_name}: {thema}"
                body = f"Hallo PetBase,\n\nneues Feedback wurde in PetBase eingereicht:\n\nVon: {zoo_name}\nThema: {thema}\n\n{text}\n\nDiese E-Mail wurde automatisch von deiner PetBase-App versendet."
                sende_email(admin_email, betreff, body, "PetBase")
        except Exception:
            pass
        return "Feedback gesendet! Vielen Dank."
    except Exception as e: return f"Fehler beim Senden: {str(e)[:80]}"

def pruefe_neues_feedback(zoo_name, letzter_stand):
    """Wird periodisch (Timer) aufgerufen, um neu eingereichtes Feedback live als
    Popup anzuzeigen, solange PetBase (Admin) selbst in der App eingeloggt ist -
    Ergänzung zur E-Mail-Benachrichtigung für den Fall, dass der Admin gerade aktiv
    in der App ist. Nur für den Superadmin-Account aktiv."""
    if not zoo_name or str(zoo_name).strip().lower() != "petbase":
        return letzter_stand, ""
    try:
        rows = ws_feedback.get_all_values()
        aktueller_stand = len(rows)
        if letzter_stand is None:
            # Erster Aufruf nach Login: nur Basislinie setzen, kein Popup für
            # bereits vorhandenes/altes Feedback.
            return aktueller_stand, ""
        if aktueller_stand > letzter_stand:
            neue_zeilen = rows[letzter_stand:]
            details = "\n".join([f"• {r[1]}{r[2]}: {r[3][:60]}{'…' if len(r[3]) > 60 else ''}" for r in neue_zeilen if len(r) > 3])
            return aktueller_stand, f"🐞 Neues Feedback erhalten!\n{details}"
        return aktueller_stand, ""
    except Exception:
        return letzter_stand, ""

def erstelle_bestand_karte(zoo_name):
    """Nur die Anzahl-Karte, die verschwindet sobald ein Tier ausgewählt ist."""
    if not zoo_name:
        return "<i>Bitte logge dich ein.</i>"
    try:
        pm = lade_modus(zoo_name)
        tier_rows = [r for r in get_cached_data("tier") if len(r) > 1 and is_zoo(r[0], zoo_name) and str(r[1]).strip().lower() != "name"]
        if not pm:
            tier_rows = [r for r in tier_rows if not (len(r) > 23 and r[23] and r[23] != "Eigenes Tier")]
        anzahl = len(tier_rows)
        return f"<div style='background:#FFFCF5; border-radius:16px; padding:12px 16px; border-left:3px solid #6D4C41; box-shadow: 0 1px 3px rgba(59,50,38,0.12); color:#3B3226;'><span style='color:#7A6E5C;'>🐾 Tiere im Bestand</span><div style='font-size:1.4em; font-weight:bold;'>{anzahl}</div></div>"
    except Exception as e:
        return f"<i>Konnte nicht geladen werden: {e}</i>"

def berechne_futter_warnungen(zoo_name, tier_rows=None):
    """Berechnet je Futtersorte die geschätzten verbleibenden Tage anhand von
    Bestand, täglichem Verbrauch und Zeit seit letzter Aktualisierung. Gemeinsame
    Basis für die Dashboard-Anzeige ('Futter wird knapp') und die automatischen
    E-Mail-Erinnerungen. Gibt eine Liste von
    (tage_rest, sorte, letzte_update_str, hersteller, sackgroesse_g, barcode) zurück,
    sortiert nach tage_rest aufsteigend."""
    if tier_rows is None:
        pm = lade_modus(zoo_name)
        tier_rows = [r for r in get_cached_data("tier") if len(r) > 1 and is_zoo(r[0], zoo_name) and str(r[1]).strip().lower() != "name"]
        if not pm:
            tier_rows = [r for r in tier_rows if not (len(r) > 23 and r[23] and r[23] != "Eigenes Tier")]
    heute = datetime.now()
    inv_rows = [r for r in get_cached_data("inv") if len(r) > 9 and r[8] and r[9] and is_zoo(r[0], zoo_name)]
    bedarf = {}
    for r in tier_rows:
        if len(r) > 16 and str(r[7]).upper() != 'TRUE':
            sorte = r[8].strip()
            try: g_tag = float(str(r[16]).replace(",", "."))
            except: g_tag = 0
            if sorte and g_tag > 0: bedarf[sorte] = bedarf.get(sorte, 0) + g_tag
    ergebnisse = []
    for r in inv_rows:
        sorte = r[8].strip()
        verbr_tag = bedarf.get(sorte, 0)
        if verbr_tag <= 0: continue
        try: gespeichert = float(str(r[9]).replace(",", "."))
        except: gespeichert = 0
        letzte_update_str = str(r[22]).strip() if len(r) > 22 and r[22] else ""
        try: letzte_update = datetime.strptime(letzte_update_str, "%d.%m.%Y")
        except: letzte_update = heute
        tage_vergangen = max(0, (heute - letzte_update).days)
        live_bestand = max(0, gespeichert - tage_vergangen * verbr_tag)
        tage_rest = int(live_bestand / verbr_tag)
        hersteller = str(r[7]).strip() if len(r) > 7 and r[7] else ""
        sackgroesse_g = str(r[21]).strip() if len(r) > 21 and r[21] else ""
        barcode = str(r[6]).strip() if len(r) > 6 and r[6] and str(r[6]).strip().lower() != "unbekannt" else ""
        ergebnisse.append((tage_rest, sorte, letzte_update_str, hersteller, sackgroesse_g, barcode))
    ergebnisse.sort(key=lambda x: x[0])
    return ergebnisse

def erstelle_anstehend_karte(zoo_name):
    """Anstehende Termine + Futter-Warnungen - erscheint unter den Stammdaten des ausgewählten Tieres."""
    if not zoo_name:
        return ""
    try:
        pm = lade_modus(zoo_name)
        tier_rows = [r for r in get_cached_data("tier") if len(r) > 1 and is_zoo(r[0], zoo_name) and str(r[1]).strip().lower() != "name"]
        if not pm:
            tier_rows = [r for r in tier_rows if not (len(r) > 23 and r[23] and r[23] != "Eigenes Tier")]

        heute = datetime.now()
        anstehend = []
        for r in tier_rows:
            name = r[1]
            # Die gespeicherten Felder (Zeile 13-15) sind das Datum der LETZTEN
            # Impfung/Wurmkur/Behandlung, nicht das nächste Fälligkeitsdatum! Vorher
            # wurde das rohe gespeicherte Datum direkt mit heute verglichen, wodurch
            # JEDE Impfung der letzten 10 Jahre fälschlich als "anstehend" auftauchte,
            # sobald sie in der Vergangenheit lag. Jetzt wird - wie schon bei den
            # ICS-Erinnerungen (erstelle_erinnerung) - das nächste Fälligkeitsdatum
            # aus letztem Termin + Intervall berechnet.
            para_int_wert = r[24] if len(r) > 24 and r[24] else "3 Monate"
            intervall_tage = {"Impfung": 365, "Wurmkur": 90, "Parasitenschutz": 365 if para_int_wert == "1 Jahr" else 90}
            for idx, label in [(13, "Impfung"), (14, "Wurmkur"), (15, "Parasitenschutz")]:
                if len(r) > idx and r[idx]:
                    try:
                        letzter_termin = datetime.strptime(str(r[idx]).strip(), "%d.%m.%Y")
                        naechster_termin = letzter_termin + timedelta(days=intervall_tage[label])
                        tage = (naechster_termin - heute).days
                        if -3650 < tage <= 30:
                            anstehend.append((tage, name, label))
                    except Exception:
                        pass
        anstehend.sort(key=lambda x: x[0])

        warnungen_roh = berechne_futter_warnungen(zoo_name, tier_rows=tier_rows)
        warnungen = [(tage_rest, sorte, hersteller, barcode) for tage_rest, sorte, _, hersteller, _, barcode in warnungen_roh if tage_rest <= 14]

        if not anstehend and not warnungen:
            return "<div style='color:#4CAF50; padding:6px 0;'>✓ Aktuell nichts Dringendes – alles im grünen Bereich.</div>"

        html = "<div style='display:flex; flex-direction:column; gap:10px;'>"
        if warnungen:
            html += "<div style='background:#FFFCF5; border-radius:16px; padding:10px 16px; border-left:3px solid #D32F2F; box-shadow: 0 1px 3px rgba(59,50,38,0.12); color:#3B3226;'><b style='color:#D32F2F;'>🥫 Futter wird knapp</b>"
            for tage_rest, sorte, hersteller, barcode in warnungen[:6]:
                # Zwei Preisvergleichs-Links statt einem: Idealo pflegt Barcodes bei
                # Tierfutter oft lückenhaft, eine Barcode-Suche dort schlug meist fehl -
                # daher bei Idealo NUR Hersteller+Sorte als Textsuche. Google Shopping
                # indexiert Barcodes deutlich besser, dort wird er zusätzlich mitgegeben
                # für präzisere Treffer.
                text_query = f"{hersteller} {sorte}".strip()
                idealo_url = f"https://www.idealo.de/preisvergleich/MainSearchProductCategory.html?q={url_quote(text_query)}"
                google_query = f"{text_query} {barcode}".strip() if barcode else text_query
                google_url = f"https://www.google.com/search?tbm=shop&q={url_quote(google_query)}"
                html += f"<div style='padding:4px 0; font-size:0.9em; display:flex; justify-content:space-between; align-items:center;'><span>{sorte}</span><span style='display:flex; align-items:center; gap:8px;'><span style='color:#D32F2F;'>noch ca. {tage_rest} Tage</span><a href='{idealo_url}' target='_blank' rel='noopener' style='color:#6D4C41; text-decoration:none; font-size:0.85em; white-space:nowrap;'>🛒 Idealo</a><a href='{google_url}' target='_blank' rel='noopener' style='color:#6D4C41; text-decoration:none; font-size:0.85em; white-space:nowrap;'>🔍 Google</a></span></div>"
            html += "</div>"

        if anstehend:
            html += "<div style='background:#FFFCF5; border-radius:16px; padding:10px 16px; border-left:3px solid #6D4C41; box-shadow: 0 1px 3px rgba(59,50,38,0.12); color:#3B3226;'><b style='color:#4E342E;'>📅 Anstehend (nächste 30 Tage)</b>"
            for tage, name, label in anstehend[:6]:
                farbe = "#D32F2F" if tage < 0 else ("#EF6C00" if tage <= 7 else "#2E7D32")
                text = f"überfällig seit {abs(tage)} Tagen" if tage < 0 else (f"heute fällig" if tage == 0 else f"in {tage} Tagen")
                html += f"<div style='padding:4px 0; font-size:0.9em; display:flex; justify-content:space-between;'><span>{name}{label}</span><span style='color:{farbe};'>{text}</span></div>"
            html += "</div>"

        html += "</div>"
        return html
    except Exception as e:
        return f"<i>Übersicht konnte nicht geladen werden: {e}</i>"

def erstelle_dashboard(zoo_name):
    """Nur noch für Status-Meldungen zuständig - die eigentliche Anzeige läuft über fuelle_futter_slots."""
    if not zoo_name: return "<i>Bitte logge dich ein.</i>"
    try:
        inv_rows = [r for r in get_cached_data("inv") if len(r) > 0 and is_zoo(r[0], zoo_name)]
        hat_futter = any(len(r) > 9 and r[8] and r[9] for r in inv_rows)
        return "" if hat_futter else "<i>Noch kein Futter im Futter Vorrat.</i>"
    except Exception as e: return f"Dashboard Fehler: {e}"

def fuelle_futter_slots(zoo_name):
    """Liefert für jeden der FUTTER_SLOT_ANZAHL Slots: (sichtbar, Karten-HTML, Sortenname)."""
    leer = [(gr.update(visible=False), "", "")] * FUTTER_SLOT_ANZAHL
    if not zoo_name: return [x for tpl in leer for x in tpl]
    try:
        pm = lade_modus(zoo_name)
        inv_rows = [r for r in get_cached_data("inv") if len(r) > 0 and is_zoo(r[0], zoo_name)]
        tier_rows = [r for r in get_cached_data("tier") if len(r) > 0 and is_zoo(r[0], zoo_name)]
        heute = datetime.now()

        bedarf = {}
        for r in tier_rows:
            if len(r) > 1 and str(r[1]).strip().lower() != "name":
                if len(r) > 16 and str(r[7]).upper() != 'TRUE':
                    kat = r[23] if len(r) > 23 and r[23] else "Eigenes Tier"
                    if not pm and kat != "Eigenes Tier": continue
                    sorte = r[8].strip()
                    try: g_tag = float(str(r[16]).replace(",", "."))
                    except: g_tag = 0
                    if sorte and g_tag > 0: bedarf[sorte] = bedarf.get(sorte, 0) + g_tag

        posten = {}
        for r in inv_rows:
            if len(r) > 9 and r[8] and r[9]:
                sorte, art = r[8].strip(), str(r[19]).strip() if len(r) > 19 and r[19] else "Unkategorisiert"
                try: gespeichert = float(str(r[9]).replace(",", "."))
                except: gespeichert = 0
                try: sackgroesse = float(str(r[21]).replace(",", ".")) if len(r) > 21 and r[21] else gespeichert
                except: sackgroesse = gespeichert
                letzte_update_str = str(r[22]).strip() if len(r) > 22 and r[22] else ""
                try: letzte_update = datetime.strptime(letzte_update_str, "%d.%m.%Y")
                except: letzte_update = heute
                posten[sorte] = {"art": art, "gespeichert": gespeichert, "sackgroesse": sackgroesse, "letzte_update": letzte_update}

        # Sortiert nach Kategorie, dann Sortenname
        sortierte_sorten = sorted(posten.keys(), key=lambda s: (posten[s]["art"], s))

        ergebnisse = []
        letzte_art = None
        for i in range(FUTTER_SLOT_ANZAHL):
            if i < len(sortierte_sorten):
                sorte = sortierte_sorten[i]
                info = posten[sorte]
                verbr_tag = bedarf.get(sorte, 0)
                tage_vergangen = max(0, (heute - info["letzte_update"]).days)
                live_bestand = max(0, info["gespeichert"] - tage_vergangen * verbr_tag)
                sack_kg = info["sackgroesse"] / 1000
                bestand_kg = live_bestand / 1000
                if verbr_tag > 0:
                    tage_rest = int(live_bestand / verbr_tag)
                    status_color = "#2E7D32" if tage_rest > 14 else ("#EF6C00" if tage_rest > 7 else "#D32F2F")
                    tage_text = f"noch <b>{tage_rest} Tage</b>"
                    percent = min(100, (tage_rest/30)*100)
                else:
                    status_color = "#9E9E9E"; tage_text = "kein Verbrauch"; percent = 0
                art_header = ""
                if info["art"] != letzte_art:
                    art_header = f"<div style='color:#4E342E; font-size:0.85em; font-weight:bold; margin:6px 0 2px 2px;'>{info['art']}</div>"
                    letzte_art = info["art"]
                karte = (
                    f"{art_header}"
                    f"<div style='border:1px solid #E6DAC4; border-radius:12px; padding:6px 10px; background:#FFFCF5; color:#3B3226; font-size:0.85em; box-shadow: 0 1px 2px rgba(59,50,38,0.10);'>"
                    f"<div style='display:flex; justify-content:space-between; align-items:baseline; flex-wrap:wrap; gap:2px 8px;'>"
                    f"<b style='font-size:1em;'>{sorte}</b>"
                    f"<span style='color:{status_color};'>{tage_text}</span>"
                    f"</div>"
                    f"<div style='display:flex; justify-content:space-between; flex-wrap:wrap; gap:2px 8px; color:#7A6E5C; margin-top:1px; font-size:0.9em;'>"
                    f"<span>Sack: {sack_kg:.2f} kg</span>"
                    f"<span>Bestand: <b>{bestand_kg:.2f} kg</b></span>"
                    f"</div>"
                    f"<div style='width:100%; background:#E6DAC4; border-radius:3px; height:4px; overflow:hidden; margin-top:3px;'>"
                    f"<div style='width:{percent}%; height:100%; background:{status_color};'></div></div>"
                    f"</div>"
                )
                ergebnisse.append((gr.update(visible=True), karte, sorte))
            else:
                ergebnisse.append((gr.update(visible=False), "", ""))
        return [x for tpl in ergebnisse for x in tpl]
    except Exception:
        return [x for tpl in leer for x in tpl]

# ==========================================
# 4. OCR SCANNER ENGINE & INVENTAR
# ==========================================
def resize_for_ocr(bild_np, target_width=1800):
    if bild_np is None: return None
    h, w = bild_np.shape[:2]
    scale = target_width / w
    new_w = int(w * scale)
    new_h = int(h * scale)
    return cv2.resize(bild_np, (new_w, new_h), interpolation=cv2.INTER_CUBIC if scale > 1 else cv2.INTER_AREA)

def optimiere_bild_fuer_ocr(bild_np):
    try:
        grau = cv2.cvtColor(bild_np, cv2.COLOR_RGB2GRAY)
        adaptiv = cv2.adaptiveThreshold(grau, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 51, 15)
        _, otsu = cv2.threshold(grau, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        return grau, adaptiv, otsu
    except:
        g = cv2.cvtColor(bild_np, cv2.COLOR_RGB2GRAY) if bild_np is not None else None
        return g, g, g

def scanne_und_suche(b1, b2, b3):
    bilder = [b for b in [b1, b2, b3] if b is not None]
    if not bilder: return "Kein Bild", "", "", "0", "Unbekannt", "", "", "", "", "", "", "", "", "Bitte Bild laden."
    try:
        bc, combined_text = "Unbekannt", ""

        for raw_bild in bilder:
            if bc != "Unbekannt": break
            bc_bild = resize_for_ocr(raw_bild, target_width=2000)
            barcodes = decode(bc_bild)
            if barcodes:
                bc = barcodes[0].data.decode('utf-8')
            else:
                grau_bc = cv2.cvtColor(bc_bild, cv2.COLOR_RGB2GRAY)
                grau_bc = cv2.equalizeHist(grau_bc)
                barcodes = decode(grau_bc)
                if barcodes:
                    bc = barcodes[0].data.decode('utf-8')

        for raw_bild in bilder:
            bild = resize_for_ocr(raw_bild, target_width=1800)
            grau, adaptiv, otsu = optimiere_bild_fuer_ocr(bild)
            text_pool = []

            for img_variant in [grau, adaptiv, otsu]:
                if img_variant is None: continue
                pil_img = PILImage.fromarray(img_variant)
                t3 = pytesseract.image_to_string(pil_img, config='--psm 3 -l deu+eng')
                if len(t3.strip()) > 15: text_pool.append(t3)
                t11 = pytesseract.image_to_string(pil_img, config='--psm 11 -l deu+eng')
                if len(t11.strip()) > 15: text_pool.append(t11)

            best_segment = ""
            for t in text_pool:
                if len(t) > len(best_segment):
                    best_segment = t
            combined_text += best_segment + "\n" + "\n".join(text_pool) + "\n--\n"

        clean_text_for_weight = re.sub(r'\b\d+\s*-\s*\d+\s*(?:kg|g)\b', '', combined_text, flags=re.IGNORECASE)
        gew_matches = re.findall(r"(?<![\-\+])\b([0-9.,]+)\s*(kg|g)\b(?![\s*\+\-])", clean_text_for_weight, re.IGNORECASE)
        gewicht = "0"

        if gew_matches:
            kandidaten = []
            for wert_str, einheit in gew_matches:
                wert_clean = wert_str.replace(",", ".").strip()
                try:
                    wert = float(wert_clean)
                    in_g = int(wert * 1000) if einheit.lower() == 'kg' else int(wert)
                    if 50 <= in_g <= 30000:
                        kandidaten.append(in_g)
                except: continue

            if kandidaten:
                gewicht = str(max(kandidaten))
            else:
                try:
                    wert_str, einheit = gew_matches[0]
                    wert = float(wert_str.replace(",", ".").strip())
                    if einheit.lower() == 'kg' or wert < 50.0:
                        gewicht = str(int(wert * 1000))
                    else:
                        gewicht = str(int(wert))
                except: pass

        lower_text = combined_text.lower()
        phase = "Unbekannt"
        if re.search(r"\b(junior|welpe|puppy|young)\b", lower_text): phase = "Junior / Welpe"
        elif re.search(r"\b(senior|older|ageing)\b", lower_text): phase = "Senior"
        elif re.search(r"\b(adult|erwachsen)\b", lower_text): phase = "Adult"
        elif re.search(r"\b(all life stages|all ages)\b", lower_text): phase = "All Ages"

        lines = combined_text.split("\n")
        table_pairs = []
        for line in lines:
            line_clean = re.sub(r'\s+-\s+', '-', line)
            line_clean = re.sub(r'\s*,\s*', ',', line_clean)
            kg_match = re.search(r"(\d+[\.,]?\d*-\d+[\.,]?\d*)\s*kg", line_clean, re.IGNORECASE)
            if not kg_match: kg_match = re.search(r"\b(\d+[\.,]?\d*)\s*kg", line_clean, re.IGNORECASE)
            if kg_match:
                kg_val = kg_match.group(1).replace(" ", "")
                g_matches = re.findall(r"\b(\d+-\d+)\s*g\b", line_clean, re.IGNORECASE)
                if g_matches: table_pairs.append((kg_val, g_matches[0]))
                else:
                    g_singles = re.findall(r"\b(\d+)\s*g\b", line_clean, re.IGNORECASE)
                    if len(g_singles) >= 2: table_pairs.append((kg_val, f"{g_singles[0]}-{g_singles[1]}"))
                    elif len(g_singles) == 1: table_pairs.append((kg_val, g_singles[0]))

        k1, g1, k2, g2, k3, g3, k4, g4 = "", "", "", "", "", "", "", ""
        einzigartige_paare = []
        gesehene_kgs = set()
        for k, g in table_pairs:
            if k not in gesehene_kgs:
                gesehene_kgs.add(k)
                einzigartige_paare.append((k, g))

        if einzigartige_paare:
            if len(einzigartige_paare) > 0: k1, g1 = einzigartige_paare[0]
            if len(einzigartige_paare) > 1: k2, g2 = einzigartige_paare[1]
            if len(einzigartige_paare) > 2: k3, g3 = einzigartige_paare[2]
            if len(einzigartige_paare) > 3: k4, g4 = einzigartige_paare[3]

        nutzbarer_text = combined_text.replace("--", "").strip()
        bereinigte_zeilen = [l.strip() for l in nutzbarer_text.split('\n') if len(l.strip()) > 10]
        vorschau = "\n".join(bereinigte_zeilen[:12]) + "\n..." if len(bereinigte_zeilen) > 12 else "\n".join(bereinigte_zeilen)

        if len(nutzbarer_text) < 40:
            vorschau = "⚠️ Nur wenig Text erkannt – bitte Foto bei besserem Licht/schärfer/gerader aufnehmen.\n\n" + vorschau

        return bc, "", "", gewicht, phase, k1, g1, k2, g2, k3, g3, k4, g4, vorschau
    except Exception as e: return "Fehler", "", "", "0", "Unbekannt", "", "", "", "", "", "", "", "", str(e)

def parse_zu_gramm(wert_str):
    if not wert_str: return "0"
    try:
        wert = float(str(wert_str).replace(",", ".").strip())
        if wert < 50.0: return str(int(wert * 1000))
        return str(int(wert))
    except: return "0"
def speichere_inventar(zoo_name, bc, hersteller, sorte, gewicht, phase, art, futtertyp, k1, g1, k2, g2, k3, g3, k4, g4):
    if not zoo_name: return "Fehler: Nicht eingeloggt!", gr.update(), gr.update(), gr.update(), gr.update()
    row = [""] * 23
    row[0] = zoo_name
    row[6], row[7], row[8], row[9] = str(bc), str(hersteller), str(sorte).strip(), parse_zu_gramm(gewicht)
    row[10], row[11] = str(k1), str(g1); row[12], row[13] = str(k2), str(g2); row[14], row[15] = str(k3), str(g3)
    row[16], row[17] = str(k4), str(g4); row[18], row[19] = str(phase), str(art)
    row[20] = str(futtertyp) if futtertyp else "Trockenfutter"
    row[21] = row[9]  # Sackgröße = Referenzwert, bleibt bei Aufstocken unverändert
    row[22] = datetime.now().strftime("%d.%m.%Y")  # Zeitstempel für Live-Bestandsberechnung
    # Race-sicher: append_row() statt "Zeile zählen, dann fest dorthin schreiben" -
    # append_row() selbst ist an anderer Stelle (Tier_Profile, siehe v7.73.17)
    # nachweislich gelegentlich unzuverlässig gewesen, daher zusätzlich eine
    # Rücklese-Prüfung mit Warnhinweis als Sicherheitsnetz (gleiches Muster wie
    # bei Tagebuch/Haustierapotheke, siehe v7.88.3).
    ws_inv.append_row(row[:23])
    invalidate_cache()
    gefunden = False
    for versuch in range(3):
        if versuch > 0: time.sleep(1.5)
        for r in ws_inv.get_all_values():
            if len(r) >= 10 and r[0] == zoo_name and r[7] == str(hersteller) and r[8] == str(sorte).strip():
                gefunden = True
                break
        if gefunden:
            break
    d = update_dropdowns(zoo_name)
    status = f"Erfolgreich gespeichert: {sorte} ({row[9]}g)" if gefunden else f"⚠️ Konnte nicht sicher bestätigt werden: {sorte} - bitte in der Futter-Zentrale prüfen und ggf. erneut speichern."
    return status, d[5], d[6], d[7], d[8]
    
# ==========================================
# 5. INVENTAR DROPDOWNS & DATEN-UPDATES
# ==========================================
def load_futter_edit(zoo_name, sorte):
    if not zoo_name or not sorte or sorte == "Hier Auswählen": return [gr.update(value="")] * 15
    try:
        raw = [r for r in get_cached_data("inv") if len(r) > 0 and is_zoo(r[0], zoo_name)]
        s = str(sorte).strip()
        for r in raw:
            if len(r) > 8 and r[8].strip() == s:
                while len(r) < 23: r.append("")
                art_val = str(r[19]) if len(r) > 19 and str(r[19]) else "Hundefutter"
                futtertyp_val = str(r[20]) if len(r) > 20 and str(r[20]) else "Trockenfutter"
                sackgroesse_val = str(r[21]) if len(r) > 21 and r[21] else str(r[9])
                return [gr.update(value=str(r[7])), gr.update(value=str(r[8])), gr.update(value=str(r[9])), gr.update(value=sackgroesse_val), gr.update(value=str(r[18])), gr.update(value=art_val), gr.update(value=futtertyp_val), gr.update(value=str(r[10])), gr.update(value=str(r[11])), gr.update(value=str(r[12])), gr.update(value=str(r[13])), gr.update(value=str(r[14])), gr.update(value=str(r[15])), gr.update(value=str(r[16])), gr.update(value=str(r[17]))]
        return [gr.update(value="Sorte nicht gefunden!")] + [gr.update(value="")] * 14
    except Exception as e: return [gr.update(value=f"Fehler: {str(e)[:20]}")] + [gr.update(value="")] * 14

def save_futter_edit(zoo_name, alt_sorte, hersteller, sorte, gewicht, sackgroesse, phase, art, futtertyp, k1, g1, k2, g2, k3, g3, k4, g4):
    if not zoo_name or not alt_sorte or alt_sorte == "Hier Auswählen": return "Kein Futter gewählt!", gr.update(), gr.update(), erstelle_dashboard(zoo_name), gr.update()
    try:
        raw = ws_inv.get_all_values()
        s = str(alt_sorte).strip()
        for i, r in enumerate(raw):
            if len(r) > 8 and is_zoo(r[0], zoo_name) and r[8].strip() == s:
                while len(r) < 23: r.append("")
                r[7], r[8], r[9] = str(hersteller), str(sorte).strip(), parse_zu_gramm(gewicht)
                r[10], r[11] = str(k1), str(g1); r[12], r[13] = str(k2), str(g2); r[14], r[15] = str(k3), str(g3)
                r[16], r[17] = str(k4), str(g4); r[18], r[19] = str(phase), str(art)
                r[20] = str(futtertyp) if futtertyp else "Trockenfutter"
                r[21] = str(parse_zu_gramm(sackgroesse)) if sackgroesse else r[9]
                r[22] = datetime.now().strftime("%d.%m.%Y")  # manuelle Korrektur -> Live-Zähler neu starten
                ws_inv.update(range_name=f"A{i+1}:W{i+1}", values=[r[:23]])
                invalidate_cache()
                d = update_dropdowns(zoo_name)
                return f"Erfolgreich aktualisiert: {sorte}", d[7], d[6], erstelle_dashboard(zoo_name), d[8]
        return "Fehler: Sorte nicht gefunden.", gr.update(), gr.update(), erstelle_dashboard(zoo_name), gr.update()
    except Exception as e: return f"Fehler: {str(e)[:30]}", gr.update(), gr.update(), erstelle_dashboard(zoo_name), gr.update()

def delete_futter_edit(zoo_name, sorte):
    if not zoo_name or not sorte or sorte == "Hier Auswählen": return "Kein Futter gewählt!", gr.update(), gr.update(), erstelle_dashboard(zoo_name), gr.update()
    try:
        raw = ws_inv.get_all_values()
        s = str(sorte).strip()
        for i, r in enumerate(raw):
            if len(r) > 8 and is_zoo(r[0], zoo_name) and r[8].strip() == s:
                ws_inv.delete_rows(i+1)
                invalidate_cache()
                d = update_dropdowns(zoo_name)
                return f"Erfolgreich gelöscht: {sorte}", d[7], d[6], erstelle_dashboard(zoo_name), d[8]
        return "Fehler: Sorte nicht gefunden.", gr.update(), gr.update(), erstelle_dashboard(zoo_name), gr.update()
    except Exception as e: return f"Fehler beim Löschen: {str(e)[:30]}", gr.update(), gr.update(), erstelle_dashboard(zoo_name), gr.update()

def schnell_aufstocken(zoo_name, sorte, menge):
    if not zoo_name or not sorte or sorte == "Hier Auswählen" or not menge or menge <= 0: return erstelle_dashboard(zoo_name), gr.update()
    try:
        raw = ws_inv.get_all_values()
        s = str(sorte).strip()
        for i, r in enumerate(raw):
            if len(r) > 8 and is_zoo(r[0], zoo_name) and r[8].strip() == s:
                while len(r) < 23: r.append("")
                alt_menge = float(str(r[9]).replace(",", ".")) if len(r) > 9 and r[9] else 0
                eingegebene_menge = float(menge)
                if eingegebene_menge < 50.0: eingegebene_menge *= 1000
                neu_menge = alt_menge + eingegebene_menge
                ws_inv.update_cell(i+1, 10, str(int(neu_menge) if neu_menge.is_integer() else neu_menge))
                ws_inv.update_cell(i+1, 23, datetime.now().strftime("%d.%m.%Y"))  # Zeitstempel zurücksetzen
                invalidate_cache()
                return erstelle_dashboard(zoo_name), gr.update(value="Hier Auswählen")
        return erstelle_dashboard(zoo_name), gr.update()
    except Exception as e: return erstelle_dashboard(zoo_name), gr.update()

def inline_aufstocken(zoo_name, sorte, menge):
    """Für die Inline-Buttons direkt an der Futter-Karte: bucht die Menge und aktualisiert Dashboard + Slots."""
    schnell_aufstocken(zoo_name, sorte, menge)
    return (erstelle_dashboard(zoo_name),) + tuple(fuelle_futter_slots(zoo_name))

def hole_bestand_sackgroesse(zoo_name, sorte):
    """Liefert aktuellen Lagerbestand und Sackgröße (g) für das Inline-Formular."""
    if not zoo_name or not sorte: return "", ""
    try:
        rows = get_cached_data("inv")
        for r in rows:
            if len(r) > 8 and is_zoo(r[0], zoo_name) and r[8].strip() == str(sorte).strip():
                bestand = str(r[9]) if len(r) > 9 and r[9] else ""
                sack = str(r[21]) if len(r) > 21 and r[21] else bestand
                return bestand, sack
    except Exception: pass
    return "", ""

def inline_bestand_setzen(zoo_name, sorte, bestand, sackgroesse):
    """Setzt Lagerbestand und Sackgröße direkt (absolute Werte, kein Aufaddieren) und aktualisiert Dashboard + Slots."""
    if zoo_name and sorte:
        try:
            raw = ws_inv.get_all_values()
            s = str(sorte).strip()
            for i, r in enumerate(raw):
                if len(r) > 8 and is_zoo(r[0], zoo_name) and r[8].strip() == s:
                    while len(r) < 23: r.append("")
                    ws_inv.update_cell(i+1, 10, str(bestand))
                    ws_inv.update_cell(i+1, 22, str(sackgroesse))
                    ws_inv.update_cell(i+1, 23, datetime.now().strftime("%d.%m.%Y"))
                    invalidate_cache()
                    break
        except Exception: pass
    return (erstelle_dashboard(zoo_name),) + tuple(fuelle_futter_slots(zoo_name))

def inline_loeschen(zoo_name, sorte):
    """Löscht eine Futtersorte komplett aus dem Bestand und aktualisiert Dashboard + Slots."""
    if zoo_name and sorte:
        delete_futter_edit(zoo_name, sorte)
    return (erstelle_dashboard(zoo_name),) + tuple(fuelle_futter_slots(zoo_name))

def inline_mehr_speichern(zoo_name, alt_sorte, herst, sorte, gew, sack, phase, art, futtertyp, k1, g1, k2, g2, k3, g3, k4, g4):
    """Speichert die erweiterten Detail-Felder (Hersteller, Fütterungsart, Gewichts-Brackets) für eine Sorte."""
    if zoo_name and alt_sorte:
        save_futter_edit(zoo_name, alt_sorte, herst, sorte, gew, sack, phase, art, futtertyp, k1, g1, k2, g2, k3, g3, k4, g4)
    return (erstelle_dashboard(zoo_name),) + tuple(fuelle_futter_slots(zoo_name))

BARF_PROZENT_TABELLE = {
    "Welpe (bis 6 Monate)": 0.06,
    "Junghund (6-12 Monate)": 0.035,
    "Erwachsen (Normalaktiv)": 0.025,
    "Sportlich / Sehr aktiv": 0.035,
    "Senior (Wenig aktiv)": 0.015,
    "Übergewichtig (Diät)": 0.0125,
    "Trächtig (ab 5. Woche)": 0.035,
    "Laktierend (Säugend)": 0.05,
    "Kastriert (Neigung zu Übergewicht)": 0.0175,
}

def get_barf_typen(zoo_name): return ["Hier Auswählen"] + list(BARF_PROZENT_TABELLE.keys())
def get_trockenfutter_sorten(zoo_name): return ["Hier Auswählen"] + [r[8].strip() for r in get_cached_data("inv")[1:] if len(r) > 8 and is_zoo(r[0], zoo_name) and r[8].strip()]

def update_dropdowns(zoo_name, select_name=None):
    if not zoo_name: return (gr.update(),)*9
    try:
        pm = lade_modus(zoo_name)
        ws_data = get_cached_data("tier")
        own_names, foster_names, all_visible_names = [], [], []
        for r in ws_data:
            if len(r) > 1 and str(r[1]).strip() and str(r[1]).strip().lower() != "name" and is_zoo(r[0], zoo_name):
                kat = r[23] if len(r) > 23 and r[23] else "Eigenes Tier"
                if kat == "Eigenes Tier":
                    own_names.append(r[1]); all_visible_names.append(r[1])
                else:
                    foster_names.append(r[1])
                    if pm: all_visible_names.append(r[1])
        archiv_names = []
        for r in get_cached_data("archiv"):
            if len(r) > 1 and str(r[1]).strip() and str(r[1]).strip().lower() != "name" and is_zoo(r[0], zoo_name):
                kat = r[23] if len(r) > 23 and r[23] else "Eigenes Tier"
                if pm or kat == "Eigenes Tier": archiv_names.append(r[1])
        barf, trocken = get_barf_typen(zoo_name), get_trockenfutter_sorten(zoo_name)
        # Tier-Dropdown: choices UND value explizit zusammen setzen. Bei nur choices ohne
        # value versucht Gradio den bisherigen (evtl. veralteten) Client-Wert beizubehalten -
        # passt der nicht mehr zur frischen Liste, wirft Gradio serverseitig einen
        # ValueError ("X is not in the list of choices"), der die komplette Server-Antwort
        # abbrechen kann, obwohl der eigentliche Sheet-Schreibvorgang schon gelaufen war.
        tier_choices = ["Hier Auswählen", "Neues Tier..."] + all_visible_names
        if select_name and select_name in tier_choices:
            tier_dd_update = gr.update(choices=tier_choices, value=select_name)
        else:
            tier_dd_update = gr.update(choices=tier_choices, value="Hier Auswählen")
        return (tier_dd_update, gr.update(choices=all_visible_names), gr.update(choices=["Hier Auswählen"] + own_names), gr.update(choices=["Hier Auswählen"] + foster_names), gr.update(choices=["Hier Auswählen"] + archiv_names), gr.update(choices=barf), gr.update(choices=trocken), gr.update(choices=trocken), gr.update(choices=trocken))
    except: return (gr.update(),)*9

def change_modus(zoo_name, is_aktiv):
    if not zoo_name: return (gr.update(),)*21
    rows = ws_set.get_all_values()
    found = False
    for i, r in enumerate(rows):
        if len(r) > 0 and is_zoo(r[0], zoo_name):
            ws_set.update_cell(i+1, 2, "Ja" if is_aktiv else "Nein")
            found = True; break
    if not found: ws_set.append_row([zoo_name, "Ja" if is_aktiv else "Nein", ""])
    invalidate_cache()
    pm = is_aktiv
    d = update_dropdowns(zoo_name)
    titel_text = "### 📂 Dokument / Verträge / Tierarztunterlagen" if pm else "### 📂 Tierunterlagen"
    choices = ["Bitte wählen...", "Pflegevertrag", "Vermittlungsvertrag", "Tierarzt Dokumente (1)", "Tierarzt Dokumente (2)", "Tierarzt Dokumente (3)", "Tierarzt Dokumente (4)", "Tierarzt Dokumente (5)", "Tierarzt Dokumente (6)", "Alle Dokumente anzeigen"] if pm else ["Bitte wählen...", "Tierarzt Dokumente (1)", "Tierarzt Dokumente (2)", "Tierarzt Dokumente (3)", "Tierarzt Dokumente (4)", "Tierarzt Dokumente (5)", "Tierarzt Dokumente (6)", "Alle Dokumente anzeigen"]
    return [gr.update(visible=False), gr.update(visible=pm), gr.update(visible=pm), gr.update(visible=pm)] + list(d) + [gr.update(value=titel_text), gr.update(choices=choices, value="Bitte wählen..."), d[0], d[3], d[4], gr.update(visible=pm), gr.update(visible=pm), gr.update(visible=pm)]

def filter_tiere(zoo_name, filter_val):
    try:
        pm = lade_modus(zoo_name)
        filtered = ["Hier Auswählen", "Neues Tier..."]
        for r in get_cached_data("tier"):
            if len(r) > 1 and str(r[1]).strip() and str(r[1]).strip().lower() != "name" and is_zoo(r[0], zoo_name):
                kat = r[23] if len(r) > 23 and r[23] else "Eigenes Tier"
                if not pm and kat != "Eigenes Tier": continue
                if filter_val == "🐾 Alle" or (filter_val == "🏠 Eigene Tiere" and kat == "Eigenes Tier") or (filter_val == "🧳 Pflege & Gäste" and kat in ["Pflegetier", "Urlaubsgast"]): filtered.append(r[1])
        return gr.update(choices=filtered, value="Hier Auswählen")
    except: return gr.update()

# ==========================================
# 6. DOKUMENTE, TIERPROFILE & PDF EXPORT
# ==========================================
def get_file_hash(filepath):
    if not filepath or not os.path.exists(filepath): return ""
    with open(filepath, "rb") as f: return hashlib.md5(f.read()).hexdigest()

def convert_pdf_to_jpgs(pdf_path):
    if not pdf_path or not str(pdf_path).lower().endswith('.pdf'): return [pdf_path]
    try:
        doc = fitz.open(pdf_path)
        pfade = []
        for page_num in range(len(doc)):
            page = doc.load_page(page_num)
            pix = page.get_pixmap(dpi=150)
            tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".jpg")
            pix.save(tmp.name)
            pfade.append(tmp.name)
        return pfade if pfade else [pdf_path]
    except Exception: return [pdf_path]

def upload_image_to_cloud(filepath):
    if not filepath or str(filepath).startswith("http"): return filepath
    try:
        seiten = convert_pdf_to_jpgs(filepath) if str(filepath).lower().endswith('.pdf') else [filepath]
        urls = []
        for idx, seite in enumerate(seiten):
            url = None
            for versuch in range(3):
                try:
                    with open(seite, 'rb') as f:
                        r = requests.post('https://freeimage.host/api/1/upload', data={'key': '6d207e02198a847aa98d0a2a901485a5', 'action': 'upload'}, files={'source': f}, timeout=15)
                    if r.status_code == 200:
                        url = r.json()['image']['url']
                        break
                except Exception:
                    pass
                time.sleep(2)  # kurze Pause vor erneutem Versuch, falls Rate-Limit die Ursache war
            if url: urls.append(url)
            if idx < len(seiten) - 1:
                time.sleep(1.2)  # Pause zwischen den Seiten, um Rate-Limiting von vornherein zu vermeiden
        return ",".join(urls)
    except Exception: pass
    return ""

def cloud_helper(img, hidden_str):
    h_str = str(hidden_str) if hidden_str else ""
    alte_url = h_str.split("|")[0] if "|" in h_str else ""
    alter_hash = h_str.split("|")[1] if "|" in h_str and len(h_str.split("|")) > 1 else None
    if not img:
        return alte_url
    # WICHTIG (v7.91.1): load_doc() lädt bei bestehenden, mehrseitigen Dokumenten
    # aus Performance-Gründen nur die ERSTE Seite lokal herunter, um sie als
    # Vorschau anzuzeigen - die vollständige, ursprüngliche URL-Liste (alle Seiten)
    # bleibt nur im versteckten Textfeld erhalten. Ohne Hash-Vergleich wurde diese
    # reine Vorschau-Datei bei JEDEM Speichern (auch wenn nur z.B. das
    # Allergien-Feld geändert wurde) fälschlich als "neu hochgeladen" behandelt und
    # hat damit die übrigen Seiten 2-6 stillschweigend gelöscht. Nur wenn sich der
    # Datei-Inhalt tatsächlich vom zuletzt bekannten Stand unterscheidet, wird
    # wirklich neu hochgeladen.
    aktueller_hash = get_file_hash(img)
    if alter_hash and aktueller_hash == alter_hash:
        return alte_url
    return upload_image_to_cloud(img)

def load_doc(raw_url):
    if not raw_url or not raw_url.startswith("http"): return gr.update(value=None), ""
    erste_url = raw_url.split(",")[0].strip()
    try:
        resp = requests.get(erste_url, headers={"User-Agent": "Mozilla/5.0"}, timeout=3)
        if resp.status_code == 200:
            tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".jpg")
            tmp.write(resp.content); tmp.close()
            return gr.update(value=tmp.name), f"{raw_url}|{get_file_hash(tmp.name)}"
    except: pass
    return gr.update(value=None), ""

def nachtrag_archiv_vertrag(zoo_name, tier_name, vertrag_typ, img_path):
    if not zoo_name: return "❌ Bitte einloggen!"
    if not tier_name or tier_name == "Hier Auswählen": return "❌ Bitte archiviertes Tier auswählen!"
    if not img_path: return "❌ Bitte ein Vertragsdokument hochladen!"
    try:
        cloud_url = cloud_helper(img_path, "")
        if not cloud_url: return "❌ Fehler beim Cloud-Upload."
        rows = ws_archiv.get_all_values()
        for i, r in enumerate(rows):
            if len(r) > 1 and is_zoo(r[0], zoo_name) and str(r[1]).strip() == str(tier_name).strip():
                col_idx = 27 if vertrag_typ == "Pflegevertrag" else 28
                ws_archiv.update_cell(i + 1, col_idx, cloud_url)
                invalidate_cache()
                return f"✅ {vertrag_typ} erfolgreich hinzugefügt!"
        return "❌ Tier im Archiv nicht gefunden."
    except Exception as e: return f"❌ Fehler: {str(e)[:30]}"

def nachtrag_aktiv_vertrag(zoo_name, tier_name, vertrag_typ, img_path):
    if not zoo_name: return "❌ Bitte einloggen!"
    if not tier_name or tier_name == "Hier Auswählen": return "❌ Bitte Tier auswählen!"
    if not img_path: return "❌ Bitte ein Vertragsdokument hochladen!"
    try:
        cloud_url = cloud_helper(img_path, "")
        if not cloud_url: return "❌ Fehler beim Cloud-Upload."
        rows = ws.get_all_values()
        for i, r in enumerate(rows):
            if len(r) > 1 and is_zoo(r[0], zoo_name) and str(r[1]).strip() == str(tier_name).strip():
                col_idx = 27 if vertrag_typ == "Pflegevertrag" else 28
                ws.update_cell(i + 1, col_idx, cloud_url)
                invalidate_cache()
                return f"✅ {vertrag_typ} erfolgreich hinzugefügt!"
        return "❌ Tier nicht gefunden."
    except Exception as e: return f"❌ Fehler: {str(e)[:30]}"

def nachtrag_speichern(zoo_name, ziel, aktiv_name, archiv_name, vertrag_typ, img_path):
    if ziel and "Archiv" in ziel:
        return nachtrag_archiv_vertrag(zoo_name, archiv_name, vertrag_typ, img_path)
    return nachtrag_aktiv_vertrag(zoo_name, aktiv_name, vertrag_typ, img_path)

def safe_float(val):
    try: return float(str(val).replace(",", ".").strip())
    except: return 0.0

def load_tier(zoo_name, n):
    if not zoo_name: return [""] * 53
    pm = lade_modus(zoo_name)
    if n in ["Hier Auswählen", "Neues Tier...", "", None]:
        is_new = (n == "Neues Tier...")
        choices = get_gefilterte_sorten(zoo_name, "", "Trockenfutter")
        trock_f_update = gr.update(value="Hier Auswählen", choices=choices)
        nass_choices = get_gefilterte_sorten(zoo_name, "", "Nassfutter")
        nass_f_update = gr.update(value="Hier Auswählen", choices=nass_choices)
        barf_choices = get_barf_typen(zoo_name)
        typ_f_update = gr.update(value="Hier Auswählen", choices=barf_choices)
        return ["", gr.update(value="Eigenes Tier", visible=pm), "", "", "", "", "Nein", 0, "", "", "", "", False, trock_f_update, "", typ_f_update, 0, 0, 0, False, nass_f_update, 0, "", "", "", "3 Monate", 0, gr.update(value=None), "", ""] + [gr.update(value=None), ""]*8 + [gr.update(visible=is_new), gr.update(visible=is_new), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=not is_new), gr.update(visible=not is_new)]
    try:
        rows = get_cached_data("tier")
        row = next((r for r in rows if len(r) > 1 and str(r[1]).strip() == str(n).strip() and is_zoo(r[0], zoo_name) and str(r[1]).strip().lower() != "name"), None)
        if not row: return ["", gr.update(value="Eigenes Tier", visible=pm), "", "", "", "", "Nein", 0, "", "", "", "", False, "Hier Auswählen", "", "Hier Auswählen", 0, 0, 0, False, "Hier Auswählen", 0, "", "", "", "3 Monate", 0, gr.update(value=None), "", ""] + [gr.update(value=None), ""]*8 + [gr.update(), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)]
        while len(row) < 40: row.append("")
        is_barf = True if str(row[7]).upper() == 'TRUE' else False

        # Profilbild und die 8 Dokumente parallel laden. Vorher teilten sich alle 9
        # Downloads EIN hartes 4-Sekunden-Gesamtlimit - bei mehreren gleichzeitig
        # langsamen Dokumenten konnte dadurch auch das Profilbild mit abgebrochen
        # werden, obwohl es das wichtigste sichtbare Element ist. Jetzt bekommt das
        # Profilbild sein eigenes, großzügigeres Zeitbudget, unabhängig von den
        # Dokumenten.
        doc_urls = [str(row[i]) for i in range(26, 34)]
        ergebnisse = [(gr.update(value=None), "")] * len(doc_urls)
        with ThreadPoolExecutor(max_workers=len(doc_urls) + 1) as executor:
            img_future = executor.submit(load_doc, str(row[19]))
            future_zu_index = {executor.submit(load_doc, url): idx for idx, url in enumerate(doc_urls)}
            fertig, offen = concurrent.futures.wait(list(future_zu_index.keys()) + [img_future], timeout=4)
            for future in fertig:
                if future is img_future: continue
                idx = future_zu_index[future]
                try: ergebnisse[idx] = future.result()
                except: pass
            try: img_val, hidden_val = img_future.result(timeout=4) if img_future in offen else img_future.result()
            except Exception: img_val, hidden_val = gr.update(value=None), ""
            for future in offen:
                future.cancel()  # zu langsame Dokumente abbrechen, Rest wird trotzdem angezeigt
        doc_vals = []
        for v, h in ergebnisse:
            doc_vals.extend([v, h])
        sorte, phase = str(row[8]).strip(), ""
        try:
            for ir in get_cached_data("inv")[1:]:
                if len(ir) > 18 and is_zoo(ir[0], zoo_name) and ir[8].strip() == sorte: phase = ir[18]; break
        except: pass
        kat_val, para_int_val = row[23] if row[23] else "Eigenes Tier", str(row[24]) if row[24] else "3 Monate"

        tierart_str = str(row[2])
        choices = get_gefilterte_sorten(zoo_name, tierart_str, "Trockenfutter")
        trock_f_val = row[8].strip() if row[8] else "Hier Auswählen"
        trock_f_update = gr.update(value=trock_f_val, choices=choices)
        
        barf_choices = get_barf_typen(zoo_name)
        typ_f_val = row[9].strip() if row[9] else "Hier Auswählen"
        typ_f_update = gr.update(value=typ_f_val, choices=barf_choices)

        nass_aktiv_val = True if str(row[34]).strip().lower() in ("ja", "true") else False
        nass_choices = get_gefilterte_sorten(zoo_name, tierart_str, "Nassfutter")
        nass_sorte_val = row[35].strip() if row[35] else "Hier Auswählen"
        nass_f_update = gr.update(value=nass_sorte_val, choices=nass_choices)
        nass_gramm_val = safe_float(row[36])

        base_data = [row[1], gr.update(value=kat_val, visible=pm), row[2], row[3], row[20], row[21], row[22], safe_float(row[4]), row[5], row[6], von_text(row[37]), von_text(row[38]), is_barf, trock_f_update, phase, typ_f_update, safe_float(row[10]), safe_float(row[11]), safe_float(row[12]), nass_aktiv_val, nass_f_update, nass_gramm_val, row[13], row[14], row[15], para_int_val, safe_float(row[16]), img_val, hidden_val, row[39]]
        return base_data + doc_vals + [gr.update(visible=True), gr.update(visible=True), gr.update(visible=not is_barf), gr.update(visible=is_barf), gr.update(visible=nass_aktiv_val), gr.update(visible=False), gr.update(visible=False)]
    except Exception as e: return ["", gr.update(value="Eigenes Tier", visible=pm), "", "", "", "", "Nein", 0, "", "", "", "", False, "Hier Auswählen", "", "Hier Auswählen", 0, 0, 0, False, "Hier Auswählen", 0, "", "", "", "3 Monate", 0, gr.update(value=None), "", ""] + [gr.update(value=None), ""]*8 + [gr.update(), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)]
        
def save_data(zoo_name, urspruenglicher_name, name, kat, tierart, rasse, geb_datum, farbe, kastriert, gew, chip, tasso, kontakt_tel, kontakt_tel2, barf_cb, trock, typ, gramm, prot, kohl, nass_cb, nass_sorte, nass_gramm, impf, wurm, para, para_int, tf_gramm, p_img, h_img, vp_img, vp_h, vv_img, vv_h, t1_img, t1_h, t2_img, t2_h, t3_img, t3_h, t4_img, t4_h, t5_img, t5_h, t6_img, t6_h, allergien):
    if not zoo_name: return "Fehler: Nicht eingeloggt!", *[gr.update()]*5, gr.update(), gr.update(), gr.update(), erstelle_dashboard(zoo_name)
    if not name or name == "Hier Auswählen": return "Fehler! Bitte Namen eintragen.", *[gr.update()]*5, gr.update(), gr.update(), gr.update(), erstelle_dashboard(zoo_name)
    try:
        pm = lade_modus(zoo_name)
        kat_val = str(kat) if pm and kat else "Eigenes Tier"
        rows = ws.get_all_values()
        such_name = urspruenglicher_name if urspruenglicher_name and urspruenglicher_name not in ("Hier Auswählen", "Neues Tier...") else name
        gs_row = None
        for i, r in enumerate(rows):
            if len(r) > 1 and is_zoo(r[0], zoo_name) and str(r[1]).strip() == str(such_name).strip(): gs_row = i + 1; break
        existing_row = rows[gs_row-1] if gs_row else [""] * 40
        while len(existing_row) < 40: existing_row.append("")
        existing_row[0] = zoo_name
        existing_row[1:17] = [str(name).strip(), str(tierart), str(rasse), str(gew), str(chip), str(tasso), str(barf_cb), str(trock).strip(), str(typ), str(gramm), str(prot), str(kohl), str(impf), str(wurm), str(para), str(tf_gramm)]
        existing_row[20:23] = [str(geb_datum), str(farbe), str(kastriert)]
        existing_row[23], existing_row[24] = kat_val, str(para_int)
        if not existing_row[25]: existing_row[25] = datetime.now().strftime("%d.%m.%Y")
        existing_row[19] = cloud_helper(p_img, h_img)
        docs = [(vp_img, vp_h), (vv_img, vv_h), (t1_img, t1_h), (t2_img, t2_h), (t3_img, t3_h), (t4_img, t4_h), (t5_img, t5_h), (t6_img, t6_h)]
        for idx, (img, h) in enumerate(docs): existing_row[26 + idx] = cloud_helper(img, h)
        existing_row[34] = "Ja" if nass_cb else "Nein"
        existing_row[35] = str(nass_sorte).strip() if nass_sorte and nass_sorte != "Hier Auswählen" else ""
        existing_row[36] = str(nass_gramm) if nass_gramm else ""
        existing_row[37] = als_text(kontakt_tel) if kontakt_tel else ""
        existing_row[38] = als_text(kontakt_tel2) if kontakt_tel2 else ""
        existing_row[39] = str(allergien).strip() if allergien else ""
        # v7.73.17: ws.append_row() durch ws.update() an einer explizit berechneten
        # Zeile ersetzt. Grund: append_row() nutzt intern eine eigene "finde die
        # nächste leere Zeile"-Logik von gspread, die in dieser App wiederholt und
        # reproduzierbar OHNE Fehlermeldung nichts geschrieben hat (mehrfach mit
        # Diagnose bestätigt: 0 von X Testversuchen kamen an). ws.update() mit einer
        # von uns selbst berechneten Zielzeile ist dagegen nachweislich zuverlässig -
        # genau diese Methode nutzt der "bestehendes Tier bearbeiten"-Zweig oben
        # bereits erfolgreich (bestätigt: Kategorie-Wechsel bei Flocke hat geklappt).
        if gs_row:
            ws.update(range_name=f"A{gs_row}:AN{gs_row}", values=[existing_row[:40]])
        else:
            naechste_zeile = len(rows) + 1
            ws.update(range_name=f"A{naechste_zeile}:AN{naechste_zeile}", values=[existing_row[:40]])
        # Die Diagnose-Rücklese-Prüfung (v7.73.13-v7.73.16) ist raus: sie hat bei
        # JEDEM Speichern zusätzlich das komplette (kontenübergreifende) Sheet neu
        # eingelesen, nur um sich selbst zu bestätigen - das kostete spürbar Zeit,
        # besonders bei vielen Tieren/Nutzern. War nur nötig, um den echten Bug
        # (unzuverlässiges append_row(), siehe v7.73.17) zu finden - der ist behoben,
        # ws.update() mit expliziter Zielzeile ist nachweislich zuverlässig.
        diagnose_status = "Gespeichert!"
        # Kontakttelefon(e) gelten für den ganzen Account -> bei allen anderen Tieren
        # desselben Zoos übernehmen. WICHTIG (Performance): früher zwei einzelne
        # update_cell()-Netzwerk-Anfragen PRO anderem Tier - bei vielen Tieren im
        # Konto summierte sich das spürbar (z.B. 10 Tiere = 20 einzelne Anfragen
        # nacheinander). Jetzt EIN batch_update() für alle betroffenen Zellen auf
        # einmal, unabhängig von der Anzahl der Tiere.
        try:
            batch_updates = []
            for i, r in enumerate(rows):
                if len(r) > 1 and is_zoo(r[0], zoo_name) and str(r[1]).strip() != str(such_name).strip() and str(r[1]).strip().lower() != "name":
                    batch_updates.append({"range": f"AL{i + 1}", "values": [[existing_row[37]]]})
                    batch_updates.append({"range": f"AM{i + 1}", "values": [[existing_row[38]]]})
            if batch_updates:
                ws.batch_update(batch_updates, value_input_option="RAW")
        except: pass
        invalidate_cache()
        d = update_dropdowns(zoo_name, select_name=str(name).strip())
        return diagnose_status, d[0], d[1], d[2], d[3], d[4], gr.update(value=p_img), gr.update(value=f"{existing_row[19]}|{get_file_hash(p_img)}" if p_img else ""), gr.update(visible=True), erstelle_dashboard(zoo_name)
    except Exception as e: return f"Fehler beim Speichern: {type(e).__name__}: {str(e)[:120]}", *[gr.update()]*5, gr.update(), gr.update(), gr.update(visible=True), erstelle_dashboard(zoo_name)

def archive_tier(zoo_name, name, grund, add_vertrag):
    if not zoo_name or not name or name == "Hier Auswählen": return "Kein Tier ausgewählt!", *[gr.update()]*5
    rows = get_cached_data("tier")
    row_idx = next((i for i, r in enumerate(rows) if len(r) > 1 and str(r[1]).strip() == str(name).strip() and is_zoo(r[0], zoo_name)), None)
    if row_idx is None: return "Tier nicht gefunden!", *[gr.update()]*5
    row = list(rows[row_idx])
    while len(row) < 34: row.append("")
    row[17], row[18] = grund, datetime.now().strftime("%d.%m.%Y")
    if grund == "Vermittelt" and add_vertrag: row[27] = cloud_helper(add_vertrag, "")
    ws_archiv.insert_row(row[:34], index=2)
    ws.delete_rows(row_idx + 1)
    invalidate_cache()
    d = update_dropdowns(zoo_name)
    return f"{name} wurde erfolgreich ins Archiv verschoben!", d[0], d[1], d[2], d[3], d[4]

# ==========================================
# TAGEBUCH (Beobachtungen / Verlauf pro Tier)
# ==========================================
def _tiere_gruppiert(zoo_name):
    """Liefert alle aktiven Tiere eines Kontos gruppiert nach Kategorie, für die
    neue Tier-Auswahl-Liste (v7.91.7, ersetzt das Dropdown). Pflegetiere und
    Urlaubsgäste nur, wenn der Erweiterte Modus aktiv ist - genau wie bisher
    beim Dropdown auch. Nur Gruppen mit mindestens einem Tier werden geliefert,
    leere Gruppen fallen weg."""
    pm = lade_modus(zoo_name)
    gruppen = {"Eigene Tiere": [], "Pflegetiere": [], "Urlaubsgäste": []}
    for r in get_cached_data("tier"):
        if len(r) > 1 and is_zoo(r[0], zoo_name) and str(r[1]).strip() and str(r[1]).strip().lower() != "name":
            kat = str(r[23]).strip() if len(r) > 23 and r[23] else "Eigenes Tier"
            if kat == "Pflegetier" and pm:
                gruppen["Pflegetiere"].append(str(r[1]).strip())
            elif kat == "Urlaubsgast" and pm:
                gruppen["Urlaubsgäste"].append(str(r[1]).strip())
            elif kat == "Eigenes Tier" or not pm:
                if kat == "Eigenes Tier":
                    gruppen["Eigene Tiere"].append(str(r[1]).strip())
    return [(name, tiere) for name, tiere in gruppen.items() if tiere]

def _alle_tiere_namen(zoo_name):
    """Liefert die Namen aller (nicht-archivierten) Tiere eines Kontos, für die
    Tagebuch-Übersicht - unabhängig davon, ob schon Einträge existieren."""
    pm = lade_modus(zoo_name)
    namen = []
    for r in get_cached_data("tier"):
        if len(r) > 1 and is_zoo(r[0], zoo_name) and str(r[1]).strip() and str(r[1]).strip().lower() != "name":
            if pm or not (len(r) > 23 and r[23] and r[23] != "Eigenes Tier"):
                namen.append(str(r[1]).strip())
    return namen

def _tiere_mit_tagebuch_stand(zoo_name):
    """Liefert alle aktiven (nicht archivierten) Tiere dieses Zoos mit Anzahl und
    letztem Datum ihrer Tagebuch-Einträge, für die Tagebuch-Übersicht (v7.91.4).
    Tiere mit Einträgen zuerst (neuestes Datum zuerst), Tiere ohne Einträge danach
    alphabetisch - damit das Aktivste ganz oben steht, aber nichts verschwindet."""
    if not zoo_name:
        return []
    tb_nach_tier = {}
    for r in get_cached_data("tagebuch"):
        if len(r) >= 3 and is_zoo(r[0], zoo_name):
            name = str(r[1]).strip()
            eintrag = tb_nach_tier.setdefault(name, {"anzahl": 0, "letztes_datum": ""})
            eintrag["anzahl"] += 1
            try:
                d = datetime.strptime(str(r[2]).strip(), "%d.%m.%Y")
                if not eintrag["letztes_datum"] or d > datetime.strptime(eintrag["letztes_datum"], "%d.%m.%Y"):
                    eintrag["letztes_datum"] = str(r[2]).strip()
            except Exception:
                pass
    ergebnis = []
    for r in get_cached_data("tier"):
        if len(r) > 1 and is_zoo(r[0], zoo_name):
            name = str(r[1]).strip()
            if not name:
                continue
            stand = tb_nach_tier.get(name, {"anzahl": 0, "letztes_datum": ""})
            tierart = str(r[2]).strip() if len(r) > 2 else ""
            ergebnis.append({"name": name, "tierart": tierart, "anzahl": stand["anzahl"], "letztes_datum": stand["letztes_datum"]})
    def sortier_schluessel(t):
        try:
            datum_sort = datetime.strptime(t["letztes_datum"], "%d.%m.%Y") if t["letztes_datum"] else datetime.min
        except Exception:
            datum_sort = datetime.min
        return (t["anzahl"] == 0, -datum_sort.timestamp() if t["anzahl"] > 0 else 0, t["name"].lower())
    ergebnis.sort(key=sortier_schluessel)
    return ergebnis

def _tagebuch_eintraege(zoo_name, tier_name):
    """Liefert alle Tagebuch-Einträge für dieses Tier, chronologisch (älteste
    zuerst, damit sortiert nach Datum stabil bleibt auch bei gleichem Tagesdatum).
    Enthält die tatsächliche Sheet-Zeilennummer für den Bearbeiten-Modus."""
    if not zoo_name or not tier_name:
        return []
    eintraege = []
    for i, r in enumerate(get_cached_data("tagebuch")):
        if len(r) >= 4 and is_zoo(r[0], zoo_name) and str(r[1]).strip() == str(tier_name).strip():
            foto_url = str(r[4]).strip() if len(r) > 4 and r[4] else ""
            titel = str(r[5]).strip() if len(r) > 5 and r[5] else ""
            eintraege.append({"zeile": i + 1, "datum": str(r[2]).strip(), "text": str(r[3]).strip(), "foto": foto_url, "titel": titel})
    return eintraege

def tagebuch_eintrag_speichern(zoo_name, tier_name, titel, text, foto_pfad, anzahl_anzeigen, refresh_zaehler):
    """Speichert einen NEUEN Tagebuch-Eintrag inkl. Titel und optionalem Foto.
    Race-sicheres append_row() + Rücklese-Prüfung (siehe v7.88.3) statt "Zeile
    zählen, dann fest dorthin schreiben" - Letzteres kann bei zeitgleichem
    Speichern auf zwei Geräten (z.B. Android + iOS) zum gegenseitigen
    Überschreiben führen, ganz ohne Fehlermeldung. Das Bearbeiten bestehender
    Einträge läuft seit v7.83.1 direkt inline in der jeweiligen Karte (siehe
    @gr.render weiter unten), dieses Formular ist nur noch für neue Einträge
    zuständig. 'refresh_zaehler' wird hochgezählt, um die Karten-Liste zum
    Neuzeichnen mit den frischen Daten zu bewegen."""
    if not zoo_name:
        return "❌ Nicht eingeloggt.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    if not tier_name or tier_name in ("Hier Auswählen", "Neues Tier..."):
        return "❌ Bitte zuerst ein Tier auswählen (und einmal speichern, falls neu angelegt).", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    if not text or not text.strip():
        return "❌ Bitte einen Text eintragen.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    try:
        foto_url = upload_image_to_cloud(foto_pfad) if foto_pfad else ""
        neue_zeile = [zoo_name, tier_name, datetime.now().strftime("%d.%m.%Y"), text.strip(), foto_url, str(titel).strip() if titel else ""]
        # WICHTIG (v7.88.3): "Zeile zählen, dann an genau diese Zeile schreiben" ist
        # anfällig für ein Wettlauf-Problem, wenn zwei Geräte (z.B. Android + iOS)
        # nahezu gleichzeitig einen neuen Eintrag anlegen - beide berechnen dieselbe
        # "nächste freie Zeile" und überschreiben sich gegenseitig, ganz ohne
        # Fehlermeldung (das ist vermutlich genau passiert). append_row() lässt
        # Google Sheets selbst serverseitig die nächste freie Zeile bestimmen, das
        # ist bei gleichzeitigen Zugriffen sicher. append_row() war früher an
        # anderer Stelle unzuverlässig (siehe v7.73.17), daher zusätzlich eine
        # Rücklese-Prüfung mit einem Wiederholungsversuch als Sicherheitsnetz.
        ws_tagebuch.append_row(neue_zeile)
        invalidate_cache()
        # Nur die LESE-Prüfung wiederholen, NICHT append_row() erneut aufrufen -
        # sonst würde bei einer nur kurzzeitig veralteten Lese-Antwort ein
        # doppelter Eintrag entstehen, statt nur die Bestätigung nachzuholen.
        gefunden = False
        for versuch in range(3):
            if versuch > 0: time.sleep(1.5)
            for r in ws_tagebuch.get_all_values():
                if len(r) >= 4 and r[0] == zoo_name and r[1] == tier_name and r[3] == text.strip():
                    gefunden = True
                    break
            if gefunden:
                break
        status = "✅ Eintrag gespeichert." if gefunden else "⚠️ Eintrag konnte nicht sicher bestätigt werden - bitte prüfen und ggf. erneut speichern."
        return status, "", "", None, 8, (refresh_zaehler or 0) + 1
    except Exception as e:
        return f"❌ Fehler beim Speichern: {str(e)[:80]}", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()

# ==========================================
# HAUSTIERAPOTHEKE (kontenübergreifende, von Admins gepflegte Wissensdatenbank)
# ==========================================
APOTHEKE_KATEGORIE_ICONS = [
    (("hund",), "🐶"),
    (("katze", "kater"), "🐱"),
    (("pferd", "pony"), "🐴"),
    (("kaninchen", "hase"), "🐰"),
    (("meerschwein",), "🐹"),
    (("hamster",), "🐹"),
    (("vogel", "wellensittich", "papagei"), "🐦"),
    (("schlange", "gecko", "reptil"), "🦎"),
    (("schildkröte", "leguan"), "🐢"),
    (("fisch", "aquarium"), "🐠"),
]

def _apotheke_icon(kategorie):
    """Liefert ein zur Tierart passendes Icon für eine Haustierapotheke-Kategorie
    (z.B. 🐶 für 'Hund'), erkannt per Stichwort im Kategorienamen - fällt bei
    unbekannten/freien Kategorien auf die neutrale Pfote zurück."""
    k = str(kategorie).strip().lower()
    for stichworte, icon in APOTHEKE_KATEGORIE_ICONS:
        if any(s in k for s in stichworte):
            return icon
    return "🐾"

def _apotheke_eintraege():
    """Liefert alle Einträge der Haustierapotheke (kontenübergreifend, nicht nach
    Zoo gefiltert - das ist bewusst eine gemeinsame Datenbank für alle Nutzer)."""
    eintraege = []
    for i, r in enumerate(get_cached_data("apotheke")):
        if i == 0 and str(r[0]).strip().lower() == "kategorie":
            continue
        if len(r) >= 3 and str(r[1]).strip():
            eintraege.append({"zeile": i + 1, "kategorie": str(r[0]).strip(), "titel": str(r[1]).strip(), "inhalt": str(r[2]).strip(), "datum": str(r[3]).strip() if len(r) > 3 else ""})
    return eintraege

def lade_apotheke_kategorien():
    """Liefert die Liste aller vorhandenen Kategorien, alphabetisch sortiert."""
    return sorted(set(e["kategorie"] for e in _apotheke_eintraege() if e["kategorie"]))

def apotheke_banner_ist_aktiv():
    """Liest, ob das Kopf-Banner-Bild der Haustierapotheke gerade angezeigt werden
    soll. Über den Admin-Bereich umschaltbar, ohne dass Code geändert werden muss -
    praktisch z.B. wenn gerade ein neues Bild hochgeladen wird und das alte/kaputte
    Bild zwischenzeitlich nicht angezeigt werden soll."""
    try:
        for r in ws_app_einstellungen.get_all_values():
            if len(r) >= 2 and str(r[0]).strip() == "apotheke_banner_aktiv":
                return str(r[1]).strip().lower() == "ja"
    except Exception:
        pass
    return True

def apotheke_banner_umschalten(neuer_status):
    """Schaltet das Haustierapotheke-Banner an/aus (Admin-Checkbox)."""
    try:
        rows = ws_app_einstellungen.get_all_values()
        for i, r in enumerate(rows):
            if len(r) >= 1 and str(r[0]).strip() == "apotheke_banner_aktiv":
                ws_app_einstellungen.update(range_name=f"A{i + 1}:B{i + 1}", values=[["apotheke_banner_aktiv", "ja" if neuer_status else "nein"]])
                return gr.update(visible=neuer_status)
        naechste_zeile = len(rows) + 1
        ws_app_einstellungen.update(range_name=f"A{naechste_zeile}:B{naechste_zeile}", values=[["apotheke_banner_aktiv", "ja" if neuer_status else "nein"]])
        return gr.update(visible=neuer_status)
    except Exception:
        return gr.update()

def apotheke_eintrag_speichern(zoo_name, kategorie, titel, inhalt, refresh_zaehler):
    """Legt einen neuen Haustierapotheke-Eintrag an - nur für Admins zugänglich
    (siehe UI-Sichtbarkeit), da die Datenbank bewusst kuratiert wird und nicht von
    allen Nutzern frei bearbeitbar ist. Race-sicheres append_row() + Rücklese-
    Prüfung (siehe v7.88.3) statt "Zeile zählen, dann fest dorthin schreiben" -
    Letzteres kann bei zeitgleichen Speicherungen mehrerer Admins zum
    gegenseitigen Überschreiben führen. 'refresh_zaehler' wird hochgezählt, um die
    Kategorien-Ansicht (siehe @gr.render weiter unten) zum Neuzeichnen mit den
    frischen Daten zu bewegen. Öffnet zusätzlich automatisch die passende
    Kategorie-Karte, sonst landet der neue Eintrag in einer eingeklappten Karte
    und sieht aus wie 'nicht gespeichert'."""
    if not zoo_name:
        return "❌ Nicht eingeloggt.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    if not kategorie or not kategorie.strip():
        return "❌ Bitte eine Kategorie eintragen.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    if not titel or not titel.strip():
        return "❌ Bitte einen Titel eintragen.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    if not inhalt or not inhalt.strip():
        return "❌ Bitte einen Inhalt eintragen.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
    try:
        neue_zeile = [kategorie.strip(), titel.strip(), inhalt.strip(), datetime.now().strftime("%d.%m.%Y")]
        # Race-sicher: append_row() statt "Zeile zählen, dann fest dorthin schreiben"
        # - siehe ausführliche Begründung in tagebuch_eintrag_speichern() (v7.88.3).
        # Bei der Haustierapotheke könnten mehrere Admins gleichzeitig Einträge
        # anlegen, dasselbe Wettlauf-Risiko besteht also auch hier.
        ws_apotheke.append_row(neue_zeile)
        invalidate_cache()
        gefunden = False
        for versuch in range(3):
            if versuch > 0: time.sleep(1.5)
            for r in ws_apotheke.get_all_values():
                if len(r) >= 3 and r[0] == kategorie.strip() and r[1] == titel.strip():
                    gefunden = True
                    break
            if gefunden:
                break
        status = "✅ Eintrag gespeichert." if gefunden else "⚠️ Eintrag konnte nicht sicher bestätigt werden - bitte prüfen und ggf. erneut speichern."
        return status, "", "", "", (refresh_zaehler or 0) + 1, kategorie.strip()
    except Exception as e:
        return f"❌ Fehler beim Speichern: {str(e)[:80]}", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()

def exportiere_tagebuch_pdf(zoo_name, tier_name):
    """Exportiert den kompletten chronologischen Verlauf eines Tieres als PDF
    inkl. Titel und Fotos, z.B. zum Mitgeben an neue Paten von Pflegetieren."""
    if not zoo_name or not tier_name or tier_name in ("Hier Auswählen", "Neues Tier..."):
        err_file = "Keine_Auswahl.txt"
        with open(err_file, "w") as f: f.write("Bitte wähle zuerst ein Tier aus.")
        return err_file, "❌ Bitte wähle zuerst ein Tier aus!"
    eintraege = _tagebuch_eintraege(zoo_name, tier_name)
    if not eintraege:
        err_file = "Keine_Eintraege.txt"
        with open(err_file, "w") as f: f.write("Für dieses Tier gibt es noch keine Tagebuch-Einträge.")
        return err_file, "❌ Für dieses Tier gibt es noch keine Einträge!"
    try:
        def safe_txt(t): return str(t).encode('latin-1', 'replace').decode('latin-1')
        pdf = FPDF()
        pdf.add_page()
        pdf.set_font("Arial", 'B', 16)
        pdf.cell(190, 10, txt=safe_txt(f"Tagebuch: {tier_name}"), ln=True, align='C')
        pdf.set_font("Arial", '', 10)
        pdf.set_text_color(120, 110, 92)
        pdf.cell(190, 6, txt=safe_txt(f"Erstellt am {datetime.now().strftime('%d.%m.%Y')} mit PetBase"), ln=True, align='C')
        pdf.set_text_color(0, 0, 0)
        pdf.ln(6)
        for e in eintraege:
            # Vor jedem Eintrag prüfen, ob genug Platz auf der Seite bleibt (Titel +
            # Text + ggf. Foto), sonst neue Seite - vermeidet, dass Fotos mitten
            # zerschnitten werden.
            geschaetzte_hoehe = 20 + (7 if e.get("titel") else 0) + (55 if e.get("foto") else 0)
            if pdf.get_y() + geschaetzte_hoehe > 280:
                pdf.add_page()
            pdf.set_font("Arial", 'B', 11)
            pdf.cell(190, 7, txt=safe_txt(e["datum"]), ln=True)
            if e.get("titel"):
                pdf.set_font("Arial", 'B', 12)
                pdf.cell(190, 7, txt=safe_txt(e["titel"]), ln=True)
            pdf.set_font("Arial", '', 11)
            pdf.multi_cell(190, 6, txt=safe_txt(e["text"]))
            if e.get("foto") and e["foto"].startswith("http"):
                erste_foto_url = e["foto"].split(",")[0].strip()
                try:
                    resp = requests.get(erste_foto_url, headers={"User-Agent": "Mozilla/5.0"}, timeout=5)
                    if resp.status_code == 200:
                        with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp:
                            tmp.write(resp.content); tmp.close()
                            with PILImage.open(tmp.name) as img:
                                if img.mode != "RGB": img = img.convert("RGB")
                                img.save(tmp.name, "JPEG")
                                aspect = img.size[1] / max(img.size[0], 1)
                                bild_breite = 70
                                pdf.image(tmp.name, x=10, y=pdf.get_y() + 2, w=bild_breite, h=bild_breite * aspect)
                                pdf.set_y(pdf.get_y() + bild_breite * aspect + 4)
                except Exception:
                    pass
            pdf.ln(3)
        clean_name = str(tier_name).replace(" ", "_").replace("/", "").strip()
        filename = f"{clean_name}_Tagebuch.pdf"
        pdf.output(filename)
        return filename, "✅ PDF wurde erstellt."
    except Exception as e:
        err_file = "Fehler.txt"
        with open(err_file, "w") as f: f.write(f"Fehler: {e}")
        return err_file, f"❌ Fehler beim Erstellen: {str(e)[:80]}"

def export_pdf(zoo_name, sel_name, is_archive, mit_info=True, mit_pflege=True, mit_vermittlung=True, mit_tierarzt=True, mit_tagebuch=False):
    if not zoo_name or not sel_name or sel_name == "Hier Auswählen":
        err_file = "Keine_Auswahl.txt"
        with open(err_file, "w") as f: f.write("Bitte wähle zuerst etwas aus.")
        return err_file, "❌ Bitte wähle zuerst ein Tier aus!"
    if not (mit_info or mit_pflege or mit_vermittlung or mit_tierarzt or mit_tagebuch):
        err_file = "Keine_Auswahl.txt"
        with open(err_file, "w") as f: f.write("Bitte mindestens eine Option auswählen.")
        return err_file, "❌ Bitte mindestens eine Option auswählen (Infoblatt/Pflegevertrag/Vermittlungsvertrag/Tagebuch)!"
    try:
        pm = lade_modus(zoo_name)
        pdf = FPDF()
        clean_name = sel_name.replace(" ", "_").replace("/", "").strip()
        filename = f"{clean_name}_Akte.pdf"
        def safe_txt(t): return str(t).encode('latin-1', 'replace').decode('latin-1')
        raw_data = ws_archiv.get_all_values() if is_archive else get_cached_data("tier")
        data = []
        for r in raw_data:
            if len(r) == 0 or not str(r[1]).strip() or str(r[1]).strip().lower() == "name" or not is_zoo(r[0], zoo_name): continue
            kat = r[23] if len(r) > 23 and r[23] else "Eigenes Tier"
            if sel_name == "Alle eigenen Tiere" and kat == "Eigenes Tier": data.append(r)
            elif sel_name == "Alle Pflege- & Gasttiere" and kat in ["Pflegetier", "Urlaubsgast"]: data.append(r)
            elif sel_name == "Alle Archivierten":
                if pm or kat == "Eigenes Tier": data.append(r)
            elif str(r[1]).strip() == str(sel_name).strip(): data.append(r)
        if not data:
            err_file = "Keine_Daten.txt"
            with open(err_file, "w") as f: f.write("Keine Tiere gefunden.")
            return err_file, "❌ Keine Tiere gefunden!"
        inv_data = [ir for ir in get_cached_data("inv")[1:] if len(ir) > 0 and is_zoo(ir[0], zoo_name)]
        futter_details = {ir[8].strip(): ir for ir in inv_data if len(ir) > 8}
        for r in data:
            row = r + [""] * (40 - len(r))
            img_url = str(row[19])
            if mit_info:
                pdf.add_page()
                start_y = pdf.get_y()
                pdf.set_font("Arial", 'B', 16)
                pdf.cell(200, 10, txt=safe_txt(f"Digitale Akte: {row[1]}"), ln=True, align='C')
                pdf.ln(5)
                pdf.set_font("Arial", 'B', 14)
                kat_str = f" [{row[23]}]" if row[23] and pm else ""
                pdf.cell(130, 8, txt=safe_txt(f"Name: {row[1]}{kat_str}"), ln=True)
                pdf.set_font("Arial", '', 11)
                pdf.cell(130, 6, txt=safe_txt(f"Tierart: {row[2]} | Rasse: {row[3]}"), ln=True)
                pdf.cell(130, 6, txt=safe_txt(f"Geburtsdatum: {row[20]} | Farbe: {row[21]}"), ln=True)
                pdf.cell(130, 6, txt=safe_txt(f"Kastriert: {row[22]} | Gewicht: {row[4]} kg"), ln=True)
                pdf.cell(130, 6, txt=safe_txt(f"ID/Ring: {row[5]} | Registrierung: {row[6]}"), ln=True)
                if len(row) > 39 and row[39]:
                    pdf.ln(1)
                    pdf.set_font("Arial", 'B', 11)
                    pdf.set_text_color(180, 30, 30)
                    pdf.multi_cell(130, 6, txt=safe_txt(f"ACHTUNG - Allergien/Unverträglichkeiten: {row[39]}"))
                    pdf.set_text_color(0, 0, 0)

                if is_archive:
                    grund = row[17] if len(row) > 17 and row[17] else "Unbekannt"
                    datum_archiv = row[18] if len(row) > 18 and row[18] else "Unbekannt"
                    pdf.cell(130, 6, txt=safe_txt(f"Status: Archiviert am {datum_archiv} | Grund: {grund}"), ln=True)

                pdf.ln(2); pdf.set_font("Arial", 'B', 11); pdf.cell(130, 6, txt=safe_txt("Fütterung:"), ln=True); pdf.set_font("Arial", '', 11)
                if str(row[7]).upper() == 'TRUE': 
                    pdf.cell(130, 6, txt=safe_txt(f"Frischfütterung/BARF: {row[9]} | Gesamt: {row[10]} g/Tag"), ln=True)
                    pdf.cell(130, 6, txt=safe_txt(f"  -> davon Protein/Fleisch: {row[11]} g | Kohlenhydrate/Faser: {row[12]} g"), ln=True)
                else:
                    sorte = str(row[8]).strip()
                    pdf.cell(130, 6, txt=safe_txt(f"Hauptfutter: {sorte} | {row[16]} g/Tag"), ln=True)
                if len(row) > 36 and str(row[34]).strip().lower() in ("ja", "true") and row[35]:
                    pdf.cell(130, 6, txt=safe_txt(f"Zusätzlich Nassfutter: {row[35]} | {row[36]} g/Tag"), ln=True)

                pdf.ln(2); pdf.set_font("Arial", 'B', 11); pdf.cell(130, 6, txt=safe_txt("Gesundheit & Termine:"), ln=True); pdf.set_font("Arial", '', 11)
                pdf.cell(130, 6, txt=safe_txt(f"Impfung / Check-up: {row[13]}"), ln=True)
                pdf.cell(130, 6, txt=safe_txt(f"Wurmkur / Kotprobe: {row[14]}"), ln=True)
                para_int = row[24] if len(row) > 24 and row[24] else "3 Monate"
                pdf.cell(130, 6, txt=safe_txt(f"Spezialpflege / Parasitenschutz: {row[15]} (Intervall: {para_int})"), ln=True)

                if img_url.startswith("http"):
                    try:
                        resp = requests.get(img_url, headers={"User-Agent": "Mozilla/5.0"}, timeout=3)
                        if resp.status_code == 200:
                            with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp:
                                tmp.write(resp.content); tmp.close()
                                with PILImage.open(tmp.name) as img:
                                    if img.mode != "RGB": img = img.convert("RGB")
                                    img.save(tmp.name, "JPEG")
                                    aspect = img.size[1]/max(img.size[0], 1)
                                    pdf.image(tmp.name, x=160, y=start_y+10, w=30, h=30*aspect)
                    except: pass

            docs_to_print = []
            if mit_pflege and len(row) > 26 and row[26].startswith("http"): docs_to_print.append((row[26], "Pflegevertrag"))
            if mit_vermittlung and len(row) > 27 and row[27].startswith("http"): docs_to_print.append((row[27], "Vermittlungsvertrag"))
            if mit_tierarzt:
                for i in range(28, 34):
                    if len(row) > i and row[i].startswith("http"): 
                        docs_to_print.append((row[i], f"Tierarzt-Dokument {i-27}"))
            
            for doc_url, doc_title in docs_to_print:
                seiten_urls = [u.strip() for u in doc_url.split(",") if u.strip().startswith("http")]
                for seiten_idx, einzel_url in enumerate(seiten_urls):
                    try:
                        resp = requests.get(einzel_url, headers={"User-Agent": "Mozilla/5.0"}, timeout=5)
                        if resp.status_code == 200:
                            with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp:
                                tmp.write(resp.content); tmp.close()
                                with PILImage.open(tmp.name) as img:
                                    if img.mode != "RGB": img = img.convert("RGB")
                                    img.save(tmp.name, "JPEG")
                                    titel = f"{doc_title} (Seite {seiten_idx+1}/{len(seiten_urls)})" if len(seiten_urls) > 1 else doc_title
                                    pdf.add_page(); pdf.set_font("Arial", 'B', 12)
                                    pdf.cell(200, 10, txt=safe_txt(titel), ln=True, align='C')
                                    w, h = img.size; ratio = min(190/w, 250/h)
                                    pdf.image(tmp.name, x=10, y=25, w=w*ratio, h=h*ratio)
                    except: pass

            if mit_tagebuch:
                # Chronologischer Tagebuch-Verlauf dieses Tieres, optional mit in die
                # Gesamt-Akte aufgenommen - z.B. praktisch zum Mitgeben an neue Paten
                # von Pflegetieren, statt eines separaten PDFs. Gleiche Aufbereitung
                # (Titel/Text/Foto, Seitenumbruch-Logik) wie exportiere_tagebuch_pdf().
                tb_eintraege = _tagebuch_eintraege(zoo_name, row[1])
                if tb_eintraege:
                    pdf.add_page()
                    pdf.set_font("Arial", 'B', 14)
                    pdf.cell(190, 10, txt=safe_txt(f"Tagebuch: {row[1]}"), ln=True, align='C')
                    pdf.ln(4)
                    for e in tb_eintraege:
                        geschaetzte_hoehe = 20 + (7 if e.get("titel") else 0) + (55 if e.get("foto") else 0)
                        if pdf.get_y() + geschaetzte_hoehe > 280:
                            pdf.add_page()
                        pdf.set_font("Arial", 'B', 11)
                        pdf.cell(190, 7, txt=safe_txt(e["datum"]), ln=True)
                        if e.get("titel"):
                            pdf.set_font("Arial", 'B', 12)
                            pdf.cell(190, 7, txt=safe_txt(e["titel"]), ln=True)
                        pdf.set_font("Arial", '', 11)
                        pdf.multi_cell(190, 6, txt=safe_txt(e["text"]))
                        if e.get("foto") and e["foto"].startswith("http"):
                            erste_foto_url = e["foto"].split(",")[0].strip()
                            try:
                                resp = requests.get(erste_foto_url, headers={"User-Agent": "Mozilla/5.0"}, timeout=5)
                                if resp.status_code == 200:
                                    with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp:
                                        tmp.write(resp.content); tmp.close()
                                        with PILImage.open(tmp.name) as img:
                                            if img.mode != "RGB": img = img.convert("RGB")
                                            img.save(tmp.name, "JPEG")
                                            aspect = img.size[1] / max(img.size[0], 1)
                                            bild_breite = 70
                                            pdf.image(tmp.name, x=10, y=pdf.get_y() + 2, w=bild_breite, h=bild_breite * aspect)
                                            pdf.set_y(pdf.get_y() + bild_breite * aspect + 4)
                            except Exception:
                                pass
                        pdf.ln(3)

        if pdf.page_no() == 0:
            err_file = "Keine_Inhalte.txt"
            with open(err_file, "w") as f: f.write("Für die gewählte Auswahl gibt es keine passenden Inhalte.")
            return err_file, "❌ Für die gewählte Kombination gibt es keine passenden Inhalte (z.B. kein hochgeladener Vertrag vorhanden)."
        pdf.output(filename)
        return filename, f"✅ Komplette Akte für {sel_name} erstellt!"
    except Exception as e:
        err_file = "PDF_Fehler.txt"
        with open(err_file, "w") as f: f.write(f"Fehler:\n{str(e)}")
        return err_file, "❌ Fehler bei PDF Erstellung."

def zeige_vermisst_daten(name, tierart, rasse, farbe, chip, tasso, kontakt_tel, kontakt_tel2):
    if not name or name == "Hier Auswählen":
        return "Bitte zuerst ein Tier auswählen bzw. laden."
    zeilen = [
        f"Name: {name}",
        f"Tierart: {tierart or '-'}",
        f"Rasse: {rasse or '-'}",
        f"Farbe: {farbe or '-'}",
        f"Chip-/Ringnummer: {chip or '-'}",
        f"Tasso-/Registrierungsnummer: {tasso or '-'}",
        f"Kontakttelefon: {kontakt_tel or '-'}",
    ]
    if kontakt_tel2: zeilen.append(f"Kontakttelefon 2 (Backup): {kontakt_tel2}")
    return "\n".join(zeilen)

def export_vermisst_steckbrief(zoo_name, name, tierart, rasse, farbe, chip, tasso, kontakt_tel, kontakt_tel2, notiz, p_img_path):
    if not zoo_name or not name or name == "Hier Auswählen":
        err_file = "Kein_Tier.txt"
        with open(err_file, "w") as f: f.write("Bitte zuerst ein Tier auswählen.")
        return err_file, "❌ Bitte zuerst ein Tier auswählen bzw. laden."
    try:
        def safe_txt(t): return str(t).encode('latin-1', 'replace').decode('latin-1')
        pdf = FPDF()
        pdf.add_page()
        pdf.set_font("Arial", 'B', 30)
        pdf.set_text_color(200, 0, 0)
        pdf.cell(0, 22, txt="VERMISST", ln=True, align='C')
        pdf.set_text_color(0, 0, 0)

        y_nach_bild = 32
        if p_img_path:
            try:
                with PILImage.open(p_img_path) as img:
                    if img.mode != "RGB": img = img.convert("RGB")
                    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".jpg")
                    img.save(tmp.name, "JPEG")
                    w = 100
                    aspect = img.size[1] / max(img.size[0], 1)
                    pdf.image(tmp.name, x=(210 - w) / 2, y=32, w=w, h=w * aspect)
                    y_nach_bild = 32 + w * aspect + 8
            except: pass

        pdf.set_y(y_nach_bild)
        pdf.set_font("Arial", 'B', 18)
        pdf.cell(0, 10, txt=safe_txt(name), ln=True, align='C')
        pdf.set_font("Arial", '', 13)
        pdf.ln(2)
        for label, wert in [("Tierart", tierart), ("Rasse", rasse), ("Farbe", farbe), ("Chip-/Ringnummer", chip), ("Tasso-/Registrierung", tasso)]:
            if wert:
                pdf.cell(0, 8, txt=safe_txt(f"{label}: {wert}"), ln=True, align='C')

        if notiz:
            pdf.ln(3)
            pdf.set_font("Arial", 'I', 12)
            pdf.multi_cell(0, 7, txt=safe_txt(notiz), align='C')

        pdf.ln(6)
        pdf.set_font("Arial", 'B', 15)
        pdf.set_text_color(200, 0, 0)
        kontakt_zeile = f"Bitte melden unter: {kontakt_tel}" if kontakt_tel else "Bitte bei TASSO oder einem Tierarzt melden"
        if kontakt_tel2: kontakt_zeile += f"\noder: {kontakt_tel2}"
        pdf.multi_cell(0, 9, txt=safe_txt(kontakt_zeile), align='C')

        clean_name = str(name).replace(" ", "_").replace("/", "")
        filename = f"Vermisst_{clean_name}.pdf"
        pdf.output(filename)
        return filename, f"✅ Steckbrief für {name} erstellt!"
    except Exception as e:
        err_file = "PDF_Fehler.txt"
        with open(err_file, "w") as f: f.write(f"Fehler:\n{str(e)}")
        return err_file, "❌ Fehler beim Erstellen des Steckbriefs."

def zeige_als_toast(msg):
    """Zeigt einen Status-Text als kurzes Overlay statt in einer sichtbaren Textbox."""
    if not msg or not str(msg).strip():
        return
    msg = str(msg)
    if msg.startswith("❌") or msg.startswith("⚠️") or "Fehler" in msg:
        gr.Warning(msg)
    else:
        gr.Info(msg)

def get_heute(): return datetime.now().strftime("%d.%m.%Y")
# ==========================================
# E-MAIL-VERSAND (Zugangsdaten kommen ausschließlich aus Hugging-Face-Space-
# "Secrets" / Umgebungsvariablen - werden NIE in der Google Sheet gespeichert)
# ==========================================
def smtp_env(key):
    """Liest eine Umgebungsvariable und entfernt versehentlich mitkopierte
    Anführungszeichen/Leerzeichen - ein häufiger Fehler beim Einfügen von Secrets."""
    wert = os.environ.get(key, "")
    if not wert: return ""
    wert = wert.strip()
    if len(wert) >= 2 and wert[0] == wert[-1] and wert[0] in ("'", '"'):
        wert = wert[1:-1].strip()
    return wert

def email_konfiguriert():
    return all([smtp_env("BREVO_API_KEY"), smtp_env("SMTP_FROM")])

def email_status_text():
    if email_konfiguriert():
        return f"<div style='color:#2E7D32;'>✅ E-Mail-Versand ist konfiguriert (Absender: {smtp_env('SMTP_FROM')})</div>"
    fehlend = [k for k in ["BREVO_API_KEY", "SMTP_FROM"] if not smtp_env(k)]
    return f"<div style='color:#D32F2F;'>❌ E-Mail-Versand noch nicht eingerichtet. Fehlende Secrets: {', '.join(fehlend)}</div>"

def _from_name_email(smtp_from):
    """Zerlegt 'Name <email@x.de>' in (Name, E-Mail), akzeptiert auch nur 'email@x.de'."""
    m = re.match(r'^\s*(.*?)\s*<(.+?)>\s*$', smtp_from)
    if m: return (m.group(1).strip() or "PetBase"), m.group(2).strip()
    return "PetBase", smtp_from.strip()

def sende_email(empfaenger, betreff, text, zoo_name=""):
    if not email_konfiguriert():
        return False, "E-Mail-Versand ist nicht eingerichtet (fehlende Secrets)."
    if not empfaenger or "@" not in str(empfaenger):
        return False, "Keine gültige E-Mail-Adresse hinterlegt."
    try:
        name, email = _from_name_email(smtp_env("SMTP_FROM"))
        # HTML-Version zusätzlich zur reinen Textversion, mit dem App-Banner als
        # Footer-Bild. textContent bleibt als Fallback für Clients ohne HTML-Anzeige.
        html_text = text.replace("\n", "<br>")
        html_content = (
            "<div style='font-family:Arial,sans-serif; color:#3B3226; font-size:15px; line-height:1.5;'>"
            f"{html_text}"
            "</div>"
            "<div style='margin-top:28px;'>"
            f"<img src='{BANNER_URL}' alt='PetBase' style='max-width:100%; width:400px; height:auto; border-radius:8px; display:block;'>"
            "</div>"
        )
        resp = requests.post(
            "https://api.brevo.com/v3/smtp/email",
            headers={"api-key": smtp_env("BREVO_API_KEY"), "Content-Type": "application/json", "Accept": "application/json"},
            json={"sender": {"name": name, "email": email}, "to": [{"email": empfaenger}], "subject": betreff, "textContent": text, "htmlContent": html_content},
            timeout=10,
        )
        if resp.status_code not in (200, 201):
            return False, f"❌ E-Mail konnte nicht versendet werden ({resp.status_code}): {resp.text[:80]}"
        try:
            naechste_zeile = len(ws_emaillog.get_all_values()) + 1
            ws_emaillog.update(range_name=f"A{naechste_zeile}:D{naechste_zeile}", values=[[datetime.now().strftime("%d.%m.%Y %H:%M"), zoo_name, empfaenger, betreff]])
            invalidate_cache()
        except: pass
        return True, "✅ E-Mail wurde versendet."
    except Exception as e:
        return False, f"❌ E-Mail konnte nicht versendet werden (Host: '{smtp_env('SMTP_HOST')}'): {str(e)[:60]}"

def email_zaehler():
    try:
        rows = get_cached_data("emaillog")
        start_idx = 1 if len(rows) > 0 and "datum" in str(rows[0][0]).lower() else 0
        return len(rows) - start_idx
    except Exception: return 0

def test_email_senden(zoo_name):
    email = lade_einstellungen(zoo_name)
    if not email:
        return "❌ Für dein Konto ist keine E-Mail-Adresse hinterlegt (Konto → Einstellungen → Erinnerungen)."
    ok, msg = sende_email(email, "🐾 PetBase Test-E-Mail", f"Hallo {zoo_name},\n\ndies ist eine Test-E-Mail von PetBase. Wenn du diese E-Mail liest, funktioniert der E-Mail-Versand einwandfrei! 🎉\n\nDiese E-Mail wurde automatisch versendet.", zoo_name)
    return msg

def erinnerung_per_email(zoo_name, tier_name, typ, datum_str):
    email = lade_einstellungen(zoo_name)
    if not email:
        return "❌ Für dein Konto ist keine E-Mail-Adresse hinterlegt (Konto → Erinnerungen)."
    betreff = f"🐾 PetBase Erinnerung: {typ} für {tier_name}"
    text = f"Hallo {zoo_name},\n\ndies ist eine Erinnerung von PetBase:\n\n{tier_name} - {typ}\nDatum: {datum_str}\n\nDiese E-Mail wurde automatisch von deiner PetBase-App versendet."
    ok, msg = sende_email(email, betreff, text, zoo_name)
    return msg

def pruefe_und_sende_futter_erinnerungen():
    """Prüft für ALLE Zoo-Accounts den Futterbestand und verschickt automatisch je
    eine Erinnerungs-Mail pro Futtersorte, sobald der Restbestand 14, 5 oder 0 Tage
    erreicht. Gedacht für TÄGLICHEN externen Aufruf (Cron) - Hugging Face Spaces hat
    keinen eingebauten Scheduler. Erreichbar über die Gradio-API dieser App unter
    api_name='futter_cron_check' (siehe UI-Definition weiter unten). Ein externer
    Dienst wie cron-job.org kann diese Adresse einmal täglich per POST aufrufen:
    https://<dein-space>.hf.space/gradio_api/call/futter_cron_check
    Duplikat-Schutz: pro (Zoo, Sorte, Datum der letzten Bestandsaktualisierung,
    Schwelle) wird nur einmal verschickt (Tracking-Sheet Futter_Erinnerungen_Log) -
    ändert sich die Aktualisierung (neuer Einkauf), beginnt der Zyklus neu. Nutzt
    denselben sicheren Schreib-Mechanismus wie save_data() (explizite Zielzeile
    statt append_row(), siehe v7.73.17 - append_row() hatte sich als unzuverlässig
    erwiesen)."""
    SCHWELLEN = [14, 5, 0]
    gesendet = 0
    try:
        alle_zoos = sorted(set(str(r[0]).strip() for r in get_cached_data("set") if len(r) >= 3 and str(r[0]).strip()))
        bereits_rows = get_cached_data("futtererinnerung")
        bereits_set = {(str(r[0]).strip().lower(), str(r[1]).strip(), str(r[2]).strip(), str(r[3]).strip()) for r in bereits_rows if len(r) >= 4}
        for zoo_name in alle_zoos:
            email = lade_einstellungen(zoo_name)
            if not email:
                continue
            for tage_rest_echt, sorte, letzte_update_str, hersteller, sackgroesse_g, barcode in berechne_futter_warnungen(zoo_name):
                # Welche Schwelle wurde erreicht/unterschritten? Die HÖCHSTE Schwelle
                # nehmen, die noch nicht für diesen Zyklus (Zoo+Sorte+letztes Update)
                # verschickt wurde - NICHT auf exakte Gleichheit prüfen (v7.74.2-Bug:
                # "genau 14" traf z.B. nie, wenn der Wert direkt von 15 auf 13 sprang).
                # Die E-Mail nennt trotzdem den ECHTEN Restbestand, nicht die Schwelle.
                schwelle_treffer = None
                for schwelle in SCHWELLEN:
                    if tage_rest_echt <= schwelle:
                        key = (zoo_name.strip().lower(), sorte, letzte_update_str, str(schwelle))
                        if key not in bereits_set:
                            schwelle_treffer = schwelle
                            break
                if schwelle_treffer is None:
                    continue
                key = (zoo_name.strip().lower(), sorte, letzte_update_str, str(schwelle_treffer))
                # Sackgröße menschenfreundlich formatieren (g oder kg)
                try:
                    sack_num = float(str(sackgroesse_g).replace(",", "."))
                    sack_text = f"{sack_num/1000:.1f} kg".replace(".0 kg", " kg") if sack_num >= 1000 else f"{int(sack_num)} g"
                except (ValueError, TypeError):
                    sack_text = ""
                produkt_bezeichnung = " – ".join([t for t in [hersteller, sorte] if t]) or sorte
                if sack_text:
                    produkt_bezeichnung += f" ({sack_text})"
                if tage_rest_echt > 0:
                    betreff = f"🐾 PetBase: {produkt_bezeichnung} reicht nur noch ca. {tage_rest_echt} Tage"
                    text = f"Hallo {zoo_name},\n\nder Futtervorrat für '{produkt_bezeichnung}' reicht laut Berechnung nur noch ca. {tage_rest_echt} Tage.\n\nBitte rechtzeitig nachkaufen, damit es nicht knapp wird.\n\nDiese E-Mail wurde automatisch von deiner PetBase-App versendet."
                else:
                    betreff = f"🐾 PetBase: {produkt_bezeichnung} sollte jetzt leer sein!"
                    text = f"Hallo {zoo_name},\n\nder Futtervorrat für '{produkt_bezeichnung}' sollte laut Berechnung heute aufgebraucht sein.\n\nBitte den Bestand prüfen und ggf. nachtragen, sobald neues Futter da ist.\n\nDiese E-Mail wurde automatisch von deiner PetBase-App versendet."
                ok, _ = sende_email(email, betreff, text, zoo_name)
                if ok:
                    naechste_zeile = len(ws_futtererinnerung.get_all_values()) + 1
                    ws_futtererinnerung.update(range_name=f"A{naechste_zeile}:E{naechste_zeile}", values=[[zoo_name, sorte, letzte_update_str, str(schwelle_treffer), datetime.now().strftime("%d.%m.%Y %H:%M")]])
                    bereits_set.add(key)
                    gesendet += 1
        invalidate_cache()
        return f"Futter-Check abgeschlossen: {gesendet} Erinnerungs-E-Mail(s) versendet."
    except Exception as e:
        return f"Fehler beim Futter-Check: {type(e).__name__}: {str(e)[:150]}"

def erstelle_erinnerung(tier_name, datum_str, typ, intervall=""):
    if not datum_str or not tier_name: return gr.update(visible=False)
    try:
        date_obj = datetime.strptime(datum_str.strip(), "%d.%m.%Y")
        days = 365 if typ == "Impfung" or intervall == "1 Jahr" else 90
        next_date = date_obj + timedelta(days=days)
        date_format = next_date.strftime("%Y%m%d")
        now_stamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
        safe_name = str(tier_name).replace(",", "")
        ics_content = (
            "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Zoo-Zentrale//DE\nMETHOD:PUBLISH\n"
            "BEGIN:VEVENT\n"
            f"UID:{now_stamp}-{safe_name}@zoozentrale\n"
            f"DTSTAMP:{now_stamp}\n"
            f"SUMMARY: 🐾 Fällig: {typ} ({tier_name})\n"
            f"DTSTART;VALUE=DATE:{date_format}\n"
            f"DTEND;VALUE=DATE:{date_format}\n"
            f"DESCRIPTION:Die Behandlung {typ} ist fällig!\n"
            "BEGIN:VALARM\n"
            "TRIGGER:-P4W\n"
            "ACTION:DISPLAY\n"
            f"DESCRIPTION:🐾 In 4 Wochen fällig: {typ} ({tier_name})\n"
            "END:VALARM\n"
            "END:VEVENT\nEND:VCALENDAR"
        )
        tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".ics")
        tmp.write(ics_content.encode("utf-8")); tmp.close()
        return gr.update(value=tmp.name, visible=True, label=f"📅 Termin speichern")
    except: return gr.update(visible=False)

def zeige_futter_phase(zoo_name, sorte):
    if not zoo_name or not sorte or sorte == "Hier Auswählen": return ""
    try:
        for r in get_cached_data("inv")[1:]:
            if len(r) > 18 and is_zoo(r[0], zoo_name) and r[8].strip() == str(sorte).strip(): return r[18]
    except: pass
    return ""

def get_gefilterte_sorten(zoo_name, tierart, futtertyp="Trockenfutter"):
    if not zoo_name: return ["Hier Auswählen"]
    tierart_lower = str(tierart).strip().lower()

    gesuchte_kategorie = "Alle"
    if any(h in tierart_lower for h in ["hund", "hünd"]): gesuchte_kategorie = "Hundefutter"
    elif any(k in tierart_lower for k in ["katz", "kater"]): gesuchte_kategorie = "Katzenfutter"
    elif any(n in tierart_lower for n in ["kaninchen", "meerschwein", "hamster", "maus", "ratte", "nager"]): gesuchte_kategorie = "Nagerfutter"
    elif any(v in tierart_lower for v in ["vogel", "papagei", "sittich", "huhn", "wachtel"]): gesuchte_kategorie = "Vogelfutter"
    elif "pferd" in tierart_lower or "pony" in tierart_lower: gesuchte_kategorie = "Pferdefutter"
    elif any(r in tierart_lower for r in ["reptil", "schildkröte", "bartagame", "schlange", "gecko"]): gesuchte_kategorie = "Reptilienfutter"
    elif "fisch" in tierart_lower or "koi" in tierart_lower or "aquarium" in tierart_lower: gesuchte_kategorie = "Aquaristik"
    elif tierart_lower != "": gesuchte_kategorie = "Sonstiges"

    gefilterte_sorten = ["Hier Auswählen"]
    try:
        for r in get_cached_data("inv")[1:]:
            if len(r) > 8 and is_zoo(r[0], zoo_name):
                sorte = r[8].strip()
                kategorie = r[19].strip() if len(r) > 19 and r[19] else "Sonstiges"
                ftyp = r[20].strip() if len(r) > 20 and r[20] else "Trockenfutter"
                if sorte and ftyp == futtertyp:
                    if gesuchte_kategorie == "Alle" or kategorie == gesuchte_kategorie or kategorie == "Sonstiges":
                        gefilterte_sorten.append(sorte)
        return gefilterte_sorten
    except: return ["Hier Auswählen"]

BARF_GEEIGNETE_TIERARTEN = {"hund", "hündin", "katze", "kater", "reptil - schlange/gecko"}
NASS_GEEIGNETE_TIERARTEN = {"hund", "hündin", "katze", "kater"}

def toggle_futterarten_verfuegbarkeit(tierart, ist_barf_aktiv, ist_nass_aktiv):
    art_lower = str(tierart).strip().lower()
    barf_geeignet = art_lower in BARF_GEEIGNETE_TIERARTEN
    nass_geeignet = art_lower in NASS_GEEIGNETE_TIERARTEN

    barf_update = gr.update(visible=True) if barf_geeignet else gr.update(visible=False, value=False)
    nass_update = gr.update(visible=True) if nass_geeignet else gr.update(visible=False, value=False)

    barf_effektiv = bool(ist_barf_aktiv) and barf_geeignet
    group_trocken_update = gr.update(visible=not barf_effektiv)
    group_barf_update = gr.update(visible=barf_effektiv)
    group_nass_update = gr.update(visible=bool(ist_nass_aktiv) and nass_geeignet)

    return {barf_cb: barf_update, group_trocken: group_trocken_update, group_barf: group_barf_update, nass_cb: nass_update, group_nass: group_nass_update}

KASTRIERT_RELEVANTE_ARTEN = {"hund", "hündin", "katze", "kater", "pferd", "pony", "kaninchen", "meerschweinchen", "hamster", "sonstiges"}
CHIP_TASSO_RELEVANTE_ARTEN = {"hund", "hündin", "katze", "kater", "pferd", "pony", "vogel", "reptil - schlange/gecko", "reptil - schildkröte/leguan", "sonstiges"}

def toggle_id_felder(tierart):
    art_lower = str(tierart).strip().lower()
    kastriert_sichtbar = art_lower in KASTRIERT_RELEVANTE_ARTEN if art_lower else True
    chip_tasso_sichtbar = art_lower in CHIP_TASSO_RELEVANTE_ARTEN if art_lower else True
    return {kastriert_f: gr.update(visible=kastriert_sichtbar), chip_f: gr.update(visible=chip_tasso_sichtbar), tasso_f: gr.update(visible=chip_tasso_sichtbar)}

def filter_futter_nach_tierart_ui(zoo_name, tierart):
    return gr.update(choices=get_gefilterte_sorten(zoo_name, tierart, "Trockenfutter"))

def filter_nassfutter_nach_tierart_ui(zoo_name, tierart):
    return gr.update(choices=get_gefilterte_sorten(zoo_name, tierart, "Nassfutter"))

def _parse_bereich(s):
    """Parst '5-10' -> (5.0, 10.0) oder '120' -> (120.0, 120.0). None bei leer/ungültig."""
    s = str(s).strip().replace(",", ".")
    if not s: return None
    if "-" in s:
        teile = s.split("-")
        try: return float(teile[0]), float(teile[1])
        except: return None
    try:
        v = float(s)
        return v, v
    except: return None

WEIGHT_BASIERTE_ARTEN = {"hund", "hündin", "katze", "kater", "pferd", "pony", "kaninchen", "meerschweinchen", "hamster"}

def berechne_trockenfutter(zoo_name, hund_gew, sorte, current_val, tierart=None):
    if tierart is not None and str(tierart).strip().lower() not in WEIGHT_BASIERTE_ARTEN:
        return current_val  # Keine gewichtsbasierte Dosierung für diese Tierart -> manuelle Eingabe
    if not zoo_name or not sorte or sorte in ("Hier Auswählen", "") or not hund_gew:
        return current_val
    try:
        gew = float(str(hund_gew).replace(",", "."))
    except:
        return current_val
    if gew <= 0:
        return current_val
    try:
        s = str(sorte).strip()
        row = next((r for r in get_cached_data("inv")
                    if len(r) > 8 and is_zoo(r[0], zoo_name) and r[8].strip() == s), None)
        if not row: return current_val
        row = row + [""] * (18 - len(row)) if len(row) < 18 else row
        paare = [(row[10], row[11]), (row[12], row[13]), (row[14], row[15]), (row[16], row[17])]

        # 1. Exakter Treffer: Gewicht liegt in einem der Kg-Bereiche
        for k, g in paare:
            k_bereich = _parse_bereich(k)
            if not k_bereich: continue
            k_low, k_high = min(k_bereich), max(k_bereich)
            if k_low <= gew <= k_high:
                g_bereich = _parse_bereich(g)
                if g_bereich:
                    return round((g_bereich[0] + g_bereich[1]) / 2, 1)

        # 2. Kein exakter Treffer: nächstgelegenen Bereich nehmen
        kandidaten = []
        for k, g in paare:
            k_bereich, g_bereich = _parse_bereich(k), _parse_bereich(g)
            if k_bereich and g_bereich:
                mitte_k = (k_bereich[0] + k_bereich[1]) / 2
                mitte_g = (g_bereich[0] + g_bereich[1]) / 2
                kandidaten.append((abs(mitte_k - gew), mitte_g))
        if kandidaten:
            kandidaten.sort(key=lambda x: x[0])
            return round(kandidaten[0][1], 1)

        return current_val
    except:
        return current_val
def hole_barf_prozent(zoo_name, typ):
    return BARF_PROZENT_TABELLE.get(str(typ).strip(), 0.025)

def berechne_barf(zoo_name, gew, barf_aktiv, barf_typ, tierart):
    if not barf_aktiv or not gew: return 0, 0, 0
    try:
        gew_kg = float(str(gew).replace(",", "."))
    except: return 0, 0, 0
    if gew_kg <= 0: return 0, 0, 0
    
    prozent = hole_barf_prozent(zoo_name, barf_typ)
    gesamt_gramm = gew_kg * 1000 * prozent
    
    tierart_lower = str(tierart).strip().lower() if tierart else ""
    anteil_protein = 0.80
    anteil_kohl = 0.20
    
    if any(t in tierart_lower for t in ["pferd", "pony", "esel", "alpaka"]):
        anteil_protein = 0.10; anteil_kohl = 0.90
    elif any(t in tierart_lower for t in ["kaninchen", "meerschwein", "chinchilla", "nager"]):
        anteil_protein = 0.20; anteil_kohl = 0.80
    elif any(t in tierart_lower for t in ["vogel", "papagei", "sittich", "huhn"]):
        anteil_protein = 0.30; anteil_kohl = 0.70
    elif any(t in tierart_lower for t in ["schlange", "gecko", "karnivor"]):
        anteil_protein = 1.00; anteil_kohl = 0.00
    elif any(t in tierart_lower for t in ["schildkröte", "leguan"]):
        anteil_protein = 0.15; anteil_kohl = 0.85
        
    return round(gesamt_gramm, 1), round(gesamt_gramm * anteil_protein, 1), round(gesamt_gramm * anteil_kohl, 1)

# ==========================================
# 7A. GRAPHICS INTERFACE (START)
# ==========================================
def on_doc_type_change(selected_type, is_aktiv):
    cols = [col_vp, col_vv, col_t1, col_t2, col_t3, col_t4, col_t5, col_t6]
    vals = [False] * 8
    if selected_type == "Pflegevertrag" and is_aktiv:
        vals[0] = True
    elif selected_type == "Vermittlungsvertrag" and is_aktiv:
        vals[1] = True
    elif selected_type == "Tierarzt Dokumente (1)":
        vals[2] = True
    elif selected_type == "Tierarzt Dokumente (2)":
        vals[3] = True
    elif selected_type == "Tierarzt Dokumente (3)":
        vals[4] = True
    elif selected_type == "Tierarzt Dokumente (4)":
        vals[5] = True
    elif selected_type == "Tierarzt Dokumente (5)":
        vals[6] = True
    elif selected_type == "Tierarzt Dokumente (6)":
        vals[7] = True
    elif selected_type == "Alle Dokumente anzeigen":
        vals = [is_aktiv, is_aktiv, True, True, True, True, True, True]
    return {col: gr.update(visible=v) for col, v in zip(cols, vals)}

def init_ui(zoo_name):
    pm = lade_modus(zoo_name)
    d = update_dropdowns(zoo_name)
    titel_text = "### 📂 Dokument / Verträge / Tierarztunterlagen" if pm else "### 📂 Tierunterlagen"
    choices = ["Bitte wählen...", "Pflegevertrag", "Vermittlungsvertrag", "Tierarzt Dokumente (1)", "Tierarzt Dokumente (2)", "Tierarzt Dokumente (3)", "Tierarzt Dokumente (4)", "Tierarzt Dokumente (5)", "Tierarzt Dokumente (6)", "Alle Dokumente anzeigen"] if pm else ["Bitte wählen...", "Tierarzt Dokumente (1)", "Tierarzt Dokumente (2)", "Tierarzt Dokumente (3)", "Tierarzt Dokumente (4)", "Tierarzt Dokumente (5)", "Tierarzt Dokumente (6)", "Alle Dokumente anzeigen"]
    return [gr.update(visible=False), gr.update(visible=pm), gr.update(visible=pm), gr.update(visible=pm)] + list(d) + [gr.update(value=titel_text), gr.update(choices=choices, value="Bitte wählen..."), d[0], d[3], d[4], gr.update(visible=pm), gr.update(visible=pm), gr.update(visible=pm)]

FUTTER_SLOT_ANZAHL = 8  # Maximale Anzahl gleichzeitig anzeigbarer Futtersorten-Kacheln mit Mini-Buttons

APP_VERSION = "7.92.4"
# Öffentlich erreichbare URL des App-Banners (aus dem Hugging-Face-Repo, nicht über
# Gradios eigene Dateiauslieferung - die ist für externe E-Mail-Clients weniger
# zuverlässig). Wird als Footer-Bild in automatischen E-Mails eingebunden.
# Nutzt SPACE_ID, das Hugging Face automatisch für jede laufende Space setzt
# (Format "username/spacename") - dadurch stimmt die URL automatisch, egal auf
# welcher Space (alt oder neu) der Code läuft, ohne manuelle Anpassung.
_space_id = os.environ.get("SPACE_ID", "Bob15982/Zoo-Zentrale-Julian")
BANNER_URL = f"https://huggingface.co/spaces/{_space_id}/resolve/main/banner.jpg"
# Kopf-Banner für die Haustierapotheke - macht den Bereich auffälliger, ohne wie
# ein echter Hintergrund die Lesbarkeit von Kategorien/Text zu beeinträchtigen
# (das Bild selbst ist recht detail- und textreich).
APOTHEKE_BANNER_URL = f"https://huggingface.co/spaces/{_space_id}/resolve/main/apotheke_banner.jpg"

# Vollständige Versionshistorie seit v7.5.0. Bei jedem neuen Release hier oben
# (an Position 0) einen neuen Eintrag ergänzen: (version, [änderungen]).
VERSION_HISTORY = [
    ("7.92.4", [
        "Aufräumrunde: Filter (⚙️ Filter), die Stammdaten/Fütterung/Gesundheit/Dokumente-Buttonleiste sowie der separate 'Tagebuch / Beobachtungen'-Button in den Stammdaten sind jetzt dauerhaft ausgeblendet - alle drei sind seit der Umstellung auf die Popup-Navigation an der unteren Leiste überflüssig geworden",
        "Haustierapotheke: Kategorien zeigen jetzt ein zur Tierart passendes Icon (🐶 Hund, 🐱 Katze, 🐴 Pferd, 🐰 Kaninchen, 🦎 Reptil, ...) statt für alle Kategorien dieselbe neutrale Pfote",
        "Bugfix Profilbild: Profilbild und die 8 Dokumente teilten sich bisher EIN hartes 4-Sekunden-Gesamtzeitlimit beim Laden eines Tiers - bei mehreren gleichzeitig langsamen Dokumenten konnte dadurch auch das Profilbild grundlos mit abgebrochen werden, obwohl es das wichtigste sichtbare Element ist. Profilbild hat jetzt sein eigenes, unabhängiges Zeitbudget",
    ]),
    ("7.92.3", [
        "Die alte Bereich-Auswahlzeile (Tiere/Futter-Zentrale/Archiv/Futter Vorrat/Vertrag nachtragen/Haustierapotheke/PDF-Export/Tagebuch-Übersicht) unter 'Verwaltung' ist jetzt ausgeblendet - komplett überflüssig geworden, seit die Navigation über die Icons unten läuft. Deutlich aufgeräumtere Ansicht",
    ]),
    ("7.92.2", [
        "Wichtiger Bugfix: JEDE Auswahl in den Popups (Tiere, Futter, Tagebuch, Konto, Sonstiges) landete auf der Startseite statt am gewünschten Ziel. Ursache: Schließen und Springen liefen in zwei getrennten Schritten - das Schließen hat dabei sofort den Button neu gezeichnet, der gerade noch mitten in seiner eigenen Aktion steckte, wodurch der zweite Schritt (der eigentliche Sprung) verloren ging. Jetzt läuft beides in einem einzigen, untrennbaren Schritt",
    ]),
    ("7.92.1", [
        "Bugfix: 'Start'-Icon reagierte gar nicht und schloss auch kein offenes Popup - jetzt ein echter Sprung, der zusätzlich jedes offene Popup mit schließt",
        "Vermutlicher Grund gefunden & behoben, warum bei Tieren immer nur Stammdaten erschienen: die Sprung-Funktionen setzten den Unterbereich über einen Umweg zurück, der die eigentlich gewünschte Auswahl (Fütterung/Gesundheit/...) teilweise wieder überschrieben haben könnte - jetzt direkter und einfacher aufgebaut",
        "Doppelte Tier-Liste entfernt: die eigene, direkt in der Tiere-Seite eingebaute Liste (aus v7.91.7) war seit dem neuen Popup überflüssig geworden und wird nicht mehr angezeigt",
        "Startseite: die Kachel oben zeigt jetzt 'Haustierapotheke' statt 'Futter Vorrat' (Vorrat ist über das Futter-Popup weiterhin erreichbar) - dafür wurde Haustierapotheke aus dem Sonstiges-Popup entfernt, um die Dopplung zu vermeiden",
        "'Menü' umbenannt in 'Sonstiges'",
        "Popup-Fenster passen sich jetzt der Breite ihres Inhalts an, statt immer die volle Bildschirmbreite einzunehmen",
    ]),
    ("7.92.0", [
        "Schritt 3 des App-Umbaus: die 5 Icons unten (außer Start) öffnen jetzt ein kleines Popup direkt über der Leiste statt sofort auf eine große Zwischenseite zu springen. 'Tiere' zeigt eine Liste zum Auswählen, danach eine zweite kleine Auswahl (Stammdaten/Fütterung/Gesundheit/Dokumente/Tagebuch) für genau dieses Tier. 'Tagebuch' zeigt direkt die Tierliste zur Auswahl. 'Futter' zeigt Scanner/Vorrat zur Auswahl. 'Konto' zeigt Einstellungen/Admin/Abmelden. 'Menü' zeigt die restlichen Bereiche (Archiv/Haustierapotheke/PDF-Export/Vertrag nachtragen). Jede Auswahl schließt das Popup und springt direkt in den gewünschten Unterbereich, nie über eine Zwischenseite",
    ]),
    ("7.91.7", [
        "Navigation entwirrt: 'Konto', 'Mein Profil' und 'Mehr' führten alle zum selben Ziel (Konto-Einstellungen) - drei verschiedene Beschriftungen für dasselbe Ziel. 'Mein Profil' zeigt jetzt direkt die Haustierapotheke, die Kachel oben (vorher 'Konto') zeigt jetzt direkt Futter Vorrat, 'Verwaltung' ist oben nicht mehr nötig. Untere Leiste: 'Mehr' heißt jetzt 'Konto' (führt weiterhin dorthin), neuer Button 'Menü' zeigt Verwaltung mit allen Bereichen zum freien Auswählen",
        "Tier-Auswahl per Dropdown ersetzt durch eine echte, antippbare Liste, gruppiert nach 'Eigene Tiere', 'Pflegetiere' und 'Urlaubsgäste' (Pflege-/Gast-Gruppen nur im Erweiterten Modus sichtbar, leere Gruppen werden ausgeblendet) - inkl. eigenem 'Neues Tier anlegen'-Knopf",
    ]),
    ("7.91.6", [
        "Wichtiger Bugfix: der 'Tagebuch'-Link war kein Timing-Problem, sondern ein echter Wettlauf zwischen zwei unabhängigen Server-Antworten (Verwaltung öffnen + Bereich wechseln), die sich teils gegenseitig überschrieben haben - unabhängig von jeder Wartezeit im Browser. Für 'Tiere'/'Futter'/'Tagebuch' unten läuft jetzt jeweils EIN einziger kombinierter Serveraufruf, kein Wettlauf mehr möglich",
        "Aufgeräumt: eine alte, nie erreichbare Tagebuch-Übersicht (toter Code, nirgends verlinkt, vermutlich ein früherer nie fertiggestellter Versuch) gefunden und entfernt, um Verwechslungen mit der neuen funktionierenden Version zu vermeiden",
        "Login-/Registrieren-Bildschirm sowie praktisch alle verbliebenen Buttons app-weit (Speichern/Bearbeiten/Abbrechen, Admin-Bereich, Futter-Vorrat-Zeilen, Tagebuch/Haustierapotheke-Karten, u.v.m.) von Emoji auf Material Symbols umgestellt",
    ]),
    ("7.91.5", [
        "Absturz behoben: Google antwortete beim Start kurzzeitig mit 503 (überlastet) genau beim ersten Verbindungsaufbau, wodurch die ganze App abstürzte statt einfach kurz zu warten. Alle Sheet-Verbindungen versuchen es jetzt bei 503/500/429 automatisch bis zu 5x mit steigender Wartezeit erneut, bevor sie aufgeben",
        "Bugfix: der 'Tagebuch'-Sprung in der unteren Navigationsleiste wirkte manchmal wie tot - Ursache war eine zu kurz bemessene feste Wartezeit (150ms) zwischen den zwei nötigen Klicks, wodurch der zweite Klick manchmal zu früh kam und vom noch laufenden ersten Schritt wieder überschrieben wurde. Jetzt wird aktiv geprüft, ob der erste Schritt wirklich fertig ist, bevor der zweite Klick erfolgt - betrifft auch 'Tiere' und 'Futter' in der Leiste",
    ]),
    ("7.91.4", [
        "Bugfix: die 5 Symbole der unteren Navigationsleiste wurden je nach Bildschirmbreite als 2-2-1 umgebrochen statt in einer Reihe zu bleiben - erzwungen auf eine Zeile",
        "'Start' unten zeigt jetzt wirklich den bestehenden Start-Tab mit den echten Dashboard-Karten, statt nur hochzuscrollen",
        "NEU: echte Tagebuch-Übersicht (Verwaltung → Tagebuch-Übersicht, auch über die untere Leiste erreichbar) - zeigt alle Tiere mit Anzahl & Datum ihrer Einträge, antippen springt direkt in dessen Tagebuch-Tab, kein Umweg mehr über die Tier-Auswahl nötig",
    ]),
    ("7.91.3", [
        "Schritt 2 des App-Umbaus: feste Navigationsleiste unten (Start/Tiere/Futter/Tagebuch/Mehr), immer sichtbar unabhängig vom Scroll-Stand. Zeigt vorerst auf die jeweils nächstliegende bestehende Funktion (z.B. 'Futter' → Futter-Zentrale, 'Tagebuch' → Tiere-Auswahl) - eigene Zielseiten dafür folgen in einem der nächsten Schritte. Technisch über Klick-Simulation auf die bereits bestehenden, bewährten Buttons gelöst statt eigener Verkabelung, um die vorhandene Logik risikoarm wiederzuverwenden",
    ]),
    ("7.91.2", [
        "Icon-Nachbesserung: 'Vermisst melden'-Symbol war ein unauffälliger Stern ohne Signalwirkung - jetzt ein deutliches, gefülltes Warndreieck. Profil-Symbol in der Status-Zeile ('Eingeloggt als...') war beim letzten Update übersehen worden (andere Komponente als die Navigation) - jetzt ebenfalls auf Material Symbols umgestellt",
    ]),
    ("7.91.1", [
        "Wichtiger Bugfix: mehrseitige Verträge (z.B. Pflegevertrag mit 4-6 Seiten) verloren beim nächsten Speichern des Tiers (auch bei unrelated Änderungen wie dem neuen Allergien-Feld) alle Seiten bis auf die erste - Ursache war eine fehlende Unterscheidung zwischen 'wirklich neu hochgeladen' und 'nur die zur Vorschau geladene erste Seite eines bestehenden Dokuments'. Jetzt per Hash-Vergleich zuverlässig unterschieden, bestehende mehrseitige Dokumente bleiben vollständig erhalten",
        "Schritt 1 des App-Umbaus: Hauptnavigation (Verwaltung/Vermisst melden/Konto, Tiere/Futter-Zentrale/Archiv/Futter Vorrat/Vertrag nachtragen/Haustierapotheke/PDF-Export, Stammdaten/Fütterung/Gesundheit/Dokumente/Tagebuch, Einstellungen/Admin) nutzt jetzt Material Symbols Icons statt Emoji - einheitlicher, moderner, sieht auf jedem Handy gleich aus",
    ]),
    ("7.91.0", [
        "NEU: Feld 'Allergien / Unverträglichkeiten' im Gesundheit-Bereich jedes Tieres - wird gespeichert, beim Laden angezeigt und erscheint zusätzlich auffällig (fett, rot) im PDF-Export der Akte, falls ausgefüllt. Datenstruktur dafür um eine Spalte erweitert (39 auf 40 Spalten)",
    ]),
    ("7.90.2", [
        "Bugfix: Foto-Upload-Felder (Tagebuch, Profilbild, Futter-Scanner) hatten sich bei zu geringer Zwangshöhe überlappt, da Text ('Bild hier ablegen/oder/Hochladen') UND die drei Auswahl-Icons alle im selben Button-Element sitzen, nicht in getrennten Bereichen. Auf min-height statt fester height umgestellt - schrumpft so weit wie möglich, wächst aber automatisch mit, falls der Inhalt mehr Platz braucht, kein Überlappen mehr",
    ]),
    ("7.90.1", [
        "Alle übrigen PDF-/Datei-Felder in der App kompakter gemacht (Download Akte, Pflegevertrag/Vermittlungsvertrag/Tierarzt-Dokumente, Nachtrag) - bestehende 'kompakte_datei'-Klasse mit den jetzt bestätigten echten Gradio-Klassen (.wrap.full, .empty.large) verstärkt",
        "NEU: PDF-Export (Verwaltung → Tiere → PDF-Export) hat jetzt eine Option '📔 Tagebuch (falls vorhanden)' - bindet den kompletten Tagebuch-Verlauf des Tieres inkl. Fotos in die Gesamt-Akte mit ein, z.B. praktisch zum Mitgeben an neue Paten von Pflegetieren statt eines separaten PDFs. Standardmäßig aus (opt-in)",
    ]),
    ("7.90.0", [
        "Die im Tagebuch gefundene Lösung (button.boundedheight) jetzt auf alle anderen Bild-/Datei-Upload-Felder in der App übertragen: Profilbild (120px), die drei Futter-Zentrale-Scanner-Bilder (150px, bewusst etwas größer für ordentliches Framing beim Fotografieren von Barcode/Etikett) und der Pflegevertrag-Upload (wie Tagebuch-PDF behandelt, da als Datei- statt Bildfeld eine andere interne Struktur hat)",
    ]),
    ("7.89.3", [
        "Foto-Upload-Feld im Tagebuch endlich mit der wirklich richtigen Klasse getroffen: es ist ein <button class=\"boundedheight\"> innerhalb von .upload-container mit fest height:240px von Gradio - die vorherigen zwei Versuche zielten auf falsche Elemente. Jetzt direkt und gezielt auf 60px reduziert",
    ]),
    ("7.89.2", [
        "PDF-Ergebnisfeld im Tagebuch jetzt bestätigt kompakt (Danke fürs Testen!). Foto-Upload-Feld nutzt offenbar eine andere interne Struktur als das PDF-Feld, die bisherige CSS griff dort nicht - stattdessen sorgte die erzwungene Mindesthöhe für ein Überlappen von Icons und Text. Zwangshöhe erstmal entfernt, echte Größen-Anpassung folgt nach Live-Inspektion der tatsächlichen Klassen",
    ]),
    ("7.89.1", [
        "PDF- und Foto-Feld im Tagebuch endlich wirklich kompakt: per Live-Inspektion (danke für die Mühe mit dem DevTools-Screenshot!) die tatsächlichen Gradio-Klassen gefunden (.wrap.full, .empty.large) - alle vorherigen Versuche zielten auf falsche/nicht existierende Klassennamen und blieben wirkungslos",
    ]),
    ("7.89.0", [
        "Tagebuch-Fotofeld zurück auf Bildvorschau (Vorschau war doch wichtig) - Größe diesmal vorsichtiger reduziert, ohne etwas zu verstecken",
        "Aufräumrunde durch den ganzen Code: Futter-Zentrale (neues Futter speichern) nutzte noch das unsichere append_row() - jetzt mit demselben race-sicheren Muster wie Tagebuch/Haustierapotheke abgesichert (append_row() + Rücklese-Prüfung). Alle elem_ids app-weit auf Eindeutigkeit geprüft (keine Duplikate mehr), keine verwaisten Verweise auf im Laufe der letzten Updates entfernte Komponenten gefunden, alle kritischen Ausgabe-Listen (login_outs, konto_toggle_outs) nochmal auf exakte Längen-Übereinstimmung gegengeprüft",
    ]),
    ("7.88.7", [
        "v7.88.6 zurückgenommen (hatte versehentlich die Upload-Icons mit versteckt, ohne den Platz zu sparen). Foto-Feld im Tagebuch jetzt ein einfacher, garantiert kompakter Upload-Knopf statt der großen Bild-Dropzone - kein Rätselraten über Gradios interne Struktur mehr nötig. PDF-Ergebnisfeld vorsichtiger angegangen (nur Mindesthöhe gelockert, nichts versteckt, das könnte sonst wieder Funktionales mit verschlucken)",
    ]),
    ("7.88.6", [
        "Tagebuch: Foto-Upload-Drop-Zone auf eine schmale Leiste zusammengestaucht (die drei Icons Hochladen/Kamera/Zwischenablage darunter reichen zum Auswählen völlig aus), PDF-Ergebnisfeld auf eine einzelne Zeile reduziert statt der bisherigen großen leeren Vorschaufläche",
    ]),
    ("7.88.5", [
        "Tagebuch: Foto-Upload-Feld und PDF-Ergebnisfeld deutlich kompakter (vorher nahmen die leeren Platzhalter viel zu viel Platz weg)",
    ]),
    ("7.88.4", [
        "Absicherung gegen stilles Hängenbleiben beim Start: Space blieb bei 'Restarting' stehen, Container-Log zeigte NICHTS außer 'Application Startup' - kein Fehler, kein Absturz, einfach endloses Warten auf eine Netzwerk-Antwort (vermutlich Google Sheets), noch bevor Python überhaupt eine Log-Zeile ausgeben konnte. Globaler 30-Sekunden-Timeout für alle Netzwerk-Verbindungen ergänzt - verwandelt ein stilles Für-immer-Hängen in einen klaren, sichtbaren Fehler im Log, der sich gezielt beheben lässt",
    ]),
    ("7.88.3", [
        "Wichtiger Bugfix: Neue Tagebuch-Einträge konnten spurlos verschwinden, wenn zwei Geräte etwa zeitgleich einen neuen Eintrag für dasselbe Tier speicherten (z.B. Android + iOS) - beide berechneten dieselbe 'nächste freie Zeile' und überschrieben sich gegenseitig, ganz ohne Fehlermeldung. Neue Einträge nutzen jetzt append_row() (lässt Google Sheets selbst serverseitig die nächste freie Zeile bestimmen, race-sicher bei gleichzeitigen Zugriffen) plus eine Rücklese-Prüfung mit Warnhinweis, falls doch mal etwas nicht ankommt. Gleiches Muster auch für neue Haustierapotheke-Einträge angewendet, da dort ebenfalls mehrere Admins gleichzeitig aktiv sein könnten",
    ]),
    ("7.88.2", [
        "Bugfix: Feld-Beschriftungen in den Stammdaten blitzten nach dem Speichern (z.B. im Tagebuch) kurz in Orange auf - Ursache war Gradios Grundthema, das intern Orange als Hauptfarbe für 'gerade aktualisiert'-Hervorhebungen nutzt. Bisher wurde das nur an einzelnen Stellen gezielt auf Braun umgestellt. Jetzt global über CSS-Variablen behoben, greift automatisch bei allen (auch zukünftigen) Gradio-Zuständen, nicht nur den bekannten",
    ]),
    ("7.88.1", [
        "Bugfix (Server-Log-Auswertung): 'erstelle_bestand_karte didn't receive enough input values' - die verkettete .then()-Reihe nach dem Login (check_saved_login → erstelle_bestand_karte → erstelle_anstehend_karte) war fehleranfällig, da current_zoo eine BrowserState-Komponente ist. Beide Karten werden jetzt direkt innerhalb von check_saved_login() mit dem bereits vorliegenden Wert berechnet statt in einer separaten, fragilen Verkettung danach",
    ]),
    ("7.88.0", [
        "Treffergenauigkeit app-weit erhöht (u.a. gegen das iOS-Problem 'Button sieht breit aus, reagiert aber nicht immer'): touch-action:manipulation verhindert Verzögerungen durch Doppeltipp-Erkennung, Mindest-Trefffläche von 44px als Untergrenze für alle Buttons",
        "Login-Bereich komplett überarbeitet: 'Anmelden'/'Registrieren' sind jetzt große, deutlich sichtbare Buttons (gleiches Muster wie der Rest der App) statt Gradios kleiner eingebauter Tabs. Nach erfolgreicher Registrierung springt die Ansicht automatisch zurück zu 'Anmelden', mit bereits ausgefülltem Zoo-Namen",
        "Nebenbei: Registrierung nutzt jetzt den sicheren Schreib-Mechanismus statt append_row()",
    ]),
    ("7.87.1", [
        "Haustierapotheke-Banner jetzt per Checkbox im Admin-Bereich ein-/ausschaltbar (kein Code-Update nötig, z.B. praktisch beim Austauschen des Bildes) - neues Sheet 'App_Einstellungen' für solche globalen Schalter",
        "Kategorie-Überschriften in der Haustierapotheke noch fetter und etwas größer",
        "Störende Symbolleiste (Vollbild/Download/Teilen) auf allen Bildern in der App entfernt (Banner, Profilbild, Tagebuch-Fotos, Futter-Scanner) - Bilder lassen sich weiterhin per Klick vergrößern/verkleinern, die Icons waren nur zusätzlicher visueller Ballast",
    ]),
    ("7.87.0", [
        "Haustierapotheke bekommt ein eigenes Kopf-Banner-Bild (analog zum Haupt-Banner) für mehr Auffälligkeit - kompakt zugeschnitten statt als Hintergrund, damit Text und Kategorien darunter gut lesbar bleiben. Bild muss als 'apotheke_banner.jpg' im Space-Repo liegen (URL wird wie beim Haupt-Banner automatisch über SPACE_ID ermittelt)",
    ]),
    ("7.86.3", [
        "Bugfix Haustierapotheke: neuer Eintrag landete in einer eingeklappten Kategorie-Karte und wirkte dadurch wie 'nicht gespeichert' - die passende Kategorie öffnet sich jetzt automatisch nach dem Speichern",
        "Kategorie-Karten in der Haustierapotheke deutlich auffälliger gestaltet (Hintergrund, Rahmen, fetter Text, Hinweis 'antippen zum Öffnen') statt unscheinbarem Fließtext",
        "Willkommensgruß für neue Nutzer komplett aktualisiert: Tagebuch, Haustierapotheke, PDF-Export, Vertrag nachtragen und Preisvergleichs-Links waren dort nicht erwähnt. Erinnerungs-Kommentar im Code ergänzt, damit das bei künftigen neuen Funktionen nicht wieder vergessen wird",
    ]),
    ("7.86.2", [
        "KRITISCHER BUGFIX: Die Seite blieb leer bzw. der Browser meldete 'Skript verlangsamt Browser' - Ursache war eine Endlos-Schleife im Tab-Ausrichtungs-Skript aus v7.86.0/7.86.1. Der MutationObserver beobachtete Klassen-Änderungen, um Ein-/Ausblenden von Tabs zu erkennen, aber derselbe Code ändert selbst Klassen (petbase_tab_stretch) - das hat den Beobachter bei jeder eigenen Änderung erneut ausgelöst und den Browser komplett blockiert. Beobachtet jetzt nur noch 'style'-Änderungen (wie Gradio Elemente tatsächlich ein-/ausblendet), nicht mehr 'class'",
    ]),
    ("7.86.1", [
        "Tab-Leisten-Ansatz aus v7.86.0 korrigiert: kein Wischen/Scrollen mehr, stattdessen das gewünschte 2-Spalten-Raster (nebeneinander, letztes Element bei ungerader Anzahl über volle Breite gestreckt), plattformübergreifend einheitlich per JS berechnet",
    ]),
    ("7.86.0", [
        "Bugfix Haustierapotheke: Klick auf 'Bearbeiten' hat das Accordion der Kategorie geschlossen, weil @gr.render beim Neuzeichnen alle Accordions zurücksetzt - jetzt wird gemerkt, welche Kategorie offen war, und beim Bearbeiten/Speichern/Abbrechen/Weitere-laden bleibt sie offen",
        "'Neuen Eintrag hinzufügen'-Formular blendet sich jetzt aus, solange eine bestehende Karte inline bearbeitet wird - bessere Übersichtlichkeit für Admins, gleiches Prinzip wie beim Tagebuch",
    ]),
    ("7.85.2", [
        "Reihenfolge der Subtabs unter Verwaltung → Tiere korrigiert: Vertrag nachtragen steht jetzt links von Haustierapotheke (war vertauscht)",
        "Alle Tab-Leisten (Haupt-Bereiche wie Subtabs) nutzen jetzt ein 2-Spalten-Raster (nebeneinander, ohne Wischen/Scrollen), das letzte Element wird bei ungerader Anzahl sichtbarer Tabs automatisch über die volle Breite gestreckt. Sichtbarkeit wird dabei per JavaScript berücksichtigt (nicht per reiner CSS-Zählung), da einzelne Tabs je nach Admin-Status/Modus dynamisch ein-/ausgeblendet werden - plattformübergreifend einheitlich",
    ]),
    ("7.85.1", [
        "Haustierapotheke: neue Auswahl '🐾 Tierart eingrenzen' direkt bei der Suche - schränkt sowohl die Suche als auch die Kategorien-Ansicht auf eine bestimmte Tierart ein, damit z.B. keine Katzen-Empfehlung auftaucht, wenn gezielt nach einer Schlangen-Empfehlung gesucht wird. Dropdown aktualisiert sich automatisch beim Öffnen des Tabs und nach jedem neuen Eintrag",
    ]),
    ("7.85.0", [
        "Haustierapotheke komplett umgebaut: Kategorien (z.B. Hund, Katze, Maus) erscheinen jetzt als eigene aufklappbare Bereiche (Accordions) statt eines Dropdown-Filters, Karten innerhalb jeder Kategorie alphabetisch nach Titel sortiert, mit '⬇️ Weitere Einträge laden'-Button pro Kategorie (wie bei der Änderungshistorie). Jede Karte hat jetzt einen echten '✏️ Bearbeiten'-Button (nur für Admins sichtbar, wie beim Tagebuch per @gr.render umgesetzt - direkt in der Karte editierbar, kein Formular zum Scrollen). Die Suche ignoriert bei Texteingabe bewusst die Kategorie-Gliederung und zeigt alle Treffer alphabetisch sortiert über alle Kategorien hinweg",
    ]),
    ("7.84.0", [
        "Tagebuch: 'Neuer Eintrag'-Formular (Titel/Text/Foto) blendet sich jetzt aus, solange gerade ein bestehender Eintrag inline in seiner Karte bearbeitet wird - vermeidet Verwirrung zwischen den beiden Modi",
        "NEU: Haustierapotheke - kontenübergreifende, von Admins kuratierte Sammlung tiermedizinischer Hausmittel-Empfehlungen. Neuer Subtab 'Haustierapotheke' unter Verwaltung → Tiere (zwischen Futter Vorrat und Vertrag nachtragen, PDF-Export steht jetzt als letztes). Nutzer können nach Kategorie blättern oder per Volltextsuche suchen (durchsucht Titel UND Inhalt, nicht nur den Eintragsnamen). Admins sehen zusätzlich ein Formular zum Anlegen neuer Einträge (Kategorie, Titel, Inhalt). Neues Sheet 'Haustierapotheke', sicherer Schreib-Mechanismus wie überall sonst (explizite Zielzeile statt append_row())",
    ]),
    ("7.83.1", [
        "Tagebuch-Bearbeiten jetzt komplett INLINE direkt in der jeweiligen Karte statt in einem Formular oben (kein Hochscrollen mehr nötig): Klick auf 'Bearbeiten' tauscht die Karte gegen echte Titel-/Text-/Foto-Eingabefelder samt eigenem Speichern/Abbrechen-Button, alle anderen Karten bleiben unverändert. Das Formular oben ist jetzt nur noch für neue Einträge zuständig",
    ]),
    ("7.83.0", [
        "Tagebuch-Bearbeiten komplett umgebaut: statt HTML+JavaScript-Brücke (die zuverlässig von Gradios HTML-Bereinigung blockiert wurde) jetzt echte, per @gr.render dynamisch erzeugte Gradio-Buttons - ein garantiert funktionierender 'Bearbeiten'-Button direkt in jeder Karte, ganz ohne JS-Tricks. Dabei auch verwaiste Verweise auf zuvor entfernte Komponenten bereinigt, die zur Laufzeit einen Fehler ausgelöst hätten",
    ]),
    ("7.82.2", [
        "Vermutliche eigentliche Ursache für den nicht reagierenden Tagebuch-'Bearbeiten'-Button: Gradios gr.HTML() bereinigt (sanitized) dynamisch gesetzten Inhalt und entfernt dabei wahrscheinlich Inline-onclick-Attribute (Sicherheitsmaßnahme gegen XSS) - das onclick im Button-HTML wurde also nie ausgeführt. Umgestellt auf Event Delegation: kein onclick mehr im HTML, stattdessen ein globaler Klick-Beobachter im statischen Seiten-Skript (unberührt von der HTML-Bereinigung), der Klicks auf die Bearbeiten-Buttons per CSS-Klasse abfängt",
    ]),
    ("7.82.1", [
        "Bugfix: Tagebuch-'Bearbeiten'-Button reagierte nicht - JS-Elementsuche war zu strikt für die tatsächliche Gradio-DOM-Struktur, jetzt robuster mit mehreren Fallback-Selektoren (deckt ab, ob elem_id auf dem Wrapper oder direkt auf Feld/Button liegt)",
        "Änderungshistorie-Popup ebenfalls mit 'Weitere laden'-Prinzip: zeigt jetzt nur noch die letzten 15 Versionen statt aller 130+, mit '⬇️ Weitere Versionen laden'-Button davor dem Schließen-Button - vorher musste man bei jedem Öffnen durch die komplette Liste scrollen, um zum Schließen-Button zu kommen",
    ]),
    ("7.82.0", [
        "Tagebuch-Layout überarbeitet gegen 'endlos lange Seite' bei vielen Einträgen über Wochen/Monate: Eingabe-Formular steht jetzt ÜBER der Liste (kein Scrollen mehr nötig, um einen neuen Eintrag zu machen), und die Liste zeigt anfangs nur die letzten 8 Einträge mit einem '⬇️ Weitere Einträge laden'-Button für ältere Einträge statt immer alle auf einmal zu stapeln",
    ]),
    ("7.81.1", [
        "Tagebuch-Bearbeiten umgebaut auf Wunsch: statt einem separaten Dropdown jetzt ein '✏️ Bearbeiten'-Button direkt unten rechts in jeder Eintrags-Karte. Klick füllt Titel/Text zum Korrigieren vor und zeigt einen Hinweis 'Du bearbeitest gerade...' inkl. Abbrechen-Button. Technisch über eine versteckte Brücke gelöst (dasselbe bewährte Muster wie beim schwebenden Zurück-Button), da die Liste als HTML gerendert wird und Buttons darin keinen Gradio-Callback direkt auslösen können",
    ]),
    ("7.81.0", [
        "NEU: Tagebuch-Einträge nachträglich bearbeiten - Dropdown 'Bestehenden Eintrag bearbeiten' füllt Titel und Text zum Korrigieren/Ergänzen vor, Speichern aktualisiert die vorhandene Zeile statt eine neue anzulegen. Bestehendes Foto bleibt erhalten, falls beim Bearbeiten kein neues hochgeladen wird",
        "NEU: Impressum-Platzhalter auf dem Login-Bildschirm (öffentlich ohne Login erreichbar, wie rechtlich für ein Impressum nötig) - Struktur für §5-TMG-Angaben vorbereitet, Inhalte sind noch Platzhalter und müssen vor produktivem Einsatz ausgefüllt werden",
    ]),
    ("7.80.0", [
        "Performance-Fix für langsames Speichern: zwei Ursachen gefunden und behoben. Erstens: die Kontakttelefon-Synchronisierung machte bisher zwei einzelne Netzwerk-Anfragen PRO anderem Tier im Konto (bei vielen Tieren summierte sich das spürbar) - jetzt ein einziger batch_update() für alle betroffenen Zellen zusammen. Zweitens: die Diagnose-Rücklese-Prüfung aus der append_row()-Fehlersuche (v7.73.13-16) las bei jedem Speichern zusätzlich das komplette, kontenübergreifende Sheet neu ein, nur um sich selbst zu bestätigen - jetzt entfernt, da das eigentliche Problem (unzuverlässiges append_row()) längst behoben und die neue Methode nachweislich zuverlässig ist",
    ]),
    ("7.79.3", [
        "Zwei getrennte Preisvergleichs-Links statt einem: '🛒 Idealo' (nur Hersteller+Sorte, kein Barcode mehr - Idealo hatte damit bei Tierfutter keine Treffer) und '🔍 Google' (Hersteller+Sorte+Barcode, Google indexiert Barcodes deutlich besser). Idealo steht zuerst, Google danach",
    ]),
    ("7.79.2", [
        "Zurück auf Idealo (statt Google Shopping) auf Nutzerwunsch, mit der Verbesserung aus v7.79.1 beibehalten: Hersteller+Sorte als primäre Textsuche statt reiner Barcode-Suche, Barcode nur noch ergänzend dazu",
    ]),
    ("7.79.1", [
        "Bugfix: Idealo hatte bei Tierfutter-Barcodes häufig keine Treffer (Barcodes dort lückenhaft gepflegt). Preisvergleichs-Link auf Google Shopping umgestellt - Hersteller+Sorte als primäre Textsuche (bei Tierfutter zuverlässiger als reine Barcode-Suche), Barcode wird ergänzend mitgegeben statt allein zu stehen",
    ]),
    ("7.79.0", [
        "NEU: 'Futter wird knapp' auf der Startseite hat jetzt pro Sorte einen '🛒 Preise vergleichen'-Link zu Idealo - sucht per Barcode (präziseste Produktidentifikation, sofern hinterlegt) oder als Fallback per Hersteller+Sorte. Kein eigenes Preis-Scraping (unzuverlässig, rechtlich unsauber, würde bei Seiten-Änderungen brechen) - Idealo vergleicht selbst live über viele Händler inkl. Versandkosten",
    ]),
    ("7.78.0", [
        "NEU: Feedback als erledigt markieren - in der Feedback-Übersicht ein Dropdown ergänzt (Auswahl des Eintrags per Datum/Absender/Thema/Kurztext) plus Button 'Als erledigt markieren & löschen'. Entfernt den Eintrag dauerhaft aus der Liste UND aus dem Google Sheet (kein Papierkorb). Auswahl bleibt auch bei inhaltlich identischen Einträgen eindeutig, da die tatsächliche Sheet-Zeilennummer intern mitgeführt wird",
    ]),
    ("7.77.3", [
        "Restliche Konto-Unterpunkte (Feedback senden, Erinnerungen, Passwort, Konto löschen, News veröffentlichen, Nutzerverwaltung) bekommen jetzt ebenfalls die passende elem_id, damit der schwebende Zurück-Button auch bei diesen Navigation korrekt im Verlauf vermerkt - durchgezogener Fix aus v7.76.2/v7.77.2",
    ]),
    ("7.77.2", [
        "Feedback-Übersicht-Button bekommt elem_id, damit der schwebende Zurück-Button diese Navigation korrekt im Verlauf vermerkt (gleicher Fix wie beim Tagebuch-Button in v7.76.2)",
    ]),
    ("7.77.1", [
        "NEU: Feedback-Übersicht im Admin-Bereich (Konto → Admin) - Liste aller eingegangenen Feedback-Einträge, neueste zuerst, mit denselben Angaben wie in der Benachrichtigungs-E-Mail (Von, Thema, Nachricht, Datum). Alle Sichtbarkeits-Ausgabelisten automatisiert auf exakte Längen-Übereinstimmung geprüft",
    ]),
    ("7.77.0", [
        "NEU: Feedback-Benachrichtigung für Admin. Wenn ein Nutzer Feedback (Fehler/Idee/Frage) einreicht, bekommt PetBase automatisch eine E-Mail mit Absender-Konto, Thema und Inhalt. Zusätzlich ein Live-Popup, solange PetBase selbst gerade in der App eingeloggt ist (fragt alle 30 Sekunden nach neuem Feedback). Nebenbei: Feedback-Speicherung nutzt jetzt den sicheren Schreib-Mechanismus statt append_row()",
    ]),
    ("7.76.4", [
        "Vorbereitung für Umzug auf eine zweite Space: Google-Zugangsdaten werden jetzt bevorzugt aus einem Secret (GOOGLE_CREDENTIALS) geladen statt aus der lokal hochgeladenen JSON-Datei - sicherer, da Secrets nie im Repo landen. Fällt auf die alte Datei zurück, falls kein Secret gesetzt ist, damit derselbe Code auf alter UND neuer Space funktioniert. BANNER_URL ermittelt sich jetzt automatisch über Hugging Faces SPACE_ID-Variable statt fest einprogrammiert zu sein - passt sich also automatisch an, egal welche Space läuft. Der Spreadsheet-Key (Verbindung zum bestehenden Sheet) bleibt unverändert, damit keine Daten verloren gehen",
    ]),
    ("7.76.3", [
        "Echten Logikfehler bei 'Anstehend' auf der Startseite gefunden: Die Felder 'Letzte Impfung/Wurmkur/Parasitenschutz' speichern das Datum des LETZTEN Termins, die Berechnung hat das aber direkt mit heute verglichen statt das nächste Fälligkeitsdatum zu berechnen - dadurch tauchte JEDE Impfung der letzten 10 Jahre als 'anstehend' bzw. 'überfällig' auf, sobald sie in der Vergangenheit lag (auch bei jährlicher Impfung nach nur 35 Tagen). Jetzt wird wie bei den ICS-Erinnerungen das nächste Fälligkeitsdatum aus letztem Termin + Intervall berechnet (Impfung 365 Tage, Wurmkur 90 Tage, Parasitenschutz je nach eingestelltem Intervall 90 oder 365 Tage). ACHTUNG: dadurch verschwinden vermutlich einige bisher angezeigte 'überfällige' Termine aus der Liste, da sie tatsächlich noch nicht fällig sind",
    ]),
    ("7.76.2", [
        "Schwebender Zurück-Button berücksichtigte den neuen Tagebuch-Button nicht, da dieser keine passende elem_id hatte (der Zurück-Mechanismus erkennt Navigation nur an IDs mit den Präfixen b_show_/b_haupt_/b_bereich_/b_konto_tab_). elem_id ergänzt - Klick auf 'Tagebuch / Beobachtungen' wird jetzt korrekt im Navigations-Verlauf vermerkt, Zurück-Button springt danach wieder sauber zum vorherigen Tab",
    ]),
    ("7.76.1", [
        "Tagebuch-Tab in der Tab-Leiste (Stammdaten/Fütterung/Gesundheit/Dokumente/Tagebuch) dauerhaft unsichtbar gemacht - stattdessen wieder ein eigener Button 'Tagebuch / Beobachtungen' direkt unter Name/Tierauswahl (an Erweiterten Modus gekoppelt wie zuvor). Klick auf diesen Button landet auf demselben Tagebuch-Inhalt, nur ohne dass der Tab selbst in der Leiste sichtbar ist",
    ]),
    ("7.76.0", [
        "Tagebuch komplett umgebaut: eigener Tab '📔 Tagebuch' neben Stammdaten/Fütterung/Gesundheit/Dokumente statt aufklappbarem Panel - Button drücken = eigene Seite, losgelöst von den Stammdaten. Neues Feld 'Titel / Überschrift' pro Eintrag, erscheint fett über dem Beobachtungstext in Liste und PDF-Export. Neue Spalte 'Titel' im Tagebuch-Sheet",
    ]),
    ("7.75.2", [
        "Tagebuch jetzt an Erweiterten Modus gekoppelt: Button nur sichtbar, wenn Erweiterter Modus aktiv ist (gleiche Logik wie beim Pflegevertrag-Upload). Beim Ausschalten des Modus schließt sich ein offenes Tagebuch-Panel automatisch mit",
    ]),
    ("7.75.1", [
        "Tagebuch-Einträge können jetzt optional mit einem Foto belegt werden - neues Feld '📷 Foto zum Eintrag' beim Speichern, Foto erscheint als Miniaturbild in der chronologischen Liste UND im PDF-Export (Bild wird automatisch mit heruntergeladen und eingebettet, Seitenumbruch berücksichtigt genug Platz dafür). Neue Spalte 'Foto_URL' im Tagebuch-Sheet",
    ]),
    ("7.75.0", [
        "NEU: Tagebuch/Beobachtungen pro Tier. Button '📔 Tagebuch / Beobachtungen' direkt unter Name+Tierauswahl (vor Pflegevertrag-Upload bzw. Tierarzt-Bereich) öffnet ein Panel mit chronologischer Liste bisheriger Einträge (neueste zuerst), einem Textfeld für neue Beobachtungen (z.B. Ankunft, Baden, Scheren, körperliche/psychische Auffälligkeiten) und einem PDF-Export des kompletten Verlaufs - z.B. zum Mitgeben an neue Paten von Pflegetieren. Neues Sheet 'Tagebuch' (Zoo_Name, Tier_Name, Datum, Eintrag), Schreibvorgänge nutzen den bewährten sicheren Mechanismus (explizite Zielzeile statt append_row())",
    ]),
    ("7.74.6", [
        "Alle automatischen E-Mails (Test-Mail, Impfung/Wurmkur/Parasiten-Erinnerungen, Futter-Erinnerungen, News-Updates) sprechen jetzt mit dem hinterlegten Account-Namen an ('Hallo PetBase,' statt 'Hallo,')",
    ]),
    ("7.74.5", [
        "Futter-Erinnerungs-E-Mails nennen jetzt Hersteller und Sackgröße mit, nicht nur die Sorte - z.B. 'Wild Plain – Huhn (2.5 kg) reicht nur noch ca. 13 Tage'. berechne_futter_warnungen() liefert dafür zusätzlich Hersteller und Sackgröße aus dem Futter-Inventar mit",
    ]),
    ("7.74.4", [
        "Alle automatischen E-Mails (Erinnerungen, Futter-Warnungen, Test-Mail) haben jetzt zusätzlich zur Textversion eine HTML-Version mit dem App-Banner als Footer-Bild, geladen von der öffentlichen Hugging-Face-Dateiablage. Nebenbei: E-Mail-Log-Eintrag nutzt jetzt denselben sicheren Schreib-Mechanismus wie save_data() (explizite Zielzeile statt append_row())",
    ]),
    ("7.74.3", [
        "Bugfix Futter-Erinnerungen: 0 E-Mails versendet trotz 'Wild Plain' bei 13 Tagen Restbestand - Ursache war eine Prüfung auf EXAKT 14/5/0 Tage, die den Wert nie traf, wenn er z.B. direkt von 15 auf 13 sprang. Jetzt wird 'Schwelle erreicht oder unterschritten' geprüft (<=14, <=5, <=0), weiterhin mit Schutz vor Doppel-Versand pro Zyklus. Die E-Mail nennt weiterhin den echten Restbestand (z.B. 13 Tage), nur die interne Nachverfolgung nutzt die Schwelle (14)",
    ]),
    ("7.74.2", [
        "Externer Cron-Aufruf für die Futter-Erinnerungen lief mit 500 Internal Server Error - Gradios eingebauter API-Mechanismus (/gradio_api/call/...) erwartet eine mehrstufige Anfrage-Choreografie, die ein simpler externer Cron-Dienst wie cron-job.org nicht mitmacht. Ersetzt durch eine eigene, simple HTTP-Route (/cron/futter-check) direkt neben der Gradio-App - normaler GET/POST-Aufruf ohne JSON-Body oder spezielle Header nötig. demo.launch() selbst bewusst unverändert gelassen (nur prevent_thread_lock ergänzt), um das bestehende PWA/Favicon-Verhalten nicht zu riskieren",
    ]),
    ("7.74.1", [
        "Neuer Button im Admin-Bereich (nur für PetBase sichtbar, wie beim Admin-Ernennen): '🥫 Futter-Check jetzt manuell auslösen' - führt dieselbe Prüfung wie der geplante tägliche Cron-Aufruf sofort aus, zum Testen vor der Cron-Einrichtung. Alle Sichtbarkeits-Ausgabelisten (Erfolgs-/Fehler-/Nicht-eingeloggt-Pfad von check_saved_login) automatisiert auf exakte Längen-Übereinstimmung geprüft, um die Art von Output-Mismatch-Bug zu vermeiden, die dieses Projekt schon öfter geplagt hat",
    ]),
    ("7.74.0", [
        "NEU: Automatische Futter-Nachkauf-Erinnerungen per E-Mail für alle Zoo-Accounts (nicht nur PetBase) - bei 14, 5 und 0 Tagen geschätztem Restbestand pro Futtersorte. Da Hugging Face Spaces keinen eingebauten Scheduler hat, läuft die Prüfung über einen extern aufrufbaren Endpunkt (api_name='futter_cron_check'), gedacht für einen täglichen Aufruf durch einen kostenlosen externen Cron-Dienst (z.B. cron-job.org). Duplikat-Schutz über neues Tracking-Sheet 'Futter_Erinnerungen_Log': pro Zoo/Sorte/Bestands-Update/Schwelle wird nur einmal verschickt. Bestandsberechnung aus dem Dashboard ('Futter wird knapp') in eine gemeinsame Funktion ausgelagert, um Code-Duplikate zu vermeiden",
    ]),
    ("7.73.18", [
        "Kritischer Bugfix: Profilbilder und der Upload-Button dafür waren komplett unsichtbar (weder gespeicherte Fotos noch die Möglichkeit, neue hochzuladen). Ursache: der height:0 + overflow:hidden-Trick aus v7.73.10 zur Höhen-Angleichung ist kollabiert, sobald die Zeile keine verlässliche Höhe lieferte, und hat dadurch den kompletten Bildbereich ausgeblendet. Höhen-Angleichungs-Versuch zurückgenommen, Profilbild-Bereich läuft wieder im normalen Layout-Fluss - Sichtbarkeit und Funktion gehen vor Kosmetik",
    ]),
    ("7.73.17", [
        "Vermutlich DER eigentliche Bug hinter 'neues Tier verschwindet' (nach ausführlicher Diagnose über mehrere Versionen bestätigt: 0 von diversen Testversuchen kamen im Sheet an, obwohl append_row() nie einen Fehler warf): ws.append_row() durch ws.update() an einer selbst berechneten Zielzeile ersetzt - dieselbe Methode, die für 'bestehendes Tier bearbeiten' bereits nachweislich zuverlässig funktioniert (bestätigt durch Flockes erfolgreichen Kategorie-Wechsel). append_row()s interne 'finde die nächste leere Zeile'-Logik war offenbar die Fehlerquelle",
    ]),
    ("7.73.16", [
        "Diagnose war irreführend: die gemeldete Zeilenzahl zählte versehentlich ALLE Zoo-Accounts im gemeinsamen Sheet zusammen (z.B. auch 'Lilalaunebärin'), nicht nur PetBase - die beobachteten Sprünge (+7, +2) waren dadurch nicht unbedingt aussagekräftig. Diagnose zeigt jetzt nur noch die eigene Zeilenzahl des aktuellen Zoo-Accounts plus die letzten 3 tatsächlich vorhandenen Tiernamen, um sichtbar zu machen was wirklich geschrieben wurde statt nur ein gefunden/nicht-gefunden zu melden",
    ]),
    ("7.73.15", [
        "Der .select()-Fix aus v7.73.14 wirkt: Speichern liefert jetzt zuverlässig eine Meldung statt spurlos zu verschwinden. Die Diagnose zeigte aber 'Zeile fehlt beim Zurücklesen' bei 'Flocke2', obwohl das Sheet bereits 14 statt der erwarteten 7 Zeilen hatte - Verdacht auf Wettlauf zwischen Schreiben und sofortigem Zurücklesen bei Google Sheets. Diagnose-Prüfung wartet jetzt bis zu 2x 1,5 Sekunden und versucht es erneut, bevor sie einen echten Fehler meldet",
    ]),
    ("7.73.14", [
        "Vermutlich DIE Ursache für spurlos verschwindende neu angelegte Tiere gefunden: tier_dropdown.change() feuerte auch dann, wenn save_data() den Dropdown-Wert nach dem Speichern selbst programmatisch setzte (seit v7.73.12 werden choices+value zusammen gesetzt) - das löste eine zweite, parallele load_tier()-Kette aus, die den noch laufenden Speicher-Vorgang störte, bevor die Bestätigung sichtbar wurde (Video-Analyse zeigte: nach Klick auf 'Profil Speichern' erschien gar keine Meldung mehr, die Seite sprang einfach zurück). Fix: .change() durch .select() ersetzt - feuert nur noch bei echter Nutzer-Auswahl, nicht bei per Code gesetzten Werten",
    ]),
    ("7.73.13", [
        "Diagnose-Erweiterung: Der Dropdown-Absturz aus v7.73.12 ist behoben, aber 'Flocke2' fehlt weiterhin im Sheet - die eigentliche Ursache liegt also woanders. save_data() liest das Sheet jetzt SOFORT nach dem Schreiben nochmal zurück und prüft, ob die neue Zeile wirklich angekommen ist. Falls nicht, erscheint statt 'Gespeichert!' jetzt eine Diagnose-Meldung mit Zeilenanzahl und Status, um sichtbar zu machen ob append_row() von Google Sheets im Hintergrund verworfen wird",
    ]),
    ("7.73.12", [
        "Vermutlich DER Bug hinter 'neues Tier verschwindet': update_dropdowns() setzte für das Tier-Auswahl-Dropdown nur choices, aber kein value. Gradio versuchte dann den bisherigen (evtl. veralteten) Client-Wert beizubehalten - passte der nicht zur frischen Liste, brach die komplette Server-Antwort mit 'X is not in the list of choices' ab (reproduziert beim Wechsel Eigenes Tier -> Pflegetier). update_dropdowns() bekommt jetzt einen expliziten select_name-Parameter, save_data() übergibt den gerade gespeicherten Namen direkt mit - choices und value werden immer synchron zusammen gesetzt",
    ]),
    ("7.73.11", [
        "Diagnose-Hilfe: Fehlermeldung beim Speichern eines Tierprofils (save_data) nicht mehr auf 20 Zeichen abgeschnitten, sondern zeigt jetzt Fehlertyp + vollen Text (bis 120 Zeichen) - Hintergrund: neu angelegtes Tier ('Flocke') taucht trotz 'Gespeichert!'-Meldung nicht im Tier_Profile-Sheet auf, Ursache noch nicht gefunden, diese Änderung soll beim nächsten Versuch mehr Klarheit bringen, falls doch ein bisher verdeckter Fehler auftritt",
    ]),
    ("7.73.10", [
        "Bugfix zu v7.73.9: bei breiten Fotos (Rosi) funktionierte die Höhenangleichung des Profilbilds an die linke Spalte, bei Hochformat-Fotos (Levi) nicht - das Bild brachte über sein Datei-Seitenverhältnis weiterhin eine eigene, sehr große natürliche Höhe mit, die die komplette Zeile mit aufblähte. Jetzt wird das Bild per height:0 komplett aus der Zeilenhöhen-Berechnung herausgenommen und stattdessen per position:absolute + min-height:100% nachträglich exakt auf die von der linken Spalte vorgegebene Höhe gezogen - funktioniert jetzt unabhängig vom Seitenverhältnis des Fotos",
    ]),
    ("7.73.9", [
        "Profilbild: Höhe passt sich jetzt unabhängig vom Seitenverhältnis des hochgeladenen Fotos exakt an die linke Spalte (Name+Tierauswahl) an, statt mal zu schmal/gequetscht, mal riesig zu wirken. Umgesetzt über align-items:stretch auf der Row plus flex:1-Kette durch alle Zwischen-Container (bewusst OHNE height:100% auf Zwischenebenen - das hatte bei den Stammdaten-Feldern in v7.73.4-7.73.6 bereits den Stretch-Mechanismus blockiert, derselbe Fehler wurde hier vermieden)",
        "Graue Hintergründe im Bild-Container und der Upload/Kamera-Werkzeugleiste (Gradios Standard-Grau #F3F4F6) entfernt, damit es zur warmen Creme-Farbpalette der App passt",
    ]),
    ("7.73.8", [
        "Profilbild: Versuch, die eingebaute Werkzeugleiste (Vollbild/Löschen-Icons) am Überdecken des Bildinhalts (meist der Tierkopf oben im Foto) zu hindern - rein per CSS (Bild bekommt oben 40px 'Sicherheitszone' und wird unten ausgerichtet, Leisten-Hintergrund transparent), diesmal bewusst OHNE JavaScript-Ersatzbuttons, da dieser Ansatz in v7.68-7.72 mehrfach andere Buttons kaputt gemacht hatte. Muss noch am Gerät geprüft werden, da die genaue CSS-Struktur der Werkzeugleiste je nach Gradio-Version variieren kann",
    ]),
    ("7.73.7", [
        "Bugfix zu v7.73.6: 'background:transparent' auf den Stammdaten-Feld-Blöcken hatte keine weiße Karte freigelegt, sondern die nackte Row-/Seitenfläche dahinter - dadurch wirkte der gesamte Bereich grau/bläulich statt weiß. Jetzt dieselbe Karten-Farbe wie im Rest der App (#FFFCF5, warmes Creme statt reinem Weiß, damit es zum bestehenden Design passt)",
    ]),
    ("7.73.6", [
        "Bugfix zu v7.73.5: graue Lücke war immer noch da, weil die Höhen-Kette vom Grid bis zur Textarea an einer Zwischenstelle abriss (Label bzw. Gradios innerer 'wrap'-Div um die Textarea hatten kein height:100%, wodurch flex-grow am Textfeld ins Leere lief). Jetzt wird die Höhe durchgängig von Feld-Block über Label/Wrap bis zur Textarea durchgereicht - nur die Textarea selbst bleibt bewusst ohne feste Höhe, um Gradios JS-Autosize für mehrzeiligen Text nicht zu zerstören",
    ]),
    ("7.73.5", [
        "Bugfix zu v7.73.4: 'height:100% !important' auf den Textfeldern hatte Gradios eigene JavaScript-Autosize-Höhe bei mehrzeiligem Text überschrieben - dadurch wurde Farbe auf eine viel zu kleine Höhe zusammengestaucht (Inhalt nur noch per Scrollen sichtbar) und die graue Lücke sprang zu einer anderen Zeile. Jetzt nur noch flex-grow ohne feste Höhe, dadurch wächst das kürzere Feld mit, ohne das längere zu kappen",
    ]),
    ("7.73.4", [
        "Stammdaten-Spalten: graue Lücke behoben, die entstand, wenn ein Feld durch längeren Text höher war als sein Nachbar (z.B. Farbe vs. Geburtsdatum) - Felder einer Zeile werden jetzt per align-items:stretch auf gleiche Höhe gezogen, das kürzere wächst automatisch mit",
        "Dropdown-Text (Tierart, Kastriert etc.) wirkte fetter/anders als der Text in normalen Feldern, da Browser <select>-Elemente mit ihrer eigenen System-Schrift rendern - jetzt font-family/font-weight/font-size erzwungen identisch zu Textfeldern",
    ]),
    ("7.73.3", [
        "Stammdaten-Felder auf iOS an Android angeglichen: hellgrauer Halo/Rahmen um jedes Feld entfernt (natives Gradio-Karten-Styling, das nur auf iOS Safari durchschien) und die native Höhenabweichung von <select> gegenüber <input> per -webkit-appearance:none behoben. Da appearance:none den nativen Dropdown-Pfeil auf allen Plattformen entfernt, wurde ein eigener SVG-Pfeil ergänzt, damit Android weiterhin einen sichtbaren Pfeil hat",
    ]),
    ("7.73.2", [
        "Stammdaten-Spalten auf Android behoben: Gradio hatte die beiden Felder pro Zeile heimlich in einen gemeinsamen Wrapper-Div gepackt, wodurch das 50/50-Grid nur ein Kind-Element sah und alles in die linke Hälfte quetschte (rechte Hälfte blieb leer). Wrapper wird jetzt per display:contents fürs Layout transparent gemacht, sodass die Grid-Regel direkt auf die beiden Felder wirkt",
    ]),
    ("7.73.1", [
        "Stammdaten-Spalten: .zweispaltig-Klasse von Flexbox auf CSS Grid mit minmax(0, 1fr) umgestellt - verhindert, dass die native Mindestbreite von Eingabefeldern (besonders bei iOS Safari) das exakte 50/50-Aufteilen verhindert. Bleibt weiterhin eng auf die vier betroffenen Zeilen begrenzt",
    ]),
    ("7.73.0", [
        "Stammdaten-Spalten: alter JavaScript-Workaround entfernt, stattdessen gezielte CSS-Klasse ('zweispaltig') nur auf die vier betroffenen Zeilen angewendet - sauberer und enger begrenzt als die bisherigen Versuche, dadurch geringeres Risiko für Nebenwirkungen an anderen Stellen",
    ]),
    ("7.72.2", [
        "Profilbild-Ersatz-Buttons-Ansatz komplett zurückgenommen - hat vermutlich versehentlich auch Upload-/Kamera-/Zwischenablage-Buttons mit ausgeblendet und die Darstellung verschlechtert statt verbessert. Zurück zum Original-Verhalten von Gradio (Symbole überlappen weiterhin das Label, aber alles andere funktioniert normal)",
    ]),
    ("7.72.1", [
        "Echten Bug gefunden, warum die Profilbild-Ersatz-Buttons aus v7.72.0 komplett wirkungslos waren: Die Funktion brach vorzeitig ab, wenn sie nicht exakt den Text 'Profilbild' in einem eigenen Element fand (z.B. wegen eines mit eingebetteten Icons) - eine Prüfung, die in dieser Version gar nicht mehr nötig war, aber übersehen wurde. Jetzt entfernt, Funktion sollte jetzt tatsächlich greifen",
    ]),
    ("7.72.0", [
        "Profilbild-Symbole komplett neu gelöst (Idee vom Nutzer): statt die Original-Symbole zu verschieben, werden sie jetzt unsichtbar (aber weiterhin funktional) gemacht und durch eigene, garantiert sichtbare Ersatz-Buttons unter dem Bild ersetzt, die beim Klick die Original-Funktion auslösen - umgeht das wiederholt gescheiterte Positionierungsproblem",
    ]),
    ("7.71.2", [
        "Beide JS-Fixes (Stammdaten-Spalten, Profilbild-Icons) robuster gemacht: weniger strenge Bedingungen, textbasierte statt klassenbasierte Suche, MutationObserver für sofortige statt verzögerte Reaktion",
    ]),
    ("7.71.1", [
        "Vorherige CSS-Grid-Regel für Stammdaten-Felder zurückgenommen, da sie auf BEIDEN Plattformen (iOS und Android) zu einspaltiger Darstellung geführt hat - war ein Fehlversuch",
        "Stammdaten-Zweispaltigkeit jetzt per JavaScript direkt gesetzt statt per CSS-Regel, da sich iOS Safari und Android Chrome bei Flexbox/Grid-Berechnung unterschiedlich verhalten haben",
        "Profilbild-Werkzeugleiste: statt einer geschätzten Pixelzahl wird jetzt die tatsächliche Label-Höhe per JavaScript gemessen und die Icons exakt darunter positioniert",
    ]),
    ("7.71.0", [
        "Stammdaten-Felder auf iOS: von Flexbox auf CSS Grid umgestellt, da Flexbox mit nowrap dort trotzdem manchmal umgebrochen hat - Grid verhält sich browserübergreifend deutlich vorhersehbarer (Name/Profilbild-Zeile bewusst ausgenommen, behält ihr 2:1-Verhältnis)",
        "Werkzeugleiste (Vollbild/Löschen) bei Bild-Feldern weiter nach unten verschoben, da der vorherige Abstand nicht ausreichte, um das Label vollständig freizugeben",
    ]),
    ("7.70.2", [
        "Code-Review durchgeführt: echten Bug in change_modus() gefunden und behoben - eine Schutzklausel (nicht eingeloggt) gab noch die veraltete Länge von vor mehreren Erweiterungsrunden zurück (16 statt 21 Werte), was beim Umschalten von Erweitertem Modus ohne aktive Sitzung zu einem Fehler geführt hätte. Alle anderen Kernfunktionen (load_tier, save_data, archive_tier, check_saved_login, Nutzerverwaltung) auf Längen-Konsistenz geprüft - keine weiteren Probleme gefunden",
    ]),
    ("7.70.1", [
        "PDF-Export: Checkboxen stehen jetzt direkt über 'Download Akte', vierte Checkbox für alle Tierarzt-Dokumente ergänzt (bisher immer fest enthalten, jetzt ebenfalls wählbar)",
    ]),
    ("7.70.0", [
        "PDF-Export: Auswahl ergänzt, was enthalten sein soll - Infoblatt, Pflegevertrag und Vermittlungsvertrag jetzt einzeln an-/abwählbar (frei kombinierbar), statt immer alles zusammen zu exportieren",
    ]),
    ("7.69.0", [
        "Banner oben in der App ist jetzt anklickbar und führt direkt zur Startseite - ohne Umweg über den Zurück-Button, setzt dabei auch dessen Verlauf zurück",
    ]),
    ("7.68.2", [
        "Zurück-Button: Selbstheilungs-Prüfung von 1x/Sekunde auf 4x/Sekunde erhöht, da der Wechsel zwischen Haupttabs sehr viele Elemente gleichzeitig aktualisiert und dadurch länger dauert als bei Sub-Tabs - sollte das Verschwinden speziell bei Haupttab-Wechseln beheben",
        "Garantierter Endpunkt 'Startseite': Genug Zurück-Klicks führen jetzt immer zur echten Startseite (Verwaltung, kein Tier ausgewählt), unabhängig davon, wo die Navigation begonnen hat",
    ]),
    ("7.68.1", [
        "Zurück-Button 'selbstheilend' gemacht: Falls er durch eine interne Aktualisierung der Seite zeitweise aus dem DOM entfernt wird, fügt er sich jetzt sofort automatisch wieder ein, statt erst beim nächsten Tab-Klick wieder aufzutauchen - sollte sowohl das gelegentliche Verschwinden als auch die uneinheitliche Funktion beheben",
    ]),
    ("7.68.0", [
        "Echten Logikfehler beim Zurück-Button gefunden: Er hat versehentlich den gerade aktiven Tab nochmal angeklickt statt zum Tab davor zu springen - ein erneuter Klick auf den bereits aktiven Tab tut aber nichts, daher schien der Button wirkungslos. Jetzt korrekt behoben",
        "Zurück-Button ist jetzt frei verschiebbar (Ziehen mit Maus/Finger) und merkt sich seine Position dauerhaft (pro Gerät/Browser), bis er erneut verschoben wird",
    ]),
    ("7.67.1", [
        "Zurück-Button komplett überarbeitet: eigener Navigations-Speicher statt Browser-History-API (war vermutlich die Ursache, warum er keine Aktion ausgelöst hat - unzuverlässig in eingebetteten Kontexten), Position jetzt vertikal mittig statt unten links, damit er immer sichtbar ist ohne scrollen zu müssen",
    ]),
    ("7.67.0", [
        "Schwebender Zurück-Button ergänzt (unten links, erscheint erst nach dem ersten Tab-Wechsel), da die native Zurück-Geste browserabhängig nicht immer zuverlässig funktioniert - nutzt dieselbe Verlaufs-Logik, springt also genauso zum vorherigen Tab zurück",
    ]),
    ("7.66.0", [
        "Native Zurück-Geste (iOS-Wischen vom Rand / Android-Zurück-Geste bzw. -Taste) springt jetzt zum vorherigen Tab innerhalb der App zurück, statt die Seite/App zu verlassen - funktioniert für Haupt-, Bereichs-, Kategorie- und Konto-Tabs",
    ]),
    ("7.65.0", [
        "Bugfix: Navigations-Reset (Verwaltung → anderer Tab → zurück) setzte zwar die Tier-Auswahl im Dropdown zurück, die Formularfelder darunter (Name, Bild etc.) blieben aber fälschlich mit dem zuletzt ausgewählten Tier gefüllt - jetzt wird load_tier korrekt mit aufgerufen, genau wie bei einer echten manuellen Auswahl",
        "Vollbild-/Löschen-Symbole bei Bildfeldern (z.B. Profilbild) liegen nicht mehr über dem Feld-Label",
        "Stammdaten-Felder werden jetzt unabhängig von der Plattform (iOS/Android) konsequent zweispaltig dargestellt",
    ]),
    ("7.64.1", [
        "Bugfix: Bei mehrseitigen PDFs (z.B. 6-seitiger Pflegevertrag) wurden manche Seiten beim Hochladen stillschweigend übersprungen, wenn der kostenlose Bilder-Hosting-Dienst zu viele schnell aufeinanderfolgende Uploads gedrosselt hat. Jetzt mit Wiederholungsversuchen und kurzer Pause zwischen den Seiten deutlich zuverlässiger",
    ]),
    ("7.64.0", [
        "ECHTE Ursache für den E-Mail-Timeout gefunden (bestätigt u.a. von einem Hugging-Face-Mitarbeiter im offiziellen Forum): Hugging Face Spaces blockiert grundsätzlich alle ausgehenden SMTP-Ports (25/465/587) - das betrifft jeden, der SMTP von einem Space aus nutzt, nicht nur uns",
        "E-Mail-Versand komplett auf Brevo-HTTP-API umgestellt (Port 443, nicht blockiert) statt klassischem SMTP - braucht jetzt einen Brevo-API-Schlüssel statt SMTP-Zugangsdaten",
    ]),
    ("7.63.0", [
        "Nutzerverwaltung: Freitextfelder durch vier Dropdowns ersetzt - Aktive Nutzer sperren, Gesperrte Nutzer entsperren, Admin-Status vergeben (nur Nicht-Admins), Admin-Status entziehen (nur aktuelle Admins) - Listen aktualisieren sich automatisch nach jeder Aktion",
        "Bugfix: 'Vollbild-Bild schließt sich bei erneutem Klick' hat versehentlich das Profilbild statt nur die Vollbildansicht gelöscht, da der falsche Button getroffen wurde - jetzt über Escape-Taste gelöst, sicherer als Button-Suche",
        "E-Mail-Versand: Secrets werden jetzt defensiv bereinigt (Anführungszeichen/Leerzeichen), Fehlermeldung zeigt jetzt den tatsächlich verwendeten Server-Namen zur besseren Fehlersuche",
    ]),
    ("7.62.0", [
        "Test-E-Mail-Button im Admin-Bereich - sendet eine Test-Mail an die eigene hinterlegte E-Mail-Adresse, um den Versand zu prüfen",
        "News können jetzt optional (Checkbox) auch per E-Mail an alle Nutzer mit hinterlegter E-Mail-Adresse verschickt werden",
        "Neuer Zähler 'Versendete E-Mails gesamt' in der Admin-Übersicht (App-Reichweite)",
        "Admin-Status kann jetzt auch wieder entzogen werden, nicht nur vergeben (nur durch Hauptadmin)",
        "'Inventar' umbenannt zu 'Futter Vorrat' in der Navigation und den Übersichtstexten",
        "Bugfix: Mehrseitige PDFs (z.B. Pflegevertrag) wurden nur mit der ersten Seite gespeichert - jetzt werden alle Seiten konvertiert, hochgeladen und beim PDF-Export vollständig eingebettet",
        "Bugfix: Telefonnummern mit führender 0 oder + wurden von Google Sheets als Zahl fehlinterpretiert und verloren diese Zeichen - jetzt wird Textformat erzwungen",
        "Vollbild-Profilbild schließt sich jetzt bei erneutem Antippen automatisch, falls der Schließen-Button außerhalb des Bildschirms liegt",
    ]),
    ("7.61.1", [
        "Filter (Alle/Eigene Tiere/Pflege & Gäste) hatte weiterhin zu wenig Platz, da er sich eine Zeile mit 'Tier wählen' teilte - steht jetzt in voller Breite darunter statt in einer engen halben Spalte",
    ]),
    ("7.61.0", [
        "Eigener Admin-Tab unter Konto ('⚙️ Einstellungen' / '👑 Admin') - alle administrativen Werkzeuge (App-Reichweite, News, Nutzerverwaltung) jetzt gebündelt, statt zwischen den normalen Einstellungen zu stehen",
        "Startseite neu geordnet: 'Futter wird knapp' steht jetzt über 'Anstehende Erinnerungen', beide zusammen über 'Tiere im Bestand' - erscheinen nur noch zentral auf der Startseite, nicht mehr zusätzlich doppelt nach Tier-Auswahl",
        "Navigations-Reset: Von Verwaltung zu einem anderen Haupt-Tab und zurück landet man jetzt wieder neutral auf der Tiere-Startseite, statt automatisch beim zuletzt ausgewählten Tier",
    ]),
    ("7.60.0", [
        "E-Mail-Versand hinzugefügt (sicher): Zugangsdaten werden ausschließlich über Hugging-Face-Space-Secrets gelesen, NIE in der Google Sheet gespeichert",
        "Neue '📧 E-Mail'-Buttons bei Impfung/Wurmkur/Parasitenschutz senden eine Erinnerung an die im Konto hinterlegte E-Mail-Adresse",
        "Status 'E-Mail-Versand eingerichtet/nicht eingerichtet' erscheint jetzt in der Admin-Übersicht (App-Reichweite)",
    ]),
    ("7.59.0", [
        "PDF-Export und Vertrag nachtragen sind jetzt eigenständige Bereichs-Tabs (neben Tiere/Futter-Zentrale/Archiv/Inventar), nicht mehr als Unter-Buttons in Archiv versteckt",
        "Archiv, PDF-Export und Vertrag nachtragen sind jetzt nur noch sichtbar, wenn Erweiterter Modus aktiv ist",
        "Vertrag nachtragen: neue Auswahl 'Aktueller Bestand' oder 'Archiv' - bei aktuellem Bestand kann jetzt direkt ein Pflege- oder Gasttier aus dem laufenden Bestand ausgewählt werden (vorher nur archivierte Tiere möglich)",
    ]),
    ("7.58.0", [
        "Bugfix: Namensänderung bei einem Tier legte ein komplett neues Profil an statt das bestehende zu aktualisieren - Ursache war, dass die Suche nach der zu ändernden Zeile über den (evtl. schon geänderten) neuen statt den ursprünglichen Namen lief. Jetzt korrekt über den ursprünglich ausgewählten Namen",
        "Filter-Auswahl (Alle/Eigene Tiere/Pflege & Gäste) steht jetzt untereinander statt bei Erweitertem Modus zusammengequetscht nebeneinander",
        "Vollbild-Profilbild auf Bildschirmgröße begrenzt, damit der Schließen-Button immer erreichbar ist, ohne seitlich scrollen zu müssen",
    ]),
    ("7.57.0", [
        "Neue Nutzerverwaltung im Admin-Bereich (Konto): Liste aller registrierten Nutzer mit E-Mail, Anzahl Tiere, letztem Login und Login-Zähler",
        "Nutzer können jetzt gesperrt/entsperrt werden (gesperrte Konten können sich nicht mehr einloggen)",
        "Blacklist-Mechanismus: Nach Löschen eines Kontos (über die App selbst) können derselbe Zoo-Name UND dieselbe E-Mail-Adresse nicht mehr neu registriert werden",
        "Mehrfach-Admin-Unterstützung: Der Hauptadmin (PetBase) kann weitere Nutzer zu Admins ernennen, die dann ebenfalls die Nutzerverwaltung sehen (Ernennen selbst bleibt dem Hauptadmin vorbehalten)",
    ]),
    ("7.56.3", [
        "Checkbox-Häkchen (BARF, Zusätzlich Nassfutter) bestätigt funktionsfähig. Radio-Punkt bei Fütterungsart (Trockenfutter/Nassfutter) auf robustere Zeichentechnik umgestellt (box-shadow statt Positionierung), da die vorherige Methode dort nicht griff",
    ]),
    ("7.56.2", [
        "Fehlender Punkt/Haken: Radio-/Checkbox-Kreise werden jetzt komplett selbst per CSS gezeichnet (Kreis, Punkt bzw. Häkchen), statt uns auf Gradios eigene, teils unterdrückte native Darstellung zu verlassen - reagiert direkt auf die eingebaute Browser-Eigenschaft ':checked'",
    ]),
    ("7.56.1", [
        "Bugfix (per markiertem Screenshot gefunden): Goldener Hintergrund blieb bei Trockenfutter/Nassfutter & BARF manchmal hängen, obwohl die Option schon abgewählt war - lag an einer zusätzlichen, unzuverlässigen CSS-Regel neben der eigentlich korrekten. Diese entfernt, nur noch die direkt zustandsreaktive Regel aktiv",
        "Fehlender Punkt/Haken im Auswahlkreis: native Eingabe-Elemente werden jetzt explizit sichtbar und ausreichend groß gehalten, damit Gradio den Haken/Punkt überhaupt zeichnen kann",
    ]),
    ("7.56.0", [
        "Ursache für 'BARF kaum sichtbar' gefunden: Anders als 'Erweiterter Modus' stand BARF nicht in einer eigenen Gruppe und bekam dadurch keine Karten-Umrandung - jetzt in eigene Gruppe gepackt, gleiche Optik wie Erweiterter Modus",
        "Aktive Auswahl-Pillen (Trockenfutter/Nassfutter, Parasitenschutz-Intervall, Filter etc.) mit deutlich breiterem Satz an CSS-Regeln abgesichert, da sich Gradios interne Struktur zwischen 5.x und 6.x unterscheiden kann",
        "'Aktualisieren'-Button in Inventar entfernt, da das Dashboard jetzt automatisch beim Öffnen lädt",
    ]),
    ("7.55.1", [
        "Das per Screenshot markierte Feld identifiziert: 'Kategorie'-Dropdown (Eigenes Tier/Pflegetier/Urlaubsgast) in Stammdaten heißt jetzt 'Tierauswahl', die umgebende graue Box ist jetzt durchsichtig und verschmilzt mit dem Hintergrund",
    ]),
    ("7.55.0", [
        "Enter/Tab springt jetzt zum nächsten Feld (mehrzeilige Felder ausgenommen, dort bleibt Enter ein Zeilenumbruch) - Speichern-Button bleibt wie gewünscht bestehen",
        "Inventar-Dashboard erscheint jetzt sofort beim Öffnen des Tabs, ohne erst 'Aktualisieren' drücken zu müssen",
        "Schließen-Button (✕) bei Bearbeiten steht jetzt direkt neben '🔧 Mehr' statt am Zeilenanfang",
        "'Neu laden'-Button im Konto entfernt (durch Gradio-5.50-Downgrade nicht mehr nötig), 'Abmelden' zentriert, Anmelden/Registrieren-Tabs auf dem Login-Bildschirm gleichmäßig verbreitert",
        "Download-Feld beim Vermisst-Steckbrief deutlich verkleinert",
        "Aktive Auswahl bei Radio-/Checkbox-Feldern (BARF, Parasitenschutz-Intervall, Filter etc.) jetzt überall deutlich sichtbar hervorgehoben, nicht nur bei Erweiterter Modus",
        "'Tierarzt Dok 1-6' umbenannt zu 'Tierarzt Dokumente (1-6)' im Auswahlmenü, Feld selbst heißt schlicht 'Tierarzt Dokument'",
        "Neues Upload-Feld für den Pflegevertrag direkt unter Kategorie bei der Tier-Anlage, erscheint automatisch bei Kategorie='Pflegetier' + aktivem Erweiterten Modus - nutzt denselben Speicherpfad wie das bestehende Feld in Dokumente",
    ]),
    ("7.54.0", [
        "PWA-Icon auf dem Homescreen gefixt: iOS Safari ignoriert beim 'Zum Home-Bildschirm hinzufügen' meistens das Web-App-Manifest und braucht einen expliziten apple-touch-icon-Tag - den gab es bisher nicht, deshalb erschien statt unserem Icon ein Seiten-Screenshot",
        "apple-touch-icon zeigt jetzt auf Gradios eigenen /pwa_icon/180-Endpunkt (aus favicon_path generiert), plus Meta-Tags für Vollbild-Start ohne Browserleiste auf iOS",
        "Android/Chrome nutzte bereits automatisch das manifest.json-Icon (Fix seit Gradio-PR #10341, in unserer 5.50.0 enthalten) - betrifft daher nur iPhones",
        "Hinweis: Bereits vorhandene Homescreen-Icons werden von iOS/Android beim Hinzufügen gecacht und müssen einmalig gelöscht + neu hinzugefügt werden, damit das neue Icon greift",
    ]),
    ("7.53.0", [
        "GROSSE ÄNDERUNG: Downgrade von Gradio 6.19.0 auf 5.50.0. Ein bestätigtes, unabhängiges Gradio-Issue (#12511) beschreibt exakt unser 'erster Klick zeigt Sichtbarkeits-Änderung nicht'-Problem als Regression, die genau beim Wechsel von 5.9 auf 6.0.2 eingeführt wurde. Keine der bekannten 5→6 Breaking-Changes (Dataframe, ChatInterface, Chatbot) betrifft unseren Code",
        "Falls dieser Test nicht hilft: einfach die vorherige Version zurückspielen",
    ]),
    ("7.52.1", [
        "Per Live-Debugging die eigentliche Ursache für 'Fütterung/Gesundheit öffnet nicht beim ersten Klick' gefunden: Die Tier-Lade-Kette (die selbst am Ende 'Stammdaten' setzt) war im Abbruch-Mechanismus NACH den Kategorie-Buttons registriert - dadurch konnte ein Kategorie-Klick diese ältere, noch laufende Kette nicht abbrechen. Jetzt davor registriert",
    ]),
    ("7.52.0", [
        "Per Live-Debugging (direkte DOM-Inspektion) bestätigt: Die 'hide'-CSS-Klasse blieb manchmal auf einem Zielbereich hängen, obwohl der Inhalt selbst korrekt aktualisiert wurde - vermutlich ein Svelte-internes Verhalten, das eine Änderung manchmal 'übersieht'",
        "Experimenteller Fix: Ein sich bei jedem Klick ändernder Dummy-Wert zwingt Svelte, die Änderung zuverlässig zu erkennen - angewendet bei Haupt-, Bereichs- und Kategorie-Tabs",
    ]),
    ("7.51.0", [
        "Live-Test aufgedeckt: Kategorie-Tabs (Stammdaten/Fütterung/Gesundheit/Dokumente) öffneten konsistent nicht beim ersten Klick - derselbe Überholt-Antworten-Mechanismus wie bei den Haupt-/Bereichs-Tabs, aber der Abbruch-Fix aus v7.50.0 war dort noch nicht angewendet",
        "Abbruch-Mechanismus jetzt konsequent auf ALLE Umschalt-Button-Gruppen ausgeweitet: Kategorie-Tabs, Tier-Auswahl/Speichern-Kette, Archiv-Umschaltung, Konto-Umschaltung",
    ]),
    ("7.50.1", [
        "Ursache für 'Bearbeiten im Inventar öffnet nicht beim ersten Klick' gefunden (live getestet + vom Nutzer mit bewusstem Einzelklick bestätigt): Ein (oft unbewusster) Doppelklick traf beim zweiten Mal auf den neu erschienenen Schließen-Button, der zufällig an derselben Bildschirmposition lag wie der ursprüngliche Bearbeiten-Button - dadurch schloss sich das Feld sofort wieder",
        "Fix: Schließen-Buttons (✕) bei Aufstocken/Bearbeiten/Mehr stehen jetzt am Zeilenanfang statt am Ende, sodass sie nicht mehr mit der Position des auslösenden Buttons überlappen",
    ]),
    ("7.50.0", [
        "ECHTE Ursache live im Browser gefunden und bestätigt: Bei schnell aufeinanderfolgenden Klicks auf verschiedene Tabs kamen Server-Antworten nicht in der richtigen Reihenfolge zurück - eine ältere, überholte Antwort konnte eine neuere überschreiben. Live nachgestellt: 5 Tabs schnell hintereinander geklickt, am Ende zeigte sich der ERSTE statt der letzte Klick",
        "Fix: Jeder Haupt- und Bereichs-Tab-Klick bricht jetzt die noch offenen (überholten) Anfragen der anderen Buttons derselben Ebene ab, damit sich Antworten nicht mehr gegenseitig überholen können",
    ]),
    ("7.49.0", [
        "Vermutlich DER Fund zum monatelangen 'erst beim zweiten Klick'-Problem: Die Eruda-Debug-Konsole, die sich beim Login als 'PetBase' automatisch geladen hat, fing Gradios eigene Netzwerk-Anfragen ab und brach sie dabei offenbar ab - laut Konsolen-Stack-Trace des Nutzers. Komplett entfernt.",
        "Das würde erklären, warum das Problem plattformübergreifend (PC und Handy) auftrat, praktisch jeden Button betraf und sich nicht zuverlässig reproduzieren ließ - alles typisch für einen selbst verursachten Netzwerk-Abfang-Konflikt, nicht für einen echten App-Fehler",
    ]),
    ("7.48.0", [
        "Alle Haupt- und Bereichs-Tab-Buttons (Verwaltung/Vermisst/Konto, Tiere/Futter-Zentrale/Archiv/Inventar) von zwei- bzw. vierstufigen .then()-Ketten auf jeweils EINEN einzigen Server-Aufruf zusammengeführt - eliminiert unnötige Mehrfach-Rundreisen pro Klick komplett bei diesen Buttons",
        "Weiterer gezielter Versuch gegen das durchgängige 'erst beim zweiten Klick'-Problem, diesmal durch Reduzierung der Server-Rundreisen statt durch Warm-up-Versuche",
    ]),
    ("7.47.3", [
        "Neuer, gezielter Versuch gegen das durchgängige 'erst beim zweiten Klick'-Problem: ein einziger, unsichtbarer Aufruf beim Laden weckt Gradios Warteschlangen-Verbindung sofort auf (Verdacht: der allererste Klick nach dem Laden trifft manchmal auf eine noch nicht ganz aufgebaute Verbindung) - diesmal bewusst nur EIN Aufruf statt der langen Kette von v7.36.3, die frühere Verzögerungen verursacht hatte",
    ]),
    ("7.47.2", [
        "Experimenteller, aber gut begründeter Versuch gegen das durchgängige 'öffnet erst beim zweiten Tippen'-Problem: touch-action:manipulation ergänzt, um die browsereigene Doppel-Tipp-Erkennungsverzögerung zu verhindern (die seit dem Aktivieren von Pinch-to-Zoom in v7.46.2 vermutlich neu hinzugekommen ist bzw. sich verschlimmert hat) - ohne Zoomen zu deaktivieren",
    ]),
    ("7.47.1", [
        "Bugfix (iPhone): Tab-Beschriftungen (Stammdaten/Fütterung/Gesundheit/Dokumente etc.) wurden auf schmaleren Bildschirmen abgeschnitten ('tammdate' statt 'Stammdaten') - Ursache war mein eigener Gewicht/Menge-Fix, der versehentlich auch die Tab-Reihen erfasst hatte. Tab-Reihen jetzt gezielt ausgenommen, dürfen wieder normal umbrechen",
    ]),
    ("7.47.0", [
        "Übersicht in Tiere umstrukturiert: 'Tiere im Bestand' verschwindet jetzt, sobald ein Tier ausgewählt ist; Stammdaten stehen direkt unter der Tier-Auswahl; 'Anstehende Termine' rutschen unter die Stammdaten des ausgewählten Tieres",
    ]),
    ("7.46.2", [
        "Zoomen (Pinch-to-Zoom) jetzt auf beiden Plattformen ausdrücklich erlaubt, für bessere Lesbarkeit bei Bedarf",
    ]),
    ("7.46.1", [
        "Bugfix (iOS): Beschriftungen wie 'Gewicht 1'/'Menge 1' konnten auf schmaleren/anders skalierten Bildschirmen (z.B. iPhone) ineinander überlappen - dürfen jetzt bei Bedarf auf eine zweite Zeile umbrechen statt zu überlappen, zusätzlich kompaktere Schrift auf sehr schmalen Bildschirmen",
    ]),
    ("7.46.0", [
        "App-Titel auf 'PetBase' gesetzt und PWA-Icon aktiviert: 'Zum Startbildschirm hinzufügen' aus dem Browser zeigt jetzt das eigene Icon und den Namen 'PetBase' statt einer generischen Browser-Kachel",
    ]),
    ("7.45.5", [
        "Bugfix: Gewicht/Menge im '🔧 Mehr'-Formular waren wieder untereinander statt nebeneinander - lag an der zusätzlichen Akkordeon-Verschachtelung dort, jetzt allgemeiner gelöst (betrifft auch andere verschachtelte Stellen)",
        "Weiterer Versuch, das alte Akkordeon-Pfeilsymbol zuverlässig auszublenden (zeigte vorher beide Symbole gleichzeitig)",
    ]),
    ("7.45.4", [
        "Bugfix: Akkordeon-Symbol zeigte kaputte Zeichen ('ƐƐ') statt drei Punkte - lag an einer fehlerhaften Zeichen-Kodierung, jetzt mit echtem Unicode-Zeichen behoben",
        "Zusätzlich die vermutlich eigentliche Pfeil-Quelle (native HTML-Markierung) gezielter ausgeblendet, da das alte Pfeilsymbol vorher weiterhin sichtbar war",
    ]),
    ("7.45.3", [
        "Bugfix: Gewicht/Menge-Felder (und andere Zwei-Spalten-Zeilen) waren durch die verschachtelten Karten untereinander statt nebeneinander gerutscht - jetzt per CSS erzwungen nebeneinander",
        "Akkordeon-Symbol in allen Akkordeons von Pfeil/Dreieck auf klassische drei Punkte (⋮) umgestellt",
    ]),
    ("7.45.2", [
        "Bugfix: 'Archivieren' war rot statt braun - korrigiert (Vermisst-Steckbrief bleibt bewusst rot)",
        "Verbleibende orange Beschriftungen (z.B. Kategorie, Fütterungsart) über zusätzliche Gradio-Variablen auf Braun umgestellt",
        "Ausgewählte Radio/Checkbox-Option (z.B. Trockenfutter/Nassfutter) heller gemacht - vorher war die dunkle Schrift auf dunkelbraunem Grund kaum lesbar",
    ]),
    ("7.45.1", [
        "Bugfix: 'Speichern' beim Futter-Scan und 'Konto erstellen' waren fälschlich rot (Löschen-Farbe) statt braun (normale Aktion)",
        "Native Gradio-Elemente (Radio-Buttons wie Trockenfutter/Nassfutter, Checkboxen, Slider) nutzen jetzt ebenfalls Braun statt des alten Orange - vorher hatten diese noch die alte Akzentfarbe",
    ]),
    ("7.45.0", [
        "Farbpalette an den PetBase-Banner angepasst: warmes Creme/Sand statt kühlem Google-Grau, warmes Braun statt grellem Orange als Akzentfarbe - das Banner fügt sich jetzt nahtlos in die Seite ein statt in einer grauen Box zu sitzen",
    ]),
    ("7.44.0", [
        "Großer Kurswechsel: Dark Mode komplett entfernt, App läuft jetzt in einem hellen, freundlichen Material-Design-Look (stark an Stock-Android angelehnt) - weißer Hintergrund, dezente graue Flächen, klare dunkle Schrift",
        "Alle Dashboard-/Übersicht-Karten (Konto, Inventar, Tiere-Übersicht) auf helle Farben umgestellt",
        "Das ist Schritt 1 des angeforderten Kurswechsels weg vom '2010-Gefühl' hin zu einem modernen, simplen Erscheinungsbild",
    ]),
    ("7.43.0", [
        "Design-System 2.0: durchgängige Card-Optik für alle Formulare/Gruppen (dezenter Schatten, weicher Rand), verfeinerte Farbpalette, sanfte Verläufe bei Haupt-Buttons, klarere Überschriften-Hierarchie",
        "Neue Übersicht beim Öffnen von Tiere (solange kein Tier ausgewählt ist): Anzahl Tiere, anstehende Impfungen/Wurmkuren/Parasitenschutz der nächsten 30 Tage, Futter das bald ausgeht",
        "Das ist Schritt 1+2 der Modernisierung (visuelles Fundament + Startübersicht). Schritt 3 (Formulare entschlacken) folgt als nächstes",
    ]),
    ("7.42.0", [
        "Das '🔧 Mehr'-Detail-Formular (Hersteller, Fütterungsart, Gewichts-Brackets) erscheint jetzt ebenfalls direkt inline unter der jeweiligen Sorte, statt an einer gemeinsamen Stelle - konsistent mit Aufstocken und Bearbeiten",
        "Das frühere gemeinsame Formular für alle Sorten wurde komplett entfernt",
    ]),
    ("7.41.0", [
        "Aufstocken und Bearbeiten erscheinen jetzt direkt inline unter der jeweiligen Futtersorte (Futter 1 -> Formular Futter 1 -> Futter 2 -> ...), statt an einer gemeinsamen Stelle",
        "Inline-Bearbeiten zeigt nur noch Bestand + Sackgröße sowie einen 🗑️ Löschen-Button - für seltene Detail-Änderungen (Hersteller, Fütterungsart, Gewichts-Brackets) gibt es einen '🔧 Mehr'-Link ins gemeinsame Detail-Formular",
        "Neuer Löschen-Button direkt im Inline-Formular, um eine Futtersorte komplett aus dem Bestand zu entfernen",
        "Maximale Anzahl gleichzeitig angezeigter Sorten von 10 auf 8 reduziert (mehr Felder pro Sorte als zuvor)",
    ]),
    ("7.40.0", [
        "Inventar-Formulare aufgeräumt: Futter-Auswahl-Dropdowns bei Aufstocken und Bearbeiten ausgeblendet (nicht mehr nötig, da Sorte jetzt direkt über die Mini-Buttons vorausgewählt wird)",
        "Fütterungsart und die vier Gewicht/Menge-Felder wandern beim Bearbeiten in ein aufklappbares 'Erweitert'-Akkordeon",
        "Reihenfolge umgestellt: Dashboard mit Futtersorten zuerst, die Aufstocken/Bearbeiten-Formulare erscheinen jetzt darunter statt darüber",
    ]),
    ("7.39.1", [
        "Neues Futter hinzufügen aktualisiert jetzt automatisch die Dashboard-Übersicht samt Mini-Buttons - kein manuelles 'Aktualisieren' mehr nötig",
    ]),
    ("7.39.0", [
        "Inventar umgebaut: Die großen 'Aufstocken'/'Bearbeiten'-Tabs oben sind komplett entfernt",
        "Stattdessen hängen ➕ Aufstocken und ✏️ Bearbeiten jetzt direkt an der echten Live-Karte jeder einzelnen Futtersorte (keine separate vereinfachte Liste mehr)",
        "Formulare haben einen eigenen ✕ Schließen-Button, da es keine übergeordneten Umschalt-Buttons mehr gibt",
    ]),
    ("7.38.0", [
        "Tabs merken sich keine Unter-Auswahl mehr: Beim Wechsel eines Haupt- oder Bereichs-Tabs wird immer die Übersicht gezeigt (Kategorie zurück auf Stammdaten, offene Formulare in Archiv/Inventar/Konto schließen sich)",
        "Futter-Übersicht im Inventar kompakter gestaltet",
        "Unter jeder Futtersorte gibt es jetzt direkt ➕ Aufstocken und ✏️ Bearbeiten Mini-Buttons, die automatisch die richtige Sorte vorauswählen",
    ]),
    ("7.37.1", [
        "Die 25-stufige Vorwärm-Kette aus v7.36.3 wieder komplett entfernt: Sie war reine Spekulation ohne bestätigten Nutzen und hat vermutlich neue Verzögerungen verursacht (eigene Klicks reihten sich hinter der Kette in die Warteschlange ein, fühlte sich wie 'angezogene Handbremse' an)",
        "Die tatsächlich bestätigten Fixes bleiben erhalten: Dictionary-Format überall, gegenseitiger Ausschluss bei Toggle-Paaren, Konto-Akkordeons entfernt, Archiv/Inventar-Umstrukturierung",
    ]),
    ("7.37.0", [
        "Archiv und Inventar umstrukturiert: PDF-Export/Vertrag nachtragen bzw. Aufstocken/Bearbeiten stehen jetzt direkt oben als Unter-Navigation (wie bei Tiere die Kategorien), statt unten im Formular versteckt zu sein",
        "Diese Buttons zeigen jetzt auch optisch den aktiven Zustand (Unterstrich), wie die anderen Tab-Ebenen",
    ]),
    ("7.36.3", [
        "Vorwärm-Sequenz beim Laden auf WIRKLICH alle Umschalt-Buttons ausgeweitet, nicht nur die äußeren Bereiche: jetzt auch News/Feedback/Erinnerungen/Passwort/Konto löschen (Konto), PDF-Export/Vertrag nachtragen (Archiv), Aufstocken/Bearbeiten (Inventar), alle vier Tiere-Kategorien und Foto-Scan",
    ]),
    ("7.36.2", [
        "Experimenteller Workaround für Archiv/Inventar/Konto (öffneten erst beim zweiten Versuch): App durchläuft beim Laden einmal automatisch diese Bereiche, um Gradios verzögerte Darstellung vorwegzunehmen, bevor der Nutzer sie zu sehen bekommt",
        "Keine JavaScript-Ursache mehr gefunden (vollständig durchsucht) - vermutlich ein Darstellungsverhalten in Gradio selbst, betrifft auffällig die zuletzt im Code erstellten Bereiche",
    ]),
    ("7.36.1", [
        "Bugfix: Aufstocken schloss sich nicht, wenn man Bearbeiten öffnete (waren unabhängige Schalter statt eines Paares) - jetzt schließt das eine automatisch, wenn das andere geöffnet wird. Gleiches für PDF-Export/Vertrag nachtragen in Archiv",
        "Kritischer Bugfix: Der obige Umbau hatte eine verwaiste Referenz auf entfernte Variablen hinterlassen (hätte die App zum Absturz gebracht) - gefunden und korrigiert, bevor ausgeliefert",
    ]),
    ("7.36.0", [
        "Ursache für 'Konto öffnet erst beim zweiten Versuch' gefunden: Konto enthielt als einziger Hauptbereich noch echte Gradio-Akkordeons (News, Einstellungen, Feedback, Erinnerungen, Passwort, Konto löschen) - diese jetzt auf dasselbe zuverlässige Button+Feld-Muster umgestellt wie der Rest der App",
    ]),
    ("7.35.0", [
        "Vollständige Konsistenz-Bereinigung: BARF/Nassfutter- und Dokumententyp-Umschaltung ebenfalls auf das zuverlässige Dictionary-Format umgestellt",
        "Bugfix: Trockenfutter/BARF/Nassfutter-Anzeige verließ sich beim Tier-Laden auf eine verkettete Reaktion, die nicht immer zuverlässig auslöste - lädt jetzt direkt die korrekte Ansicht mit",
        "Nach dem Archivieren eines Tieres schließt sich die Detailansicht jetzt zuverlässig, statt möglicherweise eine veraltete Ansicht stehen zu lassen",
        "Gesamten Code auf verwaiste Referenzen und Formatinkonsistenzen geprüft - keine weiteren gefunden",
    ]),
    ("7.34.2", [
        "Bugfix: Beim Tier-Wechsel oder Speichern wurde nur Stammdaten sichtbar gemacht, ohne Fütterung/Gesundheit/Dokumente zurückzusetzen - falls man zuvor auf einer anderen Kategorie war, blieben beide gleichzeitig sichtbar. Setzt jetzt beim Laden/Speichern konsequent auf Stammdaten zurück",
    ]),
    ("7.34.1", [
        "Dictionary-Rückgabeformat aus v7.34.0 jetzt auf ALLE Auf/Zu-Buttons ausgeweitet, die es vorher noch nicht hatten: Foto-Scan, PDF-Export, Vertrag nachtragen, Aufstocken, Bearbeiten, Onboarding-Popup, Versionshistorie",
        "Zuvor war der Fix nur auf die drei Haupt-Navigationsebenen angewendet worden, nicht auf die einzelnen Auf/Zu-Buttons innerhalb der Bereiche",
    ]),
    ("7.34.0", [
        "Tab-Umschaltung komplett neu implementiert nach Gradios offiziellem Dokumentations-Muster: Rückgabe als Dictionary mit Komponenten als Schlüssel statt als Positions-Liste, einstufig statt zweistufig",
        "Recherche bestätigt bekannte Gradio-Regressionen bei dynamisch umgeschalteten Bereichen in neueren Versionen - dieser Ansatz folgt jetzt exakt Gradios eigenem kanonischen Beispiel für Sichtbarkeits-Umschaltung",
    ]),
    ("7.33.0", [
        "Kritischer Architektur-Fix: Client-seitige JavaScript-Sichtbarkeits-Hacks komplett entfernt (kämpften gegen Gradios eigenen Server-Zustand -> Effekt 'öffnet für einen Moment und schließt sofort wieder'). Alle Tab-Ebenen laufen jetzt ausschließlich über Python, das Gradios Zustand korrekt aktualisiert",
        "Vollständiges Audit: keine doppelten Klick-Bindungen, keine doppelten IDs, keine verwaisten Referenzen mehr gefunden",
        "Schriftgröße app-weit etwas verkleinert für ein weniger überladenes Erscheinungsbild",
    ]),
    ("7.32.1", [
        "Kritischer Bugfix: Beim Wechseln zwischen den neuen Tab-Buttons blieb der vorherige Inhalt sichtbar und der neue kam nur dazu, statt ihn zu ersetzen - jetzt zweistufig (erst alles verstecken, dann garantiert nachgelagert nur das Ziel zeigen)",
    ]),
    ("7.32.0", [
        "Alle drei Tab-Ebenen (Verwaltung/Vermisst melden/Konto, Tiere/Futter-Zentrale/Archiv/Inventar, Stammdaten/Fütterung/Gesundheit/Dokumente) laufen jetzt über eigene Buttons statt Gradios Tabs-Komponente - kein \"⋮\"-Overflow-Menü mehr möglich",
        "Buttons sind optisch als Reiter gestaltet (flache Kanten, farbiger Unterstrich für den aktiven Tab) - auf den ersten Blick nicht von echten Tabs zu unterscheiden",
        "Nutzt dieselbe reine Python-Sichtbarkeits-Logik ohne JavaScript, die sich beim Aufstocken/Bearbeiten-Fix als zuverlässig erwiesen hat",
    ]),
    ("7.31.2", [
        "Tabs werden jetzt senkrecht untereinander statt nebeneinander angezeigt - verhindert Gradios \"⋮\"-Overflow-Menü zuverlässiger, da kein Platzmangel in der Breite mehr entstehen kann",
    ]),
    ("7.31.1", [
        "Kritischer Bugfix: App startete gar nicht mehr (NameError) - verwaiste Verkabelung des in v7.31.0 entfernten Vermisst-Toggle-Buttons wurde übersehen und jetzt entfernt",
    ]),
    ("7.31.0", [
        "Stammdaten/Fütterung/Gesundheit/Dokumente-Felder erscheinen jetzt erst, wenn wirklich ein Tier ausgewählt ist (vorher immer sichtbar)",
        "\"🚨 Vermisst melden\" ist jetzt ein eigener Haupt-Tab mit eigener Tier-Auswahl - kein Umweg mehr über die Verwaltung nötig, direkt auffindbar und nutzbar",
    ]),
    ("7.30.2", [
        "Tab-Umbruch-CSS (verhindert das \"⋮\"-Overflow-Menü) robuster gemacht: zielt jetzt zusätzlich auf die ARIA-Rolle [role=\"tablist\"], falls sich Gradio-6-interne Klassennamen geändert haben - betrifft alle Tab-Ebenen (Verwaltung/Konto, Bereichs- und Kategorie-Tabs)",
    ]),
    ("7.30.1", [
        "Kritischer Bugfix: Aufstocken/Bearbeiten (und Vermisst melden, Foto-Scan, PDF-Export, Vertrag nachtragen) öffneten sich gar nicht - Ursache war eine eigene, fehlerhafte JS-Zusatzabsicherung aus v7.29.0, die das korrekte Anzeigen sofort wieder rückgängig gemacht hat. Diese wurde entfernt, Gradios eigene Sichtbarkeits-Logik war für diese einfachen Auf/Zu-Bereiche bereits zuverlässig",
    ]),
    ("7.30.0", [
        "Grundlegender Umbau gegen das \"zeigt beim ersten Klick nichts\"-Problem: Bereichs-Auswahl (Tiere/Futter-Zentrale/Archiv/Inventar) und Kategorie-Auswahl (Stammdaten/Fütterung/Gesundheit/Dokumente) laufen jetzt über echte, native Gradio-Tabs statt über Buttons mit ein-/ausgeblendeten Feldern",
        "Diese Tabs waren in der App schon immer zuverlässig (Verwaltung/Konto, Anmelden/Registrieren) - das Problem sollte damit an der Wurzel gelöst sein",
    ]),
    ("7.29.1", [
        "Kritischer Bugfix (vermutliche Ursache): SSR-Modus (Server-Side Rendering) explizit deaktiviert - ein bekanntes, offenes Gradio-6-Problem verursacht Hydrations-Fehler genau bei bedingt sichtbaren Bereichen (Tabs, Umschalt-Buttons), die erst beim zweiten Versuch korrekt anzeigen",
    ]),
    ("7.29.0", [
        "JS-Zwangsdurchsetzung (aus dem Gesundheit-Fix) jetzt app-weit auf alle Auf/Zu-Bereiche angewendet: Vermisst melden, Futter-Zentrale-Scan, PDF-Export, Vertrag nachtragen, Aufstocken, Bearbeiten sowie die Haupt-Bereichsauswahl (Tiere/Futter-Zentrale/Archiv/Inventar)",
    ]),
    ("7.28.0", [
        "Bugfix: Gesundheit (und andere Kategorien) zeigten nach dem ersten Klick manchmal keine Daten - zusätzliche JavaScript-Zwangsdurchsetzung sorgt jetzt garantiert für korrekte Sichtbarkeit, unabhängig von Gradios eigener Reaktivität",
        "requirements.txt: Gradio-Version gepinnt (6.19.0) statt unversioniert, damit sich das Verhalten nicht mehr bei jedem Neustart ändern kann",
    ]),
    ("7.27.4", [
        "Gradio-6-Warnung behoben: css-Parameter läuft jetzt korrekt über launch() statt über den Blocks()-Konstruktor",
    ]),
    ("7.27.3", [
        "Vollständiges Code-Audit: alle Funktions-Rückgaben gegen ihre UI-Verkabelung geprüft (keine weiteren Zählfehler gefunden)",
        "Verwaiste/falsch eingerückte Abschnitts-Kommentare aus früheren Umbauten bereinigt",
        "Unbenutzte Importe und tote Funktionen geprüft - keine gefunden",
    ]),
    ("7.27.2", [
        "Kritischer Bugfix: Login schlug bei jedem Versuch fehl (Seite blieb kaputt/leer), weil seit dem Akkordeon-Umbau (v7.27.0) versucht wurde, eine Beschriftung auf ein Feld zu setzen, das keine Beschriftung mehr unterstützt - die Beschriftung läuft jetzt korrekt über einen eigenen Text-Baustein",
    ]),
    ("7.27.1", [
        "Bugfix: Dark-Mode-Erzwingung löste bei jedem Aufruf über die huggingface.co-Seite einen Seiten-Reload aus, der laufende Anfragen abbrach und kurzzeitig zu kaputter Darstellung führte - jetzt per direkter CSS-Klasse ohne Reload gelöst",
    ]),
    ("7.27.0", [
        "Stammdaten/Fütterung/Gesundheit/Dokumente sind jetzt normale Felder statt Akkordeons - behebt endgültig das Problem, dass Inhalte nach Button-Klick leer/geschlossen blieben",
    ]),
    ("7.26.2", [
        "Bugfix: Kategorie-Akkordeons (Stammdaten/Fütterung/Gesundheit/Dokumente) wurden zwar sichtbar, blieben dabei aber optisch geschlossen - Sichtbar-machen und Öffnen laufen jetzt als zwei garantiert nacheinander ablaufende Schritte statt gemeinsam in einem Update",
    ]),
    ("7.26.1", [
        "Bugfix: Kategorie-Buttons luden bei jedem Klick unnötig alle Bilder/Dokumente neu, was zu Verzögerungen und gelegentlichem Fehlverhalten führte - zurückgesetzt auf schnelles reines Umschalten",
        "Bild-/Dokument-Downloads beim Tier-Laden haben jetzt ein hartes Gesamtzeitlimit (4 Sekunden) statt potenziell unbegrenzt zu warten",
    ]),
    ("7.26.0", [
        "Design an Android 17 / Material 3 Expressive angeglichen: weichere größere Rundungen, Frosted-Glass-Effekt bei Popups, federndere Übergänge bei Buttons und Tabs",
        "Dashboard-Karten im Inventar ebenfalls weicher gestaltet",
    ]),
    ("7.25.0", [
        "Kontakttelefon wird jetzt automatisch bei allen Tieren desselben Accounts übernommen, sobald es bei einem Tier geändert wird",
        "Neues Feld \"Kontakttelefon 2\" als Backup-Nummer (z.B. Familie), erscheint auch im Vermisst-Steckbrief",
        "Kategorie-Buttons (Stammdaten/Fütterung/Gesundheit/Dokumente) laden jetzt bei jedem Klick frische Daten und öffnen garantiert direkt, kein doppeltes Klicken mehr nötig",
    ]),
    ("7.24.1", [
        "Filter steht bei aktivem Erweitertem Modus jetzt direkt neben der Tier-Auswahl statt darüber",
    ]),
    ("7.24.0", [
        "Tiere/Archiv/Inventar: Inhalte erscheinen jetzt direkt unter der Auswahl (Tier-Auswahl bzw. Hauptaktion), die Auswahl-Buttons stehen danach statt davor",
        "So landet man beim Aufrufen einer Unterkategorie direkt beim Inhalt, ohne an den Buttons vorbei scrollen zu müssen",
    ]),
    ("7.23.0", [
        "Nach Aufstocken/Bearbeiten/Löschen im Inventar klappen sich die Formulare automatisch zu und die Ansicht springt zurück zum Dashboard",
    ]),
    ("7.22.0", [
        "Inventar-Dashboard komplett überarbeitet: zeigt jetzt Name, Sackgröße, aktueller Lagerbestand und verbleibende Tage pro Futtersorte",
        "Aktueller Lagerbestand wird jetzt live berechnet (Bestand minus Tage seit letzter Aktualisierung × täglicher Verbrauch aus den Stammdaten)",
        "Neues Feld \"Sackgröße\" beim Bearbeiten von Futter, unabhängig vom aktuellen Lagerbestand",
        "Dashboard-Karten deutlich kompakter gestaltet",
    ]),
    ("7.21.0", [
        "Archiv: \"PDF-Akte exportieren\" und \"Vertrag nachtragen\" jetzt per Button ein-/ausblendbar statt permanent als Akkordeon sichtbar",
        "Inventar: \"Aufstocken\" und \"Bearbeiten\" jetzt per Button ein-/ausblendbar, Futter-Übersicht bleibt direkt sichtbar",
    ]),
    ("7.20.0", [
        "Futter-Zentrale umgebaut: manuelle Eingabe aller Futterdaten ist jetzt Standard, sofort sichtbar",
        "Fotoscan (3 Bilder + Auswertung) ist jetzt eine optionale Zusatzfunktion, per Button ein-/ausblendbar",
    ]),
    ("7.19.0", [
        "Stammdaten/Fütterung/Gesundheit/Dokumente erscheinen jetzt erst nach Klick auf den jeweiligen Button, statt permanent als Akkordeon sichtbar zu sein",
        "Filter (Alle/Eigene/Pflege & Gäste) ist bei deaktiviertem \"Erweiterter Modus\" jetzt komplett unsichtbar statt nur eingeklappt",
        "Reihenfolge in der Tiere-Sektion: Filter → Tier-Auswahl → Unterkategorien → Vermisst melden",
        "Bugfix: Stammdaten öffnete sich beim Laden/Speichern eines Tiers, blieb dabei aber unsichtbar",
    ]),
    ("7.18.0", [
        "Futter-Zentrale: Scan-Ergebnisfelder erscheinen erst nach dem Auswerten, nicht mehr permanent",
        "Archiv: Archivieren-Aktion jetzt oben (häufigste Aktion), PDF-Export in eigenem Akkordeon",
        "Inventar: Futter-Übersicht jetzt direkt oben sichtbar, Aufstocken/Bearbeiten weiterhin eingeklappt",
    ]),
    ("7.17.0", [
        "Filter (Alle/Eigene/Pflege & Gäste) jetzt standardmäßig eingeklappt statt immer sichtbar",
        "\"🚨 Vermisst melden\" ist jetzt ein immer sichtbarer Button direkt unter der Tier-Auswahl, kein Scrollen mehr nötig",
        "Neue Auswahlleiste (Stammdaten/Fütterung/Gesundheit/Dokumente) öffnet immer nur einen Bereich statt aller Akkordeons gleichzeitig",
    ]),
    ("7.16.2", [
        "Kritischer Bugfix: Nach einem Lade-Fehler beim Login blieben Dashboard, Archiv und Inventar leer/veraltet, obwohl die Daten unangetastet in der Tabelle lagen (Zählfehler in der Login-Logik, bestand seit dem allerersten Upload)",
    ]),
    ("7.16.1", [
        "Bugfix: Versionshistorie-Overlay blockierte die App beim Schließen (gleicher Fehler wie damals beim Onboarding-Popup)",
    ]),
    ("7.16.0", [
        "Neu: Versionsnummer unten ist jetzt anklickbar und zeigt die komplette Änderungshistorie seit v7.5",
    ]),
    ("7.15.0", [
        "Kalender-Erinnerungen (.ics) lösen jetzt automatisch 4 Wochen vor Fälligkeit eine Benachrichtigung aus",
        "Layout bei \"Gesundheit & Kalender\" aufgeräumt (Textfeld über den Buttons, kompaktere Datei-Box)",
    ]),
    ("7.14.2", ["Bugfix: \"TASSO Vermisst-Formular öffnen\" konnte die App im Browser-iFrame überschreiben und weiß bleiben"]),
    ("7.14.1", ["Bugfix-Versuch: TASSO-Link in der Standalone-App (abgelöst durch 7.14.2)"]),
    ("7.14.0", [
        "Bugfix: Tier-Laden konnte über 30 Sekunden hängen (Dokumente werden jetzt parallel statt nacheinander geladen)",
        "\"Scanner\" heißt jetzt \"🥫 Futter-Zentrale\" – klarer benannt",
    ]),
    ("7.13.0", ["Nur noch 2 Haupt-Tabs: \"Verwaltung\" (mit Auswahl Tiere/Futter-Zentrale/Archiv/Inventar) und \"Konto\""]),
    ("7.12.0", [
        "Status-Meldungen erscheinen jetzt als kurzes Overlay statt in sichtbaren Textboxen",
        "\"Neu laden\"/\"Abmelden\" sind jetzt am Fußende der App",
        "Bugfix: Admin-Debug-Konsole (Eruda) erschien nicht zuverlässig für PetBase",
    ]),
    ("7.11.1", ["Tab-Namen gekürzt (z. B. \"Tiere verwalten\" → \"Tiere\")"]),
    ("7.11.0", ["Tabs brechen um statt sich hinter einem \"⋮\"-Menü zu verstecken", "\"Neu laden\"/\"Abmelden\"-Buttons kompakter"]),
    ("7.10.0", ["Neu: 🚨 Vermisst melden – TASSO-Schnellzugriff + Daten zum Abtippen + PDF-Steckbrief"]),
    ("7.9.1", ["Bugfix: Willkommens-Popup blockierte die App, wenn man es schließen wollte"]),
    ("7.9.0", ["Willkommens-Popup erscheint jetzt auch bei neuen Versionen, nicht nur bei der Registrierung"]),
    ("7.8.0", ["Neu: Willkommens-Popup mit Feature-Übersicht beim ersten Login"]),
    ("7.7.0", ["Eruda-Debug-Konsole nur noch für Admin (PetBase) sichtbar", "Erzwungener Dark Mode für die App"]),
    ("7.6.0", ["Nassfutter kann zusätzlich zu Trockenfutter gefüttert werden", "Tierart als Dropdown mit BARF/Futter-Kategorien"]),
    ("7.5.0", ["Ausgangsstand vor Beginn der gemeinsamen Weiterentwicklung"]),
]

def baue_versionshistorie_text(anzahl_anzeigen=15):
    teile = ["## 🕓 Versionshistorie\n"]
    sichtbare = VERSION_HISTORY[:anzahl_anzeigen]
    for version, aenderungen in sichtbare:
        teile.append(f"### v{version}")
        teile.append("\n".join(f"- {a}" for a in aenderungen))
        teile.append("")
    rest = len(VERSION_HISTORY) - len(sichtbare)
    return "\n\n".join(teile), rest

# Kurzer "Was ist neu"-Text für Nutzer, die schon länger dabei sind (wird bei jedem Versions-Update angezeigt).
CHANGELOG_TEXT = (
    "## 🆕 Was ist neu in v" + APP_VERSION + "\n\n"
    + "\n".join(f"- {a}" for a in VERSION_HISTORY[0][1])
    + "\n\n_Komplette Historie: unten auf die Versionsnummer tippen._"
)

APP_CSS = """
/* ==========================================
   ICON-AUSTAUSCH (v7.91.1, Schritt 1 des App-Umbaus): Material Symbols statt
   Emoji für die Hauptnavigation. Gradio-Buttons können nur reinen Text
   anzeigen, kein gemischtes HTML - daher NICHT das Icon-Zeichen in den
   Button-Text selbst schreiben (würde den ganzen Button-Text in die
   Icon-Schriftart zwingen und wie Kauderwelsch aussehen), sondern per
   ::before-Pseudoelement sauber DAVOR einfügen, während der eigentliche
   Button-Text in der normalen Schrift bleibt. Jede Regel ist auf eine
   einzelne elem_id begrenzt, betrifft also nur genau den einen Button.
   ========================================== */
.msr {
    font-family: 'Material Symbols Rounded';
    font-weight: normal;
    font-style: normal;
    display: inline-block;
    vertical-align: -3px;
    -webkit-font-smoothing: antialiased;
}
#b_haupt_verwaltung::before { content: "pets"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_haupt_vermisst::before { content: "warning"; font-family:'Material Symbols Rounded'; font-variation-settings:'FILL' 1; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_haupt_konto::before { content: "account_circle"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_bereich_tiere::before { content: "pets"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_bereich_scanner::before { content: "photo_camera"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_bereich_archiv::before { content: "archive"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_bereich_inventar::before { content: "inventory_2"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_bereich_nachtrag::before { content: "attach_file"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_bereich_apotheke::before { content: "medication"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_bereich_pdf::before { content: "description"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_bereich_tagebuch::before { content: "auto_stories"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_tier_neu::before { content: "add_circle"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
/* Tier-Auswahl-Liste (v7.91.7) und Tagebuch-Übersicht: Karten-Look statt
   nackter Standard-Buttons, Gruppen-Überschriften dezent abgesetzt. */
.tier_liste_gruppe {
    margin: 14px 4px 4px !important;
    font-size: 11px !important;
    font-weight: 700 !important;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: #7A6E5C !important;
}
.tier_liste_btn, .tier_liste_btn button, .tb_uebersicht_btn, .tb_uebersicht_btn button {
    background: #FFFCF5 !important;
    border: 1px solid #DED2BA !important;
    border-radius: 14px !important;
    box-shadow: 0 1px 4px rgba(59,50,38,0.08) !important;
    text-align: left !important;
    justify-content: flex-start !important;
    font-weight: 600 !important;
    margin-bottom: 8px !important;
}
#b_login_tab::before { content: "login"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_reg_tab::before { content: "person_add"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_login_submit::before { content: "login"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_reg_submit::before { content: "how_to_reg"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_quick_login::before { content: "bolt"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_impressum::before { content: "info"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }

/* Große Sammel-Runde (v7.91.6): restliche einzelne Buttons app-weit auf
   Material Symbols umgestellt. Gemeinsame Klassen für inhaltlich gleichartige
   Buttons (Speichern/Abbrechen/Bearbeiten/...), die an mehreren Stellen bzw.
   innerhalb von @gr.render-Schleifen vorkommen (dort sind eindeutige IDs nicht
   möglich, da pro Zeile neu erzeugt) - Klassen funktionieren dort zuverlässig,
   IDs nicht (müssten eindeutig sein). Einzelne, wirklich nur einmal
   vorkommende Buttons bekommen wie gewohnt eine eigene ID. */
.btn-ic-save button::before, .btn-ic-save::before { content: "save"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
.btn-ic-cancel button::before, .btn-ic-cancel::before { content: "close"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
.btn-ic-edit button::before, .btn-ic-edit::before { content: "edit"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
.btn-ic-more button::before, .btn-ic-more::before { content: "expand_more"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
.btn-ic-more-inline button::before, .btn-ic-more-inline::before { content: "tune"; font-family:'Material Symbols Rounded'; font-size:15px; vertical-align:-3px; margin-right:4px; display:inline-block; }
.btn-ic-reminder button::before, .btn-ic-reminder::before { content: "event"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
.btn-ic-email button::before, .btn-ic-email::before { content: "mail"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
.btn-ic-check button::before, .btn-ic-check::before { content: "check"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
/* Reine Icon-Buttons ohne Begleittext (z.B. die kleinen +/✏️/✕-Knöpfe in den
   Futter-Vorrat-Zeilen): der gesamte Button-Text IST der Icon-Ligatur-Name,
   deshalb hier die ganze Schrift umstellen statt nur ein ::before davorzusetzen. */
.msr-icon-btn, .msr-icon-btn button {
    font-family: 'Material Symbols Rounded' !important;
    font-size: 20px !important;
}

#b_show_tagebuch::before { content: "auto_stories"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_onboarding_close::before { content: "rocket_launch"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_tagebuch_speichern::before { content: "save"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_tagebuch_pdf::before { content: "description"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_profil_speichern::before { content: "save"; font-family:'Material Symbols Rounded'; font-size:20px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_scan_toggle::before { content: "photo_camera"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_scan_auswerten::before { content: "search"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_save_futter::before { content: "inventory_2"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_archivieren::before { content: "archive"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_apotheke_speichern::before { content: "save"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_nach_save::before { content: "save"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_vermisst_daten::before { content: "list_alt"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_tasso_link::before { content: "open_in_new"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_vermisst_pdf::before { content: "description"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_logout::before { content: "logout"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_versionshistorie_mehr::before { content: "expand_more"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_show_fb::before { content: "bug_report"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_fb_senden::before { content: "send"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_show_erinn::before { content: "notifications"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_show_pw::before { content: "lock"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_pw_save::before { content: "key"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_show_del::before { content: "person_remove"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_del_account::before { content: "delete_forever"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_test_email::before { content: "forward_to_inbox"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_futter_cron_test::before { content: "restart_alt"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_show_news::before { content: "campaign"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_news_veroeffentlichen::before { content: "publish"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_show_users::before { content: "group"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_sperren::before { content: "block"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_admin_vergeben::before { content: "shield_person"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_admin_entziehen::before { content: "remove_moderator"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_show_feedback_liste::before { content: "rate_review"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_show_stam::before { content: "badge"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_show_futter::before { content: "nutrition"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_show_gesund::before { content: "vaccines"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_show_doku::before { content: "folder"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_show_tagebuch_open::before { content: "auto_stories"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_konto_tab_einst::before { content: "settings"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_konto_tab_admin::before { content: "admin_panel_settings"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }

/* Feste Navigationsleiste unten (v7.91.3) - immer sichtbar, unabhängig vom
   Scroll-Stand. Am Ende von main_app im DOM platziert, aber per position:fixed
   optisch immer am unteren Bildschirmrand verankert. gradio-container braucht
   zusätzlichen unteren Abstand, sonst würde die Leiste den letzten Inhalt
   verdecken. */
.gradio-container { padding-bottom: 78px !important; }
/* Popup-Flyout über der unteren Leiste (v7.92.0) - sitzt direkt über der
   Navigationsleiste, damit der Bezug zum angetippten Icon klar bleibt. */
#nav_popup_box {
    position: fixed !important;
    bottom: 78px !important;
    left: 50% !important;
    transform: translateX(-50%);
    width: fit-content !important;
    min-width: 220px;
    max-width: calc(100vw - 40px);
    max-height: 55vh;
    overflow-y: auto;
    background: #FFFCF5 !important;
    border: 1px solid #DED2BA !important;
    border-radius: 18px !important;
    box-shadow: 0 -4px 20px rgba(59,50,38,0.22) !important;
    z-index: 490;
    padding: 14px 14px 8px !important;
}
.nav_popup_titel {
    font-size: 14px !important;
    color: #3B3226 !important;
    margin-bottom: 6px !important;
}
.nav_popup_gruppe {
    font-size: 10.5px !important;
    font-weight: 700 !important;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: #7A6E5C !important;
    margin: 10px 2px 2px !important;
}
.nav_popup_item, .nav_popup_item button {
    background: #F2ECE0 !important;
    border: 1px solid #DED2BA !important;
    border-radius: 12px !important;
    text-align: left !important;
    justify-content: flex-start !important;
    font-weight: 600 !important;
    margin-bottom: 6px !important;
    min-height: 40px !important;
}
.nav_popup_back, .nav_popup_back button {
    background: transparent !important;
    box-shadow: none !important;
    border: none !important;
    color: #7A6E5C !important;
    font-size: 12px !important;
    justify-content: flex-start !important;
    margin-top: 4px !important;
}
.nav_popup_danger, .nav_popup_danger button {
    color: #D32F2F !important;
    border-color: #E8B4B0 !important;
}

#bottom_nav_bar {
    position: fixed !important;
    bottom: 0; left: 0; right: 0;
    z-index: 500;
    background: #FFFCF5 !important;
    border-top: 1px solid #DED2BA;
    box-shadow: 0 -2px 10px rgba(59,50,38,0.12);
    padding: 6px 4px 4px !important;
    gap: 0 !important;
    flex-wrap: nowrap !important;
}
#bottom_nav_bar > * {
    flex: 1 1 16.66% !important;
    min-width: 0 !important;
}
#bottom_nav_bar button {
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
    color: #7A6E5C !important;
    font-size: 9.5px !important;
    font-weight: 600 !important;
    display: flex !important;
    flex-direction: column !important;
    align-items: center !important;
    gap: 2px !important;
    min-height: 56px !important;
    padding: 4px 1px !important;
}
#bottom_nav_bar button::before {
    font-family: 'Material Symbols Rounded';
    font-size: 20px;
    line-height: 1;
    display: block;
}
#b_nav_start::before { content: "home"; }
#b_nav_tiere::before { content: "pets"; }
#b_nav_futter::before { content: "nutrition"; }
#b_nav_tagebuch::before { content: "auto_stories"; }
#b_nav_konto::before { content: "account_circle"; }
#b_nav_menu::before { content: "menu"; }
#b_haupt_futtervorrat::before { content: "medication"; font-family:'Material Symbols Rounded'; font-size:18px; vertical-align:-3px; margin-right:6px; display:inline-block; }
#b_start_vermisst::before { content: "warning"; font-family:'Material Symbols Rounded'; font-variation-settings:'FILL' 1; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }
#b_start_konto::before { content: "medication"; font-family:'Material Symbols Rounded'; font-size:16px; vertical-align:-3px; margin-right:5px; display:inline-block; }

/* Gradios Grundthema (zoo_theme) nutzt intern Orange als "primary_hue" - das
   erzeugt eine komplette Orange-Farbskala als CSS-Variablen (--primary-50 bis
   --primary-950), die JEDES Gradio-Element für Fokus-Ringe, "gerade
   aktualisiert"-Hervorhebungen, Slider, Checkboxen usw. verwendet. Bisher wurden
   nur EINZELNE Stellen gezielt auf Braun umgestellt (siehe Changelog v7.2x) -
   das griff nicht überall, z.B. blitzten Feld-Beschriftungen nach einem
   Speichervorgang kurz in Orange auf (siehe v7.88.2). Robusterer Ansatz: die
   Variablen selbst global auf eine braune Skala umbiegen, dann greift die
   App-Farbe automatisch überall, auch bei zukünftigen Gradio-Zuständen, die wir
   nicht einzeln kennen/vorhersehen.
   Skala grob am bestehenden Braun (#6D4C41) und den App-Cremetönen ausgerichtet. */
:root, .gradio-container {
    --primary-50: #FDF9F3 !important;
    --primary-100: #F7EFE1 !important;
    --primary-200: #EDE0C8 !important;
    --primary-300: #DED2BA !important;
    --primary-400: #C7AD8F !important;
    --primary-500: #A8846A !important;
    --primary-600: #8A6650 !important;
    --primary-700: #6D4C41 !important;
    --primary-800: #55392F !important;
    --primary-900: #3B2A22 !important;
    --primary-950: #251A15 !important;
}
/* Erzwingt eine zweispaltige Ansicht (50/50) für die damit markierten Zeilen.
   CSS Grid statt Flexbox, mit minmax(0, 1fr) statt einfachem 1fr: normales "1fr"
   ist eigentlich Kurzform für "minmax(auto, 1fr)" - das "auto" lässt den Browser
   auf die native Mindestbreite des Inhalts schauen. iOS Safari hat bei
   Eingabefeldern eine feste interne Mindestbreite und verweigert dann das exakte
   Halbieren. Mit minmax(0, 1fr) wird dem Grid diese Rücksichtnahme komplett
   entzogen, es teilt gnadenlos exakt in der Mitte, unabhängig vom Inhalt. */
.zweispaltig {
    display: grid !important;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
    gap: 12px !important;
    width: 100% !important;
    /* War vorher "start": wenn ein Feld (z.B. Farbe) durch längeren Text höher
       wurde als sein Nachbar (z.B. Geburtsdatum), blieb darunter eine hässliche
       graue Lücke stehen. "stretch" zieht beide Felder einer Zeile auf dieselbe
       Höhe - das kürzere wächst automatisch auf die Höhe des längeren mit. */
    align-items: stretch !important;
}
/* Gradio gruppiert benachbarte Formularfelder (Textbox, Dropdown etc.) innerhalb
   einer Row manchmal automatisch in einen zusätzlichen Wrapper-Div (Klasse "form"
   oder "wrap"). Dadurch sieht .zweispaltig nur EIN Kind-Element statt zwei, und
   die 50/50-Aufteilung landet komplett in Spalte 1 (auf Android beobachtet, Spalte
   2 blieb leer). display:contents macht diesen Wrapper für das Grid-Layout
   "unsichtbar", sodass seine eigentlichen Kinder direkt als Grid-Items zählen -
   ohne die Grid-Regeln ein zweites Mal definieren zu müssen. */
.zweispaltig > .form,
.zweispaltig > .wrap {
    display: contents !important;
}
/* 1. Alle Feld-Blöcke (egal ob direktes Grid-Kind oder - falls Gradio den
   Auto-Wrapper gesetzt hat - ein Kind davon) zu Flex-Containern über die volle
   Zeilenhöhe machen. Das ist die Basis der Höhen-Kette, die unten bis zur
   Textarea durchgereicht wird. */
.zweispaltig > *,
.zweispaltig > .form > *,
.zweispaltig > .wrap > * {
    display: flex !important;
    flex-direction: column !important;
    height: 100% !important;
    min-width: 0 !important;
    max-width: 100% !important;
    width: auto !important;
    margin: 0 !important;
    flex: none !important;
    /* iOS Safari zeigt sonst einen hellgrauen Halo/Rahmen um jedes Feld (natives
       Karten-Styling der Gradio-Block-Wrapper, das auf Android unsichtbar bleibt,
       auf iOS aber durchscheint). "transparent" war hier falsch - dahinter liegt
       keine weiße Karte, sondern direkt die Row/Seiten-Fläche, wodurch ein
       grauer Bereich sichtbar wurde. Stattdessen dieselbe Karten-Farbe wie der
       Rest der App (#FFFCF5, siehe .gr-group/.gr-form weiter unten) - kein
       reines Weiß, das hätte nicht zum warmen Creme-Ton gepasst. */
    background: #FFFCF5 !important;
    border: none !important;
    box-shadow: none !important;
}
/* 2. Label und Gradios innerer "wrap"-Div (der die Textarea direkt umschließt)
   müssen die Höhe ebenfalls durchreichen - sonst bricht die Kette hier ab und
   die Textarea hat unten keinen Platz zum Hineinwachsen. */
.zweispaltig label,
.zweispaltig > * > .wrap {
    display: flex !important;
    flex-direction: column !important;
    flex: 1 1 auto !important;
    height: 100% !important;
}
/* 3. Das eigentliche Eingabefeld füllt den verbleibenden Platz. Bewusst KEIN
   height:100% hier - das würde Gradios JS-Autosize für mehrzeiligen Text wieder
   überschreiben (siehe Bugfix v7.73.5, dort ist das Feld auf eine viel zu kleine
   Höhe zusammengestaucht). flex-grow ohne feste Höhe lässt das kürzere Feld
   mitwachsen, ohne die "auto"-Höhe des längeren Feldes zu kappen. */
.zweispaltig textarea,
.zweispaltig input,
.zweispaltig select {
    flex: 1 1 auto !important;
    min-height: 0 !important;
}
/* Tier-Filter (Alle/Eigene Tiere/Pflege & Gäste): untereinander statt nebeneinander
   gequetscht, da der Bereich zu schmal für 3 Optionen in einer Zeile ist */
#tier_filter_radio .wrap {
    flex-direction: column !important;
}
#tier_filter_radio label {
    width: 100% !important;
}
/* Vollbild-Bildansicht (z.B. Profilbild antippen) auf die Bildschirmgröße begrenzen,
   damit der Schließen-Button (X) immer sichtbar/erreichbar bleibt, ohne seitlich
   scrollen zu müssen */
.gradio-container [class*="fullscreen"],
.gradio-container [data-testid="image"] > div:has(img) {
    max-width: 100vw !important;
    max-height: 100vh !important;
}
.gradio-container [class*="fullscreen"] img {
    max-width: 100% !important;
    max-height: 90vh !important;
    width: auto !important;
    object-fit: contain !important;
}
/* Profilbild: Gradios eingebaute Werkzeugleiste (Vollbild/Löschen-Icons) legt sich
   standardmäßig als halbtransparenter Balken ÜBER das Bild - bei Tierfotos sitzt
   der Kopf meist oben, wird also genau verdeckt. Statt die Buttons per JS zu
   verstecken/ersetzen (das ist in dieser App schon mehrfach schiefgegangen, siehe
   Changelog v7.68-7.72), schaffen wir hier rein per CSS oben im Bild eine kleine
   "Sicherheitszone": das Bild wird minimal verkleinert und nach unten ausgerichtet,
   die Werkzeugleiste bekommt einen transparenten Hintergrund.
   v7.73.10 hatte hier zusätzlich versucht, das Profilbild per height:0 +
   overflow:hidden + position:absolute exakt auf die Höhe der linken Spalte zu
   ziehen. RÜCKGÄNGIG GEMACHT in v7.73.18: dieser Trick ist zu fragil - wenn die
   Zeile aus irgendeinem Grund keine verlässliche Höhe liefert (z.B. durch einen
   von Gradio eingefügten Wrapper-Div, wie schon bei den Stammdaten-Feldern
   beobachtet), kollabiert der GESAMTE Bildbereich auf 0px und overflow:hidden
   blendet dann Foto UND Upload-Button komplett aus - genau das ist passiert.
   Funktionierende Sichtbarkeit ist wichtiger als exakte Höhen-Angleichung, daher
   zurück auf normalen Fluss ohne erzwungene Höhe. */
/* Tagebuch: Foto-Upload-Fläche etwas kompakter - bewusst OHNE overflow:hidden
   oder erzwungene max-height (das hatte in einem früheren Versuch versehentlich
   die Icons Hochladen/Kamera/Zwischenablage mit verschluckt, siehe v7.88.6/7).
   Nur die künstliche Mindesthöhe wird gelockert, das kann höchstens wirkungslos
   bleiben, aber nichts Funktionales verstecken. Vorschau nach dem Hochladen
   bleibt so oder so erhalten, unabhängig von dieser Regel. */
/* Tagebuch: Foto-Upload-Fläche kompakter - per Live-Inspektion (danke fürs
   Dranbleiben!) die WIRKLICHEN Klassen gefunden: das eigentliche Element ist
   ein <button class="... boundedheight ..."> innerhalb von .upload-container,
   mit fest height:240px/min-height:44px direkt von Gradios "boundedheight"-
   Klasse. Die vorherigen Versuche (.wrap.full/.empty.large) zielten auf die
   falschen Elemente - die gehören offenbar zum File-Feld (PDF), nicht zum
   Image-Feld (Foto). Bewusst nur innerhalb dieser beiden elem_ids begrenzt,
   damit andere Bildfelder in der App (Profilbild, Apotheke-Banner) ihre eigene
   Größe behalten. */
#tagebuch_foto_upload .upload-container, #tagebuch_foto_edit .upload-container {
    min-height: 0 !important;
}
/* v7.90.2: 60px war zu knapp - der komplette Inhalt (drei Textzeilen "Bild hier
   ablegen/oder/Hochladen" UND die drei Icons darunter) sitzt alles im selben
   Button, nicht in getrennten Bereichen. Bei zu wenig Höhe überlappen sich Text
   und Icons. min-height statt fixer height: schrumpft so weit wie möglich,
   wächst aber automatisch mit, falls der Inhalt mehr Platz braucht. */
#tagebuch_foto_upload button.boundedheight, #tagebuch_foto_edit button.boundedheight {
    height: auto !important;
    min-height: 140px !important;
}
/* Gleiche Lösung jetzt auf alle anderen Bild-/Datei-Upload-Felder in der App
   übertragen (v7.90.0). Unterschiedliche Höhen je nach Zweck statt überall
   gleich winzig: Profilbild bleibt sichtbar prominent, die Futter-Scanner-Bilder
   brauchen genug Platz zum Fotografieren/Framing von Barcode & Etikett, der
   Pflegevertrag-Upload ist wie das Tagebuch-PDF ein reines Datei-Feld (andere
   Struktur, siehe .wrap.full/.empty.large statt button.boundedheight). */
#profil_img_komponente .upload-container {
    min-height: 0 !important;
}
#profil_img_komponente button.boundedheight {
    height: auto !important;
    min-height: 140px !important;
}
#futter_bild_upload_1 .upload-container, #futter_bild_upload_2 .upload-container, #futter_bild_upload_3 .upload-container {
    min-height: 0 !important;
}
#futter_bild_upload_1 button.boundedheight, #futter_bild_upload_2 button.boundedheight, #futter_bild_upload_3 button.boundedheight {
    height: auto !important;
    min-height: 150px !important;
}
#pflege_vertrag_upload.block {
    min-height: 0 !important;
}
#pflege_vertrag_upload .wrap.full {
    min-height: 0 !important;
    height: auto !important;
}
#pflege_vertrag_upload .empty.large {
    min-height: 0 !important;
    padding: 10px !important;
}
/* Tagebuch: PDF-Ergebnisfeld soll nur so hoch sein wie der Dateiname braucht,
   nicht die große leere Standard-Vorschaufläche. Per Live-Inspektion (Danke!)
   die TATSÄCHLICHEN Gradio-Klassen gefunden: .wrap.full (füllt die Höhe des
   äußeren Blocks) und .empty.large (Gradios eigene "große" Leerzustand-Klasse,
   inkl. großem Icon+Innenabstand). Der vorherige Versuch zielte auf ".container",
   die es hier gar nicht gibt - deshalb wirkungslos. Bewusst nur INNERHALB von
   #tagebuch_pdf_result begrenzt, damit andere Datei-Felder in der App (z.B.
   Pflegevertrag-Upload) unangetastet bleiben. */
#tagebuch_pdf_result.block {
    min-height: 0 !important;
}
#tagebuch_pdf_result .wrap.full {
    min-height: 0 !important;
    height: auto !important;
}
#tagebuch_pdf_result .empty.large {
    min-height: 0 !important;
    padding: 10px !important;
}
#tagebuch_pdf_result .empty.large .icon {
    width: 24px !important;
    height: 24px !important;
    margin: 0 auto !important;
}
/* Haustierapotheke-Kopfbanner: das Bild ist hochformatig und detailreich - als
   volle Höhe würde es den ganzen Bildschirm einnehmen, bevor Nutzer überhaupt
   die eigentlichen Kategorien sehen. Auf eine kompakte Banner-Höhe zugeschnitten,
   oberer Bildausschnitt (Logo/Schriftzug) bleibt sichtbar. */
#apotheke_banner_img img {
    max-height: 200px !important;
    width: 100% !important;
    object-fit: cover !important;
    object-position: top !important;
    border-radius: 14px !important;
}
#profil_img_komponente img {
    padding-top: 40px !important;
    box-sizing: border-box !important;
    object-position: center bottom !important;
    border-radius: 12px !important;
}
#profil_img_komponente div:has(> button),
#profil_img_komponente [data-testid="image"] > div:has(button) {
    background: transparent !important;
    border: none !important;
}
/* Name/Tierauswahl-Box in Stammdaten: keine eigene Karten-Optik, verschmilzt mit
   dem Hintergrund. */
#row_name_kategorie {
    background: transparent !important;
    box-shadow: none !important;
    border: none !important;
}
#row_name_kategorie > * {
    background: transparent !important;
    box-shadow: none !important;
    border: none !important;
}
/* Fußbereich (z.B. Abmelden) zentrieren statt linksbündig */
.zentrierte_reihe {
    justify-content: center !important;
}
/* Anmelden/Registrieren-Tabs auf dem Login-Bildschirm gleichmäßig auf volle Breite verteilen */
#login_screen .tab-nav button, #login_screen [role="tab"] {
    flex: 1 !important;
}
/* Verhindert die browsereigene Verzögerung/Mehrdeutigkeits-Prüfung beim Antippen
   (Browser muss sonst kurz abwarten: "war das ein Klick oder der Beginn eines
   Doppel-Tipps zum Reinzoomen?"). Das äußert sich als "Button reagiert erst
   beim zweiten Tippen" bzw. "öffnet kurz und schließt sofort wieder" - vor
   allem auf iOS/Safari. touch-action: manipulation erlaubt weiterhin normales
   Scrollen und Pinch-to-Zoom, schaltet nur die Doppel-Tipp-Erkennung pro
   Element ab, sodass ein Einzeltipp immer sofort und eindeutig auslöst. */
.gradio-container, .gradio-container * {
    touch-action: manipulation !important;
}

/* Gradios eigene CSS-Variablen überschreiben, damit auch native Elemente
   (Radio-Buttons, Checkboxen, Slider) das Braun statt des Standard-Orange
   verwenden - robuster als einzelne Klassennamen zu erraten. */
.gradio-container {
    --color-accent: #6D4C41 !important;
    --color-accent-soft: #EDE5D6 !important;
    --button-primary-background-fill: #6D4C41 !important;
    --button-primary-background-fill-hover: #4E342E !important;
    --button-primary-border-color: #6D4C41 !important;
    --button-primary-border-color-hover: #4E342E !important;
    --checkbox-background-color-selected: #6D4C41 !important;
    --checkbox-border-color-selected: #6D4C41 !important;
    --checkbox-label-background-fill-selected: #6D4C41 !important;
    --checkbox-label-border-color-selected: #6D4C41 !important;
    --slider-color: #6D4C41 !important;
    --block-label-text-color: #7A6E5C !important;
    --block-title-text-color: #3B3226 !important;
    --body-text-color-subdued: #7A6E5C !important;
    /* Ausgewählte Radio/Checkbox-Option: heller Ton statt dunklem Braun,
       damit die dunkle Schrift darauf lesbar bleibt */
    --checkbox-label-background-fill-selected: #E6C9A0 !important;
    --checkbox-label-border-color-selected: #C9A66B !important;
    --checkbox-label-text-color-selected: #3B3226 !important;
}
/* Checkbox/Radio komplett selbst zeichnen, statt uns auf Gradios eigene (teils
   unterdrückte) native Darstellung zu verlassen. Reagiert direkt auf die
   eingebaute, immer korrekte Browser-Eigenschaft ":checked" - unabhängig davon,
   ob Gradio das native Element optisch versteckt. */
.gradio-container input[type="radio"] {
    appearance: none !important;
    -webkit-appearance: none !important;
    width: 18px !important;
    height: 18px !important;
    min-width: 18px !important;
    border-radius: 50% !important;
    border: 2px solid #6D4C41 !important;
    background: #FFFCF5 !important;
    position: relative !important;
    opacity: 1 !important;
    margin: 0 6px 0 0 !important;
    flex-shrink: 0;
    box-sizing: border-box !important;
}
.gradio-container input[type="radio"]:checked {
    background: #6D4C41 !important;
    box-shadow: inset 0 0 0 4px #FFFCF5 !important;
}
.gradio-container input[type="checkbox"] {
    appearance: none !important;
    -webkit-appearance: none !important;
    width: 18px !important;
    height: 18px !important;
    min-width: 18px !important;
    border-radius: 4px !important;
    border: 2px solid #6D4C41 !important;
    background: #FFFCF5 !important;
    position: relative !important;
    opacity: 1 !important;
    margin: 0 6px 0 0 !important;
    flex-shrink: 0;
}
.gradio-container input[type="checkbox"]:checked {
    background: #6D4C41 !important;
}
.gradio-container input[type="checkbox"]:checked::after {
    content: "✓" !important;
    position: absolute !important;
    top: -3px !important;
    left: 1px !important;
    color: #FFFCF5 !important;
    font-size: 14px !important;
    font-weight: bold !important;
}
/* Ausgewählte Radio-"Pillen" (z.B. Trockenfutter/Nassfutter, Parasitenschutz-Intervall,
   Filter) deutlich sichtbar hervorheben - reagiert direkt auf den echten Checked-Zustand
   des Eingabe-Elements (zuverlässiger als Gradios eigene Klassen, die manchmal hängen bleiben) */
.gradio-container label:has(input[type="radio"]:checked),
.gradio-container label:has(input[type="checkbox"]:checked) {
    background: #C9A66B !important;
    border: 2px solid #6D4C41 !important;
    color: #3B3226 !important;
    font-weight: bold !important;
}
/* Nicht ausgewählte Pillen: dezent, aber klar als anklickbar erkennbar */
.gradio-container label:has(input[type="radio"]):not(:has(input:checked)),
.gradio-container label:has(input[type="checkbox"]):not(:has(input:checked)) {
    background: #FFFCF5 !important;
    border: 1px solid #C9A66B !important;
}

/* Etwas kleinere Schrift app-weit, damit es weniger überladen wirkt */
.gradio-container {
    font-size: 0.92em !important;
}
.gradio-container label, .gradio-container span {
    font-size: 0.95em !important;
}
.gradio-container button {
    font-size: 0.92em !important;
}
/* Treffergenauigkeit app-weit erhöhen (v7.88.0) - besonders auf iOS Safari kam es
   vor, dass optisch breite Buttons trotzdem nicht bei jedem Tipp reagierten.
   touch-action:manipulation verhindert, dass der Browser auf Doppeltipp/Zoom
   wartet, bevor er den Tipp verarbeitet (das kostet iOS-Nutzern gefühlt oft den
   ersten Tipp). -webkit-tap-highlight-color macht zusätzlich sichtbar, dass ein
   Tipp überhaupt registriert wurde. min-height als Untergrenze nach Apples
   Empfehlung (44px Mindest-Trefffläche) - spezifischere Regeln für bewusst
   kompakte Buttons (z.B. Karten-Aktionen) greifen weiterhin, da sie später in
   der Kaskade stehen.
   Wichtig: KEIN pointer-events hier verändert, das würde bei Bedarf spezifischere
   Regeln blockieren statt nur die Mindest-Treffgröße abzusichern. */
.gradio-container button, .gradio-container a[role="button"] {
    touch-action: manipulation !important;
    -webkit-tap-highlight-color: rgba(109,76,65,0.25) !important;
    min-height: 44px;
}
.gradio-container h1 { font-size: 1.3em !important; }
.gradio-container h2 { font-size: 1.15em !important; }
.gradio-container h3 { font-size: 1.05em !important; }

#onboarding_backdrop {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    width: 100%; height: 100%;
    background: rgba(59,50,38,0.55);
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}
#onboarding_card {
    background: #FFFCF5;
    border: 1px solid #6D4C41;
    border-radius: 16px;
    padding: 24px;
    max-width: 480px;
    max-height: 85vh;
    overflow-y: auto;
    box-shadow: 0 8px 32px rgba(59,50,38,0.25);
}

/* Versionshistorie-Overlay (gleiches Muster wie Onboarding) */
#versionshistorie_backdrop {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    width: 100%; height: 100%;
    background: rgba(59,50,38,0.55);
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}
#versionshistorie_card {
    background: #FFFCF5;
    border: 1px solid #6D4C41;
    border-radius: 16px;
    padding: 24px;
    max-width: 480px;
    max-height: 85vh;
    overflow-y: auto;
    box-shadow: 0 8px 32px rgba(59,50,38,0.25);
}
#versionshistorie_close button {
    min-height: 34px !important;
    max-width: 90px;
    padding: 4px 12px !important;
    font-size: 1em !important;
    margin-left: auto;
    margin-right: auto;
    display: block;
}
/* Footer-Versionsnummer als dezent klickbarer Text statt Button-Optik */
#version_footer_btn button {
    background: none !important;
    border: none !important;
    box-shadow: none !important;
    color: #7A6E5C !important;
    font-size: 0.8em !important;
    text-decoration: underline;
    min-height: unset !important;
    padding: 4px !important;
    margin: 0 auto !important;
    display: block !important;
    width: auto !important;
}

/* Tabs senkrecht untereinander statt nebeneinander anzeigen - umgeht Gradios
   JS-basierte "passt nicht in die Breite -> Overflow-Menü"-Logik komplett,
   da bei einer Spalte kein Platzmangel in der Breite mehr entstehen kann.
   Doppelt abgesichert: alte Gradio-Klasse .tab-nav UND die ARIA-Rolle [role="tablist"]. */
.tab-nav, [role="tablist"] {
    flex-direction: column !important;
    flex-wrap: nowrap !important;
    overflow: visible !important;
    height: auto !important;
    align-items: stretch !important;
    gap: 4px !important;
}
.tab-nav > button, [role="tablist"] > button, [role="tablist"] [role="tab"] {
    width: 100% !important;
    text-align: center;
    white-space: normal;
}

/* Kompakte Zeilen für die einzelnen Futtersorten-Slots mit Mini-Buttons */
.futter_slot_row {
    gap: 6px !important;
    align-items: center !important;
    padding: 4px 8px !important;
    margin-bottom: 2px !important;
    background: #EDE5D6;
    border-radius: 10px;
}
.futter_slot_row button {
    min-height: 30px !important;
    padding: 2px 6px !important;
}
.futter_slot_inline {
    gap: 4px !important;
    align-items: center !important;
    padding: 6px 8px !important;
    margin: 0 0 6px 4px !important;
    background: #FFF3E0;
    border-left: 2px solid #6D4C41;
    border-radius: 0 8px 8px 0;
}
.futter_slot_inline button {
    min-height: 34px !important;
}

/* Button-Reihen, die wie echte Reiter-Tabs aussehen sollen (statt Gradios eigener
   Tabs-Komponente, die das "⋮"-Overflow-Menü nicht zuverlässig vermeidet) */
.tab_row {
    gap: 4px !important;
    border-bottom: 2px solid #DED2BA;
    flex-wrap: wrap !important;
    padding-bottom: 4px;
}
.tab_row > * {
    flex: 1 1 calc(50% - 4px) !important;
    min-width: 0 !important;
}
.tab_row > *.petbase_tab_stretch {
    flex: 1 1 100% !important;
}
.tab_row button {
    border-radius: 10px 10px 0 0 !important;
    box-shadow: none !important;
    border: none !important;
    margin: 0 !important;
    min-height: 38px !important;
    width: 100% !important;
    white-space: normal !important;
}
.tab_row button.primary {
    border-bottom: 3px solid #6D4C41 !important;
}
.tab_row button.secondary {
    background: transparent !important;
    opacity: 0.75;
}

#acc_top_row {
    gap: 8px !important;
}
#acc_top_row button {
    min-height: 30px !important;
    padding: 4px 10px !important;
    font-size: 0.85em !important;
}

/* Kompakte Datei-Downloadbox (statt großer grauer Fläche) - seit v7.90.1 mit den
   per Live-Inspektion bestätigten echten Gradio-Klassen (.wrap.full, .empty.large,
   siehe Tagebuch-PDF-Fix) ergänzt, die alten Regeln bleiben als Sicherheitsnetz. */
.kompakte_datei {
    min-height: unset !important;
}
.kompakte_datei .wrap.full {
    min-height: 0 !important;
    height: auto !important;
}
.kompakte_datei .empty {
    min-height: unset !important;
}
.kompakte_datei .empty.large {
    min-height: 0 !important;
    padding: 8px !important;
}
.kompakte_datei > div {
    min-height: 40px !important;
    padding: 6px !important;
}
.kompakte_datei svg {
    width: 24px !important;
    height: 24px !important;
}
.kompakte_datei .icon-wrap {
    margin: 0 !important;
}

/* ==========================================
   MATERIAL 3 - Weichere große Rundungen, federnde Übergänge
   ========================================== */
.gradio-container button, .gradio-container .gr-button {
    border-radius: 20px !important;
    transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.18s ease, background-color 0.2s ease !important;
}
.gradio-container button:active {
    transform: scale(0.96);
}
.gradio-container input, .gradio-container textarea, .gradio-container select,
.gradio-container .gr-box, .gradio-container .block {
    border-radius: 16px !important;
    transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.gradio-container .gr-group, .gradio-container .gr-form, .gradio-container .gr-panel {
    border-radius: 20px !important;
}
/* Frosted-Glass-Karten für Popups (hell) */
#onboarding_card, #versionshistorie_card {
    background: rgba(255, 255, 255, 0.92) !important;
    backdrop-filter: blur(18px) saturate(160%);
    -webkit-backdrop-filter: blur(18px) saturate(160%);
    border-radius: 28px !important;
    border: 1px solid rgba(93, 64, 55, 0.5) !important;
}
#onboarding_backdrop, #versionshistorie_backdrop {
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}
/* Akkordeons: weichere Rundung statt eckiger Kästen */
.gradio-container .label-wrap, .gradio-container details {
    border-radius: 18px !important;
    transition: background-color 0.25s ease;
}
/* Tabs: weichere, "pill"-artige aktive Markierung statt scharfer Linie */
.tab-nav > button, [role="tablist"] [role="tab"] {
    border-radius: 14px 14px 0 0 !important;
    transition: background-color 0.2s ease, transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* ==========================================
   DESIGN-SYSTEM 3.0 - Hell, freundlich, an Stock-Android/Material Design angelehnt
   Google-typische helle Oberfläche statt Dark Mode: weißer Grund, dezente
   graue Flächen für Karten, klare Schrift, sparsamer Einsatz von Farbe.
   ========================================== */

/* Seitenhintergrund: helles, freundliches Grau (wie Android-Systemhintergrund) statt Dunkel */
.gradio-container {
    background: #F2ECE0 !important;
    color: #3B3226 !important;
}

/* Alle Container-artigen Blöcke (Group, Panel, Form, Row mit Inhalt) bekommen
   eine einheitliche "Karten"-Optik: weiße Fläche, dezenter Schatten (Material Elevation 1). */
.gradio-container .gr-group,
.gradio-container .gr-panel,
.gradio-container .gr-form,
.gradio-container fieldset {
    background: #FFFCF5 !important;
    border: 1px solid #E6DAC4 !important;
    box-shadow: 0 1px 2px rgba(59,50,38,0.10), 0 1px 3px rgba(59,50,38,0.08) !important;
    border-radius: 18px !important;
    padding: 14px !important;
}

/* Etwas mehr Luft zwischen aufeinanderfolgenden Karten/Blöcken */
.gradio-container .gr-group + .gr-group,
.gradio-container .gr-form + .gr-form {
    margin-top: 10px !important;
}

/* Eingabefelder: leicht getönte Fläche, damit sie sich von der weißen Karte abheben */
.gradio-container input:not([type="checkbox"]):not([type="radio"]),
.gradio-container textarea,
.gradio-container select {
    background: #EDE5D6 !important;
    border: 1px solid #DED2BA !important;
    color: #3B3226 !important;
    /* iOS Safari zwingt <select>/<input> sonst eine eigene native Höhe/Optik auf
       (leicht abweichend von <input>-Feldern), dadurch wirkte das Dropdown minimal
       niedriger als die Textfelder daneben. -webkit-appearance:none unterbindet
       das native Styling komplett, sodass beide Feldtypen identisch aussehen. */
    -webkit-appearance: none !important;
    appearance: none !important;
    /* Browser rendern <select>-Text sonst mit ihrer eigenen System-Schrift (meist
       fetter/anders als <input>/<textarea>) - hier erzwungen identisch. */
    font-family: inherit !important;
    font-weight: normal !important;
    font-size: 1em !important;
}
/* appearance:none entfernt bei <select> auch den nativen Dropdown-Pfeil (auf
   ALLEN Plattformen, nicht nur iOS) - hier durch einen eigenen SVG-Pfeil ersetzt,
   damit Android nicht plötzlich den Pfeil verliert. */
.gradio-container select {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233B3226' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'></polyline></svg>") !important;
    background-repeat: no-repeat !important;
    background-position: right 10px center !important;
    background-size: 18px !important;
    padding-right: 34px !important;
}
.gradio-container input:focus, .gradio-container textarea:focus, .gradio-container select:focus {
    background: #FFFCF5 !important;
    border-color: #6D4C41 !important;
    box-shadow: 0 0 0 2px rgba(93,64,55,0.22) !important;
}

/* Primäre Buttons: sanfter Verlauf statt platter Fläche, für mehr Tiefe */
.gradio-container button.primary {
    background: linear-gradient(135deg, #6D4C41, #4E342E) !important;
    border: none !important;
    color: #FFFCF5 !important;
    box-shadow: 0 2px 8px rgba(93,64,55,0.30) !important;
}
.gradio-container button.primary:hover {
    box-shadow: 0 4px 14px rgba(93,64,55,0.40) !important;
}
.gradio-container button.secondary {
    background: #EDE5D6 !important;
    border: 1px solid #DED2BA !important;
    color: #3B3226 !important;
}
.gradio-container button.stop {
    background: linear-gradient(135deg, #EF5350, #E53935) !important;
    border: none !important;
    color: #FFFCF5 !important;
}

/* Überschriften: klarere Hierarchie, mehr Abstand nach oben zur vorigen Sektion */
.gradio-container h3 {
    color: #4E342E !important;
    margin-top: 18px !important;
    letter-spacing: 0.2px;
}
.gradio-container label span,
.gradio-container label,
.gradio-container .block > label span,
.gradio-container [data-testid="block-label"] {
    color: #7A6E5C !important;
}
.gradio-container .gr-group h3:first-child {
    margin-top: 0 !important;
}

/* Zwei-Felder-Zeilen (z.B. Gewicht/Menge, Hersteller/Sorte) bleiben immer
   nebeneinander statt untereinander umzubrechen - auch bei tief verschachtelten
   Karten mit viel Innenabstand, der sonst zu wenig Platz pro Spalte lässt. */
.gradio-container .form .row:not(.tab_row),
.gradio-container .gr-group .row:not(.tab_row),
.gradio-container .gr-form .row:not(.tab_row),
.gradio-container details .row:not(.tab_row) {
    flex-wrap: nowrap !important;
    align-items: flex-start !important;
}
.gradio-container .form .row:not(.tab_row) > *,
.gradio-container .gr-group .row:not(.tab_row) > *,
.gradio-container .gr-form .row:not(.tab_row) > *,
.gradio-container details .row:not(.tab_row) > * {
    min-width: 0 !important;
    overflow: hidden;
}
/* Tab-artige Button-Reihen (Stammdaten/Fütterung/.../Bereich-Tabs): 2-Spalten-Raster
   mit Umbruch (siehe .tab_row Regel weiter oben), letztes Element bei ungerader
   sichtbarer Anzahl über volle Breite gestreckt (per JS berechnet, siehe
   petbaseTabRowsAusrichten - reine CSS-Lösung geht nicht zuverlässig, da einzelne
   Tabs je nach Admin-Status/Modus dynamisch ein-/ausgeblendet werden). */
/* Beschriftungs-"Pillen" (z.B. Gewicht 1, Menge 1): dürfen bei wenig Platz
   auf eine zweite Zeile umbrechen statt in die Nachbar-Spalte zu überlappen */
.gradio-container label span {
    white-space: normal !important;
    overflow-wrap: break-word !important;
    word-break: break-word !important;
    display: inline-block;
    max-width: 100%;
}
@media (max-width: 480px) {
    .gradio-container label span {
        font-size: 0.85em !important;
        padding: 3px 8px !important;
    }
}
/* Etwas weniger Innenabstand bei verschachtelten Karten, damit mehr Platz
   für den eigentlichen Inhalt bleibt */
.gradio-container .gr-group .gr-group,
.gradio-container .gr-form .gr-group {
    padding: 8px !important;
}

/* Akkordeon-Symbol: klassische drei Punkte statt Pfeil/Dreieck, in allen Akkordeons.
   Gradios Akkordeons nutzen vermutlich das native HTML <summary>-Element, dessen
   Pfeil-Markierung eine Browser-eigene Darstellung ist (kein SVG) - deshalb hier
   gezielt über ::marker/-webkit-details-marker ausgeblendet, nicht nur über svg. */
.gradio-container summary::-webkit-details-marker {
    display: none !important;
}
.gradio-container summary::marker {
    content: "" !important;
}
.gradio-container summary {
    list-style: none !important;
}
.gradio-container .label-wrap svg,
.gradio-container summary svg,
.gradio-container .label-wrap > *:last-child,
.gradio-container summary > *:last-child {
    display: none !important;
    width: 0 !important;
    visibility: hidden !important;
}
/* Haustierapotheke: Kategorie-Karten deutlich als antippbar erkennbar machen -
   sonst wirken sie wie normaler Fließtext statt wie ein aufklappbares Element
   (das Standard-Pfeilsymbol wird ja app-weit ausgeblendet, siehe oben). */
.apotheke_kategorie_accordion {
    background: #FFFCF5 !important;
    border: 1px solid #DED2BA !important;
    border-radius: 14px !important;
    margin-bottom: 10px !important;
    box-shadow: 0 1px 4px rgba(59,50,38,0.12) !important;
}
.apotheke_kategorie_accordion > .label-wrap,
.apotheke_kategorie_accordion summary {
    padding: 14px 16px !important;
}
.apotheke_kategorie_accordion > .label-wrap,
.apotheke_kategorie_accordion summary,
.apotheke_kategorie_accordion > .label-wrap *,
.apotheke_kategorie_accordion summary * {
    font-weight: 800 !important;
    font-size: 1.08em !important;
    color: #6D4C41 !important;
}
.gradio-container .label-wrap::after,
.gradio-container summary::after {
    content: "⋮" !important;
    font-size: 1.3em;
    line-height: 1;
    color: #6D4C41;
    margin-left: auto;
    padding-left: 8px;
}

/* Fließtext/Markdown: dunkle, gut lesbare Schrift statt heller Schrift auf hellem Grund */
.gradio-container .prose, .gradio-container .prose p, .gradio-container .prose li {
    color: #3B3226 !important;
}
"""

zoo_theme = gr.themes.Soft(primary_hue="orange", secondary_hue="amber")
CUSTOM_HEAD = """
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,300..600,0..1,0" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes">
<link rel="apple-touch-icon" sizes="180x180" href="/pwa_icon/180">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="PetBase">
<script>
// Enter springt zum nächsten Feld (wie Tab), statt nichts zu tun oder abzuschicken.
// Mehrzeilige Textfelder (mehr als 1 Zeile) sind ausgenommen - dort bleibt Enter ein normaler Zeilenumbruch.
document.addEventListener('keydown', function(e) {
    if (e.key !== 'Enter' || e.shiftKey) return;
    const el = e.target;
    if (el.tagName === 'TEXTAREA' && el.rows > 1) return;
    if (el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA') return;
    if (el.type === 'checkbox' || el.type === 'radio' || el.type === 'button' || el.type === 'submit') return;
    e.preventDefault();
    const focusable = Array.from(document.querySelectorAll('input, textarea, select'))
        .filter(x => x.offsetParent !== null && !x.disabled);
    const idx = focusable.indexOf(el);
    if (idx > -1 && idx < focusable.length - 1) { focusable[idx + 1].focus(); }
});

// Tab-Leisten (Haupt-Bereiche, Bereich-/Detail-Tabs): 2-Spalten-Raster, letztes
// Element bekommt volle Breite, wenn die Anzahl der SICHTBAREN (nicht aller!)
// Kinder ungerade ist. Reine CSS-Lösung (:nth-last-child) würde auch versteckte
// Tabs mitzählen, die je nach Admin-Status/Modus dynamisch ein-/ausgeblendet
// werden - deshalb per JS anhand der tatsächlichen Sichtbarkeit berechnet, und
// über einen MutationObserver + Sicherheitsnetz-Intervall aktuell gehalten.
function petbaseTabRowsAusrichten() {
    document.querySelectorAll('.tab_row').forEach(function(row) {
        const kinder = Array.from(row.children);
        kinder.forEach(function(k) { k.classList.remove('petbase_tab_stretch'); });
        const sichtbare = kinder.filter(function(k) {
            return k.offsetParent !== null;
        });
        if (sichtbare.length % 2 === 1) {
            sichtbare[sichtbare.length - 1].classList.add('petbase_tab_stretch');
        }
    });
}
const petbaseTabRowObserver = new MutationObserver(function() { petbaseTabRowsAusrichten(); });
function petbaseTabRowsBeobachten() {
    document.querySelectorAll('.tab_row').forEach(function(row) {
        // WICHTIG: NUR 'style' beobachten, NICHT 'class'! Gradio blendet Elemente
        // über inline-style (display:none) ein/aus - das reicht zur Erkennung.
        // 'class' wurde vorher zusätzlich beobachtet, aber petbaseTabRowsAusrichten()
        // selbst ändert Klassen (petbase_tab_stretch) - das hat den Beobachter bei
        // JEDER eigenen Änderung erneut ausgelöst: Endlos-Schleife, die den Browser
        // komplett lahmgelegt hat ("Skript verlangsamt Browser"-Warnung).
        petbaseTabRowObserver.observe(row, { attributes: true, attributeFilter: ['style'], subtree: true });
    });
}
petbaseTabRowsBeobachten();
petbaseTabRowsAusrichten();
// Sicherheitsnetz: neue .tab_row-Elemente (z.B. nach Login) werden so auch ohne
// erneuten Observer-Aufruf erfasst, und verpasste Änderungen holen sich nach.
setInterval(function() { petbaseTabRowsBeobachten(); petbaseTabRowsAusrichten(); }, 1000);

// Erneuter Klick auf ein Vollbild-Bild (z.B. maximiertes Profilbild) schließt es automatisch,
// da der eigentliche Schließen-Button auf kleinen Bildschirmen oft außerhalb des sichtbaren
// Bereichs liegt und nicht erreichbar ist. Simuliert dazu die Escape-Taste (sicherer als das
// Klicken eines Buttons zu versuchen, da dabei versehentlich der falsche - z.B. "Bild löschen"
// statt "Vollbild schließen" - Button getroffen werden könnte).
document.addEventListener('click', function(e) {
    const fsContainer = e.target.closest('[class*="fullscreen"]');
    if (!fsContainer || e.target.tagName !== 'IMG') return;
    document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true }));
});
// Native Zurück-Geste (iOS-Wischen vom Rand / Android-Zurück-Geste bzw. -Taste)
// unterstützen: springt zum vorherigen Tab zurück, statt die Seite/App zu verlassen.
// Jeder Tab-Wechsel (Haupt-, Bereichs-, Kategorie- und Konto-Tabs) wird dazu im
// Browser-Verlauf vermerkt.
// Zurück-Button: springt zum vorherigen Tab zurück (Subtab -> Haupttab -> Startseite).
// Nutzt einen eigenen Navigations-Speicher statt der Browser-History-API, da diese sich
// in eingebetteten/iframe-Kontexten (z.B. Hugging-Face-Wrapper-Seite) unzuverlässig
// verhalten kann. Position vertikal mittig, damit der Button immer sichtbar ist, egal
// wie weit man auf einer langen Seite gescrollt hat.
(function() {
    const navAuswahl = '[id^="b_haupt_"], [id^="b_bereich_"], [id^="b_show_"], [id^="b_konto_tab_"]';
    let aktuellerTab = null;
    const verlauf = [];
    const zurueckBtn = document.createElement('button');
    zurueckBtn.textContent = '◀';
    zurueckBtn.setAttribute('aria-label', 'Zurück');
    zurueckBtn.style.setProperty('position', 'fixed', 'important');
    zurueckBtn.style.setProperty('z-index', '2147483647', 'important');
    zurueckBtn.style.setProperty('width', '48px', 'important');
    zurueckBtn.style.setProperty('height', '48px', 'important');
    zurueckBtn.style.setProperty('border-radius', '50%', 'important');
    zurueckBtn.style.setProperty('background', '#6D4C41', 'important');
    zurueckBtn.style.setProperty('color', '#FFFCF5', 'important');
    zurueckBtn.style.setProperty('border', 'none', 'important');
    zurueckBtn.style.setProperty('font-size', '20px', 'important');
    zurueckBtn.style.setProperty('box-shadow', '0 2px 8px rgba(0,0,0,0.4)', 'important');
    zurueckBtn.style.setProperty('display', 'none', 'important');
    zurueckBtn.style.setProperty('align-items', 'center', 'important');
    zurueckBtn.style.setProperty('justify-content', 'center', 'important');
    zurueckBtn.style.setProperty('touch-action', 'none', 'important');
    zurueckBtn.style.setProperty('cursor', 'grab', 'important');

    // Gespeicherte Position laden - bleibt erhalten, bis der Nutzer den Button
    // erneut selbst verschiebt (auch über Neustarts der App hinweg, pro Gerät/Browser).
    function ladePosition() {
        try {
            const g = JSON.parse(localStorage.getItem('petbase_zurueck_position'));
            if (g && typeof g.top === 'number' && typeof g.left === 'number') return g;
        } catch (e) {}
        return { top: window.innerHeight / 2 - 24, left: 12 };
    }
    function setzePosition(top, left) {
        top = Math.max(4, Math.min(top, window.innerHeight - 52));
        left = Math.max(4, Math.min(left, window.innerWidth - 52));
        zurueckBtn.style.setProperty('top', top + 'px', 'important');
        zurueckBtn.style.setProperty('left', left + 'px', 'important');
        return { top, left };
    }
    let position = setzePosition(ladePosition().top, ladePosition().left);

    function einhaengen() { if (document.body && !document.body.contains(zurueckBtn)) document.body.appendChild(zurueckBtn); }
    einhaengen();
    document.addEventListener('DOMContentLoaded', einhaengen);
    // Selbstheilung: falls der Button durch eine interne Aktualisierung der Seite aus
    // dem DOM entfernt wird, sofort automatisch wieder einfügen (statt erst beim
    // nächsten Tab-Klick, was zu einem zeitweisen "Verschwinden" führte).
    if (window.MutationObserver && document.body) {
        new MutationObserver(function() { einhaengen(); }).observe(document.body, { childList: true });
    }
    setInterval(einhaengen, 250); // häufige Prüfung, da größere Tab-Wechsel (z.B. Haupttabs) länger dauern können

    // Button frei ziehbar machen (Maus für Desktop-Tests, Touch für Handy)
    let ziehStart = null, wurdeGezogen = false;
    function ziehBeginn(x, y) {
        ziehStart = { x, y, top: parseFloat(zurueckBtn.style.top), left: parseFloat(zurueckBtn.style.left) };
        wurdeGezogen = false;
        zurueckBtn.style.setProperty('cursor', 'grabbing', 'important');
    }
    function ziehBewegung(x, y) {
        if (!ziehStart) return;
        const dx = x - ziehStart.x, dy = y - ziehStart.y;
        if (Math.abs(dx) > 6 || Math.abs(dy) > 6) wurdeGezogen = true;
        if (wurdeGezogen) position = setzePosition(ziehStart.top + dy, ziehStart.left + dx);
    }
    function ziehEnde() {
        if (ziehStart && wurdeGezogen) { try { localStorage.setItem('petbase_zurueck_position', JSON.stringify(position)); } catch (e) {} }
        ziehStart = null;
        zurueckBtn.style.setProperty('cursor', 'grab', 'important');
    }
    zurueckBtn.addEventListener('mousedown', function(e) { ziehBeginn(e.clientX, e.clientY); });
    document.addEventListener('mousemove', function(e) { ziehBewegung(e.clientX, e.clientY); });
    document.addEventListener('mouseup', ziehEnde);
    zurueckBtn.addEventListener('touchstart', function(e) { const t = e.touches[0]; ziehBeginn(t.clientX, t.clientY); }, { passive: true });
    zurueckBtn.addEventListener('touchmove', function(e) { const t = e.touches[0]; ziehBewegung(t.clientX, t.clientY); }, { passive: true });
    zurueckBtn.addEventListener('touchend', ziehEnde);

    // Navigation: springt zum Tab VOR dem aktuell aktiven zurück (nicht zum aktuellen
    // selbst - das war der Grund, warum vorher keine sichtbare Aktion passierte, da ein
    // erneuter Klick auf den bereits aktiven Tab wirkungslos ist).
    zurueckBtn.addEventListener('click', function(ev) {
        if (wurdeGezogen) { wurdeGezogen = false; return; }
        ev.preventDefault(); ev.stopPropagation();
        const zielId = verlauf.pop();
        if (verlauf.length === 0) zurueckBtn.style.setProperty('display', 'none', 'important');
        if (!zielId) return;
        aktuellerTab = (zielId === '__START__') ? null : zielId;
        const zielBtn = (zielId === '__START__') ? document.getElementById('b_haupt_verwaltung') : document.getElementById(zielId);
        if (zielBtn) zielBtn.click();
    });
    document.addEventListener('click', function(e) {
        if (e.target === zurueckBtn) return;
        const btn = e.target.closest(navAuswahl);
        if (!btn || !btn.id || btn.id === aktuellerTab) return;
        if (aktuellerTab) verlauf.push(aktuellerTab);
        else verlauf.push('__START__'); // garantiert, dass genug Zurück-Klicks immer bei der echten Startseite ankommen
        aktuellerTab = btn.id;
        einhaengen();
        if (verlauf.length > 0) zurueckBtn.style.setProperty('display', 'flex', 'important');
    });

    // Banner als direkter "Zurück zur Startseite"-Knopf: springt sofort zur Startseite
    // und setzt dabei auch den Zurück-Verlauf zurück, da man ja bewusst ganz von vorne beginnt.
    function bannerEinrichten() {
        const banner = document.getElementById('app_banner');
        if (!banner || banner.dataset.petbaseKlickbar) return;
        banner.dataset.petbaseKlickbar = '1';
        banner.style.setProperty('cursor', 'pointer', 'important');
        banner.addEventListener('click', function() {
            verlauf.length = 0;
            aktuellerTab = null;
            zurueckBtn.style.setProperty('display', 'none', 'important');
            const heim = document.getElementById('b_haupt_verwaltung');
            if (heim) heim.click();
        });
    }
    bannerEinrichten();
    document.addEventListener('DOMContentLoaded', bannerEinrichten);
    setInterval(bannerEinrichten, 1000);
})();
</script>
"""
with gr.Blocks(title="PetBase", theme=zoo_theme, css=APP_CSS, head=CUSTOM_HEAD) as demo:
    gr.Image("banner.jpg", show_label=False, interactive=False, container=False, elem_id="app_banner", show_download_button=False, show_fullscreen_button=False, show_share_button=False)
    current_zoo = gr.BrowserState("")
    auto_login_pref = gr.BrowserState("ja")
    hidden_img_data = gr.Textbox(visible=False)
    news_display = gr.Markdown(visible=True)

    with gr.Column(visible=False) as onboarding_overlay:
        with gr.Column(elem_id="onboarding_backdrop"):
            with gr.Column(elem_id="onboarding_card"):
                # ERINNERUNG: Diesen Text bei jeder größeren neuen Funktion (neuer Tab,
                # neue Kernfunktion) mit aktualisieren - sonst kennen neue Nutzer die
                # Funktion gar nicht erst (siehe v7.86.3, wo Tagebuch & Haustierapotheke
                # hier zunächst gefehlt hatten, obwohl schon länger in der App).
                onboarding_content = gr.Markdown(
                    "## 🐾 Willkommen bei PetBase!\n"
                    "Kurzer Überblick, bevor es losgeht:\n\n"
                    "**🐾 Tiere** – Stammdaten, Fütterung (Trocken-/Nass-/BARF), Gesundheit & Dokumente pro Tier anlegen. Im Erweiterten Modus zusätzlich ein **📔 Tagebuch** für Beobachtungen mit Foto, als PDF exportierbar.\n\n"
                    "**🥫 Futter-Zentrale** – Foto vom Futtersack machen, App liest Barcode, Sorte & Dosierung automatisch aus.\n\n"
                    "**📁 Archiv** – Tiere archivieren (vermittelt/verstorben) und komplette PDF-Akten exportieren.\n\n"
                    "**📊 Futter Vorrat** – Behält den Überblick, wie lange dein Futtervorrat noch reicht, inkl. Preisvergleichs-Links wenn's knapp wird.\n\n"
                    "**💊 Haustierapotheke** – Durchstöberbare Sammlung tiermedizinischer Hausmittel-Empfehlungen, nach Tierart geordnet, mit Suche.\n\n"
                    "**📄 PDF-Export / 📎 Vertrag nachtragen** – Tier-Akten exportieren bzw. Pflegeverträge nachträglich hochladen.\n\n"
                    "**👤 Konto** – Erweiterter Modus für Pflege-/Gasttiere, Erinnerungen, Feedback & Einstellungen.\n\n"
                    "Du findest diese Übersicht nicht wieder – aber du wächst schnell rein. Bei Fragen hilft das 🐞 Feedback-Formular unter 'Konto'."
                )
                onboarding_close_btn = gr.Button("Los geht's!", variant="primary", elem_id="b_onboarding_close")

    with gr.Column(visible=True, elem_id="login_screen") as login_screen:
        gr.Markdown("## Willkommen!\nBitte logge dich ein oder erstelle ein kostenloses Test-Konto.")
        with gr.Row(visible=False) as quick_row:
            quick_btn = gr.Button("Weiter", variant="primary", elem_id="b_quick_login")
        pref_checkbox = gr.Checkbox(label="🔁 Zukünftig automatisch einloggen (ohne Passwort)", value=True)
        # Große, deutlich antippbare Buttons statt Gradios eingebauter (kleiner,
        # unauffälliger) Tabs - gleiches bewährtes Muster wie überall sonst in der App.
        with gr.Row(elem_classes="tab_row"):
            btn_login_tab = gr.Button("Anmelden", variant="primary", elem_id="b_login_tab")
            btn_reg_tab = gr.Button("Registrieren", variant="secondary", elem_id="b_reg_tab")
        with gr.Column(visible=True) as login_tab_box:
            login_name = gr.Textbox(label="Zoo-Name")
            login_pw = gr.Textbox(label="Passwort", type="password")
            login_btn = gr.Button("Einloggen", variant="primary", elem_id="b_login_submit")
            login_status = gr.Textbox(label="Status", visible=False)
        with gr.Column(visible=False) as reg_tab_box:
            reg_name = gr.Textbox(label="Gewünschter Zoo-Name")
            reg_pw = gr.Textbox(label="Passwort wählen", type="password")
            reg_email = gr.Textbox(label="E-Mail-Adresse (Optional)")
            reg_btn = gr.Button("Konto erstellen", variant="primary", elem_id="b_reg_submit")
            reg_status = gr.Textbox(label="Status", visible=False)
        btn_show_impressum = gr.Button("Impressum", variant="secondary", size="sm", elem_id="b_impressum")
        with gr.Column(visible=False) as impressum_box:
            # PLATZHALTER - vor echtem Betrieb (insbesondere vor einer kostenpflichtigen
            # Version) durch reale Angaben gemäß §5 TMG ersetzen: vollständiger Name,
            # ladungsfähige Anschrift, Kontakt (E-Mail und/oder Telefon), bei
            # Personengesellschaften/Unternehmen ggf. Vertretungsberechtigte,
            # Registereintrag, USt-IdNr. Für Streitschlichtung zusätzlich Hinweis auf
            # die OS-Plattform der EU (§36 VSBG) sinnvoll, sobald ein Bezahlmodell
            # existiert. Diese Platzhalter-Version ersetzt keine Rechtsberatung.
            gr.Markdown("""### Impressum
**Angaben gemäß § 5 TMG** *(Platzhalter - noch zu vervollständigen)*

**Name:** [Vor- und Nachname]
**Anschrift:** [Straße Hausnummer, PLZ Ort]
**Kontakt:** [E-Mail-Adresse] · [Telefonnummer, optional]

**Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:** [Name wie oben]

*Hinweis: Diese Angaben sind noch nicht vollständig und müssen vor dem produktiven Einsatz - insbesondere vor Einführung einer kostenpflichtigen Version - ausgefüllt werden.*
""")

    with gr.Column(visible=False) as main_app:
        acc_status_md = gr.Markdown("<span class='msr'>account_circle</span> Eingeloggt")
        with gr.Row(elem_classes="tab_row"):
            btn_haupt_start = gr.Button("Start", variant="primary", elem_id="b_haupt_start")
            btn_haupt_verwaltung = gr.Button("Verwaltung", variant="secondary", elem_id="b_haupt_verwaltung", visible=False)
            btn_haupt_vermisst = gr.Button("Vermisst melden", variant="secondary", elem_id="b_haupt_vermisst")
            btn_haupt_konto = gr.Button("Konto", variant="secondary", elem_id="b_haupt_konto", visible=False)
            btn_haupt_futtervorrat = gr.Button("Haustierapotheke", variant="secondary", elem_id="b_haupt_futtervorrat")
        with gr.Column(visible=True, elem_id="c_haupt_start") as haupt_start:
            with gr.Row(elem_id="start_quickrow"):
                btn_start_vermisst = gr.Button("Tier vermisst", elem_id="b_start_vermisst")
                btn_start_konto = gr.Button("Haustierapotheke", elem_id="b_start_konto")
            gr.Markdown("### Auf einen Blick", elem_classes="start_section_label")
            start_bestand_html = gr.HTML(value="")
            start_anstehend_html = gr.HTML(value="")
        with gr.Column(visible=False, elem_id="c_haupt_verwaltung") as haupt_verwaltung:
            # Diese Zeile war die alte Navigation innerhalb "Verwaltung" - seit dem
            # Popup-System (v7.92.0) läuft die komplette Navigation über die Icons
            # unten, diese Zeile ist nur noch überflüssige Doppelung ("Mist", siehe
            # Nutzer-Feedback v7.92.3). Buttons bleiben im Code bestehen (werden von
            # BEREICH_MAP für die interne Sichtbarkeits-Steuerung gebraucht), sind
            # aber nicht mehr sichtbar.
            with gr.Row(elem_classes="tab_row", visible=False):
                btn_bereich_tiere = gr.Button("Tiere", variant="primary", elem_id="b_bereich_tiere")
                btn_bereich_scanner = gr.Button("Futter-Zentrale", variant="secondary", elem_id="b_bereich_scanner")
                btn_bereich_archiv = gr.Button("Archiv", variant="secondary", elem_id="b_bereich_archiv", visible=False)
                btn_bereich_inventar = gr.Button("Futter Vorrat", variant="secondary", elem_id="b_bereich_inventar")
                btn_bereich_nachtrag = gr.Button("Vertrag nachtragen", variant="secondary", elem_id="b_bereich_nachtrag", visible=False)
                btn_bereich_apotheke = gr.Button("Haustierapotheke", variant="secondary", elem_id="b_bereich_apotheke")
                btn_bereich_pdf = gr.Button("PDF-Export", variant="secondary", elem_id="b_bereich_pdf", visible=False)
                btn_bereich_tagebuch = gr.Button("Tagebuch-Übersicht", variant="secondary", elem_id="b_bereich_tagebuch")
            with gr.Column(visible=True, elem_id="c_sektion_tiere") as sektion_tiere:
                # Das Dropdown bleibt als unsichtbarer Wert-Speicher bestehen, da sehr
                # viele andere Stellen im Code (Speichern, Laden, ...) sich darauf
                # verlassen. Die sichtbare Tier-Auswahl läuft seit v7.92.1 NUR NOCH
                # über das Popup-Flyout an der unteren Leiste (nicht mehr zusätzlich
                # hier als eigene Liste - das war doppelt und wurde nicht mehr
                # gebraucht, sobald die direkte Auswahl unten da ist).
                tier_dropdown = gr.Dropdown(label="Tier wählen", choices=["Hier Auswählen", "Neues Tier..."], scale=2, visible=False)
                with gr.Accordion("⚙️ Filter", open=False, visible=False) as filter_accordion:
                    tier_filter = gr.Radio(choices=["🐾 Alle", "🏠 Eigene Tiere", "🧳 Pflege & Gäste"], value="🐾 Alle", label="Filter", interactive=True, elem_id="tier_filter_radio")
                anstehend_karte_html = gr.HTML(value="", elem_id="anstehend_karte_anchor")
                bestand_karte_html = gr.HTML(value="", elem_id="bestand_karte_anchor")
                with gr.Group(visible=False) as tier_inhalt_wrapper:
                    with gr.Row(elem_classes="tab_row", visible=False):
                        btn_show_stam = gr.Button("Stammdaten", variant="primary", elem_id="b_show_stam")
                        btn_show_futter = gr.Button("Fütterung", variant="secondary", elem_id="b_show_futter")
                        btn_show_gesund = gr.Button("Gesundheit", variant="secondary", elem_id="b_show_gesund")
                        btn_show_doku = gr.Button("Dokumente", variant="secondary", elem_id="b_show_doku")
                        btn_show_tagebuch = gr.Button("Tagebuch", variant="secondary", elem_id="b_show_tagebuch", visible=False)
                    with gr.Column(visible=True, elem_id="c_acc_stam") as acc_stam:
                        with gr.Row(elem_id="row_name_kategorie"):
                            with gr.Column(scale=2):
                                name_f = gr.Textbox(label="Name")
                                kat_f = gr.Dropdown(label="Tierauswahl", choices=["Eigenes Tier", "Pflegetier", "Urlaubsgast"], value="Eigenes Tier")
                            profil_img = gr.Image(type="filepath", label="Profilbild", scale=1, elem_id="profil_img_komponente", show_download_button=False, show_fullscreen_button=False, show_share_button=False)
                        btn_tagebuch_oeffnen = gr.Button("Tagebuch / Beobachtungen", variant="secondary", visible=False, elem_id="b_show_tagebuch_open")
                        with gr.Row(visible=False) as row_pflege_upload:
                            pflege_vertrag_neu = gr.File(label="📄 Pflegevertrag direkt hochladen (PDF oder Bild)", file_types=["image", ".pdf"], elem_id="pflege_vertrag_upload")
                        with gr.Row(elem_classes="zweispaltig"): tierart_f, rasse_f = gr.Dropdown(label="Tierart", choices=["Hund", "Hündin", "Katze", "Kater", "Pferd", "Pony", "Kaninchen", "Meerschweinchen", "Hamster", "Vogel", "Reptil - Schlange/Gecko", "Reptil - Schildkröte/Leguan", "Fisch/Aquarium", "Sonstiges"], value=None, allow_custom_value=True), gr.Textbox(label="Rasse")
                        with gr.Row(elem_classes="zweispaltig"): geb_datum_f, farbe_f = gr.Textbox(label="Geburtsdatum"), gr.Textbox(label="Farbe")
                        with gr.Row(elem_classes="zweispaltig"): kastriert_f, gew_f = gr.Dropdown(label="Kastriert", choices=["Ja", "Nein", "Unbekannt"], allow_custom_value=True), gr.Number(label="Gewicht (kg)")
                        with gr.Row(elem_classes="zweispaltig"): chip_f, tasso_f = gr.Textbox(label="Chip- / Ringnummer"), gr.Textbox(label="Registrierung (Tasso / Equidenpass etc.)")
                        kontakt_tel_f = gr.Textbox(label="📞 Kontakttelefon (für Vermisst-Steckbrief)")
                        kontakt_tel2_f = gr.Textbox(label="📞 Kontakttelefon 2 (Backup, z.B. Familie)")
                    with gr.Column(visible=False, elem_id="c_acc_tagebuch") as acc_tagebuch:
                        gr.Markdown("### 📔 Tagebuch / Beobachtungen")
                        with gr.Column(visible=True) as tagebuch_neu_eintrag_box:
                            gr.Markdown("**Neuer Eintrag**")
                            tagebuch_titel_f = gr.Textbox(label="Titel / Überschrift", placeholder="z.B. Ankunft, Tierarztbesuch, Verhaltensänderung ...")
                            tagebuch_neu_f = gr.Textbox(label="Beobachtung", lines=3, placeholder="z.B. angekommen, gebadet, geschoren, auffälliges Verhalten, körperliche Veränderung ...")
                            tagebuch_foto_f = gr.Image(type="filepath", label="📷 Foto zum Eintrag (optional)", elem_id="tagebuch_foto_upload", show_download_button=False, show_fullscreen_button=False, show_share_button=False)
                            btn_tagebuch_speichern = gr.Button("Eintrag speichern", variant="primary", elem_id="b_tagebuch_speichern")
                        btn_tagebuch_pdf = gr.Button("Als PDF exportieren", variant="secondary", elem_id="b_tagebuch_pdf")
                        tagebuch_status = gr.Textbox(label="Status", visible=False)
                        tagebuch_pdf_datei = gr.File(label="Tagebuch-PDF", height=80, elem_id="tagebuch_pdf_result")
                        gr.Markdown("---")
                        gr.Markdown("**Bisherige Einträge** (neueste zuerst)")
                        # Nur die letzten N Einträge anzeigen statt immer alle - vermeidet
                        # eine endlos lange Seite bei vielen Einträgen über Wochen/Monate.
                        # "Weitere laden" erhöht die Anzahl schrittweise.
                        tagebuch_anzahl_state = gr.State(8)
                        # Zählt hoch nach jedem Speichern, um das @gr.render unten zum
                        # Neuzeichnen mit frischen Daten zu bewegen.
                        tagebuch_refresh_zaehler = gr.State(0)
                        # Welcher Eintrag (Zeilennummer) wird gerade INLINE in seiner Karte
                        # bearbeitet - None bedeutet "keiner, alle Karten normal anzeigen".
                        tagebuch_editier_zeile_state = gr.State(None)

                        # Echte Gradio-Buttons statt HTML+JavaScript-Brücke: Gradios gr.HTML()
                        # bereinigt (sanitized) dynamischen Inhalt und entfernt dabei
                        # vermutlich Inline-Events wie onclick - das hatte den früheren
                        # "Bearbeiten"-Button unbrauchbar gemacht. @gr.render erzeugt echte,
                        # per Python verkabelte Buttons, die zuverlässig funktionieren.
                        # Bearbeiten passiert INLINE in der jeweiligen Karte (kein Scrollen
                        # zu einem Formular oben nötig): die betroffene Karte tauscht ihre
                        # Anzeige gegen echte Eingabefelder, alle anderen bleiben normal.
                        @gr.render(inputs=[current_zoo, name_f, tagebuch_anzahl_state, tagebuch_refresh_zaehler, tagebuch_editier_zeile_state])
                        def render_tagebuch_karten(zoo_name, tier_name, anzahl_anzeigen, _refresh, editier_zeile):
                            if not zoo_name or not tier_name or tier_name in ("Hier Auswählen", "Neues Tier..."):
                                gr.Markdown("<i>Bitte zuerst ein Tier auswählen.</i>")
                                return
                            eintraege = _tagebuch_eintraege(zoo_name, tier_name)
                            if not eintraege:
                                gr.Markdown("<div style='color:#7A6E5C;'><i>Noch keine Einträge vorhanden.</i></div>")
                                return
                            neueste_zuerst = list(reversed(eintraege))
                            sichtbare = neueste_zuerst[:anzahl_anzeigen]
                            rest = len(neueste_zuerst) - len(sichtbare)
                            for e in sichtbare:
                                with gr.Group():
                                    if editier_zeile == e["zeile"]:
                                        # --- Inline-Bearbeitungsmodus für genau diese Karte ---
                                        gr.Markdown(f"*{e['datum']}* – ✏️ **wird bearbeitet**")
                                        edit_titel = gr.Textbox(label="Titel / Überschrift", value=e["titel"])
                                        edit_text = gr.Textbox(label="Beobachtung", value=e["text"], lines=3)
                                        bestehendes_foto = e["foto"].split(",")[0].strip() if e["foto"] else None
                                        edit_foto = gr.Image(type="filepath", label="📷 Foto (nur bei Änderung neu hochladen)", value=bestehendes_foto, elem_id="tagebuch_foto_edit", show_download_button=False, show_fullscreen_button=False, show_share_button=False)
                                        with gr.Row():
                                            save_btn = gr.Button("Speichern", variant="primary", size="sm", elem_classes="btn-ic-save")
                                            cancel_btn = gr.Button("Abbrechen", size="sm", elem_classes="btn-ic-cancel")

                                        def _inline_speichern(titel_val, text_val, foto_val, zn=zoo_name, tn=tier_name, zeile=e["zeile"], altes_foto=e["foto"], rz=_refresh):
                                            if not text_val or not text_val.strip():
                                                return "❌ Bitte einen Text eintragen.", zeile, rz
                                            try:
                                                bestehende_zeile = ws_tagebuch.row_values(zeile)
                                                datum = bestehende_zeile[2] if len(bestehende_zeile) > 2 else datetime.now().strftime("%d.%m.%Y")
                                                foto_url = upload_image_to_cloud(foto_val) if foto_val else altes_foto
                                                neue_zeile = [zn, tn, datum, text_val.strip(), foto_url, str(titel_val).strip() if titel_val else ""]
                                                ws_tagebuch.update(range_name=f"A{zeile}:F{zeile}", values=[neue_zeile])
                                                invalidate_cache()
                                                return "✅ Eintrag aktualisiert.", None, (rz or 0) + 1
                                            except Exception as ex:
                                                return f"❌ Fehler beim Speichern: {str(ex)[:80]}", zeile, rz

                                        save_btn.click(_inline_speichern, inputs=[edit_titel, edit_text, edit_foto], outputs=[tagebuch_status, tagebuch_editier_zeile_state, tagebuch_refresh_zaehler], show_progress="hidden").then(zeige_als_toast, inputs=tagebuch_status, outputs=None)
                                        cancel_btn.click(lambda: None, inputs=None, outputs=tagebuch_editier_zeile_state, show_progress="hidden")
                                    else:
                                        # --- Normale, schreibgeschützte Anzeige ---
                                        inhalt = f"*{e['datum']}*"
                                        if e["titel"]:
                                            inhalt += f"  \n**{e['titel']}**"
                                        inhalt += f"  \n{e['text']}"
                                        gr.Markdown(inhalt)
                                        if e["foto"] and e["foto"].startswith("http"):
                                            gr.Image(value=e["foto"].split(",")[0].strip(), show_label=False, container=False, height=180, interactive=False, show_download_button=False, show_fullscreen_button=False, show_share_button=False)
                                        edit_btn = gr.Button("Bearbeiten", size="sm", elem_classes="btn-ic-edit")
                                        edit_btn.click(lambda zeile=e["zeile"]: zeile, inputs=None, outputs=tagebuch_editier_zeile_state, show_progress="hidden")
                            if rest > 0:
                                mehr_btn = gr.Button(f"Weitere Einträge laden ({rest} älter)", size="sm", elem_classes="btn-ic-more")
                                mehr_btn.click(lambda a=anzahl_anzeigen: a + 10, inputs=None, outputs=tagebuch_anzahl_state, show_progress="hidden")
                    with gr.Column(visible=False, elem_id="c_acc_futter") as acc_futter:
                        with gr.Group():
                            barf_cb = gr.Checkbox(label="BARF / Frisch- & Rohfütterung aktiv")
                        with gr.Group(visible=True) as group_trocken:
                            with gr.Row(): trock_f, tf_phase_hinweis = gr.Dropdown(label="Trockenfutter Sorte", choices=["Hier Auswählen"], value="Hier Auswählen", allow_custom_value=True, scale=3), gr.Textbox(label="Hinweis", interactive=False, scale=1)
                            tf_gramm_f = gr.Number(label="Gramm pro Tag")
                        with gr.Group(visible=False) as group_barf:
                            typ_f, gramm_f = gr.Dropdown(label="BARF Typ", choices=["Hier Auswählen"], value="Hier Auswählen", allow_custom_value=True), gr.Number(label="Gramm pro Tag")
                            prot_f, kohl_f = gr.Number(label="Protein (g)"), gr.Number(label="Kohlenhydrate (g)")
                        nass_cb = gr.Checkbox(label="🥫 Zusätzlich Nassfutter füttern", visible=False)
                        with gr.Group(visible=False) as group_nass:
                            nass_f = gr.Dropdown(label="Nassfutter Sorte", choices=["Hier Auswählen"], value="Hier Auswählen", allow_custom_value=True)
                            nass_gramm_f = gr.Number(label="Gramm pro Tag (Nassfutter)")
                    with gr.Column(visible=False, elem_id="c_acc_gesund") as acc_gesund:
                        with gr.Group():
                            allergien_f = gr.Textbox(label="⚠️ Allergien / Unverträglichkeiten", placeholder="z.B. Nickel-Allergie, Getreide-Unverträglichkeit ...", lines=2)
                        with gr.Group():
                            impf_f = gr.Textbox(label="Letzte Impfung / Check-up")
                            with gr.Row():
                                btn_impf = gr.Button("Heute", size="sm")
                                btn_ics_impf = gr.Button("Erinnerung", size="sm", elem_classes="btn-ic-reminder")
                                btn_email_impf = gr.Button("E-Mail", size="sm", elem_classes="btn-ic-email")
                            dl_impf = gr.File(visible=False, elem_classes="kompakte_datei")
                            email_impf_status = gr.Textbox(label="Status", visible=False)
                        with gr.Group():
                            wurm_f = gr.Textbox(label="Letzte Wurmkur / Kotprobe")
                            with gr.Row():
                                btn_wurm = gr.Button("Heute", size="sm")
                                btn_ics_wurm = gr.Button("Erinnerung", size="sm", elem_classes="btn-ic-reminder")
                                btn_email_wurm = gr.Button("E-Mail", size="sm", elem_classes="btn-ic-email")
                            dl_wurm = gr.File(visible=False, elem_classes="kompakte_datei")
                            email_wurm_status = gr.Textbox(label="Status", visible=False)
                        with gr.Group():
                            para_f = gr.Textbox(label="Parasitenschutz / Spezialpflege (z.B. Klauen, Häutung)")
                            para_int_f = gr.Radio(choices=["3 Monate", "1 Jahr"], value="3 Monate", label="Intervall")
                            with gr.Row():
                                btn_para = gr.Button("Heute", size="sm")
                                btn_ics_para = gr.Button("Erinnerung", size="sm", elem_classes="btn-ic-reminder")
                                btn_email_para = gr.Button("E-Mail", size="sm", elem_classes="btn-ic-email")
                            dl_para = gr.File(visible=False, elem_classes="kompakte_datei")
                            email_para_status = gr.Textbox(label="Status", visible=False)
                    with gr.Column(visible=False, elem_id="c_doc_accordion") as doc_accordion:
                        doc_titel_md = gr.Markdown("### 📂 Dokumente & Verträge (PDF)")
                        gr.Markdown("Wähle im Dropdown aus, welches Dokument oder welchen Vertrag du hinzufügen oder anzeigen möchtest:")
                        doc_type_select = gr.Dropdown(
                            label="Dokument oder Vertrag zum Hinzufügen / Bearbeiten auswählen",
                            choices=["Bitte wählen...", "Pflegevertrag", "Vermittlungsvertrag", "Tierarzt Dokumente (1)", "Tierarzt Dokumente (2)", "Tierarzt Dokumente (3)", "Tierarzt Dokumente (4)", "Tierarzt Dokumente (5)", "Tierarzt Dokumente (6)", "Alle Dokumente anzeigen"],
                            value="Bitte wählen..."
                        )
                        with gr.Row():
                            with gr.Column(visible=False) as col_vp: vp_img = gr.File(label="Pflegevertrag", file_types=["image", ".pdf"], elem_classes="kompakte_datei"); vp_h = gr.Textbox(visible=False)
                            with gr.Column(visible=False) as col_vv: vv_img = gr.File(label="Vermittlungsvertrag", file_types=["image", ".pdf"], elem_classes="kompakte_datei"); vv_h = gr.Textbox(visible=False)
                        with gr.Row():
                            with gr.Column(visible=False) as col_t1: t1_img = gr.File(label="Tierarzt Dokument", file_types=["image", ".pdf"], elem_classes="kompakte_datei"); t1_h = gr.Textbox(visible=False)
                            with gr.Column(visible=False) as col_t2: t2_img = gr.File(label="Tierarzt Dokument", file_types=["image", ".pdf"], elem_classes="kompakte_datei"); t2_h = gr.Textbox(visible=False)
                        with gr.Row():
                            with gr.Column(visible=False) as col_t3: t3_img = gr.File(label="Tierarzt Dokument", file_types=["image", ".pdf"], elem_classes="kompakte_datei"); t3_h = gr.Textbox(visible=False)
                            with gr.Column(visible=False) as col_t4: t4_img = gr.File(label="Tierarzt Dokument", file_types=["image", ".pdf"], elem_classes="kompakte_datei"); t4_h = gr.Textbox(visible=False)
                        with gr.Row():
                            with gr.Column(visible=False) as col_t5: t5_img = gr.File(label="Tierarzt Dokument", file_types=["image", ".pdf"], elem_classes="kompakte_datei"); t5_h = gr.Textbox(visible=False)
                            with gr.Column(visible=False) as col_t6: t6_img = gr.File(label="Tierarzt Dokument", file_types=["image", ".pdf"], elem_classes="kompakte_datei"); t6_h = gr.Textbox(visible=False)
                    save_btn = gr.Button("Profil Speichern", variant="primary", size="lg", elem_id="b_profil_speichern")
                    status = gr.Textbox(label="Status", visible=False)

            with gr.Column(visible=False, elem_id="c_sektion_scanner") as sektion_scanner:
                gr.Markdown("### 🥫 Neues Futter hinzufügen")
                btn_scan_toggle = gr.Button("Per Foto scannen (optional – füllt die Felder unten automatisch aus)", variant="secondary", elem_id="b_scan_toggle")
                scan_bild_offen = gr.State(False)
                with gr.Group(visible=False, elem_id="grp_scan_bild") as scan_bild_gruppe:
                    with gr.Row():
                        futter_bild_1 = gr.Image(sources=["webcam", "upload"], type="numpy", label="Bild 1", elem_id="futter_bild_upload_1", show_download_button=False, show_fullscreen_button=False, show_share_button=False)
                        futter_bild_2 = gr.Image(sources=["webcam", "upload"], type="numpy", label="Bild 2", elem_id="futter_bild_upload_2", show_download_button=False, show_fullscreen_button=False, show_share_button=False)
                        futter_bild_3 = gr.Image(sources=["webcam", "upload"], type="numpy", label="Bild 3", elem_id="futter_bild_upload_3", show_download_button=False, show_fullscreen_button=False, show_share_button=False)
                    btn_scan = gr.Button("Bilder auswerten", variant="primary", elem_id="b_scan_auswerten")
                with gr.Row(): scan_bc, scan_hersteller, scan_sorte = gr.Textbox(label="Barcode"), gr.Textbox(label="Hersteller"), gr.Textbox(label="Sorte")
                with gr.Row(): scan_phase, scan_gewicht, scan_tierart = gr.Textbox(label="Lebensphase"), gr.Textbox(label="Füllgewicht"), gr.Dropdown(label="Kategorie", choices=["Hundefutter", "Katzenfutter", "Nagerfutter", "Vogelfutter", "Pferdefutter", "Reptilienfutter", "Aquaristik", "Sonstiges"], value="Hundefutter", allow_custom_value=True)
                scan_futtertyp = gr.Radio(label="Fütterungsart", choices=["Trockenfutter", "Nassfutter"], value="Trockenfutter")
                with gr.Row(): scan_k1, scan_g1 = gr.Textbox(label="Gewicht 1"), gr.Textbox(label="Menge 1")
                with gr.Row(): scan_k2, scan_g2 = gr.Textbox(label="Gewicht 2"), gr.Textbox(label="Menge 2")
                with gr.Row(): scan_k3, scan_g3 = gr.Textbox(label="Gewicht 3"), gr.Textbox(label="Menge 3")
                with gr.Row(): scan_k4, scan_g4 = gr.Textbox(label="Gewicht 4"), gr.Textbox(label="Menge 4")
                with gr.Accordion("🔤 Gelesener Rohtext (zur Kontrolle)", open=False):
                    scan_raw = gr.Textbox(label="Gelesener Text", lines=3, interactive=False, show_label=False)
                btn_save_futter = gr.Button("Speichern", variant="primary", elem_id="b_save_futter")
                scan_status = gr.Textbox(label="Status", visible=False)
            with gr.Column(visible=False, elem_id="c_sektion_archiv") as sektion_archiv:
                gr.Markdown("### 📦 Tier archivieren")
                with gr.Row():
                    move_dropdown = gr.Dropdown(label="Tier in Archiv verschieben", scale=2)
                    a_grund = gr.Dropdown(choices=["Vermittelt", "Verstorben", "Sonstiges"], label="Grund", scale=1)
                with gr.Row(visible=False) as extra_vertrag_row:
                    a_vertrag_img = gr.File(label="📄 Vermittlungsvertrag", file_types=["image", ".pdf"], elem_classes="kompakte_datei")
                btn_archive, archive_status = gr.Button("Archivieren", variant="primary", size="lg", elem_id="b_archivieren"), gr.Textbox(label="Status", visible=False)
            with gr.Column(visible=False, elem_id="c_sektion_pdf") as sektion_pdf_export:
                gr.Markdown("### 📄 PDF-Akte exportieren")
                with gr.Row():
                    sel_pdf_own = gr.Dropdown(label="🏠 Eigene Tiere", choices=["Hier Auswählen"], value="Hier Auswählen", scale=2)
                    btn_pdf_own = gr.Button("PDF generieren", scale=1)
                with gr.Row(visible=False) as row_pdf_foster:
                    sel_pdf_foster = gr.Dropdown(label="🧳 Pflege- & Gasttiere", choices=["Hier Auswählen"], value="Hier Auswählen", scale=2)
                    btn_pdf_foster = gr.Button("PDF generieren", scale=1)
                with gr.Row():
                    sel_pdf_archiv = gr.Dropdown(label="🗄️ Archivierte Tiere", choices=["Hier Auswählen"], value="Hier Auswählen", scale=2)
                    btn_pdf_archiv = gr.Button("PDF generieren", scale=1)
                pdf_status = gr.Textbox(label="Status PDF", visible=False)
                with gr.Group():
                    gr.Markdown("**Was soll enthalten sein?**")
                    pdf_mit_info = gr.Checkbox(label="📋 Infoblatt (Stammdaten, Fütterung, Gesundheit)", value=True)
                    pdf_mit_pflege = gr.Checkbox(label="📎 Pflegevertrag (falls vorhanden)", value=True)
                    pdf_mit_vermittlung = gr.Checkbox(label="📄 Vermittlungsvertrag (falls vorhanden)", value=True)
                    pdf_mit_tierarzt = gr.Checkbox(label="🩺 Alle Tierarzt-Dokumente (falls vorhanden)", value=True)
                    pdf_mit_tagebuch = gr.Checkbox(label="📔 Tagebuch (falls vorhanden)", value=False)
                pdf_file = gr.File(label="Download Akte", elem_classes="kompakte_datei")
            with gr.Column(visible=False, elem_id="c_sektion_nachtrag") as sektion_nachtrag:
                gr.Markdown("### 📎 Vertrag nachtragen")
                nach_ziel_radio = gr.Radio(choices=["🧳 Aktueller Bestand", "🗄️ Archiv"], value="🧳 Aktueller Bestand", label="Wo befindet sich das Tier?")
                nach_aktiv_dd = gr.Dropdown(label="Pflege- & Gasttier auswählen", choices=["Hier Auswählen"], value="Hier Auswählen", visible=True)
                nach_archiv_dd = gr.Dropdown(label="Archiviertes Tier auswählen", choices=["Hier Auswählen"], value="Hier Auswählen", visible=False)
                with gr.Row():
                    nach_typ_dd = gr.Dropdown(label="Vertragsart", choices=["Vermittlungsvertrag", "Pflegevertrag"], value="Vermittlungsvertrag")
                    nach_img = gr.File(label="📄 Vertragsdokument (Bild/PDF)", file_types=["image", ".pdf"], elem_classes="kompakte_datei")
                btn_nach_save, nach_status = gr.Button("Vertrag anhängen", variant="primary", elem_id="b_nach_save"), gr.Textbox(label="Status", visible=False)
            with gr.Column(visible=False, elem_id="c_sektion_tagebuch_uebersicht") as sektion_tagebuch_uebersicht:
                gr.Markdown("### 📔 Tagebuch")
                gr.Markdown("Wähle ein Tier aus, um sein Tagebuch zu öffnen.")
                tb_uebersicht_refresh = gr.State(0)
                @gr.render(inputs=[current_zoo, tb_uebersicht_refresh])
                def render_tagebuch_uebersicht(zoo_name, _refresh):
                    tiere = _tiere_mit_tagebuch_stand(zoo_name)
                    if not tiere:
                        gr.Markdown("<i>Noch keine Tiere angelegt.</i>")
                        return
                    for t in tiere:
                        with gr.Row(elem_classes="tb_uebersicht_row"):
                            if t["anzahl"] > 0:
                                label = f"🐾 {t['name']} ({t['tierart']})  ·  {t['anzahl']} Einträge  ·  zuletzt {t['letztes_datum']}"
                            else:
                                label = f"🐾 {t['name']} ({t['tierart']})  ·  noch keine Einträge"
                            btn = gr.Button(label, elem_classes="tb_uebersicht_btn")
                            btn.click(oeffne_tier_tagebuch_fabrik(t["name"]), inputs=[current_zoo, tagebuch_refresh_zaehler], outputs=tagebuch_uebersicht_sprung_outs, show_progress="hidden")
            with gr.Column(visible=False, elem_id="c_sektion_inventar") as sektion_inventar:
                gr.Markdown("### 📊 Futter-Übersicht")
                dash_html = gr.HTML(value="<i>Bitte logge dich ein.</i>", elem_id="dash_html_anchor")
                futter_slot_rows, futter_slot_labels, futter_slot_sorten = [], [], []
                futter_slot_aufstock_btns, futter_slot_edit_btns = [], []
                futter_slot_aufstock_rows, futter_slot_aufstock_menge, futter_slot_aufstock_confirm, futter_slot_aufstock_close = [], [], [], []
                futter_slot_edit_rows, futter_slot_edit_bestand, futter_slot_edit_sack, futter_slot_edit_confirm, futter_slot_edit_del, futter_slot_edit_more, futter_slot_edit_close = [], [], [], [], [], [], []
                futter_slot_mehr_rows, futter_slot_mehr_fields, futter_slot_mehr_speichern, futter_slot_mehr_close = [], [], [], []
                for _i in range(FUTTER_SLOT_ANZAHL):
                    with gr.Row(visible=False, elem_classes="futter_slot_row") as _row:
                        _lbl = gr.HTML("")
                        _btn_a = gr.Button("add", size="sm", scale=0, min_width=44, elem_classes="msr-icon-btn")
                        _btn_e = gr.Button("edit", size="sm", scale=0, min_width=44, elem_classes="msr-icon-btn")
                    with gr.Row(visible=False, elem_classes="futter_slot_inline") as _aufstock_row:
                        _aufstock_close = gr.Button("close", scale=0, min_width=40, elem_classes="msr-icon-btn")
                        _aufstock_menge = gr.Number(label="Menge (g)", value=500, scale=2)
                        _aufstock_confirm = gr.Button("Aufstocken", variant="primary", scale=1, elem_classes="btn-ic-check")
                    with gr.Row(visible=False, elem_classes="futter_slot_inline") as _edit_row:
                        _edit_bestand = gr.Textbox(label="Bestand (g)", scale=1)
                        _edit_sack = gr.Textbox(label="Sackgröße (g)", scale=1)
                        _edit_confirm = gr.Button("save", scale=0, min_width=40, elem_classes="msr-icon-btn")
                        _edit_del = gr.Button("delete", scale=0, min_width=40, variant="stop", elem_classes="msr-icon-btn")
                        _edit_more = gr.Button("Mehr", scale=0, min_width=60, elem_classes="btn-ic-more-inline")
                        _edit_close = gr.Button("close", scale=0, min_width=40, elem_classes="msr-icon-btn")
                    with gr.Group(visible=False, elem_classes="futter_slot_inline") as _mehr_row:
                        with gr.Row(): _m_herst, _m_sorte = gr.Textbox(label="Hersteller"), gr.Textbox(label="Sorte")
                        _m_gew, _m_sack = gr.Textbox(visible=False), gr.Textbox(visible=False)
                        with gr.Row(): _m_phase, _m_art = gr.Textbox(label="Lebensphase"), gr.Dropdown(label="Kategorie", choices=["Hundefutter", "Katzenfutter", "Nagerfutter", "Vogelfutter", "Pferdefutter", "Reptilienfutter", "Aquaristik", "Sonstiges"], value="Hundefutter", allow_custom_value=True)
                        with gr.Accordion("⚙️ Fütterungsart & Gewichts-Brackets", open=False):
                            _m_futtertyp = gr.Radio(label="Fütterungsart", choices=["Trockenfutter", "Nassfutter"], value="Trockenfutter")
                            with gr.Row(): _m_k1, _m_g1 = gr.Textbox(label="Gewicht 1"), gr.Textbox(label="Menge 1")
                            with gr.Row(): _m_k2, _m_g2 = gr.Textbox(label="Gewicht 2"), gr.Textbox(label="Menge 2")
                            with gr.Row(): _m_k3, _m_g3 = gr.Textbox(label="Gewicht 3"), gr.Textbox(label="Menge 3")
                            with gr.Row(): _m_k4, _m_g4 = gr.Textbox(label="Gewicht 4"), gr.Textbox(label="Menge 4")
                        with gr.Row():
                            _m_close = gr.Button("close", scale=0, min_width=40, elem_classes="msr-icon-btn")
                            _m_speichern = gr.Button("Speichern", variant="primary", scale=2, elem_classes="btn-ic-save")
                    _sorte_state = gr.State("")
                    futter_slot_rows.append(_row)
                    futter_slot_labels.append(_lbl)
                    futter_slot_sorten.append(_sorte_state)
                    futter_slot_aufstock_btns.append(_btn_a)
                    futter_slot_edit_btns.append(_btn_e)
                    futter_slot_aufstock_rows.append(_aufstock_row)
                    futter_slot_aufstock_menge.append(_aufstock_menge)
                    futter_slot_aufstock_confirm.append(_aufstock_confirm)
                    futter_slot_aufstock_close.append(_aufstock_close)
                    futter_slot_edit_rows.append(_edit_row)
                    futter_slot_edit_bestand.append(_edit_bestand)
                    futter_slot_edit_sack.append(_edit_sack)
                    futter_slot_edit_confirm.append(_edit_confirm)
                    futter_slot_edit_del.append(_edit_del)
                    futter_slot_edit_more.append(_edit_more)
                    futter_slot_edit_close.append(_edit_close)
                    futter_slot_mehr_rows.append(_mehr_row)
                    futter_slot_mehr_fields.append([_m_herst, _m_sorte, _m_gew, _m_sack, _m_phase, _m_art, _m_futtertyp, _m_k1, _m_g1, _m_k2, _m_g2, _m_k3, _m_g3, _m_k4, _m_g4])
                    futter_slot_mehr_speichern.append(_m_speichern)
                    futter_slot_mehr_close.append(_m_close)
                # Unsichtbare Platzhalter (werden von init_ui/change_modus/speichere_inventar noch referenziert, aber nirgends angezeigt)
                edit_f_dd = gr.Dropdown(choices=["Hier Auswählen"], value="Hier Auswählen", allow_custom_value=True, visible=False)
                dash_aufstock_dd = gr.Dropdown(choices=["Hier Auswählen"], value="Hier Auswählen", visible=False)
            with gr.Column(visible=False, elem_id="c_sektion_apotheke") as sektion_apotheke:
                apotheke_banner_img = gr.Image(value=APOTHEKE_BANNER_URL, show_label=False, container=False, interactive=False, elem_id="apotheke_banner_img", visible=apotheke_banner_ist_aktiv(), show_download_button=False, show_fullscreen_button=False, show_share_button=False)
                gr.Markdown("### 💊 Haustierapotheke")
                gr.Markdown("Hausmittel-Empfehlungen auf tiermedizinischer Basis. **Tippe auf eine Kategorie unten, um sie zu öffnen** (z.B. Hund, Katze, Maus), oder nutze die Suche (durchsucht auch die Inhalte, nicht nur die Titel).")
                with gr.Row():
                    apotheke_tierart_filter_dd = gr.Dropdown(label="🐾 Tierart eingrenzen", choices=["Alle Tierarten"], value="Alle Tierarten", scale=1)
                    apotheke_suche_f = gr.Textbox(label="🔍 Suche (z.B. 'Erbrechen', 'Magen', 'Durchfall')", placeholder="Stichwort eingeben ...", scale=2)
                with gr.Column(visible=False) as apotheke_admin_box:
                    gr.Markdown("---")
                    apotheke_banner_cb = gr.Checkbox(label="🖼️ Kopf-Banner-Bild anzeigen", value=apotheke_banner_ist_aktiv())
                    gr.Markdown("**Neuen Eintrag hinzufügen** (nur für Admins sichtbar)")
                    apotheke_neu_kategorie_f = gr.Textbox(label="Kategorie", placeholder="z.B. Hund, Katze, Maus ...")
                    apotheke_neu_titel_f = gr.Textbox(label="Titel", placeholder="z.B. Übersäuerter Magen / häufiges Erbrechen")
                    apotheke_neu_inhalt_f = gr.Textbox(label="Inhalt", lines=5, placeholder="Ausführliche Empfehlung, Hausmittel, Dosierung, wann zum Tierarzt ...")
                    btn_apotheke_speichern = gr.Button("Eintrag speichern", variant="primary", elem_id="b_apotheke_speichern")
                    apotheke_status = gr.Textbox(label="Status", visible=False)
                gr.Markdown("---")
                # Zeigt pro Kategorie einen eigenen aufklappbaren Bereich (Accordion),
                # Karten alphabetisch nach Titel sortiert, mit "Weitere laden"-Button
                # (wie bei der Änderungshistorie) und einem echten "Bearbeiten"-Button
                # pro Karte (wie beim Tagebuch) - über @gr.render erzeugt, damit die
                # Buttons zuverlässig funktionieren (siehe Tagebuch-Historie, v7.83.0).
                apotheke_seiten_state = gr.State({})
                apotheke_refresh_zaehler = gr.State(0)
                apotheke_editier_zeile_state = gr.State(None)
                # Merkt sich, welche Kategorie (Accordion) gerade aufgeklappt ist. Ohne
                # diesen Merker würde jedes Neuzeichnen (z.B. beim Klick auf "Bearbeiten")
                # ALLE Accordions wieder auf "zu" zurücksetzen, da @gr.render den
                # kompletten Baum inkl. Accordions neu erzeugt.
                apotheke_offene_kategorie_state = gr.State(None)

                @gr.render(inputs=[current_zoo, apotheke_tierart_filter_dd, apotheke_suche_f, apotheke_seiten_state, apotheke_refresh_zaehler, apotheke_editier_zeile_state, apotheke_offene_kategorie_state])
                def render_apotheke(zoo_name, tierart_filter, suchbegriff, seiten, _refresh, editier_zeile, offene_kategorie):
                    admin = ist_admin(zoo_name)
                    alle_eintraege = _apotheke_eintraege()
                    # Tierart-Eingrenzung greift VOR allem anderen (Suche wie Kategorie-
                    # Ansicht) - verhindert, dass z.B. eine Katzen-Empfehlung auftaucht,
                    # wenn jemand gezielt nach "Schlange" eingegrenzt hat.
                    if tierart_filter and tierart_filter != "Alle Tierarten":
                        alle_eintraege = [e for e in alle_eintraege if e["kategorie"] == tierart_filter]

                    def _karte(e, kat=None):
                        with gr.Group():
                            if editier_zeile == e["zeile"]:
                                edit_kat = gr.Textbox(label="Kategorie", value=e["kategorie"])
                                edit_titel = gr.Textbox(label="Titel", value=e["titel"])
                                edit_inhalt = gr.Textbox(label="Inhalt", value=e["inhalt"], lines=5)
                                with gr.Row():
                                    save_btn = gr.Button("Speichern", variant="primary", size="sm", elem_classes="btn-ic-save")
                                    cancel_btn = gr.Button("Abbrechen", size="sm", elem_classes="btn-ic-cancel")

                                def _speichern(kat_val, titel_val, inhalt_val, zeile=e["zeile"], altes_datum=e["datum"], rz=_refresh, k=kat):
                                    if not titel_val or not titel_val.strip() or not inhalt_val or not inhalt_val.strip() or not kat_val or not kat_val.strip():
                                        return "❌ Kategorie, Titel und Inhalt dürfen nicht leer sein.", zeile, rz, k
                                    try:
                                        neue_zeile = [kat_val.strip(), titel_val.strip(), inhalt_val.strip(), altes_datum or datetime.now().strftime("%d.%m.%Y")]
                                        ws_apotheke.update(range_name=f"A{zeile}:D{zeile}", values=[neue_zeile])
                                        invalidate_cache()
                                        return "✅ Eintrag aktualisiert.", None, (rz or 0) + 1, k
                                    except Exception as ex:
                                        return f"❌ Fehler beim Speichern: {str(ex)[:80]}", zeile, rz, k

                                save_btn.click(_speichern, inputs=[edit_kat, edit_titel, edit_inhalt], outputs=[apotheke_status, apotheke_editier_zeile_state, apotheke_refresh_zaehler, apotheke_offene_kategorie_state], show_progress="hidden").then(zeige_als_toast, inputs=apotheke_status, outputs=None)
                                cancel_btn.click(lambda k=kat: (None, k), inputs=None, outputs=[apotheke_editier_zeile_state, apotheke_offene_kategorie_state], show_progress="hidden")
                            else:
                                gr.Markdown(f"**{e['titel']}**  \n{e['inhalt']}")
                                if admin:
                                    edit_btn = gr.Button("Bearbeiten", size="sm", elem_classes="btn-ic-edit")
                                    edit_btn.click(lambda zeile=e["zeile"], k=kat: (zeile, k), inputs=None, outputs=[apotheke_editier_zeile_state, apotheke_offene_kategorie_state], show_progress="hidden")

                    if suchbegriff and suchbegriff.strip():
                        # Suche ignoriert die Kategorie-Gliederung bewusst - durchsucht
                        # Titel UND Inhalt über alle (ggf. per Tierart eingegrenzten)
                        # Kategorien hinweg, alphabetisch sortiert.
                        begriff = suchbegriff.strip().lower()
                        treffer = [e for e in alle_eintraege if begriff in e["titel"].lower() or begriff in e["inhalt"].lower() or begriff in e["kategorie"].lower()]
                        if not treffer:
                            gr.Markdown("<i>Keine Einträge gefunden. Versuch es mit einem anderen Suchbegriff oder einer anderen Tierart.</i>")
                            return
                        treffer.sort(key=lambda e: e["titel"].lower())
                        gr.Markdown(f"**{len(treffer)} Treffer für '{suchbegriff}'**")
                        for e in treffer:
                            _karte(e)
                        return

                    if not alle_eintraege:
                        gr.Markdown("<div style='color:#7A6E5C;'><i>Noch keine Einträge vorhanden.</i></div>")
                        return
                    kategorien = sorted(set(e["kategorie"] for e in alle_eintraege if e["kategorie"]))
                    for kat in kategorien:
                        kat_eintraege = sorted([e for e in alle_eintraege if e["kategorie"] == kat], key=lambda e: e["titel"].lower())
                        with gr.Accordion(f"{_apotheke_icon(kat)} {kat}{len(kat_eintraege)} Einträge  ▸  antippen zum Öffnen", open=(kat == offene_kategorie), elem_classes="apotheke_kategorie_accordion"):
                            anzahl_anzeigen = seiten.get(kat, 5)
                            sichtbare = kat_eintraege[:anzahl_anzeigen]
                            rest = len(kat_eintraege) - len(sichtbare)
                            for e in sichtbare:
                                _karte(e, kat=kat)
                            if rest > 0:
                                mehr_btn = gr.Button(f"⬇️ Weitere Einträge laden ({rest} weitere)", size="sm")

                                def _mehr_laden(alte_seiten=seiten, k=kat, a=anzahl_anzeigen):
                                    neu = dict(alte_seiten)
                                    neu[k] = a + 10
                                    return neu, k
                                mehr_btn.click(_mehr_laden, inputs=None, outputs=[apotheke_seiten_state, apotheke_offene_kategorie_state], show_progress="hidden")

        with gr.Column(visible=False, elem_id="c_haupt_vermisst") as haupt_vermisst:
            gr.Markdown(
                "### 🚨 Tier als vermisst melden\n"
                "Direkt hier auswählen – kein Umweg über die Verwaltung nötig."
            )
            vermisst_tier_dd = gr.Dropdown(label="Welches Tier ist vermisst?", choices=["Hier Auswählen"], value="Hier Auswählen")
            v_name = gr.Textbox(visible=False)
            v_tierart = gr.Textbox(visible=False)
            v_rasse = gr.Textbox(visible=False)
            v_farbe = gr.Textbox(visible=False)
            v_chip = gr.Textbox(visible=False)
            v_tasso = gr.Textbox(visible=False)
            v_kontakt = gr.Textbox(visible=False)
            v_kontakt2 = gr.Textbox(visible=False)
            v_img = gr.Image(visible=False, type="filepath")
            with gr.Group(visible=False) as vermisst_gruppe:
                gr.Markdown(
                    "TASSO bietet leider keine automatische Anbindung – hier bekommst du aber alle Daten "
                    "übersichtlich zum Abtippen sowie einen fertigen Steckbrief zum Ausdrucken/Teilen."
                )
                vermisst_notiz_f = gr.Textbox(label="Zusatzinfo für den Steckbrief (optional, z. B. wo/wann zuletzt gesehen)", lines=2)
                btn_vermisst_daten = gr.Button("Daten für TASSO anzeigen", elem_id="b_vermisst_daten")
                vermisst_copy_text = gr.Textbox(label="Zum Kopieren ins TASSO-Formular", lines=8, interactive=False)
                btn_tasso_link = gr.Button("TASSO Vermisst-Formular öffnen", elem_id="b_tasso_link")
                gr.Markdown(
                    "*Falls sich nichts öffnet (kann in der Standalone-App passieren): Link unten antippen & halten zum Kopieren, dann im normalen Browser (Safari/Chrome) einfügen.*\n\n"
                    "`https://www.tasso.net/Tierregister/Tier-vermisst/Tier-vermisst-melden`"
                )
                btn_vermisst_pdf = gr.Button("Vermisst-Steckbrief als PDF erstellen", variant="stop", elem_id="b_vermisst_pdf")
                vermisst_pdf_status = gr.Textbox(label="Status", visible=False)
                vermisst_pdf_file = gr.File(label="Download Steckbrief", elem_classes="kompakte_datei")

        with gr.Column(visible=False, elem_id="c_haupt_konto") as haupt_konto:
            with gr.Row(elem_classes="tab_row"):
                btn_konto_tab_einst = gr.Button("Einstellungen", variant="primary", elem_id="b_konto_tab_einst")
                btn_konto_tab_admin = gr.Button("Admin", variant="secondary", elem_id="b_konto_tab_admin", visible=False)
            with gr.Column(visible=True) as konto_tab_einstellungen:
                gr.Markdown("### ⚙️ Allgemeine Einstellungen")
                modus_cb = gr.Checkbox(label="Erweiterter Modus (Pflege- & Gasttiere aktivieren)", value=False)
                btn_show_fb = gr.Button("Feedback senden", variant="secondary", elem_id="b_show_fb")
                with gr.Column(visible=False) as konto_fb_box:
                    fb_thema = gr.Dropdown(choices=["Fehler", "Idee", "Frage"], label="Thema"); fb_text = gr.Textbox(label="Nachricht")
                    btn_fb, fb_status = gr.Button("Senden", variant="primary", elem_id="b_fb_senden"), gr.Textbox(label="Status", visible=False)
                btn_show_erinn = gr.Button("Erinnerungen", variant="secondary", elem_id="b_show_erinn")
                with gr.Column(visible=False) as konto_erinn_box:
                    set_email = gr.Textbox(label="E-Mail")
                    btn_set_save, set_status = gr.Button("Speichern", variant="primary", elem_classes="btn-ic-save"), gr.Textbox(label="Status", visible=False)
                btn_show_pw = gr.Button("Passwort", variant="secondary", elem_id="b_show_pw")
                with gr.Column(visible=False) as konto_pw_box:
                    old_pw_f = gr.Textbox(label="Alt", type="password"); new_pw_f = gr.Textbox(label="Neu", type="password")
                    btn_pw_save, pw_status = gr.Button("Ändern", elem_id="b_pw_save"), gr.Textbox(label="Status", visible=False)
                btn_show_del = gr.Button("Konto löschen", variant="secondary", elem_id="b_show_del")
                with gr.Column(visible=False) as konto_del_box:
                    del_pw_f = gr.Textbox(label="Passwort", type="password")
                    btn_del_account, del_status = gr.Button("Löschen", variant="stop", elem_id="b_del_account"), gr.Textbox(label="Status", visible=False)
            with gr.Column(visible=False) as konto_tab_admin:
                with gr.Column(visible=False) as admin_stats:
                    admin_counter = gr.HTML(value="Lade Statistiken...")
                btn_test_email, test_email_status = gr.Button("Test-E-Mail an mich selbst senden", variant="secondary", visible=False, elem_id="b_test_email"), gr.Textbox(label="Status", visible=False)
                btn_futter_cron_test, futter_cron_status = gr.Button("Futter-Check jetzt manuell auslösen (alle Konten)", variant="secondary", visible=False, elem_id="b_futter_cron_test"), gr.Textbox(label="Status", visible=False)
                btn_show_news = gr.Button("News veröffentlichen", variant="secondary", visible=False, elem_id="b_show_news")
                with gr.Column(visible=False) as admin_news_box:
                    news_text = gr.Textbox(label="Update-Nachricht")
                    news_per_email_cb = gr.Checkbox(label="📧 Auch per E-Mail an alle Nutzer mit hinterlegter E-Mail-Adresse senden", value=False)
                    btn_news, news_status = gr.Button("Veröffentlichen", variant="primary", elem_id="b_news_veroeffentlichen"), gr.Textbox(label="Status", visible=False)
                btn_show_users = gr.Button("Nutzerverwaltung", variant="secondary", visible=False, elem_id="b_show_users")
                with gr.Column(visible=False) as admin_users_box:
                    nutzerliste_html = gr.HTML(value="")
                    with gr.Group():
                        gr.Markdown("**Nutzer sperren**")
                        sperren_dd = gr.Dropdown(label="Aktiver Nutzer", choices=["Keine aktiven Nutzer"])
                        btn_sperren, sperr_status = gr.Button("Sperren", variant="stop", elem_id="b_sperren"), gr.Textbox(label="Status", visible=False)
                    with gr.Group():
                        gr.Markdown("**Nutzer entsperren**")
                        entsperren_dd = gr.Dropdown(label="Gesperrter Nutzer", choices=["Keine gesperrten Nutzer"])
                        btn_entsperren, entsperr_status = gr.Button("Entsperren", variant="primary", elem_classes="btn-ic-check"), gr.Textbox(label="Status", visible=False)
                    with gr.Group(visible=False) as admin_ernennen_box:
                        gr.Markdown("**Admin-Status vergeben** (nur Hauptadmin)")
                        admin_vergeben_dd = gr.Dropdown(label="Nutzer ohne Admin-Status", choices=["Keine passenden Nutzer"])
                        btn_admin_vergeben, admin_vergeben_status = gr.Button("Zum Admin machen", variant="primary", elem_id="b_admin_vergeben"), gr.Textbox(label="Status", visible=False)
                        gr.Markdown("**Admin-Status entziehen** (nur Hauptadmin)")
                        admin_entziehen_dd = gr.Dropdown(label="Aktueller Admin", choices=["Keine weiteren Admins"])
                        btn_admin_entziehen, admin_entziehen_status = gr.Button("Admin-Status entziehen", variant="stop", elem_id="b_admin_entziehen"), gr.Textbox(label="Status", visible=False)
                btn_show_feedback_liste = gr.Button("Feedback-Übersicht", variant="secondary", visible=False, elem_id="b_show_feedback_liste")
                with gr.Column(visible=False) as admin_feedback_liste_box:
                    feedback_liste_html = gr.HTML(value="")
                    with gr.Group():
                        gr.Markdown("**Als erledigt markieren** (löscht den Eintrag dauerhaft aus Liste & Sheet)")
                        feedback_loeschen_dd = gr.Dropdown(label="Feedback-Eintrag", choices=[])
                        btn_feedback_loeschen, feedback_loeschen_status = gr.Button("Als erledigt markieren & löschen", variant="stop", elem_classes="btn-ic-check"), gr.Textbox(label="Status", visible=False)
        gr.Markdown("<br>")
        with gr.Row(elem_id="version_footer_btn"):
            btn_versionshistorie = gr.Button(f"PetBase App v{APP_VERSION} · Änderungshistorie ansehen")
        with gr.Row(elem_id="acc_top_row", elem_classes="zentrierte_reihe"):
            btn_logout = gr.Button("Abmelden", size="sm", elem_id="b_logout")

        # ============ Popup-Flyout für die 6 Icon (v7.92.0, Schritt 3 des
        # App-Umbaus): jedes Icon springt direkt in eine kleine Auswahl, statt
        # erst eine große Zwischenseite zu zeigen. Bei "Tiere" öffnet ein Tier in
        # der Liste eine zweite kleine Ebene zur Auswahl des Unterbereichs
        # (Stammdaten/Fütterung/...) - danach schließt sich das Popup und man
        # landet direkt im gewünschten Unterbereich, nie auf einer Gruppen- oder
        # Haupt-Seite. nav_popup_state kodiert, was gerade angezeigt wird:
        # "" = geschlossen, "tiere"/"tagebuch"/"futter"/"konto"/"menu" = jeweilige
        # Liste, "tiere:<Name>" = zweite Ebene (Unterbereichs-Auswahl für dieses
        # Tier). ============
        nav_popup_state = gr.State("")
        with gr.Column(visible=False, elem_id="nav_popup_box") as nav_popup_box:
            @gr.render(inputs=[nav_popup_state, current_zoo])
            def render_nav_popup(state, zoo_name):
                render_nav_popup_inhalt(state, zoo_name)

        # ============ Feste Navigationsleiste unten - "Start" springt direkt,
        # die übrigen 5 Icons öffnen seit v7.92.0 erst das kleine Popup-Flyout
        # oben (nav_popup_box) mit einer direkten Auswahl statt sofort auf eine
        # große Zwischenseite zu springen (siehe _popup_umschalten_fabrik und
        # render_nav_popup_inhalt weiter unten für die genaue Logik). ============
        with gr.Row(elem_id="bottom_nav_bar"):
            btn_nav_start = gr.Button("Start", elem_id="b_nav_start")
            btn_nav_tiere = gr.Button("Tiere", elem_id="b_nav_tiere")
            btn_nav_futter = gr.Button("Futter", elem_id="b_nav_futter")
            btn_nav_tagebuch = gr.Button("Tagebuch", elem_id="b_nav_tagebuch")
            btn_nav_konto = gr.Button("Konto", elem_id="b_nav_konto")
            btn_nav_menu = gr.Button("Sonstiges", elem_id="b_nav_menu")

    with gr.Column(visible=False) as versionshistorie_overlay:
        with gr.Column(elem_id="versionshistorie_backdrop"):
            with gr.Column(elem_id="versionshistorie_card"):
                versionshistorie_anzahl_state = gr.State(15)
                versionshistorie_md = gr.Markdown(baue_versionshistorie_text(15)[0])
                btn_versionshistorie_mehr = gr.Button("Weitere Versionen laden", size="sm", visible=(len(VERSION_HISTORY) > 15), elem_id="b_versionshistorie_mehr")
                with gr.Row(elem_id="versionshistorie_close"):
                    btn_versionshistorie_schliessen = gr.Button("Zurück", elem_classes="btn-ic-cancel")
        # ==========================================
# 8. EVENT ROUTINES & AUTO-LOGIN
# ==========================================
    a_grund.change(lambda g: gr.update(visible=(g == "Vermittelt")), inputs=a_grund, outputs=extra_vertrag_row)
    btn_fb.click(submit_feedback, inputs=[current_zoo, fb_thema, fb_text], outputs=fb_status).then(zeige_als_toast, inputs=fb_status, outputs=None)
    def _login_reg_umschalten(ziel):
        return gr.update(visible=(ziel == "login")), gr.update(visible=(ziel == "reg")), gr.update(variant="primary" if ziel == "login" else "secondary"), gr.update(variant="primary" if ziel == "reg" else "secondary")
    btn_login_tab.click(lambda: _login_reg_umschalten("login"), inputs=None, outputs=[login_tab_box, reg_tab_box, btn_login_tab, btn_reg_tab], show_progress="hidden")
    btn_reg_tab.click(lambda: _login_reg_umschalten("reg"), inputs=None, outputs=[login_tab_box, reg_tab_box, btn_login_tab, btn_reg_tab], show_progress="hidden")
    reg_btn.click(register_user, inputs=[reg_name, reg_pw, reg_email], outputs=[reg_status, login_name, login_tab_box, reg_tab_box, btn_login_tab, btn_reg_tab], show_progress="hidden").then(zeige_als_toast, inputs=reg_status, outputs=None)
    btn_pw_save.click(change_password, inputs=[current_zoo, old_pw_f, new_pw_f], outputs=pw_status).then(zeige_als_toast, inputs=pw_status, outputs=None)
    btn_del_account.click(delete_account, inputs=[current_zoo, del_pw_f], outputs=[del_status, login_screen, main_app, current_zoo]).then(zeige_als_toast, inputs=del_status, outputs=None)
    btn_news.click(publish_news, inputs=[current_zoo, news_text, news_per_email_cb], outputs=[news_status]).then(zeige_als_toast, inputs=news_status, outputs=None)
    dropdown_outs = [sperren_dd, entsperren_dd, admin_vergeben_dd, admin_entziehen_dd]
    btn_sperren.click(nutzer_sperren, inputs=[current_zoo, sperren_dd], outputs=[sperr_status, nutzerliste_html] + dropdown_outs).then(zeige_als_toast, inputs=sperr_status, outputs=None)
    btn_entsperren.click(nutzer_entsperren, inputs=[current_zoo, entsperren_dd], outputs=[entsperr_status, nutzerliste_html] + dropdown_outs).then(zeige_als_toast, inputs=entsperr_status, outputs=None)
    btn_admin_vergeben.click(admin_ernennen, inputs=[current_zoo, admin_vergeben_dd], outputs=[admin_vergeben_status, nutzerliste_html] + dropdown_outs).then(zeige_als_toast, inputs=admin_vergeben_status, outputs=None)
    btn_admin_entziehen.click(admin_entziehen, inputs=[current_zoo, admin_entziehen_dd], outputs=[admin_entziehen_status, nutzerliste_html] + dropdown_outs).then(zeige_als_toast, inputs=admin_entziehen_status, outputs=None)
    btn_test_email.click(test_email_senden, inputs=current_zoo, outputs=test_email_status, show_progress="hidden").then(zeige_als_toast, inputs=test_email_status, outputs=None)
    btn_futter_cron_test.click(pruefe_und_sende_futter_erinnerungen, inputs=None, outputs=futter_cron_status, show_progress="hidden").then(zeige_als_toast, inputs=futter_cron_status, outputs=None)

    ui_outs = [filter_accordion, kat_f, row_pdf_foster, extra_vertrag_row, tier_dropdown, move_dropdown, sel_pdf_own, sel_pdf_foster, sel_pdf_archiv, typ_f, trock_f, edit_f_dd, dash_aufstock_dd, doc_titel_md, doc_type_select, vermisst_tier_dd, nach_aktiv_dd, nach_archiv_dd, btn_bereich_archiv, btn_bereich_pdf, btn_bereich_nachtrag]

    def check_saved_login(zoo):
        if zoo and str(zoo).strip():
            now = datetime.now().strftime("%d.%m.%Y %H:%M")
            ist_erster_login = False
            zeige_overlay = False
            onboarding_text_update = gr.update()
            try:
                rows = ws_users.get_all_values()
                for i, r in enumerate(rows):
                    if r[0] == zoo:
                        while len(r) < 6: r.append("")
                        ist_erster_login = str(r[4]).strip() in ("", "0")
                        gesehene_version = str(r[5]).strip()
                        if ist_erster_login:
                            zeige_overlay = True
                            # Standard-Onboarding-Text bleibt wie definiert (keine Änderung nötig)
                        elif gesehene_version != APP_VERSION:
                            zeige_overlay = True
                            onboarding_text_update = gr.update(value=CHANGELOG_TEXT)
                        ws_users.update_cell(i + 1, 4, now)
                        try: count = int(r[4]) + 1 if str(r[4]).isdigit() else 1
                        except: count = 1
                        ws_users.update_cell(i + 1, 5, count)
                        ws_users.update_cell(i + 1, 6, APP_VERSION)
                        break
                invalidate_cache()
            except: pass
            acc_text = f"<span class='msr'>account_circle</span> Eingeloggt als **{zoo}**"
            onboarding_update = gr.update(visible=zeige_overlay)
            try:
                unread = get_unread_news(zoo)
                news_msg = "### 📢 Neue Updates:\n" + "\n".join([f"- {n}" for n in unread]) if unread else ""
                is_admin = ist_admin(zoo)
                is_superadmin = str(zoo).strip().lower() == "petbase"
                stats_html = get_admin_stats() if is_admin else ""
                nutzerliste = erstelle_nutzerliste() if is_admin else ""
                ui = init_ui(zoo)
                return [gr.update(visible=False), gr.update(visible=True)] + ui + [lade_einstellungen(zoo), lade_modus(zoo), erstelle_dashboard(zoo), news_msg, gr.update(visible=is_admin), gr.update(visible=is_admin), gr.update(visible=is_admin), stats_html, acc_text, onboarding_update, onboarding_text_update, gr.update(visible=is_admin), nutzerliste, gr.update(visible=is_superadmin), gr.update(visible=is_admin), gr.update(visible=is_admin), gr.update(visible=is_superadmin), gr.update(visible=is_admin), gr.update(visible=is_admin), erstelle_bestand_karte(zoo), erstelle_anstehend_karte(zoo), erstelle_bestand_karte(zoo), erstelle_anstehend_karte(zoo)]
            except Exception:
                return [gr.update(visible=False), gr.update(visible=True)] + [gr.update()]*21 + [gr.update(), gr.update(), "<i>⚠️ Beim Laden ist etwas schiefgelaufen. Bitte oben auf '🔄 Neu laden' tippen.</i>", "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), "", acc_text, onboarding_update, onboarding_text_update, gr.update(visible=False), "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(), gr.update(), gr.update(), gr.update()]
        return [gr.update(visible=True), gr.update(visible=False)] + [gr.update()]*21 + [gr.update(), gr.update(), "<i>Bitte logge dich ein.</i>", "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), "", "<span class='msr'>account_circle</span> Nicht eingeloggt", gr.update(visible=False), gr.update(), gr.update(visible=False), "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(), gr.update(), gr.update(), gr.update()]

    login_outs = [login_screen, main_app] + ui_outs + [set_email, modus_cb, dash_html, news_display, admin_news_box, btn_show_news, admin_stats, admin_counter, acc_status_md, onboarding_overlay, onboarding_content, btn_show_users, nutzerliste_html, admin_ernennen_box, btn_konto_tab_admin, btn_test_email, btn_futter_cron_test, btn_show_feedback_liste, apotheke_admin_box, bestand_karte_html, anstehend_karte_html, start_bestand_html, start_anstehend_html]

    def zoo_erkannt(zoo, pref):
        checkbox_val = (pref != "nein")
        if zoo and str(zoo).strip() and pref != "nein":
            return gr.update(visible=True), gr.update(value=f"➡️ Weiter als {zoo}"), gr.update(value=zoo), gr.update(value=checkbox_val)
        return gr.update(visible=False), gr.update(), gr.update(value=zoo if zoo else ""), gr.update(value=checkbox_val)

    demo.load(lambda: None, inputs=None, outputs=None)
    demo.load(zoo_erkannt, inputs=[current_zoo, auto_login_pref], outputs=[quick_row, quick_btn, login_name, pref_checkbox])
    pref_checkbox.change(lambda v: "ja" if v else "nein", inputs=pref_checkbox, outputs=auto_login_pref)

    impressum_sichtbar_state = gr.State(False)
    def toggle_impressum(sichtbar):
        neu = not sichtbar
        return gr.update(visible=neu), neu
    btn_show_impressum.click(toggle_impressum, inputs=impressum_sichtbar_state, outputs=[impressum_box, impressum_sichtbar_state], show_progress="hidden")
    login_btn.click(login_user, inputs=[login_name, login_pw], outputs=[login_status, current_zoo]).then(zeige_als_toast, inputs=login_status, outputs=None).then(check_saved_login, inputs=[current_zoo], outputs=login_outs)
    quick_btn.click(check_saved_login, inputs=[current_zoo], outputs=login_outs)

    def logout():
        return "", gr.update(visible=True), gr.update(visible=False), "<span class='msr'>account_circle</span> Nicht eingeloggt"
    btn_logout.click(logout, inputs=None, outputs=[current_zoo, login_screen, main_app, acc_status_md])
    onboarding_close_btn.click(lambda: {onboarding_overlay: gr.update(visible=False)}, inputs=None, outputs=onboarding_overlay)
    def _versionshistorie_oeffnen():
        text, rest = baue_versionshistorie_text(15)
        return gr.update(visible=True), text, gr.update(visible=rest > 0), 15
    btn_versionshistorie.click(_versionshistorie_oeffnen, inputs=None, outputs=[versionshistorie_overlay, versionshistorie_md, btn_versionshistorie_mehr, versionshistorie_anzahl_state])
    def _versionshistorie_mehr_laden(anzahl):
        neue_anzahl = anzahl + 15
        text, rest = baue_versionshistorie_text(neue_anzahl)
        return text, gr.update(visible=rest > 0), neue_anzahl
    btn_versionshistorie_mehr.click(_versionshistorie_mehr_laden, inputs=versionshistorie_anzahl_state, outputs=[versionshistorie_md, btn_versionshistorie_mehr, versionshistorie_anzahl_state], show_progress="hidden")
    btn_versionshistorie_schliessen.click(lambda: {versionshistorie_overlay: gr.update(visible=False)}, inputs=None, outputs=versionshistorie_overlay)

    modus_cb.change(change_modus, inputs=[current_zoo, modus_cb], outputs=ui_outs)

    def zeige_pflege_upload(k, m):
        return gr.update(visible=(k == "Pflegetier" and bool(m)))
    kat_f.change(zeige_pflege_upload, inputs=[kat_f, modus_cb], outputs=row_pflege_upload)
    modus_cb.change(zeige_pflege_upload, inputs=[kat_f, modus_cb], outputs=row_pflege_upload)

    def zeige_tagebuch_button(m):
        # Dauerhaft ausgeblendet (v7.92.4) - Tagebuch/Beobachtungen ist über die
        # eigene Tagebuch-Navigation unten längst erreichbar, dieser Button in
        # den Stammdaten war doppelt und wurde nicht mehr gebraucht.
        return gr.update(visible=False)
    pflege_vertrag_neu.change(lambda f: gr.update(value=f), inputs=pflege_vertrag_neu, outputs=vp_img)

    konto_toggle_state = gr.State("")
    def toggle_konto_box(ziel, aktuell):
        neu = "" if aktuell == ziel else ziel
        boxen = {"news": admin_news_box, "users": admin_users_box, "fb": konto_fb_box, "erinn": konto_erinn_box, "pw": konto_pw_box, "del": konto_del_box, "feedback_liste": admin_feedback_liste_box}
        updates = {box: gr.update(visible=(key == neu)) for key, box in boxen.items()}
        updates[konto_toggle_state] = neu
        return updates
    konto_toggle_outs = [konto_toggle_state, admin_news_box, admin_users_box, konto_fb_box, konto_erinn_box, konto_pw_box, konto_del_box, admin_feedback_liste_box]
    _konto_toggle_events = []
    _e = btn_show_news.click(lambda a: toggle_konto_box("news", a), inputs=konto_toggle_state, outputs=konto_toggle_outs, cancels=_konto_toggle_events.copy())
    _konto_toggle_events.append(_e)
    _e = btn_show_users.click(lambda a: toggle_konto_box("users", a), inputs=konto_toggle_state, outputs=konto_toggle_outs, cancels=_konto_toggle_events.copy()).then(lambda: (erstelle_nutzerliste(),) + nutzer_dropdowns_update(), inputs=None, outputs=[nutzerliste_html] + dropdown_outs)
    _konto_toggle_events.append(_e)
    _e = btn_show_fb.click(lambda a: toggle_konto_box("fb", a), inputs=konto_toggle_state, outputs=konto_toggle_outs, cancels=_konto_toggle_events.copy())
    _konto_toggle_events.append(_e)
    _e = btn_show_erinn.click(lambda a: toggle_konto_box("erinn", a), inputs=konto_toggle_state, outputs=konto_toggle_outs, cancels=_konto_toggle_events.copy())
    _konto_toggle_events.append(_e)
    _e = btn_show_pw.click(lambda a: toggle_konto_box("pw", a), inputs=konto_toggle_state, outputs=konto_toggle_outs, cancels=_konto_toggle_events.copy())
    _konto_toggle_events.append(_e)
    _e = btn_show_del.click(lambda a: toggle_konto_box("del", a), inputs=konto_toggle_state, outputs=konto_toggle_outs, cancels=_konto_toggle_events.copy())
    _konto_toggle_events.append(_e)
    _e = btn_show_feedback_liste.click(lambda a: toggle_konto_box("feedback_liste", a), inputs=konto_toggle_state, outputs=konto_toggle_outs, cancels=_konto_toggle_events.copy()).then(lambda: (erstelle_feedback_liste(), gr.update(choices=erstelle_feedback_dropdown_choices(), value=None)), inputs=None, outputs=[feedback_liste_html, feedback_loeschen_dd], show_progress="hidden")
    _konto_toggle_events.append(_e)
    btn_feedback_loeschen.click(feedback_als_erledigt, inputs=feedback_loeschen_dd, outputs=[feedback_loeschen_status, feedback_liste_html, feedback_loeschen_dd], show_progress="hidden").then(zeige_als_toast, inputs=feedback_loeschen_status, outputs=None)

    HAUPT_MAP = {"start": (haupt_start, btn_haupt_start), "verwaltung": (haupt_verwaltung, btn_haupt_verwaltung), "vermisst": (haupt_vermisst, btn_haupt_vermisst), "konto": (haupt_konto, btn_haupt_konto)}
    def zeige_haupt(ziel):
        _force = f"force-{datetime.now().timestamp()}"
        updates = {}
        for key, (col, btn) in HAUPT_MAP.items():
            updates[col] = gr.update(visible=(key == ziel), elem_classes=[_force] if key == ziel else [])
            updates[btn] = gr.update(variant="primary" if key == ziel else "secondary")
        return updates
    haupt_outs = [c for pair in HAUPT_MAP.values() for c in pair]


    DETAIL_MAP = {"stam": (acc_stam, btn_show_stam), "futter": (acc_futter, btn_show_futter), "gesund": (acc_gesund, btn_show_gesund), "doku": (doc_accordion, btn_show_doku), "tagebuch": (acc_tagebuch, btn_show_tagebuch)}
    def zeige_detail(ziel):
        _force = f"force-{datetime.now().timestamp()}"
        updates = {}
        for key, (col, btn) in DETAIL_MAP.items():
            updates[col] = gr.update(visible=(key == ziel), elem_classes=[_force] if key == ziel else [])
            updates[btn] = gr.update(variant="primary" if key == ziel else "secondary")
        return updates
    detail_outs = [c for pair in DETAIL_MAP.values() for c in pair]
    _detail_events = []

    KONTO_TAB_MAP = {"einst": (konto_tab_einstellungen, btn_konto_tab_einst), "admin": (konto_tab_admin, btn_konto_tab_admin)}
    def zeige_konto_tab(ziel):
        updates = {}
        for key, (col, btn) in KONTO_TAB_MAP.items():
            updates[col] = gr.update(visible=(key == ziel))
            updates[btn] = gr.update(variant="primary" if key == ziel else "secondary")
        return updates
    konto_tab_outs = [c for pair in KONTO_TAB_MAP.values() for c in pair]
    _konto_tab_events = []
    _e = btn_konto_tab_einst.click(lambda: zeige_konto_tab("einst"), inputs=None, outputs=konto_tab_outs, cancels=_konto_tab_events.copy())
    _konto_tab_events.append(_e)
    _e = btn_konto_tab_admin.click(lambda: zeige_konto_tab("admin"), inputs=None, outputs=konto_tab_outs, cancels=_konto_tab_events.copy())
    _konto_tab_events.append(_e)

    # Tier laden/speichern zuerst registrieren, damit die Kategorie-Buttons unten
    # eine noch laufende, überholte Antwort davon zuverlässig abbrechen können.
    t_ins = [current_zoo, tier_dropdown]
    t_o1 = [name_f, kat_f, tierart_f, rasse_f, geb_datum_f, farbe_f, kastriert_f, gew_f, chip_f, tasso_f, kontakt_tel_f, kontakt_tel2_f]
    t_o2 = [barf_cb, trock_f, tf_phase_hinweis, typ_f, gramm_f, prot_f, kohl_f, nass_cb, nass_f, nass_gramm_f, impf_f, wurm_f, para_f]
    t_o3 = [para_int_f, tf_gramm_f, profil_img, hidden_img_data, allergien_f, vp_img, vp_h, vv_img, vv_h, t1_img, t1_h]
    t_o4 = [t2_img, t2_h, t3_img, t3_h, t4_img, t4_h, t5_img, t5_h, t6_img, t6_h, acc_stam, tier_inhalt_wrapper, group_trocken, group_barf, group_nass, bestand_karte_html, anstehend_karte_html]
    # WICHTIG: .select() statt .change()! tier_dropdown.change() würde auch dann
    # feuern, wenn save_data() den Dropdown-Wert NACH dem Speichern selbst
    # programmatisch setzt (v7.73.12: choices+value zusammen gesetzt) - das hätte
    # eine ZWEITE, parallele load_tier()-Kette ausgelöst, die den gerade laufenden
    # Speicher-Vorgang störte, bevor die "Gespeichert!"-Meldung sichtbar wurde
    # (vermutliche Ursache für spurlos verschwindende neu angelegte Tiere).
    # .select() feuert dagegen NUR bei echter Nutzer-Auswahl aus der Liste, nicht
    # bei per Code gesetzten Werten - tier_dropdown erlaubt ohnehin keine freie
    # Texteingabe (kein allow_custom_value), daher deckt .select() jede echte
    # Nutzerinteraktion zuverlässig ab.
    _e = tier_dropdown.select(load_tier, inputs=t_ins, outputs=t_o1 + t_o2 + t_o3 + t_o4, show_progress="hidden").then(lambda: zeige_detail("stam"), inputs=None, outputs=detail_outs, cancels=_detail_events.copy())

    # Neue Tier-Auswahl-Liste (v7.91.7, ersetzt das Dropdown): ein Klick auf ein
    # Tier in der Liste soll direkt zu dessen Stammdaten springen - gleiches
    # Grundmuster wie bei der Tagebuch-Übersicht (oeffne_tier_tagebuch_fabrik),
    # nur landet es hier auf "stam" statt "tagebuch" und die sichtbare Bereich
    # ("Tiere") ändert sich dabei nicht, nur der ausgewählte Tier-Inhalt selbst.
    def oeffne_tier_stam_fabrik(tier_name, ziel_tab="stam"):
        def _oeffnen(zoo_name):
            d = {}
            d.update(zeige_haupt("verwaltung"))
            d.update(zeige_bereich("tiere"))
            d[bestand_karte_html] = erstelle_bestand_karte(zoo_name)
            d[anstehend_karte_html] = erstelle_anstehend_karte(zoo_name)
            d[tier_dropdown] = gr.update(value=tier_name)
            tier_werte = load_tier(zoo_name, tier_name)
            d.update(dict(zip(t_o1 + t_o2 + t_o3 + t_o4, tier_werte)))
            d.update(zeige_detail(ziel_tab))
            return d
        return _oeffnen
    def _neues_tier_oeffnen(zoo_name):
        d = {}
        d.update(zeige_haupt("verwaltung"))
        d.update(zeige_bereich("tiere"))
        d[bestand_karte_html] = erstelle_bestand_karte(zoo_name)
        d[anstehend_karte_html] = erstelle_anstehend_karte(zoo_name)
        d[tier_dropdown] = gr.update(value="Neues Tier...")
        tier_werte = load_tier(zoo_name, "Neues Tier...")
        d.update(dict(zip(t_o1 + t_o2 + t_o3 + t_o4, tier_werte)))
        d.update(zeige_detail("stam"))
        return d
    _detail_events.append(_e)

    sv_ins1 = [current_zoo, tier_dropdown, name_f, kat_f, tierart_f, rasse_f, geb_datum_f, farbe_f, kastriert_f, gew_f, chip_f]
    sv_ins2 = [tasso_f, kontakt_tel_f, kontakt_tel2_f, barf_cb, trock_f, typ_f, gramm_f, prot_f, kohl_f, nass_cb, nass_f, nass_gramm_f, impf_f, wurm_f, para_f, para_int_f]
    sv_ins3 = [tf_gramm_f, profil_img, hidden_img_data, vp_img, vp_h, vv_img, vv_h, t1_img, t1_h, t2_img, t2_h]
    sv_ins4 = [t3_img, t3_h, t4_img, t4_h, t5_img, t5_h, t6_img, t6_h, allergien_f]
    sv_outs = [status, tier_dropdown, move_dropdown, sel_pdf_own, sel_pdf_foster, sel_pdf_archiv, profil_img, hidden_img_data, acc_stam, dash_html]
    _e = save_btn.click(save_data, inputs=sv_ins1 + sv_ins2 + sv_ins3 + sv_ins4, outputs=sv_outs, show_progress="hidden").then(zeige_als_toast, inputs=status, outputs=None).then(lambda: zeige_detail("stam"), inputs=None, outputs=detail_outs, cancels=_detail_events.copy())
    _detail_events.append(_e)

    _e = btn_show_stam.click(lambda: zeige_detail("stam"), inputs=None, outputs=detail_outs, cancels=_detail_events.copy())
    _detail_events.append(_e)
    _e = btn_show_futter.click(lambda: zeige_detail("futter"), inputs=None, outputs=detail_outs, cancels=_detail_events.copy())
    _detail_events.append(_e)
    _e = btn_show_gesund.click(lambda: zeige_detail("gesund"), inputs=None, outputs=detail_outs, cancels=_detail_events.copy())
    _detail_events.append(_e)
    _e = btn_show_doku.click(lambda: zeige_detail("doku"), inputs=None, outputs=detail_outs, cancels=_detail_events.copy())
    _detail_events.append(_e)
    _e = btn_tagebuch_oeffnen.click(lambda: zeige_detail("tagebuch"), inputs=None, outputs=detail_outs, cancels=_detail_events.copy()).then(lambda rz: (8, (rz or 0) + 1, None), inputs=tagebuch_refresh_zaehler, outputs=[tagebuch_anzahl_state, tagebuch_refresh_zaehler, tagebuch_editier_zeile_state], show_progress="hidden")
    _detail_events.append(_e)
    modus_cb.change(zeige_tagebuch_button, inputs=modus_cb, outputs=btn_tagebuch_oeffnen).then(lambda: zeige_detail("stam"), inputs=None, outputs=detail_outs, cancels=_detail_events.copy())

    BEREICH_MAP = {"tiere": (sektion_tiere, btn_bereich_tiere), "scanner": (sektion_scanner, btn_bereich_scanner), "archiv": (sektion_archiv, btn_bereich_archiv), "inventar": (sektion_inventar, btn_bereich_inventar), "apotheke": (sektion_apotheke, btn_bereich_apotheke), "pdf": (sektion_pdf_export, btn_bereich_pdf), "nachtrag": (sektion_nachtrag, btn_bereich_nachtrag), "tagebuch_uebersicht": (sektion_tagebuch_uebersicht, btn_bereich_tagebuch)}
    def zeige_bereich(ziel):
        _force = f"force-{datetime.now().timestamp()}"
        updates = {}
        for key, (col, btn) in BEREICH_MAP.items():
            updates[col] = gr.update(visible=(key == ziel), elem_classes=[_force] if key == ziel else [])
            updates[btn] = gr.update(variant="primary" if key == ziel else "secondary")
        return updates
    bereich_outs = [c for pair in BEREICH_MAP.values() for c in pair]



    doc_type_select.change(on_doc_type_change, inputs=[doc_type_select, modus_cb], outputs=[col_vp, col_vv, col_t1, col_t2, col_t3, col_t4, col_t5, col_t6])
    tier_filter.change(filter_tiere, inputs=[current_zoo, tier_filter], outputs=tier_dropdown)
    tierart_f.change(filter_futter_nach_tierart_ui, inputs=[current_zoo, tierart_f], outputs=trock_f, show_progress="hidden")
    tierart_f.change(filter_nassfutter_nach_tierart_ui, inputs=[current_zoo, tierart_f], outputs=nass_f, show_progress="hidden")
    tierart_f.change(toggle_futterarten_verfuegbarkeit, inputs=[tierart_f, barf_cb, nass_cb], outputs=[barf_cb, group_trocken, group_barf, nass_cb, group_nass], show_progress="hidden")
    tierart_f.change(toggle_id_felder, inputs=tierart_f, outputs=[kastriert_f, chip_f, tasso_f], show_progress="hidden")
    
    s_outs = [scan_bc, scan_hersteller, scan_sorte, scan_gewicht, scan_phase, scan_k1, scan_g1, scan_k2, scan_g2, scan_k3, scan_g3, scan_k4, scan_g4, scan_raw]
    btn_scan.click(scanne_und_suche, inputs=[futter_bild_1, futter_bild_2, futter_bild_3], outputs=s_outs)

    def toggle_scan_bilder(offen):
        neu = not offen
        return {scan_bild_offen: neu, scan_bild_gruppe: gr.update(visible=neu)}
    btn_scan_toggle.click(toggle_scan_bilder, inputs=scan_bild_offen, outputs=[scan_bild_offen, scan_bild_gruppe])

    futter_slot_outs = [x for i in range(FUTTER_SLOT_ANZAHL) for x in (futter_slot_rows[i], futter_slot_labels[i], futter_slot_sorten[i])]

    def reset_alle_untertabs():
        updates = {}
        updates.update(zeige_detail("stam"))
        updates.update(zeige_konto_tab("einst"))
        for _i in range(FUTTER_SLOT_ANZAHL):
            updates[futter_slot_aufstock_rows[_i]] = gr.update(visible=False)
            updates[futter_slot_edit_rows[_i]] = gr.update(visible=False)
            updates[futter_slot_mehr_rows[_i]] = gr.update(visible=False)
        updates[konto_toggle_state] = ""
        updates[admin_news_box] = gr.update(visible=False)
        updates[admin_users_box] = gr.update(visible=False)
        updates[konto_fb_box] = gr.update(visible=False)
        updates[konto_erinn_box] = gr.update(visible=False)
        updates[konto_pw_box] = gr.update(visible=False)
        updates[konto_del_box] = gr.update(visible=False)
        return updates
    reset_untertab_outs = (detail_outs + konto_tab_outs
                            + futter_slot_aufstock_rows + futter_slot_edit_rows + futter_slot_mehr_rows
                            + konto_toggle_outs)

    def haupt_klick_kombiniert(ziel):
        d = {}
        d.update(zeige_haupt(ziel))
        d.update(reset_alle_untertabs())
        return d
    haupt_combo_outs = haupt_outs + reset_untertab_outs
    tier_liste_sprung_outs = haupt_combo_outs + bereich_outs + [bestand_karte_html, anstehend_karte_html, tier_dropdown] + t_o1 + t_o2 + t_o3 + t_o4 + detail_outs
    _haupt_events = []
    def haupt_verwaltung_klick_kombiniert(zoo_name):
        d = haupt_klick_kombiniert("verwaltung")
        d.update(zeige_bereich("tiere"))
        tier_werte = load_tier(zoo_name, "Hier Auswählen")
        d.update(dict(zip(t_o1 + t_o2 + t_o3 + t_o4, tier_werte)))
        d[tier_dropdown] = gr.update(value="Hier Auswählen")
        return d
    _e = btn_haupt_verwaltung.click(haupt_verwaltung_klick_kombiniert, inputs=current_zoo, outputs=haupt_combo_outs + bereich_outs + [tier_dropdown] + t_o1 + t_o2 + t_o3 + t_o4, cancels=_haupt_events.copy())
    _haupt_events.append(_e)
    def start_klick_kombiniert(zoo_name):
        d = haupt_klick_kombiniert("start")
        d[start_bestand_html] = erstelle_bestand_karte(zoo_name)
        d[start_anstehend_html] = erstelle_anstehend_karte(zoo_name)
        return d
    _e = btn_haupt_start.click(start_klick_kombiniert, inputs=current_zoo, outputs=haupt_combo_outs + [start_bestand_html, start_anstehend_html], cancels=_haupt_events.copy())
    _haupt_events.append(_e)
    _e = btn_haupt_vermisst.click(lambda: haupt_klick_kombiniert("vermisst"), inputs=None, outputs=haupt_combo_outs, cancels=_haupt_events.copy())
    _haupt_events.append(_e)
    _e = btn_haupt_konto.click(lambda: haupt_klick_kombiniert("konto"), inputs=None, outputs=haupt_combo_outs, cancels=_haupt_events.copy())
    _haupt_events.append(_e)
    _e = btn_start_vermisst.click(lambda: haupt_klick_kombiniert("vermisst"), inputs=None, outputs=haupt_combo_outs, cancels=_haupt_events.copy())
    _haupt_events.append(_e)

    def bereich_klick_kombiniert(ziel):
        d = {}
        d.update(zeige_bereich(ziel))
        d.update(reset_alle_untertabs())
        return d
    bereich_combo_outs = bereich_outs + reset_untertab_outs
    def bereich_tiere_klick_kombiniert(zoo_name):
        d = bereich_klick_kombiniert("tiere")
        d[bestand_karte_html] = erstelle_bestand_karte(zoo_name)
        d[anstehend_karte_html] = erstelle_anstehend_karte(zoo_name)
        return d
    def bereich_inventar_klick_kombiniert(zoo_name):
        d = bereich_klick_kombiniert("inventar")
        d[dash_html] = erstelle_dashboard(zoo_name)
        d.update(dict(zip(futter_slot_outs, fuelle_futter_slots(zoo_name))))
        return d
    # Tagebuch-Übersicht (v7.91.4): ein Klick auf ein Tier dort soll direkt zu
    # dessen Tagebuch-Tab springen - kombiniert Bereich-Wechsel (wie
    # bereich_tiere_klick_kombiniert), Tier laden (wie tier_dropdown.select) und
    # Tab-Wechsel (wie btn_tagebuch_oeffnen.click) in einem Rutsch. Als
    # Fabrik-Funktion, damit jede der dynamisch gerenderten Zeilen ihren eigenen
    # Tiernamen fest eingebunden bekommt (gleiches Muster wie bei den
    # Tagebuch-/Haustierapotheke-Bearbeiten-Buttons).
    def oeffne_tier_tagebuch_fabrik(tier_name):
        def _oeffnen(zoo_name, rz):
            d = bereich_tiere_klick_kombiniert(zoo_name)
            d[tier_dropdown] = gr.update(value=tier_name)
            tier_werte = load_tier(zoo_name, tier_name)
            d.update(dict(zip(t_o1 + t_o2 + t_o3 + t_o4, tier_werte)))
            d.update(zeige_detail("tagebuch"))
            d[tagebuch_anzahl_state] = 8
            d[tagebuch_refresh_zaehler] = (rz or 0) + 1
            d[tagebuch_editier_zeile_state] = None
            return d
        return _oeffnen
    tagebuch_uebersicht_sprung_outs = bereich_combo_outs + [bestand_karte_html, anstehend_karte_html, tier_dropdown] + t_o1 + t_o2 + t_o3 + t_o4 + detail_outs + [tagebuch_anzahl_state, tagebuch_refresh_zaehler, tagebuch_editier_zeile_state]

    # Untere Navigationsleiste (v7.91.6): "Tiere"/"Futter"/"Tagebuch" bedeuten
    # dort jeweils zwei Schritte auf einmal (Haupt-Tab UND Bereich wechseln).
    # Das vorher über zwei verkettete Klicks per JS zu lösen war KEIN reines
    # Timing-Problem, sondern ein grundsätzlicher Wettlauf zwischen zwei
    # UNABHÄNGIGEN Server-Antworten, die teils dieselben Bereiche beschreiben -
    # welche zuletzt ankommt, gewinnt, unabhängig von der Wartezeit im Browser.
    # Robuste Lösung: EINE einzige kombinierte Funktion pro Ziel, ein einziger
    # Server-Rundlauf, kein Wettlauf mehr möglich.
    # Diese kombinierte Funktion wird jetzt vom Popup-Flyout genutzt (siehe
    # render_nav_popup_inhalt weiter unten) statt von einer direkten
    # Bottom-Nav-Verkabelung - "Futter" öffnet seit v7.92.0 erst eine kleine
    # Auswahl (Scanner/Vorrat), statt sofort direkt zum Scanner zu springen.
    def nav_futter_klick_kombiniert(zoo_name):
        d = haupt_klick_kombiniert("verwaltung")
        d.update(bereich_klick_kombiniert("scanner"))
        return d
    nav_futter_outs = haupt_combo_outs + bereich_combo_outs
    _bereich_events = []
    _e = btn_bereich_tiere.click(bereich_tiere_klick_kombiniert, inputs=current_zoo, outputs=bereich_combo_outs + [bestand_karte_html, anstehend_karte_html], cancels=_bereich_events.copy())
    _bereich_events.append(_e)
    _e = btn_bereich_scanner.click(lambda: bereich_klick_kombiniert("scanner"), inputs=None, outputs=bereich_combo_outs, cancels=_bereich_events.copy())
    _bereich_events.append(_e)
    _e = btn_bereich_archiv.click(lambda: bereich_klick_kombiniert("archiv"), inputs=None, outputs=bereich_combo_outs, cancels=_bereich_events.copy())
    _bereich_events.append(_e)
    _e = btn_bereich_inventar.click(bereich_inventar_klick_kombiniert, inputs=current_zoo, outputs=bereich_combo_outs + [dash_html] + futter_slot_outs, cancels=_bereich_events.copy())
    _bereich_events.append(_e)
    def bereich_apotheke_klick_kombiniert():
        d = bereich_klick_kombiniert("apotheke")
        d[apotheke_tierart_filter_dd] = gr.update(choices=["Alle Tierarten"] + lade_apotheke_kategorien())
        return d
    _e = btn_bereich_apotheke.click(bereich_apotheke_klick_kombiniert, inputs=None, outputs=bereich_combo_outs + [apotheke_tierart_filter_dd], cancels=_bereich_events.copy())
    _bereich_events.append(_e)

    # Navigations-Umbau (v7.91.7): "Konto", "Mein Profil" und "Mehr" führten alle
    # zum selben Ziel (Konto-Einstellungen) - drei verschiedene Beschriftungen für
    # dasselbe Ziel ergaben keinen Sinn. "Mein Profil" zeigt jetzt direkt die
    # Haustierapotheke, die Konto-Kachel oben zeigt jetzt direkt Futter Vorrat,
    # "Verwaltung" ist oben nicht mehr nötig (dafür jetzt "Menü" unten).
    def nav_futtervorrat_klick_kombiniert(zoo_name):
        d = haupt_klick_kombiniert("verwaltung")
        d.update(bereich_inventar_klick_kombiniert(zoo_name))
        return d
    nav_futtervorrat_outs = haupt_combo_outs + bereich_combo_outs + [dash_html] + futter_slot_outs
    def nav_apotheke_klick_kombiniert(zoo_name):
        d = haupt_klick_kombiniert("verwaltung")
        d.update(bereich_apotheke_klick_kombiniert())
        return d
    nav_apotheke_outs = haupt_combo_outs + bereich_combo_outs + [apotheke_tierart_filter_dd]
    _e = btn_start_konto.click(nav_apotheke_klick_kombiniert, inputs=current_zoo, outputs=nav_apotheke_outs, cancels=_haupt_events.copy())
    _haupt_events.append(_e)
    _e = btn_haupt_futtervorrat.click(nav_apotheke_klick_kombiniert, inputs=current_zoo, outputs=nav_apotheke_outs, cancels=_haupt_events.copy())
    _haupt_events.append(_e)
    _e = btn_bereich_pdf.click(lambda: bereich_klick_kombiniert("pdf"), inputs=None, outputs=bereich_combo_outs, cancels=_bereich_events.copy())
    _bereich_events.append(_e)
    _e = btn_bereich_nachtrag.click(lambda: bereich_klick_kombiniert("nachtrag"), inputs=None, outputs=bereich_combo_outs, cancels=_bereich_events.copy())
    _bereich_events.append(_e)
    _e = btn_bereich_tagebuch.click(lambda rz: dict(bereich_klick_kombiniert("tagebuch_uebersicht"), **{tb_uebersicht_refresh: (rz or 0) + 1}), inputs=tb_uebersicht_refresh, outputs=bereich_combo_outs + [tb_uebersicht_refresh], cancels=_bereich_events.copy())
    _bereich_events.append(_e)
    btn_apotheke_speichern.click(apotheke_eintrag_speichern, inputs=[current_zoo, apotheke_neu_kategorie_f, apotheke_neu_titel_f, apotheke_neu_inhalt_f, apotheke_refresh_zaehler], outputs=[apotheke_status, apotheke_neu_kategorie_f, apotheke_neu_titel_f, apotheke_neu_inhalt_f, apotheke_refresh_zaehler, apotheke_offene_kategorie_state], show_progress="hidden").then(zeige_als_toast, inputs=apotheke_status, outputs=None).then(lambda: gr.update(choices=["Alle Tierarten"] + lade_apotheke_kategorien()), inputs=None, outputs=apotheke_tierart_filter_dd, show_progress="hidden")
    apotheke_banner_cb.change(apotheke_banner_umschalten, inputs=apotheke_banner_cb, outputs=apotheke_banner_img, show_progress="hidden")
    # "Neuen Eintrag hinzufügen"-Formular ausblenden, solange gerade eine
    # bestehende Karte inline bearbeitet wird - bessere Übersichtlichkeit für Admins.
    apotheke_editier_zeile_state.change(lambda z: gr.update(visible=(z is None)), inputs=apotheke_editier_zeile_state, outputs=apotheke_admin_box, show_progress="hidden")
    s_in = [current_zoo, scan_bc, scan_hersteller, scan_sorte, scan_gewicht, scan_phase, scan_tierart, scan_futtertyp, scan_k1, scan_g1, scan_k2, scan_g2, scan_k3, scan_g3, scan_k4, scan_g4]
    btn_save_futter.click(speichere_inventar, inputs=s_in, outputs=[scan_status, typ_f, trock_f, edit_f_dd, dash_aufstock_dd], show_progress="hidden").then(zeige_als_toast, inputs=scan_status, outputs=None).then(erstelle_dashboard, inputs=current_zoo, outputs=dash_html).then(fuelle_futter_slots, inputs=current_zoo, outputs=futter_slot_outs)

    SCROLL_ZU_DASHBOARD_JS = """
    () => {
        const el = document.getElementById('dash_html_anchor');
        if (el) { el.scrollIntoView({behavior: 'smooth', block: 'start'}); }
    }
    """


    def oeffne_inline_aufstock(_i):
        def fn():
            return {futter_slot_aufstock_rows[_i]: gr.update(visible=True), futter_slot_edit_rows[_i]: gr.update(visible=False), futter_slot_mehr_rows[_i]: gr.update(visible=False)}
        return fn
    for _i in range(FUTTER_SLOT_ANZAHL):
        futter_slot_aufstock_btns[_i].click(oeffne_inline_aufstock(_i), inputs=None, outputs=[futter_slot_aufstock_rows[_i], futter_slot_edit_rows[_i], futter_slot_mehr_rows[_i]])
        futter_slot_aufstock_close[_i].click(lambda: gr.update(visible=False), inputs=None, outputs=futter_slot_aufstock_rows[_i])
        futter_slot_aufstock_confirm[_i].click(inline_aufstocken, inputs=[current_zoo, futter_slot_sorten[_i], futter_slot_aufstock_menge[_i]], outputs=[dash_html] + futter_slot_outs).then(lambda: gr.update(visible=False), inputs=None, outputs=futter_slot_aufstock_rows[_i])

    def oeffne_inline_edit(_i):
        def fn(zoo_name, sorte):
            bestand, sack = hole_bestand_sackgroesse(zoo_name, sorte)
            return {futter_slot_edit_rows[_i]: gr.update(visible=True), futter_slot_aufstock_rows[_i]: gr.update(visible=False), futter_slot_mehr_rows[_i]: gr.update(visible=False),
                    futter_slot_edit_bestand[_i]: bestand, futter_slot_edit_sack[_i]: sack}
        return fn
    for _i in range(FUTTER_SLOT_ANZAHL):
        futter_slot_edit_btns[_i].click(oeffne_inline_edit(_i), inputs=[current_zoo, futter_slot_sorten[_i]], outputs=[futter_slot_edit_rows[_i], futter_slot_aufstock_rows[_i], futter_slot_mehr_rows[_i], futter_slot_edit_bestand[_i], futter_slot_edit_sack[_i]])
        futter_slot_edit_close[_i].click(lambda: gr.update(visible=False), inputs=None, outputs=futter_slot_edit_rows[_i])
        futter_slot_edit_confirm[_i].click(inline_bestand_setzen, inputs=[current_zoo, futter_slot_sorten[_i], futter_slot_edit_bestand[_i], futter_slot_edit_sack[_i]], outputs=[dash_html] + futter_slot_outs).then(lambda: gr.update(visible=False), inputs=None, outputs=futter_slot_edit_rows[_i])
        futter_slot_edit_del[_i].click(inline_loeschen, inputs=[current_zoo, futter_slot_sorten[_i]], outputs=[dash_html] + futter_slot_outs).then(lambda: gr.update(visible=False), inputs=None, outputs=futter_slot_edit_rows[_i])

    def oeffne_inline_mehr(_i):
        def fn(zoo_name, sorte):
            felder = load_futter_edit(zoo_name, sorte)
            result = {futter_slot_mehr_rows[_i]: gr.update(visible=True), futter_slot_edit_rows[_i]: gr.update(visible=False), futter_slot_aufstock_rows[_i]: gr.update(visible=False)}
            for komponente, wert in zip(futter_slot_mehr_fields[_i], felder):
                result[komponente] = wert
            return result
        return fn
    for _i in range(FUTTER_SLOT_ANZAHL):
        futter_slot_edit_more[_i].click(oeffne_inline_mehr(_i), inputs=[current_zoo, futter_slot_sorten[_i]], outputs=[futter_slot_mehr_rows[_i], futter_slot_edit_rows[_i], futter_slot_aufstock_rows[_i]] + futter_slot_mehr_fields[_i])
        futter_slot_mehr_close[_i].click(lambda: gr.update(visible=False), inputs=None, outputs=futter_slot_mehr_rows[_i])
        futter_slot_mehr_speichern[_i].click(inline_mehr_speichern, inputs=[current_zoo, futter_slot_sorten[_i]] + futter_slot_mehr_fields[_i], outputs=[dash_html] + futter_slot_outs).then(lambda: gr.update(visible=False), inputs=None, outputs=futter_slot_mehr_rows[_i])

    btn_set_save.click(speichere_einstellungen, inputs=[current_zoo, set_email], outputs=set_status).then(zeige_als_toast, inputs=set_status, outputs=None)
    btn_impf.click(get_heute, outputs=impf_f); btn_wurm.click(get_heute, outputs=wurm_f); btn_para.click(get_heute, outputs=para_f)
    btn_ics_impf.click(lambda n, d: erstelle_erinnerung(n, d, "Impfung"), [name_f, impf_f], dl_impf, show_progress="hidden")
    btn_ics_wurm.click(lambda n, d: erstelle_erinnerung(n, d, "Wurmkur"), [name_f, wurm_f], dl_wurm, show_progress="hidden")
    btn_ics_para.click(lambda n, d, i: erstelle_erinnerung(n, d, "Parasitenschutz", i), [name_f, para_f, para_int_f], dl_para, show_progress="hidden")
    btn_email_impf.click(lambda z, n, d: erinnerung_per_email(z, n, "Impfung", d), [current_zoo, name_f, impf_f], email_impf_status, show_progress="hidden").then(zeige_als_toast, inputs=email_impf_status, outputs=None)
    btn_email_wurm.click(lambda z, n, d: erinnerung_per_email(z, n, "Wurmkur", d), [current_zoo, name_f, wurm_f], email_wurm_status, show_progress="hidden").then(zeige_als_toast, inputs=email_wurm_status, outputs=None)
    btn_email_para.click(lambda z, n, d: erinnerung_per_email(z, n, "Parasitenschutz", d), [current_zoo, name_f, para_f], email_para_status, show_progress="hidden").then(zeige_als_toast, inputs=email_para_status, outputs=None)
    barf_cb.change(lambda is_barf: {group_trocken: gr.update(visible=not is_barf), group_barf: gr.update(visible=is_barf)}, barf_cb, [group_trocken, group_barf])
    nass_cb.change(lambda is_nass: {group_nass: gr.update(visible=is_nass)}, nass_cb, group_nass)
    for f in [gew_f, barf_cb, typ_f, tierart_f]: f.change(berechne_barf, [current_zoo, gew_f, barf_cb, typ_f, tierart_f], [gramm_f, prot_f, kohl_f], show_progress="hidden")
    gew_f.change(berechne_trockenfutter, [current_zoo, gew_f, trock_f, tf_gramm_f, tierart_f], tf_gramm_f, show_progress="hidden")
    trock_f.change(berechne_trockenfutter, [current_zoo, gew_f, trock_f, tf_gramm_f, tierart_f], tf_gramm_f, show_progress="hidden")
    trock_f.change(zeige_futter_phase, [current_zoo, trock_f], tf_phase_hinweis, show_progress="hidden")
    gew_f.change(berechne_trockenfutter, [current_zoo, gew_f, nass_f, nass_gramm_f, tierart_f], nass_gramm_f, show_progress="hidden")
    nass_f.change(berechne_trockenfutter, [current_zoo, gew_f, nass_f, nass_gramm_f, tierart_f], nass_gramm_f, show_progress="hidden")
    tierart_f.change(berechne_trockenfutter, [current_zoo, gew_f, trock_f, tf_gramm_f, tierart_f], tf_gramm_f, show_progress="hidden")
    tierart_f.change(berechne_trockenfutter, [current_zoo, gew_f, nass_f, nass_gramm_f, tierart_f], nass_gramm_f, show_progress="hidden")
    
    btn_pdf_own.click(lambda z, s, i, p, v, t, tb: export_pdf(z, s, False, i, p, v, t, tb), [current_zoo, sel_pdf_own, pdf_mit_info, pdf_mit_pflege, pdf_mit_vermittlung, pdf_mit_tierarzt, pdf_mit_tagebuch], [pdf_file, pdf_status]).then(zeige_als_toast, inputs=pdf_status, outputs=None)
    btn_pdf_foster.click(lambda z, s, i, p, v, t, tb: export_pdf(z, s, False, i, p, v, t, tb), [current_zoo, sel_pdf_foster, pdf_mit_info, pdf_mit_pflege, pdf_mit_vermittlung, pdf_mit_tierarzt, pdf_mit_tagebuch], [pdf_file, pdf_status]).then(zeige_als_toast, inputs=pdf_status, outputs=None)
    btn_tagebuch_speichern.click(tagebuch_eintrag_speichern, inputs=[current_zoo, name_f, tagebuch_titel_f, tagebuch_neu_f, tagebuch_foto_f, tagebuch_anzahl_state, tagebuch_refresh_zaehler], outputs=[tagebuch_status, tagebuch_titel_f, tagebuch_neu_f, tagebuch_foto_f, tagebuch_anzahl_state, tagebuch_refresh_zaehler], show_progress="hidden").then(zeige_als_toast, inputs=tagebuch_status, outputs=None)
    # "Neuer Eintrag"-Formular (Titel/Text/Foto) ausblenden, solange gerade ein
    # bestehender Eintrag inline in seiner Karte bearbeitet wird - vermeidet
    # Verwirrung zwischen "neu anlegen" und "bestehenden Eintrag ändern".
    tagebuch_editier_zeile_state.change(lambda z: gr.update(visible=(z is None)), inputs=tagebuch_editier_zeile_state, outputs=tagebuch_neu_eintrag_box, show_progress="hidden")
    btn_tagebuch_pdf.click(exportiere_tagebuch_pdf, inputs=[current_zoo, name_f], outputs=[tagebuch_pdf_datei, tagebuch_status], show_progress="hidden").then(zeige_als_toast, inputs=tagebuch_status, outputs=None)
    btn_vermisst_daten.click(zeige_vermisst_daten, [v_name, v_tierart, v_rasse, v_farbe, v_chip, v_tasso, v_kontakt, v_kontakt2], vermisst_copy_text, show_progress="hidden")
    btn_tasso_link.click(None, None, None, js="""
    () => {
        const url = "https://www.tasso.net/Tierregister/Tier-vermisst/Tier-vermisst-melden";
        window.open(url, "_blank", "noopener,noreferrer");
    }
    """)
    btn_vermisst_pdf.click(export_vermisst_steckbrief, [current_zoo, v_name, v_tierart, v_rasse, v_farbe, v_chip, v_tasso, v_kontakt, v_kontakt2, vermisst_notiz_f, v_img], [vermisst_pdf_file, vermisst_pdf_status]).then(zeige_als_toast, inputs=vermisst_pdf_status, outputs=None)

    def lade_tier_fuer_vermisst(zoo_name, tier_name):
        leer = ("", "", "", "", "", "", "", "", gr.update(value=None), gr.update(visible=False))
        if not zoo_name or not tier_name or tier_name == "Hier Auswählen":
            return leer
        try:
            rows = get_cached_data("tier")
            row = next((r for r in rows if len(r) > 1 and str(r[1]).strip() == str(tier_name).strip() and is_zoo(r[0], zoo_name) and str(r[1]).strip().lower() != "name"), None)
            if not row: return leer
            while len(row) < 39: row.append("")
            img_val, _ = load_doc(str(row[19]))
            return row[1], row[2], row[3], row[21], row[5], row[6], von_text(row[37]), von_text(row[38]), img_val, gr.update(visible=True)
        except:
            return leer

    vermisst_tier_dd.change(lade_tier_fuer_vermisst, inputs=[current_zoo, vermisst_tier_dd], outputs=[v_name, v_tierart, v_rasse, v_farbe, v_chip, v_tasso, v_kontakt, v_kontakt2, v_img, vermisst_gruppe], show_progress="hidden")
    btn_pdf_archiv.click(lambda z, s, i, p, v, t, tb: export_pdf(z, s, True, i, p, v, t, tb), [current_zoo, sel_pdf_archiv, pdf_mit_info, pdf_mit_pflege, pdf_mit_vermittlung, pdf_mit_tierarzt, pdf_mit_tagebuch], [pdf_file, pdf_status]).then(zeige_als_toast, inputs=pdf_status, outputs=None)
    btn_archive.click(archive_tier, inputs=[current_zoo, move_dropdown, a_grund, a_vertrag_img], outputs=[archive_status, tier_dropdown, move_dropdown, sel_pdf_own, sel_pdf_foster, sel_pdf_archiv]).then(zeige_als_toast, inputs=archive_status, outputs=None).then(lambda: {tier_inhalt_wrapper: gr.update(visible=False)}, inputs=None, outputs=tier_inhalt_wrapper)
    def zeige_nachtrag_ziel(ziel):
        ist_archiv = ziel and "Archiv" in ziel
        return {nach_aktiv_dd: gr.update(visible=not ist_archiv), nach_archiv_dd: gr.update(visible=ist_archiv)}
    nach_ziel_radio.change(zeige_nachtrag_ziel, inputs=nach_ziel_radio, outputs=[nach_aktiv_dd, nach_archiv_dd])
    btn_nach_save.click(nachtrag_speichern, inputs=[current_zoo, nach_ziel_radio, nach_aktiv_dd, nach_archiv_dd, nach_typ_dd, nach_img], outputs=[nach_status], show_progress="hidden").then(zeige_als_toast, inputs=nach_status, outputs=None)
    
    def nav_start_klick_kombiniert(zoo_name):
        d = start_klick_kombiniert(zoo_name)
        d[nav_popup_state] = ""
        d[nav_popup_box] = gr.update(visible=False)
        return d
    btn_nav_start.click(nav_start_klick_kombiniert, inputs=current_zoo, outputs=haupt_combo_outs + [start_bestand_html, start_anstehend_html, nav_popup_state, nav_popup_box], cancels=_haupt_events.copy(), show_progress="hidden").then(None, None, None, js="() => { window.scrollTo({top: 0, behavior: 'smooth'}); }")

    # ============ Popup-Flyout-Inhalt (v7.92.0) - siehe Kommentar bei
    # nav_popup_box weiter oben für das Grundprinzip. Jede Zeile schließt das
    # Popup UND springt in einem Rutsch direkt zum Ziel - nie über eine
    # Zwischenseite. ============
    def _popup_umschalten_fabrik(ziel):
        def _umschalten(aktueller_zustand):
            neu = "" if aktueller_zustand == ziel else ziel
            return neu, gr.update(visible=bool(neu))
        return _umschalten
    btn_nav_tiere.click(_popup_umschalten_fabrik("tiere"), inputs=nav_popup_state, outputs=[nav_popup_state, nav_popup_box], show_progress="hidden")
    btn_nav_futter.click(_popup_umschalten_fabrik("futter"), inputs=nav_popup_state, outputs=[nav_popup_state, nav_popup_box], show_progress="hidden")
    btn_nav_tagebuch.click(_popup_umschalten_fabrik("tagebuch"), inputs=nav_popup_state, outputs=[nav_popup_state, nav_popup_box], show_progress="hidden")
    btn_nav_konto.click(_popup_umschalten_fabrik("konto"), inputs=nav_popup_state, outputs=[nav_popup_state, nav_popup_box], show_progress="hidden")
    btn_nav_menu.click(_popup_umschalten_fabrik("menu"), inputs=nav_popup_state, outputs=[nav_popup_state, nav_popup_box], show_progress="hidden")

    def nav_konto_einst_klick_kombiniert():
        d = haupt_klick_kombiniert("konto")
        d.update(zeige_konto_tab("einst"))
        return d
    def nav_konto_admin_klick_kombiniert():
        d = haupt_klick_kombiniert("konto")
        d.update(zeige_konto_tab("admin"))
        return d
    nav_konto_outs = haupt_combo_outs + konto_tab_outs

    def _mit_popup_schliessen(fn):
        """Verpackt eine Navigations-Funktion so, dass sie das Popup GLEICHZEITIG
        mit dem eigentlichen Sprung schließt - ein einziger Rutsch, ein einziges
        Update. Vorher lief das über zwei Schritte (erst schließen, dann
        springen) - dabei hat das Schließen (Ändern von nav_popup_state) das
        @gr.render, das genau diesen Button enthält, sofort neu gezeichnet und
        damit den Button MITTEN in seiner eigenen laufenden Aktion zerstört,
        bevor der zweite Schritt ankam. Ergebnis: der Sprung ging verloren, man
        landete wieder auf dem, was ohnehin schon sichtbar war (meist Start)."""
        def _wrapped(*args):
            d = fn(*args)
            d[nav_popup_state] = ""
            d[nav_popup_box] = gr.update(visible=False)
            return d
        return _wrapped

    def render_nav_popup_inhalt(state, zoo_name):
        if not state:
            return
        if state == "tiere":
            gr.Markdown("**Tier wählen**", elem_classes="nav_popup_titel")
            gruppen = _tiere_gruppiert(zoo_name)
            if not gruppen:
                gr.Markdown("<i>Noch keine Tiere angelegt.</i>")
            for gruppen_name, tiere in gruppen:
                gr.Markdown(f"*{gruppen_name}*", elem_classes="nav_popup_gruppe")
                for name in tiere:
                    btn = gr.Button(name, elem_classes="nav_popup_item")
                    btn.click(lambda n=name: (f"tiere:{n}", gr.update(visible=True)), inputs=None, outputs=[nav_popup_state, nav_popup_box], show_progress="hidden")
            btn_neu = gr.Button("+ Neues Tier anlegen", elem_classes="nav_popup_item")
            btn_neu.click(_mit_popup_schliessen(_neues_tier_oeffnen), inputs=current_zoo, outputs=tier_liste_sprung_outs + [nav_popup_state, nav_popup_box], show_progress="hidden")

        elif state.startswith("tiere:"):
            name = state[6:]
            gr.Markdown(f"**{name}** – was möchtest du anpassen?", elem_classes="nav_popup_titel")
            for label, ziel in [("Stammdaten", "stam"), ("Fütterung", "futter"), ("Gesundheit", "gesund"), ("Dokumente", "doku"), ("Tagebuch", "tagebuch")]:
                btn = gr.Button(label, elem_classes="nav_popup_item")
                btn.click(_mit_popup_schliessen(oeffne_tier_stam_fabrik(name, ziel)), inputs=current_zoo, outputs=tier_liste_sprung_outs + [nav_popup_state, nav_popup_box], show_progress="hidden")
            btn_back = gr.Button("← Zurück zur Tierliste", elem_classes="nav_popup_back")
            btn_back.click(lambda: ("tiere", gr.update(visible=True)), inputs=None, outputs=[nav_popup_state, nav_popup_box], show_progress="hidden")

        elif state == "tagebuch":
            gr.Markdown("**Tagebuch – Tier wählen**", elem_classes="nav_popup_titel")
            tiere = _tiere_mit_tagebuch_stand(zoo_name)
            if not tiere:
                gr.Markdown("<i>Noch keine Tiere angelegt.</i>")
            for t in tiere:
                label = f"{t['name']} ({t['anzahl']} Einträge)" if t["anzahl"] > 0 else f"{t['name']} (noch keine Einträge)"
                btn = gr.Button(label, elem_classes="nav_popup_item")
                btn.click(_mit_popup_schliessen(oeffne_tier_tagebuch_fabrik(t["name"])), inputs=[current_zoo, tagebuch_refresh_zaehler], outputs=tagebuch_uebersicht_sprung_outs + [nav_popup_state, nav_popup_box], show_progress="hidden")

        elif state == "futter":
            gr.Markdown("**Futter**", elem_classes="nav_popup_titel")
            btn1 = gr.Button("Futter Scanner", elem_classes="nav_popup_item")
            btn1.click(_mit_popup_schliessen(nav_futter_klick_kombiniert), inputs=current_zoo, outputs=nav_futter_outs + [nav_popup_state, nav_popup_box], show_progress="hidden")
            btn2 = gr.Button("Futter Vorrat", elem_classes="nav_popup_item")
            btn2.click(_mit_popup_schliessen(nav_futtervorrat_klick_kombiniert), inputs=current_zoo, outputs=nav_futtervorrat_outs + [nav_popup_state, nav_popup_box], show_progress="hidden")

        elif state == "konto":
            gr.Markdown("**Konto**", elem_classes="nav_popup_titel")
            btn1 = gr.Button("Einstellungen", elem_classes="nav_popup_item")
            btn1.click(_mit_popup_schliessen(lambda: nav_konto_einst_klick_kombiniert()), inputs=None, outputs=nav_konto_outs + [nav_popup_state, nav_popup_box], show_progress="hidden")
            if ist_admin(zoo_name):
                btn2 = gr.Button("Admin", elem_classes="nav_popup_item")
                btn2.click(_mit_popup_schliessen(lambda: nav_konto_admin_klick_kombiniert()), inputs=None, outputs=nav_konto_outs + [nav_popup_state, nav_popup_box], show_progress="hidden")
            btn3 = gr.Button("Abmelden", elem_classes="nav_popup_item nav_popup_danger")
            btn3.click(_mit_popup_schliessen(lambda: dict(zip([current_zoo, login_screen, main_app, acc_status_md], logout()))), inputs=None, outputs=[current_zoo, login_screen, main_app, acc_status_md, nav_popup_state, nav_popup_box], show_progress="hidden")

        elif state == "menu":
            gr.Markdown("**Sonstiges**", elem_classes="nav_popup_titel")
            for label, ziel in [("Archiv", "archiv"), ("PDF-Export", "pdf"), ("Vertrag nachtragen", "nachtrag")]:
                btn = gr.Button(label, elem_classes="nav_popup_item")
                def _fabrik(z=ziel):
                    def _f(zoo_name):
                        d = haupt_klick_kombiniert("verwaltung")
                        d.update(bereich_klick_kombiniert(z))
                        return d
                    return _f
                btn.click(_mit_popup_schliessen(_fabrik()), inputs=current_zoo, outputs=haupt_combo_outs + bereich_combo_outs + [nav_popup_state, nav_popup_box], show_progress="hidden")

    demo.load(None, None, None, js="""
    () => {
        document.documentElement.classList.remove('dark');
        document.body.classList.remove('dark');
        const container = document.querySelector('.gradio-container');
        if (container) { container.classList.remove('dark'); }
    }
    """)

    # Behalten für internes manuelles Testen über den Admin-Button (btn_futter_cron_test
    # weiter oben) - der eigentliche externe Cron-Aufruf läuft über eine eigene,
    # simple HTTP-Route (siehe ganz unten bei demo.launch()), nicht über diesen
    # Button oder Gradios eingebauten API-Mechanismus.
    _cron_out = gr.Textbox(visible=False, elem_id="cron_out_futter")
    _cron_btn = gr.Button(visible=False, elem_id="cron_btn_futter")
    _cron_btn.click(pruefe_und_sende_futter_erinnerungen, inputs=None, outputs=_cron_out)

    # Live-Popup für neues Feedback, solange PetBase (Admin) in der App eingeloggt
    # ist - Ergänzung zur E-Mail-Benachrichtigung in submit_feedback(). Fragt alle
    # 30 Sekunden nach, ob neues Feedback im Sheet steht.
    feedback_stand_state = gr.State(None)
    feedback_popup_msg = gr.Textbox(visible=False)
    feedback_timer = gr.Timer(30)
    feedback_timer.tick(pruefe_neues_feedback, inputs=[current_zoo, feedback_stand_state], outputs=[feedback_stand_state, feedback_popup_msg], show_progress="hidden").then(zeige_als_toast, inputs=feedback_popup_msg, outputs=None)

if __name__ == "__main__":
    # prevent_thread_lock=True lässt launch() sofort zurückkehren (Server läuft im
    # Hintergrund weiter), damit wir danach noch eine eigene HTTP-Route ergänzen
    # können - alle anderen Parameter (ssr_mode, pwa, favicon_path) bleiben exakt
    # wie zuvor, damit sich am bestehenden, funktionierenden Start nichts ändert.
    demo.launch(ssr_mode=False, pwa=True, favicon_path="petbase_icon.png", prevent_thread_lock=True)

    # Eigene, simple HTTP-Route für den externen Cron-Dienst (z.B. cron-job.org).
    # Gradios eingebauter API-Mechanismus (/gradio_api/call/<api_name>) erwartet
    # eine mehrstufige Anfrage-Choreografie (POST zum Einreihen, dann separates
    # GET/SSE zum Abholen des Ergebnisses) - ein simpler externer Cron-Dienst, der
    # nur einmal aufruft und die Antwort nicht weiterverfolgt, bekam dabei einen
    # 500er zurück. Diese Route ist ein ganz normaler HTTP-Endpunkt ohne
    # Gradio-Choreografie - einfacher GET- oder POST-Aufruf reicht:
    # https://<dein-space-name>.hf.space/cron/futter-check
    @demo.app.get("/cron/futter-check")
    @demo.app.post("/cron/futter-check")
    def cron_futter_check_route():
        ergebnis = pruefe_und_sende_futter_erinnerungen()
        return {"status": ergebnis}

    # Hauptthread am Leben halten, da launch() durch prevent_thread_lock sofort
    # zurückkehrt und das Programm sonst hier enden würde.
    while True:
        time.sleep(3600)