Skip to content

feat(web): rebuild the landing page on a ruled grid, with dark mode and an interactive how-it-works - #447

Open
Rish-it wants to merge 25 commits into
oblien:mainfrom
Rish-it:wip/revamp-landing
Open

feat(web): rebuild the landing page on a ruled grid, with dark mode and an interactive how-it-works#447
Rish-it wants to merge 25 commits into
oblien:mainfrom
Rish-it:wip/revamp-landing

Conversation

@Rish-it

@Rish-it Rish-it commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Rebuilds the marketing landing page around a ruled-grid visual system (navbar, hero, footer, section headers all rebuilt on shared grid primitives), adds a system-aware dark mode, and replaces the static "how it works" schematics with runnable terminal demos.

Motivation

Refreshes the landing page's visual identity and brings the marketing site to feature parity with the dashboard's dark-mode support.

Related issue

None

Changes

  • apps/web: add page-level ruled-grid primitives (rails, measure, grid node tokens) and rebuild the navbar, hero, footer, and every content section onto them
  • apps/web: add a shared SectionHeader primitive, reused across 8 sections; remove the now-unused dashboard.tsx mock
  • apps/web: add the astronaut dither brand plate to the hero and footer
  • apps/web: add a system-aware dark palette mirroring the existing --th-* token convention; dark icon variants via native <picture> + prefers-color-scheme, no JS state
  • apps/web: replace the static schematic bento in "How it works" with InteractiveTerminal — runnable terminal demos per build step (connect, build, ship, operate), respects prefers-reduced-motion

Verification

cd apps/web && bunx tsc --noEmit -p tsconfig.json

Typecheck passes clean at every stage of the branch. Manually exercised the homepage, docs layout, and download page at mobile/desktop widths, both themes.

Screenshots

Not captured in this pass — happy to add before merge if wanted.

Checklist

  • One change per PR — one bug, or one agreed feature, with nothing unrelated bundled in
  • The diff is scoped — no reformatting or lint fixes on lines I wasn't otherwise changing
  • A test fails without this change and passes with it (visual/layout change, no test harness fits this)
  • bun run test / lint / format (not run in this environment)
  • I understand every line of this diff and can explain it in review

Rish-it added 25 commits August 4, 2026 14:12
The landing page's new primary action is a saturated ultramarine that no
existing token covers - the closest, --th-clr-plum, is a violet. Added as
a token rather than a literal so the sections converted after the hero
reuse one value.
Replaces the centered column, aurora glow and rounded pills with a
drafting-table grid: two full-height rails at the page gutters, content
inset inside them, and horizontal rules running full bleed past the
rails. Rows are hatch rail, copy beside an isometric dither cluster,
then the stack ticker. Radius is 0 throughout and labels are mono.

Hatch runs diagonally - top-left and bottom-right - so the two cells
without copy never sit side by side. The reference fills the top-right
cell with headline stats; Openship has no such copy and none is
invented, so that cell is left empty.

--lp-gutter is the value the rails hang off. Sections converted after
this one have to reuse it or their rails will not line up.

Every string, href and the STACKS array are carried over unchanged,
including the &nbsp; and &#8209; in the sub - the prerendered HTML still
emits 2 U+00A0 and 2 U+2011.

The hero no longer uses the shared .hero-* kit. /features, /roadmap and
/download reuse those classes off landing.css - features.css says so in
its header - so they are left byte-identical and the new hero is
namespaced .lp-hero-*. .hero-ticker stays shared-in-name only; nothing
outside this file uses it.

The file header claimed a uniformly rounded design language. That is now
false for the hero, so it records both systems and which one applies
while the rest of the page is still unconverted.
--lp-gutter and --lp-rule started on .lp-hero, which put them out of
reach of the navbar - and the navbar has to align to the same rails or
the wordmark and the hero headline sit on different grids.

Adds --lp-inset (rail to first glyph) and --lp-nav-h. The navbar height
and the hero's top padding are the same value on purpose: the hero's top
rule is what draws the navbar's baseline, so two numbers would drift.
The navbar was a rounded pill floating over a hard-edged grid, and it
was laid out on a different grid to the hero: mx-auto max-w-7xl px-6 put
its content at 142px on a 1516px viewport while the hero headline
started at 108px. Both now derive from --lp-gutter + --lp-inset, so the
wordmark sits directly above the headline.

The centre nav becomes a hairline box with mono uppercase items, and
Download takes the electric accent. Every rounded-full and rounded-xl is
gone except the circular brand mark, which stays - the reference carries
no logo mark at all and dropping ours is not a styling decision.

