← Back to library

SaaS

Prism

Liquid-glass product landing: drifting colour fields under real backdrop-filter panels with chromatic edges.

3 tools★ 96
#glassmorphism#blur#light
Claudev0Lovable

The prompt

Prompt
# Prism — Liquid-Glass Product Landing

## What you're building

A single-page product landing for **Prism**, a fictional surface editor for design engineers. Five huge blurred colour blobs drift slowly behind the page, and every panel above them is real `backdrop-filter` glass — blurred, saturated, with an inner highlight and a two-hue chromatic ring painted into a 1.4px border. The distinctive technique is that the glass is a *material*, not an opacity value: what sits behind each panel is genuinely sampled, so the drifting field bleeds through the nav, the hero pane, the feature cards, the pricing frame and the footer differently as it moves.

Prism is an invented brand. Every price, feature and claim on the page is placeholder content, and the footer says so.

## Art direction

Mood: morning light through a prism on white paper — pale, high-key, expensive, nothing shouting.

| Token | Hex / value | Use |
|---|---|---|
| `--pz-ink` | `#141021` | Headings, primary type |
| `--pz-body` | `#413a58` | Body copy, nav links, card paragraphs, **section kickers** |
| `--pz-faint` | `#4f4766` | Eyebrow, tier names, footer column headings, footer meta |
| `--pz-line` | `rgba(255,255,255,.55)` | Hairlines inside glass |
| `--pz-glass` | `rgba(255,255,255,.42)` | Default glass fill |
| `--pz-glass-strong` | `rgba(255,255,255,.60)` | Raised glass (icon wells) |
| Page ground | `radial-gradient(120% 90% at 78% -10%, #fdf3ef 0%, rgba(253,243,239,0) 55%)`, `radial-gradient(100% 80% at 8% 105%, #eef0ff 0%, rgba(238,240,255,0) 60%)`, `linear-gradient(178deg,#f7f5fb 0%,#eceaf6 48%,#f4eef2 100%)` | Three stacked layers on `.pz-root`, in that order |
| Veil | `rgba(250,249,252,.22)` | Fixed scrim between the field and the content — see Layout §Stacking |
| Blob 1 | `#a78bfa → #7c5cf0` | Violet, top-left |
| Blob 2 | `#67e8f9 → #22b8d8` | Cyan, top-right |
| Blob 3 | `#ffb4a2 → #ff7d6b` | Coral, bottom-centre |
| Blob 4 | `#f0abfc → #c084fc` | Orchid, bottom-right, opacity `.55` |
| Blob 5 | `#fde68a → #fca5a5` | Amber, mid-page, opacity `.45` |
| CTA gradient | `linear-gradient(135deg,#5b4be8,#9333ea 52%,#ec4899)` | Primary buttons, solid tier button |
| Nav CTA gradient | `linear-gradient(135deg,#6d5bf0,#a855f7 55%,#f472b6)` | Pill button in the nav |
| Headline gradient | `linear-gradient(105deg,#4f46e5 0%,#0e7490 42%,#e11d48 100%)` | The one emphasised word |
| Card glyph fill | `#5b4bd8` | The three feature icons |
| Tick | `#6d5bf0` | Pricing feature ticks |
| Focus ring | `#4f46e5` | `outline: 2px solid`, `outline-offset: 3px`, `border-radius: 10px` |

Type — **one system stack, no webfonts.** `system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`, `-webkit-font-smoothing: antialiased`. The whole personality comes from weight and tracking, not family.

- **H1** — `clamp(2.9rem, 9vw, 6.6rem)`, weight **200**, `line-height: 1.02`, `letter-spacing: -.035em`, `--pz-ink`, `margin: 34px 0 0`. The emphasised word is an `<em>` at weight 300, `font-style: normal`, filled with the headline gradient via `-webkit-background-clip: text; background-clip: text; color: transparent`.
- **H2** — `clamp(1.95rem, 4.4vw, 3.1rem)`, weight **250**, `letter-spacing: -.028em`, `line-height: 1.1`, `margin: 14px 0 0`.
- **Lede** — `clamp(1rem, 1.6vw, 1.19rem)`, weight **350**, `line-height: 1.62`, `max-width: 37ch`, `margin: 26px auto 0`.
- **Section sub** — `1.02rem`, weight 350, `line-height: 1.65`, `max-width: 52ch`, `margin: 16px 0 0`.
- **Eyebrow / kicker** — `11.5px`, uppercase; eyebrow `letter-spacing: .2em` in `--pz-faint`, section kicker `.22em` in `--pz-body`. The kicker is the one label that sits directly on the drifting field rather than on glass, which is why it is a full step darker than the eyebrow — see Motion & accessibility.
- **Nav links** — `13.5px`, `--pz-body`, `padding: 8px 14px`, `border-radius: 999px`. **Brand mark** `12px`, uppercase, weight 600, `letter-spacing: .16em`.
- **Card H3** — `1.16rem`, weight 500, `letter-spacing: -.012em`, `margin: 20px 0 0`. **Card body** `.95rem / 1.62`, weight 350, `--pz-body`.
- **Tier name** — `12px`, `.18em`, uppercase, `--pz-faint`. **Tier price** — `2.5rem`, weight 250, `letter-spacing: -.035em`; the cadence note is a `<span>` at `.92rem`, weight 400, `--pz-faint`, `letter-spacing: 0`. **Tier feature line** — `.92rem / 1.45`, `--pz-body`. **Tier button** — `14px`, weight 500.
- **Buttons** — `15px`, weight 500, `padding: 15px 30px`; nav CTA `13.5px`, weight 500, `padding: 9px 20px`.
- **Footer** — column headings `11.5px`, `.2em`, uppercase, weight 500, `--pz-faint`; links `.92rem`, `--pz-body`; bottom row `.82rem`, `--pz-faint`.

