Skip to content

Latest commit

 

History

History
686 lines (584 loc) · 34.5 KB

File metadata and controls

686 lines (584 loc) · 34.5 KB
version alpha
name Droidspaces
description The visual language of droidspaces.org. Material 3 Expressive, derived from the Droidspaces Android app, for a static site on GitHub Pages.
colors
primary on-primary primary-container on-primary-container secondary secondary-container on-secondary-container tertiary tertiary-container on-tertiary-container error surface surface-container-lowest surface-container-low surface-container surface-container-high surface-container-highest on-surface on-surface-variant outline outline-variant primary-fixed primary-fixed-dim
#a2cde2
#174557
#2d586a
#bfe9ff
#b4cad6
#2a3e48
#adc3ce
#d1dcff
#becefa
#354569
#fa746f
#0b0f11
#000000
#0f1417
#141a1e
#1a2124
#1f272b
#dee7ec
#a4acb2
#6e777c
#41494e
#b5e0f6
#a7d2e8
typography
display-lg-emphasized display-md-emphasized headline-lg-emphasized headline-md title-lg title-md-emphasized body-lg body-md label-lg-emphasized label-md code
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
57px
700
64px
0px
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
45px
700
52px
0px
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
32px
700
40px
0px
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
28px
400
36px
0px
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
22px
400
28px
0px
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
16px
600
24px
0.15px
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
16px
400
24px
0.5px
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
14px
400
20px
0.25px
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
14px
600
20px
0.1px
fontFamily fontSize fontWeight lineHeight letterSpacing
IBM Plex Sans
12px
500
16px
0.5px
fontFamily fontSize fontWeight lineHeight letterSpacing
JetBrains Mono
14px
400
22px
0px
rounded
xs sm md lg lg-increased xl xl-increased xxl full
4px
8px
12px
16px
20px
28px
32px
48px
9999px
spacing
50 100 150 200 300 400 600 800 1200
4px
8px
12px
16px
24px
32px
48px
64px
96px
components
button-filled button-tonal button-outlined card card-nested chip code-block phone-frame
backgroundColor textColor typography rounded height padding
{colors.primary}
{colors.on-primary}
{typography.label-lg-emphasized}
{rounded.xl}
56px
0 24px
backgroundColor textColor typography rounded height padding
{colors.secondary-container}
{colors.on-secondary-container}
{typography.label-lg-emphasized}
{rounded.xl}
56px
0 24px
backgroundColor textColor typography rounded height padding
transparent
{colors.on-surface-variant}
{typography.label-lg-emphasized}
{rounded.xl}
56px
0 24px
backgroundColor textColor rounded padding
{colors.surface-container}
{colors.on-surface}
{rounded.xl}
24px
backgroundColor textColor rounded padding
{colors.surface-container-high}
{colors.on-surface}
{rounded.lg}
16px
backgroundColor textColor typography rounded height padding
{colors.surface-container-high}
{colors.on-surface}
{typography.label-lg-emphasized}
{rounded.full}
40px
0 20px
backgroundColor textColor typography rounded padding
{colors.surface-container-lowest}
{colors.on-surface}
{typography.code}
{rounded.lg}
20px
backgroundColor rounded padding
#0a0f12
{rounded.xxl}
1.75%

DESIGN.md

The visual language of droidspaces.org. It is derived from the Droidspaces Android app, whose own DESIGN.md was read out of Android/app/src/main/java/com/droidspaces/app/. The website is the app's design language taken outdoors: same palette family, same flat surfaces, same restraint, plus the shapes and springs of Material 3 Expressive that a marketing page has room for and a container manager does not.

Every number in this file has a source. Colour roles were generated from the app's dark primary (#7DC4E4, Catppuccin Sky, ui/theme/Color.kt) with Google's material-color-utilities 0.4.0 on the 2025 colour spec. Radii, spacing, type sizes and the button press morph come from the token files Google ships in @material/web 2.5.0 (labs/gb/styles/). Spring values come from the Compose Material 3 ExpressiveMotionTokens.kt. Nothing here was picked by eye.

The front matter is the normative part. The prose says when each value applies.

Overview