This is shared chrome: 17 pages render <Navbar />, so all of them get
the squared version. Only the landing page has been converted so far,
so /pricing, /about, /trust, /terms, /privacy, /contact, /mail,
/changelog, /resources, /features, /roadmap, /download and 404 will show
a hard-edged navbar over soft content until their own conversions land.
Chosen over a per-page variant so the chrome does not change shape as
you navigate.

No copy, href or nav item changed.
Every joint where a horizontal rule crosses a vertical rail gets a
crosshair. Sizing it and colouring it from --lp-* rather than inline
keeps the joints identical across sections as the rest of the landing
page is converted, the same reason --lp-gutter and --lp-rule live here.

--lp-node-rule is deliberately darker than --lp-rule: the rules stay
hairlines and only the joint darkens, which is what makes the grid read
as drawn rather than as a table border.
The cube cluster carried no information - it was texture occupying half
the fold, and at 560px tall it pushed the headline into a column narrow
enough to need a 46ch measure. Removing it lets the headline run at its
natural size across the full frame.

Restructures the hero to the centred arrangement: headline, sub, the two
calls to action, then the install command as a full bar beneath them
rather than a chip above the headline. The command reads as the third
way in, next to the two buttons, instead of as a label.

Adds two pieces of grid detail:

- Crosshair nodes at every joint where a rule crosses a rail. Drawn as
  an overlay so the lines themselves stay hairlines. Offset by half a
  node from each side instead of translated, so they stay centred under
  RTL.
- A dot field in the two margins outside the rails, which the split
  layout used to fill with diagonal hatch. It needs its own element
  because the mask limiting it to those strips would otherwise clip the
  hero text along with it.

The two-column grid, the hatch cells and the rail row go with the
cluster, along with their breakpoint overrides - all four existed only
to hold the split layout together.

Copy is unchanged: all six hero strings, both non-breaking spaces and
both non-breaking hyphens verified in the prerendered HTML.
Byte-identical copy of the plate the login screen uses as its brand
panel. apps/web and apps/dashboard do not share a public directory, so
the file has to exist in both for the landing page to serve it.
Adds the dark plate as the hero's last row, matching the reference,
where a dark textured band closes the fold under the logo wall.

Drawn rail to rail rather than edge to edge, so it reads as a panel
inside the grid instead of an image the grid sits on. It is a CSS
background, not an <img> or next/image: the plate carries nothing the
copy does not already say, so it needs no alt text and no srcset, and
the artwork is already dithered in the file - the shader pass the login
screen runs is for its animation, not for the dithering.

Carries data-section="dark" so the navbar switches to its light
treatment over the band, the same as every other dark plate on the
site.
…ails

Three corrections from the side-by-side against the reference.

The primary button was blue for no reason. The ruled system does not
have an accent of its own, and inventing one put a colour on the page
that appears nowhere else on the site. It now uses --th-btn-*, the
black the rest of the site already ships. The ghost button picks up a
border rather than a grey fill, so the seam where the two buttons meet
reads as one drawn edge.

The two buttons and the command bar are now one stack of equal width,
as in the reference, instead of two content-width buttons floating above
a 520px bar. The width is a single custom property both measure.

The dot field moved from the margins outside the rails - where it was
decorating dead page edge - to the two bottom corners of the copy row,
inside the grid. It now steps its own phase on a 2.4s cycle, which
reads as dither noise rather than as a pattern sliding, and holds still
under prefers-reduced-motion.
Last two callers of the electric accent, so the token goes with them.
Openship has no blue in its palette - the value was introduced for this
layout and nothing else on the site pairs with it.
Everything was running large at a full-width viewport. Measured against
the reference at 1497px, ours was oversized at every level, not by one
bad value:

  navbar height        92px -> 76px   (reference 70px)
  headline             90px -> 64px   (reference ~68px, sentence case)
  control stack width  520px -> 400px (reference 401px)
  button height        45px -> 40px   (reference 38px)
  copy block padding   104px -> 72px

The headline caps below the reference's own size on purpose. Uppercase
at 700 carries more weight per em than the reference's sentence-case
setting, so matching font-size would have kept it dominating the fold at
the smaller number.

Also puts a 10px gap between the two buttons and opens the gap above the
command bar from 6px to 16px. Both were measured wrong off the 50%
screenshot, where the buttons read as flush and the bar as tight.

The stack ticker keeps its 32px icons - that size was asked for
directly, so it is not part of this pass.
Three classes carry the whole grid: .lp-ruled wraps the converted
region, .lp-band is one horizontal band, .lp-band-in is the content box
between the rails.

