Skip to content
LandingPagePrompts
Landing Pagestealth/space-bunny-alphaverified 18/18partial cross-check

Basalt

A light sweep recalculates the three faces of sixty SVG basalt columns, creating a rippling geothermal field.

The exact prompt

WHAT IS BEING BUILT

Build one complete standalone index.html for a single-viewport cinematic landing page titled “Basalt — Geothermal Energy From Deep Ground.” The document language is en. The document title is “Basalt — Geothermal Energy From Deep Ground.” Include one <header>, one <nav> with an accessible name, one <main>, and one <footer>. Include exactly one <h1> reading “POWER, BUILT FROM THE EARTH.” Do not build a second heading, article, application shell, form, cookie panel, modal, card carousel, dashboard, pricing section, login flow, video, image gallery, or additional page.

Use semantic HTML, plain CSS, and vanilla JavaScript only. Do not use React, Tailwind, GSAP, Three.js, Lottie, a framework, a bundler, a build step, external JavaScript, external images, remote fonts other than Google Fonts, MP4, WebM, GIF, base64 media, or any third-party CDN URL. Every visual pixel must be produced by HTML, CSS gradients, inline SVG, or CSS transforms. Google Fonts is the only permitted network request. Load Inter from https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap and provide the exact system fallback stack.

At viewport widths of 901px and above, set html, body, and the page shell to height: 100dvh and overflow: hidden. The page itself does not scroll. At widths of 900px and below, allow vertical document scrolling while preventing horizontal scrolling. The composition must fit without clipping at 390x844, 768x1024, 1440x900, and 1920x1080. The page must remain readable with JavaScript disabled except for the decorative light sweep.

THE BACKDROP

Create a full-viewport geothermal scene using one inline SVG field containing exactly 60 hexagonal prisms. The field is not a flat honeycomb: every prism has a top rhombus/hex face, one lower-left side face, and one lower-right side face. Use 60 <g class="column"> elements and exactly 180 child face paths: 60 .top, 60 .left, and 60 .right. Generate the complete SVG in JavaScript at startup from numeric geometry; do not include 180 handwritten path elements.

Use a flat-top hex grid with horizontal spacing of 92 SVG units and row staggering of 78 SVG units. Start at x=220, y=250. The field occupies an SVG viewBox of 0 0 1600 1000. Generate rows 0 through 8 with row counts of 6, 7, 7, 7, 7, 7, 7, 7, and 5, totalling 60. Offset every odd row by 46 SVG units. Each top hexagon has radius 46 SVG units, with vertices at angles 0, 60, 120, 180, 240, and 300 degrees. The top face uses points at x±46, y+23, and x, y−39 around its center. Extrude each column downward by a height from 34 to 214 SVG units according to depth. The left face connects the top’s lower-left, bottom, and lower-right vertices to their translated counterparts. The right face uses the lower-right, bottom, and translated lower-center geometry. Use fill-rule="evenodd" where required, but keep all three faces individually addressable.

Make nearer columns taller. Compute depth with a fixed column index-based formula: minimumDepth = 34, maximumDepth = 214, and depth = 214 − round(((row / 8) * 108) + ((columnIndex % 5) * 12)). Add no randomness. Apply a perspective scale from 0.58 in the rear to 1.08 in the front, centered at x=800,y=565. Apply a 6 degree skewX(-6) to the SVG field. Do not use SVG filters on individual columns.

The background is a CSS radial gradient centered at 50% 58%: #17252a at 0%, #0d171b at 48%, #070d10 at 100%. Overlay one linear-gradient(115deg, rgba(16,35,39,.92) 0%, rgba(5,12,15,.18) 44%, rgba(18,31,35,.74) 100%). Add one absolutely positioned .grain layer using a repeating-radial-gradient at 0.34px radius, 7px spacing, rgba(226,232,240,.025) 0, rgba(226,232,240,0) 1px. The grain must be static and pointer-events:none.

Add a moving .light-sweep element across the field. It is a 720px-wide, 1400px-high rotated rectangle with opacity 0, background linear-gradient(90deg, rgba(226,232,240,0) 0%, rgba(226,232,240,.025) 35%, rgba(226,232,240,.10) 50%, rgba(226,232,240,.02) 67%, rgba(226,232,240,0) 100%), transform translate3d(-900px,-80px,0) rotate(18deg), and will-change: transform,opacity. The light is a visual only; it must not intercept pointer events.

