Landing Pagestealth/space-bunny-alphaverified 18/18partial cross-check
Upslope
A single market curve draws across a black editorial slide, then a sector marker and eight quarterly ticks resolve around it.
The exact prompt
A. WHAT IS BEING BUILT
Build one self-contained index.html for Moraine, a venture-research house publishing one market curve at a time. The document title must be “Moraine — Upslope”. Set the root language attribute to lang="en". Use a responsive viewport meta tag. The page is a cinematic, single-slide landing page, not a dashboard, scrolling article, chart tool, or application shell.
The desktop experience is locked to one viewport: at widths of 901px and above, set html and body to width:100%, height:100%, and height:100dvh where supported; set body overflow:hidden. The composition must remain visible at 1440×900, 1920×1080, and any desktop viewport whose height is at least 600px. At widths of 900px and below, permit vertical document scrolling and use overflow-x:hidden; never permit horizontal scrolling at 390×844, 768×1024, 1440×900, or 1920×1080.
Use semantic document structure: one header, one main, and one footer. The header contains the Moraine wordmark and slide identifier. Main contains the editorial introduction and the market-chart figure. Footer contains the research descriptor, publication label, and one inquiry link. Include exactly one h1: “Markets move before consensus catches up.” No other h1 element is permitted. The chart labels use div, span, text, or SVG text; they must not become additional headings.
Use semantic nav only if it contains a visible interactive or navigational control; otherwise omit nav rather than adding an empty landmark. Every visible link or button must have an explicit accessible name through its text or aria-label. Every focusable control must display a visible :focus-visible ring.
Do NOT build:
- A responsive dashboard, tooltip system, data table, legend panel, axis cross-selector, date filter, account menu, or chart settings UI.
- More than one chart, more than one curve, multiple series, bars, candlesticks, particles, floating cards, glassmorphic panels, or decorative dashboard chrome.
- Remote imagery, raster images, video, GIFs, base64 assets, icon fonts, third-party scripts, frameworks, CSS libraries, animation libraries, or runtime APIs beyond browser-native JavaScript.
- A continuous looping ambient animation. Motion occurs during the one-time entrance and on one deliberate pointer-triggered redraw.
- A mobile-first stacked chart that exceeds the viewport or creates horizontal overflow.
- A cookie banner, modal, cookie wall, lead-capture form, newsletter form, or secondary navigation.
- A canvas renderer. This concept must use one SVG so the curve remains a vector object and the sector marker remains inspectable.
- Any copy implying live financial advice, guaranteed returns, or an actual investment recommendation.
B. THE BACKDROP
Create one fixed, full-viewport backdrop element named .slide-backdrop. It is a CSS-painted field, not an image. Its base color is #0B0B0F. Add three fixed radial gradients and one linear wash:
1. An indigo radial gradient centered at 18% 18%, fading from rgba(91,43,217,0.20) at 0% to rgba(91,43,217,0) at 48%.
2. A pink radial gradient centered at 78% 68%, fading from rgba(255,111,181,0.10) at 0% to rgba(255,111,181,0) at 42%.
3. A blue radial gradient centered at 58% 44%, fading from rgba(63,186,255,0.055) at 0% to rgba(63,186,255,0) at 36%.
4. A top-to-bottom linear gradient from rgba(255,255,255,0.025) at 0% to rgba(255,255,255,0) at 34%.
The backdrop has exactly 14 stationary dust points. Implement them as 14 CSS-positioned span elements inside .backdrop-dust. Each point is 2×2px, border-radius:50%, and uses a solid color selected from rgba(255,255,255,0.20), rgba(255,255,255,0.12), or rgba(138,143,156,0.18). Their x coordinates are 4%, 9%, 14%, 19%, 25%, 31%, 37%, 43%, 49%, 55%, 61%, 68%, 76%, and 84%. Their y coordinates are 17%, 73%, 29%, 84%, 51%, 12%, 66%, 37%, 91%, 6%, 78%, 47%, 20%, and 58%. Do not animate these points.
Add a 1px horizontal rail at y=72px using the header’s bottom border. Add a 1px vertical rail at x:5vw from the top rail to the bottom edge at 20% white opacity. The rail is visual structure, not a control.
The hero chart is one inline SVG with viewBox="0 0 1200 620", preserveAspectRatio="none", overflow:visible. It occupies the lower 62% of the desktop viewport. Give its wrapper width:110vw and a left offset of -5vw so the curve and chart bounds bleed beyond the right edge. Do not use a higher-resolution canvas. The SVG coordinate system is the resolution strategy: fixed viewBox coordinates provide a stable 1200×620 drawing surface, while CSS scales it to the viewport. The SVG must have aria-labelledby and a title/description pair.
The chart contains exactly 8 x-axis ticks, one y-axis title, one curve path ghost, one area path, one visible curve path, one sector marker group, one 100×100 halo circle, one 40px crosshair line, one center dot, one numeric label, and one small caption. There are no second paths representing a comparison series.
The resting area fill uses a vertical linear gradient: #5B2BD9 at 0% with 0.22 alpha, #8D4DE4 at 48% with 0.11 alpha, #FF6FB5 at 100% with 0 alpha. The fill extends to y=520 and must fade to fully transparent at the bottom. The visible path uses a horizontal linear gradient from #5B2BD9 at 0% to #FF6FB5 at 100%. The ghost path is 24px wide with stroke rgba(255,255,255,0.09), fill="none", stroke-linecap="round", and stroke-linejoin="round". The visible path is 4px wide with the same cap and join. It must be a smooth cubic path, not a polyline. Use this exact initial path: M -70 492 C 82 475, 145 438, 230 448 S 350 350, 430 374 S 560 282, 650 305 S 760 202, 850 226 S 985 128, 1270 74.
The marker is centered at cx="850", cy="226". Its halo is exactly 100×100 with fill rgba(255,255,255,0.07) and no stroke. A 2px vertical tick drops 40px from the marker center to the x-axis region. The center dot is 10×10 with a radial or linear gradient from #3FBAFF to #536EFB. The marker label is “$18.4B” and its caption is “AI infrastructure · 2031”.
The x-axis baseline is x1="0" y1="520" x2="1200" y2="520", stroke #8A8F9C at 0.34 alpha and width 1. It is divided by 8 evenly spaced ticks at x=0, 150, 300, 450, 600, 750, 900, and 1050. Each tick has a 7px vertical stroke and a label. Labels read “Q1 ’24”, “Q2 ’24”, “Q3 ’24”, “Q4 ’24”, “Q1 ’25”, “Q2 ’25”, “Q3 ’25”, “Q4 ’25”. The labels are 12px and use #8A8F9C at 0.82 alpha. The y-axis title is rotated -90 degrees, positioned at x=34 and y=250, and reads “MARKET VALUE / USD”.
The per-frame animation budget is explicit: the entrance loop must perform no more than 2 style writes per frame, read no layout dimensions, and allocate no objects inside the rAF callback. The only animated values are SVG stroke-dashoffset, SVG opacity, and transform applied to the marker group. The rAF loop must not call getBoundingClientRect, getComputedStyle, querySelector, createElement, or canvas APIs. Cache all elements and path lengths before the loop starts. Use a duration of 1800ms, a 60fps target, and a maximum of 108 entrance frames. The redraw interaction is 900ms with a maximum of 54 frames. Both loops are time-based and must remove their frame request when complete.
C. THE LAYER STACK
Build the page in this exact back-to-front z-index order:
- z-index:0 — .slide-backdrop and its three gradients.
- z-index:1 — .backdrop-dust, containing the 14 stationary points.
- z-index:2 — .vertical-rail, the fixed 1px left guide.
- z-index:3 — .slide-header, positioned at the top 72px rail.
- z-index:4 — .hero-copy, positioned in the upper-left text region.
- z-index:5 — .chart-stage, the SVG’s wide lower-region wrapper.
- z-index:6 — .chart-annotation, only if needed for a non-SVG caption; prefer SVG text.
- z-index:7 — .slide-footer, positioned at the bottom.
- z-index:8 — :focus-visible ring state; the browser ring must appear above neighboring content.
The chart must visually pass behind the copy, but the copy remains above the chart. The chart’s right side may be clipped by the viewport intentionally; the SVG itself must never create document-level horizontal overflow. Keep every z-index explicit.
D. TOKEN TABLE
Define these CSS custom properties on :root with these exact values:
- --bg: #0B0B0F
- --indigo: #5B2BD9
- --pink: #FF6FB5
- --blue: #3FBAFF
- --blue-end: #536EFB
- --muted: #8A8F9C
- --white: #FFFFFF
- --ink: #F4F3F7
- --line: rgba(255,255,255,0.15)
- --line-soft: rgba(255,255,255,0.09)
- --ghost: rgba(255,255,255,0.09)
- --copy-width: 55%
- --header-height: 72px
- --footer-height: 56px
- --page-pad-x: 5vw
- --page-pad-y: 28px
- --hero-top: 116px
- --chart-top: 38vh
- --chart-height: 62vh
- --h1-size: clamp(40px, 4.4vw, 76px)
- --h1-line: 0.98
- --h1-weight: 560
- --body-size: 15px
- --body-line: 1.55
- --eyebrow-size: 11px
- --eyebrow-line: 1.2
- --meta-size: 12px
- --meta-line: 1.35
- --tick-size: 12px
- --rail: 1px
- --focus-ring: #3FBAFF
- --duration-entrance: 1800ms
- --duration-marker: 240ms
- --duration-tick: 420ms
- --ease-editorial: cubic-bezier(0.65, 0, 0.35, 1)
- --ease-ui: cubic-bezier(0.22, 1, 0.36, 1)
Use system fallback stack: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, “Segoe UI”, sans-serif. Load Google Fonts only if the implementation uses it; the page must remain correct if the request fails.
E. THE COMPOSITION
Build the body as follows.
1. Header. The header is fixed or absolutely positioned at inset:0 0 auto 0, height:72px, padding:0 5vw, display:flex, align-items:center, justify-content:space-between, border-bottom:1px solid var(--line), background:transparent. The left wordmark is an anchor or span with exact text “MORaine” in 13px, letter-spacing:0.18em, color:var(--ink), and text-transform:uppercase. The right header text is “RESEARCH / 03” in 11px, letter-spacing:0.16em, color:var(--muted). On desktop, both elements are 32px from the horizontal page padding.
2. Main. Main is position:relative; min-height:100%; padding:116px 5vw 76px. The hero copy is width:55%; max-width:760px; position:relative; z-index:4. Place the eyebrow at 0px below the hero top. Its exact copy is “FIELD NOTE 03 / ARTIFICIAL INTELLIGENCE”. It is 11px, uppercase, letter-spacing:0.18em, color:var(--muted). Place the h1 at margin-top:22px, width:100%, font-size:var(--h1-size), line-height:var(--h1-line), letter-spacing:-0.055em, color:var(--ink), font-weight:var(--h1-weight), and max-width:8.6em. Its exact copy is “Markets move before consensus catches up.” Place the deck 28px below the h1, width:470px, max-width:100%, font-size:var(--body-size), line-height:var(--body-line), color:var(--muted). Its exact copy is “Moraine maps the quiet inflection points shaping the next decade of venture. This curve tracks the infrastructure layer beneath the AI economy.” Place a metadata row 26px below the deck. It is a flex row with gap:18px. The first item reads “UPDATED 18 JUN 2025”. The second reads “8 QUARTERS / 1 SIGNAL”. Both are 11px, uppercase, letter-spacing:0.12em, color:var(--muted).
3. Chart stage. Place the chart stage at position:absolute; left:-5vw; top:38vh; width:110vw; height:62vh; min-height:380px; z-index:5; pointer-events:none. The SVG must fill this stage. The path begins at x=-70 and ends at x=1270 so the visible curve runs off both edges. The area fill is clipped by the SVG bounds and fades before the bottom of the chart. Preserve the x-axis labels at 12px with 90ms stagger.
4. Footer. Position the footer at left:5vw, right:5vw, bottom:24px, height:32px, display:flex, align-items:center, justify-content:space-between, z-index:7. The left exact copy is “MORAINE / A VENTURE-RESEARCH HOUSE”. The right exact copy is “ONE CURVE AT A TIME”. Use 10px, letter-spacing:0.16em, color:var(--muted), uppercase. The footer may include one link after the right copy, but it is optional; if present, use an underlined link labelled “Request the full note” with a visible focus ring.
At desktop widths, the h1 must not overlap the marker or the brightest curve section. The 55% copy measure and 62% chart placement create a deliberate diagonal from upper-left text to upper-right curve endpoint. The chart remains the dominant object: the visible stroke and ghost stroke together must occupy more visual area than any text block.
F. THE UI LANGUAGE
There are no prominent CTA buttons in the default composition. If adding the footer inquiry link, style it as a 11px uppercase text link in var(--ink), text-decoration:underline, text-underline-offset:4px, border-radius:3px, padding:4px 0. Its hover color is var(--pink), and its focus-visible outline is 2px solid var(--focus-ring) with offset:4px. Do not add a filled rectangle button.
The Moraine wordmark may be an anchor only if it links to the current page. Its hover state changes opacity from 1 to 0.72 over 160ms. It must retain a visible focus ring. Do not use a pill-shaped navigation bar. The sector marker is a data annotation, not a control, and must not receive pointer events.
All visible interactive controls use a minimum hit area of 36×36px where their visual content is smaller. Focus treatment is outline:2px solid var(--focus-ring); outline-offset:4px; border-radius:3px. Hover must not move layout; it may change color, opacity, or text-decoration only.
G. MOTION
Create the following named keyframes in CSS:
- draw-curve: from stroke-dashoffset:var(--path-length) to stroke-dashoffset:0.
- reveal-area: from opacity:0 to opacity:1.
- pop-marker: from opacity:0; transform:scale(0.7) to opacity:1; transform:scale(1).
- grow-crosshair: from transform:scaleY(0) to transform:scaleY(1).
- reveal-tick: from opacity:0; transform:translateY(5px) to opacity:1; transform:translateY(0).
- settle-copy: from opacity:0; transform:translateY(12px) to opacity:1; transform:translateY(0).
Set the resting state of all copy, chart, marker, and tick elements to opacity:1 and transform:none before animation classes are applied. The entrance sequence must be:
- .backdrop-dust: no animation.
- .eyebrow: settle-copy, duration:520ms, delay:80ms, easing var(--ease-ui).
- h1: settle-copy, duration:760ms, delay:180ms, easing var(--ease-ui).
- .hero-deck: settle-copy, duration:620ms, delay:300ms, easing var(--ease-ui).
- .hero-meta: settle-copy, duration:520ms, delay:420ms, easing var(--ease-ui).
- .curve-ghost: draw-curve, duration:1800ms, delay:0ms, easing var(--ease-editorial).
- .curve-visible: draw-curve, duration:1800ms, delay:0ms, easing var(--ease-editorial).
- .curve-area: reveal-area, duration:300ms, delay:1500ms, easing linear.
- .sector-marker: pop-marker, duration:240ms, delay:1600ms, easing var(--ease-editorial).
- .crosshair: grow-crosshair, duration:180ms, delay:1600ms, easing var(--ease-ui).
- tick 1: reveal-tick, duration:360ms, delay:1200ms.
- tick 2: reveal-tick, duration:360ms, delay:1290ms.
- tick 3: reveal-tick, duration:360ms, delay:1380ms.
- tick 4: reveal-tick, duration:360ms, delay:1470ms.
- tick 5: reveal-tick, duration:360ms, delay:1560ms.
- tick 6: reveal-tick, duration:360ms, delay:1650ms.
- tick 7: reveal-tick, duration:360ms, delay:1740ms.
- tick 8: reveal-tick, duration:360ms, delay:1830ms.
The marker’s transform origin is its center at 850px, 226px. The crosshair must grow downward from the marker center, not from the SVG top. Set the marker group pointer-events:none.
The chart has one hover behavior. Add a pointerenter listener to the chart stage. Ignore it when the reduced-motion media query matches. On the first valid pointerenter, change the path data to this alternate cubic path: M -70 492 C 78 480, 150 466, 235 420 S 352 392, 440 330 S 558 314, 650 270 S 766 236, 858 174 S 1000 118, 1270 42. Redraw both strokes over 900ms with the same draw-curve timing. Fade the area from opacity:0 to 1 during the first 300ms. The marker remains at cx=850, cy=174 and the label changes from “$18.4B” to “$21.9B”. Do not listen to pointerleave. The redraw occurs once per page lifetime.
H. THE RESPONSIVE TABLE
Use these exact overrides:
At width 901px through 1439px:
- --page-pad-x:5vw
- --hero-top:104px
- --chart-top:38vh
- --chart-height:62vh
- --h1-size:clamp(40px,4.4vw,64px)
- .hero-copy width:55%
- .hero-deck width:430px
- .slide-header height:72px
- .slide-footer bottom:24px
At width 1440px through 1919px:
- --page-pad-x:5vw
- --hero-top:116px
- --chart-top:38vh
- --chart-height:62vh
- --h1-size:76px
- .hero-copy width:55%
- .hero-deck width:470px
- .slide-header height:72px
- .slide-footer bottom:24px
At width 1920px and above:
- --page-pad-x:96px
- --hero-top:124px
- --chart-top:38vh
- --chart-height:62vh
- --h1-size:76px
- .hero-copy width:55%; max-width:820px
- .hero-deck width:470px
- .slide-header height:72px
- .slide-footer left:96px; right:96px; bottom:28px
At width 768px through 900px:
- html and body: height:auto; min-height:100dvh; overflow-y:auto; overflow-x:hidden
- --page-pad-x:32px
- --page-pad-y:24px
- --header-height:64px
- --hero-top:96px
- --chart-top:470px
- --chart-height:520px
- --h1-size:56px
- --h1-line:1.02
- .hero-copy width:76%
- .hero-deck width:510px
- .slide-header padding-left:32px; padding-right:32px; height:64px
- .slide-footer position:relative; left:auto; right:auto; bottom:auto; margin:32px; min-height:40px
- .chart-stage position:relative; left:-24px; top:auto; width:calc(100% + 48px); height:520px; margin-top:0
- main padding:96px 32px 24px
- .slide-footer text remains visible below the chart
At width 390px through 767px:
- html and body: height:auto; min-height:100dvh; overflow-y:auto; overflow-x:hidden
- --page-pad-x:20px
- --page-pad-y:20px
- --header-height:58px
- --hero-top:82px
- --chart-top:420px
- --chart-height:460px
- --h1-size:42px
- --h1-line:1.03
- .hero-copy width:100%
- .hero-deck width:100%; font-size:14px
- .hero-meta display:grid; grid-template-columns:1fr 1fr; gap:8px 14px
- .slide-header padding-left:20px; padding-right:20px; height:58px
- .slide-header right text font-size:9px
- .slide-footer left:20px; right:20px; bottom:18px; gap:12px; align-items:flex-start
- .slide-footer left copy and right copy each width:48%; font-size:9px; line-height:1.45
- .chart-stage position:relative; left:-12px; top:auto; width:calc(100% + 24px); height:460px
- main padding:82px 20px 72px
- .vertical-rail display:none
- .slide-footer position:relative; margin-top:28px; min-height:44px
At height 700px or less and width 901px or more:
- --header-height:60px
- --hero-top:82px
- --h1-size:48px
- .hero-deck font-size:13px; max-width:420px
- .slide-header height:60px
- .slide-footer bottom:14px
- .chart-stage top:36vh; height:64vh; min-height:340px
At height 600px or less and width 901px or more:
- --header-height:54px
- --hero-top:68px
- --h1-size:40px
- .hero-deck font-size:12px; line-height:1.4
- .slide-header height:54px
- .slide-footer bottom:10px
- .chart-stage top:34vh; height:66vh; min-height:320px
I. FAILURE GUARDS
All content must be visible with the page loaded even if JavaScript is delayed or unavailable. Set the base opacity of .eyebrow, h1, .hero-deck, .hero-meta, .curve-ghost, .curve-visible, .curve-area, .sector-marker, .crosshair, and every tick to 1. JavaScript may add the entrance classes, but it must first establish the final resting values, then animate from explicit initial values.
Use a two-rAF fallback before starting the timeline. The first requestAnimationFrame ensures the browser has committed the initial styles. The second requestAnimationFrame starts the entrance sequence. If requestAnimationFrame is unavailable, show the resting state immediately. If a path length is unavailable, use a precomputed fallback of 1550 SVG user units and do not throw.
Wrap initialization in try/catch. On error, set the telemetry error string, reveal the resting state, and do not allow the exception to escape. Add a window error listener before initialization. Never use console.log, console.warn, or console.error. Never leave a stale rAF loop after completion.
In the prefers-reduced-motion: reduce media query, set all animation-duration to 1ms, animation-delay to 0ms, transition-duration to 1ms, and transform to none. Do not perform the hover redraw. Keep all content at opacity:1. The chart must remain readable, with the full visible stroke, area fill, marker, number, and eight ticks already present.
J. THE EXACT JS
Use plain JavaScript at the end of body. The implementation must contain this exact telemetry initialization before any other runtime work:
window.__DEMO__ = { name: 'upslope', ready: false, frames: 0, effects: [], errors: [] };
Add this error listener immediately:
window.addEventListener('error', function (event) {
window.__DEMO__.errors.push(String(event.message || 'Unknown error'));
});
Cache all required elements once:
var root = document.documentElement;
var stage = document.querySelector('.chart-stage');
var ghost = document.querySelector('.curve-ghost');
var visible = document.querySelector('.curve-visible');
var area = document.querySelector('.curve-area');
var marker = document.querySelector('.sector-marker');
var crosshair = document.querySelector('.crosshair');
var ticks = Array.prototype.slice.call(document.querySelectorAll('.axis-tick'));
var number = document.querySelector('.sector-value');
var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var hasRedrawn = false;
var running = false;
var startTime = 0;
var mode = 'entrance';
var pathLength = 1550;
var duration = 1800;
var rafId = 0;
Use this initial path string exactly:
var initialPath = 'M -70 492 C 82 475, 145 438, 230 448 S 350 350, 430 374 S 560 282, 650 305 S 760 202, 850 226 S 985 128, 1270 74';
Use this alternate path string exactly:
var alternatePath = 'M -70 492 C 78 480, 150 466, 235 420 S 352 392, 440 330 S 558 314, 650 270 S 766 236, 858 174 S 1000 118, 1270 42';
Set the SVG ghost and visible path stroke-dasharray and stroke-dashoffset to pathLength. If getTotalLength is available, call it once for each path before the loop and use the larger finite result; otherwise retain 1550. Do not call getTotalLength inside the frame callback.
Set the marker transform-origin to 850px 226px with CSS. Set the crosshair transform-origin to 850px 226px with CSS. Set the area opacity to 0 only after the two-rAF fallback begins, and set the marker opacity to 0 and scale to 0.7 at the same time. Set all tick elements to opacity:0 and transform:translateY(5px) at the same time. Set copy elements to opacity:0 and transform:translateY(12px) at the same time.
Use the following animation function. It must contain no layout reads and no DOM creation:
function animate(now) {
if (!startTime) startTime = now;
var elapsed = now - startTime;
var progress = Math.max(0, Math.min(1, elapsed / duration));
var eased = progress < 0.5
? 4 * progress * progress * progress
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
var offset = pathLength * (1 - eased);
ghost.style.strokeDashoffset = String(offset);
visible.style.strokeDashoffset = String(offset);
if (elapsed >= 240) {
area.style.opacity = String(Math.min(1, (elapsed - 240) / 300));
}
if (elapsed >= 1200) {
for (var i = 0; i < ticks.length; i += 1) {
var tickProgress = Math.max(0, Math.min(1, (elapsed - (1200 + i * 90)) / 360));
ticks[i].style.opacity = String(tickProgress);
ticks[i].style.transform = 'translateY(' + ((1 - tickProgress) * 5) + 'px)';
}
}
if (elapsed >= 1600) {
var markerProgress = Math.max(0, Math.min(1, (elapsed - 1600) / 240));
marker.style.opacity = String(markerProgress);
marker.style.transform = 'scale(' + (0.7 + markerProgress * 0.3) + ')';
crosshair.style.opacity = String(Math.min(1, markerProgress * 1.25));
crosshair.style.transform = 'scaleY(' + Math.min(1, markerProgress * 1.4) + ')';
}
window.__DEMO__.frames += 1;
if (progress < 1) {
rafId = window.requestAnimationFrame(animate);
} else {
running = false;
window.__DEMO__.effects.push('entrance-complete');
}
}
For the two-rAF fallback, call requestAnimationFrame twice before setting startTime and requesting the third frame. The first callback only records that the browser has committed styles. The second callback sets startTime=0, running=true, and requests animate. If reduced motion is true, skip the loop, set every animated element to its final value, push 'reduced-motion', and set ready=true after the first paint.
For hover redraw, use a pointerenter listener on stage. If reduced or hasRedrawn or running, return. Set hasRedrawn=true, mode='redraw', duration=900, startTime=0, pathLength to the cached alternate path length, set ghost.setAttribute('d', alternatePath), visible.setAttribute('d', alternatePath), set area opacity to 0, set marker transform-origin to 850px 174px, set marker transform to scale(0.7), set marker opacity to 0, set crosshair transform-origin to 850px 174px, set number.textContent='$21.9B', and start the same rAF loop. During redraw, use the same eased progress but update the area after 150ms, marker after 640ms, and crosshair after 640ms. Push 'hover-redraw' when complete. Do not restart the copy or tick animations.
After the initial style setup and the first paint, set window.__DEMO__.ready = true. Use requestAnimationFrame once for this readiness assignment so it occurs after the first rendered frame.
---
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)
- The page contains exactly one h1 with the text “Markets move before consensus catches up.” and includes semantic header, main, and footer elements.
- At 1440×900, document.documentElement.scrollHeight is no greater than 900 and document.documentElement.scrollWidth equals document.documentElement.clientWidth; at 390×844, document.documentElement.scrollWidth also equals its clientWidth.
- The chart is one SVG with viewBox="0 0 1200 620", one 24px ghost stroke, one 4px gradient stroke, one area fill, one 100×100 marker halo, and eight x-axis tick labels.
- window.__DEMO__ exists with name "upslope", ready becomes true after the first paint, frames increases during animation, effects contains "entrance-complete", and errors remains an array.
- With prefers-reduced-motion: reduce, the h1, visible curve, area fill, marker, number, and all eight tick labels are visible at opacity 1, and no entrance or hover redraw animation is scheduled.
- 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