Radii: glass panels `26px`, hero pane / pricing frame / footer `30px`, pane inner surface `18px`, buttons `16px`, tier cards `20px`, tier buttons `13px`, nav and eyebrow `999px`, icon wells `14px`, focus ring `10px`.

**Copy against the type scale.** The H1's break is authored, not fluid: a literal `<br>` after `Design with`. Each half then has to fit one line at every width the page supports.

- At **1440px** the H1 renders at its `6.6rem` cap (105.6px; `9vw` = 129.6px, so the cap binds from 1173px up). The content column is 1080px wide. `Design with` sets to ≈220px and `light, not layers` to ≈810px — both comfortably inside 1080px, so the headline is exactly two lines.
- At **375px** the clamp floor binds (`2.9rem` = 46.4px, against `9vw` = 33.8px) and the column is 335px. `light, not layers` sets to ≈286px. Still two lines. The H1 never takes three at any width the page supports.
- The lede is capped at `37ch` (≈420px at 1440px) and centres under the headline; it sets to **three** lines at 1440px and to five at 375px.
- Both H2s fit one line from 1120px up. `Priced per editor. Viewers are free, always.` takes two lines below about 900px, which is fine.

## Copy

Every string the page renders, verbatim. Prism, Prism Surfaces and every price and feature are invented — they stay exactly as written, no `{{placeholder}}` substitution. The `—` in the eyebrow, the card body and the footer are real em dashes (U+2014); the `·` in the legal line is a real middle dot (U+00B7); `Montréal` carries a real `é`.

### Nav

| Slot | String |
|---|---|
| `<nav>` accessible name | `Primary` (via `aria-label="Primary"`) |
| Triangle mark | decorative — `aria-hidden="true"`, no title |
| Wordmark | `Prism` |
| Nav link 1 | `Material` |
| Nav link 2 | `Pricing` |
| Nav link 3 | `Company` |
| CTA button | `Open studio` |

### Hero

| Slot | String |
|---|---|
| Eyebrow dot | decorative — `aria-hidden="true"` |
| Eyebrow | `Version 4 — the material release` |
| H1 | `Design with` + `<br>` + `<em>light</em>` + `, not layers` |
| Lede | `Prism is a surface editor for teams who care what happens between the pixels. Build depth once, and it holds all the way to production.` |
| Primary CTA | `Start building free` (plus the arrow glyph, `aria-hidden`) |
| Ghost CTA | `See the material` |
| Hero pane | decorative in full — `aria-hidden="true"` on the pane wrapper; no strings inside it |

### Material

| Slot | String |
|---|---|
| Kicker | `The material` |
| H2 | `Three things glass has to get right.` |
| Sub | `Most tools fake translucency with an opacity slider. Prism models the whole stack — what is behind the panel, how the edge bends it, and where the light lands.` |
| Card 1 title | `Refraction engine` |
| Card 1 body | `Every surface samples what sits behind it in real time. Blur, saturation and dispersion are one continuous material, not three filters stacked in a row.` |
| Card 2 title | `Depth that survives export` |
| Card 2 body | `Shadow, highlight and edge tint are baked from the same light rig, so a panel looks identical in the canvas, the handoff spec and the shipped build.` |
| Card 3 title | `Tokens, not screenshots` |
| Card 3 body | `Publish a surface once and it arrives in code as variables your team already uses. No redlines, no pixel archaeology, no second source of truth.` |
| All three glyphs | decorative — `aria-hidden="true"` |

Exactly three cards and three glyphs, in that order, matching the H2's count. Do not add a fourth.

### Pricing

| Slot | String |
|---|---|
| Kicker | `Pricing` |
| H2 | `Priced per editor. Viewers are free, always.` |

Three tiers, left to right. Every tick glyph is `aria-hidden="true"`.