The SVG is rendered once with width:100%; height:100%; display:block. Do not use canvas. No canvas resolution is required; state this explicitly in implementation: canvas resolution is not applicable, and there is no canvas element. The per-frame budget is 60 columns, 180 fill updates maximum, one transform update, one opacity update, and zero layout reads. Update only SVG fill values and opacity or CSS custom properties. Do not call getBoundingClientRect, getComputedStyle, querySelector, querySelectorAll, classList manipulation, or DOM creation during an animation frame. Cache all nodes, geometry, colors, and previous values before the rAF loop. Precompute the three face RGB values for 24 light-angle samples in JavaScript before animation begins.

Use no per-item SVG filter, no blur filter on the moving light, and no shadowBlur equivalent. Add foreground haze with two static CSS pseudo-elements: .haze-near is a 760px by 280px radial ellipse at 12% 104% with blur(34px), opacity .22; .haze-far is a 1100px by 260px radial ellipse at 92% 70% with blur(58px), opacity .14. These are the only blur radii used by the backdrop.

THE LAYER STACK

Build the visual stack back to front with these exact z-index values. The fixed page shell has z-index:0 and position:relative; isolation:isolate. The .backdrop has z-index:-5 and position:absolute; inset:0. The .backdrop-gradient has z-index:-4. The .grain has z-index:-3. The #field has z-index:-2. The .haze-far has z-index:-1. The .light-sweep has z-index:0. The .vignette has z-index:1. The header has z-index:10. The main has z-index:5. The footer has z-index:10. The focus treatment must remain above every layer, use outline rather than z-index, and be visible on all controls. No stacking context may put the hero text behind the field.

THE TOKEN TABLE

Define these exact custom properties on :root and use them rather than untracked sizes or colors:

--page-bg: #070d10
--ink: #f4f7f8
--muted: #9aa9ad
--accent: #e2e8f0
--accent-soft: rgba(226,232,240,.14)
--line: rgba(226,232,240,.22)
--panel: rgba(12,23,27,.72)
--panel-solid: #0c171b
--black: #05090b
--teal-deep: #17252a
--teal-mid: #0d171b
--teal-low: #070d10
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
--page-pad-x: clamp(24px, 5.55vw, 120px)
--page-pad-y: 28px
--header-height: 48px
--header-row-height: 32px
--nav-gap: 26px
--hero-max-width: 720px
--hero-top: clamp(104px, 15vh, 160px)
--hero-grid-gap: 26px
--eyebrow-size: 11px
--eyebrow-tracking: .18em
--h1-size: clamp(44px, 5.25vw, 86px)
--h1-line-height: .96
--h1-weight: 600
--body-size: 15px
--body-line-height: 1.6
--body-max-width: 430px
--body-gap: 24px
--action-row-gap: 18px
--button-height: 46px
--button-padding-x: 22px
--button-radius: 2px
--button-font-size: 12px
--button-tracking: .12em
--footer-height: 42px
--footer-gap: 24px
--field-opacity: .92
--field-blur-far: 0px
--field-blur-near: 0px
--vignette-alpha: .34
--sweep-duration: 12s
--sweep-delay: -2.4s
--ease-out: cubic-bezier(.16,1,.3,1)
--ease-in-out: cubic-bezier(.65,0,.35,1)
--ease-sweep: cubic-bezier(.45,0,.55,1)
--duration-page: 900ms
--duration-stagger: 760ms
--duration-sweep: 12000ms

Set box-sizing:border-box on every element. Set body margin:0, min-width:320px, color:var(--ink), background:var(--page-bg), font-family:var(--font-sans), and text-rendering:optimizeLegibility.

THE COMPOSITION

Header is a 100% width flex row with padding:var(--page-pad-y) var(--page-pad-x), height:var(--header-height), align-items:center, justify-content:space-between, and border-bottom:1px solid rgba(226,232,240,.11). The brand is an anchor with aria-label="Basalt home", display:flex, align-items:center, gap:12px, color:var(--ink), text-decoration:none, font-size:14px, font-weight:700, letter-spacing:.2em. Its mark is a 24px square inline SVG with a 14px basalt-colored hexagon and a 2px accent diagonal line. The navigation contains exactly three links: “Approach”, “Systems”, and “Contact”. Each link has a visible accessible name, color:var(--muted), text-decoration:none, font-size:11px, font-weight:600, letter-spacing:.14em, text-transform:uppercase, and a hover color of var(--accent). The right-side header status is a 9px dot plus the exact text “GEOTHERMAL / ACTIVE”, with dot color #8ba8a1.

