// shared.jsx — content data + reusable placeholder for all three directions.
// Exports to window: DATA, Placeholder, PlayBadge, YT_THUMB.

const DATA = {
  brand: "DANCING SHAPES",
  name: "Tal Hershkovich",
  tagline: "Animator, rigger & Blender addons developer",
  email: "dancingshapes@gmail.com",
  linkedin: "https://www.linkedin.com/in/tal-hershkovich-b09785a3/",
  imdb: "https://www.imdb.com/name/nm2536940/",
  nav: ["Reel", "Work", "Addons", "Art", "About"],
  reel: { id: "qzE_nlIaMH4", label: "Animation Demoreel" },
  bio: "A 3D Animator and a Rigger working with Blender, Maya and 3dsmax, currently on feature films and TV shows at RiseFX. Alongside production work, I build animation tools with python, used by artists around the world.",
  projects: [
    {
      title: "Sano",
      role: "Complete 3D & animation",
      meta: "Commercial series · Blender 2.8 · Yoram Productions",
      // playlist — no single id; use a representative still
      thumb: "https://talhershko.wordpress.com/wp-content/uploads/2020/10/animationlayers_v1.png?w=1024",
    },
    {
      title: "Hanson · Heidelberg",
      role: "Characters, rigs, shading & sim",
      meta: "Instructional animation · Moyo Productions",
      thumb: "https://img.youtube.com/vi/4gCILBDT_DY/hqdefault.jpg",
    },
    {
      title: "Gugi & Arnold",
      role: "Original series — story to render",
      meta: "Solo production · Blender",
      thumb: "https://img.youtube.com/vi/YFRCRZj9kZ4/hqdefault.jpg",
    },
  ],
  addons: [
    {
      name: "Animation Layers",
      blurb: "A non-destructive, layer-based workflow for editing animation and mocap directly in the viewport. Create, blend, mute, solo and smart-bake layers without touching the NLA.",
      tag: "Superhive Market",
      href: "https://superhivemarket.com/products/animation-layers",
      thumb: "https://assets.superhivemarket.com/store/product/158788/image/49beb35ffb5efb627c7933b5aafd671b.jpg",
      shots: [],
    },
    {
      name: "AnimToolbox",
      blurb: "A character animation toolset Including Temp Controls, editable motion paths, retime markers, copy/paste matrices and dozens of small workflow tools that save thousands of clicks.",
      tag: "Superhive Market",
      href: "https://superhivemarket.com/products/animtoolbox",
      thumb: "https://assets.superhivemarket.com/store/product/190396/image/f506a1cc6e89435dfb16369ff344edf2.png",
      shots: [],
    },
  ],
  art: ["Sculptures", "Paintings", "Sketches", "Life Drawings", "3D Prints", "Gugi & Arnold"],
  location: "Berlin, Germany",
  resumePdf: "https://talhershko.wordpress.com/wp-content/uploads/2020/02/resume_hershko.pdf",

  // WORK — detailed project records for the Work page.
  workFull: [
    {
      title: "Gugi & Arnold",
      kind: "Original animated series",
      role: "Solo production — story to render",
      client: "Self-produced",
      year: "Blender",
      blurb: "A pitch for an animated series that I developed: modelling, rigging, animation, render, comp and edit. Every frame made in Blender, from storyboard to final cut.\nThere are about 5 written screenplays, pilot storyboard and a bible available upon request.\n\n\nSYNOPSIS\nArnold, an anarchist muppet who lives from student federal aid, strives to change Gugi and the world with his anti-capitalist ideals and conceptions.\nGugi is a working class hero who cares only about his comfort. He is content  with a routine life: going to his nine-to-five job, coming home, and falling\nasleep in front of the TV.\nWith his tools and technical skills, only he can\nrealize Arnold’s ideas into inventions.",
      media: [
        { type: "yt", id: "AUf74hvWRLA", label: "Gugi & Arnold" },
        { type: "yt", id: "YO8_B6D4rsc", label: "Gugi & Arnold — shots" },
      ],
      storyboard: [
        "https://talhershko.wordpress.com/wp-content/uploads/2014/06/page06.jpg",
        "https://talhershko.wordpress.com/wp-content/uploads/2014/06/page07.jpg",
        "https://talhershko.wordpress.com/wp-content/uploads/2014/06/page08.jpg",
      ],
      tall: true,
    },
    {
      title: "Sano",
      kind: "Commercial series",
      role: "Complete 3D & animation",
      client: "Sano · Yoram Productions",
      year: "Blender 2.8",
      blurb: "A series of commercials handled end to end — modelling, look-dev, animation and render, all in Blender 2.8.",
      media: [{ type: "yt", id: "AlUMuPn64FA", label: "Sano commercial" }],
    },
    {
      title: "Hanson · Heidelberg",
      kind: "Instructional film",
      role: "Characters, rigs, shading, sim & comp",
      client: "Heidelberg Cement · Moyo Productions",
      year: "Blender",
      blurb: "Responsible for the complete characters and animation, rigs, shading of the truck, particle sims, plus some rendering and compositing.",
      media: [{ type: "yt", id: "4gCILBDT_DY", label: "Hanson Heidelberg" }],
    },
  ],

  // VIDEO TUTORIALS — dedicated tutorials page (linked from Freebies).
  tutorials: {
    main: [
      { title: "Animation Layers", series: "Full Series", id: "gQ-_eSOn1ao", topic: "Addon Walkthrough", desc: "A complete walkthrough of the Animation Layers addon — setup, layering, blending, mute/solo and smart-baking, start to finish.", href: "https://www.youtube.com/playlist?list=PLhVnW7CgAZiYz4VeeRR_QDiiP6deGUxqx" },
      { title: "AnimToolbox", series: "Full Series", id: "yIDvkCm7_rQ", topic: "Addon Walkthrough", desc: "Every tool in AnimToolbox explained — Temp Controls, editable motion paths, retiming markers and the workflow shortcuts.", href: "https://www.youtube.com/playlist?list=PLhVnW7CgAZiaa8UWM1Yjp4g3eOTGjkvy0" },
    ],
    archive: [
      { title: "Breathing & physics tutorial", id: "b44gYk3gq2k", topic: "Rig Breakdown", desc: "Automatic layered breathing on a character, combining physics simulation with the rig for the back.", href: "https://www.youtube.com/watch?v=b44gYk3gq2k" },
      { title: "Cut-Out Animation", id: "ThCXPPxERSE", topic: "Rigging", desc: "Create a rig property that swaps objects for cut-out style animation.", href: "https://www.youtube.com/watch?v=ThCXPPxERSE" },
      { title: "Multikey addon", id: "XuUIxVvWz_E", topic: "Animation", desc: "Key multiple selected channels at once to speed up the animation workflow.", href: "https://www.youtube.com/watch?v=XuUIxVvWz_E" },
    ],
  },

  // FREEBIES — scripts, tutorials and downloads (merged into Addons page).
  freebies: [
    { name: "Blender Scripts", desc: "Open-source rigging & animation scripts", tag: "GitHub", href: "https://github.com/snotnose/blender.git" },
    { name: "Multikey Addon", desc: "Key multiple selected channels at once", tag: "Blender Wiki", href: "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Animation/Multikey" },
    { name: "Video Tutorials", desc: "Rigging & animation how-tos, cut-out workflow and more", tag: "Tutorials", href: "tutorials.html" },
    { name: "3D Prints", desc: "Printable models & designs", tag: "Thingiverse", href: "http://www.thingiverse.com/snot_nose/designs" },
    { name: "Sketchfab", desc: "Interactive 3D model gallery", tag: "Sketchfab", href: "https://sketchfab.com/snot_nose" },
    { name: "Lightboard Build", desc: "Portable LED lightbox upcycled from an LCD panel", tag: "Instructables", href: "http://www.instructables.com/id/Portable-LED-Lightbox-upcycled-From-LCD-TFT/" },
  ],

  // ART — categories for the landing grid. Counts are illustrative.
  artCats: [
    { name: "Sculptures", count: "Super Sculpey · mixed", thumb: "https://i0.wp.com/talhershko.wordpress.com/wp-content/uploads/2014/05/scalbture_darkbg.jpg?w=1200&ssl=1", href: "gallery-sculptures.html" },
    { name: "3D Prints", count: "Designed & printed · Thingiverse ↗", thumb: "https://cdn.thingiverse.com/renders/60/a7/a5/8d/3c/scaby_abs.jpg", href: "http://www.thingiverse.com/snot_nose/designs", external: true },
    { name: "Paintings", count: "Oil · acrylic", thumb: "https://i0.wp.com/talhershko.wordpress.com/wp-content/uploads/2014/06/dsc01116.jpg?w=1400&ssl=1", href: "gallery-paintings.html" },
    { name: "Life Drawings", count: "Figure studies", thumb: "https://i0.wp.com/talhershko.wordpress.com/wp-content/uploads/2016/03/couple_04.jpg?w=1200&ssl=1", href: "gallery-lifedrawings.html" },
    { name: "Sketches", count: "Pencil · ink · flyers", thumb: "https://i0.wp.com/talhershko.wordpress.com/wp-content/uploads/2014/06/gugi.jpg?w=1200&ssl=1", href: "gallery-sketches.html" },
  ],

  // ART GALLERIES — full image sets pulled from the live WordPress portfolio (i0.wp.com CDN).
  galleries: {
    sculptures: {
      title: "Sculptures", kicker: "Super Sculpey · armature · mixed media",
      lede: "Hand-built figures and creatures — sculpted over wire armatures, cast and painted. The same anatomy and gesture work that feeds the rigs.",
      base: "https://i0.wp.com/talhershko.wordpress.com/wp-content/uploads/",
      images: [
        ["2014/05/scalbture_darkbg.jpg", "Scaby"],
        ["2014/05/cheski1.jpg", "Cheski"],
        ["2014/06/cheskaya_skeleton02.jpg", "Cheskaya — skeleton"],
        ["2014/06/cheskaya_back.jpg", "Cheskaya — back"],
        ["2014/05/img_4862.jpg", "Scaby — detail"],
        ["2014/06/scabarmature.jpg", "Scaby — armature"],
        ["2014/06/1916262_1456236519796_4685796_n.jpg", "Cyborg"],
        ["2014/06/cyborgbacknopaint.jpg", "Cyborg — unpainted"],
        ["2014/05/kmfront_nobg.jpg", "Kinky Mama"],
        ["2014/06/back.jpg", "Kinky Mama — back"],
        ["2014/06/kinkymama_armature.jpg", "Kinky Mama — armature"],
        ["2014/06/dsc00224.jpg", "Studio"],
      ],
    },
    paintings: {
      title: "Paintings", kicker: "Oil · acrylic · canvas",
      lede: "Figurative and narrative paintings — circus crowds, animals and street scenes, built up in oil and acrylic.",
      base: "https://i0.wp.com/talhershko.wordpress.com/wp-content/uploads/",
      images: [
        ["2014/06/leiden_11.jpg", "Leiden I"],
        ["2014/06/leiden_21.jpg", "Leiden II"],
        ["2014/06/dsc01116.jpg", "Study"],
        ["2014/06/circus1.jpg", "Join the Circus"],
        ["2014/06/dogsandcats1.jpg", "Dogs and Cats"],
        ["2014/06/monkeysquat1.jpg", "Monkey Squat"],
        ["2014/06/punktribe1.jpg", "Punk Tribe"],
        ["2014/06/shark1.jpg", "Shark"],
        ["2014/06/opendoor1.jpg", "Open Door"],
      ],
    },
    sketches: {
      title: "Sketches", kicker: "Pencil · ink · gig flyers",
      lede: "Sketchbook pages, portraits and hand-drawn flyers — quick studies and finished ink work.",
      base: "https://i0.wp.com/talhershko.wordpress.com/wp-content/uploads/",
      images: [
        ["2014/06/gugi.jpg", "Gugi"],
        ["2014/06/karelhynek.jpg", "Karel Hynek"],
        ["2014/06/giora.jpg", "Giora"],
        ["2014/06/selfstudyflyer.jpg", "Self-study flyer"],
        ["2014/06/boedi_poster.jpg", "Boedi poster"],
        ["2014/06/carnivale.jpg", "Carnivale"],
        ["2014/06/rogatka.jpg", "Rogatka"],
        ["2014/06/imzug.jpg", "Im Zug"],
        ["2014/06/waitingforvisa1.jpg", "Waiting for Visa"],
        ["2014/06/schererbar.jpg", "Scherer Bar"],
        ["2014/06/roberta2.jpg", "Roberta"],
        ["2014/06/johana1.jpg", "Johana"],
        ["2014/06/visaoffice.jpg", "Visa Office"],
        ["2014/06/schizo.jpg", "Schizo"],
        ["2014/06/scabyonlake1.jpg", "Scaby on Lake"],
        ["2014/06/drunken_gorrila.jpg", "Drunken Gorilla"],
        ["2014/06/diyordie.jpg", "DIY or Die"],
        ["2014/06/hmmm.jpg", "Hmmm"],
        ["2014/06/busk.jpg", "Busk"],
        ["2014/06/model4.jpg", "Model IV"],
        ["2014/06/model2.jpg", "Model II"],
        ["2014/06/km_concept.jpg", "Kinky Mama — concept"],
        ["2014/06/risum1.jpg", "Risum"],
      ],
    },
    lifedrawings: {
      title: "Life Drawings", kicker: "Figure studies · Berlin sessions, 2015–16",
      lede: "3- to 30-minute drawings from life-drawing sessions around Berlin — gesture, weight and anatomy caught at speed.",
      base: "https://i0.wp.com/talhershko.wordpress.com/wp-content/uploads/",
      images: [
        ["2016/03/aktzeichnung_01.jpg", "Aktzeichnung I"],
        ["2016/03/witch_01.png", "Witch"],
        ["2016/03/couple_04.jpg", "Couple IV"],
        ["2016/03/couple_06.jpg", "Couple VI"],
        ["2016/03/sketch_the_moment1.jpg", "Sketch the Moment"],
        ["2016/03/chubby_guy_03_15min.jpg", "Seated figure — 15 min"],
        ["2016/03/drsketchy_01.jpg", "Dr Sketchy"],
        ["2016/03/skinnyguy.jpg", "Standing figure"],
        ["2016/03/couple_07.jpg", "Couple VII"],
        ["2016/03/couple_02.jpg", "Couple II"],
        ["2016/03/chubby_guy_05.jpg", "Seated figure V"],
        ["2016/03/couple_052.jpg", "Couple V"],
        ["2016/03/chubby_guy_04.jpg", "Seated figure IV"],
        ["2016/03/aktzeichnung_02.jpg", "Aktzeichnung II"],
        ["2016/03/chubby_guy_02.jpg", "Seated figure II"],
        ["2016/03/chubby_guy_01.jpg", "Seated figure I"],
        ["2016/03/aktzeichnung_04.jpg", "Aktzeichnung IV"],
        ["2016/03/aktzeichnung_03.jpg", "Aktzeichnung III"],
      ],
    },
  },

  // RESUME — experience grouped by period (from the live résumé).
  experience: [
    { period: "2021 – Today", roleLine: "3D animator · feature VFX & series",
      items: [
        ["RiseFX", "Staff 3D animator - VFX shots for feature films and series including Shazam! Fury of the Gods, Blue Beetle, Der Greif, Momo, Babylon Berlin S5 and The Hunger Games"],
      ] },
    { period: "2021", roleLine: "Freelance · rotomation & character animation",
      items: [
        ["Trixter", "Rotomation Artist - Falcon and the Winter Soldier, Shang-Chi and the Legend of the Ten Rings, and unannounced projects"],
        ["Datagen", "Animation & motion-capture cleanup"],
        ["Ground Studios", "Character animation for a TV spot"],
      ] },
    { period: "2017 – 2020", roleLine: "3D animator · feature VFX & addon dev",
      items: [
        ["RiseFX", "VFX animation - Westworld, Kingdoms of Fire, Artemis Fowl, Hobbs & Shaw, Babylon Berlin S3, The King's Man"],
        ["Animation Layers", "Developed & released the commercial Blender add-on in Python"],
        ["Key Film, NL", "Creature animation & 3D for a live-action film"],
        ["Baby Giant, DE", "Creature animation for the film Spreewaldkrimi"],
        ["Pitchipoy, IL", "Character animation - The Boy Who Painted a Donkey Blue, Ulay Pil"],
        ["Moyo, IL", "Character animation & CG generalist — instructional film, Limbobo"],
      ] },
    { period: "2015 – 2016", roleLine: "Character animation & rigging",
      items: [
        ["CET / MindCET, IL", "Modeling, texturing, rigging & animation"],
        ["Talit Communications", "2D cut-out animation"],
        ["Aureate Films, USA", "Animation & props modeling"],
        ["Tasty Pixel, DE", "Rigging & 2D animation"],
      ] },
    { period: "2013 – 2014", roleLine: "Character animation & crowd sim",
      items: [
        ["Pixomondo, DE", "Character animation (Berlin & Hamburg)"],
        ["URCHN", "Animator & crowd sim on the open film Wires for Empathy"],
        ["Technion R&D, IL", "Rigging for mocap"],
        ["Gamblify, DK", "Character animation"],
      ] },
    { period: "2009 – 2012", roleLine: "Games animation & rigging",
      items: [
        ["ByteRockers, DE", "Character animator & rigger for games"],
        ["Mackevision, DE", "Animation & rigging"],
        ["Eveimages, DE", "Crowd simulation for architecture viz"],
      ] },
    { period: "2005 – 2008", roleLine: "3D generalist — commercials & TV",
      items: [
        ["Primus Design Group, IL", "3D generalist - commercials & TV shows"],
        ["Pitchipoy Productions, IL", "3D animator across TV productions"],
      ] },
  ],
  skills: ["Blender", "Maya", "3ds Max", "Python", "Photoshop", "After Effects", "Premiere", "Rigging", "Mocap cleanup", "3D printing"],
  languages: [["Hebrew", "Native"], ["English", "Fluent"], ["Deutsch", "Good"]],
  // Small GIF gallery for the addons section (mostly AnimToolbox in-action clips).
  addonGifs: [
    { src: "https://animtoolbox-doc-0a62ec.gitlab.io/temp-controls/assets/gifs/Track_target_head_offset_09.gif", label: "Track Target" },
    { src: "https://animtoolbox-doc-0a62ec.gitlab.io/temp-controls/assets/gifs/Temp_FK_Reversed.gif", label: "Temp FK" },
    { src: "https://animtoolbox-doc-0a62ec.gitlab.io/display/assets/gifs/emp_brushes.gif", label: "Motion Path" },
    { src: "https://assets.superhivemarket.com/cache/725fca2d6f6fafe1031fc70f958a65d2.gif", label: "Animation Layers" },
  ],
};

