People talk about "complexity" and "simplicity" in code without defining them. I've arrived at a way of looking it that I think makes it reasonably unambiguous. I prefer "opaque" to "complicated" for this concept, but I think it's really the same thing people mean when they talk about code being over-complicated.
Opaque code is code that requires you to form an unnecessarily large, detailed mental model of it in order to answer a particular question you may have about it.
People rarely read code in its entirety, like a novel. There is almost always a specific question they want to answer. It might be "how will it behave in this use case?", "how will this change affect its behaviour?" or "what change should I make it to achieve this new behaviour?". Alternatively, it might be something more high level, but still specific, like "how does this fit together?" (i.e. there's a desire to understand the overall organisational principles of the code, rather than a specific detail).
Opaque code typically:
* Requires you to read and understand large volumes of what should be irrelevant code in order to answer your question, often across multiple codebases.
* Requires you to do difficult detective work in order to identify what code needs to be read and understood to answer the question with confidence.
* Only provides an answer to your question with caveats/assumptions about human behaviour, such as "well unless someone has done X somewhere, but I doubt anyone would do that and would have to read the entire codebase to be sure".
Of course, this doesn't yield some number as to how "opaque" the code is, and importantly it depends on the question you're asking. A codebase might be quite transparent to some questions and opaque to others. It can be a very useful exercise to think about what questions people are likely to seek answers for from a given codebase.
When you think about things this way, you come to realise a lot of supposedly good practices actually exacerbate code opacity, often for the sake of "reusability" of things that will never be reused. Dependency injection containers are a bête noire of mine for this reason. There's nothing wrong with dependency injection itself (giving things their dependencies rather than having them create them), but DI containers tend to end up being dependency obfuscators, and the worst ones import a huge amount of quirky, often poorly-documented behaviour into your system. They are probably the single biggest cause of having to spend an entire afternoon trawling through code, often including that of the blasted container itself (and runtime config!), to answer what should be a very simple and quick question about a codebase.
"Clever" is a different thing to "complicated" or "opaque", and it's not always a negative. People can certainly make code much more opaque by doing "clever" things, but sometimes (maybe rather too rarely) they can do the opposite. A small, well thought out bit of "clever" code can often greatly reduce the opacity of a much larger amount of code that uses it. Thinking about what a particular "clever" idea will do to the opacity (as defined above) of the codebase can be a good way to figure out whether it is worth doing.