Main is positioned absolute with inset:var(--header-height) var(--page-pad-x) var(--footer-height), display:grid, align-items:center, pointer-events:none. Main content is pointer-events:auto. The hero is a 7-column grid with the copy block spanning columns 1 through 5 and a 2-column technical readout occupying columns 6 through 7. The hero has max-width:1440px, width:100%, margin:0 auto, transform:translateY(calc(var(--hero-top) * -.16)), and gap:var(--hero-grid-gap).

The hero copy is stacked with gap:var(--body-gap). The eyebrow is one line reading “BASALT / DEEP-HEAT INFRASTRUCTURE”, color:var(--accent), font-size:var(--eyebrow-size), font-weight:600, letter-spacing:var(--eyebrow-tracking), line-height:1.2, text-transform:uppercase. Place a 24px horizontal accent rule before it. The h1 is max-width:720px, color:var(--ink), font-size:var(--h1-size), font-weight:var(--h1-weight), line-height:var(--h1-line-height), letter-spacing:-.055em, text-wrap:balance, and contains one highlighted span using color:var(--accent) and font-weight:400. The exact h1 content is “POWER, BUILT FROM THE EARTH.”

The body paragraph is max-width:var(--body-max-width), margin:0, color:var(--muted), font-size:var(--body-size), line-height:var(--body-line-height), and contains exactly: “Basalt turns the earth’s stable heat into precise, continuous power. We build the systems that make deep geothermal energy dependable at grid scale.” The action row contains two visible controls. The primary anchor text is “EXPLORE THE FIELD” and href="#field". The secondary anchor text is “OUR APPROACH” and href="#approach". Both have height:var(--button-height), padding:0 var(--button-padding-x), border-radius:var(--button-radius), font-size:var(--button-font-size), font-weight:700, letter-spacing:var(--button-tracking), line-height:var(--button-height), text-decoration:none, text-transform:uppercase.

The technical readout is a 2-column grid, align-self:end, padding:18px 0 18px 24px, border-left:1px solid var(--line), gap:18px. It contains four exact labels and values: “DEPTH / 4.8 KM”, “OUTPUT / 120 MW”, “UPTIME / 99.97%”, and “STATUS / COMMISSIONED”. Labels use 9px, 600 weight, .15em tracking, uppercase, and var(--muted). Values use 12px, 600 weight, .06em tracking, and var(--ink). The readout is decorative information, not a control.

Footer is a flex row with padding:0 var(--page-pad-x), height:var(--footer-height), align-items:center, justify-content:space-between, border-top:1px solid rgba(226,232,240,.11), color:var(--muted), font-size:10px, font-weight:500, letter-spacing:.12em, text-transform:uppercase. The left text is “BASALT ENERGY SYSTEMS”. The center text is “BUILT BELOW / DISTRIBUTED EVERYWHERE”. The right text is “© 2025 BASALT”. Each text item has a visible focus state only when interactive; these footer items are not interactive.

THE UI LANGUAGE

