# Premium "Services" 3D Cube Section — full build guide

**What you'll build:** a premium website *services* section where a realistic, lit 3D cube glides across the screen, rotating through 4 services (AI Consulting / Process Automation / Analytics & Insights / AI Chatbots). As each face turns to the front, a matching content block pops in a *different* spot, a HUD counter and progress dots advance, and the whole thing opens on a neon hero and ends on a "comment to get the guide" call-to-action. The cube idly floats even when you're not scrolling. Pure CSS 3D + GSAP. No build step.

> **You got here by commenting "GLIDE".** Welcome — you typed the word, here's the whole thing: the exact source, the exact images, and the exact code. Build it once, reuse it forever.

## Live demo

Open **`demo.html`** in any browser (it works completely offline) and scroll. The cube glides, rotates and floats; the service blocks swap; the dots fill. That file is the real, working source this guide documents — every snippet below is quoted from it.

## What's in this folder

- **`demo.html`** — the complete working section (HTML + CSS + GSAP in one file).
- **`PROMPTS.md`** — 4 self-contained AI image prompts, one per service face.
- **`stack/solutions/`** — the real source images that produced the demo:
  - `ai-consulting.png`
  - `process-automation.png`
  - `analytics.png`
  - `ai-chatbots.png`
- **`GUIDE.md`** — this file.

> Note: the cube also works with no images at all — until a face image loads, a teal line-icon glyph + label shows as a clean fallback.

## Tech (all free)

- **GSAP 3.13** + **ScrollTrigger**, from jsDelivr:
  ```html
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
  ```
- **Pure CSS 3D transforms** (`transform-style:preserve-3d`, `perspective`, `translateZ(50cqw)`, `container-type:size`) — a real lit cube, not a sprite.
- **Fonts:** Fraunces (serif display headings), Inter (body), JetBrains Mono (eyebrows/labels) — all Google Fonts.
- **No build step.** One HTML file.

---

## How it works

### 1. One `SERVICES` array drives everything

The faces, the content blocks, the dots and the counter all build from one array:

```js
const SERVICES = [
  { icon:'consult',  img:"ai-consulting.png",     title:"AI Consulting",       desc:"Strategic guidance on implementing AI solutions tailored to your business needs.",
    points:["Custom AI strategy","Expert guidance","ROI-focused approach"] },
  { icon:'automate', img:"process-automation.png",title:"Process Automation",  desc:"Automate repetitive tasks and streamline your operations for maximum efficiency.",
    points:["Save 20+ hours/week","Reduce human error","Scale effortlessly"] },
  { icon:'analytics',img:"analytics.png",         title:"Analytics & Insights",desc:"Data-driven insights to make smarter business decisions and predict trends.",
    points:["Real-time dashboards","Predictive analytics","Actionable insights"] },
  { icon:'chat',     img:"ai-chatbots.png",       title:"AI Chatbots",         desc:"24/7 intelligent customer support that learns and improves over time.",
    points:["24/7 availability","Instant responses","Multi-language support"] },
];
```

Each entry: `icon` (which inline SVG glyph to show as fallback), `img` (the face image under `stack/solutions/`), `title`, `desc`, and `points` (the checklist). `N = SERVICES.length` (4 here).

### 2. Three nested motion layers (the heart of the effect)

The cube is wrapped in **three** independent layers, each responsible for one kind of motion. This separation is what makes it look hand-animated:

```html
<div class="sol-cube-wrap"><div class="cube-move" id="cubeMove">   <!-- layer 1: screen travel -->
  <div class="cube-aura"></div>
  <div class="cube-shadow"></div>
  <div class="cube-idle" id="cubeIdle">                            <!-- layer 2: tilt + idle float -->
    <div class="sol-cube" id="cube"></div>                         <!-- layer 3: face-to-front spin -->
  </div>
</div></div>
```

```css
.cube-move{ transform-style:preserve-3d; }   /* screen-travel layer  */
.cube-idle{ transform-style:preserve-3d; }   /* tilt + idle float layer */
.sol-cube{ aspect-ratio:1; transform-style:preserve-3d; width:min(33vw,42vh); container-type:size; }
```

- **`cube-move`** — translated in screen-space (`x`/`y`) to glide the cube to a new anchor per service.
- **`cube-idle`** — holds the 3/4 tilt and the perpetual breathing float.
- **`sol-cube`** — only ever rotates the *active face to the front* (pure `rotateY`, no tilt).

**Why split them?** If one element had to do travel *and* tilt *and* face-spin, the transforms would compound and fight (a `y` translate would corrupt the 3D rotation). Three layers let each animation run on its own element and simply nest — clean, predictable, never jittery.

### 3. The realistic lit cube material

Each `.cface` is built to read as a polished, lit solid — not a flat coloured square. The face uses a curved-light gradient background plus a *five-shadow* inset stack (bevel highlight, bevel shadow, teal edge rim, ambient occlusion, bottom shade):

```css
.cface{ backface-visibility:hidden; overflow:hidden;
  background:
    radial-gradient(135% 105% at 27% 16%, rgba(255,255,255,.11), rgba(255,255,255,0) 44%),
    linear-gradient(155deg, #131b1f 0%, #0a1013 56%, #05090b 100%);
  box-shadow:
    inset 1.5px 1.5px 0 rgba(255,255,255,.07),   /* top-left bevel highlight */
    inset -2px -3px 0 rgba(0,0,0,.55),           /* bottom-right bevel shadow */
    inset 0 0 0 1px rgba(0,207,161,.55),         /* teal edge rim */
    inset 0 0 60px rgba(0,0,0,.4),               /* ambient occlusion */
    inset 0 -60px 100px rgba(0,0,0,.5); }        /* bottom shade */
```

On top, a directional shading gradient (`::after`) and a **sheen sweep** (`::before`, animated left-to-right) give it a wet/polished feel:

```css
.cface::before{ inset:-25%;
  background:linear-gradient(118deg, transparent 40%, rgba(255,255,255,.13) 50%, transparent 60%);
  transform:translateX(-45%); animation:sheen 7s ease-in-out infinite; }
@keyframes sheen{ 0%,100%{transform:translateX(-45%)} 50%{transform:translateX(45%)} }
```

There's also a soft **aura** (radial teal glow behind the cube) and a **contact shadow** (blurred ellipse below it) so it sits in the scene instead of floating flatly.

### 4. The 6 faces — 4 services + 2 brushed-metal caps

The cube is a real 6-sided cube using the same `translateZ(50cqw)` trick (where `50cqw` = half the side, made possible by `container-type:size`):

```css
.cface:nth-child(1){ transform:translateZ(50cqw); }
.cface:nth-child(2){ transform:rotateY(90deg)  translateZ(50cqw); }
.cface:nth-child(3){ transform:rotateY(180deg) translateZ(50cqw); }
.cface:nth-child(4){ transform:rotateY(270deg) translateZ(50cqw); }
.cface:nth-child(5){ transform:rotateX(90deg)  translateZ(50cqw); }
.cface:nth-child(6){ transform:rotateX(-90deg) translateZ(50cqw); }
```

The 4 services take the side faces; the top and bottom get brushed dark-metal **caps** (`.cface.cap`) so the cube reads as a solid object even when tilted:

```js
SERVICES.forEach(s => { /* ... build a side face with image/glyph + label ... */ });
for (let k=0;k<2;k++){ const cap=document.createElement("div"); cap.className="cface cap"; cube.appendChild(cap); }
```

### 5. Face ↔ scroll sync via the `ORI` lookup

Because tilt lives on a *different* layer (`cube-idle`), the inner `sol-cube` only needs a pure rotation to bring each service face to the front:

```js
const ORI = [ {ry:0},{ry:-90},{ry:-180},{ry:-270} ];
```

The scroll timeline rotates the inner cube to `ORI[i].ry` exactly when service block *i* pops in.

### 6. Content blocks that pop in different spots

Each service's spec block is absolutely positioned at a *different* location, always on the opposite side from where the cube travels — so the cube and the text never collide:

```js
const POS = [
  { left:'7vw',  top:'50%', ty:-50 },   // S1 left-center   (cube right)
  { right:'7vw', top:'50%', ty:-50 },   // S2 right-center  (cube left)
  { left:'7vw',  bottom:'14vh' },       // S3 bottom-left   (cube top-right)
  { right:'7vw', top:'16vh' },          // S4 top-right     (cube bottom-left)
];
const CUBE = [ [0.23,0.0], [-0.23,0.02], [0.21,-0.13], [-0.19,0.05] ];  // cube anchor per service
```

Each block is a `.panel.glass` (frosted card). Its inner lines stagger up on entry.

### 7. The idle float (alive even when idle)

So the cube never looks frozen, the `cube-idle` layer gets a fixed 3/4 tilt plus two slow, looping, yoyo tweens on `rotateY`/`rotateX` (and the aura gently pulses):

```js
gsap.set(cubeIdle, { rotateX:-9, rotateY:6 });
gsap.to(cubeIdle, { rotateY:10, duration:7,   ease:"sine.inOut", repeat:-1, yoyo:true });
gsap.to(cubeIdle, { rotateX:-6, duration:5.5, ease:"sine.inOut", repeat:-1, yoyo:true });
gsap.to(".cube-aura", { scale:1.1, duration:6, ease:"sine.inOut", repeat:-1, yoyo:true });
```

**Why these tiny angles?** The float stays inside `ry 6..10`, `rx -9..-6` — always 3D and alive, but the active face always clearly dominates. Never a corner-on, half-and-half view.

### 8. The pinned scroll timeline

One ScrollTrigger pins the `.solutions` section and scrubs the whole sequence. For each service it travels the cube (`cubeMove`), rotates the active face front (`cube`), crossfades the panels, staggers the new lines, and updates the dots/counter:

```js
const tl = gsap.timeline({
  defaults:{ ease:"none" },
  scrollTrigger:{ trigger:".solutions", start:"top top", end:"+=520%", pin:true, scrub:1.2 }
});

for (let i=1; i<N; i++){
  const at = i;
  tl.to(cubeMove, { x:C.x[i], y:C.y[i], duration:.95, ease:"power2.inOut" }, at-0.15);
  tl.to(cube,     { rotateY:ORI[i].ry, duration:.95, ease:"power2.inOut" }, at-0.15);
  tl.to(panels[i-1], { autoAlpha:0, duration:.35 }, at-0.05);
  tl.fromTo(panels[i], { autoAlpha:0 }, { autoAlpha:1, duration:.45 }, at+0.05);
  tl.fromTo(linesOf(i), { y:22, autoAlpha:0 }, { y:0, autoAlpha:1, stagger:.06, duration:.4, ease:"power2.out", immediateRender:false }, at+0.08);
  tl.add(()=>setActive(i),   at);
  tl.add(()=>setActive(i-1), at-0.01);   // reverse-scroll sync
}
```

`end:"+=520%"` makes the pinned section last ~5 extra viewport-heights. `scrub:1.2` is the slow, premium catch-up. The two `setActive` calls (forward *and* reverse) keep the dots/counter correct whichever way you scroll.

### 9. Neon hero + glassmorphism logo chip + CTA

The hero uses Fraunces with a layered teal `text-shadow` glow and a flickering `.neon` word (`@keyframes flicker`). The fixed logo is the same frosted glass chip pattern (translucent pill, blur backdrop, hairline border). The closing `.cta-outro` lives *after* the pinned section so it releases cleanly.

---

## Step-by-step build

1. **Generate the 4 images.** Open `PROMPTS.md`. Each of the 4 prompts is complete and self-contained — copy one, paste into your image tool (Nano Banana / Gemini 2.5 Flash Image), set aspect ratio to **1:1 square**, generate.
2. **Drop them into `stack/solutions/`** using the exact filenames the data expects: `ai-consulting.png`, `process-automation.png`, `analytics.png`, `ai-chatbots.png`. (Skip this and the cube shows clean teal glyph fallbacks.)
3. **Edit the data array.** In `demo.html`, find `const SERVICES = [...]`. Each entry has `icon`, `img`, `title`, `desc`, `points[]`. Replace with your own services. With 4 services it fills the 4 side faces + 2 caps; the timeline, dots, counter and `POS`/`CUBE` anchors are indexed off `N = SERVICES.length`.
4. **Open `demo.html`** in a browser and scroll. Done — no server, no build.
5. **Customise:**
   - **Brand colour** — search/replace `#00cfa1` (and the `--brand` CSS variable). It drives the edge rim, glow, dots, CTA and glyphs.
   - **Cube size** — change `width:min(33vw,42vh)` on `.sol-cube`.
   - **Scroll length** — change ScrollTrigger `end:"+=520%"`. Higher = slower glide per service.
   - **Block placement** — edit `POS` (where each spec block sits) and `CUBE` (the cube's anchor per service, fractions of viewport from centre). Keep block and cube on opposite sides.
   - **Idle float** — tune the `cubeIdle` tweens; keep amplitudes small.

## The AI image prompts

Open **`PROMPTS.md`**. There are 4 prompts, one per service face. **Each prompt is complete and self-contained** — it already includes the full studio/teal art-direction block. Copy one, paste, generate at **1:1 square**, save under the matching filename. Don't add a separate shared "style" block; it's already inside every prompt. (The same art-direction matches the gallery's stack set, so both cubes feel like one brand.)

## The source images

The real PNGs that produced the demo are included in **`stack/solutions/`**. They're the exact inputs — open them to see the kind of square hero-on-dark render that makes each face look like a lit product shot, then match that look with your own prompts.

## Ship it on a real site

- **Framework-free:** the whole thing is one `.solutions` section + one `<script>`. Paste the markup, CSS and JS into any page that loads GSAP + ScrollTrigger.
- **React / Next.js:** wrap the markup in a component, move the build + idle-float + timeline code into a `useEffect` (or `useGSAP`), and `ScrollTrigger.kill()` (plus kill the idle tweens) on unmount.
- **Production polish:** install GSAP from npm instead of the CDN, and serve the face images through `next/image`.

## Gotchas (hard-won)

- **Safari blocks local `file://` images** loaded via `<img src>`. The logo is **base64-inlined** so the chrome always renders offline; the cube also degrades to glyphs if a face image can't load. To see the real images from disk in Safari, run a local server (`python3 -m http.server`) or inline them.
- **Keep the idle tilt small and the perspective weak (`1600px`).** A strong tilt or short perspective gives a corner-on, half-and-half cube where two faces compete and the active service stops dominating. The angles deliberately stay near `ry 6..10 / rx -9..-6`.
- **Use ScrollTrigger `pin:true`, not CSS `position:sticky`,** and pin the *section* (`.solutions`), never a child layer. The pin-spacer is what lets the closing CTA release cleanly with no overlap.
- **Don't animate a vertically-centred panel's `y`.** The center-positioned blocks (`top:50%; transform:translateY(-50%)`) are revealed with `autoAlpha` and an *inner* line stagger — never by tweening the panel's own `y`, which would fight the centring transform and jitter. (This is also why tilt lives on its own `cube-idle` layer, not on the travelling/face-spinning layers.)

---

*Made by MAX · maxautomate.ai — I build premium sites & effects with AI. Want one for your business? DM me.*

**Comment "GLIDE" for more.**