Droidspaces runs full Linux distributions on Android with a real init system, directly on the phone's kernel. No emulation, no virtual machine, no Termux. The site exists to make that claim in ten seconds to two audiences at once: a self-hoster with an old phone in a drawer, and a kernel developer who will read the namespace list before believing anything.

The home page opens on the reason the project exists, not on the technology: an old phone already has a battery, mobile data and a Linux kernel, so it can be a homelab that keeps running when the power cuts. The kernel developer's claim (systemd as PID 1, no emulation) is the line under it.

The second beat is the philosophy, and it is the project's reason to exist: the phone boots, Droidspaces starts the containers while the phone is still locked and encrypted, and the init system inside starts every service. Gitea, Jellyfin, a website: set up once, then forgotten, the way a Linux server is. Every later section supports that claim.

Numbers on the page (release, stars, contributors) are stamped by the build from the GitHub API, never typed.

The site has to feel like the Android app. Someone who installs the app after reading the site should recognise it. That decides most of what follows: the same slate-blue palette, surfaces that step up in tone rather than float on shadows, borders that carry state, and a companion app that is flat by decision (55 sites set tonalElevation = 0.dp, zero card elevations in the tree).

Where the site differs from the app is where Material 3 Expressive gives it permission to. A marketing page can afford one large shape, one spring, one moment of motion per section. The app cannot, because it is a control surface. So the site is the app with the volume up one notch, not a different product.

Personality, in three words: native, calm, precise. Not playful. Not enterprise. The tone of someone who wrote the container runtime and is showing you it works.

The site is dark by default, because the app is, because terminals are, and because the phone screenshots that carry every page were captured in dark mode. Light mode exists and is complete, and it follows prefers-color-scheme unless the visitor overrides it with the toggle.

Colors

Roles come from Material 3. The site uses the --md-sys-color-* custom property names exactly, so the token file can be regenerated from the source colour without touching a stylesheet. The only file that may contain a hex literal is assets/css/tokens.css.

The source colour is #7DC4E4, the app's dark primary. The scheme is SchemeTonalSpot on the 2025 spec, contrast level 0. Regenerate with the script in scripts/tokens.mjs, never by hand.

Dark (default)

Role Value Where it goes
primary #a2cde2 Filled buttons, the one accent per section, focus rings, active nav item
on-primary #174557 Text and icons on primary
primary-container #2d586a Tinted surfaces that mean "running" or "active"
on-primary-container #bfe9ff Text on primary-container
secondary #b4cad6 Restart, secondary actions, quiet emphasis
secondary-container #2a3e48 Tonal buttons, selected chips
tertiary #d1dcff In-progress, downloading, the second decorative shape on a page
tertiary-container #becefa Decorative shapes only, never a button
error #fa746f Stop, failed, destructive. Nothing else.
surface #0b0f11 Page background
surface-container-lowest #000000 Code blocks and terminal panes
surface-container-low #0f1417 Footer, the docs sidebar
surface-container #141a1e Cards sitting on the page
surface-container-high #1a2124 Cards nested in cards, chips, table header row
surface-container-highest #1f272b Hover state of a surface-container card
on-surface #dee7ec Body text, headings
on-surface-variant #a4acb2 Supporting text, metadata, captions
outline #6e777c Generated but unused. Outlined buttons take outline-variant so one border colour serves the site
outline-variant #41494e Card borders, dividers, table rules
primary-fixed #b5e0f6 The phone mockups' accent. A fixed role barely moves between themes, so screenshots captured in dark mode still match in light mode

Light

Role Value
primary #28667e
on-primary #f3faff
primary-container #a8e2fe
on-primary-container #0c536a
surface #f7fafc
surface-container-lowest #ffffff
surface-container-low #eff4f8
surface-container #e9eff3
surface-container-high #e2e9ee
surface-container-highest #dbe4e9
on-surface #2b3438
on-surface-variant #586065
outline-variant #abb3b9

The full light set, including secondary, tertiary and error, lives in tokens.css and is generated by the same script. It is not repeated here because nobody should be copying it by hand.

How colour is used

Borders and tint carry state. Fills stay quiet. This is the app's rule and it is the site's. A "running" container in a screenshot caption is not a green box. It is a normal card with an accent border. The site never fills a card with primary.

