How is a locking script reliably detected as a P2PKH script? I've taken this example from a p2pkh guide:
1976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac

This is broken down into:
19 - byte length of the following unlocking script
76 - OP_DUP: Duplicates the top stack item.
a9 - OP_HASH160: The input is hashed twice: first with SHA-256 and then with RIPEMD-160.
14 - (20 bytes) length of the following public key hash
5fb0e9755a3424efd2ba0587d20b1e98ee29814a - public key hash of funds receiver
88 - OP_EQUALVERIFY: Returns 1 if the inputs are exactly equal, 0 otherwise. Then runs OP_VERIFY which fails and marks the transaction as invalid of the top stack value is false
ac - OP_CHECKSIG: The entire transaction's outputs, inputs, and script are hashed. The signature used by OP_CHECKSIG must be a valid signature for this hash and public key. If it is, 1 is returned, 0 otherwise

How does Bitcoin determine that 0x14 is the length of the following public key hash, rather than interpreting this byte as an opcode with the value 0x14 (20 decimal)? Is 0x14 special and will never be assigned to any opcode? I don't see any special handling of this value in the CScript::HasValidOps() and GetScriptOp() routines here.
Is there a magic marker to detect P2PKH scripts?
The secret is that it is being interpreted as an opcode.
Opcodes 0x01 through 0x4b (decimal 1 to 75) are opcodes that specify the length of the data being pushed after them, as their own numerical value. That is to say, 0x01 means it is followed by 1 byte of data, 0x02 means 2 bytes, and so on.
This lets you push up to 75 bytes of data using a single op code to specify the length.
Additionally, we also have 0x4c, 0x4d, and 0x4e. These specify:
0x4c - The next byte specifies the length of the data
0x4d - The next two bytes specify the length of the data in little endian order
0x4e - The next four bytes specify the length of the data in little endian order
Between these opcodes, you can push up to 4294967296 bytes of data via 0x4e. Unfortunately, you'll run out of block space a little bit before that, so I would advise against it.