// Striped placeholder for imagery the user will supply later.
function Placeholder({ label, h = 200, w = "100%", radius = 0, dark = true, accent = "rgba(255,255,255,.18)", style = {} }) {
  const stripe = dark
    ? `repeating-linear-gradient(45deg, rgba(255,255,255,.025) 0 14px, rgba(255,255,255,.05) 14px 28px)`
    : `repeating-linear-gradient(45deg, rgba(0,0,0,.03) 0 14px, rgba(0,0,0,.06) 14px 28px)`;
  return (
    <div style={{
      width: w, height: h, borderRadius: radius,
      background: stripe,
      backgroundColor: dark ? "rgba(255,255,255,.02)" : "rgba(0,0,0,.02)",
      border: `1px solid ${dark ? "rgba(255,255,255,.08)" : "rgba(0,0,0,.08)"}`,
      display: "flex", alignItems: "center", justifyContent: "center",
      boxSizing: "border-box", overflow: "hidden", position: "relative",
      ...style,
    }}>
      <span style={{
        fontFamily: '"DM Mono", ui-monospace, monospace', fontSize: 11, letterSpacing: 1,
        textTransform: "uppercase", color: accent, padding: "4px 10px",
        border: `1px solid ${accent}`, borderRadius: 2, background: dark ? "rgba(0,0,0,.25)" : "rgba(255,255,255,.5)",
      }}>{label}</span>
    </div>
  );
}

