Ok, finally got a workaround for this issue. It’s not one I came across before in all my searches; rather I got it as a tip from Protopop Games via social media. (Edit: The solution is also mentioned in [this post]( MapMagic 2 - infinite procedural land generator page-48#post-9059029), which is however not easy to find when searching for issues with broken grass.) I’m recording it here now in case it may help others who searches for it in the future.
Instead of creating the TerrainData objects via the constructor, instantiate new TerrainData objects from a dummy TerrainData object on disk.
//TerrainData data = new TerrainData(); // Doesn't work anymore for grass detail.
TerrainData data = Object.Instantiate(TerrainResources.instance.terrainData);
In the code above, TerrainResources is my own ScriptableObject class, it’s just a way to get a reference to a TerrainData asset.
My dummy TerrainData asset does include a detail prototype with the same texture as I’m setting at runtime. I haven’t tested if that’s actually required or not.
I don’t have any clue why the grass detail renders wrong in builds when created via a TerrainData object created from scratch, when it works fine in the Editor and, in previous versions of Unity, also fine in builds. But here we are - needing arcane knowledge is the reality of developing with Unity these days.