Skip to content

Make the cockpit the homepage: rocket entrance, flight, and galaxy arrival - #129

Open
HodlDee wants to merge 10 commits into
mainfrom
codex/rocket-homepage-journey
Open

HodlDee wants to merge 10 commits into
mainfrom
codex/rocket-homepage-journey

Conversation

@HodlDee

@HodlDee HodlDee commented Sep 16, 2026

Copy link
Copy Markdown
Owner

The homepage becomes the cockpit: a scene you arrive in, choose a destination from, and fly out of. The rocket launch that introduces it plays once per tab. The Quickstart flight lands on the real guide.

Depends on #128. That branch is merged in here so the three shared sources are reconciled now rather than at merge time. Once 128 lands on main, this diff loses those commits and shows only the journey. Merge 128 first.

Routes

Route What it is
index.html the cockpit — generated from build/content.mjs like every other root page
launch.html the rocket entrance, registered in the renderer's FILES map
cockpit-scene.html the scene the cockpit frames
guides/quickstart.html the arrival — the canonical guide, carrying the galaxy treatment

Every shipped file carries a production name. Nothing called review, preview or mockup remains anywhere in docs/, and the superseded first chain is deleted rather than left beside its replacement.

Entrance

assets/js/home-entrance.js decides which front door a visit lands on. It is the one script in the head that is neither deferred nor a module, because a redirect issued after the cockpit has painted is a flash of the wrong page.

sessionStorage, so "once" means once per tab — a new window earns the launch, a reload does not. location.replace, so Back from the cockpit leaves the site instead of bouncing through a launch already watched.

Home and the brand mark name the cockpit explicitly rather than relying on that flag. Without it the gate had only "have you seen the launch" to reason from, which is right for someone who came through the front door and wrong for everyone else: a reader who followed a search result to a guide has never set it, so their first click on Home would have played the launch — the one thing that link exists to skip.

The arrival is the real guide

docs/guides is deleted and rebuilt from build/guides.mjs on every build, so a preview page could never have survived there. It did not need to: the galaxy page's article was byte-identical to guides/quickstart.html — 34,491 characters, the same prose. The frame loads the canonical guide and the galaxy treatment is generated onto it.

A direct visit runs the same entrance as the flight, driven by the same --arrival-title over the same 1150ms, so the two cannot drift. What it does not borrow is the part of that stylesheet written for living inside the frame, where the page is punched transparent and the clouds suppressed because the video supplies the backdrop. Opened on its own, this page is the backdrop.

Preferences

Reduced motion and the cutscenes switch are separate. Cutscenes Off turns off travel; the guide's title and text entrance is part of the guide and survives it. Reduced motion suppresses the entrance and leaves the finished state.

Neither now costs a download. The 15MB flight video was fetched the moment the cockpit loaded, before anyone had asked to fly — so a reader who had already said they did not want to see it paid for it anyway. Nothing is fetched until travel is both permitted and wanted.

Two things had to move for that to be safe: cockpitReady() waited on a frame traced from the video's first frame, which would have left the cockpit permanently mid-arrival; and starting the flight fell through to a twelve-second wait for a video that is no longer coming.

Navigation

Guides is a split control — the label links to the hub, only the arrow opens the list. Compare has no page of its own so its label can be the opener; Guides does, and making the label open a menu put the hub two clicks from every page on the site.

The dropdown fix underneath: :hover and :focus-within used to open a submenu on their own, alongside .is-open from script. Two owners for one piece of state is what made Guides and Compare overlap. Only .is-open opens a menu now, set from pointer, focus and keyboard alike.

Dependencies

Traced transitively from the four production routes: 244 files, all resolving. Three kinds would not have been found by searching for filenames — a module reached through a dynamic import, four stylesheets injected into the arrival frame at runtime, and two orbit-menu images assembled from a bare filename and a base path. Shipping on names alone would have sent the arrival out unstyled.

