Your Majesty teardown

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.

1. What the owner liked

2. The stack

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.

3. Component playbooks

3.1 The capsule bar

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

Behaviour: the morph. Two systems run together:

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:

  1. One fixed pill at the bottom centre: overflow: hidden; border-radius: 9999px; transform: translate(-50%, var(--bar-offset)).
  2. Stack the layers absolutely in it: the mark (rest), the awake row (Menu, the ask field, Contact), the in-use row (Close, or the thread controls). Each keeps its natural width.
  3. Compute one state and write it to 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).
  4. Spring the pill's width to the active layer's measured width: GSAP 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.
  5. Rest on scroll-down past a threshold (100 px desktop, 60 px phone), wake on scroll-up, on pointerenter, on focusin; no timers. Keep the ask field's focus as "engaged", so typing never collapses the bar.
  6. Fix their blur quirk: track engagement with 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.
  7. Footer (or drawer) evade with an IntersectionObserver writing the CSS variable.
  8. Reduced motion: switch the spring to an instant width change and keep only opacity crossfades. Add 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).

3.2 The menu and contact panels

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.

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):

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:

  1. The drawer is a fixed card anchored just above the bar, its background a separate layer with transform-origin: 50% 100%.
  2. Open: background 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.
  3. The bar morphs to its in-use layer at the same time (3.1).
  4. Easy with a GSAP timeline (as they do), a Motion sequence (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.
  5. Add what they left out: 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.

3.3 The work tile: hover and preview video

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.

4. Flow and transitions

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.

  1. Click. The tile's link wrapper calls 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).
  2. Clone. The tile's video (or its full-size image, the second 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.
  3. Zoom. GSAP tweens the clone to 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).
  4. Navigate. On complete, 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.
  5. Settle. The new page's effect sees a /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).
  6. Other routes. 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

5. Shots

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.

6. Sources

7. Not confirmed


Verification

Your Majesty teardown: verification

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.

1. How it was checked

2. What held