The primary control uses background:linear-gradient(135deg,#f4f7f8 0%,#cbd5d9 100%), color:#071015, border:1px solid #f4f7f8, box-shadow:0 8px 24px rgba(0,0,0,.18). Its resting transform is translateY(0), opacity:1. On hover, background:linear-gradient(135deg,#ffffff 0%,#dce5e8 100%), transform:translateY(-2px), box-shadow:0 12px 28px rgba(0,0,0,.26). On active, transform:translateY(0), box-shadow:0 5px 14px rgba(0,0,0,.2). The secondary control uses background:rgba(226,232,240,.04), color:var(--accent), border:1px solid rgba(226,232,240,.42). Its resting transform is translateY(0), opacity:1. On hover, background:rgba(226,232,240,.11), border-color:var(--accent), transform:translateY(-2px). On active, transform:translateY(0). Every visible anchor has a focus-visible outline:2px solid #ffffff, outline-offset:4px, border-radius:2px. The brand and navigation links use a 1px transparent underline at rest and a 1px var(--accent) underline on hover, with focus-visible outline as specified.

MOTION

Use these named keyframes:

@keyframes basalt-rise { from { opacity:0; transform:translate3d(0,22px,0); } to { opacity:1; transform:translate3d(0,0,0); } }
@keyframes basalt-fade { from { opacity:0; } to { opacity:1; } }
@keyframes basalt-scan { 0% { opacity:0; transform:translate3d(-900px,-80px,0) rotate(18deg); } 16% { opacity:.82; } 68% { opacity:.5; } 100% { opacity:0; transform:translate3d(2350px,-80px,0) rotate(18deg); } }
@keyframes basalt-pulse { 0%,100% { opacity:.45; } 50% { opacity:1; } }

Apply the initial page sequence with these exact delays. Header: basalt-fade, duration 760ms, delay 0ms. Brand: basalt-fade, duration 620ms, delay 80ms. Nav links: basalt-fade, duration 620ms, delay 140ms, 170ms, 200ms for Approach, Systems, Contact. Eyebrow: basalt-rise, duration 760ms, delay 260ms. h1: basalt-rise, duration 900ms, delay 340ms. Paragraph: basalt-rise, duration 760ms, delay 460ms. Primary button: basalt-rise, duration 760ms, delay 560ms. Secondary button: basalt-rise, duration 760ms, delay 620ms. Technical readout: basalt-fade, duration 820ms, delay 700ms. Footer: basalt-fade, duration 760ms, delay 820ms. The status dot uses basalt-pulse, duration 1800ms, delay 900ms, iteration-count:infinite.

Animate the light sweep with basalt-scan, duration 12s, delay -2.4s, iteration-count:infinite, timing-function:cubic-bezier(.45,0,.55,1). The field is static in layout; its face colors change through the JavaScript light-angle loop. The loop is the only continuous color animation. In JavaScript, calculate normalizedTime as ((now - startTime) / 12000) % 1, angle as -72 + normalizedTime * 144 degrees, and sampleIndex as round(((angle + 72) / 6) % 24). Interpolate between sample 0 and sample 1 with t equal to the fractional sample position. Set each top, left, and right fill from the cached sample arrays. Set column opacity from a cached depth lookup: rear .42, middle .68, front .92. Do not animate SVG geometry, transform, or filter in the rAF loop.

RESPONSIVE TABLE

Apply the following exact overrides:

At 1920px and wider: --page-pad-x:120px; --page-pad-y:32px; --h1-size:86px; --hero-top:160px; --body-size:15px; --field-opacity:.94.

At 1440px and wider: --page-pad-x:80px; --page-pad-y:28px; --h1-size:76px; --hero-top:136px; --body-size:15px; --field-opacity:.92.

At 901px through 1439px: --page-pad-x:56px; --page-pad-y:24px; --h1-size:64px; --hero-top:112px; --body-size:14px; --field-opacity:.86.

At 768px through 900px: set height:auto, min-height:100dvh, overflow-x:hidden; --page-pad-x:32px; --page-pad-y:20px; --header-height:44px; --footer-height:38px; --h1-size:52px; --hero-top:0px; --hero-grid-template:1fr; --field-opacity:.72; --vignette-alpha:.24. Main is position:relative, inset:auto, min-height:calc(100dvh - 82px), padding:64px 0 48px, align-items:start. The hero is one column, gap:36px. The technical readout becomes a horizontal 4-column row with padding:16px 0 0, border-left:0, border-top:1px solid var(--line), gap:16px. The nav gap is 18px. The footer center text is hidden only through a semantic visually-hidden class, not display:none, so the accessible footer structure remains intact.

At 390px through 767px: set height:auto, min-height:100dvh, overflow-x:hidden; --page-pad-x:22px; --page-pad-y:18px; --header-height:42px; --footer-height:36px; --h1-size:44px; --hero-top:0px; --body-size:14px; --body-line-height:1.55; --field-opacity:.58; --vignette-alpha:.2; --nav-gap:12px; --button-height:44px; --button-padding-x:16px; --button-font-size:10px. Hide the header navigation links and the header status text from layout with a 1px visually-hidden utility, preserving their accessible names and keyboard order; do not use display:none or visibility:hidden. Main has padding:52px 0 36px, min-height:calc(100dvh - 78px). The hero is one column. The technical readout is a 2-column grid with padding-top:18px and border-top:1px solid var(--line). The footer displays only the left and right text with gap:10px; center text is visually hidden. The field remains 1600x1000 viewBox with width:160%; height:100%; left:-30%; top:0%; opacity:var(--field-opacity). The light sweep remains 720px wide and is clipped by overflow:hidden.

At height 760px and below, including 1440x760: --page-pad-y:20px; --header-height:42px; --footer-height:36px; --hero-top:76px; --h1-size:54px; --body-size:13px; --body-line-height:1.45; --body-gap:18px; --action-row-gap:12px. Reduce technical readout padding-top to 12px and padding-bottom to 12px. At height 680px and below, including 390x680: --page-pad-y:16px; --header-height:38px; --footer-height:32px; --h1-size:40px; --hero-top:56px; --body-size:13px; --body-gap:14px; --button-height:40px; --button-padding-x:14px; --footer-font-size:9px. The h1 may wrap to three lines; the hero must not overlap the footer.

FAILURE GUARDS

Set every animated foreground element to opacity:1 and transform:none as its resting CSS state. JavaScript must not make copy disappear. If JavaScript does not execute, the inline SVG field, background, copy, and controls remain visible and static. Initialize telemetry before any other script work with exactly:

window.__DEMO__ = { name: 'basalt', ready: false, frames: 0, effects: [], errors: [] };

Register one window error listener that pushes String(event.message || 'window error') into window.__DEMO__.errors. Do not call console.log, console.warn, console.error, alert, or debuggers. After the first paint, use requestAnimationFrame(() => { window.__DEMO__.ready = true; }); Mark ready only after the first frame has been composited.

Use a two-rAF fallback before starting continuous animation. The first rAF validates the viewport and caches measurements; the second rAF sets ready and starts the loop. If the second rAF is unavailable, set ready true on the first rAF and use a 50ms setTimeout to start the loop. If requestAnimationFrame is missing, set ready true and do not run a continuous loop; retain the static composition. If an individual face update throws, catch the error once outside the loop, push the message to telemetry.errors, stop the loop, and set all face fills to their cached fallback values. Never throw asynchronously.

Under prefers-reduced-motion: reduce, remove the sweep animation, set .light-sweep to display:none, remove all page entrance animations, set all animated foreground elements to opacity:1 and transform:none, set #field opacity:var(--field-opacity), and use one static light angle of 0 degrees for all faces. The reduced-motion static state must be readable, have no continuous JavaScript loop, and retain telemetry ready=true after first paint. The reduced-motion CSS block must include all entrance selectors and the light-sweep selector.

THE EXACT JS

Use this JavaScript structure, preserving the named constants, cached references, geometry, telemetry, guards, and loop semantics:

(() => {
  'use strict';
  window.__DEMO__ = { name: 'basalt', ready: false, frames: 0, effects: ['basalt-rise','basalt-fade','basalt-scan','basalt-pulse','face-light'], errors: [] };
  window.addEventListener('error', event => window.__DEMO__.errors.push(String(event.message || 'window error')));
  const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const svg = document.getElementById('field');
  const namespace = 'http://www.w3.org/2000/svg';
  if (!svg) return;
  const columns = [];
  const faces = { top: [], left: [], right: [] };
  const rowCounts = [6,7,7,7,7,7,7,7,5];
  const hex = radius => Array.from({length:6}, (_, i) => {
    const angle = Math.PI * i / 3;
    return [radius * Math.cos(angle), radius * Math.sin(angle)];
  });
  const make = (tag, className, points) => {
    const node = document.createElementNS(namespace, tag);
    node.setAttribute('class', className);
    node.setAttribute('d', points.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(2) + ' ' + p[1].toFixed(2)).join(' ') + ' Z');
    return node;
  };
  let index = 0;
  rowCounts.forEach((count, row) => {
    for (let col = 0; col < count; col++) {
      const x = 220 + col * 92 + (row % 2 ? 46 : 0);
      const y = 250 + row * 78;
      const depth = 214 - Math.round((row / 8) * 108 + (col % 5) * 12);
      const radius = 46;
      const vertices = hex(radius).map(p => [x + p[0], y + p[1]]);
      const bottom = vertices.map(p => [p[0], p[1] + depth]);
      const group = document.createElementNS(namespace, 'g');
      group.setAttribute('class', 'column');
      const leftPoints = [vertices[4], vertices[5], bottom[5], bottom[4], bottom[3]];
      const rightPoints = [vertices[5], vertices[0], bottom[0], bottom[5]];
      const top = make('path', 'top', vertices);
      const left = make('path', 'left', leftPoints);
      const right = make('path', 'right', rightPoints);
      group.append(top, left, right);
      svg.appendChild(group);
      const opacity = .42 + (row / 8) * .50;
      group.setAttribute('opacity', opacity.toFixed(3));
      columns.push(group);
      faces.top.push(top); faces.left.push(left); faces.right.push(right);
      index++;
    }
  });
  const samples = Array.from({length:25}, (_, i) => {
    const angle = -72 + (i / 24) * 144;
    const radians = angle * Math.PI / 180;
    const lightX = Math.cos(radians);
    const lightY = Math.sin(radians);
    const top = [94 + Math.round(24 * (lightY + 1)), 113 + Math.round(25 * (lightY + 1)), 119 + Math.round(27 * (lightY + 1))];
    const left = [35 + Math.round(21 * (1 - lightX)), 56 + Math.round(24 * (1 - lightX)), 61 + Math.round(26 * (1 - lightX))];
    const right = [55 + Math.round(26 * (1 + lightX)), 78 + Math.round(29 * (1 + lightX)), 83 + Math.round(31 * (1 + lightX))];
    return {top,left,right};
  });
  const applySample = sample => {
    faces.top.forEach(node => node.setAttribute('fill', 'rgb(' + sample.top.join(',') + ')'));
    faces.left.forEach(node => node.setAttribute('fill', 'rgb(' + sample.left.join(',') + ')'));
    faces.right.forEach(node => node.setAttribute('fill', 'rgb(' + sample.right.join(',') + ')'));
  };
  if (reduced) {
    applySample(samples[12]);
    requestAnimationFrame(() => { window.__DEMO__.ready = true; });
    return;
  }
  const start = performance.now();
  let lastIndex = -1;
  const render = now => {
    try {
      window.__DEMO__.frames += 1;
      const time = ((now - start) / 12000) % 1;
      const position = time * 24;
      const current = Math.floor(position);
      const next = Math.min(24, current + 1);
      const amount = position - current;
      if (current !== lastIndex) {
        const a = samples[current], b = samples[next];
        const mix = (x,y) => Math.round(x + (y - x) * amount);
        const sample = {top:a.top.map((v,i)=>mix(v,b.top[i])),left:a.left.map((v,i)=>mix(v,b.left[i])),right:a.right.map((v,i)=>mix(v,b.right[i]))};
        applySample(sample);
        lastIndex = current;
      }
      requestAnimationFrame(render);
    } catch (error) {
      window.__DEMO__.errors.push(String(error.message || error));
    }
  };
  const first = requestAnimationFrame(() => {
    const second = requestAnimationFrame(() => {
      window.__DEMO__.ready = true;
      startLoop();
    });
    if (!second) {
      window.__DEMO__.ready = true;
      startLoop();
    }
  });
  function startLoop() {
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(render);
  }
})();

