You can set the target branch on a PR. So pr A targets main, B and C target A. After you merge A, B and C even get automatically retargeted to main (or whatever you merge A into). This handles the "block B and C until A is merged" requirement as well.
Now, you can accidentally merge B and C early by accident into A and mess with the diff, and you need to have your CI properly configured to only check the diff between them and their target (not just main) but it works fine for most use cases in my experience. The other unfortunate thing is the potential for merge conflicts in the stack, of course, if you change A after branching the others off.