{
 "site": "Your Majesty, Amsterdam",
 "slug": "yourmajesty",
 "urls": [
  "https://yourmajesty.co/work",
  "https://yourmajesty.co/",
  "https://yourmajesty.co/work/tomato",
  "https://yourmajesty.co/work/too-busy-foundation"
 ],
 "read": "2026-10-22 UTC, headless Chromium on fsit2",
 "inspiration_rows": [
  11,
  6,
  7,
  9,
  18
 ],
 "stack": {
  "framework": "Next.js 16.4.1 (App Router, Turbopack build), React 19.3.0-canary",
  "cms": "Storyblok (asset space 238272, content fetched server side)",
  "animation": [
   "GSAP 3.15.0 core",
   "GSAP ScrollTrigger",
   "GSAP Flip (registered by the work grid)",
   "CSS transitions and keyframes",
   "nextjs-toploader (nprogress) for the route progress bar"
  ],
  "scroll": "native scrolling with CSS scroll-behavior: smooth and overscroll-behavior: none; no Lenis or Locomotive; inner scrollers hide their scrollbars (scrollbar-width: none plus ::-webkit-scrollbar display none), the document scrollbar stays native",
  "three_d": "three.js r156 on the home page only (120 x 120 WebGL2 canvas with the rotating monogram); none on /work",
  "fonts": [
   "IvyPresto Display Thin (self-hosted and next/font)",
   "Messina Sans 300/400/700 with italics (self-hosted)",
   "DM Mono 300/400/500 with italics (self-hosted)"
  ],
  "hosting": "Vercel (server: Vercel, x-vercel-id, x-vercel-cache), images through /_next/image",
  "analytics": [
   "Google Tag Manager GTM-P2KJHNZW",
   "GA4 G-EFEE0RW5BS",
   "Vercel Speed Insights"
  ],
  "evidence": [
   "window.next={version:\"16.4.1\"} in chunk 33lex72pdubj6.js; header x-powered-by: Next.js; x-matched-path: /[lang]/work; runtime chunk turbopack-1cvbnb3nlabgn.js",
   "React version string 19.3.0-canary-278794d7-20261002 in chunks 2kzs590tmh7cs.js, 1zpyotba8ii9p.js, 33lex72pdubj6.js",
   "GSAP \"3.15.0\", GreenSock and ScrollTrigger code in chunk 3fqn2w4mtgg9r.js; Flip plugin (killFlipsOf, isFlipping) registered in 3l4qdozz7-kr6.js",
   "no framer-motion, motion-dom or Lenis code; startViewTransition appears only in the Next and React runtime chunk 33lex72pdubj6.js and there are 0 view-transition CSS rules, so the site's own transitions do not use it; probe globals empty (GSAP bundled)",
   "images at a.storyblok.com/f/238272/ with Storyblok image filters; Storyblok fields _uid, full_slug, cached_url, component in client code",
   "NextTopLoader in chunk 3s3v888l7p6xb.js and the injected #nprogress style in the HTML",
   "window.__THREE__=\"156\" and WebGLRenderer in home-only chunk 258637avdowkj.js; probe on /: one webgl2 canvas",
   "@font-face rules for /fonts/IvyPresto, /fonts/Messina, /fonts/DM_mono; meta next-size-adjust",
   "probe scripts: gtm.js?id=GTM-P2KJHNZW, gtag/js?id=G-EFEE0RW5BS, /_vercel/speed-insights/script.js",
   "probe on /work: 16 scrollbar rules (8 hide pairs on inner scrollers), 0 view-transition rules, 0 backdrop-filter, 0 SVG filters, 16 video elements",
   "verifier 2026-10-22: window.next.version \"16.4.1\" on /work and /, window.__THREE__ \"156\" on /; HEAD /work: server Vercel, x-powered-by Next.js, x-matched-path /[lang]/work, cdn-cache-control public, s-maxage=60, stale-while-revalidate=300; 32 work tiles, 16 with a Vimeo progressive MP4 (shots/verify/meta.json)"
  ]
 },
 "components": [
  {
   "name": "Capsule bar (rest, awake, close)",
   "owner_words": "I like this really minimal menu at the bottom, which is just the logo. When you scroll it, it becomes just the logo down. And when you hover it, it reveals the word menu and the word contact, which is super. We could just have something like that, except it also revealed the chat box … I'm trying to figure out if we should have two experiences or if it should always be kind of both available … I specifically like the way this menu becomes very minimal unless used. Then it can be like the other menu I shared earlier.",
   "selectors": [
    "nav.HUhwLa_navigationBar[data-state]",
    ".HUhwLa_navWrapper",
    ".HUhwLa_closeWrapper",
    ".Z6WgPW_animatedLogo",
    "button[aria-label='Open Menu']",
    "button[aria-label='Open Contact']",
    "button.HUhwLa_closeButton",
    "button.m57DwW_arrowWrapper"
   ],
   "mechanism": "One fixed black pill (bottom 33 px, radius 9999 px, overflow hidden) holding three absolutely centred layers: the words (Menu | Contact), the monogram and Close. data-state = panelOpen ? 'close' : (pageTransitionRunning || (scrollingDown && !engaged)) ? 'collapsed' : 'default'. scrollingDown is set on every scroll event as scrollY > lastScrollY && scrollY > 100 px (60 px under 769 px wide); any upward scroll clears it; there is no timer. engaged = mouseenter on the pill itself (no proximity radius) or focus inside it. GSAP tweens the pill's width to the active layer's offsetWidth (elastic.out(0.9,0.9), 0.438 s, 63 ms delay); CSS crossfades the layers (to rest: words fade and scale to .6, monogram fades in and scales from 2 to 1; on waking the words snap back to full size and only fade, revealed by the widening pill). An IntersectionObserver on the footer sets --nav-offset directly while scrolling so the bar rides above the footer, where a scroll-to-top arrow appears; opening a panel tweens it to 0 and closing tweens it back.",
   "libraries": [
    "GSAP core",
    "React state",
    "CSS transitions",
    "IntersectionObserver"
   ],
   "states": [
    "default: MENU | CONTACT, 250 x 49 px desktop, 233 x 45.7 px phone",
    "collapsed: monogram oval, 99 x 49 px desktop, 103 px phone",
    "close: CLOSE x, 180 px desktop, 130 px phone",
    "lifted above the footer by the footer's visible height (-210 px desktop, -398 px phone at the end), with the up arrow"
   ],
   "timings": "width 438 ms elastic.out(0.9,0.9) after 63 ms; words out: opacity 313 ms linear and scale .6 313 ms cubic-bezier(.76,0,.24,1), 44 ms delay; words in: opacity 313 ms linear, 63 ms delay; monogram: opacity 156 ms linear (63 ms delay), scale 2 to 1 313 ms cubic-bezier(.76,0,.24,1); separator scaleY 125 ms after 188 ms (313 ms on phone); link colour 150 ms; footer offset tween 375 ms expo.inOut (to 0 when a panel opens, no delay; back to the footer offset 63 ms after it closes or the route changes; scrolling sets it without a tween)",
   "shots": [
    "shots/bar-d01-arrival.png",
    "shots/bar-d02-rest.png",
    "shots/bar-d03-pointer-near-not-on.png",
    "shots/bar-d04-awake-hover.png",
    "shots/bar-d05-after-scroll-up.png",
    "shots/bar-d06-keyboard-focus.png",
    "shots/bar-d07-footer.png",
    "shots/bar-p01-arrival.png",
    "shots/bar-p02-rest.png",
    "shots/bar-p03-after-tap.png",
    "shots/bar-p06-footer.png",
    "shots/bar-p08-second-tap.png",
    "shots/burst-bar-collapse-sheet.png",
    "shots/burst-bar-wake-sheet.png"
   ],
   "rebuild": "Fixed pill with stacked absolute layers (mark, awake row with Menu, the ask field and Contact, in-use row); one computed data-state; CSS crossfades for the layers; spring the pill width to the active layer's measured width (GSAP elastic.out(0.9,0.9) 0.44 s, Motion spring, or FLIP with a CSS linear() spring); rest on scroll-down past 100/60 px, wake on scroll-up, pointerenter and focusin, no timers; treat focus in the ask field as engaged; avoid their blur quirk by tracking :focus-within and moving focus to the next layer's control when a layer hides; footer or drawer evade via an IntersectionObserver writing a CSS variable; reduced motion: instant width, opacity only; add aria-expanded, aria-controls and Escape.",
   "gotchas": [
    "After clicking a control in the bar, the focused button hides, focus falls to body and onBlur clears the hover flag; if the last scroll was downward past the threshold the bar then collapses under a resting pointer until it leaves and re-enters, while at the top of the page it stays awake (headless Chromium)",
    "Width is measured from offsetWidth, so it depends on loaded fonts",
    "Any scroll closes an open panel"
   ],
   "confidence": "high"
  },
  {
   "name": "Menu and contact panels",
   "owner_words": "No words of his own on the panels yet in INSPIRATION.md (added to the brief by the coordinator). Nearest: \"Then it can be like the other menu I shared earlier.\" (row 11) and the drawer of row 7: \"it slides out like a drawer from the bottom of the page\".",
   "selectors": [
    "section.uS33ha_animatedPanel",
    ".uS33ha_menuBackground",
    ".uS33ha_scrollableContentWrapper",
    ".uS33ha_contentWrapper",
    "ul.b56tGa_list .b56tGa_listItemLink",
    "ul.b56tGa_subList .b56tGa_subListItemLink",
    "h2.Kiyn2W_heading",
    "[data-panel-item]"
   ],
   "mechanism": "A fixed black card (415 x 680 px desktop, 366 x 634 px phone, 20 px corners) 22 px above the bar, z-index under the bar. Its background is a separate layer with transform-origin 50% 100%, so a GSAP timeline scales it (scaleY 0 to 1 power3.inOut with scaleX .02 to 1 back.out(1.7)) without distorting the text, then the rows rise from y 108% with opacity (power4.out, stagger). The whole timeline runs at timeScale 2.4 (2.08 on phone); close is a separate timeline (card 0.6 s power4.inOut, rows 0.2 s power4.in, stagger 0.01 s) at timeScale 1.92. The bar springs to CLOSE at the same time. Closes on Close, any scroll, a link, a page transition; not on Escape.",
   "libraries": [
    "GSAP core (timeline, stagger)",
    "CSS"
   ],
   "states": [
    "closed: display none, then pointer-events none after the first open",
    "menu open: seven primary links (current page #579aff), three secondary, EN / NL",
    "contact open: Contact Us, New Business, Book A Call, three mailto links",
    "switching: Get In Touch closes the menu timeline while the contact timeline opens"
   ],
   "timings": "open: timeline delay 156 ms; bg 650 ms; items 700 ms, stagger 70 ms, from 370 ms; all at 2.4x, so about 65 ms delay, card full at about 335 ms, last of eleven items settled at about 0.8 s (desktop). close: bg 600 ms power4.inOut, items 200 ms power4.in stagger 10 ms, at 1.92x, about 313 ms",
   "shots": [
    "shots/menu-d01-open.png",
    "shots/menu-d02-link-hover.png",
    "shots/menu-d03-after-escape.png",
    "shots/contact-d01-open.png",
    "shots/contact-d02-from-get-in-touch.png",
    "shots/contact-d03-scroll-closes-panel.png",
    "shots/bar-p04-menu-open.png",
    "shots/bar-p07-contact-open.png",
    "shots/burst-menu-open-sheet.png",
    "shots/burst-menu-close-sheet.png",
    "shots/burst-contact-open-sheet.png",
    "shots/burst-menu-to-contact-sheet.png",
    "shots/burst-phone-menu-open-sheet.png"
   ],
   "rebuild": "The ETS27 drawer grown out of the bar: card anchored above the bar, background layer scaled from a bottom-centre origin (scaleY in-out plus scaleX back-out overshoot, about 0.27 s), rows rising from 108% with opacity, 29 ms apart; close faster and reversed. GSAP timeline, Motion sequence with stagger, or CSS @starting-style plus a linear() back-out curve and per-row transition-delay. Add role dialog, focus management, Escape and a reduced-motion branch; do not close scrolling drawers on scroll.",
   "confidence": "high"
  },
  {
   "name": "Work tile: tilt hover and in-view preview video",
   "owner_words": "No words of his own on this yet in INSPIRATION.md (added to the brief by the coordinator).",
   "selectors": [
    "article._7O2wwa_articleWrapper",
    "._7O2wwa_mediaContainer",
    "img._7O2wwa_imagePlaceholder",
    "img._7O2wwa_imageOverlay",
    "video._7O2wwa_videoOverlay",
    "ul.Vn_v4q_cardListGrid",
    "div.gsiEBG_workPageFilter"
   ],
   "mechanism": "GSAP rotates the media box from the pointer position on mousemove: rotationY = (x/w - 0.5) x 24 deg, rotationX = (y/h - 0.5) x 16 deg, transformPerspective 2000, 1.2 s expo.out (the axes have opposite senses: the side under the pointer horizontally recedes while the side under it vertically comes forward, so a pointer near the top-left puts the top-right corner nearest the viewer); back to 0 over 2 s on leave; frozen on click; off for (any-hover: none). Images are a blur-up pair (40 px Storyblok render under the full image, which fades in over 0.5 s). Video tiles (16 of 32, Vimeo MP4, muted loop playsinline preload none) play only while intersecting the viewport and pause when they leave; there is no hover-triggered preview.",
   "libraries": [
    "GSAP core",
    "IntersectionObserver",
    "next/image"
   ],
   "states": [
    "rest",
    "tilted by the pointer position (about +/-11 deg Y and +/-7 deg X measured near the corners; opposite senses per axis)",
    "video playing in view, paused out of view (readyState 0 until seen)"
   ],
   "timings": "tilt 1.2 s expo.out, return 2 s expo.out; image and video fade-in 500 ms ease-in-out; article opacity 650 ms cubic-bezier(.65,0,.35,1)",
   "shots": [
    "shots/tile-d01-pointer-on-tile.png",
    "shots/tile-d02-tilt-top-left.png",
    "shots/tile-d03-tilt-bottom-right.png",
    "shots/tile-d04-video-tile-in-view.png",
    "shots/case-d01-before-click.png",
    "shots/verify/verify-tilt-top-left.png"
   ],
   "rebuild": "For speaker cards: the same small 3D tilt (perspective 2000 px, +/-12 and +/-8 deg, expo.out 1.2 s, slow 2 s return; pick each axis's sign on purpose, theirs are mixed) with GSAP quickTo, Motion useSpring or two CSS variables; play card video only in view with preload none; blur-up every photo.",
   "confidence": "high"
  }
 ],
 "transitions": [
  {
   "name": "Work tile to case page (shared-element zoom)",
   "from": "https://yourmajesty.co/work (tile click)",
   "to": "https://yourmajesty.co/work/<case>",
   "layers": [
    "link wrapper preventDefault and clickLink(href, anchor)",
    "bar forced to rest (transitionRunning) and the 2 px top loader",
    "clone of the tile video or full image in a fixed full-screen overlay (z-index 5, under the bar), placed flat on the tilted tile's bounding box with its 5 px radius (the tilt copy parses the media element's empty inline transform, so the clone starts untilted); a video clone keeps playing from the same time",
    "GSAP zoom of the clone to 100vw x 100% with radius 0 (1.2 s expo.inOut; its rotateX and rotateY 0 targets are no-ops)",
    "router.push on complete; the page swaps under the clone",
    "overlay tweened to the case hero's top and height (1.2 s expo.inOut) while the title and headline rise from y 50 and opacity 0 at +150 ms; a video hero waits for canplay and syncs time",
    "the bar wakes as soon as the new route mounts (transitionIsRunning is cleared at the start of the settle, while the overlay still covers the screen) and springs open; the overlay is removed when the settle completes"
   ],
   "duration_ms": 2400,
   "duration_note": "1.2 s zoom plus 1.2 s settle, plus the route fetch in between; the headless sample took about 4.1 to 4.5 s end to end on fsit2",
   "shots": [
    "shots/burst-case-zoom-sheet.png",
    "shots/burst-case-settle-sheet.png",
    "shots/burst-video-case-sheet.png",
    "shots/burst-phone-case-sheet.png",
    "shots/case-d01-before-click.png",
    "shots/case-d02-arrived.png",
    "shots/case-d05-video-case-arrived.png",
    "shots/case-p02-arrived.png",
    "shots/case-p03-accidental-tile-tap-zoom.png"
   ]
  },
  {
   "name": "Any other route",
   "from": "any page",
   "to": "a page that is neither a /work/ case nor an /article/ page",
   "layers": [
    "top loader",
    "plain links (no media inside, such as the menu and footer links) push the route directly: no clone, no transitionIsRunning, the bar is left alone",
    "a media link to such a page runs the same clone zoom with the bar at rest, then the overlay fades out over 0.8 s linear instead of settling into a hero"
   ],
   "duration_ms": 800,
   "shots": []
  },
  {
   "name": "Bar rest and wake",
   "from": "default",
   "to": "collapsed (and back)",
   "layers": [
    "pill width spring (GSAP)",
    "words fade and scale (CSS)",
    "monogram fade and scale from 2x (CSS)",
    "separator fold"
   ],
   "duration_ms": 501,
   "shots": [
    "shots/burst-bar-collapse-sheet.png",
    "shots/burst-bar-wake-sheet.png"
   ]
  },
  {
   "name": "Panel open and close",
   "from": "bar default or collapsed",
   "to": "menu or contact panel open (bar shows CLOSE)",
   "layers": [
    "panel background scaleY and scaleX from the bottom centre",
    "rows rise with opacity, staggered",
    "bar width springs to the Close layer"
   ],
   "duration_ms": 800,
   "shots": [
    "shots/burst-menu-open-sheet.png",
    "shots/burst-menu-close-sheet.png",
    "shots/burst-contact-open-sheet.png",
    "shots/burst-menu-to-contact-sheet.png",
    "shots/burst-phone-menu-open-sheet.png"
   ]
  }
 ],
 "not_confirmed": [
  "Exact on-screen durations in a real browser: fsit2 burst frames came 140 to 1855 ms apart and rAF samples were coarse, so timings are source values (a 60 fps headed recording would confirm)",
  "The blur quirk (bar re-collapses under a resting pointer after a panel closes; a repeat tap does nothing) outside headless Chromium: check Safari, an iPhone and an Android phone",
  "The filter-tab reflow: GSAP Flip is registered by the grid but no tab was clicked",
  "Reduced motion: no handling found in the code read, not run with the preference set (capture.cjs has no emulateMedia step)",
  "Click outside a panel: no handler in the component, not tested",
  "Home page components (three.js monogram, custom cursor container, card carousel) were not torn down",
  "Storyblok API details: content is fetched server side; only the asset space id and field names are visible",
  "Caption type sizes (DM Mono 16 px, Messina 22 px), the three-line caption maximum and the three-column grid breakpoint: seen only at 1440 px, not measured (verifier)",
  "Real-time order of the bar's width spring and the monogram crossfade: the rAF samples disagree between runs, so only the source values hold (verifier)"
 ]
}
