Warm light page where eight artwork cards stand in a 3D ring and scrolling turns it so each one swings to the front. Pure CSS 3D and inline SVG, so every tool builds the same page.
## Goal
Design a museum exhibition landing page for Halden, a contemporary art museum. The hero is a 3D ring: eight artwork cards (eight artworks from the spring show, each with title, artist, year and medium) stand around an invisible cylinder, and scrolling turns the ring so each card swings to the front, one by one. Keep the tone calm and editorial.
Use the style, layout and motion rules below exactly. Replace the sample content with copy that fits the goal, keeping eight items, with the same fields in the same places and similar text lengths.
## Style
Keep this exactly: it is what makes the design look the way it does.
- Page background #F2EDE4, text #17140F, muted text #6F675B, card surface #FBF8F2, hairlines rgb(23 20 15 / 0.12). One accent, coral #FF5A36 (no gradients anywhere). Font: system-ui.
- Artwork palette (used only inside the artworks): sun #FFC21A, cobalt #2B4BFF, forest #1F6B4F, blush #F6B8A6, lilac #B9A6F5, sand #E8DFD0, plus coral, ink #17140F and cream #F2EDE4.
- Buttons are pills, 14px 26px padding, 15px semibold. Primary: ink #17140F background, cream text, shadow 0 8px 24px rgb(23 20 15 / 0.22). Secondary: transparent with a 1px border at 25% ink. The small header button is the primary style at 10px 20px padding, 14px text.
- Every focusable element shows a 2px coral (#FF5A36) focus outline with 3px offset.
- No images anywhere. The artworks are inline SVG made of flat shapes (circles, rectangles, arches, wavy lines), and everything else is CSS 3D transforms. Do not generate, search for or link any image, and do not use WebGL, canvas or libraries.
**The ring.** All of it lives in one 3D scene built from real planes at real depths. Use `perspective: 1600px` with perspective origin 64% 50% on the scene, and `transform-style: preserve-3d` on every element between the scene and the cards. Set `--R: 460px`, the radius of the ring, and a card size of 300px × 420px.
- **Cards:** the eight cards and the turntable floor are direct children of the ring group, with no plain wrapper element between (any ancestor without `preserve-3d`, even one `<div>` that holds the cards, flattens the ring into a flat row). Eight cards, index `--i` = 0 to 7, each placed with `transform: rotateY(calc(var(--i) * 45deg)) translateZ(calc(var(--R) + pop))`, where pop is the lift of the card in front (see Motion). The ring group is moved back by its radius, `translateZ(-460px)`, so the front card sits at the focus plane, and it spins around its own vertical axis with `rotateY(calc(var(--p) * -315deg))`. A wrapper above it tips the whole ring toward the viewer with `rotateX(-8deg)`. Center every card on the ring's center (the center of the scene at 64% across and 50% down).
- Each card has two faces with `backface-visibility: hidden`: the front face (radius 14px, surface #FBF8F2, 1px border at 12% ink, shadow 0 24px 60px rgb(23 20 15 / 0.18), `overflow: hidden`) and a back face rotated 180deg (#D8CFBF with 1px diagonal hairlines every 15px at 6% ink), so cards on the far side of the ring show their backs through the gaps.
- **Front face:** a square SVG artwork on top (full card width, 300px), then a label block (padding 16px 18px): a row with "Room 01" on the left and the year on the right (12px semibold uppercase muted text, 0.12em letter spacing), the title (20px bold, 8px above, 4px below, -0.02em letter spacing, line-height 1.2) and the artist (14px muted). On top of the face sits a flat shade layer, ink-colored, whose opacity is set from the card's distance from the front (see Motion).
- **Turntable floor:** a 1400px round plane inside the ring group, `translateY(calc(420px / 2 + 24px)) rotateX(90deg)`, drawn with 5° radial ticks (`repeating-conic-gradient`, 0.35° ticks at 22% ink) over concentric circles every 70px (1px, 10% ink), masked with a radial gradient from #000 at 30% to transparent at 68%. It spins with the ring, so the ticks show the rotation.
- Behind everything on the stage, a soft radial glow: ellipse 40% × 50% at 64% 50%, #FBF8F2 fading to transparent by 70%.
## Layout
1. **Header,** fixed, 72px tall, a fade from the page color to transparent, 6vw side padding: logo on the left (a 12px coral square rotated 45deg and the brand name, 18px bold, -0.02em) and links on the right ("Exhibitions", "Collection", "Visit", 14px muted, 32px apart) plus the small primary "Buy tickets" button.
2. **Scroll stage.** A section 320svh tall containing a sticky, `100svh` stage (`overflow: hidden`). Inside it:
- Left column, 400px wide (max 34vw), vertically centered, 6vw from the left: a pill eyebrow (13px semibold muted text, 1px border at 18% ink, 6px coral dot), an h1 (clamp(36px, 4.4vw, 62px), bold, line-height 1.02, -0.035em letter spacing) whose last words are coral, an 18px muted paragraph (line-height 1.55), a caption block, and a scroll hint (13px uppercase semibold muted text, 0.08em letter spacing, with a small down arrow).
- The caption block is 96px tall with a 1px hairline on top and 8 stacked captions (one per card, 16px below the hairline): "Room 01 / 08" (12px semibold uppercase coral, 0.12em letter spacing), the title (28px bold, -0.025em letter spacing, 10px above and 6px below) and "Artist, year · medium" (15px muted). Only the caption of the front card is visible.
- The 3D scene filling the stage, with its center at 64% across and 50% down.
- Progress dots: eight 8px coral circles, 14px apart, centered at 64% across and 68px from the bottom.
- A scroll gauge centered at 64% across and 36px from the bottom: "01", a 160px × 2px track (ink at 12%) and "08", in 11px semibold uppercase muted text, 0.16em letter spacing, 14px gaps. The track fills left to right in coral as `--p` goes from 0 to 1 (`transform: scaleX(var(--p))`, origin left).
3. **Closing call to action,** centered, padding 140px 24px 120px: a heading (clamp(34px, 4vw, 52px), bold, -0.03em, with the last word coral), an 18px muted line, then the primary and secondary buttons 14px apart.
4. **Footer,** a hairline above, 13px muted text, copyright on the left and "Privacy · Terms" on the right.
Under 900px wide: the nav links hide (the button stays), the column sits at the top (96px down, full width) with the paragraph, caption block and hint hidden and the h1 at 38px, the scene uses `perspective: 1200px` centered at 50% across and 64% down, `--R` is 340px, cards are 220px × 308px (label padding 12px 14px, title 17px, artist line hidden), and the dots and gauge center at 50% across (52px and 24px from the bottom).
## Motion
Use CSS transitions, keyframes and a small vanilla script; no animation libraries. Use the easing cubic-bezier(0.22, 1, 0.36, 1) unless stated. **The turn is plain CSS driven by one number:** the script only sets a unitless `--p` (0 to 1) on the stage, and everything below is a `calc()` on `--p`, so the result is identical in every browser.
- **Scroll progress:** target progress = how far the sticky stage has scrolled through its section, (−section top) / (section height − viewport height), clamped to 0–1. Each animation frame, move the displayed `--p` toward the target by 12% of the remaining distance, so it eases instead of snapping, and stop updating once it is within 0.0005.
- **Distance of each card:** every card, caption and progress dot has an index `--i` (0 to 7) and `--d: calc(var(--p) * 7 - var(--i))`, so card 0 is in front at `--p` 0 and card 7 at `--p` 1. Let `--a: max(var(--d), calc(var(--d) * -1))` and `--w: min(var(--a), calc(8 - var(--a)))`: `--w` is how many steps the card is from the front, going the short way around the ring (0 = in front, 4 = directly behind).
- **Pop:** the card in front lifts toward the viewer: pop = `clamp(0, calc(1 - var(--w)), 1) * 80px`, so a card at the front sits 80px closer and cards a full step away sit on the ring.
- **Shade:** the shade layer opacity is `clamp(0, calc(var(--w) * 0.26), 0.6)`. The card in front has no shade and is fully solid; neighbors are slightly darker, far cards darker still. Never fade the card or its 3D ancestors with opacity, because it flattens the 3D sorting; shade only the flat layer on the face.
- **Captions:** caption i has opacity `clamp(0, calc(1 - var(--w) * 2.2), 1)` and `translateY(calc(clamp(-1, var(--d), 1) * -14px))`, so titles cross-fade and slide up as the ring turns.
- **Progress dots:** dot i has opacity `clamp(0.25, calc(1.6 - max(var(--d), calc(var(--d) * -1)) * 2), 1)` and scale `calc(1 + clamp(0, calc(1 - max(var(--d), calc(var(--d) * -1))), 1) * 0.7)`, so the dot of the card in front glows and grows.
- **Intro:** on load the whole ring flies in: its independent `translate` property animates from `0 0 -600px` to `0 0 0` over 1000ms. Only use `translate`, never opacity.
- **Idle float:** the scene moves between -8px and 8px on the Y axis, 6s ease-in-out, alternating, forever. Put it on its own wrapper.
- **Pointer lean:** on devices with a fine pointer, the scene leans toward the cursor over the stage (up to 4deg of rotateY and 3deg of rotateX, inverted for X), easing over 600ms, and returns flat when the cursor leaves. Put this on its own wrapper, outside the float.
- **Scroll hint:** fades out as the scroll starts (opacity `calc(1 - var(--p) * 8)`); its arrow nudges down 5px and back every 1.8s.
- **Buttons:** the primary lifts 2px with a coral glow (0 12px 32px rgb(255 90 54 / 0.4)) on hover, 240ms; the secondary gets a 7% ink background.
- **Reduced motion:** with prefers-reduced-motion: reduce, **scrolling must still turn the ring**, because it is something the visitor does, not motion the page makes on its own. Keep the 320svh section, and set `--p` straight to the target progress on every scroll and resize event (no easing and no animation loop). Turn off only the self-running motion: the intro, the idle float, the pointer lean and the hint nudge.
## Sample content
- Brand "Halden", a contemporary art museum. Links "Exhibitions", "Collection", "Visit". Header button "Buy tickets".
- Eyebrow "Now showing". Heading "Eight rooms, one slow turn." with "one slow turn." in coral. Paragraph "Scroll to turn the ring and step into each room of the spring show, one work at a time." Hint "Scroll to turn the ring". Gauge "01" and "08".
- The eight works, in order (title, artist, year, medium, artwork on a 300 × 300 viewBox):
1. "Sunrise Grid", Mara Ostrow, 2019, Oil on linen: sun background, a coral circle (center 150,165, radius 88) and four ink horizontal bars across the bottom (y 210, 232, 254, 276; heights 10, 8, 6, 4).
2. "Blue Arches", Teodor Vance, 2021, Gouache on paper: cobalt background, a 22px sun circle at top left, three arches 80px wide standing on the bottom edge (y 270), getting taller left to right (tops at 130, 90, 50 before the round cap); first two cream, the last coral.
3. "Tidal Lines", Ines Calloway, 2018, Ink on paper: blush background, five wavy 3px ink lines (every 40px from y 110 to 270) and a cobalt circle (radius 26) at top right.
4. "Orchard Count", Ravi Menon, 2022, Acrylic on panel: forest background and a 3 × 3 grid of 30px-radius circles (centers 60, 150, 240 across and 70, 150, 230 down) in cream, sun and coral.
5. "Half Moon Study", Lior Basch, 2017, Oil on board: ink background, a 100px-radius circle split by a 3px cream line at y 150, upper half cream and lower half cobalt, with a 14px sun dot at the center.
6. "Staircase for Two", Anika Sorensen, 2023, Acrylic on canvas: lilac background, a sun circle (radius 24) at top left and four 60px-wide steps rising left to right (heights 50, 100, 150, 200; cobalt, cream, ink, coral).
7. "Quiet Hours", Yuki Arden, 2020, Gouache on linen: sand background, a cobalt circle (radius 80) and a sun circle (radius 34) overlapping, a 3px ink line near the bottom and a small coral square.
8. "Red Field", Dario Pell, 2024, Screenprint on paper: coral background, five 36px cream squares stepping down a diagonal and a 32px-radius ink circle at top right.
- Closing: "Plan your visit" (the word "visit" in coral), "Open Tuesday to Sunday, 10:00 to 18:00. Late opening on Thursdays until 21:00.", buttons "Buy tickets" and "See the floor plan". Footer "© 2026 Halden" and "Privacy · Terms".
- Mark the 3D scene `aria-hidden="true"` and give the stage an aria-label, e.g. "Scroll to turn a ring of eight artworks, rooms one to eight of the Halden spring show". The cards' text is decorative inside the scene, so keep the facts also in the caption block.
## Images
None. This design uses no photos, no generated images and no image links: the artworks are inline SVG shapes and the ring is built from CSS 3D transforms, so the page works the same in every tool. Do not add any `<img>`, background image, canvas or WebGL.
## Output
A single self-contained HTML file with CSS in a `<style>` tag and JavaScript in one `<script>` tag, responsive down to 375px wide, using semantic HTML and visible keyboard focus states.