One accent per section. Each section of a page gets one thing in primary: a button, or an active tab, or a highlighted table cell. If a section has two things in primary, one of them is wrong. Decorative shapes use tertiary-container or secondary-container, never primary, so the eye still finds the button first.

Terminals are black. Code blocks and terminal panes sit on surface-container-lowest, which is #000000 in dark mode. This is the one place the site goes fully black, and it matches the app's terminal, which does not follow the app theme either.

Text alpha. Secondary text is on-surface-variant at full opacity, not on-surface at 70%. The M3 role already encodes the reduced contrast, and stacking alpha on it fails WCAG on surface-container-high. Disabled text is on-surface at 38%, disabled fills at 12%, both from the M3 state spec.

Typography

One family for interface text, one for machine output. Nothing else.

IBM Plex Sans for everything a person wrote. It is the face of the project's own artwork, it is released under the SIL Open Font License, and its plain, even strokes read as a technical document rather than a consumer app. Self-host it from Fontsource as a single latin-subset variable WOFF2 carrying the weight axis. Never load it from Google Fonts at runtime, because the site should render with no third-party request.

JetBrains Mono for everything the machine wrote: commands, output, paths, unit names, kernel version strings. This matches the app, where machine output is JetBrains Mono from ui/theme/Type.kt. Self-hosted the same way.

The scale is the Material 3 type scale. The 2021 sizes are unchanged in Expressive; what changed is the addition of emphasized styles, which take the same size and line height and bump the weight. Here that step is 400 to 700 for display and headline styles and 400 to 600 for titles and labels. Body copy stays at 400. The contrast between heavy headings and light body text is the whole expressive signature, so nothing else is bold. The one exception is the product name in the hero's sentence, set in <strong> so the eye finds what the sentence is about.

Element Style Notes
Hero headline display-lg-emphasized 57/64, weight 700. Drops to display-md-emphasized under 600px
Hero headline, 840 to 1199 display-md-emphasized The 5/12 column is too narrow for 57px there
Section headline headline-lg-emphasized 32/40, weight 700. A claim of three to six words
Card title title-md-emphasized 16/24, weight 600
Section intro paragraph title-lg 22/28, weight 400, on-surface-variant
Body copy body-lg 16/24
Supporting text, captions body-md 14/20, on-surface-variant
Button and chip label label-lg-emphasized 14/20, weight 600
Table header, metadata label-md 12/16, weight 500
Code, terminal, commands code JetBrains Mono 14/22

Line length is capped at 68 characters for body copy (max-width: 68ch), which is where a 16px sans stays comfortable. Headlines do not track tighter than 0; letter-spacing of −2px on a display headline is a web-template habit, not a Material one.

Every section opens the same way: a short claim as the headline, one title-lg sentence under it, and chips for any list of facts. A second paragraph is the exception, not the pattern; if the section needs one, the claim is probably too vague.

Never accent a single word of a headline in colour or italic. The headline is one sentence in one colour. Emphasis, when needed, is the sentence break.

Layout

The grid unit is 8px. The spacing scale is the Material 3 space token set, of which the site uses nine steps: 4, 8, 12, 16, 24, 32, 48, 64, 96. If a gap wants 40, it wants 32 or 48.

Gap Value
Icon to its label, chip to chip 8
Between rows inside a card 12
Card inner padding 24
Between cards in a grid 16
Section inner padding, top and bottom 96 on desktop, 64 under 840px
Page side gutter 24 on desktop, 16 under 600px
Content max width 1200px
Prose max width 68ch

Breakpoints are the Material window size classes and nothing else: 600, 840, 1200, 1600. Compact is under 600, medium to 839, expanded to 1199, large to 1599. Layouts collapse at those lines, not at 768 or 1024.

The hero is a two-column split at expanded and above: copy on the left at 5/12, phone mockup on the right at 7/12, vertically centred. Under 840 it stacks, copy first, and the phone takes 72% of the column (at most 380px) and the shape behind it the full column (at most 520px), so the shape frames the phone on both sides and never runs past the edge of the screen. At 840 and up a shape is never wider than its own column. It sits at z-index: -1 inside a phone stage that sets isolation: isolate, so it stays behind the phone and never over copy. The section itself is not isolated: a phone shadow has to cross into the next section, and a section that isolates cuts it off in a hard line; main clips horizontal overflow so it can never scroll the page either.

