I was searching how to do exactly this (multisig with one owner wallet always offline) and came across your post with the very useful link. I eventually got this to work...
To send a transaction, I guess I have to sign the transaction data with my offline wallet first, and then use that signature as input for execTransaction on my Gnosis safe
I tested the following on both Fantom (https://safe.fantom.network/) and Polygon (https://app.safe.global/) network safes and it works. The process is probably is either exactly the same or very similar for other networks:
Once you sign the transaction hash using the offline wallet by following the instructions on your link (on your offline machine), you transfer it somehow to your online machine running Metamask. Let's say that the signature is stored in a variable, offline_signature. Then you would do:
#python code
metamask_wallet = '0x...' #your metamask wallet address, which is required for transaction approval
final_signature = '0x000000000000000000000000' + metamask_wallet[2:] + '000000000000000000000000000000000000000000000000000000000000000001' + offline_signature.signature.hex()[2:]
print (final_signature)
final_signature looks weird, but it works on the Fantom and Polygon Gnosis Safe implementations (probably ethereum too); the way I came up with this was by submitting a real multisig transaction the 'normal' way, then figuring out how all the data is glued together to create the final sig that get passed to execTransaction
Now you can use the value of final_signature to call execTransaction with your metamask wallet.
There are couple ways you can call execTransaction on your safe contract. One easy way is to open your safe contract on the appropriate web-based blockchain explorer (e.g. ftmscan, etherscan)

Then you would click "Connect to Web3" (see red circle at top of image), connect your Metamask wallet, and manually enter the transaction values under "Write as Proxy". In this case I want to send 1 FTM from my safe contract to some arbitrary address.

Click 'Write', approve the transaction in Metamask, and it will execute.
To send a non-native token, you would need to change the data and to fields and also set the value field to zero when you call getTransactionHash and execTransaction. You can get the correct data and to fields by setting up the transaction in your safe for an arbitrary non-native token, then seeing what the values for those fields are in the Metamask window (without actually signing the request).
To go even further, another way you can call execTransaction is with python. In this case you would cut out Metamask. Notice in the first screenshot where it says "ABI for the implementation contract at ...". You'd go to that contract to get the ABI and bytecode for your safe, and then call execTransaction using the same data as was entered using the block explorer UI.