Build guide · Sheet A-102

How Sightline was built.

A fictional analytics product, a pinned CSS 3D assembly, and zero images. Everything on the page is DOM, SVG, and two font files. This sheet documents the decisions, the techniques, and the honest iteration log.

Vanilla HTML, CSS, JS No build step No three.js, CSS 3D only Designed by Claude Fable 5, finished by Claude Opus 4.8

01 · Concept

The product fiction

Sightline is product analytics that shows sessions as stories. The pitch writes the art direction: analytics tools bury you in rows, so the site's whole job is to dramatize the moment raw parts become one readable thing. The hero does that literally. A dashboard arrives as six exploded, tilted layers hanging in 3D space like an axonometric drawing, then assembles into a flat, working overview as you scroll.

The mood is precise, architectural, ice-blue daylight. Background #F5F6F8 reads as drafting paper, ink #16181D carries the type, electric blue #2B5CFF is rationed to about one percent of the page: accents, plot points, the assembly spine. Cool gray #C9CED8 does the hairline work. Blueprint grids, registration marks, a stage ruler down the hero track, and a title block in the footer keep the drawing-sheet metaphor honest without turning it into a costume.

Type is Archivo with its variable width axis (stretched to 125% for display, tightened letter-spacing) over Inter for body. Expanded grotesque display plus tabular numerals everywhere data appears. Motion is scroll-mapped and lerped, never eased by default curves: the site should feel like an instrument, not a slideshow.

02 · Techniques

How the tricks work

T·01 Pinned CSS 3D assembly

The hero is a 250vh track with a sticky 100vh pin. One custom property, --e (1 = exploded, 0 = assembled), drives every transform through calc(). Each of the six layers declares its own z-altitude and drift as inline data, so the whole money shot is a ladder of numbers, not a keyframe timeline. --px and --py are the pointer parallax, multiplied by --e so the breathing dies exactly when the stack lands.

/* one variable drives the entire scene */
.stack {
  transform:
    scale(calc(1 - var(--e) * .16))
    rotateX(calc(var(--e) * (42deg + var(--py) * 5deg)))
    rotateZ(calc(var(--e) * -11deg))
    rotateY(calc(var(--e) * var(--px) * 7deg));
  transform-style: preserve-3d;
}
.layer {
  transform: translateZ(calc(var(--z) * var(--e)))
             translateY(calc(var(--dy) * var(--e)));
}
<!-- the z-ladder, in the markup -->
<div class="layer" data-chrome style="--z:-58px; --dy:16px">
<div class="layer" style="--z:40px;  --dy:5px">   <!-- metrics -->
<div class="layer" style="--z:292px; --dy:-36px"> <!-- signals -->

T·02 Lerp scroll engine, no library

A rAF loop reads the track's progress, smoothsteps it, and lerps the live value toward it. The pin never snaps to the scrollbar, it settles. When --e reaches zero the stack gets an is-settled class that removes the 3D transform entirely so text rasterizes crisp again.

const frame = () => {
  const r = track.getBoundingClientRect();
  const p = clamp(-r.top / (r.height - innerHeight), 0, 1);
  const s = p * p * (3 - 2 * p);      // smoothstep both ends
  target = 1 - s;
  e = lerp(e, target, 0.16);           // settle, never snap
  pin.style.setProperty('--e', e.toFixed(4));
  if (e < 0.004) pin.classList.add('is-settled');
  requestAnimationFrame(frame);
};

T·03 Self-drawing SVG chart

The feature chart builds its paths from an inline data array, measures them with getTotalLength, then animates stroke-dashoffset when the card scrolls into view. The tooltip snaps to the nearest data point and reports both series plus the session count behind the number.

const len = curEl.getTotalLength();
curEl.style.strokeDasharray  = len;
curEl.style.strokeDashoffset = len;   // hidden, full length
curEl.style.transition =
  'stroke-dashoffset 1.7s cubic-bezier(.65,.05,.36,1) .15s';

io.observe(wrap);                     // on reveal:
curEl.style.strokeDashoffset = '0';  // the line draws itself

T·04 Crosshair cursor and blueprint lines

Sections tagged data-crosshair swap the cursor for full-viewport hairlines, a square reticle, and a live coordinate readout, all lerped in the same rAF pattern. Hero guide lines draw in on load with the pathLength trick so every line shares one dash length.

<line x1="0" y1="76%" x2="100%" y2="76%"
      pathLength="2000" style="--len:2000"/>