// A real image with a striped placeholder fallback if it fails to load.
function Frame({ src, label, h = 200, w = "100%", radius = 0, dark = true, style = {}, imgStyle = {}, children }) {
  const [ok, setOk] = React.useState(true);
  return (
    <div style={{ position: "relative", width: w, height: h, borderRadius: radius, overflow: "hidden", ...style }}>
      {ok ? (
        <img src={src} alt={label} onError={() => setOk(false)}
          style={{ width: "100%", height: "100%", objectFit: "cover", display: "block", ...imgStyle }} />
      ) : (
        <Placeholder label={label} h={h} w="100%" radius={0} dark={dark} />
      )}
      {children}
    </div>
  );
}

function PlayBadge({ size = 84, color = "#fff", bg = "rgba(0,0,0,.35)", ring = "rgba(255,255,255,.85)" }) {
  return (
    <div style={{
      width: size, height: size, borderRadius: "50%", background: bg,
      border: `1.5px solid ${ring}`, display: "flex", alignItems: "center", justifyContent: "center",
      backdropFilter: "blur(4px)", boxShadow: "0 8px 40px rgba(0,0,0,.4)",
    }}>
      <svg width={size * 0.34} height={size * 0.34} viewBox="0 0 24 24" fill={color}>
        <path d="M5 3.5v17l14-8.5z" />
      </svg>
    </div>
  );
}