| | Tier 1 | Tier 2 (featured) | Tier 3 |
|---|---|---|---|
| Name | `Solo` | `Studio` | `Atelier` |
| Price | `$0` | `$24` | `Custom` |
| Cadence note | `/ forever` | `/ editor / month` | *(none — omit the `<span>` entirely)* |
| Feature 1 | `Three live surfaces` | `Unlimited surfaces and layers` | `SSO and audit trail` |
| Feature 2 | `Community material library` | `Token sync to your codebase` | `Private material registry` |
| Feature 3 | `PNG and SVG export` | `Shared material library` | `Named design engineer` |
| Feature 4 | — | `Version history for 12 months` | — |
| Button | `Start free` | `Start 14-day trial` | `Talk to us` |
| Button style | ghost | solid gradient | ghost |

Tier 2 is the only tier carrying the `feat` fill and the only solid button. Tiers 1 and 3 have three feature lines; tier 2 has four.

### Footer

| Slot | String |
|---|---|
| Mark | `Prism` (same triangle glyph, `aria-hidden="true"`) |
| Positioning line | `A surface editor for design engineers. Made in Lisbon and Montréal.` |
| Column 1 heading | `Product` |
| Column 1 links | `Material` · `Pricing` · `Changelog` |
| Column 2 heading | `Resources` |
| Column 2 links | `Documentation` · `Token reference` · `Community` |
| Column 3 heading | `Company` |
| Column 3 links | `About` · `Careers` · `Contact` |
| Bottom left | `© 2026 Prism Surfaces. A fictional product, built as a demo.` |
| Bottom right | `Privacy · Terms · Status` |

The bottom-right line is a single non-interactive `<span>`, not three links — do not linkify it.

### Anchors

Every link is a same-page anchor; there is no external navigation. `#material` (nav, hero ghost CTA, footer *Material*, *Changelog*, *Documentation*, *Token reference*, *Community*), `#pricing` (nav, hero primary CTA, footer *Pricing*), `#company` (nav, footer *About*, *Careers*, *Contact*, and the id on the `<footer>` itself).

## Assets and illustrations

**No raster images, no video, no webfont, no icon font, no external requests.** Every graphic is inline SVG or a CSS gradient. Every SVG carries explicit `width` and `height` attributes so nothing reflows after first paint. Do not substitute stock imagery or a screenshot; the point of the piece is that the product surface is drawn in CSS.

Six illustrations, all `aria-hidden="true"`:

1. **Triangle mark — 18 × 18, `viewBox="0 0 24 24"`.** One solid path, `M12 2 2.5 21h19L12 2Z` — a filled isosceles triangle standing on its base, i.e. a prism seen edge-on. Filled `url(#pz-mk)`: a `linearGradient` with `x1="0" y1="0" x2="1" y2="1"` and three stops — `#22d3ee` at 0, `#8b5cf6` at 0.5, `#fb7185` at 1. **Define that gradient once**, in the nav SVG's `<defs>`; the footer mark is the same 18 × 18 path referencing the same `id`. Both marks live in one document, so one definition serves both — do not duplicate the `id`.
2. **Eyebrow dot — 6 × 6 CSS**, `border-radius: 50%`, `background: linear-gradient(135deg,#22d3ee,#a855f7)`.
3. **Three feature glyphs — 21 × 21, `viewBox="0 0 24 24"`, `fill: #5b4bd8`**, one path each, in this order:
   - *Refraction engine* — `M12 3 3 20h18L12 3Zm0 5.4L17.1 18H6.9L12 8.4Z`. An **outlined** triangle: an outer triangle with a smaller concentric triangle knocked out of it. The two subpaths wind in opposite directions, so the default nonzero fill leaves the middle hollow — reverse either subpath and the glyph goes solid.
   - *Depth that survives export* — `M4 7h11v11H4V7Zm3-3h11v11h-2V6H7V4Z`. Two 11 × 11 squares offset three units apart: the front one solid, the rear one reduced to its top and right arms, so they read as two stacked panels with depth between them.
   - *Tokens, not screenshots* — `M7 4h10v2H9v5h7v2H9v5h8v2H7V4Z`. A blocky capital-E form: a two-unit spine with a full-width top arm, a shorter middle arm and a full-width bottom arm — a token list.
4. **Hero pane.** A glass frame wrapping an inner surface. The title bar carries three 10px circles in `#fb7185`, `#fbbf24`, `#34d399`, left to right, over a `1px solid rgba(255,255,255,.42)` bottom rule. The left rail is six 9px-tall bars at `border-radius: 5px` on `rgba(65,58,88,.16)`, widths **82 / 60 / 71 / 48 / 66 / 60 %** in that order, `gap: 9px`, separated from the stage by a `1px solid rgba(255,255,255,.42)` right border. The stage holds one spectrum swatch: `width: 100%; max-width: 420px; aspect-ratio: 2.1/1; border-radius: 14px`, filled `linear-gradient(100deg,#818cf8,#22d3ee 26%,#86efac 48%,#fde047 68%,#fb7185 100%)`, `filter: blur(2px)`, `opacity: .85`, `box-shadow: 0 20px 50px -22px rgba(56,42,120,.7)`. Six bars, three dots, one swatch — no other content, no text.
5. **Pricing tick — `viewBox="0 0 16 16"`**, `width: 16px; height: 16px; margin-top: 2px; flex: none`, path `M2.5 8.5 6 12l7.5-8`, `fill: none`, `stroke: currentColor`, `stroke-width: 2`, round caps and joins; the element's `color` is `#6d5bf0`.
6. **CTA arrow — 15 × 15, `viewBox="0 0 24 24"`**, `fill: none`, path `M5 12h13M12 5l7 7-7 7`, `stroke: currentColor`, `stroke-width: 2`, round caps and joins. It appears in the hero primary CTA only.

