Concept
The Velvet Hour is a fictional late-night jazz club fourteen steps under Murray Street, Perth — ninety seats, candlelight, a house piano, no sign on the street. The audience is anyone deciding where tonight ends. The page has one job: make you want to find the amber lamp and walk down, then hand you the week's setlist before you change your mind.
Everything on the page is procedural — SVG linework, CSS gradients and a 2D canvas. No images, no video, nothing raster.
Palette
Warm, dim, expensive. One near-black, one velvet, two metals — nothing cold anywhere on the page.
Typography
Marcellus — The Velvet Hour
Lora italic — fourteen steps below the street, the night keeps its own time.
Marcellus is a single-weight roman with flared, inscriptional terminals — letterspaced in caps it reads as engraved deco brass without resorting to a novelty display face. It carries every label, tab and heading; hierarchy comes from size, tracking (.14em–.44em) and case rather than weight. Lora does the talking: a bookish text serif whose italic has enough swing for lead lines and drink descriptions. The one moment of bravery is the hero — THE at .24em size but .92em tracking, stacked over VELVET / HOUR at clamp-scaled 6.9rem, drawn letter by letter.
Techniques
The signature — a frame that draws itself. The hero is wrapped in an art-deco frame assembled from nine SVGs: four corner ornaments (one drawing, mirrored with CSS transforms), four edge strips, and a pendant medallion hung from the top rule. Every path carries pathLength="1" so stroke-dasharray: 1 normalises to “one dash the length of the path”; a GSAP timeline then staggers stroke-dashoffset from 1 to 0 — edges first (1.7s, power2.inOut), corner fans next, pendant last — so the gold appears to be traced by hand around the room. The edge strips use preserveAspectRatio="none" and contain only straight lines, so stretching them between fixed-size corners distorts nothing.
Smoke. A single hero canvas drifts 14–22 pre-rendered radial-gradient sprites left to right with sinusoidal rise and phase-offset alpha, composited with lighter at very low opacity — haze, not clouds. The pointer applies a gentle “wind”. DPR is capped at 2; the rAF loop pauses on document.hidden and when the hero leaves the viewport via IntersectionObserver; reduced motion gets one static frame.
The setlist card. A CSS 3D table card (perspective 1600px) whose two faces are re-rendered from data before each cumulative 180° rotateY, so the card keeps flipping the same direction like a table tent being turned. A separate wrapper adds a ±5° pointer tilt on fine pointers.
Everything else is GSAP ScrollTrigger reveals, pure-CSS equalizer bars, and one CSS keyframe glint that travels the top rule every eleven seconds. Libraries: GSAP + ScrollTrigger only.
Iteration Log
-
Craft
- Edge SVGs didn't stretch between the corners (absolutely-positioned replaced elements ignore left+right for width) — rebuilt with explicit calc() widths so the frame actually closes.
- The medallion originally straddled the top rule behind a background-coloured mask that read as a dark box over the hero gradient — redesigned it as a pendant hanging from the rule; no mask needed.
- Nav links collided with the medallion — split the nav symmetrically around the centre, VH monogram left, live marker right.
- Softened the smoke sprite (larger radii, lower core alpha) so it reads as haze rather than blobs; tinted the scrollbar to the palette.
- Tightened hero eyebrow tracking and size under 480px.
-
Depth
- Candlelight behind the pendant: a radial glow that breathes over seven seconds, so the signature keeps living after it draws.
- Pointer tilt on the setlist card — it sits loose on the table like a real tent card.
- Second-read details: a faint coaster ring stamped at a slightly different spot on each night's card, and a gold glint that travels the top rule every eleven seconds.
- The live equalizer picks up tempo when you hover it.
-
Hardening
- 375px: hero meta line had no soft-wrap points between its nowrap spans, forcing the whole hero column off-centre — fixed, and verified zero horizontal overflow.
- Chromium renders dashed garbage when
vector-effect: non-scaling-strokemeetspathLengthnormalisation — removed the vector-effect and compensated corner stroke width under 640px instead. - Night tabs: roving tabindex plus Arrow/Home/End keyboard support.
- Reduced motion verified end-to-end: strokes pre-drawn, one static smoke frame, transitions and keyframes killed in CSS and guarded in JS.
- Chanel rule: removed the two diamonds flanking THE in the hero — the pendant above already says it.