The home page has seven sections: the hero, why it matters (set it up once, forget it), a real Linux rather than an emulation, the network, the app carousel, the comparison, and requirements with the download.

Feature sections alternate the side the phone sits on. Text, then phone; phone, then text. This replaces the row of six identical cards.

The page background is one unbroken surface. Sections carry the 1200px measure in their side padding and have no fill of their own. Alternating tonal bands were tried and removed: at every band edge the eye reads a separator line, and the page stops feeling like one surface.

Cards appear in one place: the comparison against PRoot, chroot and QEMU, where a table is the honest format and cards would hide the comparison. Everywhere else, content sits directly on the page background with a headline, a paragraph and a screenshot.

The docs pages are a three-column wiki, full width, no centred box: a 280px sidebar flush to the left edge on surface-container-low, sticky under the nav at full viewport height with its own scroll; the page at a 760px measure; and an "On this page" list of the page's h2 and h3 at 240px, from 1200 up. Between 840 and 1199 the right column goes; under 840 the sidebar becomes a top drawer. Sidebar groups are <details>, so they collapse without a script. The current section in "On this page" is primary where the browser supports scroll-target-group; there is no scroll-spy script. Code is highlighted at build time by Pygments, and its token colours are M3 roles only (keyword primary, string tertiary, name secondary, comment and punctuation on-surface-variant, number on-primary-container, deletion error), so both themes work. A code block has a 40px header row holding the language and the copy button, so neither covers the first line. Search is a native <dialog> on surface-container at radius 28, opened from the sidebar, Ctrl K or /.

Elevation & Depth

There are no shadows on this site, with one exception.

Depth is tonal. A card on the page is surface-container. A card inside it is surface-container-high. Hovering a card steps it to surface-container-highest. That is the whole elevation system, and it is the app's: depth is expressed by stepping the surface colour up one level and drawing a 1px border.

Every card and every nested surface has a 1px border in outline-variant. On the app the alpha of that border drifted across 40 sites; on the site it is one value, full opacity, because outline-variant in M3 is already the low-contrast outline.

The exception is the phone mockup. It carries 0 40px 90px rgba(0,0,0,.55) (--shadow-phone), because it is a photograph of an object, not a surface of the page, and an object sitting on a page has a shadow. No other element may have one. If a card looks like it needs a shadow, it needs a higher surface tier.

Nothing is glassmorphic and nothing is translucent. There is no backdrop-filter on the site. The navigation bar is an opaque surface of the page like every other surface, and it shows that content has scrolled under it the tonal way, by stepping a tier. A blurred shade was tried and removed: it is the one pattern every template ships, and the app has no such bar.

Shapes

The radius scale is Google's, from md-shape-tokens.css, and the three larger values are the ones Expressive added: 4, 8, 12, 16, 20, 28, 32, 48, full.

Element Radius
Card on the page 28 (xl)
Card nested in a card, code block 16 (lg)
Chip, badge full
Button at rest 28 (xl), which is a pill at 56px tall
Button pressed 12 (md)
Button, selected or toggled 16 (lg)
Text input 16
Phone frame outer 7.8% of the frame width
Phone screen 6.2% of the frame width
Decorative shapes see below

Buttons morph when pressed. A resting button is a pill, drawn with 28px corners rather than full: a 9999px radius animating to 12 on an overshooting curve dips below zero and clamps, which shows as square, jagged corners for a frame. On :active it snaps to 12px corners and springs back on release, over 350ms cubic-bezier(0.42, 1.67, 0.21, 0.9). That curve and those numbers are Google's own, from the expressive button in @material/web 2.5.0, and the overshoot in the bezier is the point. Do not replace it with ease-out.

