In addition to some of the answers already given, I can think of two reasons why a code-base deteriorates over time.
The first is that best practices, even if unanimously agreed upon, don't always survive contact with the user. Users will use software in ways that you didn't intend, and their usage patterns may expose bugs or have deleterious performance impacts on your code.
For example, if you're using a functional core, imperative shell design, you get all of the stability and ease-of-reasoning benefits from immutability within your functional core, but users may need to update part of your data model very frequently that you expected would seldom need to be changed, and the way the code is designed, changing this part of the model triggers a very expensive rebuild of the world. At that point, you're either forced to completely re-architect or come up with a clever hack for this one specific use case.
And re-architecting isn't always a guarantee of success. I once worked on a data warehouse system where the strong ACID properties of the database, along with the way the data was segmented ended up causing server with the specific hardware we were using. It's been decades, so I don't remember the exact issue, but something about issuing specific sequences of reads, seeks, and writes over and over caused and issue with the disk buffer, and when the OS periodically went to read data it needed, it ended up with our app's data instead. It was something that could be solved with a different server, but at the time, there was no budget for it, and a migration to new hardware wouldn't be possible until after the holiday retail season, so we ended up having to store some customers' data in files on disk rather than in a proper database. Then we had to change the schema in response to new requirements, so by the time we migrated to new hardware, reconciling the divergent schemas between the database and the database and the files was a nightmare, and it might never have gotten done without some serious politicking, which still took a couple years.
The second reason is that development environments change over time, and invalidate a lot of the assumptions apps are developed on. It's not just frameworks and libraries, but also languages.
I once worked on a Python 2 web service that did heavy text processing and had to make extensive use of Unicode. Just due to the history of how Unicode and Python developed in parallel, Python 2 had some eccentricities when it came to Unicode support. We understood all of these well and were able to develop a well-tested codebase that abstracted away these issues. Python 3 completely changed how the language handled Unicode, and the result on our project was disastrous. We essentially had to rewrite everything to make it work, but it was so messy we threw it all out and completely redesigned it. I can imagine a lot of companies wouldn't want to make that investment, especially since Python 3's Unicode support, while better, is still quite clunky compared to other languages. It's a hard sell to tell your boss you built something on top of broken Unicode support, and now you want to rebuild it on top of a different broken Unicode implementation. They'll just ask if you'll need to do that again in ten years.