Two assets were deliberately excluded: a 9.7MB video reachable only as a default the page always overrides, and the uncompressed 4K source, superseded by the approved quickstart-runway-moon-v14-4k-compressed-v1.mp4. Video ships at 34.1MiB total, with both 1080p fallbacks retained.

ASSET_VERSION now covers the journey's own assets. It digested four files, none of them these, so an edit to the cockpit or the arrival would have reached nobody already holding the old copy — which happened during development, with a corrected stylesheet on disk while the browser used its cached one.

Verification

npm run build and all guards pass, a second build introduces no changes, and the committed tree is what the build produces. Site gate run step for step. The Entropy Workshop job scopes in on the asset-version change and passes; the offline artifact and its digest are untouched.

Routing, with the session cleared between each: fresh arrival at the root gets the launch; the same arrival with cutscenes off goes straight to the cockpit and fetches no video; a reader who lands on a guide first and then clicks Home reaches the cockpit without a launch.

Reviewed in a rendering browser: rocket to cockpit, flight to guide, corrected Home navigation, mobile 1080p selection, 4K decode-failure fallback, the cloud handoff with no black gap, the staggered text entrance over stationary clouds and banner, and zero MP4 requests under both opt-outs.

Exchanges was measured against the figures recorded when its hero was built, to prove the shared-stylesheet reconciliation did no damage: identical at 1440 and at 375.

Not in scope

No Entropy Workshop or crypto behaviour. contact.html is untouched — it is a deliberate holding page, and the Get Help content proposed for it is a separate task. Preview and archive cleanup is its own PR, to follow this one.

🤖 Generated with Claude Code

HodlDee and others added 10 commits September 16, 2026 02:22
Three changes to the site-wide navigation, ahead of the cockpit becoming
the homepage.

Guides is now a split control. The label is a link to the hub and only
the arrow opens the list. Compare has no page of its own, so its label
can be the opener; Guides does, and making the label open a menu put the
hub two clicks from every page on the site. The arrow is a real button
with its own accessible name, so the two targets are distinguishable
rather than one control that behaves differently depending on where it
is clicked.

The dropdown's first row was "Start here", pointing at the top of the
hub -- which is where the label itself now goes, so the row duplicated
its own parent. It is Quickstart instead, going straight to the guide
that starts someone off. Glossary moves out of the top level to the foot
of that list, and the Entropy Workshop moves the other way, out of the
list and onto the bar as "Workshop", because it is a tool people go to
directly rather than something to browse for.

And the fix underneath: :hover and :focus-within used to open a submenu
on their own, alongside .is-open from script. Two sources of truth for
one piece of state is what made Guides and Compare overlap -- clicking
Guides set .is-open, hovering Compare opened that one too, and the
clicked menu then stayed open with the pointer nowhere near it. Only
.is-open opens a menu now, and site-refresh.js sets it from pointer,
focus and keyboard alike, so whatever opens one closes the others on the
way. Hover carries a 160ms close delay, because without it crossing the
gap between label and panel closes the menu you are reaching for; touch
pointers are excluded, since a tap fires pointerenter and the click
would immediately toggle it shut again. A menu also closes when the
window loses the pointer and when the layout crosses the desktop
breakpoint, which were the other two ways to get one stuck.

Measured at 1440: click Guides, hover Compare, and only Compare is open;
Escape and an outside click clear both; ArrowDown from the arrow opens
the list and moves focus to Quickstart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The homepage becomes the cockpit: a scene you arrive in, pick a
destination from, and fly out of. The rocket launch that introduces it
is worth watching once, not on every return, so it is its own route and
a session flag decides which of the two a visit lands on.

index.html is generated from build/content.mjs like every other root
page -- the cockpit container, the arrival frame and the flight video --
and launch.html is registered in the renderer's FILES map so it carries
the same generated banner as the rest of the site. It had been a
hand-copied shell with a stale nav inside it.

