Read on 2026-10-22 UTC (the evening of 2026-10-21 local), headless from fsit2. URLs read: https://yourmajesty.co/work, https://yourmajesty.co/, and the case pages reached by clicking tiles (https://yourmajesty.co/work/tomato, https://yourmajesty.co/work/too-busy-foundation). Serves inspiration row 11, and by extension rows 6 and 7 (the bar and the drawer), 9 (invisible scrollbars) and 18 (continuous page transitions).
Checked by a verifier on 2026-10-22: VERIFY.md lists what was checked, what was corrected in place below and what stays unconfirmed.
INSPIRATION.md; the coordinator added them to this brief. The nearest line is the end of row 11: "Then it can be like the other menu I shared earlier" (Büro's sheet, row 6). The panel is also the closest real-world match to his drawer (row 7): "it slides out like a drawer from the bottom of the page".| Layer | Finding | Evidence |
|---|---|---|
| Framework | Next.js 16.4.1, App Router, built with Turbopack | window.next={version:"16.4.1"} in chunk 33lex72pdubj6.js; response headers x-powered-by: Next.js, x-matched-path: /[lang]/work, vary: rsc, next-router-state-tree, …; runtime chunk turbopack-1cvbnb3nlabgn.js; static path /_next/static/immutable/chunks/ |
| UI runtime | React 19.3.0-canary-278794d7-20261002 (the React that ships inside Next) | version string in chunks 2kzs590tmh7cs.js, 1zpyotba8ii9p.js, 33lex72pdubj6.js |
| Styling | CSS Modules with hashed prefixes (HUhwLa_navigationBar, uS33ha_animatedPanel, _7O2wwa_media), fluid root size |
class names in the HTML; html{font-size:calc(4.51977vw - 2.71186px)} under 768 px and calc(.892857vw + 3.14286px) above (measured 16 px at 1440, 14.92 px at 390), so every rem scales with the viewport |
| CMS | Storyblok | images from a.storyblok.com/f/238272/… with Storyblok image-service filters (/m/40x0/filters:quality(20):blur(5) for placeholders); Storyblok content fields in client code (_uid, full_slug, cached_url, component: "caseCardModule"); no editor bridge in the public bundles, so content is fetched on the server |
| Hosting and CDN | Vercel | headers server: Vercel, x-vercel-id, x-vercel-cache, cdn-cache-control: public, s-maxage=60, stale-while-revalidate=300; images through /_next/image |
| Animation | GSAP 3.15.0 core with ScrollTrigger and Flip, plus plain CSS transitions | "3.15.0" and GreenSock/ScrollTrigger code in 3fqn2w4mtgg9r.js; the Flip plugin (killFlipsOf, isFlipping, tj.version="3.15.0") registered by the work-grid module in 3l4qdozz7-kr6.js; no Framer Motion or Motion code found; GSAP is bundled, not on window (probe globals empty) |
| Scroll | Native scrolling with CSS smooth scroll; no Lenis or Locomotive | html{scroll-behavior:smooth;overscroll-behavior:none;overflow-x:hidden}; <html data-scroll-behavior="smooth"> (Next's opt-in that keeps route changes from smooth-scrolling); probe found no scroll library |
| Page transitions | Hand-written shared-element zoom with GSAP (clone the tile media, scale to the viewport, router.push, shrink into the case hero); no View Transitions API |
PageTransitionContext / creatCopyAndAnimateAndScaleIt in 3fqn2w4mtgg9r.js; probe: view-transition CSS rules 0; startViewTransition appears only in the Next and React runtime chunk 33lex72pdubj6.js, and the tile zoom is a GSAP clone, so the site's own transitions do not use it |
| Route progress | nextjs-toploader (nprogress): a 2 px black bar at the top during navigation | NextTopLoader in 3s3v888l7p6xb.js; the injected #nprogress style in the HTML; the bar is visible in shots/burst-case-zoom-01.png |
| 3D | three.js r156, home page only: a 120 × 120 WebGL2 canvas with the rotating copper monogram | window.__THREE__="156" and WebGLRenderer in the home-only chunk 258637avdowkj.js; probe on /: one webgl2 canvas _7V9nCG_canvas; none on /work |
| Video | Vimeo progressive MP4s as tile and hero video (muted, loop, playsinline, preload="none"), played only in view |
player.vimeo.com/progressive_redirect/… sources (16 of 32 work tiles); an IntersectionObserver hook in 3l4qdozz7-kr6.js |
| Fonts | IvyPresto Display Thin (headlines), Messina Sans 300/400/700 with italics (text), DM Mono 300/400/500 with italics (labels, the bar) | self-hosted /fonts/IvyPresto/…, /fonts/Messina/…, /fonts/DM_mono/… @font-face rules; IvyPresto also through next/font (IvyprestoFont, meta name="next-size-adjust"); probe document.fonts |
| Analytics | Google Tag Manager GTM-P2KJHNZW loading GA4 G-EFEE0RW5BS; Vercel Speed Insights | gtm.js?id=GTM-P2KJHNZW preload; gtag/js?id=G-EFEE0RW5BS in the probe's script list; /_vercel/speed-insights/script.js |
| Languages | English and Dutch (/nl/werk), with a "view this page in …" suggestion from navigator.languages, dismissal kept in localStorage (ym-language-suggest-dismissed) |
hrefLang alternates; language code in 3s3v888l7p6xb.js |
Scrollbar treatment. The probe's 16 scrollbar rules on /work are eight pairs of the same hide-it pattern, -ms-overflow-style:none; scrollbar-width:none plus ::-webkit-scrollbar{display:none}, applied only to inner scrollers: the panel's scroll wrapper, the filter-tab strip, the sliders and some page wrappers (expertise, article, company, impact). The document scrollbar is left native (scrollbar-width: auto on html). So the site hides every scrollbar it owns and leaves the page's own alone. For ETS27 (row 9, "invisible scroll bars and less scrolling") the same two lines on every scroller we own is the whole recipe; hiding the document's bar is one more rule on html.
Timings and easings. The site runs on a small fixed vocabulary. Most bar and panel durations are multiples of a sixteenth of a second (63, 125, 188, 313 ms in CSS; 375 and 438 ms in GSAP), with the monogram's 156 ms (5/32 s) and the 44 ms exit delay off that grid, the CSS ease is cubic-bezier(.76,0,.24,1) (ease-in-out quart) with cubic-bezier(.65,0,.35,1) (ease-in-out cubic) for page-level fades, and GSAP carries the springs and the long moves (elastic.out(0.9,0.9), back.out(1.7), power3/4, expo.inOut, expo.out). The full table is in section 4.
What it is, in pixels. Desktop (1440 × 900, root 16 px), measured by the css step:
State (data-state) |
Shows | Width × height | Shot |
|---|---|---|---|
default |
MENU, a 1 × 24 px separator, CONTACT | 250 × 49 px | shots/bar-d01-arrival.png, shots/bar-d04-awake-hover.png |
collapsed |
the white monogram (31 × 36 px) | 99 × 49 px | shots/bar-d02-rest.png |
close (a panel is open) |
CLOSE and a 12 px cross | 180 × 49 px | shots/menu-d01-open.png |
Fixed at bottom: 33px (2.0625rem), centred with left: 50%; transform: translate(-50%, var(--offsetY)), border-radius: 9999px, background: #000, box-shadow: 2px 2px 6px 2px rgba(0,0,0,.1), overflow: hidden, z-index: 6. Labels are DM Mono 17 px (1.0625rem) uppercase, letter-spacing 0.8 px, white, turning #579aff on hover (150 ms linear). The separator is #d8d8d8 with 20 px margins. Phone (390 × 844, root 14.92 px): 233 × 45.7 px awake, 103 px at rest, 130 px with a panel open, bottom: 28px (shots/bar-p01-arrival.png, bar-p02-rest.png, bar-p07-contact-open.png).
DOM. Three layers stacked at the centre of one pill, only one visible at a time (trimmed; full excerpt in sources/bar-panels-tile.html):
<nav data-state="default" class="HUhwLa_navigationBar">
<div class="HUhwLa_closeWrapper HUhwLa_menuSection HUhwLa_hidden">
<button class="HUhwLa_closeButton" aria-label="Toggle menu"><span class="HUhwLa_navLink">Close</span><svg …/></button>
</div>
<div class="HUhwLa_navWrapper HUhwLa_menuSection"> <!-- gets HUhwLa_collapsed or HUhwLa_hidden -->
<div class="HUhwLa_navWrapperInner">
<button aria-label="Open Menu" class="HUhwLa_navLink">Menu</button>
<div class="HUhwLa_separator"></div>
<button aria-label="Open Contact" class="HUhwLa_navLink">Contact</button>
</div>
</div>
<div class="Z6WgPW_animatedLogo" style="opacity:0;transform:translate(-50%, -50%) scale(2)"><svg viewBox="0 0 31 36">…</svg></div>
</nav>
<button class="m57DwW_arrowWrapper m57DwW_hidden" aria-label="Scroll to top">…</button>
<section class="uS33ha_animatedPanel">…menu…</section>
<section class="uS33ha_animatedPanel">…contact…</section>
Every layer is position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) inside the pill. Because the layers are absolute, the pill has no intrinsic width: JavaScript sets it.
Behaviour: what triggers the rest state. Read from 3s3v888l7p6xb.js (excerpt in sources/bar-panels-transition.js.txt) and confirmed state by state in steps/bar-desktop-states.json (results in sources/capture-evidence.json):
data-state = panelOpen ? "close"
: (pageTransitionRunning || (scrollingDown && !engaged)) ? "collapsed"
: "default"
scrollingDown = on every window scroll: scrollY > lastScrollY && scrollY > (innerWidth < 769 ? 60 : 100)
engaged = mouseenter / mouseleave on the nav, focus inside it, blur unless focus moved within it
default, at 130 px it went collapsed (desktop threshold 100 px); on the phone 50 px stayed default, 75 px collapsed (threshold 60 px). Any upward scroll wakes it (a 200 px wheel up: default, shots/bar-d05-after-scroll-up.png). Five seconds idle in either state changed nothing: there is no timer.collapsed (shots/bar-d03-pointer-near-not-on.png); on the oval it opened (bar-d04-awake-hover.png); leaving collapsed it again. The "opens when the pointer comes near" impression is the oval itself; there is no proximity radius.shots/bar-d06-keyboard-focus.png).shots/bar-p03-after-tap.png). capture.cjs sends mouse clicks, not touch taps, so every phone "tap" in this folder is mouse input in a 390 px touch-emulated page; a real tap's compatibility mouse events should behave the same, but that was not tested.collapsed for their duration (section 4).Behaviour: the morph. Two systems run together:
[panelOpen, width, collapsed, isHome]): gsap.to(nav, { width: activeLayer.offsetWidth, ease: "elastic.out(0.9,.9)", duration: .438, delay: .063 }). The rAF samples show the spring: waking 99 → 248 → 251 → 250 px, sleeping 250 → 111 → 93 → 99 px, opening a panel 99 → 183 → 177 → 180 px.scale(.6) (313 ms cubic-bezier(.76,0,.24,1), 44 ms delay), the separator folds to scaleY(0); the monogram fades in (156 ms linear, 63 ms delay) while dropping from scale(2) to scale(1) (313 ms, same curve). Waking is not a mirror: the default state's transition list holds only opacity (and visibility), so the words snap back from scale(.6) to full size and fade in (313 ms linear after 63 ms), revealed by the widening pill, while the monogram fades out (156 ms after 63 ms) and grows back to scale(2) (313 ms); the separator unfolds last (125 ms, 188 ms delay; 313 ms delay on the phone). The bursts caught one in-between frame each way: shots/burst-bar-collapse-sheet.png frame 00 (full-width pill, words fading, monogram arriving) and shots/burst-bar-wake-sheet.png frame 00 (pill half open, monogram still showing).Behaviour: the footer. An IntersectionObserver on footer with a thousand thresholds writes --nav-offset (the negative visible height of the footer) on body; the pill's --offsetY reads it, so the bar rides up on top of the footer instead of covering it (measured −210 px on desktop, −398 px on the phone at the end of the page: shots/bar-d07-footer.png, bar-p06-footer.png). When more than 20 % of the footer shows, a thin up arrow (16 × 24 px SVG, bottom: 100px, same offset) fades in above the bar and smooth-scrolls to the top. Opening a panel tweens the offset to 0 (375 ms expo.inOut, no delay); closing it, or a route change, tweens it back to the stored footer offset (0 when scrollY is 0) after 63 ms. While scrolling, including back to the top, the observer sets the variable directly with gsap.set, so the bar follows the footer without a tween.
Assets. The monogram is one inline SVG path (viewBox 0 0 31 36, fill: currentColor), the cross is a 12 × 11 SVG, the arrow a 16 × 24 SVG. No images, no filters, no backdrop blur on /work (probe: 0 backdrop-filter elements, 0 SVG filters).
Accessibility and reduced motion. Real buttons with labels ("Open Menu", "Open Contact", "Toggle menu", "Scroll to top"); focus wakes the bar and shows a ring. The collapsed words are opacity: 0; pointer-events: none but stay in the tab order, which is what lets focus wake them. There is no aria-expanded or aria-controls, the panels are not dialogs (no focus trap, no Escape), and no prefers-reduced-motion handling was found in the CSS or JS read (the only reduced-motion rule is for a video shimmer on the home page).
Gotchas.
- The blur quirk. Clicking a button inside the bar focuses it; when that button's layer hides 313 to 357 ms later (visibility: hidden), focus falls to body and onBlur clears engaged. If the last scroll was downward past the threshold (scrollingDown still true), the bar then collapses with the pointer still resting on it, and it reopens only after the pointer leaves and comes back. At the top of the page it stays awake: the phone check after Close read default with BODY focused, and the burst-menu-close frames stay awake. Seen in the samples at 1400 px down (after Close: default at 449 ms, collapsed at 1137 ms), in a forced click on Contact that then missed (one sample at 75 ms read collapsed with no panel; shots/bar-d09-sampler-contact-click-missed.png was taken after a further 120 px wheel scroll, which would close a panel anyway, so the shot does not show the miss by itself), and on the phone, where a forced click on Contact opened nothing (bar-p05-contact-tap-missed.png; the next Close click timed out because no panel was open) and a second click at the same spot left it at rest (shots/bar-p08-second-tap.png). Headless Chromium only; see section 7.
- Width from measurement. The width is read from offsetWidth in an effect, so it is only as right as the fonts loaded at that moment; they re-measure on resize and on each state change.
- Scroll closes panels. Any scroll event closes an open panel (shots/contact-d03-scroll-closes-panel.png), which is right for a page but would fight a panel with its own scrolling content on touch.
How to rebuild it for ETS27. Library-agnostic recipe; the bar of bar-stage-drawers.md (rest, awake, in use) maps one to one onto collapsed, default, close:
overflow: hidden; border-radius: 9999px; transform: translate(-50%, var(--bar-offset)).data-state; drive the layer crossfades from CSS with the same pair of moves (outgoing: fade 313 ms linear and scale to .6; incoming mark: fade and scale from 2 to 1).elastic.out(0.9, 0.9) over 0.44 s (as they do), or Motion's animate(bar, { width }, { type: "spring" }), or a FLIP with a CSS linear() spring curve. For ETS27 the awake row holds the ask field, so let the field grow on focus and re-measure.pointerenter, on focusin; no timers. Keep the ask field's focus as "engaged", so typing never collapses the bar.pointerenter/pointerleave plus :focus-within, and when a layer hides, move focus to the corresponding control in the next layer instead of letting it fall to body.aria-expanded and aria-controls on Menu and Contact and close on Escape.Libraries that make it easy: GSAP core (width tween and easing), Motion (springs, animate), or none (CSS transitions plus one measured width). Confidence: high (source plus captured states).
What they are, in pixels. A black card with 20 px corners, 415 × 680 px on desktop (width: 25.9375rem; height: calc(100svh - 120px); max-height: 42.5rem), fixed at bottom: 104px and centred, so it floats 22 px above the bar; z-index: 5, under the bar (6). Phone: 366 × 634 px (max-width: calc(100vw - 24px)), bottom: 97px (shots/bar-p04-menu-open.png, bar-p07-contact-open.png). Content is a flex column centred vertically (row gap 25 px, padding 40 px) in an inner scroller with the scrollbar hidden.
shots/menu-d01-open.png): Work, Impact, Expertise, Perspectives, Testimonials, Company, Infinum in Messina 700, 32 px, line height 1.5, white, the current page in #579aff; then Careers, Get In Touch, Newsletter in Messina 400, 24 px; then EN / NL with the current language at 50 % opacity. Hover turns any link #579aff (shots/menu-d02-link-hover.png). Get In Touch swaps to the contact panel (shots/contact-d02-from-get-in-touch.png).shots/contact-d01-open.png): the heading "Contact Us" (Messina 32 px), New Business (/contact) and Book A Call (a Google Calendar booking page) as primary links, then Email Amsterdam, Email New York, Email London as mailto: links.menuPrimaryLink, menuSecondaryLink or menuOpenContact.DOM. section.uS33ha_animatedPanel > div.uS33ha_menuBackground + div.uS33ha_scrollableContentWrapper > div.uS33ha_contentWrapper > (h2) ul.b56tGa_list ul.b56tGa_subList (div[data-panel-item] language switch). The background is a separate empty layer (transform-origin: 50% 100%), so the card can be scaled without distorting the text.
Behaviour. One GSAP timeline per panel (excerpt in sources/bar-panels-transition.js.txt):
timeline({ delay: .156 }), set display: block; pointer-events: auto; background scaleY 0 → 1 (0.65 s power3.inOut) and scaleX .02 → 1 (0.65 s back.out(1.7), back.out(1.2) under 769 px) together; items (svg, a, button, h2 outside [data-panel-item], plus [data-panel-item]) y: 108% → 0, opacity 0 → 1, 0.7 s power4.out, stagger 0.07 s, starting at 0.37 s; then timeScale(2.4) (2.08 on the phone). In real time on desktop, computed from those values: starts about 65 ms after the click, the card is full size by about 335 ms, the eleven menu items rise one every 29 ms from about 220 ms and the last settles at about 0.8 s.pointer-events: none; background to scaleY 0, scaleX .02 (0.6 s power4.inOut), items to y 108%, opacity 0 (0.2 s power4.in, stagger 0.01 s), timeScale(1.92): about 313 ms in all.shots/burst-contact-open-sheet.png frame 00 (a short black slab growing out of the bar while its labels crossfade to CLOSE) and shots/burst-menu-close-sheet.png frame 00 (the card shrinking, the top items already gone, the lower ones fading).shots/menu-d03-after-escape.png, state still close). No click-outside handler in the component.Phone. Same card and timeline with the softer back.out(1.2) and timeScale(2.08); the card is nearly full width and sits above the thumb (shots/burst-phone-menu-open-sheet.png; frame 00 caught only the first instant, a wide black sliver a few pixels tall at the card's bottom edge while the bar's labels crossfade to CLOSE, and frame 01 is already the end state).
Accessibility and reduced motion. Plain lists of links and buttons, no dialog role, no focus move into the panel on open, no Escape; the panel's scrollbar is hidden. No reduced-motion branch.
How to rebuild it for ETS27. This is the drawer of row 7, grown out of the bar:
transform-origin: 50% 100%.scaleY from 0 with an in-out curve and scaleX from a sliver with a back-out overshoot, about 0.27 s; then the content rows rise from 108 % with opacity, 0.29 s each, 29 ms apart. Close at about 0.3 s, reversed and faster.animate([...]) with stagger), or CSS alone: @starting-style plus transition on the background's scale with a linear() back-out curve, and transition-delay: calc(var(--i) * 29ms) on the rows.role="dialog", aria-modal for a modal drawer, focus moved in on open and back to the bar on close, Escape to close, a reduced-motion branch. For speaker and session drawers that scroll, do not close on scroll; close on drag-down instead.Libraries that make it easy: GSAP (timeline, stagger), Motion (sequence, stagger), or CSS @starting-style. Confidence: high.
What it is, in pixels. The work grid is two columns of large tiles for the first four cases (670 × 379 px media, row gap 140 px, column gap 20 px), then three columns of compact tiles (Vn_v4q_threeColumns: 28 tiles, 440 × 249 px media, row gap 80 px, column gap 20 px at 1440 px; the breakpoints were not read). Media boxes are aspect-ratio: 668/378, 5 px corners; captions are the client in DM Mono uppercase and a one-sentence description in Messina that wraps to two lines on several tiles (the 16 px and 22 px sizes and the three-line maximum were not measured). A sticky white filter row (All, Brand Expression, Experience Realization, Accelerating Innovation) with a moving 2 px underline stays at the top while scrolling.
DOM. article._7O2wwa_articleWrapper > a (next/link wrapper) > div._7O2wwa_mediaWrapper > div (pointer handlers) > div._7O2wwa_mediaContainer > [img._7O2wwa_imagePlaceholder + img._7O2wwa_imageOverlay] | [img._7O2wwa_videoPlaceholder + video._7O2wwa_videoOverlay], then div._7O2wwa_captionWrapper.
Behaviour.
- Tilt from the pointer position (GSAP, 3l4qdozz7-kr6.js): on mousemove the media box tweens to rotationY = (x / width − 0.5) × 24°, rotationX = (y / height − 0.5) × 16°, transformPerspective: 2000, 1.2 s expo.out; on leave back to 0 over 2 s expo.out; frozen once clicked; switched off on touch screens by @media (any-hover: none) { transform: none !important }. Measured near the corners: about ±11° around Y and ±7° around X (shots/tile-d02-tilt-top-left.png, tile-d03-tilt-bottom-right.png, tile-d01-pointer-on-tile.png). The two axes have opposite senses: the side under the pointer horizontally recedes while the side under it vertically comes forward, so with the pointer near the top-left corner the left edge sinks, the top edge lifts and the top-right corner is nearest the viewer (shots/verify/verify-tilt-top-left.png, inline perspective(2000px) rotateY(-10.8179deg) rotateX(-6.69172deg)); near the bottom-right corner the bottom-left corner is nearest. The card does not simply lean toward the pointer.
- Blur-up images: a 40 px wide Storyblok render (quality 20, blur 5) sits under the full image, which fades in over 0.5 s once loaded.
- Preview video: in view, not on hover. Sixteen of the thirty-two tiles carry a Vimeo progressive MP4 (muted loop playsinline preload="none"). An IntersectionObserver (threshold 0) calls play() when the tile enters the viewport and pause() when it leaves. Captured: the in-view TOO BUSY tile playing (readyState 4, currentTime 1.33 s, shots/tile-d04-video-tile-in-view.png) while the off-screen video tiles stayed paused at readyState 0, so nothing downloads until a tile is seen.
- Filter: the grid module registers GSAP Flip, which is the usual tool for reflowing cards when a tab filters them; the filter animation itself was not captured (section 7).
How to rebuild it for ETS27. For speaker cards on the stage: the same tilt (±12° and ±8°, perspective 2000 px, expo.out 1.2 s, slow 2 s return; choose each axis's sign on purpose, since theirs are mixed) reads as weight without effects; play any card video only while it is on screen with preload="none"; use the blur-up pair for every photo. Easy with GSAP quickTo or Motion useSpring for the tilt, or plain pointermove writing two CSS variables into a transform with a transition. Confidence: high.
Work tile to case page (the shared-element zoom). Source in 3fqn2w4mtgg9r.js and 277e5h908hh8i.js; timeline from steps/case-transition-sampler.json (rAF samples in sources/capture-evidence.json), frames in shots/burst-case-zoom-sheet.png, burst-case-settle-sheet.png, burst-video-case-sheet.png, burst-phone-case-sheet.png.
preventDefault() and hands the anchor to clickLink. The bar is forced to rest (transitionRunning), and the top loader starts a 2 px black bar (burst-case-zoom-01.png).img) is cloned into a fixed overlay (.NpnXJW_copyBgc, full screen, z-index: 5, under the bar), placed flat over the tile's on-screen bounding box with its 5 px corner radius; a video clone is muted, set to the same currentTime and played, so the footage does not jump. The code tries to copy the tilt by parsing the media element's own inline transform, but the tilt lives on the parent mediaContainer, so the parse comes back empty and the clone starts untilted (verifier check: clone inline translate3d(0px, 0px, 0px), computed identity, rect [49, 158, 667 × 389], equal to the tilted container's bounding box; the parser would also mangle the units, perspective(2000pxpx), if it did read the container). On click the tilted tile is covered by a flat rectangle a few pixels larger than it.top 0, left 0, width 100vw, height 100%, borderRadius 0, rotateX 0, rotateY 0 (the rotations are no-ops, the clone has none), 1.2 s expo.inOut. Samples: the clone appears at the tile rect [36, 163, 669 × 384] about 0.37 s after the click, is at [25, 114, 899 × 538] by 0.83 s and fills the viewport by about 1.3 s (burst-case-zoom-01.png, -02.png: the tomato full bleed with the monogram bar).router.push(href); the path changed at about 2.2 s (RSC fetch included). The clone stays over everything, so the page swap underneath is invisible./work/ path and tweens the overlay to the case hero's height and top (1.2 s expo.inOut), so the full-bleed picture slides down into the hero box ([0, 404, 1440 × 810] on desktop), while the case title and headline rise from y: 50 and opacity: 0 starting 0.15 s in. For a video hero the timeline pauses until the destination video can play, then syncs its time. The overlay is removed on complete. Samples: hero position reached by about 4.1 to 4.5 s; the bar woke at 2.4 s and sprang open at about 3.6 s. The video burst caught the title mid-fade over the arriving hero (burst-video-case-sheet.png frame 02).clickLink sets transitionIsRunning and builds a clone only when the link holds media, so plain links (menu, footer) just push the route with the top loader and leave the bar alone. When a media link leads to a page that is neither a /work/ case nor an /article/ page (articles settle into their own hero the same way as cases), the overlay fades out over 0.8 s linear instead of settling.In a real browser the sequence is 1.2 s zoom, the fetch, 1.2 s settle; the headless timings above include fsit2's slow frames. On the phone the zoom is the same (tap, full bleed, settle; burst-phone-case-sheet.png), without the tilt.
Within the page. Bar rest and wake (3.1, burst-bar-collapse-sheet.png, burst-bar-wake-sheet.png); panel open, close and switch (3.2, burst-menu-open-sheet.png, burst-menu-close-sheet.png, burst-contact-open-sheet.png, burst-menu-to-contact-sheet.png, burst-phone-menu-open-sheet.png); the "Work" heading fades in over 0.8 s cubic-bezier(.65,0,.35,1) on load and the filter row 0.1 s after it.
Timing table (source values; GSAP panel values also given in real time after timeScale):
| Motion | Duration | Easing | Delay | Where |
|---|---|---|---|---|
| Bar width | 438 ms | elastic.out(0.9, 0.9) |
63 ms | GSAP |
| Words out (rest) | 313 ms opacity, 313 ms scale to .6 | linear; cubic-bezier(.76,0,.24,1) |
44 ms | CSS |
| Words in (wake) | 313 ms opacity | linear | 63 ms | CSS |
| Monogram in | 156 ms opacity, 313 ms scale 2 to 1 | linear; cubic-bezier(.76,0,.24,1) |
63 ms on opacity | CSS |
| Separator unfold | 125 ms scaleY |
cubic-bezier(.76,0,.24,1) |
188 ms (313 ms phone) | CSS |
| Link colour | 150 ms | linear | CSS | |
| Footer offset (to 0 when a panel opens; back to the footer offset when it closes or the route changes; scrolling sets it directly) | 375 ms | expo.inOut |
none on open, 63 ms otherwise | GSAP |
| Scroll-to-top arrow | 156 ms opacity | linear | CSS | |
| Panel open | bg 650 ms scaleY and scaleX; items 700 ms, stagger 70 ms from 370 ms; all at 2.4× (≈271 ms, ≈292 ms, 29 ms, from ≈154 ms) | power3.inOut; back.out(1.7); power4.out |
156 ms (≈65 ms) | GSAP |
| Panel close | bg 600 ms; items 200 ms, stagger 10 ms; all at 1.92× (≈313 ms, ≈104 ms) | power4.inOut; power4.in |
GSAP | |
| Tile tilt | 1.2 s in, 2 s back | expo.out |
GSAP | |
| Tile image or video fade-in | 500 ms | ease-in-out | CSS | |
| Tile zoom to viewport | 1.2 s | expo.inOut |
GSAP | |
| Settle into case hero | 1.2 s; title and headline from y 50 | expo.inOut |
headings at 150 ms | GSAP |
| Overlay fade (non-case routes) | 800 ms | linear | GSAP | |
| Page heading and filter row fade | 800 ms | cubic-bezier(.65,0,.35,1) |
100 ms on the filter row | CSS |
All headless Chromium on fsit2; desktop 1440 × 900 at 1×, phone 390 × 844 at 2×. Bursts are numbered frames <name>-00.png onward, with the frame times in sources/capture-evidence.json; frames on fsit2 came 140 to 1855 ms apart (the longest gaps right after a click) and the first frame landed 0.15 to 1.6 s after the burst began, so most bursts hold one in-between frame at most. Four files were renamed after review because they did not show what their step intended; the steps files keep the original names.
| File | What it shows | Device |
|---|---|---|
bar-d01-arrival.png |
/work on arrival, bar awake: MENU, CONTACT |
desktop |
bar-d02-rest.png |
scrolled 1400 px, bar at rest: the monogram oval | desktop |
bar-d03-pointer-near-not-on.png |
pointer 80 px right of centre, outside the oval: still at rest | desktop |
bar-d04-awake-hover.png |
pointer on the oval: awake again | desktop |
bar-d05-after-scroll-up.png |
after a 200 px wheel up: awake | desktop |
bar-d06-keyboard-focus.png |
Menu button focused from the keyboard: awake, white focus ring | desktop |
bar-d07-footer.png |
end of page: bar lifted 210 px above the footer, up arrow shown | desktop |
bar-d08-sampler-menu-open.png |
menu panel open mid-page, CLOSE in the bar | desktop |
bar-d09-sampler-contact-click-missed.png |
bar at rest after the blur quirk, the forced Contact click and a 120 px wheel scroll (renamed; the scroll alone would close a panel, so the miss rests on the sampler and the phone run) | desktop |
bar-p01-arrival.png |
phone arrival, bar awake | phone |
bar-p02-rest.png |
phone scrolled, monogram oval | phone |
bar-p03-after-tap.png |
tap on the oval: awake | phone |
bar-p04-menu-open.png |
menu panel on the phone | phone |
bar-p05-contact-tap-missed.png |
a forced tap on Contact after the blur quirk: bar at rest, no panel (renamed) | phone |
bar-p06-footer.png |
phone end of page: bar above the footer, up arrow | phone |
bar-p07-contact-open.png |
contact panel on the phone | phone |
bar-p08-second-tap.png |
second tap on the resting oval at the same spot: stays at rest | phone |
menu-d01-open.png |
menu panel open, current page in blue, CLOSE in the bar | desktop |
menu-d02-link-hover.png |
hover on Impact: blue | desktop |
menu-d03-after-escape.png |
after Escape: panel still open | desktop |
contact-d01-open.png |
contact panel open | desktop |
contact-d02-from-get-in-touch.png |
contact panel reached from the menu's Get In Touch | desktop |
contact-d03-scroll-closes-panel.png |
after a 150 px scroll: panel gone, bar at rest | desktop |
tile-d01-pointer-on-tile.png |
pointer on the Tomato tile: slight tilt (renamed) | desktop |
tile-d02-tilt-top-left.png |
pointer near the tile's top-left corner: tilt | desktop |
tile-d03-tilt-bottom-right.png |
pointer near the bottom-right corner: opposite tilt | desktop |
tile-d04-video-tile-in-view.png |
TOO BUSY video tile playing in view, bar at rest | desktop |
case-d01-before-click.png |
grid with the Tomato tile tilted under the pointer, just before the click | desktop |
case-d02-arrived.png |
/work/tomato settled: title, headline, hero, bar awake |
desktop |
case-d03-scrolled.png |
case page scrolled: services list in DM Mono, "The Challenge" | desktop |
case-d04-sampler-end.png |
end state of the sampled transition | desktop |
case-d05-video-case-arrived.png |
/work/too-busy-foundation settled after a video tile zoom |
desktop |
case-p01-before-tap.png |
phone grid, bar at rest | phone |
case-p02-arrived.png |
phone case page settled | phone |
case-p03-accidental-tile-tap-zoom.png |
an unplanned click on the Accessiway tile: its image full bleed in the settle phase, after the route change (the bar is already awake) (renamed) | phone |
home-d01-arrival.png |
/: rotating 3D monogram, card, IvyPresto headline, bar awake |
desktop |
home-d02-scrolled.png |
/ after a scroll attempt: one-screen page, bar stays awake, next card sliding in |
desktop |
burst-bar-collapse-00..11.png, burst-bar-collapse-sheet.png |
bar going to rest (sheet cropped to the bar; frame 00 caught mid-way) | desktop |
burst-bar-wake-00..11.png, burst-bar-wake-sheet.png |
bar waking on hover (frame 00 caught mid-way) | desktop |
burst-menu-open-00..13.png, burst-menu-open-sheet.png |
menu panel opening (before and after only) | desktop |
burst-menu-close-00..09.png, burst-menu-close-sheet.png |
menu panel closing (frame 00 caught mid-way) | desktop |
burst-contact-open-00..13.png, burst-contact-open-sheet.png |
contact panel opening (frame 00 caught the grow) | desktop |
burst-menu-to-contact-00..11.png, burst-menu-to-contact-sheet.png |
Get In Touch swapping menu for contact | desktop |
burst-case-zoom-00..15.png, burst-case-zoom-sheet.png |
tile zoom: grid, full bleed with loader, arrival | desktop |
burst-case-settle-00..13.png, burst-case-settle-sheet.png |
case page settled | desktop |
burst-video-case-00..15.png, burst-video-case-sheet.png |
video tile zoom: full-bleed video, title mid-fade (frame 02), hero | desktop |
burst-phone-menu-open-00..11.png, burst-phone-menu-open-sheet.png |
phone menu opening (frame 00: the first sliver of the card and the bar's labels crossfading; then the end state) | phone |
burst-phone-case-00..15.png, burst-phone-case-sheet.png |
phone tile zoom: full bleed, arrival | phone |
verify/verify-tilt-top-left.png |
verifier: pointer near the Tomato tile's top-left corner, left edge sunk, top edge lifted; verify/meta.json holds that run's evals |
desktop |
The earlier screenshots in ../../yourmajesty/ (desktop-top.png, desktop-scrolled.png, mobile-top.png) show the same awake and rest states and were not recaptured for that purpose.
https://yourmajesty.co/_next/static/immutable/chunks/: 3g24b-8ifpqzj.css, 3dq1zaevs20nk.css, 22f7hzxmuwp5b.css, 3swt12n07ue2n.css, 1sn_dg1u-0ouz.css2kzs590tmh7cs.js, 33lex72pdubj6.js, turbopack-1cvbnb3nlabgn.js, 00z4pmesk2f4f.js, 1u5duijecp4kl.js, 3a5kldecc8_2f.js, 3s3v888l7p6xb.js (bar, panels, top loader), 3fqn2w4mtgg9r.js (GSAP, page transition), 277e5h908hh8i.js (link wrapper, filter), 3l4qdozz7-kr6.js (tiles, Flip), 0c0hxoamwjsbw.js, 1zpyotba8ii9p.js, 2n7c4ig570s4k.js, 258637avdowkj.js (three.js, home), 0p4_x7pkzxkiu.jssources/bar-panels-tiles.css, sources/bar-panels-transition.js.txt, sources/bar-panels-tile.html; measurements, samples and probes: sources/capture-evidence.jsonsteps/): bar-desktop-states.json, bar-desktop-bursts.json, bar-desktop-sampler.json, menu-panel-desktop.json, contact-panel-desktop.json, tile-hover-desktop.json, case-transition-desktop.json, case-transition-sampler.json, video-case-transition-desktop.json, bar-phone.json, bar-phone-contact-and-retap.json, case-transition-phone.json, home-desktop.jsondesign/inspiration/INSPIRATION.md (rows 6, 7, 9, 11, 18), design/inspiration/bar-stage-drawers.md, design/inspiration/components-to-copy.md, design/inspiration/teardowns/_examples/*.json, design/inspiration/yourmajesty/*.pngdata-state flipped, although React writes both in the same commit), so the timings in section 4 are source values, and the effective panel numbers are computed from them. A 60 fps screen recording in a headed browser on real hardware would establish them.prefers-reduced-motion handling was found in the CSS and JS read, but the site was not run with the preference set; capture.cjs has no media-emulation step (a one-line page.emulateMedia({ reducedMotion: 'reduce' }) step would add it).cursor: none on three elements, 35 backdrop-filter elements, 28 videos); its components were not torn down because the owner pointed at /work.Verifier pass on 2026-10-22 UTC (the evening of 2026-10-21 local). The aim was to refute the teardown's claims, not to restate them. Result: 37 claims held, 17 statements were wrong or overstated and are corrected in place in README.md and component.json, and 8 points stay unconfirmed.
README.md and component.json and listed every stack, mechanism, timing and measurement claim.sources/capture-evidence.json (the agent's condensed meta.json evals, css steps, probes and burst times), the quoted source in sources/bar-panels-transition.js.txt, sources/bar-panels-tiles.css and sources/bar-panels-tile.html, and the steps files. Timeline arithmetic was redone from the quoted values; tilt angles were decomposed from the measured matrix3d values.bar-d01-arrival, bar-d02-rest, bar-d03-pointer-near-not-on, bar-d06-keyboard-focus, bar-d07-footer, bar-d09-sampler-contact-click-missed, bar-p08-second-tap, menu-d01-open, contact-d01-open, contact-d03-scroll-closes-panel, tile-d03-tilt-bottom-right, tile-d04-video-tile-in-view, case-d02-arrived, case-p03-accidental-tile-tap-zoom, home-d01-arrival, burst-case-zoom-01, burst-phone-menu-open-00, the sheets burst-bar-wake, burst-bar-collapse, burst-contact-open, burst-menu-close, burst-video-case, burst-phone-menu-open, and the fresh verify/verify-tilt-top-left.design/tools/capture-remote.sh: steps/verify-stack-tilt-clone.json, results in shots/verify/ (meta.json, verify-tilt-top-left.png). 23 steps, 0 step errors, 0 page errors, 19.7 s. Requests: one load of /work, one HEAD of /work for the response headers, the page's own JS chunks read back in-page with fetch(..., { cache: 'force-cache' }) and grepped for version and library strings, one tile click through to /work/tomato, one load of /. No media was fetched by the verifier and nothing was saved but the one screenshot and the meta.json.Stack
window.next.version is "16.4.1" at runtime on /work and /; "16.4.1" is in 33lex72pdubj6.js; headers x-powered-by: Next.js, x-matched-path: /[lang]/work, vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch; the Turbopack runtime chunk is in the probe's script list.2kzs590tmh7cs.js, 33lex72pdubj6.js and 1zpyotba8ii9p.js.creatCopyAndAnimateAndScaleIt) in 3fqn2w4mtgg9r.js; bundled, not on window (probe globals empty).killFlipsOf, version 3.15.0) in the work-grid chunk 3l4qdozz7-kr6.js./work chunks.menuPrimaryLink, menuSecondaryLink, menuOpenContact in 3s3v888l7p6xb.js; no StoryblokBridge, storyblok-v2-latest or storyblok-js-client string in any /work chunk; a.storyblok.com/f/238272/ image paths with /m/40x0/filters:quality(20):blur(5) placeholders in the quoted HTML and source.server: Vercel, x-vercel-id (iad1), x-vercel-cache: MISS, cdn-cache-control: public, s-maxage=60, stale-while-revalidate=300.NextTopLoader and nprogress's "0.2.0" in 3s3v888l7p6xb.js; the injected style sets #nprogress .bar to background:#000000; height:2px; z-index:1600; the bar is visible top left in burst-case-zoom-01.png.window.__THREE__ is "156" on /; one 120 × 120 canvas _7V9nCG_canvas (webgl2 in the agent's probe); 258637avdowkj.js appears only in the / script list.@font-face sources /fonts/IvyPresto/IvyPresto_Display_Thin.woff2, Messina 300, 400, 700 with italics, DM Mono 300, 400, 500 with italics, plus next/font's IvyprestoFont 100 from ../media/IvyPresto_Display_Thin-s.p…otf (relative to its CSS chunk); meta[name=next-size-adjust] present.gtag/js?id=G-EFEE0RW5BS, gtm.js?id=GTM-P2KJHNZW and /_vercel/speed-insights/script.js in the probe's script list.en /work, nl /nl/werk, x-default /work; ym-language-suggest-dismissed in 3s3v888l7p6xb.js..wpyuCG_expertisePage, .N0NZbq_articlePage, .VN-YRG_sliderWrapper, .uS33ha_scrollableContentWrapper (panel), ._7XWEZa_page, ._1We56W_companyPage, .gsiEBG_tabScrollViewport (filter strip) and .yldjWq_impactPage; html computes scrollbar-width: auto. No reduced-motion rule in the /work CSS and no prefers-reduced-motion string in the /work chunks.html{scroll-behavior:smooth;overscroll-behavior:none;overflow-x:hidden}, data-scroll-behavior="smooth" on html; the two calc() font sizes give 16 px at 1440 and 14.9152 px at 390.Capsule bar
data-state expression and the engagement handlers (onMouseEnter, onMouseLeave, onFocus, onBlur with relatedTarget) match the quoted source.scrollBy is page.mouse.wheel).bar-d06-keyboard-focus.png shows the white ring round MENU.bottom 33 px and 27.97 px; radius 9999 px; box-shadow: 2px 2px 6px 2px rgba(0,0,0,.1); z-index: 6.elastic.out(0.9,.9), 0.438 s, 0.063 s delay, to the active layer's offsetWidth; samples 99 → 248 → 251 → 250 and 250 → 111 → 93 → 99 px.opacity .313s linear 44ms and scale(.6) over .313s cubic-bezier(.76,0,.24,1) 44ms; separator folds; monogram opacity .156s linear 63ms, transform .313s; labels DM Mono 1.0625rem, letter-spacing .05rem, hover #579aff over .15 s; separator #d8d8d8, 1 × 24 px, 20 px margins, .125s after .188s (.313s under 769 px). The computed transition values in the css steps agree.--nav-offset to the footer's negative visible height; −210 px desktop and −398.016 px phone at the end; the arrow shows above 20 %, bottom: 100px, z-index: 4 (bar-d07-footer.png shows the oval and the arrow above the footer).Panels
25.9375rem by calc(100svh - 120px) capped at 42.5rem, bottom: 6.5rem, z-index: 5, background layer with transform-origin: 50% 100% and a 1.25rem radius; css steps 415 × 680 px desktop, 366 × 633.9 px phone, 22 px above the bar.power3.inOut (scaleY) with back.out(1.7) (1.2 on the phone) on scaleX, rows .7 s power4.out stagger .07 from .37, timeScale 2.4 (2.08); close .6 s power4.inOut and .2 s power4.in stagger .01 at 1.92. The README's real-time figures (about 65 ms delay, card full at about 335 ms, rows 29 ms apart from about 220 ms, last row about 0.8 s; close about 313 ms) follow from them.collapsed, and contact-d03-scroll-closes-panel.png shows no panel) and on a transition start (source); Escape leaves it open (eval still close); no aria-expanded, aria-controls or dialog role in the markup.menu-d01-open.png shows Work in blue, the six other primary links, Careers, Get In Touch, Newsletter and a dimmed EN with NL; contact-d01-open.png shows Contact Us, New Business, Book A Call and three Email links. The blue CLOSE in both shots is the hover colour: the pointer still rests on the bar. burst-contact-open frame 00 shows a wide, short slab with the bar's labels overlapping, and burst-menu-close frame 00 a shrinking card with only its lower rows left, as described.Work tile
(x/w - .5) * 24, (y/h - .5) * 16, transformPerspective: 2000, 1.2 s expo.out, 2 s back, frozen once clicked, transform: none !important under (any-hover: none)). Decomposed from the measured matrices: −10.8° Y and −6.7° X at (80, 200), +11.4° and +7.5° at (690, 530), perspective 2000 px (m34 = −0.0005), identity again after leaving. The fresh run's inline style reads perspective(2000px) rotateY(-10.8179deg) rotateX(-6.69172deg).Vn_v4q_threeColumns) 28 tiles in three 440 px columns (row gap 80 px, column gap 20 px); media 670 × 379 and 440 × 249 px.progressive_redirect MP4 (muted loop playsinline preload="none").play() and pause() for muted videos (source); the in-view TOO BUSY video at readyState 4 and currentTime 1.33 s while the off-screen ones stayed at readyState 0; tile-d04-video-tile-in-view.png shows the video frame with the bar at rest..5s ease-in-out.Tile-to-case transition
preventDefault, clickLink, the fixed .NpnXJW_copyBgc overlay (z-index: 5, full screen), the 1.2 s expo.inOut zoom and router.push on complete match the quoted source. The fresh run reached /work/tomato with no overlay left and the bar default.height and y to the hero, headings rise from y: 50 at 0.15 s, a video hero waits for canplay or loadeddata and syncs time, and the overlay is removed on complete (source).default at 2.4 s and wide at 3.6 s, hero [0, 404, 1440 × 810] by 4.1 to 4.5 s.burst-case-zoom-01 full bleed with the loader and the oval; burst-video-case frame 02 the title mid-fade over the dark hero; case-d02-arrived Tomato settled with the bar awake; home-d01-arrival the copper monogram, the card, the IvyPresto headline and the bar awake.Folder
bar-d09-sampler-after-scroll-close, bar-p05-contact-open, bar-p09-tap-elsewhere-then-mark, tile-d01-rest), and every shot named in component.json exists. The bar-phone run's one step error (step 29, the Close click timing out) fits the README's reading: no panel had opened.Each item was wrong or overstated; the text was corrected in place where named.
clickLink parses the media element's own inline transform, but GSAP writes the tilt on the parent mediaContainer, so the parse is empty and the clone starts flat on the tilted tile's bounding box. Fresh check: overlay img inline transform empty, clone inline translate3d(0px, 0px, 0px), computed identity, rect [49, 158, 667 × 389], equal to the tilted container's box. The parser would also produce perspective(2000pxpx) if it read the container. Corrected in README section 4 steps 2 and 3, and transitions[0].layers.verify-tilt-top-left.png); near the bottom-right: the bottom-left corner is nearest (tile-d03). Corrected in README 3.3 (and the rebuild note) and the tile's mechanism, states and rebuild.scrollingDown is still true; at the top of the page focus still falls to body but the bar stays awake (phone eval after Close: default, BODY focused; burst-menu-close frames awake). Corrected in README 3.1 and the bar's first gotcha.scrollY 0, after 63 ms); while scrolling the observer sets the value with gsap.set. Corrected in README 3.1, the timing table and the bar's timings and mechanism.opacity .313s linear .063s, visibility, so the words snap from scale(.6) to full size and fade in. Corrected in README 3.1 and the bar's mechanism.clickLink sets transitionIsRunning and builds a clone only when the link holds media; plain links just push the route. The 0.8 s fade applies when a media link leads to a page that is not a /work/ case or an /article/ page (articles settle like cases). Corrected in README section 4 step 6 and transitions[1].startViewTransition is in the Next and React runtime chunk 33lex72pdubj6.js; the site's own transitions still do not use it (0 view-transition CSS rules, GSAP clone). Corrected in the README stack table and stack.evidence.not_confirmed.bar-d09 does not show the missed click by itself. It was taken after the forced Contact click and a further 120 px wheel scroll, which closes any panel. The miss rests on one sample at 75 ms (collapsed, no panel), the CSS (pointer-events: none on the resting words) and the phone run (no panel, then the Close click timed out). Corrected in README 3.1 and the shot table.capture.cjs has no tap step; page.mouse.click and page.click sent mouse events in a touch-emulated page, so real touch behaviour is untested. Corrected in README 3.1.timeScale 1.92. Corrected in the panels' mechanism.transitionIsRunning is cleared when the new route mounts, at the start of the settle (sample: default at 2.4 s with the overlay still full screen), and the overlay goes when the settle completes. Corrected in transitions[0].layers.case-p03 is the settle phase. The bar is awake, so the route had already changed; the image is still the overlay. Corrected in the shot table.menu-d01-open.png, the value was not read./work has none; / was not checked.navigator.languages: the storage key was found, the trigger was not read.clickNavigationLink (which closes it) is passed to both panels, but the link handler itself is not in the quoted source.data-state flipped, another shows it ahead of the spring, although React writes both in one commit. Only the source values hold.sources/capture-evidence.json and removed the originals with its tmp folder. The condensed evals agree with the steps files, the quoted source and the screenshots, but could not be compared with the raw files.tmp/ folder left (the verifier's scratch folder was removed at the end).sources/capture-evidence.json at 36 KB.shots/bar-d06-keyboard-focus.png at 1.1 MB. The folder is about 72 MB, almost all of it the 197 PNG screenshots and contact sheets.