Decorative shapes are the one expressive flourish per page. Material 3 Expressive ships 35 named shapes. The site uses five: cookie-12, cookie-9, clover-4, sunny and pill. Each appears at most twice on a page, as a mask-image over a flat tertiary-container or secondary-container fill, sitting behind a phone mockup. Every single-phone stage on the home page has one, each shape used once; the carousel has none, because three overlapping phones leave only slivers of it, which read as a rendering fault, tertiary-container and secondary-container alternating. They are large (400 to 1000px), slow to move (see Motion), and never carry text or icons. They are never primary. The SVG masks live in assets/shapes/ and come from Beer CSS 5 (MIT), which traced them from Google's Figma kit.

The phone frame is its own shape and it is fixed: bezel at 1.75% of the frame width, outer radius 7.8%, screen radius 6.2%, a centred punch-hole camera at 5.2% of the screen width, in the bezel colour, with nothing drawn inside it. Frame colour #0a0f12 (--phone-frame). The screenshots stop at the app's title bar, so the mockup draws a status bar (time, camera, signal icons) and a gesture bar around them in the app's own surface colours (--phone-app-surface, --phone-app-bar, --phone-app-on-surface). This is a Galaxy S25 Ultra silhouette, thin and square, and it is the same on every page at every size. Screenshots inside it are object-fit: cover; object-position: top.

Where Chromium supports it, cards, chips and code blocks take corner-shape: squircle inside an @supports block, which is the true Material shape. Browsers without it get the round corner. Nothing depends on the difference. Buttons stay round: their radius animates on press, and an animating squircle renders with rough edges.

Components

Components are plain HTML with classes. No web component library, no framework. The Lit-based @material/web components are in maintenance mode and predate Expressive, so they are not used; their token stylesheets are.

Buttons

Height 56px (M3 md size). Three variants and no more:

Variant Fill Text Border
Filled primary on-primary none
Tonal secondary-container on-secondary-container none
Outlined transparent on-surface-variant 1px outline-variant

A section has one filled button. "Download" is filled. "Read the docs" beside it is outlined. Two filled buttons next to each other is the tell of a template.

Labels are label-lg-emphasized, one line, sentence case. "Download the APK", not "DOWNLOAD NOW". No arrow glyph appended to the label; the button is already the arrow.

The one icon-only button is back to top: tonal, 56px square, a 24px arrow_upward, pinned 16px from the bottom right corner of the viewport on every page. It is tonal so the filled button in whatever section is on screen stays the one primary. It appears once a viewport of content has scrolled past, by a scroll timeline, so a browser without scroll timelines just always has it.

Icons inside buttons are Material Symbols Rounded, 20px, FILL 0, and they stay outlined on hover: a glyph that fills while the button morphs was two things moving at once. The icon leads the label.

Chips

Height 40px, full radius, surface-container-high fill, 1px outline-variant border, label-lg-emphasized. Used for lists of facts: init systems, supported architectures, what a container can run, the hardware the app hands over. Every chip names something documented in the main repository's README or Documentation/Features.md; a chip that could describe any product ("Docker inside", "Fast") is not a fact. A selected chip, and any chip under the pointer, takes secondary-container with on-secondary-container text. Chips wrap; they do not scroll horizontally.

Cards

surface-container, radius 28, 1px outline-variant, padding 24. Title in title-md-emphasized, body in body-md. A card never contains a button unless the card is the download card. Hover steps the surface to surface-container-highest over the effects spring, with no transform, no lift, no shadow.

Code blocks and terminal panes

surface-container-lowest, radius 16, padding 20, JetBrains Mono. A copy button sits top-right as an icon button. Where a terminal transcript appears it is a real <pre>, not an image, with prompt in primary, output in on-surface, and the cursor block static. It does not type itself out. The hero has no terminal pane: the phone screenshot already shows the runtime working, and a boot transcript beside it repeated the next section.

Navigation

The M3 top app bar. Sticky, 64px tall, opaque surface at rest. Once the page has scrolled it steps to surface-container, the M3 on-scroll container colour, across the first 64px of scroll; that tier step is the only sign that content is passing under it. No bottom rule, no blur, no translucency. Logo mark left, links right, theme toggle last. The active link is primary; the rest are on-surface-variant, with the M3 state layer on hover.

