Landing Pagexiaomi/mimo-v2.6-proconfirmed cross-check
Vellum
Huge serif type bleeds into view through slowly expanding radial ink masks on warm cotton paper
The exact prompt
Build one complete, self-contained `index.html` for **Vellum** — a luxury letterpress and print studio landing page. Document title: `Vellum — Print That Outlives Us`. `<html lang="en">`. ONE file only: plain HTML + CSS + JS in `<style>`/`<script>`. No frameworks, no bundlers, no external media (no images, no video, no audio, no base64 blobs, no CDN scripts, no fonts but Google Fonts). Every pixel comes from CSS gradients, inline SVG, text, or `<canvas>`. This is the library's light page: warm off-white cotton ground, near-black warm ink, one spot-colour oxide red as a second ink pass.
**Viewport rule:** at `>= 901px` the page is one frame: `html, body { height:100%; overflow:hidden }` and `.page { height:100dvh; overflow:hidden }`. NO SCROLL on desktop. At `<= 900px` the page may scroll vertically. No horizontal overflow at any width — checked at 390x844, 768x1024, 1440x900, 1920x1080.
**Do NOT build:** no video, no WebGL, no Three.js, no Lottie, no GSAP, no jQuery, no Tailwind, no React, no images (remote or base64), no audio, no forms, no pricing tables, no testimonials, no carousel, no cookie banner, no scroll indicator, no desktop hamburger (menu only at `<=900px`), no sections beyond header / hero / footer, no footer links beyond the three specified, no dark mode toggle, no backdrop blur stronger than 14px.
---
## A. WHAT IS BEING BUILT
A single-viewport landing page for a Lisbon letterpress studio called **Vellum**. Warm off-white cotton ground (`#f2ece2`), six soft ink blooms drifting beneath the surface on a low-resolution `<canvas>` upscaled and blurred by CSS, a hairline rule system (96px horizontal / 160px vertical grid at 4–5% ink, one major vertical rule, four corner registration marks), and a paper-grain overlay in multiply blend. The composition carries a mono eyebrow, a giant Fraunces serif H1 in two lines, a lead paragraph, two squared buttons, and a specimen panel with ink swatches. The signature effect is the **ink-bleed reveal**: each H1 glyph is an inline-block span whose radial-mask solid radius animates 0% → 124%, so every letter appears as a blot of ink spreading outward through paper fibre, feathered at the edge, staggered left-to-right.
---
## B. THE BACKDROP — paper, blooms, rules, grain
**B1. `.paper`** — `position:fixed; inset:0; z-index:0; pointer-events:none`, `background-color: #f2ece2` plus four stacked gradients (first listed = frontmost):
1. `radial-gradient(ellipse 70% 52% at 18% 8%, rgba(255,252,246,0.92) 0%, rgba(255,252,246,0.34) 38%, transparent 68%)` — light source, top left.
2. `radial-gradient(ellipse 90% 70% at 82% 96%, rgba(120,100,74,0.16) 0%, rgba(120,100,74,0.06) 42%, transparent 74%)` — warm shade, bottom right.
3. `linear-gradient(168deg, #f6f1e8 0%, #f2ece2 46%, #ece4d7 100%)` — stock tone.
4. `linear-gradient(180deg, rgba(23,19,15,0.05) 0%, transparent 14%, transparent 86%, rgba(23,19,15,0.07) 100%)` — edge burn.
**B2. `canvas#bloom`** — `position:fixed; inset:0; width:100%; height:100%; z-index:1; display:block; pointer-events:none; filter: blur(30px); opacity:0.86; mix-blend-mode: multiply`.
**Resolution trick (critical):** the canvas never renders at device resolution. Backing store `iw = Math.max(160, Math.round(innerWidth * 0.28))`, `ih = Math.max(120, Math.round(innerHeight * 0.28))`; `canvas.width = iw; canvas.height = ih`. Dpr is deliberately NOT applied — the CSS stretch plus the single static `filter: blur(30px)` does all smoothing. At 1440x900 the store is 403x252.
**Offscreen blobs (drawn once at init, never per frame):** two 96x96 canvases, each a single radial gradient from centre (48,48) radius 48:
```
ink : rgba(23,19,15,1) 0%, rgba(23,19,15,0.62) 34%, rgba(23,19,15,0.24) 62%, rgba(23,19,15,0) 100%
oxide : rgba(176,74,42,1) 0%, rgba(176,74,42,0.58) 34%, rgba(176,74,42,0.22) 62%, rgba(176,74,42,0) 100%
```
**The six blooms** — `bx, by` normalised, `r` a fraction of `min(iw,ih)`, `a` the globalAlpha, `P` the drift period in seconds:
```
# blob bx by r a P phase ax ay
1 ink 0.18 0.26 0.52 0.16 23s 0.0 0.06 0.05
2 ink 0.74 0.18 0.46 0.13 31s 1.7 0.05 0.06
3 oxide 0.46 0.72 0.58 0.20 27s 3.1 0.07 0.04
4 ink 0.88 0.62 0.40 0.11 37s 4.4 0.04 0.05
5 oxide 0.12 0.78 0.44 0.15 41s 2.2 0.06 0.05
6 ink 0.60 0.40 0.62 0.09 47s 5.6 0.05 0.04
```
Per frame, `t` in seconds, with `w1 = 6.2832 / P`, `w2 = w1 / 1.37`, `w3 = w1 / 0.61` baked per bloom at init: `x = (bx + ax * Math.sin(t * w1 + phase)) * iw`, `y = (by + ay * Math.sin(t * w2 + phase * 0.6)) * ih`, `s = r * Math.min(iw, ih) * (1 + 0.12 * Math.sin(t * w3))`, then `ctx.globalAlpha = a; ctx.drawImage(blob, x - s/2, y - s/2, s, s)`. No division in the loop.
**Per-frame cost budget:** 1 `clearRect` + 6 `drawImage`. No `createRadialGradient`, no `ctx.filter`, no `ctx.shadowBlur`, no `getBoundingClientRect`, no path building inside the loop. Six blits of ~210x210 onto a 403x252 target, under 1.4 ms, never more than 8 canvas draw calls per frame.
**B3. `.rules`** — `position:fixed; inset:0; z-index:2; pointer-events:none`, background `repeating-linear-gradient(180deg, rgba(23,19,15,0.055) 0 1px, transparent 1px 96px), repeating-linear-gradient(90deg, rgba(23,19,15,0.040) 0 1px, transparent 1px 160px)`, masked so the grid dies at the frame edge with `radial-gradient(ellipse 78% 66% at 50% 44%, #000 0%, #000 40%, rgba(0,0,0,0.30) 72%, transparent 100%)` — set both `-webkit-mask-image` and `mask-image`.
**B4. `.rule-v`** — the one major rule: `position:fixed; left:8.6vw; top:0; bottom:0; width:1px; z-index:2; background: linear-gradient(180deg, transparent 0%, rgba(23,19,15,0.22) 11%, rgba(23,19,15,0.22) 89%, transparent 100%)`.
**B5. `.marks`** — four 18x18 registration crosshairs at `inset: 26px` (all corners), `z-index:2`, `opacity:0.5`, background an inline SVG data URI: a 6.5r circle plus a full cross, `stroke='%2317130F' stroke-opacity='.38' stroke-width='.8'`. Decorative: `aria-hidden="true"`.
**B6. `.sheen`** — `position:fixed; inset:-20% -60%; z-index:3; pointer-events:none; background: linear-gradient(104deg, transparent 0%, rgba(255,255,255,0.40) 46%, transparent 60%, transparent 100%); opacity:0.28; animation: sheen 34s cubic-bezier(0.45,0,0.55,1) infinite`; keyframes translate `-28% → 28%` on X only.
**B7. `.vign`** — `position:fixed; inset:0; z-index:4; pointer-events:none; background: radial-gradient(ellipse 92% 84% at 50% 46%, transparent 0%, transparent 52%, rgba(60,48,34,0.07) 78%, rgba(60,48,34,0.14) 100%)`.
**B8. `.grain`** — `position:fixed; inset:-50%; width:200%; height:200%; z-index:40; pointer-events:none; opacity:0.11; mix-blend-mode: multiply`, background an inline SVG `feTurbulence` data URI (`type="fractalNoise" baseFrequency="0.82" numOctaves="3" stitchTiles="stitch"` + `feColorMatrix type="saturate" values="0"`), `background-size: 180px`, animated `grain 1.05s steps(4) infinite` jittering `transform: translate` through `(0,0) (-1.5%,1%) (1%,-1.5%) (-1%,1.5%)`.
---
## C. LAYER STACK (back → front)
1. `html, body` — `#f2ece2`, forced twice (see Failure Guards).
2. `.paper` — z-index 0, the four gradients.
3. `canvas#bloom` — z-index 1, blurred ink blooms, multiply.
4. `.rules` + `.rule-v` + `.marks` — z-index 2, hairline system, all `pointer-events:none`.
5. `.sheen` — z-index 3.
6. `.vign` — z-index 4.
7. `.page` — z-index 10, `position:relative; display:grid; grid-template-rows: auto minmax(0,1fr) auto; min-height:100dvh` (height:100dvh inside the desktop lock).
8. `header` — z-index 30; `main` — z-index 20; `footer` — z-index 30.
9. `.grain` — z-index 40, above everything, `pointer-events:none`.
10. `.menu` (mobile overlay, only `<=900px`) — z-index 80 when open.
---
## D. TOKEN TABLE (defaults at 1440x900)
```
--paper: #f2ece2
--paper-2: #e9e1d5
--paper-hi: #fbf7f0
--ink: #17130f
--ink-soft: #3a322b
--muted: #6e6357
--muted-2: #9a8e80
--accent: #b04a2a
--accent-2: #c08a2e
--accent-wash: rgba(176,74,42,0.10)
--line: rgba(23,19,15,0.20)
--line-soft: rgba(23,19,15,0.10)
--line-hair: rgba(23,19,15,0.055)
--focus: #b04a2a
--serif: "Fraunces", "Times New Roman", Georgia, serif
--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
--logo: 20px
--logo-sub: 9.5px
--nav: 13px
--eyebrow: 11.5px
--h1: 104px
--h1-lh: 0.94
--lead: 18px
--btn: 12.5px
--btn-h: 44px
--spec-lg: 34px
--spec-md: 18px
--spec-sm: 11.5px
--foot: 11.5px
--track-caps: 0.18em
--radius: 2px
--header-x: 48px
--header-y: 26px
--header-h: 46px
--page-x: 48px
--hero-y: 36px
--hero-gap: 22px
--hero-max: 960px
--aside-w: 300px
--gap-col: 56px
--footer-x: 48px
--footer-y: 24px
--grain-op: 0.11
--ease-out: cubic-bezier(0.16,1,0.3,1)
--ease-ink: cubic-bezier(0.33,0.02,0.28,1)
--enter-dur: 0.9s
--bleed-dur: 0.72s
```
Fonts (Google Fonts ONLY, with the fallback stacks above): one `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>` and one stylesheet `https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300..700&family=IBM+Plex+Mono:wght@400;500&display=swap`. This is the document's only network request.
---
## E. THE COMPOSITION — exact build order
`.page` is the grid container with three rows: `<header>`, `<main>`, `<footer>`.
**HEADER** — `height: var(--header-h); padding: var(--header-y) var(--header-x) 0; display:flex; align-items:center; justify-content:space-between; gap:24px`.
- Left: `a.brand` (href `#`, accessible name "Vellum home"): `vellum` in `var(--serif)`, `font-size: var(--logo); font-weight:500; letter-spacing:-0.015em; color: var(--ink)`, then `<span class="brand-sub">EST. 1974</span>` in `var(--mono)`, `font-size: var(--logo-sub); letter-spacing: var(--track-caps); color: var(--muted-2); vertical-align: super; margin-left:8px`.
- Centre: `nav` with exactly 3 links — `Works`, `Process`, `Stock` — each `href="#"`, `position:relative`, `var(--serif)`, `font-size: var(--nav); letter-spacing:0.01em; color: var(--ink-soft); text-decoration:none`, `display:flex; gap:34px; align-items:center`. Hairline underline `::after { content:""; position:absolute; left:0; right:0; bottom:-6px; height:1px; background: var(--ink); transform: scaleX(0); transform-origin: 0 50%; transition: transform 0.28s var(--ease-out) }`; `:hover::after, :focus-visible::after { transform: scaleX(1) }`.
- Right: `a.cta` — see section F; text `Request a proof`.
**MAIN (hero)** — `display:grid; grid-template-columns: minmax(0,1fr) var(--aside-w); gap: var(--gap-col); align-items:center; padding: var(--hero-y) var(--page-x); max-width: var(--hero-max); margin: 0 auto; width:100%`. Left column `.hero-copy { display:flex; flex-direction:column; gap: var(--hero-gap); min-width:0 }`:
1. `.eyebrow` — `var(--mono)`, `font-size: var(--eyebrow); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--muted)`. Copy: `Letterpress · Hot foil · Hand-set type`, preceded by a 34px `<span class="tick">` hairline (1px tall, `background: var(--line)`, `display:inline-block; vertical-align:middle; margin-right:12px`).
2. `h1` — EXACTLY ONE `<h1>`, `var(--serif)`, `font-weight:400; font-size: var(--h1); line-height: var(--h1-lh); letter-spacing:-0.035em; color: var(--ink); margin:0`, `aria-label="Print that outlives us."`. Two `.line` blocks (`display:block`) separated by one whitespace text node, so `textContent` reads `Print that outlives us.` while each line renders on its own row: Line 1 `Print that`, Line 2 `outlives us.`. Inside each line every glyph is `<span class="ltr">X</span>`; words are wrapped in `<span class="word">` (`display:inline-block; white-space:nowrap`) with a space text node between them. Letters of `outlives` take `ltr--spot` (`color: var(--accent)`) — the second ink pass. Spaces are NOT spans. See section G for the bleed.
3. `.lead` — `var(--serif)`, `font-size: var(--lead); line-height:1.58; letter-spacing:-0.012em; color: var(--ink-soft); max-width: 52ch; margin:0`. Copy: `Vellum sets, presses and binds limited editions by hand — one sheet at a time, on presses that have not stopped running since 1974.`
4. `.actions { display:flex; gap:12px; flex-wrap:wrap }` — `a.btn.btn--ink` text `Request a proof`, then `a.btn.btn--ghost` text `See the stock`.
Right column `aside.specimen` — `border:1px solid var(--line-soft); background: linear-gradient(180deg, rgba(255,255,255,0.52) 0%, rgba(233,225,213,0.30) 100%); border-radius: var(--radius); padding: 22px 22px 18px; display:flex; flex-direction:column; gap:14px`, labelled by `<h2 class="spec-title">Specimen 04</h2>` (`var(--mono); font-size:11.5px; letter-spacing:var(--track-caps); text-transform:uppercase; color:var(--muted); margin:0; font-weight:400`) — an `h2`, never an `h1`, the only other heading. Each `.spec-row` is `display:flex; align-items:baseline; justify-content:space-between; gap:12px`; each `.rule` is `height:1px; background: var(--line-soft); transform-origin:0 50%`:
- Row 1: `<span class="spec-lg">Aa</span>` (`var(--serif); font-size: var(--spec-lg); font-weight:300; letter-spacing:-0.02em; color:var(--ink)`) + right-aligned `<span class="spec-tag">96 pt</span>` (`var(--mono); font-size: var(--spec-sm); color: var(--muted-2); letter-spacing:0.06em`).
- `.rule`.
- Row 2: `<span class="spec-md">Hand-set</span>` at `var(--spec-md)` + tag `24 pt`.
- `.rule`.
- Row 3: `<span class="spec-sm">Cotton · 600 gsm</span>` (`var(--mono); font-size: var(--spec-sm); letter-spacing:0.08em; color: var(--ink-soft)`) + tag `Ed. 90`.
- `.swatches { display:flex; gap:8px }` — 4 chips, each `width:26px; height:26px; border:1px solid rgba(23,19,15,0.18)`, backgrounds `#17130f`, `#b04a2a`, `#c08a2e`, `#e9e1d5`; the row is `aria-hidden="true"` (decorative, not a control).
**FOOTER** — `padding: var(--footer-y) var(--footer-x); display:flex; align-items:center; justify-content:space-between; gap:20px; border-top:1px solid var(--line-soft)`. Left: `var(--mono)`, `font-size: var(--foot); color: var(--muted)`: `© 2026 Vellum Press · Lisbon`.
- Centre: `ul.meta` with exactly 3 `li` — `600 gsm cotton`, `Editions of 90`, `Foil & letterpress` — `display:flex; gap:0; list-style:none; margin:0; padding:0`; each `li` is `var(--mono); font-size: var(--foot); color: var(--muted); padding: 0 14px`, `border-left:1px solid var(--line-soft)` on all but the first and `border-right` on all but the last.
- Right: exactly 3 links — `instagram`, `contact`, `studio` — `href="#"`, `var(--mono); font-size: var(--foot); color: var(--ink-soft); text-decoration:none`, `display:flex; gap:22px`, hover `color: var(--accent)`. Each link's text is its accessible name.
---
## F. THE UI LANGUAGE — squared, letterpress, hairline
Two button treatments, both `border-radius: var(--radius)` (2px — print is square), `height: var(--btn-h); padding: 0 20px; display:inline-flex; align-items:center; justify-content:center; font-family: var(--mono); font-size: var(--btn); letter-spacing:0.06em; text-transform: uppercase; text-decoration:none`, and `transition: background 0.22s var(--ease-out), color 0.22s var(--ease-out), border-color 0.22s var(--ease-out), transform 0.22s var(--ease-out)`.
- `.btn--ink`: `color: var(--paper-hi); border:1px solid var(--ink); background: linear-gradient(180deg, #241d17 0%, #17130f 58%, #0e0b09 100%); box-shadow: 0 14px 34px rgba(23,19,15,0.24), inset 0 1px 0 rgba(255,255,255,0.10)`. Hover `background: linear-gradient(180deg, #302720 0%, #1e1813 58%, #12100d 100%); transform: translateY(-1px)`. Active `transform: translateY(0)`.
- `.btn--ghost`: `color: var(--ink); border:1px solid var(--line); background: linear-gradient(180deg, rgba(255,255,255,0.46) 0%, rgba(233,225,213,0.16) 100%)`. Hover `border-color: var(--ink); background: linear-gradient(180deg, rgba(255,255,255,0.70) 0%, rgba(233,225,213,0.30) 100%); transform: translateY(-1px)`.
- `.cta` in the header: `height:34px; padding:0 14px; font-size:11.5px`, otherwise `.btn--ghost` but `color: var(--accent); border-color: rgba(176,74,42,0.42); background: var(--accent-wash)`; hover `border-color: var(--accent); background: rgba(176,74,42,0.16)`.
Every `a` and `button`: `:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--radius) }`. No default blue ring; no `text-decoration` but the nav hover hairline. `::selection { background: rgba(176,74,42,0.22); color: var(--ink) }`.
---
## G. MOTION — entrance stagger + named keyframes
Resting state first: `.appear { opacity: 1 }`. Entrance: `animation-duration: var(--enter-dur); animation-fill-mode: both; animation-timing-function: var(--ease-out); animation-delay: var(--d, 0s)`. Each animated element carries `.appear`, a modifier class, and an inline `--d`.
The ink-bleed animates a registered custom property driving a radial mask:
```
@property --bleed { syntax: "<percentage>"; inherits: false; initial-value: 124%; }
.ltr { --bleed: 124%; --ox: 50%; --oy: 58%; display:inline-block;
-webkit-mask-image: radial-gradient(circle at var(--ox) var(--oy), #000 0%, #000 var(--bleed),
rgba(0,0,0,0.62) calc(var(--bleed) * 1.14), rgba(0,0,0,0.20) calc(var(--bleed) * 1.30), transparent calc(var(--bleed) * 1.46));
mask-image: identical to -webkit-mask-image }
@keyframes in-bleed { from { --bleed: 0%; opacity: 0.55 } to { --bleed: 124%; opacity: 1 } }
```
`.ltr` also sets `--ox`/`--oy` per glyph from this six-origin cycle, in document order, restarting each line: `(34%,62%) (58%,44%) (46%,70%) (66%,52%) (38%,50%) (54%,66%)`. `.ltr--spot` letters add `+0.22s` to their delay — the oxide pass lands after the black pass.
Stagger table (element → modifier → `--d`):
```
brand appear--fade 0.06s
nav Works appear--fade 0.12s
nav Process appear--fade 0.18s
nav Stock appear--fade 0.24s
header cta appear--rise 0.30s
eyebrow appear--fade 0.34s
h1 line 1 letters appear--bleed 0.46s + i*0.034s (i = 0..8 — 9 glyphs)
h1 line 2 letters appear--bleed 0.62s + i*0.030s (i = 0..10 — 11 glyphs; +0.22s if .ltr--spot)
lead appear--soft 1.18s
btn--ink appear--rise 1.30s
btn--ghost appear--rise 1.38s
specimen panel appear--soft 1.02s
spec row 1/2/3 appear--soft 1.10s / 1.18s / 1.26s
footer left appear--fade 1.46s
footer centre appear--fade 1.52s
footer right appear--fade 1.58s
```
`.appear--bleed` uses `animation-duration: var(--bleed-dur); animation-timing-function: var(--ease-ink)`.
Named keyframes (all end at opacity 1 / identity transform):
```
in-fade from { opacity: 0 } to { opacity: 1 }
in-soft from { opacity: 0; transform: translateY(12px) } to { opacity: 1; transform: none }
in-rise from { opacity: 0; transform: translateY(9px) scale(0.985) } to { opacity: 1; transform: none }
in-bleed from { --bleed: 0%; opacity: 0.55 } to { --bleed: 124%; opacity: 1 }
in-rule from { transform: scaleX(0) } to { transform: scaleX(1) }
sheen 0% { transform: translate3d(-28%,0,0) } 50% { transform: translate3d(28%,0,0) } 100% { transform: translate3d(-28%,0,0) }
grain 0% { transform: translate(0,0) } 25% { transform: translate(-1.5%,1%) } 50% { transform: translate(1%,-1.5%) } 75% { transform: translate(-1%,1.5%) } 100% { transform: translate(0,0) }
```
The `.tick` hairline and every specimen `.rule` use `in-rule` (`transform-origin: 0 50%`), delays matching their parent row. Canvas blooms, `sheen` and `grain` run continuously, independent of the entrance stagger.
---
## H. RESPONSIVE TABLE — exact token overrides
```
>= 1600px:
--h1: 120px --logo: 21px --nav: 13.5px --lead: 19px --btn: 13px --btn-h: 46px
--header-x: 56px --page-x: 56px --footer-x: 56px --hero-max: 1060px
--aside-w: 320px --gap-col: 64px --spec-lg: 38px
>= 1920px:
--h1: 136px --logo: 22px --nav: 14px --lead: 20px --btn-h: 48px
--header-x: 72px --page-x: 72px --footer-x: 72px --hero-max: 1180px
--aside-w: 340px --gap-col: 76px --spec-lg: 42px --header-y: 30px
1280px – 1599px:
--h1: 94px --header-x: 44px --page-x: 44px --footer-x: 44px
--hero-max: 900px --aside-w: 280px --gap-col: 48px
901px – 1279px:
--h1: 72px --lead: 16.5px --header-x: 32px --page-x: 32px --footer-x: 32px
--header-y: 22px --hero-max: 760px --aside-w: 244px --gap-col: 36px --spec-lg: 28px
>= 901px AND max-height 850px:
--h1: 74px --hero-y: 24px --header-y: 18px --hero-gap: 16px
--lead: 16px --spec-lg: 28px --footer-y: 18px
>= 901px AND max-height 720px:
--h1: 58px --hero-y: 14px --header-y: 12px --hero-gap: 12px
--lead: 14.5px --eyebrow: 11px --btn-h: 38px --spec-lg: 24px --spec-md: 16px
--footer-y: 12px --aside-w: 224px
>= 901px (desktop lock, all heights):
html, body { height:100%; overflow:hidden } .page { height:100dvh; overflow:hidden }
<= 900px:
no vh lock; page may scroll; nav becomes a full-viewport overlay menu:
- header keeps brand + burger: <button class="burger" aria-label="Menu" aria-expanded="false">
with three 18x1.5px bars in var(--ink), gap 5px
- .menu { position:fixed; inset:0; z-index:80; background: rgba(242,236,226,0.94);
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); display:flex;
flex-direction:column; align-items:flex-start; justify-content:center;
padding: 0 32px; gap: 26px; transform: translateY(-100%);
transition: transform 0.45s var(--ease-out) } .menu.open { transform: none }
- menu holds the 3 nav links stacked at 22px serif plus the header cta; Escape,
link click, and resize to >900px all close it
- main becomes grid-template-columns: minmax(0,1fr); the specimen aside drops below the
copy with margin-top: 28px; .rule-v is display:none
--h1: 46px --header-x: 24px --page-x: 24px --footer-x: 24px --hero-max: 100%
.grain opacity stays --grain-op: 0.11
<= 560px:
--h1: 34px --lead: 15px --header-x: 20px --page-x: 20px --footer-x: 20px
--btn-h: 42px --spec-lg: 26px --hero-gap: 18px
footer becomes flex-direction:column; align-items:flex-start; gap:10px
.meta li:first-child { padding-left: 0 }; .meta wraps with row-gap 6px
header cta is display:none — it lives inside the menu on mobile
.swatches chips shrink to 22x22; --grain-op: 0.09; .marks is hidden
```
Canvas constants are responsive too: the 0.28 scale stays; at `<=900px` bloom `r` multiplies by 1.15 (fewer pixels, so blooms must read larger) and `a` by 0.85. The lead's `max-width` is `100%` below 901px. Each block above is a width `@media`; the max-height blocks add `and (min-width: 901px)`, desktop only.
---
## I. FAILURE GUARDS — the page is never blank, never broken
1. **Paper-first:** the FIRST CSS rule is `html, body { background:#f2ece2 !important; color:#17130f }`; the body tag carries `style="background:#f2ece2;color:#17130f"`; a later rule restates `html, body { background: var(--paper) }`.
2. **Resting opacity is 1:** every `.appear` element's un-animated computed style is its final state (opacity 1, no transform). `.ltr` spans default to `--bleed: 124%` — a fully open mask — so a failed animation leaves legible type; if `@property` is unsupported, `mask-image` falls back to `none` and the letters show anyway. `fill-mode: both` only holds the FROM state during the delay.
3. **Two-rAF entrance fallback:** after two nested `requestAnimationFrame` ticks, for each `.appear`, if `el.getAnimations` is missing or reports no animation with `playState` `"running"` or `"finished"`, add `.is-in` to every `.appear`. `.is-in { animation:none !important; opacity:1 !important; transform:none !important; -webkit-mask-image:none !important; mask-image:none !important }`.
4. **Canvas fallback:** if `canvas.getContext('2d')` returns null, add `.no-canvas` to `<html>`; CSS hides `canvas#bloom` and paints static blooms on `.paper::after`: `radial-gradient(circle at 18% 26%, rgba(23,19,15,0.07), transparent 34%), radial-gradient(circle at 46% 72%, rgba(176,74,42,0.09), transparent 36%), radial-gradient(circle at 74% 18%, rgba(23,19,15,0.05), transparent 30%)`. The page reads correctly regardless.
5. **Reduced motion:** inside `@media (prefers-reduced-motion: reduce)`: `*, *::before, *::after { animation:none !important; transition:none !important; scroll-behavior:auto !important }`; force `.appear, .ltr, .tick, .rule` to `opacity:1 !important; transform:none !important; -webkit-mask-image:none !important; mask-image:none !important`; freeze `.sheen` at `opacity:0.14` and stop the grain animation; the loop counts frames but repaints nothing.
6. **Contrast:** body copy `#3a322b` on `#f2ece2` (`> 9:1`), muted mono `#6e6357` (`> 5:1`), never lower. No text opacity below 0.86.
7. **No console output** in the happy path: no `console.*`, no thrown exceptions; every canvas and media-query call guarded.
---
## J. THE EXACT JS (one IIFE at the end of body, nothing else)
1. `window.__DEMO__ = { name: 'vellum', ready: false, frames: 0, effects: [], errors: [] };` then `window.addEventListener('error', e => window.__DEMO__.errors.push(String((e && e.message) || e)))`. Push effect names once, in this order: `'ink-bleed-mask'`, `'ink-bloom-canvas'`, `'hairline-rules'`, `'paper-grain'`.
2. **Cache at startup:** `document.documentElement`, `canvas#bloom`, its 2d context, `.burger`, `.menu`, all `.menu a`, `matchMedia('(prefers-reduced-motion: reduce)')`, `t0 = performance.now()`. No `getBoundingClientRect` anywhere in the file; all sizing reads `window.innerWidth` / `window.innerHeight`.
3. **Blob builder:** `makeBlob(r,g,b)` builds a 96x96 canvas with one `createRadialGradient(48,48,0,48,48,48)` (stops `0 → rgba(r,g,b,1)`, `0.34 → 0.62 alpha`, `0.62 → 0.24 alpha`, `1 → 0 alpha`), fills a 96x96 rect, returns it. Called exactly twice (ink `23,19,15`; oxide `176,74,42`), never in the loop.
4. **resize():** `iw = Math.max(160, Math.round(innerWidth * 0.28))`, `ih = Math.max(120, Math.round(innerHeight * 0.28))`, `canvas.width = iw; canvas.height = ih`. Scheduled only by a boolean `resizeQueued` consumed by one rAF; never a timer, never twice per frame.
5. **draw(t):** `ctx.clearRect(0,0,iw,ih)`; for each of the 6 blooms compute `x, y, s` from the section B table with `Math.sin`, set `ctx.globalAlpha = a`, `ctx.drawImage(blob, x - s/2, y - s/2, s, s)`; reset `globalAlpha = 1`. No gradients, filters, shadows, paths.
6. **Loop:** `function step(now){ window.__DEMO__.frames += 1; var paint = !document.hidden && (!reduced || !painted); if (paint) { draw(reduced ? 6.0 : (now - t0) / 1000); painted = true; window.__DEMO__.ready = true; } requestAnimationFrame(step) }`, kicked off once. `ready` becomes true after the first paint; `frames` increments once per frame; a hidden document counts the tick but skips painting.
7. **Entrance fallback:** two nested `requestAnimationFrame` callbacks, then per `.appear` run the `getAnimations` check from guard 3 and add `.is-in` to all if any check fails.
8. **Menu (mobile only):** burger click toggles `.menu.open` and `aria-expanded`; Escape closes; any menu link click closes; the `resize` handler closes the menu above 900px and sets `resizeQueued`.
9. **Reduced motion:** if the media query matches, `reduced = true`, so `draw(6.0)` runs once, `ready` becomes true, and no further pixels change.
No other JS. No libraries, no timers, no `console` calls, no `fetch`. Final file must be under 90 KB.
---
TECHNICAL CONTRACT (non-negotiable):
1. Deliver ONE single file: a complete, valid `index.html`. No build step, no bundler,
no framework, no package install. It must open correctly by double-clicking the file
and also over http://.
2. Vanilla only. No React, no Tailwind CDN, no GSAP, no Three.js, no jQuery, no Lottie.
Plain HTML + CSS + modern JavaScript in <style> and <script> tags.
3. NO external media. No .mp4, no .webm, no .gif, no remote images, no base64 blobs of
photos. Every visual must be generated procedurally with CSS gradients, SVG, or
<canvas>. This keeps the page free to host and impossible to break with a dead link.
4. Google Fonts is the ONLY permitted network request, and it must be declared with a
<link rel="preconnect"> plus a system-font fallback stack so the page still looks
intentional offline. If you prefer, skip it and use a pure system stack.
5. Responsive and correct at 390x844 (phone), 768x1024 (tablet) and 1440x900 (desktop).
No horizontal scrollbar at any of those widths. Nothing may overflow its container.
6. Accessibility: exactly one <h1>; real <header>/<main>/<footer>/<nav> landmarks; every
interactive element is a <button> or <a> with an accessible name; visible :focus-visible
ring; body text contrast ratio >= 4.5:1 against its background.
7. Motion safety: wrap all continuous motion in
`@media (prefers-reduced-motion: reduce)` rules that reduce it to a static or
near-static state. The page must remain fully readable and usable with motion off.
8. Performance: no layout thrash. Cache DOM reads, batch writes inside a single
requestAnimationFrame loop, use transform/opacity for animation, and never allocate
inside the hot loop. Target 60fps on a 2019 laptop. Total file under 90 KB.
9. Runtime telemetry hook (used by the automated verifier — implement it exactly):
at the top of your script define
window.__DEMO__ = { name: '<slug>', ready: false, frames: 0, effects: [], errors: [] };
then set `ready = true` after the first paint of your main effect, increment
`frames` once per animation frame inside your rAF loop, and push the name of each
implemented effect into `effects`. Add a `window.addEventListener('error', ...)`
handler that pushes messages into `errors`. Do not rename these fields.
10. Zero console output in the happy path. No console.log left behind. No uncaught
exceptions, no failed network requests.
RESPONSE FORMAT:
Return ONLY the raw HTML inside a single ```html fenced code block. No explanation, no
preamble, no commentary before or after the fence. The file must be complete and
immediately runnable — never truncate, never write "// rest of code here".Acceptance criteria (15)
- exactly one <h1> whose normalized textContent is 'Print that outlives us.', split into 2 .line elements containing 20 .ltr glyph spans in total
- canvas#bloom paints non-blank content and keeps repainting (pixel hash changes across frames) at 1440x900, and window.__DEMO__.frames increments once per rAF tick
- window.__DEMO__ is initialized with name 'vellum', ready becomes true after the first paint, effects contains 'ink-bleed-mask', 'ink-bloom-canvas', 'hairline-rules' and 'paper-grain', and errors stays empty
- at 1440px and 1920px html/body and .page are locked to a single 100dvh frame with overflow hidden; at 390px and 768px there is no horizontal overflow
- semantic header/nav/main/footer exist with exactly 3 nav links and 3 footer links all carrying accessible names, no network requests occur except Google Fonts, and under prefers-reduced-motion every .ltr glyph is fully visible with no running animations
- window.__DEMO__.ready becomes true within 5s
- window.__DEMO__.frames advances (>= 20 frames in 1.2s)
- window.__DEMO__.errors is empty
- zero console errors and zero page errors
- zero failed network requests
- exactly one <h1> with non-empty text
- header, main, footer landmarks present
- no horizontal overflow at 390 / 768 / 1440 widths
- no horizontal overflow after 2s of animation
- prefers-reduced-motion: reduce leaves the page usable and errors empty