Rails are real borders on .lp-band-in rather than absolutely positioned
lines on a section. That is what lets them continue from one band to
the next with nothing to keep in sync, and it is how the reference does
it - an edge class applied per block, nineteen times, instead of once
at the page level. The hero's current rails only work because the hero
happens to be one tall section; they cannot extend past it.

.lp-ruled owns the first rule and the nodes on the last one, so no
section has to know whether it is first or last on the page.

Additive only - nothing uses these yet.
Drops .lp-hero, .lp-hero-frame and .lp-hero-row along with the section's
absolutely positioned rails - 64 lines of hero-local grid for three
generic classes.

The rails were pseudo-elements spanning the hero's full height, which
only worked because the hero is one tall section. As real borders on
.lp-band-in they continue into whatever section comes next.

.lp-hero-band is renamed .lp-hero-plate: the word band now means a row
of the grid, and the astronaut plate is the content of one, not a band
itself. It uses the --flush modifier so the image meets the rails
instead of sitting 30px off them.
Puts .lp-ruled on main so the grid owns its first rule and the nodes on
its last. Sections convert into it one at a time; the ones that have not
yet sit inside it unruled and unaffected.
The rails were pinned to a proportional gutter, so on a wide display the
grid ran nearly to the window edge and stopped reading as a drawn page.
Cap the column at --lp-measure and centre it: below that width the gutter
stays proportional, above it the rails hold at 1232px.

Also darkens --lp-rule from #d0d0d0, which was faint enough that the
rails had to be looked for, and adds the panel fills the sections need -
a raking paper gradient and one ink value for every dark plate, so those
cannot drift into three different blacks.

Drops .lp-ruled's top border. The navbar draws that line itself, and the
two stacked into a 2px double rule under the bar.
A sticky chapter marker for each section: the label, a rule to the rails,
and an [n/N] counter. Scoping it to its own section is what makes each
one ride down under the navbar and then hand over to the next, rather
than piling up.

Pinned state is React state rather than an attribute written straight to
the node. The observer only fires on a threshold crossing, so a re-render
in between would reconcile a directly-written attribute back to its JSX
value and the shadow would silently stop working.

The counter is passed in. Section order is a property of the page, so no
section should have to know its own position.
The links sat in a floating pill as small uppercase mono, on a bar with
no background and no bottom rule, which read as a different design
language from every section beneath it.

Three columns instead: side columns at 1fr each put the links on the
bar's true centre. A centred flex row lands them optically left, because
the wordmark is wider than the cluster opposite it. The Download control
splits its label and arrow into separate cells behind a hairline.

Removes the scroll listener that recoloured every control whenever a dark
plate passed under the bar. An opaque bar does not need to know what is
beneath it. This also affects /mail and the mail setup guide, which share
this navbar and previously inverted it over their dark heroes.
The hero was boxed in by rails, crosshairs and a rule across its middle.
The reference's never is: the ruled document starts at the first section
header, so the hero drops all of it and the plate below runs edge to edge
rather than stopping on the measure.

Replaces the two animated corner blobs with one dither field, held still.
A texture that flickers behind a headline reads as a rendering fault, not
as noise. The field is masked to crowd the margins and leave a clear lane
down the middle for the copy, and it sits outside .lp-band-in so it spans
the viewport rather than the measure.

Moves the stack ticker inside the hero and takes the grayscale filter off
it. It is the hero's proof line, and a grey logo wall proves nothing.
One drawn window for every screenshot on the page, so the dashboard and
the mail client stop being two near-identical frames. Rounded, because a
real window is: this is the single place the ruled system's hard corners
give way, and squared it read as a cropped image rather than an app.

Chrome is drawn rather than cropped from the image, which is what keeps
it a hairline at any width.
Editorial block, link columns, a mono copy row, and the wordmark set
huge underneath. It ignores the rails on purpose: everything above it is
held inside the measure, and the footer running edge to edge is what
tells you the document has ended rather than merely paused.

The wordmark is Geist Pixel on its circle shape. ELSH is the family's
shape axis and 25 is the circle stop, so it has to load the variable cut
- the static one is square and the axis is the whole point. Sized at
22vw: the eight glyphs measure ~4.29em, so 23.3vw would fill exactly and
this leaves a margin instead of touching the edges.
Reorders the page to follow the argument rather than the org chart: what
the platform is, how it is put together, how you use it, how it compares,
what comes included, where it runs, who owns it. Each section gains a
chapter header and its [n/7] counter, numbered by the page.

