Usually what I've seen is one of two solutions, the former (usually) being slightly favored: A) hide any new feature behind feature flags, separate "what's in the code" from "how the application works" essentially or B) have two branches, one for development (master) and one for production. The production branch is what QA and releasers work with, master is what developers work with, cherry-picking stuff and backporting becomes relatively trivial.
With (B) you've just reconstructed the part of git-flow that was questioned at the start of this thread. Just switch the two branches from master/production to develop/master.
B is basically Gitflow with different branch names - “one for development” is called develop, “one for production” is called main.
We've been using feature flags but mostly for controlling when things get released. But feature flags carry their own issues, they complicate the code, introduce parallel code paths, and if not maintained properly it gets difficult to introduce new features and have everything working together seamlessly. Usually you want to remove the flag soon after release, otherwise it festers. The production branch is also ok, but committing out of order can break references if commits are not in the same order as master, and patching something directly to prod can cause issues with promoting changes from master to prod, it requires some foresight to not break builds.