ED. teardown

2026-10-21 (local; 2026-10-22 UTC) · URLs read: https://ed.studio/, https://ed.studio/work/sydney-film-festival-2025/ (reached by clicking its story circle), https://ed.studio/studio (reached by the Studio switch), the theme bundle https://ed.studio/wp-content/themes/edsite/dist/frontend/main.frontend.js?v=1780456413 · serves inspiration row 12 (and the note "Contact, personably" in bar-stage-drawers.md, which names the other two components).

1. What the owner liked

2. The stack

Layer What Evidence
CMS WordPress, page id 82, generator tag removed response headers link: <https://ed.studio/wp-json/>; rel="https://api.w.org/" and link: <https://ed.studio/wp-json/wp/v2/pages/82> (sources/home-response-headers.txt); xmlrpc.php pingback, wp-content/themes/edsite/ and body class home … page-template-views … page-id-82 wp-theme-edsite in the HTML (not in the headers file; the verifier confirmed them in the page-facts eval of shots/verify/meta.json)
WordPress version 6.8 or later (exact version hidden) core speculation rules script <script type="speculationrules"> (core since 6.8), wp-img-auto-sizes-contain-inline-css
SEO Yoast SEO 28.6 HTML comment "optimized with the Yoast SEO plugin v28.6"
Theme "edsite", a custom React front end inside a WordPress theme: each template is a .tsx view with a sibling .graphql query; the server runs the query and prints the result as window._PAGE_DATA; the browser renders everything with createRoot (the HTML body is only <div id="root"> plus that data) _PAGE_DATA.view = "views/template-homepage", viewType: "react", query monitor files views/template-homepage.tsx, /views/template-homepage.graphql, /views/_app.graphql (sources/page-data-excerpt.json); defineView, defineBlock("layout/section"), EditableText, InnerBlocks in the bundle (in-house Gutenberg blocks written in React)
Data WPGraphQL; field groups look like ACF (homepage.mediaTiles, callToAction.buttons[].defaultSvg/hoverSvg, projectDetails.avatar/videoAvatar, themeGeneralSettings) extensions.debug: "GraphQL Debug logging is not active" (the WPGraphQL message); ACF itself not visible
Framework React 18.2.0 (react-dom), webpack bundle (527 KB, one file) version:"18.2.0", rendererPackageName:"react-dom", (()=>{var e={7650:(e,t,n)=>
Styling Stitches (CSS-in-JS): tokens $lightGrey, $background, breakpoints @tablet, locally scoped $$size tokens that become CSS variables named ---size; class names c-kXlbeK createStitches (6 hits), sources/theme-tokens.js.txt
Animation framer-motion, v6 era (AnimatePresence with exitBeforeEnter only, framesync plus popmotion animate, MotionValue.attach/onChange); used for motion.div, useSpring, drag, variants with stagger the AnimatePresence warning string, stiffness:100,damping:10,mass:1 spring defaults, ["stiffness","damping","mass"], framesync Math.min(delta, 40); no package name or version string in the bundle
State a zustand-style store (create, getState) for isZoomedOut, hoverUrl, mode sources/stories-bubbles.js.txt
Scroll native; no smooth-scroll library; on the phone the scroll container is #site-wrapper (overflow: hidden auto) probe: no Lenis, Locomotive or GSAP globals, scroll-behavior: auto; phone eval in sources/meta-phone-contact-stories.json
3D, canvas, custom cursor none (no canvas, cursor: none count 0, no url() cursors); the cursors in the call to action are decorative SVGs probe in sources/meta-contact-block-desktop.json
Fonts Graphik (Commercial Type) 400, 500, 600, self-hosted woff and ttf from wp-content/themes/edsite/assets/fonts/ @font-face in sources/theme-tokens.js.txt; probe fonts
Forms Tally popup form meD6y0 (overlay, auto close after 4000 ms) behind every "Start a project" data-tally-open="meD6y0" attributes, tally.so/widgets/embed.js, iframe tally.so/popup/meD6y0?originPage=%2F&alignLeft=1
Analytics GTM GTM-MMM58V loading GA4, Universal Analytics, Meta pixel, X pixel and Plausible; PostHog 1.438.13 through the x.ed.studio reverse proxy (session recorder, surveys, dead-click capture, web vitals, events cta_clicked, phone_clicked, email_clicked, feature flag cta-label-test); reb2b visitor identification probe script list; posthog.init(... api_host: 'https://x.ed.studio'); module 134 in sources/header-and-pill.js.txt
Hosting, CDN WP Engine behind Cloudflare (bot-management challenge script injected); a custom page cache that stamps x-ed-generated-at, x-ed-cache-duration; tile videos are Vimeo (player.vimeo.com URLs in the page data); a Vercel-hosted Vimeo proxy is preconnected but not referenced in the bundle response headers x-powered-by: WP Engine, server: cloudflare, cf-cache-status: HIT, x-ed-*; cdn-cgi/challenge-platform/scripts/jsd/main.js; preconnects
Page transitions no View Transitions API, no barba or swup: every page is a live React tree that zooms in and out of a strip (section 4) viewTransitionCSS: 0; InlinePage with fetchProps/hasCachedProps in the bundle

Design tokens (from the Stitches config): black #1A1A1A, background #F4F4F4, light grey #EBEBEB, hover grey #DCDCDC; radii 4, 8, 16; breakpoints 600, 900 (tablet), 1200 (laptop), 1800, 2400; space scale 4, 8, 12, 16, 24, 32, 48, 64, 80, 96, 112 px, and both space and type grow fluidly from 1200 px to ×1.2 at 2400 px (which is why the computed values at 1440 px are ×1.04: 12.48, 24.96); h1 40 px on phones, 128 px from tablet, weight 500, letter spacing −0.06em; small text 12 px, line height 1.16.

3. Component playbooks

3.1 The contact block

What it is, in pixels (desktop 1440 × 900, scrolled to the end; shots/d02b-footer-rest.png): the footer is a flex row of two white cards on #F4F4F4, gap 16.64 px. The contact card sits at x 1136.7, y 777.2, 271.3 × 89.9 px, padding 12.48 px × 24.96 px, gap 12.48 px, radius 16 px, flex: 0 0 auto, items centred. Inside, four 46 × 46 circles with centres at x 1184.6, 1243.1, 1301.6, 1360.0 (pitch 58.5 px), y 822.1. Each circle is #EBEBEB with a 92 × 92 PNG (2x) drawn at 46 × 46; the Instagram and portrait images fill the circle, so their grey never shows. The acknowledgement card (1088 × 89.9, padding 24.96, two inline SVG flags 66 × 40 at tablet, 30 px wide on phones) takes the rest of the row.

DOM (sources/meta-contact-block-desktop.json, doms.contact-card):

<footer class="…"><div class="c-dBDZkU …">flags + acknowledgement</div>
<div class="c-dBDZkU c-egHSAE">
  <a href="tel:+61280455040" class="c-kXlbeK"><label>+61 2 8045 5040</label>
    <span class="c-dyApyW icon"><img src="/wp-content/themes/edsite/assets/icons/icon-call.png" alt="+61 2 8045 5040"></span></a>
  <a href="mailto:hello@ed.studio" class="c-kXlbeK"><label>hello@ed.studio</label>
    <span class="c-dyApyW icon"><img src="/wp-content/themes/edsite/assets/icons/icon-email.png" alt="hello@ed.studio"></span></a>
  <a href="https://www.instagram.com/ed.studio.ed/" target="_blank" …><label>Instagram</label>…icon-insta.png…</a>
  <a href="https://www.linkedin.com/company/edstudio/" target="_blank" …><label>LinkedIn</label>…icon-linkedin.png…</a>
</div></footer>

Stable selectors: footer > div:last-child a, .icon, label. Beware: the home page keeps eleven copies of the site in the DOM (one per story page, section 3.2), so footer a[href^="tel:"] matches 11 elements and the first one is a 0.2 scale copy off screen; pick the footer whose bounding box is widest and inside the viewport.

The portrait is the LinkedIn button: href="https://www.linkedin.com/company/edstudio/", target="_blank", image icon-linkedin.png, alt "LinkedIn". The joke is that LinkedIn is shown as a corporate headshot rather than a logo.

The mail icon is not an SVG. All four icons are PNGs (/wp-content/themes/edsite/assets/icons/icon-call.png, icon-email.png, icon-insta.png, icon-linkedin.png, 92 × 92); there is no SVG markup to extract, and no SVG anywhere in the contact card (svgCountInContactCard: 0). Looked at 6x (shots/d02b-footer-rest.png crop): one thin black line, about 1.5 px at 46 px, roughly 24 × 13 px, centred: a V for the flap with short level ticks where it meets the top corners, and two short strokes rising from the bottom corners toward the centre, also ending in level ticks, so it reads as an envelope drawn only by its folds, or a loose lowercase m. A redraw for study (ours, not ED.'s asset; ETS27 will draw it in the marker hand anyway):

<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
  <path d="M3 7.5h1.2c.8 0 1.2.3 1.7.9L12 14l6.1-5.6c.5-.6.9-.9 1.7-.9H21"/>
  <path d="M3 16.5h1.2c.8 0 1.2-.3 1.7-.9l3.3-3"/>
  <path d="M21 16.5h-1.2c-.8 0-1.2-.3-1.7-.9l-3.3-3"/>
</svg>

Styles that matter (computed, sources/meta-contact-hover-desktop.json; source in sources/footer-contact.js.txt): - footer: display: flex; justify-content: center; pointer-events: none (cards set pointer-events: all), column with gap 16 below 900 px, row from 900 px. - card: background: #fff; border-radius: 16px; padding: 12px 24px; gap: 12px (×1.04 at 1440 px). - link: position: relative; cursor: pointer. - circle .icon: display: block; width: 46px; height: 46px; background: #EBEBEB; border-radius: 100px; transition: transform 0.4s (ease). - label: position: absolute; left: -50px; right: -50px; top: -50px; text-align: center; font: 12px/1.16 Graphik; letter-spacing: -0.01em; color: #1A1A1A; opacity: 0; pointer-events: none, so a 146 px wide line of text floats 50 px above the circle, outside the card.

Behaviour (shots/d03-hover-phone.png to d06-hover-linkedin.png, burst shots/d07-mail-hover-sheet.png): - hover: the circle scales to 1.1 (matrix(1.1, 0, 0, 1.1, 0, 0)) over 0.4 s ease, and its label fades in over 0.2 s above the card: "+61 2 8045 5040", "hello@ed.studio", "Instagram", "LinkedIn". Plain text, no tooltip box, no arrow. - the fade is declared only inside the :hover rule, so the label fades in but disappears at once when the pointer leaves. - click: tel: and mailto: open the system handlers and fire PostHog phone_clicked or email_clicked with {location: "footer"}; Instagram and LinkedIn open in a new tab untracked. - no click or focus animation, no cursor change beyond pointer.

Phone (shots/m05-contact-block.png): the cards stack; the contact card spans 374 × 70 px with 8 px side margins, the same 46 px circles at x 85, 143, 201, 259, centred. There is no hover, so the labels never show; a tap dials or opens mail. In the capture the stacked story circle that lives at the bottom of the phone screen (section 3.2) sits on top of the mail and Instagram circles.

Accessibility and reduced motion: each link's accessible name repeats itself (the <label> text plus the identical alt); <label> is used without a form control; there are no :focus-visible styles, so keyboard focus shows the browser ring and never the label; the transitions have no reduced-motion query (they are small).

How to rebuild it for ETS27 (library-agnostic): 1. <nav aria-label="Contact" class="contact-row"> holding four <a> circles, each with an inline SVG icon (drawn in the site's marker hand, item 8) and a visually separate caption <span class="contact-row__label"> set to aria-hidden="true", with the accessible name on the link (aria-label="Email the summit team: hello@…"). 2. CSS only: circle 46 px (44 px minimum for touch), #EBEBEB or the paper tone, gap 12 px, card padding 12 px 24 px, radius 16 px; :hover .icon, :focus-visible .icon { transform: scale(1.1) } with transition: transform .4s; label opacity: 0 and :hover/:focus-visible to 1 with transition: opacity .2s declared on the hover rule only, to keep ED.'s "soft in, gone at once". 3. The fourth circle is a real Zpryme face linking to a person (not a company page), so writing to the summit means writing to someone. 4. On phones, show the captions under the circles permanently or after the first tap; reserve bottom padding equal to the bar's height so the bar never covers the row. 5. If a springier pop is wanted (to match the story circles), Motion or framer-motion whileHover={{ scale: 1.1 }} with { type: "spring", stiffness: 1000, damping: 40, mass: 2 } gives about 20 % overshoot over 0.4 s.

Gotchas: the hover label sits outside the card and can collide with whatever is above; PNG icons blur on 3x screens (use SVG); eleven duplicate footers on the home page break naive selectors and inflate the DOM.

3.2 The story circles, and the zoom-out strip they open

What it is, in pixels (shots/d21-header-rest.png): ten circles centred in the fixed header between two 200 px side columns (the ED./Studio switch on the left, the "Start a project" pill on the right). Each circle is 60 × 60 px from 1400 px wide (below that calc((100vw - 400px) / 15)), gap 8 px (16 px from 1400 px); measured first circle x 345.2, y 41. They are the ten "key projects" of the homepage, in CMS order, each an avatar image (logo or face) and, for four of them (New Zealand Herpes Foundation, ASAP, FAO Schwarz, SJB), a looping muted video. Captions sit 10 px under the circle (top 70 px), 260 px wide, 12 px text, hidden until hover.

DOM (sources/meta-stories-desktop.json, evals.bubble-row):

<div class="c-fEHiFt">                       <!-- motion.div, staggered variants -->
  <a href="/work/the-new-zealanderlivery-service/" class="c-iHUEbK" style="transform: translateY(-1px) scale(1) translateZ(0px);">
    <div class="c-ikjvYm"><img src="…/Favicon.svg">
      <!-- <video src="….mp4" playsinline autoplay muted loop poster="…png"> appears 2 s after mount when the project has one -->
      <span class="c-kBCxdF"><svg width="12" height="12" …><path d="M7.4125 5.99999L11.675 1.73749…"/></svg></span> <!-- the × shown when active -->
    </div>
    <div class="c-kCCKZe bubble-caption">The New Zealanderlivery Service</div>
  </a> …
</div>

Behaviour, desktop (source sources/stories-bubbles.js.txt; shots d22-zoomout-sheet.png, d23-zoomed-out.png, d24-scrub-left.png, d25-scrub-right.png, d26-open-project-sheet.png, d27-project-page.png, d28-active-bubble-close.png, d29-close-back-sheet.png): 1. Hover a circle: it springs to scale 1.1 (useSpring, stiffness 1000, damping 40, mass 2: damping ratio 0.45, about 20 % overshoot, settled in about 0.4 s) and its caption fades in (0.2 s): "Sydney Film Festival 2025". 2. The same hover sets isZoomedOut: the whole site zooms out. The current page (the feed) is pushed down by 1.4 × (0.2 × 100vw), 403 px at 1440, and every key project appears above it as a live page drawn at scale 0.2 (each mini page is 0.2 × 100vw wide, laid out every 100vw + 100 px in its own scaled space), the hovered project centred; the strip fades into the background at its foot through a 200 px gradient: every page carries a ::before placed just above its top edge (top −199 px, 200 px tall, transparent to #F4F4F4, z-index 50), the small pages clip theirs with overflow: hidden, so the fade you see is the pushed-down feed's own ::before lying over the lower 200 px of the strip (verifier's strip-pages eval). They are real rendered pages, not screenshots: InlinePage fetches each page's props (cached) and renders the same React view, with pointer-events: none while small (the inert prop in the source is a Stitches variant that only sets pointer-events: none; no element in the DOM carries the inert attribute) and a shortened tile grid off screen (offscreenDesktopRows: 2 in the project view, 3 in the home view; exactly when the cap applies was not traced). shots/d23-zoomed-out.png shows New Zealand Herpes Foundation, Trellis Data, Sydney Film Festival ("See the bigger picture"), Art Gallery of Ballarat and Grok side by side. 3. While zoomed out, the pointer's x position (anywhere below y 100) picks the page: index = round(clientX / innerWidth × (n − 1)), and each page's ---page offset spring (stiffness 300, damping 60, mass 2: damping ratio 1.22, overdamped, within 2 % in about 0.65 s) slides the strip; a 300/50/2 spring in the same component is set to the hovered index but not read in the quoted code, and 300/50/2 (damping ratio 1.02) drives the zoom level and the opacity. d24 (pointer at x 260) shows the first projects, d25 (x 1180) the last ones (Grok, ASAP, FAO Schwarz, SJB, Naomi Milgrom Foundation). 4. Leaving the circles and the strip un-zooms after a 400 ms grace period (the timer is cleared if the pointer enters a mini page, so you can travel from the circles down into the strip). 5. Click a circle or a mini page: the router navigates to the project; the mini page grows to full size (zoom spring stiffness 300, damping 50, mass 2) while its neighbours slide away, and the page's title fades from grey to black. The clicked circle becomes a white disc with a 1 px black border and a 12 px × ("Close", d28), its link now pointing home. 6. Click "Close": the project shrinks back into the strip and the feed rises back into place (d29). 7. Scroll more than 50 px and the circles slide up out of view (tween 0.4 s, 50 ms stagger); back above 50 px they drop in again (spring stiffness 500, damping 50, mass 5: damping ratio 0.5, about 16 % overshoot, about 0.8 s, 40 ms stagger), and because the row remounts, each circle also grows from scale 0 on its 1000/40/2 scale spring (useSpring(0, …) set to 1 on mount; source only).

Behaviour, phone (shots/m06-top-again.png, m07-stories-spread-sheet.png, m08-spread.png, m09-panned.png): the circles live in a fixed strip at the bottom of the screen, stacked into one (the ED. "Feed" avatar is inserted in the middle of the list and sits on top). Tap it: the page scrolls to the top, the site zooms out into a horizontal carousel of pages titled in large type ("Feed", then each project), and the circles spread into a row (spread spring stiffness 600, damping 80, mass 2). Panning across the row pages through the carousel at delta.x / 10 pages per pan event (a short drag in the capture jumped to the last page, "Naomi Milgrom Foundation"); tapping the centred circle opens it. All eleven pages sit side by side in the DOM at 460 px spacing even at rest.

Accessibility and reduced motion: the circles are real links with the caption as their name (the images have no alt); the mini pages are not inert (only pointer-events: none, see above) and are covered by one overlay link each, aria-label="Open /work/…"; while zoomed out they still hold 4 to 173 tabbable links each besides that overlay (verifier's strip-pages eval); the zoom-out happens on hover alone, with no keyboard equivalent found; framer-motion's reducedMotion stays at its default "never" (no MotionConfig override in the bundle), so all of this runs under prefers-reduced-motion: reduce.

How to rebuild it for ETS27 (the speakers as stories; it is the stage and the drawers in one gesture): 1. A row of speaker faces in the bar (or across the top of the stage) as links; hover or focus springs the face to 1.1 and writes the name and session into the bar (item 14's "the bar speaks for the canvas"). 2. Keep one shared state { zoomed, focusIndex }. When zoomed, render each speaker's drawer as a real component inside a strip, scaled to 0.2 with CSS transforms (not iframes), inert while small (the real attribute; ED.'s inert is only pointer-events: none); compose one transform from CSS variables like ED. does: transform: scale(calc(.2 + .8 * var(--zoom))) translateX(calc((100vw + 100px) * var(--page))) translateY(calc(var(--push-amount) * var(--push))), and animate the variables with springs. 3. Libraries that make it easy: Motion for React or framer-motion (useSpring into CSS variables, AnimatePresence for the mode swap), or GSAP (quickTo for the pointer scrub, Flip for the grow into the stage); a router that can render a page's view inline from cached data (React Server Components or a fetchProps(url) cache like ED.'s InlinePage). 4. Grow the clicked mini drawer into the stage rather than navigating away; the circle becomes "Close" and closing shrinks the drawer back into the strip and returns the thread. 5. Phone: stack the faces in the bar, tap to spread, swipe the row to page; snap to one item per swipe (ED.'s delta / 10 overshoots). 6. Add a keyboard path (focus a face, Enter to zoom out, arrows to move, Enter to open, Escape to close) and a reduced-motion path (crossfade instead of zoom).

Gotchas: rendering every page live means the home page carries eleven copies of the site (45 <video> elements, eleven footers) and headless captures stall; avatar videos wait 2 s before mounting; un-zoom is delayed 400 ms; the zoom-out on plain hover can surprise someone just aiming for the pill on the right.

3.3 The "Call to Action" scatter

What it is, in pixels (shots/d02-footer-rest.png, d12-cta-hover.png): a stage 100vw × 80vh (1440 × 720) above the footer, the words "Call to Action" centred in h1 (Graphik 500, about 133 px at 1440, letter spacing −0.06em), and nine client "Start a project" buttons scattered around it, each in a Figma-style frame: a small grey label with a dot ("Button/Sydney Film Festival", "Button/Art Gallery of Ballarat", "Button/Drive in the Moment", "Button/Rouser", "Button/Enko", "Button/Tender", "Button/Boston School of Theatre", "Button/Grok", "Button/ASAP") above a dashed rounded rectangle with 16 px padding around the button. The buttons are SVG files from the media library, one per client in that client's brand (natural sizes 106 to 248 × 19 to 58 px, for example 181 × 48), eight of them with a second hover SVG. 30 px fades in #F4F4F4 soften the stage's top and bottom edges.

Static, animated, physics or draggable? Placed at random, animated in by 2 s tweens (driven through framer-motion's useSpring, see Fly-in), and draggable; no physics engine. - Placement: random on every load (two loads gave different layouts, d02 and d12). Nine random points in the unit square, then ten passes that push apart any pair closer than 0.3 (steps of 0.03), clamp to 0.2 to 0.8 on both axes, and nudge points out of the band |y − 0.5| < 0.08 where the heading sits. Positions are fractions of the stage (---x1, ---y1 in px). - Fly-in: when the stage first enters the viewport (IntersectionObserver, once), each button waits a random 0 to 500 ms, then its ---progress goes from 2 to 0 (curve below). The transform is translate(x1, y1) translate(x2 · p, y2 · p) translate(-50%, -50%) with x2 = (x − 0.5) · width · 4, so each button starts pushed outward by 8 times its distance from the centre, far off screen, and speeds up then slows down radially into place. Not a spring, despite useSpring: the options are {duration: 2000, stiffness: 300, damping: 100, restDelta: .01, restSpeed: .01}, useSpring hands them to popmotion's animate(), and animate() picks its keyframes (tween) generator whenever duration is present without dampingRatio (bundle: t.has("ease")||t.has("duration")&&!t.has("dampingRatio")?m:…), so stiffness and damping are ignored and ---progress runs 2 → 0 over 2000 ms on popmotion's default ease-in-out (quadratic). Measured by the verifier in animation time: 4 % of the way at 250 ms, 14 % at 500 ms, 30 % at 750 ms, 53 % at 1000 ms, 89 % at 1500 ms, at 0 by 2000 ms, which matches that tween to within 0.05 (the sampler reads one frame late) and not the 300/100 spring (90 % in at 750 ms) (shots/verify/meta.json, cta-timing.analysis). - Multiplayer cursors: each button arrives with a coloured cursor, a 20 px arrow (polygon 6.82 1.12 1.07 12.62 6.82 10.09 12.57 12.62 6.82 1.12 in a 13.63 viewBox) rotated −30°, white stroke, fill from #5452FB #F0501E #FBED4E #00B5FF #FF8578 #0FA858 #C7BAFE #B58C20 by index, with a black copy blurred 1 px at 20 % opacity offset 5 px down as its shadow. Two seconds after its button set off, the cursor leaves outward along the same radial direction, by 4 times its button's distance from the centre (---progressAway 0 to 1, the same 2000 ms ease-in-out tween). A cursor ends at 5 × its button's position − 2 (as fractions of the stage), so it stays on the stage only when its button sits between 0.4 and 0.6 on both axes; that happened in two of the three settled loads looked at (the green Boston School of Theatre cursor in d02b and in d12 and d13, at the spot the formula predicts; none in shots/verify/verify-cta-after-9s.png; the lilac cursor in d02 is still mid-departure): ED. is joking that the buttons were dragged in live by a team in Figma. - Hover: the hover SVG swaps in instantly (opacity 0 to 1, no transition): cta-hoverimg-rest opacity 0, cta-hoverimg-hover opacity 1; in d12 Drive in the Moment turns a lighter green. Art Gallery of Ballarat has no hover art. - Drag: every frame is a framer-motion drag wrapper with dragMomentum: false: it follows the pointer 1:1, stays where dropped, has no bounds (dragged past the stage edge it is clipped by the stage's overflow: hidden) and no throw. While dragged, the label, dot and outline turn #A340FF and the outline goes solid (stroke-dasharray from "0 10 0", effectively 10 on 10 off, to none), the Figma selection look (d13-cta-dragging.png: Sydney Film Festival moved 216 × 120 px). On release it returns to grey dashes (d14-cta-dropped.png). Nothing is remembered. - Click: every button carries the same Tally attributes, so any of them opens the "Start a Project" popup (section 3.4).

DOM (evals.cta-structure in sources/meta-cta-flyin-drag-desktop.json):

<div class="c-cvYNpd" style="---x1: 384px; ---y1: 410.6px; ---x2: -1344px; ---y2: 202.6px; ---progress: 0;">
  <div class="c-jRrOyx">
    <div class="c-cXKTYQ">Button/Drive in the Moment</div>
    <div><div class="c-iuuDOZ">
      <a draggable="false" data-tally-open="meD6y0" data-tally-overlay="1" data-tally-auto-close="4000" … class="c-dvWdEQ c-dvWdEQ-fgtUkD-hasHover-true">
        <img class="default-state" draggable="false" src="…/ED-Website_Homepage-Buttons_DITM.svg">
        <img class="hover-state" draggable="false" src="…/ED-Website_Homepage-Buttons_DITM-Hover.svg">
      </a>
      <div class="c-jthrOb"><svg width="214.25" height="81.25"><rect x="2" y="2" width="210.25" height="77.25" rx="8" stroke-width="1" fill="rgba(255,255,255,0.3)" stroke-dasharray="0 10 0"/></svg></div>
    </div></div>
  </div>
</div>

The drag wrapper (a motion.div with pointer-events: all) wraps this; the cursors live in a separate layer of motion.divs with the same variables plus ---color and ---progressAway. Selectors: a[data-tally-open]:has(img.default-state), img.hover-state, svg.cursor, svg.blur. Source: sources/cta-scatter.js.txt.

Phone (shots/m03-cta-settled.png, m02-cta-flyin-sheet.png): the same nine frames on a 390 px stage, buttons capped at 150 px wide, and they overlap each other heavily (Art Gallery of Ballarat over Rouser, Grok over Enko: later items in CMS order paint on top); the placement does not account for the narrow screen.

Accessibility and reduced motion: the buttons are <a> elements without href, so they are not keyboard focusable; the scatter is pointer-only (the header pill is the keyboard route to the same form). The fly-in and cursors ignore prefers-reduced-motion.

How to rebuild it for ETS27: a "the mind is placing things" moment: suggested asks, partner marks or speakers' handwritten notes arrive on the stage carried by marker-drawn cursors (or a hand), then stay draggable like paper on a desk. 1. Real <button> or <a href> elements inside a frame with a label; positions from a seeded scatter with the same repulsion and a reserved band for the headline, recomputed for the phone (fewer items or a column). 2. Trigger on the first intersection; each item flies in from 8 × (position − centre) on ED.'s curve, a 2 s ease-in-out tween (Motion animate with duration: 2, ease: "easeInOut", or GSAP power1.inOut over 2 s; a shorter overdamped spring if it should feel snappier), with a random 0 to 500 ms stagger; the cursor leaves 2 s later and fades if it would stay on screen. 3. Drag with Motion drag plus dragMomentum={false} and dragConstraints set to the stage, or GSAP Draggable, or Pointer Events by hand; selected state in the accent colour with a solid outline. 4. Reduced motion: place items without flight, no cursors.

Gotchas: random layout per load makes screenshots and QA unrepeatable (seed it); no bounds on drag; overlapping on phones; animation time stretches when frames drop (framesync advances at most 40 ms per frame): unblocked, the 2 s tweens took 3.9 to 4.3 s of wall time in the verifier's headless capture, and the screenshot bursts, which stall frames, ran slower still.

3.4 The "Start a project" pill (the question asked of it)

4. Flow and transitions

All durations below are computed from the spring and tween constants in the bundle, except the call to action, which the verifier measured in animation time; the bursts confirm the order and direction but ran slow (frames every 150 to 1500 ms on the headless software renderer, and framesync advances an animation by at most 40 ms per rendered frame). Burst timestamps are in the bursts field of each sources/meta-*.json.

Transition What moves, in order Timing Shots
Load left and right header columns slide in from ∓200 px (spring 500/70/5: about 0.6 s); 500 ms later the circles drop from −130 px with a 40 ms stagger (spring 500/50/5: about 0.8 s, about 16 % overshoot) while each grows from scale 0 (spring 1000/40/2); last circle at rest about 1.65 s after the header mounts not captured (first frame came 3.3 s after DOMContentLoaded); source only d20-load-sheet.png (settled header, the SJB tile scrolling its own site)
Scroll past 50 px circles slide up 130 px (tween 0.4 s, 50 ms stagger); back when above 50 px d10-cta-flyin-sheet.png frame 00 (circles still visible) and later frames (gone)
Hover a circle circle scale 1.1 with overshoot and caption; site zooms out: feed pushed down, projects appear as 0.2 scale live pages, the hovered one centred about 0.4 s for the circle, about 0.6 s for the zoom d22-zoomout-sheet.png, d23-zoomed-out.png
Scrub pointer x selects the page; the strip slides about 0.6 s per change d24-scrub-left.png, d25-scrub-right.png
Open a project the mini page grows to full size while neighbours slide off; title fades in; circle becomes "Close" about 0.6 s (zoom 300/50/2) d26-open-project-sheet.png, d27-project-page.png, d28-active-bubble-close.png
Close page shrinks back into the strip, strip fades, feed rises about 0.6 to 0.7 s d29-close-back-sheet.png, d30-back-home.png
ED. to Studio the black switch pill slides to "Studio" (spring 600/100/5); the page shrinks to 0.7 and fades (tween 0.4 s ease-in), then the Studio page grows in from 0.7 (spring 300/40, 0.4 s delay); the circle row is empty on Studio (no team circles in the CMS today) about 1.2 s in all d44-mode-switch-sheet.png, d45-studio-mode.png
Call to action enters nine buttons fly in radially, each after 0 to 500 ms; each cursor leaves 2 s after its button each button a 2.0 s ease-in-out tween, all in by about 2.5 s; each cursor leaves over another 2.0 s, gone by about 4.5 s (measured, see 3.3) d10-cta-flyin-sheet.png (frame 07 mid-flight, frames 09 and 10 still moving, still from frame 11)
Drag a button follows 1:1; purple solid frame while held immediate d13-cta-dragging.png, d14-cta-dropped.png
Start a project page dims; Tally popup appears at the right network bound, about 1 to 2 s in the capture d41-tally-open-sheet.png, d42-tally-overlay.png
Phone, tap the circle stack page scrolls to top; site zooms out to a titled carousel; circles spread into a row about 0.5 s (spread 600/80/2) m07-stories-spread-sheet.png, m08-spread.png, m09-panned.png

5. Shots

Desktop shots are 1440 × 900; phone shots are 390 × 844 at 2x (780 × 1688). Burst frames are stored at half size; each burst has a contact sheet made with magick montage from the full-size frames.

File What it shows Device
../../edstudio/contact-block-owner-crop.png the owner's crop of the contact card desktop
../../edstudio/desktop-top.png, ../../edstudio/desktop-footer.png earlier renders: top of the home page; call to action caught before the buttons flew in desktop
shots/d01-home-top.png home at rest: switch, ten circles, pill, tiles desktop
shots/d02-footer-rest.png call to action settled (one random layout) and the footer with both cards desktop
shots/d02b-footer-rest.png footer at rest on a second load (another layout) desktop
shots/d03-hover-phone.png handset circle at 1.1, "+61 2 8045 5040" above the card desktop
shots/d04-hover-mail.png mail circle at 1.1, "hello@ed.studio" above desktop
shots/d05-hover-instagram.png Instagram circle at 1.1, "Instagram" above desktop
shots/d06-hover-linkedin.png portrait circle at 1.1, "LinkedIn" above desktop
shots/d07-mail-hover-00..07.png, d07-mail-hover-sheet.png mail hover over 0.8 s (label already in by the first frame) desktop
shots/d10-cta-flyin-00..23.png, d10-cta-flyin-sheet.png call to action entering: buttons and cursors flying in from the edges, then settled desktop
shots/d12-cta-hover.png hover art swapped on Drive in the Moment (lighter green); a lingering green cursor desktop
shots/d13-cta-dragging.png Sydney Film Festival being dragged: purple label and solid outline desktop
shots/d14-cta-dropped.png after release: grey dashed frame at the new spot desktop
shots/d20-load-00..13.png, d20-load-sheet.png after load (entrance missed); a tile scrolling its own site desktop
shots/d21-header-rest.png header row at rest desktop
shots/d22-zoomout-00..11.png, d22-zoomout-sheet.png hover on the Sydney Film Festival circle: zoom-out building desktop
shots/d23-zoomed-out.png zoomed out: strip of live mini project pages over the pushed-down feed, caption under the hovered circle desktop
shots/d24-scrub-left.png, shots/d25-scrub-right.png the strip scrubbed by pointer x to the first and last projects desktop
shots/d26-open-project-00..13.png, d26-open-project-sheet.png click: the mini page grows into the project page desktop
shots/d27-project-page.png Sydney Film Festival 2025 project page desktop
shots/d28-active-bubble-close.png active circle as white × disc with "Close" desktop
shots/d29-close-back-00..11.png, d29-close-back-sheet.png Close: project shrinks back, feed returns desktop
shots/d30-back-home.png home again, caption visible under the hovered circle desktop
shots/d40-pill-hover.png pill with the pointer highlight desktop
shots/d41-tally-open-00..07.png, d41-tally-open-sheet.png Tally popup opening desktop
shots/d42-tally-overlay.png the "Start a Project" form over the dimmed page desktop
shots/d43-tally-closed.png after Escape: still open desktop
shots/d44-mode-switch-00..09.png, d44-mode-switch-sheet.png ED. to Studio: tiles shrink and fade, Studio page grows in desktop
shots/d45-studio-mode.png Studio page, empty circle row, scroll hint chip desktop
shots/m01-home-top.png phone home: header, call to action mid-flight, footer, circle stack at the bottom phone
shots/m02-cta-flyin-00..09.png, m02-cta-flyin-sheet.png phone call to action settling; buttons overlapping phone
shots/m03-cta-settled.png phone call to action settled phone
shots/m05-contact-block.png phone contact card; the circle stack covering two circles phone
shots/m06-top-again.png phone home at the top phone
shots/m07-stories-spread-00..07.png, m07-stories-spread-sheet.png tap the stack: zoom out to a titled carousel, circles spread phone
shots/m08-spread.png spread state: "Feed" with neighbours, circle row phone
shots/m09-panned.png after a pan on the row: "Naomi Milgrom Foundation" phone
shots/verify/verify-cta-after-9s.png verifier's load: call to action 9 s after it scrolled into view, settled, no cursor left on the stage desktop
shots/verify/verify-zoomed-out.png verifier's load: hover on the Sydney Film Festival circle, strip over the pushed-down feed (measured in strip-pages) desktop

6. Sources

7. Not confirmed


Verification

ED. teardown: verification

2026-10-22 (UTC) · verifier pass over README.md and component.json in this folder · goal: refute claims, not admire them.

What was checked

What held

Stack (16): 1. WordPress: api.w.org and pages/82 link headers, xmlrpc.php pingback, theme edsite, body class with page-id-82 and page-template-views, no generator meta. 2. WordPress 6.8 or later: one speculationrules script, the wp-img-auto-sizes-contain-inline-css style. 3. Yoast SEO 28.6 (HTML comment). 4. React views: _PAGE_DATA view, viewType: "react", query monitor files; defineView (15 hits), EditableText (4), InnerBlocks (9), InlinePage (4), fetchProps (5), hasCachedProps (4). 5. React 18.2.0 (two "18.2.0" hits); one webpack bundle of 527,788 characters (163.8 KB over the wire); no other theme chunk loaded. 6. Stitches: createStitches 6 hits; three-dash locally scoped variables (---x1, ---progress) in inline styles. 7. framer-motion with popmotion and framesync inside: the AnimatePresence exitBeforeEnter warning string, popmotion's animator choice, Math.max(Math.min(e-u.timestamp,40),1), MotionValue.attach. 8. reducedMotion occurs twice, as the context default "never" and its reader: no override. 9. No View Transitions API (startViewTransition 0 hits in the bundle, no view-transition CSS). 10. zustand-style store with isZoomedOut, hoverUrl and the 400 ms delayed un-zoom (source). 11. Native scroll; #site-wrapper is overflow: hidden auto on the phone. 12. No canvas, no cursor: none, no url() cursor; nine svg.blur cursor shadows are the only filtered elements. 13. Graphik 400, 500, 600 from the theme, woff and ttf. 14. Tally meD6y0 attributes on every "Start a project"; popup iframe tally.so/popup/meD6y0?originPage=%2F&alignLeft=1, 376 × 614.4. 15. Third parties: GTM-MMM58V, GA4 G-Q4GHCQC1WM, Universal Analytics, Meta pixel, X pixel, Plausible, PostHog 1.438.13 through x.ed.studio (api_host: 'https://x.ed.studio'), reb2b, the Cloudflare challenge script cdn-cgi/challenge-platform/scripts/jsd/main.js. 16. Hosting headers (WP Engine, Cloudflare HIT, x-ed-*); vimeo-proxy-one.vercel.app preconnected with no vercel in the bundle; design tokens, breakpoints, space scale and the ×1.04 fluid factor at 1440 (12.48, 16.64, 24.96 measured).

Contact block (14): 1. Footer row gap 16.64; contact card 1136.7, 777.2, 271.3 × 89.9; padding 12.48 × 24.96; gap 12.48; radius 16. 2. Circle centres 1184.6, 1243.1, 1301.6, 1360.0 at y 822.1 (pitch 58.5). 3. 46 px #EBEBEB circles holding 92 × 92 PNGs; Instagram and portrait fill the circle (d02b, crops). 4. Acknowledgement card 1088.1 × 89.9, padding 24.96, two flag SVGs drawn 66 × 40 at tablet (viewBoxes 66 × 40 and 60 × 40). 5. DOM structure and the four hrefs, labels, alts, target="_blank" on Instagram and LinkedIn. 6. Eleven footers in the DOM; footer a[href^="tel:"] hits a 0.2 scale copy first. 7. The portrait is the LinkedIn company link. 8. No SVG in the contact card; all four icons are PNG. 9. The mail icon description (one thin line, about 24 × 13 px, a V flap with level ticks and two short strokes from the bottom corners), checked on a 6x crop. 10. Label CSS (absolute, −50 px on three sides, 146 px wide, 12.48 px Graphik, #1A1A1A, opacity 0) floating outside the card (label bottom 763.6, card top 777.2). 11. Hover: matrix(1.1, …), label opacity 1, opacity 0.2s declared only on the hover rule, so leaving is instant (source); d04, d05, d06 show the labels. 12. PostHog phone_clicked and email_clicked with location: "footer"; Instagram and LinkedIn untracked. 13. Phone: card 374 × 70 at x 8, circles at x 85, 143, 201, 259; the stacked story circle covers mail and Instagram (m05). 14. No :focus-visible rules in the quoted source.

Story circles and strip (16): 1. Ten circles, 60 × 60, first at 345.2, 41, pitch 76.64; calc((100vw - 400px) / 15) below 1400 px. 2. Captions 260 wide at top 70 px, opacity 0 until hover. 3. Ten key projects in CMS order, four with video avatars; the video mounts after 2 s. 4. Hover spring 1000/40/2 (damping ratio 0.45, about 20 % overshoot, about 0.4 s). 5. The feed pushed down 403 px: the current page's rect top is 547 = 144 + 403. 6. Mini pages 288 × 432 (0.2 × 1440, 0.2 × 150vw), spaced 308 px (0.2 × 1540), hovered page centred (576 to 864). 7. d23 shows New Zealand Herpes Foundation, Trellis Data, Sydney Film Festival, Art Gallery of Ballarat, Grok; the verifier's load shows the same strip. 8. The scrub index formula: pointer x 1180 gives index 7, and FAO Schwarz is the centred page in d25. 9. Un-zoom after 400 ms, cancelled when the pointer enters a mini page (source). 10. Zoom level spring 300/50/2; open grows the page, d26 frame 00 shows the grey title mid-zoom, frame 01 black. 11. The active circle becomes a white disc with a black border, a 12 px ×, caption "Close" (d28, captions-on-project). 12. Circles hide after 50 px of scroll (tween 0.4 s, 50 ms stagger) and return with the 500/50/5 spring and 40 ms stagger. 13. Phone: eleven circles stacked at 165, 770; "Feed" inserted at index 5; tap spreads them 68 px apart; a 180 px drag jumped to the last page (m09). 14. Eleven article pages 460 px apart on the phone, the home one 911 px tall. 15. Overlay links aria-label="Open /work/…" (10 found); circle images without alt. 16. The ED./Studio switch springs (600/100/5) and the Studio page with an empty circle row (d45, no captions on /studio).

Call to Action scatter (13): 1. Stage 100vw × 80vh (1440 × 720); heading 133.1 px (128 × 1.04), Graphik 500. 2. Nine frames with "Button/" labels, dashed rx 8 rects, 16.64 px padding; 30 px #F4F4F4 edge fades. 3. Natural sizes 106 to 248 × 19 to 58; eight of nine with a hover SVG (Art Gallery of Ballarat without). 4. Placement code as described (random, ten repulsion passes at 0.3 with 0.03 steps, clamp 0.2 to 0.8, heading band nudge); seven of nine points in the cta-items eval sit on the clamp edges. 5. A new layout on every load (d02, d02b, d12, verifier's load all differ). 6. IntersectionObserver once, then a random 0 to 500 ms delay per button. 7. The transform formula and the 8× start offset (---x2 values check out, for example −1343.96 for x1 384.01). 8. Cursor arrow polygon, colours by index, −30° rotation, blurred 20 % shadow; departure 2 s after its button, by 4× the offset. 9. Hover art swap without transition (cta-hoverimg opacity 0 then 1; d12 shows the lighter Drive in the Moment). 10. Drag with dragMomentum: false; purple solid frame while held; Sydney Film Festival moved −216, +120 (d13), grey dashes after release (d14). 11. Every button carries the Tally attributes and no href. 12. Phone buttons capped at 150 px; heavy overlaps (m03, m05). 13. The cursor formula itself: the green Boston School of Theatre cursor sits where 5 × position − 2 predicts in d02b and d12.

Header pill (8): 1. 124.2 × 46 at 1282.8, 49; #EBEBEB; radius 100 px; background-color 0.3s. 2. Highlight band 2.4× the pill's width, 230 to 220 to 230, opacity 0.3 s, from 900 px only (source). 3. Pixel samples re-measured: 235 at rest; 220 to 221 at the centre and 223 to 224 at the ends on hover. 4. Offset spring 600/100 (mass 1, overdamped). 5. "Start your project now" for the cta-label-test flag value "test". 6. cta_clicked with label and location: "header". 7. On /job/ pages "Apply for this role" to https://join.ed.studio/2026 (bundle: const fe="https://join.ed.studio/2026"). 8. The Tally popup opens at the right over a dimmed page and Escape left it open (d43).

What was wrong

  1. The call to action does not fly in on a spring. The options passed to useSpring are {duration: 2000, stiffness: 300, damping: 100, restDelta: .01, restSpeed: .01}; framer-motion's useSpring spreads them into popmotion's animate() ((0,i.j)((0,r.pi)((0,r.pi)({from:d.get(),to:e,velocity:d.getVelocity()},t),{onUpdate:o}))), and animate() returns its keyframes generator whenever duration is present without dampingRatio (t.has("ease")||t.has("duration")&&!t.has("dampingRatio")?m:…, where m is the generator also used for array targets). The README's reasoning described popmotion's spring generator, which is never reached. Measured in framesync time, all nine buttons follow a 2000 ms quadratic ease-in-out: 1.925, 1.726, 1.402, 0.941, 0.227 at 267, 500, 750, 1007 and 1524 ms against 1.929, 1.750, 1.437, 0.986, 0.227 for the tween and 0.905, 0.439, 0.203, 0.092, 0.018 for the claimed 300/100 spring; each reaches 0 at 1983 to 2017 ms. The cursors' departures are the same 2000 ms tween (1953 to 1960 ms measured from their first moving frame). So "90 % in about 0.75 s, at rest after about 1.7 to 2.1 s, cursors gone by about 3.5 s" becomes "2.0 s per button, all in by about 2.5 s, cursors gone by about 4.5 s", and the slow bursts were not only framesync's clamp.
  2. The mini pages are not inert. inert in the source is a Stitches variant that sets pointer-events: none; no element in the DOM carries the inert attribute (inertAttrCount: 0, and none inside any strip page). While zoomed out the strip pages hold 4 to 173 tabbable links each besides their overlay link.
  3. The strip's fade belongs to the feed, not to each mini page. Every page has a 200 px ::before above its top edge (top −199 px, z-index 50); the small pages clip theirs with overflow: hidden, and the visible fade is the pushed-down feed's ::before over the lower 200 px of the strip.
  4. The strip slides on 300/60/2, not 300/50/2. Each page's ---page offset uses stiffness 300, damping 60, mass 2 (damping ratio 1.22); the 300/50/2 spring in the same component is set but not read in the quoted code; 300/50/2 drives zoom level and opacity.
  5. "Capped at 3 rows of tiles while small": the strip shows project pages, whose view passes offscreenDesktopRows: 2; 3 is the home view's value.
  6. Phone overlap direction: in m03 Grok sits over Enko (later CMS items paint on top), not Enko over Grok.
  7. "One usually lingers on screen": a cursor stays on the stage only when its button sits between 0.4 and 0.6 of the stage on both axes; that was true in two of three settled loads (d02b, d12) and false in the verifier's load; the cursor in d02 is mid-departure, not lingering.
  8. "It can leave the stage" when dragged: the stage has overflow: hidden, so a button dragged past its edge is clipped.
  9. Circle drop-in: "slight overshoot" is about 16 % (damping ratio 0.5) of the 130 px drop, and each circle also grows from scale 0 on mount (its scale spring starts at 0); with the 40 ms stagger the last circle settles about 1.65 s after the header mounts, not 1.3 s.
  10. d10 frame 09 is not settled: 2.8 % of pixels change from frame 09 to 10 and 0.6 % from 10 to 11; the frames are still from 11 on.
  11. Evidence pointer: the body class and the pingback are not in sources/home-response-headers.txt; they are in the HTML and were confirmed by the verifier's page-facts eval.

Corrections, verifier, 2026-10-22

README.md: - Section 2, CMS row: evidence cell rewritten; the headers file is cited for the two link headers only, and the body class (home … page-template-views … page-id-82 wp-theme-edsite) and pingback are cited to shots/verify/meta.json. - Section 3.2, item 2: the fade mechanism rewritten (feed's ::before over the strip, small pages clip theirs); "inert and capped at 3 rows of tiles while small" replaced by pointer-events: none (the inert prop is a Stitches variant, no inert attribute in the DOM) and offscreenDesktopRows 2 in the project view, 3 in the home view. - Section 3.2, item 3: the strip spring changed from 300/50/2, critically damped, about 0.6 s to the per-page ---page spring 300/60/2, damping ratio 1.22, within 2 % in about 0.65 s; where 300/50/2 is actually used added. - Section 3.2, item 7: damping ratio 0.5, about 16 % overshoot, and the scale-from-0 pop on remount added. - Section 3.2, accessibility: "the mini pages are inert" replaced by "not inert (only pointer-events: none)", with the 4 to 173 tabbable links per page. - Section 3.2, rebuild item 2: note that the real inert attribute is meant, since ED.'s is only pointer-events: none. - Section 3.3, summary line: "animated in by springs" became "animated in by 2 s tweens (driven through framer-motion's useSpring)". - Section 3.3, fly-in: "then a spring takes its ---progress" became "then its ---progress goes from 2 to 0"; "decelerates" became "speeds up then slows down"; the spring paragraph (300/100/1, duration ignored, 90 % in 0.75 s, rest 1.7 to 2.1 s) replaced by the tween explanation with the bundle quote and the measured curve. - Section 3.3, cursors: "same spring" became "the same 2000 ms ease-in-out tween"; "barely moves, so one usually lingers" replaced by the 0.4 to 0.6 condition and the two-of-three observation. - Section 3.3, drag: "it can leave the stage" became "dragged past the stage edge it is clipped by the stage's overflow: hidden". - Section 3.3, phone: "Enko over Grok" became "Grok over Enko: later items in CMS order paint on top". - Section 3.3, rebuild step 2: the overdamped spring recipe replaced by ED.'s 2 s ease-in-out tween (Motion animate or GSAP power1.inOut), the spring kept as the snappier option. - Section 3.3, gotchas: "which is why the headless bursts ran three times slower than the springs say" replaced by the measured wall time of the 2 s tweens (3.9 to 4.3 s headless). - Section 4: intro now says the call to action was measured; Load row gets about 16 % overshoot, the scale-from-0 pop and about 1.65 s; Call to action row gets 2.0 s per button, all in by about 2.5 s, cursors gone by about 4.5 s, and "frame 09 settled" became "frames 09 and 10 still moving, still from frame 11". - Section 5: two rows added for shots/verify/verify-cta-after-9s.png and shots/verify/verify-zoomed-out.png. - Section 7: the real-time bullet now excludes the measured call to action; a "Not re-checked by the verifier" bullet added.

component.json: - stack.evidence: two lines added (popmotion's animator choice and the useSpring spread; 0 inert attributes, no startViewTransition or vercel in the bundle, body class). - Story circles mechanism: "a live, inert, 0.2-scale React page" became "a live 0.2-scale React page … with pointer-events: none (a Stitches variant named inert; no element carries the inert attribute)"; the 300/60/2 offset spring and the feed's ::before fade added. - Call to Action mechanism: "on an overdamped spring" became "on a 2000 ms ease-in-out tween (…)"; "(no bounds)" gained the overflow: hidden clip. - Call to Action timings: rewritten (2000 ms ease-in-out per button with the measured points; cursor departure another 2000 ms tween). - Call to Action rebuild: the spring recipe replaced by ED.'s 2 s ease-in-out tween, spring kept as the snappier option. - transitions: Load entrance layer gains about 16 % overshoot and the scale-from-0 pop, duration_ms 1300 → 1650; Scrub the strip layer 300/50/2 → 300/60/2, duration_ms 600 → 650; Call to action fly-in layers rewritten as tweens, duration_ms 3500 → 4500. - not_confirmed: the real-time line updated; a "not re-checked by the verifier" line added.

Still unconfirmed

Tidiness