## Layout

Content column `max-width: 1120px`, `padding: 0 20px` — 1080px of content from 1120px up. Sections are `96px` vertical padding, dropping to `70px` at ≤420px. The whole page runs to roughly three viewport heights on a 1080px-tall screen — a sanity figure, not a target; do not pad whitespace to reach it.

**Stacking, in full.** `.pz-root` sets `isolation: isolate` and `overflow-x: hidden`. Five fixed or positioned layers, bottom to top:

| z-index | Layer |
|---|---|
| — | `.pz-root` background: the three page-ground gradients |
| 0 | `.pz-field` — the drifting blobs |
| 1 | `.pz-veil` — `position: fixed; inset: 0; pointer-events: none; background: rgba(250,249,252,.22)` |
| 2 | `.pz-grain` |
| 3 | `.pz-wrap` — all content |
| 40 | `.pz-nav` |

The veil is load-bearing, not decoration: it is what lets body copy sit directly on the field and still clear 4.5:1. The arithmetic is in Motion & accessibility. Every glass panel's `backdrop-filter` samples the veil and grain along with the blobs, because both are painted behind it.

**Clip the blobs at the field, not at the root.** `.pz-field` carries its own `overflow: hidden`, which is what contains the five oversized circles. `.pz-root`'s `overflow-x: hidden` cannot do that job: the field is `position: fixed` and `.pz-root` establishes no containing block for it (no `transform`, `filter`, `contain` or `will-change`), so fixed descendants escape an ancestor's overflow clip. Keep both declarations, but rely on the field's.

1. **Floating nav** — fixed, `top: 16px`, centred via `left: 50%; transform: translateX(-50%)`, `width: min(1080px, calc(100vw - 24px))`, a glass pill (`border-radius: 999px`, `padding: 9px 10px 9px 20px`, `display: flex; align-items: center; gap: 20px`). Left to right: mark + wordmark (with `margin-right: auto`), the three links, the gradient CTA. `.pz-links` is `display: none` below 900px.

   **The nav is 16px wider than the content column below 1120px** and exactly aligned above it. At ≥1120px both resolve to 1080px, so the pill's edges sit flush with the copy. Below 1120px the column is `100vw − 40px` and the pill is `100vw − 24px`, so the pill overhangs by 8px each side — intentional, and it never causes horizontal scroll because the value is a `min()` cap.
2. **Hero** — `min-height: 100svh`, flex column, `justify-content: center; align-items: center; text-align: center`, `padding: 150px 0 90px` (`132px 0 70px` at ≤420px). Order: eyebrow pill → H1 → lede → two CTAs (`gap: 12px`, `margin-top: 38px`, wrapping) → the hero pane (`margin-top: 60px`, `width: 100%; max-width: 900px`, `padding: 14px`, `border-radius: 30px`).
3. **Material** — kicker → H2 → sub → `.pz-cards`, `grid-template-columns: repeat(3,1fr)`, `gap: 18px`, `margin-top: 44px`. Each card: `.pz-glass`, `padding: 30px 26px 32px`, `overflow: hidden`, a 44px icon well (`border-radius: 14px`, `display: grid; place-items: center`, `background: rgba(255,255,255,.62)`, `box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 6px 16px -8px rgba(38,26,72,.4)`), H3, body.
4. **Pricing** — kicker → H2 → a single glass frame (`padding: 14px`, `border-radius: 30px`, `margin-top: 46px`) containing `.pz-tiers`, `repeat(3,1fr)`, `gap: 12px`. Each tier: `border-radius: 20px`, `padding: 28px 24px 30px`, `background: rgba(255,255,255,.34)`, `box-shadow: inset 0 0 0 1px rgba(255,255,255,.5)`. The featured tier overrides to `linear-gradient(160deg,rgba(255,255,255,.72),rgba(233,229,255,.55))` with `inset 0 0 0 1px rgba(255,255,255,.85), 0 20px 44px -22px rgba(70,52,160,.55)`. Feature list `gap: 11px`, `margin: 22px 0 26px`; button full width, `padding: 13px`.
5. **Footer** — `.pz-glass`, `margin: 40px 0 34px`, `padding: 40px 34px 30px`, `border-radius: 30px`. Grid `1.6fr repeat(3,1fr)`, `gap: 32px`. The bottom row is `margin-top: 34px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,.5)`, flex with `justify-content: space-between`.

**Column arithmetic at the four widths that matter.** Everything on this page keys off the same 1120px content column and fractional grids, so nothing can diverge and collide the way a fixed-width object beside a fractional column does:

| Viewport | Content column | Feature card | Tier | Footer brand column |
|---|---|---|---|---|
| 1440px | 1080px | (1080 − 36) / 3 = **348px** | (1052 − 24) / 3 = **342.7px** | (1012 − 96) × 1.6/4.6 = **318.6px** |
| 1280px | 1080px | 348px | 342.7px | 318.6px |
| 1080px | 1040px | (1040 − 36) / 3 = **334.7px** | (1012 − 24) / 3 = **329.3px** | (876) × 1.6/4.6 = **304.7px** |
| 768px | 728px | (728 − 36) / 3 = **230.7px** | (700 − 24) / 3 = **225.3px** | (564) × 1.6/4.6 = **196.2px** |

The tier width is measured inside the pricing frame's own 14px padding, which is why it trails the card width by ~6px; the footer column is measured inside the footer's `34px` side padding and after three 32px gaps, which is why it trails both. At 768px the tier is 225px and the widest feature line, `Version history for 12 months`, sets to two lines at `.92rem` — expected. At ≤900px the card and tier grids both collapse to one column, so 768px above is a check on the *pre-collapse* geometry only; the real 768px page is single-column.

**Mobile.** At ≤900px: cards and tiers go to one column, the footer to `1fr 1fr`, nav links hide, the hero pane's `grid-template-columns` collapses to `1fr` and the left rail is `display: none`. At ≤420px: nav padding `8px 8px 8px 16px`, mark `11px / .12em`, nav CTA `8px 14px / 12.5px`, hero padding `132px 0 70px`, buttons `14px 22px / 14px`, the hero CTA row switches to `flex-direction: column; align-items: stretch` with centred button content, footer grid one column at `gap: 24px`, sections `70px`.

## The technique

**The drifting colour field.** A `position: fixed` container inset `-18vmax` on all sides, `z-index: 0`, `overflow: hidden`, `pointer-events: none`, holding **five** absolutely-positioned circles. Each is `border-radius: 50%`, `filter: blur(80px)`, `opacity: .72` (blob 4 → `.55`, blob 5 → `.45`), `will-change: transform`. Sizes in `vmax` so they scale with the viewport's long edge:

| Blob | Size | Anchor | Fill | Drift |
|---|---|---|---|---|
| 1 | `46vmax` | `left:2%; top:4%` | `radial-gradient(circle at 34% 32%, #a78bfa 0%, #7c5cf0 46%, rgba(124,92,240,0) 72%)` | `pz-d1` 34s |
| 2 | `40vmax` | `right:0%; top:-4%` | `radial-gradient(circle at 60% 40%, #67e8f9 0%, #22b8d8 48%, rgba(34,184,216,0) 74%)` | `pz-d2` 41s |
| 3 | `44vmax` | `left:24%; bottom:-8%` | `radial-gradient(circle at 45% 55%, #ffb4a2 0%, #ff7d6b 46%, rgba(255,125,107,0) 73%)` | `pz-d3` 47s |
| 4 | `32vmax` | `right:12%; bottom:6%` | `radial-gradient(circle at 50% 50%, #f0abfc 0%, #c084fc 50%, rgba(192,132,252,0) 75%)` | `pz-d4` 38s |
| 5 | `28vmax` | `left:44%; top:32%` | `radial-gradient(circle at 50% 50%, #fde68a 0%, #fca5a5 55%, rgba(252,165,165,0) 78%)` | `pz-d5` 52s |

The anchors are percentages of the *inset* container, not the viewport, and that is what keeps the two dark blobs off the copy. Worked at 1440 × 1080 (`vmax` = 14.4px, so the field box is 1958.4 × 1598.4 at −259.2, −259.2): blob 1's gradient centre lands at viewport (5px, 17px) — the top-left corner — and its `#7c5cf0` stop, at 46% of a 331.2px radius, reaches only x ≈ 158px, just short of the content column's left edge at x = 160px. Blob 5's centre lands at (804px, 454px), squarely behind the hero, which is why blob 5 is the palest of the five and runs at `opacity: .45`. If you re-anchor a blob, redo this arithmetic and re-check the contrast table below.

Every drift is `ease-in-out infinite` on `translate3d` + `scale` only — never on `left/top`, so the whole field stays on the compositor. The durations are deliberately co-prime-ish (34/41/47/38/52s) so the composition never visibly loops. Exact keyframes:

- `pz-d1`: 0/100% `translate3d(0,0,0) scale(1)` · 33% `translate3d(16vmax,10vmax,0) scale(1.16)` · 66% `translate3d(6vmax,22vmax,0) scale(.9)`
- `pz-d2`: 0/100% `translate3d(0,0,0) scale(1.05)` · 40% `translate3d(-20vmax,14vmax,0) scale(.86)` · 70% `translate3d(-8vmax,30vmax,0) scale(1.2)`
- `pz-d3`: 0/100% `translate3d(0,0,0) scale(.95)` · 35% `translate3d(18vmax,-16vmax,0) scale(1.18)` · 68% `translate3d(-14vmax,-6vmax,0) scale(1)`
- `pz-d4`: 0/100% `translate3d(0,0,0) scale(1)` · 50% `translate3d(-16vmax,-22vmax,0) scale(1.25)`
- `pz-d5`: 0/100% `translate3d(0,0,0) scale(1)` · 45% `translate3d(14vmax,18vmax,0) scale(1.3)` · 80% `translate3d(-12vmax,8vmax,0) scale(.85)`

