{
 "site": "ED., Melbourne",
 "slug": "edstudio",
 "urls": [
  "https://ed.studio/",
  "https://ed.studio/work/sydney-film-festival-2025/",
  "https://ed.studio/studio"
 ],
 "stack": {
  "framework": "React 18.2.0 client-rendered from a WordPress theme ('edsite'): .tsx views with sibling .graphql queries, data printed as window._PAGE_DATA, rendered with createRoot; webpack bundle; Stitches for styles",
  "cms": "WordPress 6.8 or later (generator hidden) with WPGraphQL, ACF-shaped field groups (not confirmed), Yoast SEO 28.6, in-house Gutenberg blocks written in React; hosted on WP Engine behind Cloudflare",
  "animation": [
   "framer-motion (v6 line: AnimatePresence exitBeforeEnter, framesync + popmotion, useSpring, drag)",
   "CSS transitions (contact circles, captions, pill highlight)"
  ],
  "scroll": "native; no smooth-scroll library; #site-wrapper is the scroll container on phones",
  "three_d": "none (no canvas, no WebGL)",
  "fonts": [
   "Graphik 400, 500, 600 (Commercial Type), self-hosted woff/ttf from the theme"
  ],
  "evidence": [
   "response header link: <https://ed.studio/wp-json/>; rel=\"https://api.w.org/\"; x-powered-by: WP Engine; server: cloudflare",
   "body class page-template-views wp-theme-edsite; script wp-content/themes/edsite/dist/frontend/main.frontend.js?v=1780456413",
   "_PAGE_DATA.view views/template-homepage, viewType react, queryMonitor files views/template-homepage.tsx, /views/template-homepage.graphql, /views/_app.graphql",
   "WPGraphQL message 'GraphQL Debug logging is not active' in _PAGE_DATA.extensions",
   "bundle: react-dom version 18.2.0; createStitches; AnimatePresence exitBeforeEnter warning; popmotion spring keys [stiffness, damping, mass]; framesync delta clamp 40 ms",
   "Yoast SEO v28.6 HTML comment; core speculationrules script (WordPress 6.8+)",
   "probe: no gsap/Lenis/THREE globals, 0 canvases, 0 custom cursors, fonts Graphik normal/500/600, 45 video elements, 11 footers in the DOM",
   "third parties: GTM-MMM58V (GA4, Universal Analytics, Meta pixel, X pixel, Plausible), PostHog 1.438.13 via x.ed.studio, reb2b, Tally embed (form meD6y0), Cloudflare challenge script",
   "bundle: popmotion animate() picks keyframes when duration is set (t.has(\"ease\")||t.has(\"duration\")&&!t.has(\"dampingRatio\")), and useSpring spreads its options into animate(); the call-to-action useSpring carries duration 2000, so it tweens (verifier, shots/verify/meta.json)",
   "DOM: 0 elements with the inert attribute; startViewTransition and vercel absent from the bundle; body class includes page-id-82 and page-template-views (verifier page-facts and bundle-greps evals)"
  ]
 },
 "components": [
  {
   "name": "Contact block (footer card of four round buttons)",
   "owner_words": "I like the contact block here especially the mail icon. And the way it's done it looks fun and personable.",
   "selectors": [
    "footer > div:last-child",
    "footer > div:last-child a",
    "footer > div:last-child a .icon",
    "footer > div:last-child a label",
    "a[href^='mailto:'] img[src$='icon-email.png']"
   ],
   "mechanism": "White card (radius 16, padding 12/24, gap 12) of four <a> links, each a 46 px #EBEBEB circle holding a 92 px PNG icon (call, email, Instagram, LinkedIn portrait) and a hidden <label> placed 50 px above; :hover scales the circle to 1.1 (transition transform 0.4s) and fades the label in (opacity 0.2s, declared only on :hover so it vanishes instantly). The portrait links to https://www.linkedin.com/company/edstudio/ (target _blank). The mail icon is a PNG, not an SVG. tel/mailto clicks send PostHog phone_clicked/email_clicked {location: footer}.",
   "libraries": [
    "none (Stitches CSS-in-JS for the styles)"
   ],
   "states": [
    "rest",
    "hover phone (label +61 2 8045 5040)",
    "hover mail (label hello@ed.studio)",
    "hover Instagram (label Instagram)",
    "hover LinkedIn portrait (label LinkedIn)",
    "phone: stacked card, no labels"
   ],
   "timings": "icon scale 1 to 1.1 over 0.4 s ease; label opacity 0 to 1 over 0.2 s ease on enter, instant on leave",
   "shots": [
    "shots/d02b-footer-rest.png",
    "shots/d03-hover-phone.png",
    "shots/d04-hover-mail.png",
    "shots/d05-hover-instagram.png",
    "shots/d06-hover-linkedin.png",
    "shots/d07-mail-hover-sheet.png",
    "shots/m05-contact-block.png",
    "../../edstudio/contact-block-owner-crop.png"
   ],
   "rebuild": "nav of four <a> circles with inline SVG icons in the marker hand and a real Zpryme face last; CSS-only hover/focus-visible scale 1.1 (0.4 s) and caption fade-in (0.2 s, hover rule only); accessible names on the links, captions aria-hidden; captions always visible or on first tap on phones; reserve space so the bar never covers the row; optional spring pop with Motion (stiffness 1000, damping 40, mass 2)",
   "confidence": "high"
  },
  {
   "name": "Story circles and the zoom-out strip of live pages",
   "owner_words": "No words of his own on this component in INSPIRATION.md; listed from the coordinator's note on row 12: 'the clients run across the top as a row of story-style circles (logos and faces)'.",
   "selectors": [
    "a:has(> .bubble-caption)",
    ".bubble-caption",
    "#site-wrapper",
    "article (one per page, eleven in the DOM)"
   ],
   "mechanism": "Ten 60 px project circles (calc((100vw - 400px)/15) below 1400 px) in the fixed header; hover springs the circle to 1.1 and shows its caption, and sets a shared isZoomedOut flag: the feed is pushed down 0.28 x 100vw and every key project renders as a live 0.2-scale React page (InlinePage with cached props) with pointer-events: none (a Stitches variant named inert; no element carries the inert attribute) in a horizontal strip (per-page offset spring 300/60/2; the strip's foot fades under the pushed-down feed's 200 px ::before gradient); pointer x picks the page (round(clientX/innerWidth x (n-1))); click grows the mini page to full size as the route changes and turns the circle into a white x 'Close' disc linking home; un-zoom after a 400 ms grace; circles hide after 50 px of scroll. Phone: circles stacked in a fixed bottom strip, tap spreads them and zooms the site out into a titled horizontal carousel, pan pages at delta.x/10 per event.",
   "libraries": [
    "framer-motion (motion components, useSpring, AnimatePresence, onPan)",
    "zustand-style store",
    "custom InlinePage router helper"
   ],
   "states": [
    "rest",
    "hover (scale 1.1, caption)",
    "zoomed out (strip of mini pages, feed pushed down)",
    "scrubbed left",
    "scrubbed right",
    "opening (mini page grows)",
    "project page with active circle as Close",
    "closing back to feed",
    "hidden after scroll",
    "phone: stacked",
    "phone: spread carousel",
    "phone: panned"
   ],
   "timings": "circle scale spring 1000/40/2 (about 0.4 s, ~20 % overshoot); zoom and page springs 300/50/2 and 300/60/2 (about 0.6 to 0.7 s); circles drop-in spring 500/50/5 (about 0.8 s, 40 ms stagger) and exit tween 0.4 s (50 ms stagger); avatar video mounts after 2000 ms; phone spread spring 600/80/2 (about 0.5 s)",
   "shots": [
    "shots/d21-header-rest.png",
    "shots/d22-zoomout-sheet.png",
    "shots/d23-zoomed-out.png",
    "shots/d24-scrub-left.png",
    "shots/d25-scrub-right.png",
    "shots/d26-open-project-sheet.png",
    "shots/d27-project-page.png",
    "shots/d28-active-bubble-close.png",
    "shots/d29-close-back-sheet.png",
    "shots/m07-stories-spread-sheet.png",
    "shots/m08-spread.png",
    "shots/m09-panned.png"
   ],
   "rebuild": "speaker faces in the bar as links; hover/focus springs the face and writes name and session into the bar; zoom-out state renders each speaker drawer as a real component at scale 0.2 (CSS transform from --zoom, --page, --push variables driven by springs), inert while small, pointer-x scrub, click grows the drawer into the stage and the face becomes Close; phone: stacked faces that spread on tap and snap one item per swipe; keyboard path and reduced-motion crossfade. Motion/framer-motion useSpring or GSAP quickTo + Flip make it easy.",
   "confidence": "high"
  },
  {
   "name": "Call to Action scatter of client 'Start a project' buttons",
   "owner_words": "No words of his own on this component in INSPIRATION.md; listed from the coordinator's note on row 12: 'a giant heading that literally says \"Call to Action\", scattered with every client's own \"Start a project\" button, each labelled like a design-system component'.",
   "selectors": [
    "a[data-tally-open]:has(img.default-state)",
    "img.default-state",
    "img.hover-state",
    "svg.cursor",
    "svg.blur",
    "[style*='---progress']"
   ],
   "mechanism": "Not physics: nine client SVG buttons (default + hover SVG, instant opacity swap) in Figma-style frames (grey 'Button/<client>' label with dot, dashed rx 8 rect with 16 px padding) placed by a random scatter with 10 repulsion passes (min distance 0.3, clamp 0.2 to 0.8, kept out of the heading band), new on every load; on first intersection each flies in radially from 8x its offset on a 2000 ms ease-in-out tween (the useSpring options carry duration 2000, and popmotion's animate() tweens whenever duration is set, ignoring stiffness and damping) after a random 0 to 500 ms delay, carried by a coloured multiplayer-style cursor that leaves 2 s later; each frame is a framer-motion drag wrapper with dragMomentum false (no bounds; past the stage edge it is clipped by the stage's overflow hidden), purple #A340FF solid frame while dragged; every button opens the Tally popup meD6y0.",
   "libraries": [
    "framer-motion (useSpring, drag)",
    "IntersectionObserver",
    "Tally embed"
   ],
   "states": [
    "before entry (off screen)",
    "flying in with cursors",
    "settled",
    "hover (hover SVG)",
    "dragging (purple solid frame)",
    "dropped (grey dashed at new spot)",
    "phone: settled, overlapping"
   ],
   "timings": "2000 ms ease-in-out tween per button, not a spring (options {duration: 2000, stiffness: 300, damping: 100}; measured by the verifier: 14 % of the way at 500 ms, 53 % at 1000 ms, 89 % at 1500 ms, done at 2000 ms of animation time); per-button delay random 0 to 500 ms; cursor departure starts 2000 ms after its button and runs another 2000 ms tween; hover swap instant",
   "shots": [
    "shots/d10-cta-flyin-sheet.png",
    "shots/d02-footer-rest.png",
    "shots/d12-cta-hover.png",
    "shots/d13-cta-dragging.png",
    "shots/d14-cta-dropped.png",
    "shots/m02-cta-flyin-sheet.png",
    "shots/m03-cta-settled.png"
   ],
   "rebuild": "real buttons in labelled frames placed by a seeded scatter with a reserved headline band and a phone layout; fly in from 8x offset on ED.'s 2 s ease-in-out tween (Motion animate with duration 2 and easeInOut, or GSAP power1.inOut over 2 s), or a shorter overdamped spring for a snappier feel, with 0 to 500 ms stagger, marker-drawn cursors that leave after 2 s; drag with Motion drag (dragMomentum false, constraints to the stage) or GSAP Draggable; selected state in the accent colour; reduced motion places items without flight or cursors",
   "confidence": "high"
  },
  {
   "name": "'Start a project' header pill",
   "owner_words": "Not one of his named components; the brief asked how the pill behaves.",
   "selectors": [
    "a[data-tally-open]:not(:has(img)) > button",
    ".hover-decoration",
    ".button-inner"
   ],
   "mechanism": "#EBEBEB pill (46 px tall from 900 px, 36 px below) wrapped in an <a> with Tally attributes; on hover a grey gradient band 2.4x the pill's width fades in (0.3 s) and follows the pointer x through a spring (stiffness 600, damping 100); label 'Start a project' or 'Start your project now' by PostHog flag cta-label-test; click opens the Tally overlay popup (376 x 614 at the right) and records cta_clicked {location: header}; on /job/ pages it reads 'Apply for this role'.",
   "libraries": [
    "framer-motion useSpring",
    "Tally embed",
    "PostHog feature flags"
   ],
   "states": [
    "rest",
    "hover highlight",
    "Tally popup open",
    "after Escape (still open)"
   ],
   "timings": "highlight opacity 0.3 s; offset spring 600/100 (overdamped, about 0.45 s lag); popup network bound (about 1 to 2 s in capture)",
   "shots": [
    "shots/d40-pill-hover.png",
    "shots/d41-tally-open-sheet.png",
    "shots/d42-tally-overlay.png",
    "shots/d43-tally-closed.png"
   ],
   "rebuild": "pill with a pointer-following highlight from one CSS variable smoothed by a spring or transition; opens an ETS27 drawer form instead of a third-party popup",
   "confidence": "high"
  }
 ],
 "transitions": [
  {
   "name": "Load entrance",
   "from": "blank header",
   "to": "header at rest",
   "layers": [
    "left column from -200 px (spring 500/70/5)",
    "right column from +200 px",
    "circles drop from -130 px after 500 ms (spring 500/50/5: about 16 % overshoot, 40 ms stagger) and grow from scale 0 (spring 1000/40/2)"
   ],
   "duration_ms": 1650,
   "shots": [
    "shots/d20-load-sheet.png"
   ]
  },
  {
   "name": "Circles hide on scroll",
   "from": "scrollTop <= 50",
   "to": "scrollTop > 50",
   "layers": [
    "circles up 130 px (tween 0.4 s, 50 ms stagger)"
   ],
   "duration_ms": 850,
   "shots": [
    "shots/d10-cta-flyin-sheet.png"
   ]
  },
  {
   "name": "Hover a circle: zoom out",
   "from": "feed at full size",
   "to": "strip of 0.2-scale live project pages above the pushed-down feed",
   "layers": [
    "circle scale 1.1 + caption",
    "feed pushDown 0.28 x 100vw",
    "mini pages zoom 0.2 and slide to the hovered index"
   ],
   "duration_ms": 600,
   "shots": [
    "shots/d22-zoomout-sheet.png",
    "shots/d23-zoomed-out.png"
   ]
  },
  {
   "name": "Scrub the strip",
   "from": "hovered index",
   "to": "index under pointer x",
   "layers": [
    "per-page offset spring 300/60/2 (damping ratio 1.22, overdamped)"
   ],
   "duration_ms": 650,
   "shots": [
    "shots/d24-scrub-left.png",
    "shots/d25-scrub-right.png"
   ]
  },
  {
   "name": "Open a project",
   "from": "/ zoomed out",
   "to": "/work/sydney-film-festival-2025/",
   "layers": [
    "mini page grows to scale 1",
    "neighbours slide off",
    "title fades in",
    "circle becomes Close"
   ],
   "duration_ms": 650,
   "shots": [
    "shots/d26-open-project-sheet.png",
    "shots/d27-project-page.png",
    "shots/d28-active-bubble-close.png"
   ]
  },
  {
   "name": "Close a project",
   "from": "/work/sydney-film-festival-2025/",
   "to": "/",
   "layers": [
    "project shrinks back into the strip",
    "strip fades",
    "feed rises"
   ],
   "duration_ms": 700,
   "shots": [
    "shots/d29-close-back-sheet.png",
    "shots/d30-back-home.png"
   ]
  },
  {
   "name": "ED. to Studio",
   "from": "/",
   "to": "/studio",
   "layers": [
    "switch pill slides (spring 600/100/5)",
    "page to scale 0.7 + fade (tween 0.4 s ease-in)",
    "Studio page from 0.7 (spring 300/40, delay 0.4 s)"
   ],
   "duration_ms": 1200,
   "shots": [
    "shots/d44-mode-switch-sheet.png",
    "shots/d45-studio-mode.png"
   ]
  },
  {
   "name": "Call to action fly-in",
   "from": "buttons off screen",
   "to": "scattered",
   "layers": [
    "buttons radial 2000 ms ease-in-out tween after 0 to 500 ms delays (measured)",
    "cursors leave 2 s after their buttons, over another 2000 ms tween"
   ],
   "duration_ms": 4500,
   "shots": [
    "shots/d10-cta-flyin-sheet.png",
    "shots/m02-cta-flyin-sheet.png"
   ]
  },
  {
   "name": "Start a project popup",
   "from": "page",
   "to": "Tally overlay",
   "layers": [
    "page dims",
    "popup at the right"
   ],
   "duration_ms": 1500,
   "shots": [
    "shots/d41-tally-open-sheet.png",
    "shots/d42-tally-overlay.png"
   ]
  },
  {
   "name": "Phone: tap the circle stack",
   "from": "stacked circle, feed",
   "to": "titled carousel of pages, circles spread",
   "layers": [
    "scroll to top",
    "pages zoom out into a carousel",
    "circles spread (spring 600/80/2)"
   ],
   "duration_ms": 500,
   "shots": [
    "shots/m07-stories-spread-sheet.png",
    "shots/m08-spread.png",
    "shots/m09-panned.png"
   ]
  }
 ],
 "not_confirmed": [
  "framer-motion exact version (v6 line by code shape; LICENSE.txt or a source map would settle it)",
  "WordPress core version beyond 6.8+, and ACF as the field source",
  "real-time durations: computed from the bundle's constants, except the call to action's 2 s tweens, which the verifier measured in framesync time; the headless bursts ran slow (framesync caps a frame's step at 40 ms)",
  "the load entrance of the header was not caught on camera",
  "the phone home rendered no media tiles in headless iPhone emulation (feed 911 px tall); real-device behaviour unknown",
  "the drag used synthetic pointer events dispatched in the page, not a real mouse; click-after-drag not tested",
  "Escape did not close the Tally popup; the close button was not tried; the form was not submitted",
  "reduced-motion behaviour inferred from source only",
  "keyboard focus styles for the contact and story circles not captured",
  "team circles on /studio not observable (keyTeam null)",
  "phone: opening a project by tapping the centred circle read from source only",
  "the mail icon's vector original (only a 92 px PNG is published; the README's SVG is a redraw)",
  "not re-checked by the verifier: defineBlock(\"layout/section\") in the bundle, the WPGraphQL message in _PAGE_DATA.extensions, the Vimeo tile URLs in the page data"
 ]
}