Under 840px the links collapse into the M3 modal navigation drawer, from the right: 360px wide on surface-container-low, radius 16 on its open edge, no border, the scrim behind it does the separating. Items are 56px tall at full radius, and the current page sits on secondary-container with on-secondary-container text, which is the drawer's active indicator.

The docs page tree is the M3 standard navigation drawer: 280px, on surface like the bar and the page, no divider. The section labels and the active pill are its only structure, so the bar, the tree and the article read as one plane. Under 840px it becomes a modal drawer from the left, the mirror of the menu drawer, opened from the breadcrumb row that stays under the bar.

Phone mockup

One component, one silhouette, described under Shapes. It takes a screenshot. Phones do not float: an idle loop on a phone beside a turning shape drifts out of step with it and reads as a glitch, so the shape is the only thing that keeps moving.

Three phones in a group form the app carousel, one <figure> each with a one-line body-md caption saying what the screen does. Exactly one phone is in front and only its caption shows.

  • On a wide pointer screen (hover: hover, 840 and up) the phones overlap, the side ones at 80% scale. Hovering or keyboard-focusing a phone brings it to the front; at rest the middle one is. Hover wins over focus, so two phones are never in front at once.
  • Everywhere else the group is a horizontal scroll-snap carousel, edge to edge under 840, opening on the first screen. Not the middle one: scroll-initial-target also scrolls the page itself to the carousel on every load, so the visitor never sees the hero. A scroll-driven view(inline) timeline makes the centred phone full size and its neighbours 85%, and shows only the centred caption.

No JavaScript runs either mode.

Boot chain

The one sequence on the home page: phone boots, Droidspaces starts the containers, the init system starts the services. An <ol> of three steps, each an h3 in title-md-emphasized and one body-md line. Steps are joined by a 1px outline-variant rule through 12px ring markers in outline-variant on surface. No numerals and no primary: it is a chain of events, not a numbered feature list. It fills with scroll (see Motion).

Tables

The comparison table is surface-container with a surface-container-high header row, 1px outline-variant rules, label-md headers, body-md cells. The Droidspaces column is tinted primary-container at 20%, which is the only fill of primary on the page and is why the comparison section has no filled button.

Footer

surface-container-low, three columns on desktop (project, community, legal), the logo mark and a one-line description, GPLv3, and the maintainer's name. No newsletter box.

Motion

Material 3 Expressive replaced duration-and-easing with springs. Two families: spatial springs move, resize and reshape things, and may overshoot; effects springs change colour and opacity and never overshoot. The site uses the Expressive spatial set and the shared effects set, encoded as CSS linear() easings generated from the Compose damping and stiffness values. No JavaScript animation library.

Token Damping / stiffness Settles in Use
--motion-spatial-fast 0.6 / 800 480ms Button press, chip select, icon fill
--motion-spatial-default 0.8 / 380 470ms Cards and phones entering the viewport, drawer open
--motion-spatial-slow 0.8 / 200 640ms The hero phone's arrival, section shapes
--motion-effects-fast 1.0 / 3800 180ms Hover colour, focus ring
--motion-effects-default 1.0 / 1600 270ms Surface tier step on hover, theme change

The linear() point lists are in tokens.css, along with --motion-press for the button morph and --motion-float, which is generated but unused since phones stopped floating. They were produced from the spring equation, not drawn, so --motion-spatial-fast really does overshoot by 9.5% and settle, the way the Android button does.