// ---- In-page video lightbox ------------------------------------------------
// Plays YouTube videos INSIDE the site (no new tab). Accepts a bare video id,
// a watch URL, a youtu.be URL, or a playlist URL, and converts it to an
// autoplaying embed shown in a full-viewport overlay. Call: window.playVideo(src)
function ytEmbedSrc(src) {
  const base = "https://www.youtube.com/embed/";
  const params = "autoplay=1&rel=0&modestbranding=1&playsinline=1";
  if (!src) return null;
  // Bare id (no scheme / path / query chars).
  if (!/[\/:?=&]/.test(src)) return `${base}${src}?${params}`;
  try {
    const u = new URL(src, window.location.href);
    const list = u.searchParams.get("list");
    const v = u.searchParams.get("v");
    if (v) return `${base}${v}?${params}${list ? `&list=${list}` : ""}`;
    if (list) return `${base}videoseries?list=${list}&${params}`;
    if (u.hostname.includes("youtu.be")) return `${base}${u.pathname.slice(1)}?${params}`;
    const m = u.pathname.match(/\/embed\/([^/?]+)/);
    if (m) return `${base}${m[1]}?${params}`;
  } catch (e) {/* fall through */}
  return src;
}

let _videoOverlay = null;
function _videoKey(e) { if (e.key === "Escape") window.closeVideo(); }
function closeVideo() {
  if (_videoOverlay) {
    _videoOverlay.remove();
    _videoOverlay = null;
    document.removeEventListener("keydown", _videoKey);
  }
}
function playVideo(src) {
  const embed = ytEmbedSrc(src);
  if (!embed) return;
  closeVideo();

  if (!document.getElementById("ds-video-style")) {
    const st = document.createElement("style");
    st.id = "ds-video-style";
    st.textContent = "@keyframes dsVideoIn{from{opacity:0}to{opacity:1}}";
    document.head.appendChild(st);
  }

  const ov = document.createElement("div");
  ov.setAttribute("role", "dialog");
  ov.setAttribute("aria-label", "Video player");
  ov.style.cssText =
    "position:fixed;inset:0;z-index:2147483600;display:flex;align-items:center;justify-content:center;" +
    "padding:5vmin;background:rgba(5,5,5,.92);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);" +
    "animation:dsVideoIn .18s ease;";

  const wrap = document.createElement("div");
  wrap.style.cssText =
    "position:relative;width:min(1280px,94vw);aspect-ratio:16/9;background:#000;box-shadow:0 40px 140px rgba(0,0,0,.7);";

  const iframe = document.createElement("iframe");
  iframe.src = embed;
  iframe.style.cssText = "position:absolute;inset:0;width:100%;height:100%;border:0;";
  iframe.allow = "autoplay; encrypted-media; picture-in-picture; fullscreen";
  iframe.setAttribute("allowfullscreen", "");
  wrap.appendChild(iframe);

  const close = document.createElement("button");
  close.setAttribute("aria-label", "Close video");
  close.innerHTML = "&times;";
  close.style.cssText =
    "position:absolute;top:max(18px,2.5vmin);right:max(22px,3vmin);background:none;border:none;color:#fff;" +
    "font:300 42px/1 system-ui,sans-serif;cursor:pointer;padding:4px 10px;opacity:.85;";
  close.addEventListener("mouseenter", () => (close.style.opacity = "1"));
  close.addEventListener("mouseleave", () => (close.style.opacity = ".85"));
  close.addEventListener("click", closeVideo);

  ov.appendChild(wrap);
  ov.appendChild(close);
  ov.addEventListener("click", (e) => { if (e.target === ov) closeVideo(); });

  document.body.appendChild(ov);
  _videoOverlay = ov;
  document.addEventListener("keydown", _videoKey);
}