.hero-lines line {
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
  transition: stroke-dashoffset 1.3s var(--ease-io);
}
.is-loaded .hero-lines line { stroke-dashoffset: 0; }

T·05 Real DOM, so it can reflow

The assembling UI isn't a screenshot. It's a 960x620 design space of absolutely positioned cards, so it stays sharp at any size and costs zero image bytes. It also means mobile doesn't need a second dashboard: dropping the layers to display:contents promotes every card to a grid item of .stack, and the same markup reflows into a 2x2 metric grid with the chart under it. Scaling the real thing down to 350px would have rendered 12px UI text at 4px, and cropping to a window sliced the chart legend in half.

/* desktop: one design space, two fitted targets, --e blends them */
.dash-scaler { width: 960px; height: 620px; perspective: 2200px; }
.hero-scene  { transform: scale(calc(
    var(--fit-a) + var(--e) * (var(--fit-e) - var(--fit-a)))); }

/* mobile: collapse the stack, let the cards become the grid */
@media (max-width: 860px) {
  .stack { display: grid; grid-template-columns: 1fr 1fr; }
  .layer, .layer-inner { display: contents; }
  .dash-card { position: static;
               width: auto !important; height: auto !important; }
}

03 · Asset pipeline

Zero images, on purpose

Every visual is procedural. The blueprint grid is layered CSS linear-gradients at two densities. The dashboard is live DOM and inline SVG. Both charts are hand-plotted polylines (the feature chart generates its path from a data array at runtime, the in-dashboard one is authored SVG). Logos for the proof strip are typographic marks with small geometric SVG glyphs, drawn by hand in the markup. The favicon is an inline SVG data URI of the same reticle mark the nav uses.

The compression figure in the story section is worth calling out because it looks like an illustration and isn't. The raw event stream is a repeating-linear-gradient (a 1px tick every 5px) behind a mask that fades both ends. The funnel below it is a single div whose taper is a clip-path polygon. Note that the clip eats any border you put on that box, so the fill has to carry the shape by itself. The five chapter bars are five divs that scale up on a stagger. Zero bytes, and it redraws at any width.

The grain over the whole page is one inline feTurbulence SVG as a data URI, fixed to the viewport. It's deliberately not mix-blend-mode: a blended full-viewport fixed layer forces a full recomposite every frame and would cost the pinned 3D scroll its smoothness. Plain alpha over near-white reads the same and stays cheap.

No stock, no screenshots, no generated raster assets, no icon fonts. The only external requests on the whole site are the two Google Fonts families (Archivo variable width, Inter). Total payload lands well under the 4MB budget.

04 · Recreate it

Prompt to build something like this

ROLE
You're an art director and creative front-end developer who ships
hand-written HTML/CSS/JS. No frameworks, no build step, no images.

TASK
Build a one-page marketing site for a fictional SaaS product whose
hero is a scroll-driven CSS 3D "assembly": a product UI built from
5-6 stacked layers that starts exploded and tilted, then assembles
into a flat screen as the user scrolls a pinned section.

CONTEXT
Product: [name], [one-line pitch]. The metaphor of the hero should
dramatize the product's core promise (parts becoming one clear thing).
Mood: precise, architectural, daylight. Palette: one paper-tone field,
one ink, ONE electric accent under 2% of the page, one hairline gray.
Type: a variable-width grotesque for display (expanded, tight
tracking) plus a neutral body face.

FORMAT
Static files: index.html, styles.css, main.js. Sections: nav, pinned
hero, logo proof strip, product storyboard, features with one live
animated demo (SVG line chart drawing itself, hover tooltip), pricing
(3 tiers, one highlighted), full-bleed final CTA, structured footer.

CONSTRAINTS
- Pin via position: sticky inside a 250vh track. Drive everything
  from one custom property (--e) set by a rAF lerp loop. No libraries.
- Layers: per-layer --z and --dy consumed by calc() transforms.
- The base plate must show a dashed footprint for every part still
  in the air, or the exploded state is a big empty slab.
- Every airborne card needs its altitude in its shadow: scale the
  blur, spread and registration outline by --e.
- Blueprint details: drawn-in hairlines (stroke-dashoffset +
  pathLength), corner registration marks, tabular numerals, a HUD
  readout of assembly progress.
- Mobile: kill the pin. Don't scale or crop the design space, reflow
  it (display:contents on the layers) so cards become grid items.
- Respect prefers-reduced-motion. Zero console errors.
- Copy: contractions, no em-dashes, no buzzwords, no testimonials.