Six kinds of motion exist on the site, and nothing else moves:

  1. Arrival. When a phone mockup or decorative shape reaches the screen, it translates up 24px and fades in over --motion-spatial-slow, once. It is timed, not scroll-driven: a scroll-linked entrance ran at the scroll's speed, so a fast flick popped objects in and every scroll frame repainted them. site.js watches with an IntersectionObserver and starts the CSS animation; the motion itself is still CSS. Objects already on screen at load are left alone. Text does not animate in. Cards do not animate in. Only the objects. The section's headline is already there when you get to it.
  2. Press and hover. Buttons morph on press. Surfaces step a tier on hover. Chips take secondary-container on hover. Icons do not change. All use the springs above.
  3. Carousel. A phone promoted by hover or focus scales to 100% over --motion-spatial-default; its caption fades over --motion-effects-default. In the swipe carousel the scale and caption follow the scroll position, not a timer. When the overlapping group arrives, the side phones fan out from behind the front one, on the same timed trigger as arrival.
  4. Shape turn. Decorative shapes turn once every 120 seconds, linear, forever, the way the M3 Expressive loading indicator they come from turns. secondary-container shapes turn the other way, so neighbours never look like one pattern. The turn runs on a timer, not on scroll, so the page keeps moving while the visitor reads. A shape arrives by fading in from 90% scale rather than by translating, because its translate is what centres it.
  5. Boot chain fill. As the boot chain moves up the screen, each ring turns primary in order and the rule between rings fills, on the chain's own view() timeline. It is scroll-linked on purpose: it pauses when the reader stops and runs back when they scroll back. The page never locks scrolling to play it; a page that stops scrolling reads as broken and fights keyboard and assistive scrolling. The fill is the section's one primary.
  6. Shape morph. The loading indicator on the downloads page cycles through cookie-12, clover-4 and sunny by morphing the mask, which is the M3 Expressive loading indicator. Nothing else morphs.

Only site.js ever hides an object before its arrival, and only one that is below the screen, so nothing is hidden without JavaScript: the page renders complete with JavaScript disabled and with prefers-reduced-motion: reduce, in which case the shape turn stops, arrival becomes an instant fade, the button morph is disabled, the carousel loses its fan-out and scale (it still swipes and still hovers, instantly), and the swipe carousel shows every caption, and the boot chain stays drawn without filling. The reduced-motion branch is not optional.

There is no float, no parallax, no marquee, no typing effect, no counter that counts up, no pulsing dot, no cursor that blinks, no hover transform that scales or rotates an image, and no page-load sequence that stagger-reveals every element in turn. Each of those is a template tell and each has been seen on a hundred landing pages this year.

Do's and Don'ts

Do use primary on exactly one element per section. Do give every card a 1px outline-variant border and no shadow. Do let the phone mockups carry the page. They are real screenshots of a real app. Do write headlines as one plain sentence in one colour. Do keep body copy at body-lg and under 68 characters a line. Do run the site with JavaScript off before every release and confirm nothing is missing. Do test prefers-reduced-motion and prefers-color-scheme on every page. Do keep a 48px minimum on anything tappable. This is the one rule design does not override.

Don't use a gradient anywhere: not on a background, not on text, not as a glow behind a phone. Don't use a colour that is not in tokens.css. Don't put an uppercase, letter-spaced label above a heading. Don't number sections 01, 02, 03. Nothing on the site is a sequence except the install steps and the boot chain. Don't join metadata with middle dots. Use a comma or a separate line. Don't append an arrow to a link or button label. Don't use an em dash. Use a comma, a full stop, or rewrite the sentence. Don't write "Unlock", "Supercharge", "Seamless", "Blazing fast", "Next-generation" or "Reimagine". Don't build a row of three or six identical icon-title-text cards. Don't animate text, and don't animate anything on scroll except the swipe carousel and the boot chain fill. Don't add a second shadow, a glow, a frosted card, or a dot grid background. Don't put backdrop-filter on anything. The navigation bar is opaque; the blurred shade was tried and removed. Don't load a font, icon or script from a third-party origin at runtime.

Decided exceptions

Looked at, deliberately left alone. Do not re-open these without a reason the original one misses.

  • The phone mockup has a shadow. It is the only shadow on the site. It is an object, not a surface, and the same silhouette is used in the LinkedIn and README artwork, so the two must match.
  • Terminal panes go to #000000 in dark mode rather than a tonal surface. The app's terminal does the same, and a slate terminal reads as a disabled one.
  • primary-fixed is used for accents inside the phone screenshots' frames, so the mockups look the same in light mode. A screenshot captured in dark mode cannot change with the theme, and a fixed role is what M3 provides for that case.
  • The docs pages keep their generated structure from .github/scripts/build-docs.py and only restyle it. The Markdown source is in the main repository, and the site must not fork it.
  • Light mode was generated, not designed. It is the same source colour through the same scheme. If a light-mode surface looks wrong, fix the generator input, not the CSS.