assets/js/home-entrance.js is the gate. It is the one script in the head
that is neither deferred nor a module, because a redirect issued after
the cockpit has painted is a flash of the wrong page. sessionStorage
rather than localStorage, so "once" means once per tab: a new window
earns the launch again, a reload does not. location.replace rather than
assign, so Back from the cockpit leaves the site instead of bouncing
through a launch already watched. Reduced motion and a cutscenes-off
preference are both checked here, so neither ever loads the launch at
all -- no video fetched, nothing to skip. The flag is set either way,
so turning cutscenes back on mid-visit does not drop someone into a
launch from an ordinary click on Home.

Every shipped file now carries a production name. Nothing named review,
preview or mockup remains anywhere in docs/, and the superseded first
chain -- the older loader, transition script and three entry pages --
is gone rather than left to rot beside its replacement.

The arrival is the real guide. docs/guides is deleted and rebuilt from
build/guides.mjs on every build, so a preview page could never have
survived there; and it did not need to. The galaxy page's article was
byte-identical to guides/quickstart.html -- 34,491 characters, the same
prose -- so the arrival frame now loads the canonical guide and the
journey injects the galaxy skin into it, as it already did for the
arrival stylesheet. One copy of the writing, and direct access to the
guide keeps working without the flight.

Three dependencies came across that no filename search would have
found: a stylesheet injected into the arrival frame at runtime, a
module reached only through a dynamic import, and two orbit-menu images
assembled from a bare filename and a base path. The first pass missed
the injected ones because their paths carry a ?v= query inside the
string literal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two gaps from the integration.

The Quickstart guide now carries the galaxy arrival itself -- the
nebula behind the title, the staggered reveal of the heading and
opening lines, and a control to watch it again. It is generated onto
that one guide by build/render.mjs rather than injected by the journey,
for two reasons. The reveal has to survive Cutscenes being switched
off, because that switch turns off travel and not the arrival; and the
guide has to look the same reached cold from a search result as it does
at the end of a flight. The journey still injects its own arrival
stylesheet over the top for the blend out of the video, which genuinely
does belong to the transition rather than to the page. The paragraph
the cloud fade aligns to gets its anchor in build/guides.mjs, where the
prose lives.

And the banner's Home link now names the cockpit outright. The gate on
index.html had only the launch-seen flag to reason from, which is fine
for someone who came in through the front door and wrong for everyone
else: a reader who followed a search result to a guide has never set
that flag, so their first click on Home would have played the launch --
precisely the thing that link is supposed to skip. Home and the brand
mark now carry an explicit destination, the gate honours it before it
consults anything else, and the fragment is cleared from the address
bar afterwards so the cockpit keeps one canonical URL.

Verified by routing, with the session cleared between each: a fresh
arrival at the root still gets the launch; the same arrival with
cutscenes off goes straight to the cockpit and fetches no video; and a
reader who lands on a guide first and then clicks Home reaches the
cockpit without a launch.

Every dependency of the four production routes resolves, including the
three kinds that no filename search finds -- injected at runtime,
reached by dynamic import, or assembled from a bare filename.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three faults found in the rendered build.

The text entrance never played on a direct visit. The reveal only had
two triggers -- the Replay button and the journey -- so a reader who
reached the Quickstart guide from a search result or the Guides menu
got the finished state with no entrance at all. The page starts it
itself now, and skips doing so inside the arrival frame, where the
parent owns the timing. Deliberately not gated on the cutscenes switch:
that switch turns off travel, and the title and text entrance belongs
to the guide rather than to the journey. Reduced motion is the one
thing that suppresses it.

The cutscenes control was missing from the standalone guide, so the
preference could only be changed from a page with a flight on it. It
ships with the guide now, reading the same stored value, and hides
itself inside the arrival frame so there is never a second copy sitting
on top of the first.

And the flight video -- 15MB -- was fetched the moment the cockpit
loaded, before anyone had asked to fly and whether or not the journey
was ever going to play it. Someone with reduced motion set, or
cutscenes switched off, paid for a download of the thing they had
already said they did not want. Nothing is fetched now until travel is
both permitted and wanted; flip the switch back on and the fetch
happens then.

