Why does upper coroutine wait a non-related coroutine?

I’ve just learned that upper coroutine waits its child coroutine because there is a parent-child relationship(Structured concurrency). please check this code below.

fun main(): Unit = runBlocking {
    launch(Job()) { // the new job replaces one from parent
        delay(1000)
        println("Will not be printed")
    }
}

The result on the example is nothing. Nothing printed. because the new job replaces one from parent. So, runBlocking doesn’t know the new Job. it means there is no reference(relationship) of the launch’s job. That’s why runBlocking doesn’t wait for the completion of the launch.
But if i change the code like below, the result will be different.

fun main(): Unit = runBlocking {
    val childJob = launch(Job()) { // the new job replaces one from parent
        delay(1000)
        println("Will not be printed")
    }
    childJob.join()
}

the “Will not be printed” printed. But how and why? there is still no relationship between runBlocking and launch(Job()). is there any magic of the join()?

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html

Suspends the coroutine until this job is complete.

I know join() suspends a coroutine until the job is complete. but i thought runBlocking wouldn’t wait for launch(Job()) because there is no parent-child relationship but it’s still structured concurrency.
Finally, I misunderstood. i thought join() can only suspend the coroutine which has parent-child relationship. but join() means it suspends its coroutine itself until the coroutine’'s job is complete even though there is no parent-child relationship there.

Thank you.