**Grain.** Because 80px-blurred gradients band on 8-bit displays, a fixed `z-index: 2` overlay sits above the veil: `pointer-events: none`, `opacity: .5`, `mix-blend-mode: overlay`, two stacked `radial-gradient(rgba(255,255,255,.55) .5px, transparent .6px)` and `radial-gradient(rgba(20,16,33,.28) .5px, transparent .6px)` at `background-size: 3px 3px, 4px 4px` and `background-position: 0 0, 1px 2px`. No image file, no noise texture. Because `.pz-root` sets `isolation: isolate`, the overlay blend group stops at the root and cannot reach a host page.

**The glass primitive.** One class, applied to nav, hero pane, hero ghost button, the three cards, the pricing frame and the footer:

```css
.pz-glass{
  position:relative; border-radius:26px;
  background:var(--pz-glass);
  -webkit-backdrop-filter:blur(24px) saturate(180%);
          backdrop-filter:blur(24px) saturate(180%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.78),   /* top inner highlight */
    inset 0 -1px 0 rgba(255,255,255,.22),  /* bottom bounce */
    0 1px 2px rgba(38,26,72,.05),          /* contact */
    0 18px 44px -18px rgba(38,26,72,.30),  /* mid lift */
    0 42px 90px -50px rgba(38,26,72,.45);  /* long ambient */
}
```

The three-tier shadow (contact → lift → ambient) is what makes a translucent panel read as a solid object rather than a tint. Always ship the `-webkit-` prefix first; Safari still needs it.

Three elements override the 26px radius, and the overrides must come **after** `.pz-glass` in the stylesheet — same specificity, so source order decides: `.pz-nav` → `999px`, the hero pane and pricing frame and footer → `30px`, the hero ghost button → `16px`. The chromatic ring inherits whatever radius wins.

**The chromatic edge.** A refractive ring, not a border — a `::before` at `inset: 0`, `border-radius: inherit`, `padding: 1.4px`, `pointer-events: none`, filled with `linear-gradient(142deg, rgba(125,211,252,.95) 0%, rgba(255,255,255,.20) 34%, rgba(255,255,255,.10) 58%, rgba(249,168,212,.90) 100%)` and masked down to just the padding band:

```css
-webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
        mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite:xor; mask-composite:exclude;
```

At `142deg` the gradient runs top-left → bottom-right, so **cyan enters at the top-left corner and magenta exits at the bottom-right** — dispersion read as a two-stop hue shift around the rim. Reverse the angle and the ring reads as a lighting error rather than refraction.

**Pointer specular on cards.** Each card carries an `::after` at `inset: 0`, `border-radius: inherit`, `pointer-events: none`, `opacity: 0`, `transition: opacity .4s`, painted:

```css
background:radial-gradient(340px circle at var(--mx,50%) var(--my,0%),
  rgba(255,255,255,.75),rgba(186,230,253,.30) 38%,transparent 70%);
```

A plain React `onMouseMove` prop writes the two custom properties from the card's own rect:

```js
const r = el.getBoundingClientRect();
el.style.setProperty("--mx", `${((e.clientX - r.left) / r.width) * 100}%`);
el.style.setProperty("--my", `${((e.clientY - r.top) / r.height) * 100}%`);
```

**The pool sits under the cursor, 1:1, with no damping and no offset** — it is a highlight the pointer carries, not an object that leads or lags it. Hover also lifts the card `translateY(-8px)` over `.38s cubic-bezier(.2,.7,.3,1)` and swaps the shadow to `inset 0 1px 0 rgba(255,255,255,.9), 0 34px 70px -26px rgba(38,26,72,.42)`, and `:hover::after` takes `opacity` to 1. Because the `::after` inherits the card's 26px radius, the pool is clipped to the corner curve.

**The fallback values are the rest pose.** `--mx: 50%` and `--my: 0%` place the pool at the top edge, horizontally centred — a plain top-lit panel. That is what a touch device sees, because `mousemove` never fires there and `:hover` never resolves, so the `::after` also stays at `opacity: 0`. It is also what a pointer user sees for the first frame of a hover before the first move event lands. Do not "improve" the fallbacks to `50% 50%`: a pool centred in the card at zero opacity is invisible, but it makes the first frame of every hover slide the highlight up from the middle instead of down from the edge.

**Mandatory fallback for absent `backdrop-filter`.** It is unsupported or force-disabled often enough that translucent-white-on-white becomes unreadable. Ship:

```css
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))){
  .pz-glass   { background:rgba(255,255,255,.86); }
  .pz-nav     { background:rgba(255,255,255,.92); }
  .pz-eyebrow { background:rgba(255,255,255,.90); }
}
```

The nav gets a heavier fill than the panels because it overlaps the busiest part of the blob field. The eyebrow is in the list because it carries its own `blur(12px)` and its own `rgba(255,255,255,.45)` fill and is **not** a `.pz-glass` element — leave it out and it is the one label on the page that stays translucent when the branch fires. Measured in that branch, `--pz-body` on the `.86` fill is **9.4:1** over the darkest blob and **10.4:1** over the pale ground; `--pz-faint` on the `.90` eyebrow fill is **8.6:1**. Nothing washes out.

## Motion & accessibility

- **Scroll reveal** — elements carrying `.pz-rise` start at `opacity: 0; transform: translateY(28px)` and transition both over `.85s cubic-bezier(.2,.7,.3,1)` to `opacity: 1; transform: none`. Eight targets carry the class and only these: the eyebrow, the H1, the lede, the CTA row, the hero pane, each of the two section heading groups' kicker/H2/sub nodes (six nodes across the two sections — the kicker, H2 and sub are separate targets), the three cards, the pricing frame and the footer.
- **Observe with a root that has real height** — `{ rootMargin: "0px 0px -12% 0px", threshold: 0.12 }` — and handle each entry independently: on `isIntersecting`, add the class and `unobserve` that target, then `io.disconnect()` on unmount. Nothing in the callback depends on the order entries arrive in, which matters because entry order is not sorted by position: a fast scroll delivers several entries in one callback and reading them positionally would reveal the wrong element.
- **Interaction timings** — nav links `background .22s, color .22s`; nav CTA `transform .22s, box-shadow .22s` with hover `translateY(-1px)`; buttons `transform .24s cubic-bezier(.2,.7,.3,1), box-shadow .24s` with hover `translateY(-2px)`; cards `.38s` on transform and shadow; tiers `background .3s, transform .3s` with hover `translateY(-4px)`; tier buttons `.24s`; specular fade `.4s`.
- **`prefers-reduced-motion: reduce` resolves to exactly one behaviour.** The effect reads the query first, adds `.pz-in` to every `.pz-rise` node synchronously, and **returns before the observer is ever constructed**. The same branch handles `typeof IntersectionObserver === "undefined"`. In CSS the same media block kills every blob animation and pins **all five** blobs to a static transform, so the composition still reads as arranged rather than stacked at their origins:

  ```css
  .pz-b1{transform:translate3d(8vmax,6vmax,0) scale(1.08)}
  .pz-b2{transform:translate3d(-10vmax,8vmax,0) scale(1.02)}
  .pz-b3{transform:translate3d(9vmax,-8vmax,0) scale(1.06)}
  .pz-b4{transform:translate3d(-8vmax,-10vmax,0) scale(1.1)}
  .pz-b5{transform:translate3d(9vmax,11vmax,0) scale(1.18)}
  ```

  Blob 5 is on this list. Omit it and one of the five sits at its untransformed origin while the other four are composed — a visible difference between the reduced-motion page and the animated one at t = 0, and the reason to state five poses rather than four. The same block forces `.pz-rise` to `opacity: 1; transform: none; transition: none`, kills the transitions on `.pz-card, .pz-btn, .pz-tier, .pz-navcta, .pz-tbtn`, and zeroes their hover transforms. That CSS also covers the no-JS case.
- **Honour a mid-session change.** Put the whole effect body in a function, call it once, and subscribe to the media query's `change` event: on change, run the existing cleanup (`io.disconnect()`) and re-run the function. The `useEffect` has an empty dependency array and will not re-run on its own, so a visitor who flips the OS setting mid-session is otherwise stranded in whichever mode the page loaded in. Remove the `change` listener on unmount.
- **Performance: what is static and what is live.** Nothing on this page is recomputed per frame in JavaScript — there is no rAF and no scroll listener. The five blobs are five compositor layers (`will-change: transform`, `translate3d` + `scale` only), so their motion costs no main-thread work. The live cost is the GPU re-sampling `backdrop-filter` for every glass panel intersecting the viewport while the field moves underneath, which is why no glass panel ever animates `filter`, `opacity` or `background` — those would invalidate the backdrop every frame. The card specular is one `getBoundingClientRect()` read plus two custom-property writes per `mousemove`; the handler reads layout and then writes only properties consumed by a `background`, so there is no read/write thrash. Sizes are in `vmax` and grids are fractional, so resize triggers no JavaScript at all.
- **Contrast, measured.** The worst case is body copy sitting directly on the field over blob 1's `#7c5cf0` stop, which is the darkest point the field reaches. Through the `.22` veil that backdrop resolves to `#b09ef4`:

  | Text | On | Ratio |
  |---|---|---|
  | `--pz-body` `#413a58` | veiled blob-1 stop (page floor) | **4.6:1** |
  | `--pz-body` `#413a58` | veiled pale ground | 9.6:1 |
  | `--pz-body` `#413a58` | `.42` glass over the veiled blob-1 stop | 7.5:1 |
  | `--pz-faint` `#4f4766` | `.42` glass over the veiled blob-1 stop | 5.3:1 |
  | `--pz-faint` `#4f4766` | `.45` eyebrow fill over the veiled blob-1 stop | 5.6:1 |
  | `--pz-ink` `#141021` | veiled blob-1 stop | 8.0:1 |
  | Headline gradient, all three stops | veiled blob-5 stop, behind the hero | 3.9–4.6:1 (large text, 3:1 required) |

  Three things follow, and all three are requirements, not suggestions. **The veil is not optional** — remove it and body copy over blob 1 falls to 3.6:1. **Section kickers are `--pz-body`, not `--pz-faint`** — the kicker is the only small label that sits on the raw field, and `--pz-faint` there measures 3.7:1. **The headline gradient's mid stop is `#0e7490`, not a lighter cyan** — at `#0891b2` the emphasised word measures 2.7:1 against the amber blob that sits behind the hero, under the 3:1 floor for large text.