Object.assign(window, { DATA, Placeholder, Frame, PlayBadge, playVideo, closeVideo });

// ---- Scroll reveal ---------------------------------------------------------
// Fades section blocks up into view as you scroll the real navigable pages.
// Only runs when the scaled-page shell (#scaler) exists, so the multi-artboard
// design canvas (which uses #root) is left untouched. Respects reduced-motion.
(function () {
  const REVEAL_CSS =
    ".ds-reveal{opacity:0;transform:translateY(26px);" +
    "transition:opacity .8s cubic-bezier(.16,.8,.26,1),transform .8s cubic-bezier(.16,.8,.26,1);" +
    "will-change:opacity,transform}" +
    ".ds-reveal.ds-in{opacity:1;transform:none}";

  function injectCss() {
    if (document.getElementById("ds-reveal-style")) return;
    const s = document.createElement("style");
    s.id = "ds-reveal-style";
    s.textContent = REVEAL_CSS;
    document.head.appendChild(s);
  }

  // Scroll-driven reveal, backed up by two redundant mechanisms so content
  // never gets stranded at opacity:0 in hosts where scroll events are flaky
  // (e.g. some standalone/bundled exports opened straight from disk, or
  // embeds that swap the document after load): a rAF polling loop, and a
  // hard force-reveal safety timeout.
  const pending = [];
  let ticking = 0;
  function check() {
    ticking = 0;
    const vh = window.innerHeight || document.documentElement.clientHeight;
    for (let i = pending.length - 1; i >= 0; i--) {
      const el = pending[i];
      const r = el.getBoundingClientRect();
      if (r.top < vh * 0.88) {
        el.classList.add("ds-in");
        pending.splice(i, 1);
      }
    }
  }

  function onScroll() {
    if (ticking) return;
    ticking = setTimeout(check, 16);
  }

  // rAF loop: keeps checking every frame while anything is still pending.
  // Cheap (a few getBoundingClientRect calls), self-stops once done, and
  // doesn't depend on the 'scroll' event actually firing.
  function rafLoop() {
    if (pending.length === 0) return;
    check();
    requestAnimationFrame(rafLoop);
  }

  // Absolute safety net: if scroll position genuinely never brings an
  // element into view (or this host never dispatches the signals above),
  // force it visible after a few seconds rather than hide content forever.
  function forceRevealAll() {
    for (let i = pending.length - 1; i >= 0; i--) {
      pending[i].classList.add("ds-in");
    }
    pending.length = 0;
  }

  let collected = false;

  function tag(el, i) {
    if (!el || el.classList.contains("ds-reveal")) return;
    el.classList.add("ds-reveal");
    el.style.transitionDelay = Math.min(i, 6) * 55 + "ms";
    pending.push(el);
  }

  // Pick the elements to fade: top-level page sections (skipping the sticky nav),
  // but for a tall section that holds a uniform list/grid, fade its rows/cards
  // individually so they cascade.
  function collectTargets() {
    const scaler = document.getElementById("scaler");
    const root = scaler && scaler.firstElementChild;
    if (!root) return;
    const sections = Array.from(root.children);
    sections.forEach((sec, idx) => {
      if (idx === 0) return; // leave the nav bar alone
      let host = sec;
      // descend through single-child wrappers (e.g. max-width containers)
      while (host.children.length === 1 && host.firstElementChild.children.length >= 2) {
        host = host.firstElementChild;
      }
      const kids = Array.from(host.children);
      // Only treat as a splittable list/grid when children are in normal flow
      // (stacked rows/cards) — never absolutely-positioned layers like a hero's
      // image + overlay + play button stack.
      const inFlow = kids.every((k) => {
        const p = getComputedStyle(k).position;
        return p === "static" || p === "relative" || p === "";
      });
      const tall = kids.filter((k) => k.offsetHeight >= 120);
      if (inFlow && kids.length >= 3 && tall.length >= 3) {
        kids.forEach((k, i) => tag(k, i));
      } else {
        tag(sec, idx);
      }
    });
    collected = true;
    check();
  }

  function boot() {
    const scaler = document.getElementById("scaler");
    if (!scaler) return; // not a scrolling page (design canvas etc.) — skip
    injectCss();
    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce) return; // show everything, no motion
    let raf = 0;
    const scan = () => {
      clearTimeout(raf);
      raf = setTimeout(collectTargets, 30);
    };
    if (window.MutationObserver) {
      new MutationObserver(scan).observe(scaler, { childList: true, subtree: true });
    }
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("wheel", onScroll, { passive: true });
    window.addEventListener("touchmove", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    window.addEventListener("load", scan);
    // Polling fallback: some embedded/scaled hosts don't dispatch scroll events
    // reliably. Cheap no-op once everything has revealed.
    const poll = setInterval(() => {
      check();
      if (collected && pending.length === 0) clearInterval(poll);
    }, 250);
    collectTargets();
    scan();
    requestAnimationFrame(rafLoop);
    setTimeout(forceRevealAll, 3000);
  }

  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
  else boot();
})();