Two things had to move for that to be safe. cockpitReady() waited on a
frame traced from the video's first frame, so deferring the fetch would
have left the cockpit permanently mid-arrival -- it now treats the
cockpit as ready when there is no departure to register. And starting
the flight fell through to a twelve-second wait for a video that is no
longer coming, so choosing to skip meant staring at "Preparing the
sequence"; that early-out covers the switch as well as reduced motion.

The preference is read from storage rather than through the helper
window.cutscenesEnabled. Both scripts are deferred and the journey is
earlier in the document, so the helper does not exist yet when it first
runs -- and `undefined !== false` reads as "allowed", which is how a
switched-off visit still fetched the video on the first attempt at
this.

Measured on the running build: cutscenes off, zero .mp4 requests and no
source assigned, cockpit still arrives; toggled on mid-visit, the fetch
happens and the video attaches; direct guide visit, the reveal runs
from ~180ms to ~1430ms and the control is present; inside the arrival
frame, one toggle rather than two and no second reveal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A direct visit was running the old replay effect: a black shade wiped
off the screen and the whole guide head moved as one block. The
approved entrance does neither. It fades the head's rows up in
sequence, over clouds and a banner that do not move at all.

The difference was that the two effects are driven by different things.
The replay is a pair of keyframe animations behind an opaque shade. The
arrival is --arrival-title, read by quickstart-galaxy-arrival.css, which
offsets each row against the last through --row-start. A direct visit
only ever had the first of those stylesheets, so it could only run the
first effect.

It loads both now and drives the same property over the same 1150ms the
journey uses, so the two entrances cannot drift apart -- there is one
set of row offsets and one duration, not a copy of each. The replay
button runs it too, rather than the wipe it used to.

What a direct visit does not take from that stylesheet is the part
written for living inside the journey's iframe. There the page is
punched transparent and the cloud artwork is suppressed, because the
frame behind it already carries clouds and the video supplies the
backdrop. Opened on its own this page IS the backdrop, so the charcoal
and the clouds come back, and the replay control with them.
--arrival-content is pinned at 1 from the start for the same reason: in
the flight the banner and body are arriving out of a video, here they
are already on screen and stay put. Only the head rows move.

Two things found while fixing it.

The rows start at zero opacity, so a reveal that never runs leaves the
heading and its opening lines invisible -- and it genuinely does not
run in a background tab or anywhere the page is not being composited.
A timer now acts as a floor, slightly longer than the reveal, so the
text ends up on screen whether or not it got to animate on the way.

And none of the journey's assets were covered by the version stamp.
ASSET_VERSION digests four files, none of them these, and the links
added to index.html carried no ?v= at all -- so an edit to the cockpit
or the arrival would reach nobody already holding the old copy. It is
not hypothetical: a corrected stylesheet sat on disk for several
attempts here while the browser went on using its cached one, and the
fix read as broken when it was not. Both the digest list and the stamp
regex now cover them, kept in step as that file asks.

Measured on the running build. Direct visit: shade display:none, clouds
visible, banner opacity 1, background #080a0d, replay shown, six rows
carrying the flight's own offsets. Inside the arrival frame: no
galaxy-direct class, both properties still at 0 awaiting the journey,
replay and toggle suppressed, rows holding at 0 -- the flight path
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guide ships the galaxy arrival stylesheet in its own head, with the
build's version on it. The journey then appended a second copy under a
hand-written ?v=9 -- the same rules fetched twice, and one of the two
pinned to a version that stops moving when the file changes. Edit that
stylesheet and the frame could go on being styled by a stale copy the
rest of the site had already replaced.

It reuses whatever the document already carries now, and only injects
when there is nothing to reuse -- the non-galaxy arrival, which has no
generated link of its own.

The existing link stays where it is rather than being moved last. The
order is deliberate: the hero sheet has to follow the arrival sheet
because the two collide at equal specificity over the cloud artwork,
and on a direct visit the hero's rule is the one that has to win.
Appending here would have put the arrival last and hidden the clouds
again.