- **Accessibility** — heading order is H1 → H2 → H3 with no skips. The blob field, the veil, the grain layer, the eyebrow dot, the hero pane and every SVG are `aria-hidden="true"`. `<nav aria-label="Primary">`. `:focus-visible` gives `outline: 2px solid #4f46e5; outline-offset: 3px; border-radius: 10px` on both links and buttons. Eighteen focusable elements in DOM order: three nav links, the nav CTA button, two hero CTAs, three tier buttons, nine footer links. The wordmark is a `<span>`, not a link, and is deliberately not in the order.

## Build notes

- React + Tailwind, **zero external packages** — no animation library, no blur library, no noise image. The entire effect is CSS keyframes, `backdrop-filter`, a masked gradient ring and one eight-line pointer handler.
- All styling ships in a single `<style>{CSS}</style>` block inside the component, every selector prefixed `pz-` so the page cannot leak into a host site.
- No `useState` anywhere. The only effect is the IntersectionObserver plus its `matchMedia` `change` subscription; it returns `io.disconnect()` and removes the media listener. The specular handler is a plain `onMouseMove` prop — nothing to remove on unmount, nothing to debounce on resize.
- **Test on a mid-range Android.** Five 80px-blurred layers under live `backdrop-filter` is the one real performance risk here. If it drops frames, drop to three blobs inside a `(max-width: 640px)` block — hide blobs 4 and 5 — rather than lowering the blur radius, which destroys the material. Do not lower `saturate(180%)` either; it is what separates this from a white overlay.

## Acceptance checks

- Scrolling past a glass panel visibly changes the colour *inside* it as different blobs pass behind. If a panel's tint is constant, `backdrop-filter` is not applying and you have shipped a flat overlay.
- Every glass panel shows a cyan-to-magenta rim: cyan concentrated at the top-left corner, magenta at the bottom-right, ring thickness a consistent 1.4px around all four corner radii, at 999px on the nav, 30px on the pane/frame/footer, 26px on the cards and 16px on the hero ghost button.
- Forcing the `@supports not` branch (or disabling `backdrop-filter` in devtools) leaves every string readable, including the eyebrow — measure `--pz-faint` on the eyebrow's fill and `--pz-body` on a card, both over a blob, and both clear 8.5:1.
- Sample the rendered pixel behind the emphasised word `light` and behind a section kicker at 1080px, 1280px and 1440px. Body copy clears 4.5:1 everywhere; the emphasised word clears 3:1 at all three of its gradient stops. Delete the veil and the kicker check fails — that is the test that the veil is present and at `.22`.
- With reduced motion on: nothing moves anywhere, no element is stuck at `opacity: 0`, and **all five** blobs sit at a composed static transform rather than at their origin points. Toggling the OS setting mid-session flips the page to the other mode without a reload.
- Hovering a feature card moves a soft white pool 1:1 with the cursor and lifts the card 8px; the pool is clipped to the card's 26px radius and fades in over `.4s`. Before the first move event, and on any touch device, the pool is invisible and its position resolves to `50% 0%`.
- Tab traversal reaches eighteen elements in DOM order — three nav links, the nav CTA, both hero CTAs, all three pricing buttons, all nine footer links — each with a visible `#4f46e5` ring offset 3px. The wordmark is not among them.
- The hero pane shows six rail bars at 82/60/71/48/66/60 %, three title-bar dots in coral/amber/green, and one blurred spectrum swatch capped at 420px. Below 900px the rail is gone and the spectrum fills the pane.
- The pricing frame shows three tiers; tiers 1 and 3 have three feature lines, tier 2 has four and is the only tier with a solid gradient button.
- At 375px wide there is no horizontal scroll, the hero H1 still sets to exactly two lines broken at the authored `<br>`, and the two hero CTAs are stacked full-width.
- No network request leaves the page for an image, font or script; every graphic is inline SVG or a CSS gradient, and the layout does not shift after first paint.

More like this