DESIGN.md@site · git:20260719.b172db1 · 2026-07-19 · sha256 2e86ff419451b25f

DESIGN.md@site git:20260719.b172db1A

Immutable. This exact content never changes and is served at /api/v1/blob/2e86ff419451b25f.

# Design

Visual system for the Roblox Suite marketing site. Strategy and audience live in [PRODUCT.md](./PRODUCT.md); this file records how it looks.

The form language is **injection-moulded plastic**: flat saturated fills, solid 2px ink edges, and hard offset shadows. There are no glows, no glass, no gradients on text, and no ambient blur anywhere in the system.

## Theme

Drenched. The cobalt is the surface, not an accent on a neutral ground. Sections alternate between three grounds so the scroll has rhythm rather than one repeated template:

| Ground | Used for |
| --- | --- |
| `brand` cobalt | Hero, correction panel, catalog index, page headers |
| `ink` near-black | Ticker band, footer, code panels |
| `paper` off-white | Long-form reading: how-it-works, skill body, evidence table |
| `toy-yellow` | The closing CTA only, as the page's brightest surface |

## Color

All values OKLCH. Neutrals are tinted toward the brand hue (258), never toward warm.

| Token | Value | Role |
| --- | --- | --- |
| Primary | `oklch(0.45 0.235 258)` | `--brand`, the drench |
| Brand deep | `oklch(0.33 0.19 258)` | Recessed brand surfaces |
| Brand lift | `oklch(0.56 0.22 258)` | Raised brand surfaces, brick blue |
| Ink | `oklch(0.17 0.035 258)` | Near-black ground and every border |
| Ink soft | `oklch(0.24 0.04 258)` | Dividers inside ink panels |
| Paper | `oklch(0.965 0.012 258)` | Off-white ground and light text |
| Accent | `oklch(0.87 0.185 92)` | `--toy-yellow`, second voice, primary CTA |
| Deprecated | `oklch(0.63 0.235 27)` | `--toy-red`, dead APIs |
| Current | `oklch(0.75 0.19 148)` | `--toy-green`, working APIs |

Muted text is a `color-mix` tint of its own ground, never flat gray on a colored surface. Every pair is verified at WCAG AA: the weakest is muted-on-cobalt at 4.72:1 and ink-on-red at 4.88:1. Paper on `toy-red` measures 3.54:1 and is **not** used for small text.

Deprecated and current are never signalled by hue alone; a `-` / `+` gutter and a text label carry the same distinction.

## Typography

| Role | Face | Notes |
| --- | --- | --- |
| Wordmark | Unbounded Variable 700 | Wide geometric, reads extruded |
| Display | Unbounded Variable 700 | All h1–h3 |
| Body | Hanken Grotesk Variable 400 | Humanist, high legibility |
| Code | JetBrains Mono Variable | Literal code only, never decoration |

Paired on a contrast axis (geometric display against humanist body) rather than two near-identical sans faces. Both are self-hosted via `@fontsource-variable`; nothing loads from a CDN.

Modular scale, ≥1.25 between steps. Display is capped at `4rem` rather than the 6rem ceiling because Unbounded is wide and ran to five lines at larger sizes. Tracking bottoms out at `-0.03em`. Light-on-dark body runs at 1.7 line-height to offset optical lightening.

```
display  clamp(2.25rem, 4.8vw, 4rem)   lh 0.98
title    clamp(1.875rem, 4.2vw, 3.25rem)
heading  clamp(1.375rem, 2.4vw, 1.875rem)
lead     1.375rem
base     1.0625rem
small    0.9375rem
```

Running prose is capped at 68ch via the `measure` utility.

## Components

**Panel** — `panel` / `panel-sm`. 2px ink border, 12px radius (6px small), hard offset shadow `6px 6px 0`. The one container primitive; there are no nested panels. Variants: ink, paper, brand, yellow-shadow.

**Pressable** — `pressable`. Hover lifts 2px and deepens the shadow; active travels 6px into it, like a key bottoming out. Transform and box-shadow only, so nothing reflows. Frozen under reduced motion.

**Buttons** — Primary is yellow fill with ink text (the GitHub star). Secondary is ink fill with paper text. Both are panels with `pressable`.

**Brick scene** — `BrickScene.tsx`. Hand-written 3D canvas renderer: perspective projection, painter's-algorithm depth sort, flat-shaded faces with 2px ink strokes, projected elliptical studs. Bricks free-fall into a tower on a 7.5s loop. Ambient shading floor is high (0.78) so faces read as coloured plastic rather than grey against the cobalt. Renders one settled frame under reduced motion and pauses off-screen via IntersectionObserver.

**Correction panel** — `CorrectionPanel.tsx`. The product's argument staged as a real diff. First paint is never gated behind hydration; only transitions between corrections animate.

**Skill index** — `SkillIndex.astro`. A grouped index of full-width rows with rules between them, deliberately not a grid of sixteen equal cards. Rows fill yellow on hover and focus.

**Ticker** — `DeprecatedTicker.tsx`. Two counter-scrolling marquees of dead APIs, pausing on hover.

## Motion

Exponential ease-out (`--ease-out-expo`, `--ease-out-quart`). No bounce, no elastic, and no animation of layout properties. Scroll reveals use `animation-timeline: view()` and layer on top of already-visible content, so nothing ships blank when scripting or hydration does not run. Every animation has a reduced-motion path; the brick scene freezes rather than merely slowing.

## Layout

Semantic z-scale: `--z-scene: 1`, `--z-content: 2`, `--z-sticky: 20`, `--z-modal: 40`, `--z-toast: 60`. No arbitrary values.

Grid children that contain nowrap content carry `min-w-0`, since `min-width: auto` otherwise forces the column wider than the viewport and the section clips it.

## Deliberately excluded

Glassmorphism, gradient text, glow shadows on dark, violet/cyan palettes, tracked uppercase eyebrows above section headings, numbered section markers, identical card grids, hero-metric stat rows, Inter, and any component-library decoration. Each is a recognizable generated-UI tell; see the anti-references in PRODUCT.md.