Why variables in shell are not inherited by sub-shell now that the sub-shell is implemented by fork?

The first error here is in thinking that the shell just runs fork() to execute an external command and that the description of fork() is thus a description of the operation of the shell. In reality both the parent and the child process do a whole lot of additional stuff. And in particular the child calls execve() to run the external command.

At this point, from reading the manual page for execve() a light should be dawning. One of the parameters to the system call is the environment that the overlaid process image will have at program startup. Obviously, there's no magic under-the-covers environment passing by the operating system. The shell has to explicitly concoct an environment and pass it to execvce() in the child process. That environment can be whatever the shell likes.

Which brings us to the second error. Environment variables are not shell variables. When the shell program starts up, it imports environment variables into shell variables of the same names (and in one shell, into shell functions). When it comes to spawning an external command, it then exports the shell variables into the (new) environment that it creates and passes to the kernel in execve(). In between whiles, all that you do in scripts and interactively with shell variables affects those variables, which are distinct from the shell process' environment. (The shell process' environment generally remains largely untouched and unused after import at program startup.)

Which — Hey presto! — explains why the export shell built-in is called "export".

And which also explains how built-ins such as set see unexported variables. They don't involve overlaying the shell program with another program, and don't involve setting up a new environment to pass to execve(). Similarly, creating subshells with parentheses doesn't — by itself — involve execve() and so doesn't involve exporting shell variables to create an environment.