EXAMPLES
Exploded: rotateX(42deg) rotateZ(-11deg), a six-rung z-ladder from
-58px (base plate) to 292px (floating signals). Assembled: the
transform is removed via an is-settled class so text rasterizes
crisp, and one light pass sweeps the sheet to mark it complete.

05 · Iteration log

Three passes, honestly

All three passes are Claude Opus 4.8's work. Fable 5 never got to run one.

Pass 1Structure · Opus 4.8
  • Found a real bug in the money shot. .chrome had inset:0 but no position, so the rule was inert, the box collapsed to 80px, and the assembled dashboard rendered with no radius, no border and no lift shadow. The rounded look in the exploded state was coming from a separate veil that faded out on landing. One line fixed the whole payoff.
  • Fixed the copy handoff. At half assembly the headline was still a 25% ghost with the dashboard sliding over it. Retuned the fade so the copy clears before the sidebar arrives.
  • Fixed two landing collisions: the signals toast came down exactly on top of the header controls, and the plot-point callout lay across the trend line it was annotating. Both now land in clear air, with a dashed leader from the callout to the reticle.
  • Filled the two dead zones. The base plate was a big empty white slab, so it now carries a dashed footprint slot for every part still in the air. The dashboard sidebar ran ~290px of empty white, so it now holds the fix queue the story section links to.
  • Raised the metrics layer from z:40 (was 12) and evened the z-ladder, so the KPI cards read as airborne instead of stuck to the base plate.
  • Feature cards were #FAFBFC on a #fff band, a 1.5% tonal step that read as nothing. Moved them onto the paper field over a blueprint grid, as white cards with real shadows.
  • Rebuilt the proof strip as a drafting register with hairline cells and tick marks, instead of gray words floating in a tall white band.
  • Rebuilt the mobile hero. It was a cropped screenshot slicing the chart legend and callout mid-text; it now reflows via display:contents into a real 2x2 metric grid with the full chart and funnel.
  • Made the footer title block accurate: DRAWN BY / FINISHED BY, which is what a real drawing title block carries anyway.
Pass 2Depth · Opus 4.8
  • Added Fig. 01, a procedural compression figure: a barcode event stream tapers through a drawn funnel into five chapter bars, one flagged, with the 797:1 ratio. It earns the half-empty row beside the story headline and states the product's claim as a picture.
  • Gave every section a sheet rule: a drawn hairline with a blue registration tick and its sheet code (A-101, A-102, A-103). The page now has an architectural cadence, and the section joins have something to look at other than stacked padding.
  • Added a settle flash. When the last layer lands, one light pass reads across the sheet: the drawing is complete. It re-fires if you scroll back up and reassemble.
  • Added a read-through progress hairline on the nav's own bottom edge, because the site is an instrument and should say where in the drawing you are.
  • Richer feature hovers: the corner registration bracket opens from 9px to 20px and a hairline sweeps the card's top edge.
  • Turned the stats ribbon into register cells with hairline dividers and tick marks, matching the proof strip and title block.
  • Shortened the hero track 280vh to 250vh so the pin's tail isn't a huge empty field in a full-page render.
Pass 3Final QA · Opus 4.8
  • Caught a real mobile break the new proof register introduced: six nowrap wordmarks in a 7-cell grid blew the body out to 806px at a 390px viewport. Re-cut to two columns with the borders redrawn.
  • Rewrote every code excerpt in this guide to match the shipped source. They described a 330vh track, rotateX(55deg), --z:274px, perspective:1750px and a --s variable that no longer exists. A build guide that misdescribes its own build is worse than no guide.
  • Verified 390px: no horizontal scroll, tap targets at 44px+, the hero reflow legible at native type sizes.
  • Verified prefers-reduced-motion: the assembly renders statically assembled, the settle flash and figure animations are off, and every reveal lands in its finished state.
  • Console clean on desktop and mobile, on file:// and on the deployed URL. Guide route verified live.

06 · Attribution

Two models built this

Claude Fable 5 wrote the initial design and markup. The art direction, the palette, the type system, the layer stack, the copy, and the first cut of index.html and guide/index.html are Fable 5's work.

Fable 5's usage credits ran out mid-project, before a single iteration pass had run. The site at that point was a draft: never screenshotted, never reviewed, never fixed.

Claude Opus 4.8 finished it. Opus wrote the JavaScript engine (the lerped scroll assembly, the IntersectionObserver reveals, the self-drawing chart and its tooltip, the crosshair cursor, the counters), ran all three iteration passes documented above, and deployed the site. Every fix in that log is Opus 4.8's.

Both models are credited in the footer. That's the whole story, no spin.