Rebuilds each one on the layout its content actually wants. The platform
becomes a catalog - a spine of seven groups with one open at a time,
because the old six-by-six grid had to show forty-two things at once and
gave none of them room. Comparison becomes a scoreboard with the Openship
column lifted out as one connected card. Mail becomes a rail of cards
behind a full-bleed shot. How it works becomes a bento of four schematics
closing on a drenched payoff plate.

The product shot moves from under the hero into the closing plate, where
it runs off the bottom edge. That is why dashboard.tsx goes: it existed
only to place a screenshot the final CTA now carries.

No copy changed anywhere in this commit.
Replace the static schematic bento with runnable terminal demos for
each build step, backed by a shared module-scoped stylesheet.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Rebuilds the marketing homepage around a ruled-grid layout system, adds system-aware dark mode tokens, and replaces the “How it works” section with interactive terminal demos, while updating multiple landing sections to use shared grid primitives and a new sticky SectionHeader.

Changes:

  • Add light/dark theme token palettes and landing-page ruled-grid metrics (--lp-*) and rebuild landing CSS around those primitives.
  • Introduce SectionHeader and convert multiple landing sections (platform catalog, system map, comparison, mail server, deployment models, open source) to the new ruled layout.
  • Add interactive “How it works” terminal demos (InteractiveTerminal) plus supporting CSS module; remove the old dashboard mock section.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/web/src/styles/theme.css Adds system-aware dark palette and new landing ruled-grid tokens (--lp-*).
apps/web/src/styles/landing.css Major landing restyle: ruled-grid primitives, new navbar/hero/footer, updated section layouts.
apps/web/src/styles/fonts.css Adds Geist Pixel font-face for the new footer wordmark treatment.
apps/web/src/components/landing/system-map/index.tsx Converts SystemMap to ruled sections + SectionHeader; updates API to accept index/total.
apps/web/src/components/landing/section-header.tsx New sticky ruled-grid section header primitive with [n/N] counter.
apps/web/src/components/landing/open-source.tsx Converts OpenSource section to ruled grid + SectionHeader; updates API to accept index/total.
apps/web/src/components/landing/navbar.tsx Replaces scroll-aware pill navbar with fixed ruled-grid bar + mobile sheet.
apps/web/src/components/landing/mail-server.tsx Rebuilds mail section into ruled-grid headline + shared window frame + horizontal card rail; updates API to accept index/total.
apps/web/src/components/landing/interactive-terminal.tsx New interactive terminal component for runnable “How it works” demos.
apps/web/src/components/landing/index.ts Exports SectionHeader and removes Dashboard export.
apps/web/src/components/landing/how-it-works.tsx Replaces static step list with a grid of InteractiveTerminal demos + new headline layout; adds SectionHeader.
apps/web/src/components/landing/how-it-works.module.css New CSS module for the interactive terminal grid and terminal UI styling.
apps/web/src/components/landing/hero.tsx Rebuilds hero onto ruled-grid primitives, adds dark-mode icon variants via <picture>, and adds new install/CTA layout.
apps/web/src/components/landing/footer.tsx Rebuilds footer as a full-bleed ink plate with socials and a giant Geist Pixel wordmark.
apps/web/src/components/landing/final-cta.tsx Converts final CTA to ruled grid, adds shared window-frame product shot, removes DarkSection.
apps/web/src/components/landing/deployment-models.tsx Converts deployment models into ruled-grid “cells” + SectionHeader; updates API to accept index/total.
apps/web/src/components/landing/dashboard.tsx Removes the old dashboard mock showcase section entirely.
apps/web/src/components/landing/complete-platform.tsx Replaces the 6×6 grid with an interactive catalog (spine + plinth + timed rotation) and adds SectionHeader.
apps/web/src/components/landing/comparison.tsx Converts comparison table to ruled-grid board layout + SectionHeader; refactors grouped rows rendering.
apps/web/src/app/(site)/page.tsx Reorders/renumbers homepage sections, wraps content in .lp-ruled, and passes index/total into section components.
apps/web/src/app/(site)/layout.tsx Updates viewport theme colors and enables colorScheme: "light dark".
apps/web/src/app/(site)/download/download.css Switches download hero grid colors to use theme tokens for dark-mode compatibility.
apps/web/src/app/(docs)/docs/layout.tsx Enables system-aware theming in docs (defaultTheme: "system", enableSystem: true).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/web/src/components/landing/complete-platform.tsx
Comment thread apps/web/src/components/landing/mail-server.tsx
Comment thread apps/web/src/components/landing/navbar.tsx
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.

2 participants