Stack

  1. Next.js 16.4.1: 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.
  2. React 19.3.0-canary-278794d7-20261002 in 2kzs590tmh7cs.js, 33lex72pdubj6.js and 1zpyotba8ii9p.js.
  3. GSAP 3.15.0 with the GreenSock and ScrollTrigger strings (and creatCopyAndAnimateAndScaleIt) in 3fqn2w4mtgg9r.js; bundled, not on window (probe globals empty).
  4. Flip (killFlipsOf, version 3.15.0) in the work-grid chunk 3l4qdozz7-kr6.js.
  5. No Lenis, Framer Motion or motion-dom string in any of the 12 /work chunks.
  6. Storyblok: 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.
  7. Vercel: server: Vercel, x-vercel-id (iad1), x-vercel-cache: MISS, cdn-cache-control: public, s-maxage=60, stale-while-revalidate=300.
  8. nextjs-toploader: 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.
  9. three.js r156 on the home page only: 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.
  10. Fonts: @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.
  11. Analytics: gtag/js?id=G-EFEE0RW5BS, gtm.js?id=GTM-P2KJHNZW and /_vercel/speed-insights/script.js in the probe's script list.
  12. Languages: hreflang en /work, nl /nl/werk, x-default /work; ym-language-suggest-dismissed in 3s3v888l7p6xb.js.
  13. Scrollbars: the 16 rules are exactly 8 hide pairs, on .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.
  14. Scroll and root size: 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

  1. The data-state expression and the engagement handlers (onMouseEnter, onMouseLeave, onFocus, onBlur with relatedTarget) match the quoted source.
  2. Rest on scroll-down only: threshold 100 px (60 px under 769 px), direction-only, no timer. Evals: 80 px default, 130 px collapsed; phone 50 px default, 75 px collapsed; 5 s idle changed nothing in either state; a 200 px wheel up woke it (scrollBy is page.mouse.wheel).
  3. No proximity radius: pointer 30 px above, and 80 px right of centre (outside the 99 px oval), left it collapsed; on the oval it woke.
  4. Keyboard focus on Menu wakes it from rest; bar-d06-keyboard-focus.png shows the white ring round MENU.
  5. Geometry from the css steps: 250 × 49, 99 × 49 and 180 × 49 px on desktop; 233 × 45.67, 103 and 130 px wide on the phone; bottom 33 px and 27.97 px; radius 9999 px; box-shadow: 2px 2px 6px 2px rgba(0,0,0,.1); z-index: 6.
  6. Width spring 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.
  7. CSS to rest: words 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.
  8. Footer: an IntersectionObserver with 1,001 thresholds sets --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

  1. Card geometry: 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.
  2. Timelines as quoted: open delay .156, background .65 s 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.
  3. The open samples fit simultaneous scale tweens ([0.965, 0.106] then [1.073, 0.951] then [1, 1]) and rows starting at y 51.8 px, which is 108 % of their 48 px height.
  4. Closes on any scroll (eval after 150 px: 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.
  5. Contents: 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

  1. Tilt formula as quoted ((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).
  2. Grid at 1440 px: the first list holds 4 tiles in two 670 px columns (row gap 140 px, column gap 20 px), the second (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.
  3. 32 tiles, 16 of them with a Vimeo progressive_redirect MP4 (muted loop playsinline preload="none").
  4. Video plays only in view: IntersectionObserver with threshold 0 calls 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.
  5. Blur-up: a 40 px wide Storyblok render under the full image, both overlays fading in over .5s ease-in-out.

Tile-to-case transition

  1. The link wrapper's 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.
  2. The settle tweens the overlay's 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).
  3. Sample timeline: clone at the tile at 0.37 s, [25, 114, 899 × 538] at 0.83 s, full viewport by 1.3 s, path change at 2.2 s, bar default at 2.4 s and wide at 3.6 s, hero [0, 404, 1440 × 810] by 4.1 to 4.5 s.
  4. Shots: 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

  1. The four renamed shots map one to one onto the steps files' original names (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.

3. Corrections, verifier, 2026-10-22

Each item was wrong or overstated; the text was corrected in place where named.

  1. The zoom clone does not keep the tile's tilt. Said: placed over the tile "with its corner radius and its current tilt". True: 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.
  2. The tile does not simply tilt toward the pointer. The axes have opposite senses: the side under the pointer horizontally recedes while the side under it vertically comes forward. Pointer near the top-left: the top-right corner is nearest (matrix row 4 and 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.
  3. The blur quirk needs a downward scroll. Said: after a bar click the bar collapses under the resting pointer. True only while 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.
  4. The footer offset tween. Said: opening a panel "or reaching the top" tweens the offset to 0, with a 63 ms delay. True: the tween runs on panel open (to 0, no delay) and on panel close or a route change (back to the stored offset, or 0 at 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.
  5. Waking is not the reverse of resting. The default state's computed transition is only 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.
  6. What re-runs the width tween. Said: every change of state, viewport width or route. True: the effect depends on the panel flag, the rest flag, the viewport width and an is-home-page flag, not on every route. Corrected in README 3.1.
  7. "Any other route" does not force the bar to rest. 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].
  8. View-transition code is present in the runtime. Said: no view-transition code found. True: 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.
  9. Burst frame spacing. Said: 150 to 1000 ms. True: 140 to 1855 ms between frames, and the first frame landed 0.15 to 1.6 s after each burst began. Corrected in README sections 5 and 7 and not_confirmed.
  10. The duration grid. Said: CSS durations sit on a sixteenth-of-a-second grid including 44 and 156 ms. True: 63, 125, 188, 313 ms (and GSAP's 375, 438 ms) are sixteenths; 156 ms is 5/32 s and the 44 ms delay is off the grid. Corrected in README section 2.
  11. The phone menu burst did catch the grow. Said: end state only, frames too far apart. True: frame 00 shows a wide black sliver a few pixels tall at the card's bottom edge with the bar's labels crossfading to CLOSE. Corrected in README 3.2 and the shot table.
  12. 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.
  13. Phone "taps" were mouse clicks. 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.
  14. Grid and caption wording. "Three columns at 1440 px and wider" assumed a breakpoint that was never read, and "one line of Messina" is contradicted by the shots, where several descriptions wrap to two lines. Corrected in README 3.3; sizes moved to not confirmed.
  15. The panel close is not a reversed timeline. Said: "close runs reversed at 1.92". True: a separate timeline with its own eases and durations at timeScale 1.92. Corrected in the panels' mechanism.
  16. The bar wakes before the overlay is removed. Said (layer list): "overlay removed; the bar wakes". True: 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.
  17. 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.

4. Not confirmed

  1. The 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.
  2. The current language at 50 % opacity: EN is visibly dimmer in menu-d01-open.png, the value was not read.
  3. The home page's reduced-motion rule for a video shimmer: /work has none; / was not checked.
  4. The language suggestion's use of navigator.languages: the storage key was found, the trigger was not read.
  5. Links closing the panel: clickNavigationLink (which closes it) is passed to both panels, but the link handler itself is not in the quoted source.
  6. The real-time order of the bar's width spring and the monogram crossfade: one sample run shows the monogram starting about 400 ms after data-state flipped, another shows it ahead of the spring, although React writes both in one commit. Only the source values hold.
  7. The raw capture output: the agent condensed the meta.json files into 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.
  8. Everything the README already lists in its section 7 (real-browser timing, the blur quirk outside headless Chromium, the filter reflow, reduced motion, click outside, the home page, Storyblok API details) stays open; this pass did not test them.

5. Folder tidiness