Do not use any other JavaScript API in the animation loop. Do not add a resize observer, pointer listener, scroll listener, canvas, or media-query listener. The implementation must remain below 90 KB, produce zero console output, have no uncaught exceptions, sustain at least 20 frames per 1.2 seconds at 1440x900, and expose the exact telemetry object.

---

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)

  1. The generated document is one index.html with exactly one h1, semantic header/nav/main/footer elements, no external media, and no framework or bundler dependency.
  2. At 1440x900, the page has height 100dvh with overflow hidden, no horizontal overflow, and the visible copy reads “POWER, BUILT FROM THE EARTH.”
  3. The inline field contains exactly 60 column groups and 180 face paths, each column has one top, one left, and one right face, and the background uses no canvas or external images.
  4. window.__DEMO__ exists with name basalt, ready true after first paint, a frames counter incremented once per animation frame, the five listed effect names, and an errors array; console output remains empty.
  5. At prefers-reduced-motion: reduce, the sweep is absent, all animated content is readable and static, no continuous rAF loop runs, and the page still exposes ready true with no uncaught exceptions.
  6. window.__DEMO__.ready becomes true within 5s
  7. window.__DEMO__.frames advances (>= 20 frames in 1.2s)
  8. window.__DEMO__.errors is empty
  9. zero console errors and zero page errors
  10. zero failed network requests
  11. exactly one <h1> with non-empty text
  12. header, main, footer landmarks present
  13. no horizontal overflow at 390 / 768 / 1440 widths
  14. no horizontal overflow after 2s of animation
  15. prefers-reduced-motion: reduce leaves the page usable and errors empty

More landing page prompts