One link in the frame carrying the generated version, and the direct
visit unchanged: clouds visible, no shade, banner steady.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR 128 is verified but not yet on main, and the two branches share
build/content.mjs, site-refresh.css and site-refresh.js. Merging it
here reconciles them now rather than leaving it to be discovered at
merge time.

All three sources merged without conflict. The only conflicts were
generated pages, and only over the asset-version string each branch had
stamped into them -- resolved by regenerating from the merged sources,
which is the only resolution that can be right for a file nobody edits
by hand.

What git could not see is that the merge leaves the homepage half of
every vortex rule pointing at a hero that is no longer there. That is
the next commit.

Once 128 lands on main, this branch's diff loses these commits and
shows only the journey.
The vortex rules were written as two-page groups when the hero moved to
Exchanges, on the understanding that this branch would drop the homepage
half once the cockpit replaced that hero. The cockpit has replaced it:
index.html renders no .sc-hero at all, so every selector naming the home
page beside a hero selector now describes an element that is not there.

37 dead selectors gone, and 17 rules that had nothing left once their
home selector was removed. All 37 of the Exchanges vortex rules stay.

The risk in this is doing damage to the hero that is still in use, so
Exchanges was measured against the figures recorded when that hero was
built. Hero box, minimum height, padding, background, figure box and
transform, video box and fit, copy box and offset, headline size, action
row, the 100px overlap and the media control's position are identical at
1440. At 375 the actions are still stacked at 50px with 30px gaps and no
overflow. Nothing that is still rendered changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Everything below 900px shared one composition, built for a phone and
positioned off --cabin-panel: the intro at .1963 of it, the menu at
.4444, the destination 35vw below it. At max(600px,120vw) that panel is
921px on a 768 tablet and 1012px on a phone held sideways, so both got
the phone arrangement scaled by width -- which is why the menu started
at y=467 on the tablet and the destination sat at y=1190, and why
landscape ran to nearly five screens with its menu below the fold.

Tablet portrait binds the panel to the shorter of a cap and the viewport
instead. The artwork keeps its internal relationships and only the
framing tightens: one value moves and the composition follows, rather
than a second set of positions to keep in step with the first.

Short landscape needs an arrangement, not a smaller one. The copy and
menu take the left half, the destination the right, and the two art
panels split one screen between them -- the lower panel's fixed ratio
alone resolved to ~860px, which is what kept the stage three screens
tall even once the content fitted in the first. Keyed on height, because
that is the condition that matters.

Phone portrait keeps its composition; the destination's visible bottom
was landing nine pixels past the fold, close enough to read as cut off
rather than as something to scroll for.

Measured, before and after, at each size:

  tablet portrait   menu 467 -> 304, destination bottom 1357 -> 943
                    inside the first screen, stage 1674 -> 1357
  phone landscape   menu 514 -> 153 and wholly on screen, destination
                    1170 -> 70-283, scroll 4.7 screens -> exactly one
  phone portrait    destination bottom 853 -> 794, inside the fold
  desktop 1440      cabin, intro, menu, destination and row heights
                    unchanged; every new block is max-width:900 or
                    narrower and cannot reach it

The hamburger was a 28px icon and therefore a 28px target. Padding grows
the box to 44 and an equal negative margin returns that growth to the
layout, so the glyph does not move -- including in the open state, where
`top` positions the margin edge and the two cancel. Tablet landscape
gets the desktop bar with its 40-42px rows and no hover to soften them,
so those lift to 44 under (pointer: coarse) -- the actual condition,
rather than a width that a mouse might also match.

Floating controls move from max(16px, env(...)) to env(...) + 16px. On a
device that reports an inset, max() resolves to the inset itself and
puts the control flat against the boundary it was meant to clear.

And the scene's eight stylesheets join the version digest. They carried
hand-written versions -- v=17, v=sun-finisher-2 -- frozen at whatever
each was when last edited by hand, so a change to any of them reached
nobody already holding the old copy. cockpit-scene.html has no shell and
so is not in FILES, which is why nothing was stamping it; it gets a pass
of its own rather than a shell it does not want.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant