Case · Decant8 · 2026

How the desk is built

An editorial site for niche perfume, designed and built as one system. Every value on this page is read from the same stylesheet the site ships with, so the page cannot drift from the product.

Role
Art direction, design, motion, front-end
Stack
Astro 7, Tailwind 4, GSAP, Lenis, Figma, Adobe Illustrator
Content
12 pages: home, news, note, library, review, journal, guide, search, method, authors, author, 404
System
23 components · 24 motion modules
Download tokens.json

01 · Tokens

Colour

11 roles and 9 family grounds. Contrast is computed against paper and ink at build; a unit test fails the build if a text colour drops below AA.

Family grounds · review sheets and cards

02 · Tokens

Type

Two families. A thin serif carries voice; a grotesk carries facts. Five fluid sizes, no middle tier. Sizes resolve per viewport; the live column reads your screen.

Aa

Editorial New · 100 · 300

Bought by the desk. Worn three times.

--font-editorial

Aa

PP Neue Montreal · 400 · 600

Prices by the millilitre. No press kit. 0123456789

--font-sans · --font-mono
TokenCSS3901440LiveLine
--text-megaclamp(4rem, 15vw, 15rem)64px216px·1.18

Decant

--text-statementclamp(2.75rem, 8vw, 8rem)44px115.2px·1.18

Wear it first

--text-displayclamp(2.25rem, 5vw, 4.5rem)36px72px·1.18

The reading list

--text-titleclamp(1.5rem, 3vw, 2.25rem)24px36px·1.15

Tobacco Vanille

--text-micro0.625rem10px10px·1.2

Cursor · tap echo

--text-label0.6875rem11px11px·1.2

Library · 28 reviews

03 · Tokens

Space and radius

Tailwind's 0.25rem step for components, four named tokens for page rhythm. Lint rejects any step that is not a multiple of 0.25, raw radii in px and raw z-index numbers.

Layers · z-index scale

TokenValueUsed by
--z-below-1Fills behind their own content.
--z-base1Lift inside a component.
--z-raised2Hero flow over columns.
--z-float20Hero cue, floating preview.
--z-overlay40Wear clock, chapter pill.
--z-dock45Rail dock, read progress.
--z-header50Site header.
--z-cursor60Custom cursor, tap echo.
--z-sheet70Filter sheet, menu panel.
--z-scrollbar75Desk scrollbar.
--z-toggle80Menu toggle above its panel.
--z-skip90Skip link, always on top.

04 · Tokens

Motion

Two curves, 6 durations. GSAP registers both curves through CustomEase from the same CSS variables and reads durations at runtime, so CSS and script moves feel like one hand. Lint rejects any other ease. The one exception is gravity in the splash pour. Hover a card to play it.

--ease-quietcubic-bezier(0.18, 0.71, 0.11, 1)Default. Hover, reveals, colour. Fast start, long settle.
--ease-expocubic-bezier(0.19, 1, 0.22, 1)Mass: sheets, filters, liquid fill, page-level moves.
ModuleTriggerWhat movesReduced motion
indexEvery pageShared modules eagerly; page modules imported only when their selector existsSame
view transitionsSame-origin navigationNative cross-document fade; card still morphs into the review stillOff
headerScroll directionHides on the way down, returns on the way up and on focusAlways shown
splashFirst visit, homePer-letter wordmark, liquid pour, sheen sweepSkipped
revealEnters 85% of viewportFade-up, SplitText lines and charsStatic
heroGridLoad, scrollFour columns rise with staggerStatic
heroWave · heroCarouselPointer / small screensPointer wave; canvas slideshow paused off-screenOff
skin · skinLayoutPinned scrubBlotter to skin, drydown clock, seeded collision-free notesStatic layout
storyScroll scrubHome wear clock, parallax scenesOff
wearReview scrollWear clock walks top, heart, base from longevity; ground shifts per phaseNo label motion
drag · carouselFine pointer / touchInertia drag; on touch a swipe hint and a one-time nudgeNative scroll
roadScroll scrubHand-drawn dashed road, stops pop inFull path
previewHover / touchHover preview loads on first entry; on touch the centred row opens a thumbnailOff
cursorHover / touchCustom cursor; on touch a label echoes at the tap pointOff
menuToggleOverlay, stagger, focus trap, inert pageNo stagger
railDock · railSheet< 64remDock with progress ring, bottom sheet for railsSame
article · tablesScrollActive chapter, table revealStatic
vapor404Canvas vapour that follows the pointerStatic

05 · Layout

Grid and layout

Three breakpoints, one gutter of 20px. Usage counts come from the stylesheet.

NameMin widthRulesWhat changes
base0n/aOne column. Rail becomes a sheet, dock appears.
md48rem · 768px20Two-column cards, split heroes start.
lg64rem · 1024px23Sticky rail, header nav, desktop motion.
xl80rem · 1280px2Three-column catalog.

06 · Components

Atoms

Live components, not pictures. Hover the buttons: one liquid fill for all of them.

Logo currentColor SVG, header and footer wordmark.
Arrow dir one Editorial glyph, rotated: left, up, right, down, out.

01 · Library · 28 reviews

Label 11px, caps, tracking 0.14em. Kickers, meta, nav.
Buy the bottleDecant firstSkip
VerdictPill buy · decant first · skip.
Buy the bottleDecant firstSkip
VerdictPill theme="deep"
.btn-line · .reading-btn pill, liquid fill top-down, 1.1s expo.
.btn-gold · .btn-line.is-light dark grounds only.
Filter chip pressed state draws a gold rule.
Breadcrumb tone ink · paper.

07 · Components

Molecules

Atoms bound to real data: the sample below is the Tobacco Vanille file.

ArrowLink text roll, gold underline, arrow swap. Dark grounds.
Longevity9 / 10
Sillage8 / 10
Per ml4 / 10
ScoreBars longevity, sillage, value per ml.
Brand
Tom Ford
Year
2007
Concentration
EDP
Family
Oriental · Gourmand
Perfumer
Olivier Gillotin
Price
€270 to €360
Longevity
9 / 10
Sillage
8 / 10
Per ml
4 / 10
SpecTable definition list, one label–value row per spec.
FragranceCard ground by family, verdict pill, square still.

08 · Components

Organisms

Sections as they ship, 1440 wide. Each links to its page.

09 · Build

Engineering

Content as typed collections, a static build, and gates that fail the build instead of a review.

Content model

fragrances
28
guides · compares
10 · 6
news
4
pages
12
authors · brands
2 · 16

Quality gates

  • No colour literal outside @theme: CSS, Astro, TS.
  • No raw label size, tracking, radius, z-index or transition duration in CSS.
  • No ease outside quiet and expo in GSAP; no Tailwind duration off the scale.
  • No muted text below AA: text-ink/50 fails, text-ink-muted passes.
  • No inline style, no arbitrary -[ ] classes in markup.
  • Spacing steps must be multiples of 0.25.
  • Unit tests for search, copy, images and token contrast (Vitest).
  • CSP with sha256 for every inline script, generated after build.
  • Reduced motion handled in every motion module.

Measured · 5 Oct 2026

Pages builtStatic, no runtime
59
JS, inner pagegzip; GSAP, Lenis, shared modules
59 KB
JS, homegzip; home chunk loaded on demand
+24 KB
CSSgzip, 14 ordered partials, one file
23 KB
Home weightWas 1.47 MB; AVIF 480/800/1200, lazy preview
494 KB
LCP, libraryMobile, 4× CPU, 1.6 Mbps
2.2 s
LCP, reviewSame profile
1.6 s
CLSEvery image has width and height
0

10 · Handoff

Figma handoff

One JSON in W3C design-token format. core is a single-mode collection;mobile and desktop share keys and become two modes of one collection, so frames at 390 and 1440 switch sizes with a mode toggle.

  1. Download tokens.json.
  2. Figma: Local variables, import JSON. Or Tokens Studio: load file, sets core, mobile, desktop.
  3. Map mobile and desktop to modes of a Size collection.
  4. Frames 390 and 1440; layout grid with 20px margins; 12 columns on desktop.
  5. Names map one to one: color/gold = --color-gold = bg-gold.
{
  "core": {
    "color": {
      "gold": {
        "$type": "color",
        "$value": "#bc8569"
      },
      "ink": {
        "$type": "color",
        "$value": "#17201e"
      }
    }
  },
  "desktop": {
    "text": {
      "title": {
        "$type": "dimension",
        "$value": "36px",
        "$extensions": {
          "css": "clamp(1.5rem, 3vw, 2.25rem)",
          "lineHeight": 1.15
        }
      }
    }
  },
  "mobile": {
    "text": {
      "title": {
        "$type": "dimension",
        "$value": "24px",
        "$extensions": {
          "css": "clamp(1.5rem, 3vw, 2.25rem)",
          "lineHeight": 1.15
        }
      }
    }
  }
}