/* services.jsx — Sanket Chitnis · Services page · 3-service catalog
   USD only. Project-based pricing, never hourly.
   Build log: docs/superpowers/specs/2026-06-11-sunset-redesign-design.md */

const { useState, useEffect } = React;

/* Extract the first sentence of a string (everything up to and including
   the first "."). Used to derive hub-card taglines from existing scope
   text without changing any source copy. Falls back to the full string
   if no period is found. */
function firstSentence(s) {
  if (!s) return "";
  const i = s.indexOf(".");
  return i === -1 ? s : s.slice(0, i + 1);
}

/* Per-service display values derived from existing SERVICES data + build-log
   project references. These return view-only strings — no new fields added
   to SERVICES, no copy invented for the service itself. Sources noted per line.
   If Sanket wants to override any of these, change values here (centralized). */

/* LEGACY capped price bands · preserved for reference (2026-06-26).
   Replaced with open "From $X" values to remove the self-imposed ceiling
   and stay consistent with standalones / retainers. Full tier ladder
   (Starter / Standard / Premium with their exact prices) remains visible
   one scroll down. Old values:
     business-website: "$1.8k–$9.5k"
     ecommerce:        "$4k–$28k"
     product-build:    "$12k–$70k+"
*/
const SERVICE_DISPLAY = {
  "business-website": {
    slabText: "Conversion-focused · SEO + analytics baked in",
    statBand: "From $1,800",
    statTime: "2–9 wks",
    statThird: "5–12 pages",
    statThirdLbl: "Page count"
  },
  "ecommerce": {
    slabText: "Region-aware payments",
    statBand: "From $4,000",
    statTime: "3–16 wks",
    statThird: "≤500+ SKUs",
    statThirdLbl: "Catalogue depth"
  },
  "product-build": {
    slabText: "Multi-surface · web + mobile + admin",
    statBand: "From $15,000",
    statTime: "4–20 wks",
    statThird: "mobile + web + admin",
    statThirdLbl: "Surfaces"
  }
};

/* ──────────────────────────────────────────────────────────────
   DATA
   ──────────────────────────────────────────────────────────── */

const SERVICES = [
  {
    id: "product-build",
    n: "01",
    name: "Product build (apps)",
    scope: "Custom software products with login and a database. Web apps, mobile apps, both. Real users creating and changing their own data.",
    fit:   "Founders shipping a real product, not a brochure. Flexible scope and stack. Productized process: Scope sprint, Build, Care plan.",
    process: [
      { n: "1", name: "Scope sprint", meta: "$500 · credited back", desc: "Paid scoping engagement. Becomes the brief, the price, the timeline. Credited back to the build if you continue." },
      { n: "2", name: "Build",        meta: "fixed price + scope",  desc: "Fixed price, fixed scope, fixed timeline. Tier prices below." },
      { n: "3", name: "Care plan",    meta: "optional retainer",    desc: "Optional retainer after launch, sorted once you're on board." },
    ],
    tiers: [
      {
        id: "prod-starter",
        name: "Starter",
        from: "$15,000",
        role: "entry",
        timeline: "4 to 6 weeks",
        step: "Get a real first version into users' hands.",
        who: "Founder validating a real idea. Needs a working first version in users' hands, not a prototype on Figma.",
        inclOutcome: [
          "One platform, web or mobile, your pick",
          "The core features we scope together on the call",
          "Secure login and saved user data",
          "Live and usable (TestFlight or web)",
          "Basic analytics, so you can see how many people use it and what they do",
          "One round of revisions after launch",
        ],
        incl: [
          "Single platform (web or mobile, your pick)",
          "Core features, scoped on the call",
          "Auth + data persistence",
          "Deployed live (TestFlight or web)",
          "Basic analytics",
          "One round of revisions after launch",
        ],
        notIncl: "Payments. Push notifications. Admin dashboard. Multi-user roles. AI features.",
        looks: { text: "A waitlist with auth. A weather app for cyclists. A logged-in calculator with user history.", link: null },
      },
      {
        id: "prod-growth",
        name: "Growth",
        from: "$30,000",
        role: "hero",
        timeline: "6 to 10 weeks",
        step: "Launch publicly and start charging.",
        who: "You have validated the idea and are ready to ship a real first version. Launch on both stores, start charging, start growing.",
        inclOutcome: [
          "One app for both iOS and Android",
          "A solid backend running everything the app does",
          "Smooth sign-up and onboarding for your users",
          "Take payments online, with a provider that fits your market",
          "Push notifications to bring users back",
          "Live on the App Store and Play Store",
          "Crash monitoring",
          "A simple admin panel to see your users and activity",
          "30 days of support after launch",
        ],
        incl: [
          "Cross-platform mobile (iOS + Android)",
          "Backend with ~15 to 20 endpoints",
          "Auth + onboarding flow",
          "One payment provider, integrated",
          "Push notifications",
          "App Store + Play Store submissions",
          "Sentry crash reporting",
          "Light read-only admin panel",
          "30 days of post-launch support",
        ],
        notIncl: "AI / LLM features. Multi-user permission systems. Custom analytics pipeline.",
        looks: { text: "Subscription meal-planning app. Booking app for a chain of yoga studios. Tutoring marketplace with payments.", link: null },
      },
      {
        id: "prod-scale",
        name: "Scale",
        from: "$45,000",
        toCeiling: "to $90,000+",
        role: "showcase",
        timeline: "10 to 20 weeks",
        step: "Run it as a production business.",
        who: "Building a real business. Production-grade, compliance, AI features, multiple user types (consumers + providers + admin).",
        inclOutcome: [
          "App, website, and admin dashboard, all three",
          "A backend built to handle real scale",
          "Payments plus subscription plans",
          "Notifications your users can count on",
          "Product analytics set up, so you can see how people actually use it",
          "Crash and performance monitoring",
          "Privacy compliance built in (GDPR and your region's rules)",
          "AI features where they genuinely help (smart search, recommendations, automation)",
          "Role-based access, so customers, staff, and admins each get their own view and permissions",
          "60 to 90 days of support after launch",
        ],
        incl: [
          "Mobile + responsive web + admin dashboard (3 surfaces)",
          "Backend with 40+ endpoints",
          "Payments + subscription tiers",
          "Push orchestration (retry-safe state)",
          "Analytics wired in (GA4, Mixpanel, or self-hosted PostHog)",
          "Crash + performance monitoring",
          "Local compliance baked in (GDPR and your region's rules)",
          "Test coverage above 70%",
          "AI / LLM features (Claude, Groq, OpenAI)",
          "Role-based access control (RBAC) with per-role permissions",
          "60 to 90 days of post-launch support",
        ],
        notIncl: null,
        looks: { text: "A consumer community app with mobile + web + admin + payments + compliance baked in. A vet teleconsult product with video + prescriptions. A multi-tenant B2B SaaS.", link: null },
      },
    ],
  },

  {
    id: "business-website",
    n: "02",
    name: "Business website",
    scope: "Sites people read to find and contact a business. Home, about, services, projects, contact. Not e-commerce. Not a web app (no login, no database).",
    fit:   "Most service businesses. The site that sells the work, not the product.",
    tiers: [
      {
        id: "biz-starter",
        name: "Starter",
        from: "$1,800",
        role: "entry",
        timeline: "~2 weeks",
        who: "You need a credible web presence fast. A simple, well-built site that loads, ranks, and converts a lead form.",
        incl: [
          "Semi-custom 5-page design, responsive",
          "Lead form to email or Sheets",
          "Basic on-page SEO (titles, meta, schema)",
          "Hosting setup + handover",
        ],
        notIncl: null,
        looks: { text: "A clinic, a studio, a small consultancy. The site you can hand a client and not apologise for.", link: null },
      },
      {
        id: "biz-standard",
        name: "Standard",
        from: "$5,000",
        role: "hero",
        timeline: "3 to 5 weeks",
        who: "You have real positioning and want a site that earns trust, ranks, and feeds your pipeline. Most clients land here.",
        incl: [
          "Custom 8 to 12 page design, fully responsive",
          "CMS so you can edit copy and projects yourself",
          "CRM or email integration (HubSpot, Zoho, Mailchimp)",
          "Conversion-focused UX (sectioning, CTAs, trust strip)",
          "Technical SEO + analytics (GA4, GSC, schema)",
          "Two rounds of revisions",
        ],
        notIncl: null,
        looks: { text: "A developer-tooling marketing site with documentation entry points. A consultancy site with structured lead capture. A clinic with bookings tied to a CRM.", link: null },
      },
      {
        id: "biz-premium",
        name: "Premium",
        from: "$12,000",
        role: "showcase",
        timeline: "6 to 9 weeks",
        who: "You want the site to be the proof. Bespoke design system, motion that signals craft, lead pipelines that route into your CRM, copy that does the heavy lifting.",
        incl: [
          "Bespoke design system, no template DNA",
          "Motion + scroll work (GSAP, Lenis)",
          "Multi-step lead pipeline with CRM automation",
          "Full SEO programme (technical, content, schema)",
          "Copywriting collaboration",
          "GA4 + event tracking + dashboards",
        ],
        notIncl: null,
        looks: { text: "A motion-led brand site that earns dwell time. A premium real-estate showcase with SEO tuned for unbranded queries.", link: null },
      },
    ],
  },

  {
    id: "ecommerce",
    n: "03",
    name: "E-commerce",
    scope: "Online stores. Cart, checkout, payments, inventory, product taxonomies. Shopify, WooCommerce, or custom where it earns its keep.",
    fit:   "Brands that sell, not list. From a clean small catalogue to multi-currency stores with cross-border payment rails.",
    tiers: [
      {
        id: "ecom-starter",
        name: "Starter",
        from: "$4,000",
        role: "entry",
        timeline: "3 to 5 weeks",
        who: "Launching a clean storefront on a proven platform. Theme-led, fast to ship.",
        incl: [
          "Shopify or WooCommerce theme customisation",
          "Up to 50 products, one collection structure",
          "One payment gateway",
          "Basic on-page SEO + product schema",
          "30 days of post-launch support",
        ],
        notIncl: "Multi-currency. Multi-region payment routing. Custom integrations. Bespoke theme.",
        looks: { text: "A boutique brand, a single-category catalogue, a soft launch to test demand before the bigger build.", link: null },
      },
      {
        id: "ecom-standard",
        name: "Standard",
        from: "$12,000",
        role: "hero",
        timeline: "6 to 9 weeks",
        who: "You have real volume and need the store to do real work. Region-aware payments, integrations into your stack, conversion UX that holds up.",
        incl: [
          "Semi-custom theme on Shopify or Woo",
          "Up to 500 products, faceted filters",
          "1 to 2 integrations (CRM, ERP, fulfilment, reviews)",
          "Multi-region payment gateways",
          "Product + collection schema, technical SEO",
          "90 days of post-launch support",
        ],
        notIncl: "Bespoke from-scratch theme. Multi-currency / multi-region storefront. 3+ integrations. Custom checkout flows.",
        looks: { text: "A mid-catalogue D2C launch with HubSpot sync. A cross-border brand with Apple Pay + Google Pay at checkout. A made-to-order line with configurable products.", link: null },
      },
      {
        id: "ecom-premium",
        name: "Premium",
        from: "$32,000",
        role: "showcase",
        timeline: "10 to 16 weeks",
        who: "Bespoke storefront for a serious brand. Large catalogue, multi-currency, custom checkout, upsell and bundle logic, performance-tuned.",
        incl: [
          "Bespoke theme, no template DNA",
          "500+ SKUs, deep taxonomy",
          "3+ integrations (ERP, CRM, fulfilment, loyalty)",
          "Custom checkout + upsell + bundle logic",
          "Multi-currency, multi-region routing",
          "Full performance + SEO programme",
        ],
        notIncl: "Native iOS / Android apps. Marketplace or multi-vendor architecture. ERP-grade B2B portal.",
        looks: { text: "Multi-region D2C brand. Catalogue with configurable products + B2B portal. Premium storefront with regional payment rails.", link: null },
      },
    ],
  },
];

/* ADD_ONS + RETAINERS parked 2026-07-22 · see ../removed-from-website/ */

const SECTIONS = [
  { id: "product-build",    label: "Product build (apps)"     },
  { id: "business-website", label: "Business website" },
  { id: "ecommerce",        label: "E-commerce"        },
  { id: "process",          label: "Process"           },
];

/* ──────────────────────────────────────────────────────────────
   COMPONENTS
   ──────────────────────────────────────────────────────────── */

/* Toggle `html.scrolled` once the user scrolls past [data-glass-anchor]'s top
   crossing the viewport top. Falls back to a 40px sentinel if no anchor exists.
   Anchor pattern lets the main nav stay visible through the hero, then swap
   with the snav pill the moment the first real section reaches the top.
   IntersectionObserver = no scroll-event spam, plays nice with Lenis. */
function useScrolledState() {
  useEffect(() => {
    let anchor = document.querySelector("[data-glass-anchor]");
    let createdSentinel = false;
    if (!anchor) {
      anchor = document.createElement("div");
      anchor.setAttribute("aria-hidden", "true");
      anchor.style.cssText =
        "position:absolute;top:40px;left:0;width:1px;height:1px;pointer-events:none;";
      document.body.prepend(anchor);
      createdSentinel = true;
    }
    const obs = new IntersectionObserver(
      ([entry]) => {
        document.documentElement.classList.toggle(
          "scrolled",
          entry.boundingClientRect.top < 0
        );
      },
      { rootMargin: "0px 0px -100% 0px", threshold: 0 }
    );
    obs.observe(anchor);
    return () => {
      obs.disconnect();
      if (createdSentinel) anchor.remove();
      document.documentElement.classList.remove("scrolled");
    };
  }, []);
}

function Nav() {
  useScrolledState();
  return (
    <nav className="nav" aria-label="Main">
      <input type="checkbox" id="navtoggle" className="nav-toggle" hidden />
      <label htmlFor="navtoggle" className="nav-burger" aria-label="Menu"><span></span><span></span><span></span></label>
      <a className="nav-brand" href="home-sunset.html" aria-label="Home"><span className="sjlogo" aria-hidden="true"><svg viewBox="12 12 72 72" xmlns="http://www.w3.org/2000/svg"><g className="sjlogo-top"><path fill="currentColor" d="M78.2 41 H41 V55 H17.8 A31 31 0 1 1 78.2 41 Z"/></g><g className="sjlogo-bot"><path className="sjlogo-acc" d="M17.8 55 H41 V41 H78.2 A31 31 0 0 1 17.8 55 Z"/></g></svg></span></a>
      <div className="nav-links">
        <a href="home-sunset.html">Home</a>
        <a href="featured-work-night.html">Featured Work</a>
        <a href="services.html" className="active">Services</a>
        <a href="how-i-work.html">How I work</a>        <a className="nav-mcta" href="start-project.html">Get in touch</a>
      </div>
      <a className="nav-cta" href="start-project.html">Get in touch</a>
    </nav>
  );
}

function SectionNav() {
  const [active, setActive] = useState(SECTIONS[0].id);
  useEffect(() => {
    const obs = new IntersectionObserver(
      entries => {
        entries.forEach(e => { if (e.isIntersecting) setActive(e.target.id); });
      },
      { rootMargin: "-45% 0px -50% 0px", threshold: 0 }
    );
    SECTIONS.forEach(s => {
      const el = document.getElementById(s.id);
      if (el) obs.observe(el);
    });
    return () => obs.disconnect();
  }, []);
  return (
    <nav className="snav" aria-label="On this page">
      <div className="snav-inner">
        {SECTIONS.map(s => (
          <a key={s.id} href={"#" + s.id}
             className={"snav-link" + (active === s.id ? " active" : "")}>
            {s.label}
          </a>
        ))}
      </div>
    </nav>
  );
}

function Hero() {
  useEffect(() => {
    // word-mask reveal trigger (unchanged)
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        document.documentElement.classList.add("loaded");
      });
    });
  }, []);
  return (
    <section className="day" id="day-hero">
      <div className="day-stage">
        <canvas className="day-canvas" aria-hidden="true"></canvas>
        <div className="sky-dusk" aria-hidden="true"></div>
        <div className="sky-night" aria-hidden="true"></div>
        <div className="stars" aria-hidden="true">
          <i className="a"></i><i className="b"></i>
        </div>

        <div className="hero-copy">
          <div className="hero-eyebrow">
            <span className="sq"></span>
            <span>§ Services · Three build services · USD pricing</span>
          </div>
          <h1 className="hero-title">
            <span className="w accent"><span className="wi">Apps,{" "}</span></span><span className="w"><span className="wi">websites,</span></span><br/>
            <span className="w"><span className="wi">and{" "}</span></span><span className="w"><span className="wi">e-commerce.</span></span>
          </h1>
          <p className="hero-sub">
            Every idea worth building deserves more than a template. The one you've been carrying around for a while, ready to be shaped with intent and built into a product that feels like <span className="accent">where things are headed</span>.
          </p>
        </div>

        <a href="#services-overview" className="descend-hint" aria-label="Scroll to services">
          Scroll <span className="dn">↓</span>
        </a>
      </div>
    </section>
  );
}

function ServiceHub() {
  const [selectedId, setSelectedId] = useState(null);

  function onCardClick(e, id) {
    e.preventDefault();
    setSelectedId(id);
    const target = document.getElementById(id);
    if (!target) return;
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    target.scrollIntoView({ behavior: reduced ? "auto" : "smooth", block: "start" });
  }

  return (
    <nav className="hub" aria-label="Service overview">
      {SERVICES.map((s) => (
        <a
          key={s.id}
          href={"#" + s.id}
          className={"hub-card" + (selectedId === s.id ? " selected" : "")}
          data-svc={s.id}
          onClick={(e) => onCardClick(e, s.id)}
        >
          <div className="hub-n">{s.n}</div>
          <h3 className="hub-name">{s.name}</h3>
          <p className="hub-tag">{firstSentence(s.scope)}</p>
          <div className="hub-ladder">
            <div className="hub-floor"><span className="hub-from">from</span><span className="hub-floor-amt">{s.tiers[0].from}</span></div>
            {s.tiers.map((t) => {
              const isHero = t.role === "hero";
              return (
                <div key={t.id} className={"hub-row" + (isHero ? " mc" : "")}>
                  <span>
                    {t.name}
                    
                  </span>
                  <span className="hub-time">{t.timeline}</span>
                </div>
              );
            })}
          </div>
        </a>
      ))}
    </nav>
  );
}

function BusinessWebsitePlaceholder() {
  return (
    <div className="demo-placeholder">
      <div className="dp-chrome"><i></i><i></i><i></i><span>preview · business-website</span></div>
      <div className="dp-body">Business website demo · coming next</div>
    </div>
  );
}

function BusinessWebsiteDemo() {
  const containerRef = React.useRef(null);

  useEffect(() => {
    const root = containerRef.current;
    if (!root) return;

    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduced) return;

    // refs
    const page         = root.querySelector('[data-ref="page"]');
    const vHome        = root.querySelector('[data-ref="vHome"]');
    const vDetail      = root.querySelector('[data-ref="vDetail"]');
    const homeScroll   = root.querySelector('[data-ref="homeScroll"]');
    const dots         = root.querySelectorAll('[data-ref="dt"]');
    const cursor       = root.querySelector('[data-ref="cursor"]');
    const heroCta      = root.querySelector('[data-ref="heroCta"]');
    const urlPath      = root.querySelector('[data-ref="urlPath"]');
    const property     = root.querySelector('[data-ref="property"]');
    const viewFpCta    = root.querySelector('[data-ref="viewFpCta"]');
    const brochureCta  = root.querySelector('[data-ref="brochureCta"]');
    const modal        = root.querySelector('[data-ref="modal"]');
    const fName        = root.querySelector('[data-ref="fName"]');
    const fPhone       = root.querySelector('[data-ref="fPhone"]');
    const fEmail       = root.querySelector('[data-ref="fEmail"]');
    const sendBtn      = root.querySelector('[data-ref="sendBtn"]');
    const sendLbl      = root.querySelector('[data-ref="sendLbl"]');

    function setStage(i) {
      dots.forEach(function(d, j){ d.classList.toggle('on', j <= i); });
    }

    function setView(v) {
      [vHome, vDetail].forEach(function(view){ view.classList.remove('in'); });
      v.classList.add('in');
    }

    function setUrl(path) {
      urlPath.style.opacity = 0;
      setTimeout(function(){
        urlPath.textContent = path;
        urlPath.style.opacity = 1;
      }, 180);
    }

    function moveCursorTo(el) {
      if (!page || !el || !cursor) return;
      var pageR = page.getBoundingClientRect();
      var elR   = el.getBoundingClientRect();
      var x = elR.left + elR.width/2  - pageR.left - 9;
      var y = elR.top  + elR.height/2 - pageR.top  - 9;
      cursor.style.left = x + 'px';
      cursor.style.top  = y + 'px';
    }

    function clickAt(el, after) {
      moveCursorTo(el);
      setTimeout(function(){
        if (cursor) { cursor.classList.remove('click'); void cursor.offsetWidth; cursor.classList.add('click'); }
        if (after) setTimeout(after, 220);
      }, 850);
    }

    function typeText(el, text, perChar) {
      if (!el) return;
      el.textContent = '';
      var i = 0;
      perChar = perChar || 35;
      function step(){
        if (i <= text.length){
          el.textContent = text.slice(0, i);
          i++;
          setTimeout(step, perChar);
        }
      }
      step();
    }

    var timers = [];
    function tick(delay, fn){ timers.push(setTimeout(fn, delay)); }

    function reset() {
      timers.forEach(clearTimeout);
      timers = [];
      if (modal)    modal.classList.remove('in');
      if (property) property.classList.remove('hov');
      if (sendBtn)  sendBtn.classList.remove('confirm');
      if (sendLbl)  sendLbl.textContent = 'Request brochure →';
      if (fName)    fName.textContent   = '';
      if (fPhone)   fPhone.textContent  = '';
      if (fEmail)   fEmail.textContent  = '';
      if (homeScroll) homeScroll.scrollTop = 0;
      setView(vHome);
      setUrl('northbank-residences.com');
      if (cursor) { cursor.style.left = '60%'; cursor.style.top = '60%'; }
      setStage(-1);
    }

    function play() {
      reset();

      tick(400,   function(){ setStage(0); });
      tick(800,   function(){ moveCursorTo(heroCta); });
      tick(1500,  function(){ setStage(1); });
      tick(1500,  function(){ clickAt(heroCta, function(){
        homeScroll.scrollTo({ top: 380, behavior: 'smooth' });
        setUrl('northbank-residences.com#residences');
      }); });

      tick(3500,  function(){ setStage(2); });
      tick(3500,  function(){ moveCursorTo(property); });
      tick(3700,  function(){ if (property) property.classList.add('hov'); });

      tick(5000,  function(){ setStage(3); });
      tick(5000,  function(){ clickAt(viewFpCta, function(){
        if (property) property.classList.remove('hov');
        setView(vDetail);
        setUrl('northbank-residences.com/a-302');
      }); });

      tick(7600,  function(){ moveCursorTo(brochureCta); });
      tick(8000,  function(){ setStage(4); });
      tick(8000,  function(){ clickAt(brochureCta, function(){
        if (modal) modal.classList.add('in');
      }); });

      tick(9500,  function(){ setStage(5); });
      tick(9500,  function(){ typeText(fName,  'Reema Khanna', 32); });
      tick(11200, function(){ typeText(fPhone, '+91 98765 43210', 28); });
      tick(13400, function(){ typeText(fEmail, 'reema.khanna@email.com', 28); });

      tick(16000, function(){ setStage(6); });
      tick(16000, function(){ clickAt(sendBtn, function(){
        if (sendBtn) sendBtn.classList.add('confirm');
        if (sendLbl) sendLbl.textContent = '✓ Brochure sent. Check your inbox in 5 minutes.';
        setUrl('northbank-residences.com/brochure-sent');
      }); });

      tick(19000, function(){ setStage(7); });

      tick(22500, play);
    }

    const io = new IntersectionObserver(function(entries){
      entries.forEach(function(e){
        if (e.isIntersecting) {
          if (timers.length === 0) play();
        } else {
          timers.forEach(clearTimeout); timers = [];
        }
      });
    }, { threshold: 0.25 });
    io.observe(root);

    return function(){
      timers.forEach(clearTimeout);
      io.disconnect();
    };
  }, []);

  return (
    <div ref={containerRef} className="bw-demo">
      <div className="bwd-browser">
        <div className="bwd-chrome">
          <i></i><i></i><i></i>
          <div className="bwd-urlbar"><span data-ref="urlPath">northbank-residences.com</span></div>
        </div>
        <div className="bwd-page" data-ref="page">
          <div className="bwd-stage-dots">
            <span className="bwd-dt" data-ref="dt"></span>
            <span className="bwd-dt" data-ref="dt"></span>
            <span className="bwd-dt" data-ref="dt"></span>
            <span className="bwd-dt" data-ref="dt"></span>
            <span className="bwd-dt" data-ref="dt"></span>
            <span className="bwd-dt" data-ref="dt"></span>
            <span className="bwd-dt" data-ref="dt"></span>
            <span className="bwd-dt" data-ref="dt"></span>
          </div>

          {/* HOME VIEW */}
          <div className="bwd-view bwd-v-home in" data-ref="vHome">
            <div className="bwd-top-strip">
              <div className="bwd-brand">Northbank</div>
              <div className="bwd-navs">
                <span className="active">Residences</span>
                <span>Amenities</span>
                <span>Visit</span>
              </div>
            </div>
            <div className="bwd-home-scroll" data-ref="homeScroll">
              <div className="bwd-hero">
                <div className="bwd-hero-img"></div>
                <div className="bwd-hero-text">
                  <div className="bwd-eyebrow">A 32-residence development &middot; Possession Q3 2026</div>
                  <h2>Beyond, <em>by the river.</em></h2>
                  <p>Riverside living, twenty minutes from the city. Two- and three-bedroom homes with private balconies, designed around light and view.</p>
                  <span className="bwd-cta" data-ref="heroCta">Explore residences &rarr;</span>
                </div>
              </div>

              <div className="bwd-property-section">
                <div className="bwd-section-head">
                  <h3>This residence</h3>
                  <span>A-302 &middot; available</span>
                </div>
                <div className="bwd-property" data-ref="property">
                  <div className="bwd-prop-photo bwd-prop-photo-main"></div>
                  <div className="bwd-prop-content">
                    <div className="bwd-prop-meta">
                      <div className="bwd-prop-id">A-302</div>
                      <div className="bwd-prop-tag">2BHK &middot; River-facing</div>
                    </div>
                    <h4 className="bwd-prop-title">A north-east corner, <em>twelve floors up.</em></h4>
                    <p className="bwd-prop-body">Floor-to-ceiling windows on two sides, a deep balcony that faces the bend in the river, and direct light from 6am until late afternoon. The kitchen opens onto the living room. The second bedroom doubles as a study.</p>
                    <div className="bwd-prop-stats">
                      <div className="bwd-prop-stat"><span className="bwd-ps-v">1,180</span><span className="bwd-ps-l">sqft carpet</span></div>
                      <div className="bwd-prop-stat"><span className="bwd-ps-v">2</span><span className="bwd-ps-l">bedrooms</span></div>
                      <div className="bwd-prop-stat"><span className="bwd-ps-v">3 / 12</span><span className="bwd-ps-l">floor</span></div>
                      <div className="bwd-prop-stat"><span className="bwd-ps-v">&#8377;1.84 Cr</span><span className="bwd-ps-l">all-inclusive</span></div>
                    </div>
                    <div className="bwd-prop-gallery">
                      <div className="bwd-prop-photo bwd-prop-photo-1"></div>
                      <div className="bwd-prop-photo bwd-prop-photo-2"></div>
                    </div>
                    <span className="bwd-cta" data-ref="viewFpCta">View floor plan + specs &rarr;</span>
                  </div>
                </div>
              </div>
            </div>
          </div>

          {/* UNIT DETAIL VIEW */}
          <div className="bwd-view bwd-v-detail" data-ref="vDetail">
            <span className="bwd-back" data-ref="back">&larr; Back to residences</span>
            <div className="bwd-unit-detail">
              <div className="bwd-unit-hero"></div>
              <div className="bwd-unit-meta">
                <div className="bwd-unit-id">A-302</div>
                <h2 className="bwd-unit-title">2BHK &middot; <em>River-facing.</em></h2>
                <div className="bwd-unit-price">&#8377;1.84 Cr <span>&middot; all-inclusive</span></div>
              </div>
              <div className="bwd-unit-body">
                <div className="bwd-floorplan">
                  <div className="bwd-fp-label">Floor plan</div>
                  <div className="bwd-fp-frame">
                    <div className="bwd-fp-room bwd-fp-living"><span>Living</span></div>
                    <div className="bwd-fp-room bwd-fp-bed1"><span>Bed 1</span></div>
                    <div className="bwd-fp-room bwd-fp-bed2"><span>Bed 2</span></div>
                    <div className="bwd-fp-room bwd-fp-kitchen"><span>Kitchen</span></div>
                    <div className="bwd-fp-room bwd-fp-bath"><span>Bath</span></div>
                    <div className="bwd-fp-room bwd-fp-balcony"><span>Balcony</span></div>
                  </div>
                </div>
                <div className="bwd-unit-specs">
                  <div className="bwd-spec"><span className="bwd-spec-l">Carpet area</span><span className="bwd-spec-v">1,180 sqft</span></div>
                  <div className="bwd-spec"><span className="bwd-spec-l">Bedrooms</span><span className="bwd-spec-v">2</span></div>
                  <div className="bwd-spec"><span className="bwd-spec-l">Bathrooms</span><span className="bwd-spec-v">2</span></div>
                  <div className="bwd-spec"><span className="bwd-spec-l">Balconies</span><span className="bwd-spec-v">1 (river-side)</span></div>
                  <div className="bwd-spec"><span className="bwd-spec-l">Floor</span><span className="bwd-spec-v">3 of 12</span></div>
                  <div className="bwd-spec"><span className="bwd-spec-l">View</span><span className="bwd-spec-v">River-facing</span></div>
                  <div className="bwd-cta" data-ref="brochureCta">Request floor plan + brochure &rarr;</div>
                </div>
              </div>
            </div>

            {/* BROCHURE FORM MODAL */}
            <div className="bwd-modal" data-ref="modal">
              <div className="bwd-modal-card">
                <div className="bwd-modal-h">Request brochure &middot; A-302</div>
                <p className="bwd-modal-sub">Floor plan, full specs, and price breakup sent to your inbox in 5 minutes.</p>
                <div className="bwd-field"><label>Name</label><div className="bwd-input"><span data-ref="fName"></span><span className="bwd-caret"></span></div></div>
                <div className="bwd-field"><label>Phone</label><div className="bwd-input"><span data-ref="fPhone"></span><span className="bwd-caret"></span></div></div>
                <div className="bwd-field"><label>Email</label><div className="bwd-input"><span data-ref="fEmail"></span><span className="bwd-caret"></span></div></div>
                <button className="bwd-send" data-ref="sendBtn"><span data-ref="sendLbl">Request brochure &rarr;</span></button>
              </div>
            </div>
          </div>

          <div className="bwd-cursor" data-ref="cursor"></div>
        </div>
      </div>
      <div className="bwd-stack-caption"><span><b>Real estate &middot; brochure flow</b></span><span className="bwd-live">Live</span></div>
    </div>
  );
}

function ProductBuildPlaceholder() {
  return (
    <div className="demo-placeholder">
      <div className="dp-chrome"><i></i><i></i><i></i><span>preview · product-build</span></div>
      <div className="dp-body">Product build demo · coming next</div>
    </div>
  );
}

function ProductBuildDemo() {
  const containerRef = React.useRef(null);

  useEffect(() => {
    const root = containerRef.current;
    if (!root) return;

    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduced) return;

    // refs — cursor is relative to phone screen for positioning
    const phoneScreen  = root.querySelector('.pb-phone-screen');
    const dots         = root.querySelectorAll('[data-ref="dt"]');
    const cursor       = root.querySelector('[data-ref="cursor"]');
    const mHome        = root.querySelector('[data-ref="mHome"]');
    const mBook        = root.querySelector('[data-ref="mBook"]');
    const mConfirm     = root.querySelector('[data-ref="mConfirm"]');
    const push         = root.querySelector('[data-ref="push"]');
    const reserveBtn   = root.querySelector('[data-ref="reserveBtn"]');
    const timeSlot8    = root.querySelector('[data-ref="timeSlot8"]');
    const confirmBtn   = root.querySelector('[data-ref="confirmBtn"]');
    const coversN      = root.querySelector('[data-ref="coversN"]');
    const coversTile   = root.querySelector('[data-ref="coversTile"]');
    const growBar      = root.querySelector('[data-ref="growBar"]');
    const activityList = root.querySelector('[data-ref="activityList"]');
    const aiCard       = root.querySelector('[data-ref="aiCard"]');
    const aiBody       = root.querySelector('[data-ref="aiBody"]');

    function setStage(i) {
      dots.forEach(function(d, j){ d.classList.toggle('on', j <= i); });
    }

    function setMView(v) {
      [mHome, mBook, mConfirm].forEach(function(view){
        if (view) view.classList.remove('in');
      });
      if (v) v.classList.add('in');
    }

    function moveCursorTo(el) {
      if (!phoneScreen || !el || !cursor) return;
      var screenR = phoneScreen.getBoundingClientRect();
      var elR     = el.getBoundingClientRect();
      var x = elR.left + elR.width / 2  - screenR.left - 9;
      var y = elR.top  + elR.height / 2 - screenR.top  - 9;
      cursor.style.left = x + 'px';
      cursor.style.top  = y + 'px';
    }

    function clickAt(el, after) {
      moveCursorTo(el);
      setTimeout(function(){
        if (cursor) { cursor.classList.remove('click'); void cursor.offsetWidth; cursor.classList.add('click'); }
        if (after) setTimeout(after, 220);
      }, 850);
    }

    function typeText(el, text, perChar) {
      if (!el) return;
      el.textContent = '';
      var i = 0;
      perChar = perChar || 35;
      function step(){
        if (i <= text.length){
          el.textContent = text.slice(0, i);
          i++;
          setTimeout(step, perChar);
        }
      }
      step();
    }

    function appendActivity(text) {
      if (!activityList) return;
      var li = document.createElement('li');
      li.className = 'pb-aitem';
      li.textContent = text;
      activityList.appendChild(li);
    }

    var timers = [];
    function tick(delay, fn){ timers.push(setTimeout(fn, delay)); }

    function reset() {
      timers.forEach(clearTimeout);
      timers = [];
      setMView(mHome);
      if (activityList) activityList.innerHTML = '';
      if (coversN)    coversN.textContent    = '47';
      if (aiBody)     aiBody.textContent     = '';
      if (aiCard)     aiCard.classList.remove('in');
      if (push)       push.classList.remove('in');
      if (growBar)    growBar.style.height   = '42%';
      if (coversTile) coversTile.classList.remove('flash');
      if (timeSlot8)  timeSlot8.classList.remove('active');
      if (cursor) { cursor.style.left = '50%'; cursor.style.top = '72%'; }
      setStage(-1);
    }

    function play() {
      reset();

      // Stage 0 — Land on mHome
      tick(400,  function(){ setStage(0); });
      tick(800,  function(){ moveCursorTo(reserveBtn); });

      // Stage 1 — Tap "Reserve a table" -> mBook
      tick(1500, function(){ setStage(1); });
      tick(1500, function(){ clickAt(reserveBtn, function(){ setMView(mBook); }); });

      // Stage 2 — Pick 8:00 PM time slot
      tick(3000, function(){ moveCursorTo(timeSlot8); });
      tick(3000, function(){ setStage(2); });
      tick(3500, function(){ clickAt(timeSlot8, function(){
        if (timeSlot8) timeSlot8.classList.add('active');
      }); });

      // Stage 3 — Confirm reservation -> mConfirm + admin updates
      tick(4500, function(){ moveCursorTo(confirmBtn); });
      tick(4500, function(){ setStage(3); });
      tick(4500, function(){ clickAt(confirmBtn, function(){
        setMView(mConfirm);
        setTimeout(function(){
          if (coversN) coversN.textContent = '49';
          if (coversTile) {
            coversTile.classList.add('flash');
            setTimeout(function(){ coversTile.classList.remove('flash'); }, 500);
          }
          appendActivity('Party of 2 · 8:00 PM · Fri 28 Jun');
          if (growBar) growBar.style.height = '88%';
        }, 500);
      }); });

      // Stage 4 — Pre-auth captured
      tick(7000, function(){ setStage(4); });
      tick(7000, function(){ appendActivity('Pre-auth captured · €60 hold'); });

      // Stage 5 — AI insight types in
      tick(9000, function(){ setStage(5); });
      tick(9000, function(){
        if (aiCard) aiCard.classList.add('in');
        typeText(aiBody, 'Saturday 8pm is filling fast (87% booked). Open the 9:30 slot for 4-tops?', 30);
      });

      // Stage 6 — Reminder push slides in
      tick(13500, function(){ setStage(6); });
      tick(13500, function(){ if (push) push.classList.add('in'); });

      // Stage 7 — Push slides away; dashboard fully populated
      tick(16500, function(){ setStage(7); });
      tick(16500, function(){ if (push) push.classList.remove('in'); });

      // Loop
      tick(24000, play);
    }

    const io = new IntersectionObserver(function(entries){
      entries.forEach(function(e){
        if (e.isIntersecting) {
          if (timers.length === 0) play();
        } else {
          timers.forEach(clearTimeout); timers = [];
        }
      });
    }, { threshold: 0.25 });
    io.observe(root);

    return function(){
      timers.forEach(clearTimeout);
      io.disconnect();
    };
  }, []);

  return (
    <div ref={containerRef} className="pb-demo">
      <div className="pb-grid">

        {/* LEFT — Premium iPhone mockup */}
        <div className="pb-phone-col">
          <div className="pb-phone">
            <div className="pb-phone-island"></div>
            <div className="pb-phone-screen">
              <div className="pb-statusbar">
                <span className="pb-time">9:41</span>
                <span className="pb-icons">&#x25B2;&#x25B2;&#x25B2; &#x1F4F6; &#x1F50B;</span>
              </div>

              {/* mHome — land screen */}
              <div className="pb-mview pb-mv-home in" data-ref="mHome">
                <div className="pb-mapp-header">
                  <span className="pb-mapp-brand">Marlow &amp; Park</span>
                  <span className="pb-mapp-dot"></span>
                </div>
                <div className="pb-mhome-wordmark">Marlow &amp; Park</div>
                <div className="pb-mhome-h"><em>Tonight, with us.</em></div>
                <p className="pb-mhome-sub">A table, a menu, an evening worth remembering.</p>
                <div className="pb-mhome-hero-img"></div>
                <button className="pb-mcta" data-ref="reserveBtn">Reserve a table</button>
              </div>

              {/* mBook — booking step */}
              <div className="pb-mview pb-mv-book" data-ref="mBook">
                <div className="pb-mapp-header">
                  <span className="pb-mapp-brand">Marlow &amp; Park</span>
                  <span className="pb-mapp-dot"></span>
                </div>
                <div className="pb-mbook-hero" data-ref="bookHero"></div>
                <div className="pb-mbook-h"><em>Reserve a table</em></div>
                <div className="pb-mbook-section-lbl">Party size</div>
                <div className="pb-mbook-pills-row">
                  <span className="pb-mbook-pill">1</span>
                  <span className="pb-mbook-pill active">2</span>
                  <span className="pb-mbook-pill">3</span>
                  <span className="pb-mbook-pill">4</span>
                  <span className="pb-mbook-pill">5</span>
                  <span className="pb-mbook-pill">6+</span>
                </div>
                <div className="pb-mbook-section-lbl">Date</div>
                <div className="pb-mbook-pills-row">
                  <span className="pb-mbook-pill active">Fri 28 Jun</span>
                  <span className="pb-mbook-pill">Sat 29 Jun</span>
                  <span className="pb-mbook-pill">Sun 30 Jun</span>
                </div>
                <div className="pb-mbook-section-lbl">Time</div>
                <div className="pb-mbook-time-grid">
                  <span className="pb-mbook-pill">7:00</span>
                  <span className="pb-mbook-pill">7:30</span>
                  <span className="pb-mbook-pill" data-ref="timeSlot8">8:00</span>
                  <span className="pb-mbook-pill">8:30</span>
                  <span className="pb-mbook-pill">9:00</span>
                  <span className="pb-mbook-pill">9:30</span>
                </div>
                <button className="pb-mcta" data-ref="confirmBtn">Confirm reservation</button>
                <div className="pb-mbook-trust">Free cancellation up to 24 hrs &middot; No card needed to hold</div>
              </div>

              {/* mConfirm — confirmed */}
              <div className="pb-mview pb-mv-confirm" data-ref="mConfirm">
                <div className="pb-mapp-header">
                  <span className="pb-mapp-brand">Marlow &amp; Park</span>
                  <span className="pb-mapp-dot"></span>
                </div>
                <div className="pb-mconfirm-img" data-ref="confirmImg"></div>
                <div className="pb-mconfirm-status">RESERVATION CONFIRMED</div>
                <div className="pb-mconfirm-check">&#10003;</div>
                <div className="pb-mconfirm-h"><em>Table for 2 &middot; Fri 28 Jun &middot; 8:00 PM</em></div>
                <p className="pb-mconfirm-sub">We've held your table. Card pre-auth &middot; &euro;60 per cover, charged only on no-show.</p>
                <div className="pb-mconfirm-ref">REF &middot; M-2847</div>
                <div className="pb-mconfirm-buttons">
                  <button className="pb-mconfirm-btn-sm">Add to calendar</button>
                  <button className="pb-mconfirm-btn-sm pb-mconfirm-btn-ghost">Cancel</button>
                </div>
              </div>

              {/* Push notification overlay */}
              <div className="pb-push" data-ref="push">
                <div className="pb-push-ic">&#127374;</div>
                <div className="pb-push-body">
                  <div className="pb-push-t">Marlow &amp; Park &middot; Reminder</div>
                  <div className="pb-push-b">We'll see you tomorrow at 8:00 PM. Tap for directions.</div>
                </div>
              </div>

              {/* Cursor — relative to phone screen */}
              <div className="pb-cursor" data-ref="cursor"></div>

              <div className="pb-home-indicator"></div>
            </div>
          </div>
        </div>

        {/* RIGHT — Analytics column on dark bg */}
        <div className="pb-admin-col">
          <div className="pb-admin-head">
            <h3 className="pb-admin-today"><em>Tonight</em></h3>
            <span className="pb-admin-meta">Marlow &amp; Park &middot; Live</span>
          </div>

          <div className="pb-stats">
            <div className="pb-stat" data-ref="coversTile">
              <div className="pb-stat-l">Covers tonight</div>
              <div className="pb-stat-v"><span data-ref="coversN">47</span></div>
            </div>
            <div className="pb-stat">
              <div className="pb-stat-l">Avg spend</div>
              <div className="pb-stat-v">&euro;142</div>
            </div>
            <div className="pb-stat">
              <div className="pb-stat-l">No-shows</div>
              <div className="pb-stat-v">2</div>
            </div>
          </div>

          <div className="pb-chart">
            <div className="pb-chart-h">Reservations by hour</div>
            <div className="pb-chart-bars">
              <div className="pb-bar" style={{height:"28%"}}></div>
              <div className="pb-bar" style={{height:"45%"}}></div>
              <div className="pb-bar" style={{height:"60%"}}></div>
              <div className="pb-bar" style={{height:"75%"}}></div>
              <div className="pb-bar" data-ref="growBar" style={{height:"42%"}}></div>
              <div className="pb-bar" style={{height:"55%"}}></div>
              <div className="pb-bar" style={{height:"35%"}}></div>
              <div className="pb-bar" style={{height:"18%"}}></div>
            </div>
          </div>

          <div className="pb-activity">
            <div className="pb-activity-h">Recent activity</div>
            <ul className="pb-activity-list" data-ref="activityList"></ul>
          </div>

          <div className="pb-ai" data-ref="aiCard">
            <div className="pb-ai-h"><span className="pb-ai-ic">&#9889;</span><em>AI Insight</em></div>
            <p className="pb-ai-body" data-ref="aiBody"></p>
          </div>
        </div>

      </div>

      {/* Stage progress dots */}
      <div className="pb-stage-dots">
        <span className="pb-dt" data-ref="dt"></span>
        <span className="pb-dt" data-ref="dt"></span>
        <span className="pb-dt" data-ref="dt"></span>
        <span className="pb-dt" data-ref="dt"></span>
        <span className="pb-dt" data-ref="dt"></span>
        <span className="pb-dt" data-ref="dt"></span>
        <span className="pb-dt" data-ref="dt"></span>
        <span className="pb-dt" data-ref="dt"></span>
      </div>
    </div>
  );
}

function EcommercePlaceholder() {
  return (
    <div className="demo-placeholder">
      <div className="dp-chrome"><i></i><i></i><i></i><span>preview · ecommerce</span></div>
      <div className="dp-body">E-commerce demo · landing in Task 9</div>
    </div>
  );
}

function EcommerceDemo() {
  const containerRef = React.useRef(null);

  useEffect(() => {
    const root = containerRef.current;
    if (!root) return;

    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduced) return;

    // refs
    const page       = root.querySelector('[data-ref="page"]');
    const vHome      = root.querySelector('[data-ref="vHome"]');
    const vDetail    = root.querySelector('[data-ref="vDetail"]');
    const vCheckout  = root.querySelector('[data-ref="vCheckout"]');
    const homeScroll = root.querySelector('[data-ref="homeScroll"]');
    const products   = root.querySelectorAll('[data-ref="product"]');
    const dots       = root.querySelectorAll('[data-ref="dt"]');
    const cursor     = root.querySelector('[data-ref="cursor"]');
    const heroCta    = root.querySelector('[data-ref="heroCta"]');
    const cartBtn    = root.querySelector('[data-ref="cart"]');
    const cartCount  = root.querySelector('[data-ref="cartCount"]');
    const cartCount2 = root.querySelector('[data-ref="cartCount2"]');
    const urlPath    = root.querySelector('[data-ref="urlPath"]');
    const pdName     = root.querySelector('[data-ref="pdName"]');
    const pdPrice    = root.querySelector('[data-ref="pdPrice"]');
    const pdCat      = root.querySelector('[data-ref="pdCat"]');
    const addBag     = root.querySelector('[data-ref="addBag"]');
    const toast      = root.querySelector('[data-ref="toast"]');
    const subtotal   = root.querySelector('[data-ref="subtotal"]');
    const vatEl      = root.querySelector('[data-ref="vat"]');
    const totalEl    = root.querySelector('[data-ref="total"]');
    const payEl      = root.querySelector('[data-ref="pay"]');
    const payLbl     = root.querySelector('[data-ref="payLbl"]');
    const addrText   = root.querySelector('[data-ref="addrText"]');
    const coCount    = root.querySelector('[data-ref="coCount"]');

    function setStage(i) {
      dots.forEach(function(d, j){ d.classList.toggle('on', j <= i); });
    }

    function setView(v) {
      [vHome, vDetail, vCheckout].forEach(function(view){ view.classList.remove('in'); });
      v.classList.add('in');
    }

    function setUrl(path) {
      urlPath.style.opacity = 0;
      setTimeout(function(){
        urlPath.textContent = path;
        urlPath.style.opacity = 1;
      }, 180);
    }

    function setBag(n) {
      if (cartCount)  cartCount.textContent = n;
      if (cartCount2) cartCount2.textContent = n;
      [cartBtn].forEach(function(b){
        if (!b) return;
        b.classList.add('pop');
        setTimeout(function(){ b.classList.remove('pop'); }, 360);
      });
    }

    function moveCursorTo(el) {
      if (!page || !el || !cursor) return;
      var pageR = page.getBoundingClientRect();
      var elR   = el.getBoundingClientRect();
      var x = elR.left + elR.width/2  - pageR.left - 9;
      var y = elR.top  + elR.height/2 - pageR.top  - 9;
      cursor.style.left = x + 'px';
      cursor.style.top  = y + 'px';
    }

    function clickAt(el, after) {
      moveCursorTo(el);
      setTimeout(function(){
        if (cursor) { cursor.classList.remove('click'); void cursor.offsetWidth; cursor.classList.add('click'); }
        if (el.classList.contains('ecd-cta') || el.classList.contains('ecd-add-bag') || el.classList.contains('ecd-pay')){
          el.classList.add('hov');
          setTimeout(function(){ el.classList.remove('hov'); }, 600);
        }
        if (after) setTimeout(after, 220);
      }, 850);
    }

    function hoverHighlight(el, ms) {
      el.classList.add('hov');
      setTimeout(function(){ el.classList.remove('hov'); }, ms || 600);
    }

    function countUp(el, from, to, dur) {
      var start = performance.now();
      function step(now){
        var t = Math.min(1, (now - start) / dur);
        var v = from + (to - from) * (1 - Math.pow(1 - t, 3));
        el.textContent = '$' + v.toFixed(2);
        if (t < 1) requestAnimationFrame(step);
      }
      requestAnimationFrame(step);
    }

    function typeText(el, text, perChar) {
      el.textContent = '';
      var i = 0;
      perChar = perChar || 35;
      function step(){
        if (i <= text.length){
          el.textContent = text.slice(0, i);
          i++;
          setTimeout(step, perChar);
        }
      }
      step();
    }

    var bag = [];
    var timers = [];
    function tick(delay, fn){ timers.push(setTimeout(fn, delay)); }

    function reset() {
      timers.forEach(clearTimeout);
      timers = [];
      bag = [];
      products.forEach(function(p){ p.classList.remove('adding','hov'); });
      root.querySelectorAll('[data-gw]').forEach(function(g){ g.classList.remove('sel'); });
      if (payEl)  { payEl.classList.remove('glow','confirm'); }
      if (payLbl) { payLbl.textContent = 'Continue to pay'; }
      if (subtotal) subtotal.textContent = '$0';
      if (vatEl)    vatEl.textContent    = '$0';
      if (totalEl)  totalEl.textContent  = '$0';
      if (cartCount)  cartCount.textContent  = '0';
      if (cartCount2) cartCount2.textContent = '0';
      if (addrText)   addrText.textContent   = '';
      if (coCount)    coCount.textContent     = '0';
      if (homeScroll) homeScroll.style.transform = 'translateY(0)';
      setView(vHome);
      setUrl('sample.store');
      if (cursor) { cursor.style.left = '60%'; cursor.style.top = '60%'; }
      setStage(-1);
    }

    function play() {
      reset();

      // STAGE 0 — Landed
      tick(400,  function(){ setStage(0); });
      tick(600,  function(){ moveCursorTo(heroCta); });

      // STAGE 1 — Click CTA, scroll to products
      tick(1500, function(){ setStage(1); });
      tick(1500, function(){ clickAt(heroCta, function(){
        if (homeScroll) homeScroll.style.transform = 'translateY(-220px)';
        setUrl('sample.store/shop');
      }); });

      // STAGE 2 — Browse, hover a product
      tick(3300, function(){ setStage(2); });
      tick(3500, function(){ moveCursorTo(products[0]); hoverHighlight(products[0], 1200); });

      // STAGE 3 — Click product → detail view
      tick(4900, function(){ setStage(3); });
      tick(5000, function(){ clickAt(products[0], function(){
        if (pdName)  pdName.textContent  = 'The Everyday Tote';
        if (pdPrice) pdPrice.textContent = '$84';
        if (pdCat)   pdCat.textContent   = 'Apparel · New in';
        setView(vDetail);
        setUrl('sample.store/everyday-tote');
        vDetail.scrollTop = 0;
      }); });

      // Scroll down through editorial content
      tick(6500, function(){ setStage(3); });
      tick(6500, function(){ vDetail.scrollTo({ top: 260, behavior: 'smooth' }); });
      tick(8200, function(){ vDetail.scrollTo({ top: 480, behavior: 'smooth' }); });
      tick(9800, function(){ setStage(3); });
      tick(9800, function(){ vDetail.scrollTo({ top: 0, behavior: 'smooth' }); });

      // STAGE 4 — Add to bag
      tick(10900, function(){ setStage(4); });
      tick(11000, function(){ clickAt(addBag, function(){
        bag.push({ name: 'The Everyday Tote', price: 84 });
        setBag(bag.length);
        if (toast) {
          toast.classList.add('in');
          setTimeout(function(){ toast.classList.remove('in'); }, 1400);
        }
      }); });

      // STAGE 5 — Back to shop, pick another product
      tick(12900, function(){ setStage(5); });
      tick(13000, function(){ setView(vHome); setUrl('sample.store/shop'); });
      tick(13600, function(){ moveCursorTo(products[2]); hoverHighlight(products[2], 800); });
      tick(14500, function(){ setStage(6); });
      tick(14600, function(){
        products[2].classList.add('adding');
        setTimeout(function(){
          products[2].classList.remove('adding');
          bag.push({ name: 'Field Notes Set', price: 56 });
          setBag(bag.length);
        }, 600);
      });

      // STAGE 6 — Bag icon → checkout
      tick(16400, function(){ setStage(7); });
      tick(16600, function(){ clickAt(cartBtn, function(){
        setView(vCheckout);
        setUrl('sample.store/checkout');
        if (coCount) coCount.textContent = bag.length;
        // type address
        setTimeout(function(){ if (addrText) typeText(addrText, 'Sanket Chitnis · 18 Sheikh Zayed Rd, Dubai · UAE', 28); }, 240);
        // compute totals
        var sub      = bag.reduce(function(s,i){ return s + i.price; }, 0);
        var ship     = 12;
        var vatVal   = (sub + ship) * 0.05;
        var totalVal = sub + ship + vatVal;
        setTimeout(function(){ if (subtotal) countUp(subtotal, 0, sub, 600); }, 500);
        setTimeout(function(){ if (vatEl)    countUp(vatEl,    0, vatVal, 700); }, 700);
        setTimeout(function(){ if (totalEl)  countUp(totalEl,  0, totalVal, 800); }, 900);
      }); });

      // STAGE 7 — Pick payment method
      tick(19700, function(){ setStage(7); });
      tick(19800, function(){
        var ap = root.querySelector('[data-gw="apple"]');
        if (!ap) return;
        clickAt(ap, function(){
          ap.classList.add('sel');
          if (payEl) payEl.classList.add('glow');
        });
      });

      // STAGE 8 — Pay & confirm
      tick(21800, function(){ setStage(7); });
      tick(21900, function(){ clickAt(payEl, function(){
        if (payEl)  payEl.classList.remove('glow');
        if (payEl)  payEl.classList.add('confirm');
        if (payLbl) payLbl.textContent = '✓ Order confirmed';
      }); });
      tick(23400, function(){ setStage(7); });

      // LOOP
      tick(25900, play);
    }

    const io = new IntersectionObserver(function(entries){
      entries.forEach(function(e){
        if (e.isIntersecting) {
          if (timers.length === 0) play();
        } else {
          timers.forEach(clearTimeout); timers = [];
        }
      });
    }, { threshold: 0.25 });
    io.observe(root);

    return function(){
      timers.forEach(clearTimeout);
      io.disconnect();
    };
  }, []);

  return (
    <div ref={containerRef} className="ecom-demo">
      <div className="ecd-browser" data-ref="browser">
        <div className="ecd-chrome">
          <i></i><i></i><i></i>
          <div className="ecd-urlbar"><span data-ref="urlPath">sample.store</span></div>
        </div>
        <div className="ecd-page" data-ref="page">
          <div className="ecd-stage-dots">
            <span className="ecd-dt" data-ref="dt"></span>
            <span className="ecd-dt" data-ref="dt"></span>
            <span className="ecd-dt" data-ref="dt"></span>
            <span className="ecd-dt" data-ref="dt"></span>
            <span className="ecd-dt" data-ref="dt"></span>
            <span className="ecd-dt" data-ref="dt"></span>
            <span className="ecd-dt" data-ref="dt"></span>
            <span className="ecd-dt" data-ref="dt"></span>
          </div>

          {/* HOME VIEW */}
          <div className="ecd-view ecd-v-home in" data-ref="vHome">
            <div className="ecd-top-strip">
              <div className="ecd-brand">Daily Daydreams</div>
              <div className="ecd-navs">
                <span className="active" data-nav="shop">Shop</span>
                <span data-nav="stories">Stories</span>
                <span data-nav="about">About</span>
              </div>
              <div className="ecd-cart-btn" data-ref="cart">Bag &middot; <span className="ecd-badge" data-ref="cartCount">0</span></div>
            </div>
            <div className="ecd-home-scroll" data-ref="homeScroll">
              <div className="ecd-hero-grid">
                <div className="ecd-h-text">
                  <h2>Made slowly.<br/>Worn forever.</h2>
                  <p>A clean small catalogue of well-built things. Card, Apple Pay, Google Pay. Free worldwide shipping.</p>
                  <span className="ecd-cta" data-ref="heroCta">Shop the collection &rarr;</span>
                </div>
                <div className="ecd-h-img"></div>
              </div>
              <div className="ecd-grid-label">
                <h3>The collection</h3>
                <span className="ecd-filter">All &middot; New in &middot; Apparel &middot; Home</span>
              </div>
              <div className="ecd-products" data-ref="products-wrap">
                <div className="ecd-pcard" data-ref="product" data-name="The Everyday Tote" data-price="84"><div className="ecd-img"></div><div className="ecd-info"><div className="ecd-nm">The Everyday Tote</div><div className="ecd-pr">$84</div></div></div>
                <div className="ecd-pcard" data-ref="product" data-name="Linen Shirt No. 4" data-price="129"><div className="ecd-img"></div><div className="ecd-info"><div className="ecd-nm">Linen Shirt No. 4</div><div className="ecd-pr">$129</div></div></div>
                <div className="ecd-pcard" data-ref="product" data-name="Field Notes Set" data-price="56"><div className="ecd-img"></div><div className="ecd-info"><div className="ecd-nm">Field Notes Set</div><div className="ecd-pr">$56</div></div></div>
                <div className="ecd-pcard" data-ref="product" data-name="Brushed Jumper" data-price="98"><div className="ecd-img"></div><div className="ecd-info"><div className="ecd-nm">Brushed Jumper</div><div className="ecd-pr">$98</div></div></div>
                <div className="ecd-pcard" data-ref="product" data-name="Heavy Mug Set" data-price="42"><div className="ecd-img"></div><div className="ecd-info"><div className="ecd-nm">Heavy Mug Set</div><div className="ecd-pr">$42</div></div></div>
                <div className="ecd-pcard" data-ref="product" data-name="Wool Throw" data-price="186"><div className="ecd-img"></div><div className="ecd-info"><div className="ecd-nm">Wool Throw</div><div className="ecd-pr">$186</div></div></div>
              </div>
            </div>
          </div>

          {/* DETAIL VIEW */}
          <div className="ecd-view ecd-v-detail" data-ref="vDetail">
            <div className="ecd-top-strip">
              <div className="ecd-brand">Daily Daydreams</div>
              <div className="ecd-navs"><span data-nav="shop">Shop</span><span data-nav="stories">Stories</span><span data-nav="about">About</span></div>
              <div className="ecd-cart-btn">Bag &middot; <span className="ecd-badge" data-ref="cartCount2">0</span></div>
            </div>
            <span className="ecd-back">&larr; Back to shop</span>
            <div className="ecd-pd">
              <div className="ecd-pd-img"><span className="ecd-pd-badge">In stock</span></div>
              <div className="ecd-pd-info">
                <div className="ecd-cat" data-ref="pdCat">Apparel &middot; New in</div>
                <h2 data-ref="pdName">The Everyday Tote</h2>
                <div className="ecd-price" data-ref="pdPrice">$84</div>
                <p>Heavy canvas, brass hardware, double-stitched handles. Made in small batches. Ships within 48 hours.</p>
                <div className="ecd-sizes">Size</div>
                <div className="ecd-size-row">
                  <div className="ecd-sz">S</div>
                  <div className="ecd-sz on">M</div>
                  <div className="ecd-sz">L</div>
                </div>
                <div className="ecd-add-bag" data-ref="addBag">Add to bag &rarr;</div>
              </div>
            </div>
            <div className="ecd-pd-editorial">
              <div className="ecd-ed-block">
                <h4>The making</h4>
                <p>Cut and sewn in our Lisbon workshop. We use 14oz untreated canvas from a fourth-generation mill in northern Portugal. Each tote takes three hours from cut to finish, hand-checked before it ships.</p>
              </div>
              <div className="ecd-ed-block">
                <h4>Materials</h4>
                <ul>
                  <li>Body <b>14oz untreated canvas</b></li>
                  <li>Handles <b>Vegetable-tanned leather</b></li>
                  <li>Hardware <b>Solid brass, burnished</b></li>
                  <li>Lining <b>Unbleached cotton</b></li>
                </ul>
              </div>
              <div className="ecd-ed-block ecd-ed-quote">
                <p>"The tote has been in our line for six years, refined twice. We'll keep refining it."</p>
                <span className="ecd-author">Niamh &amp; Theo &middot; founders</span>
              </div>
              <div className="ecd-ed-block">
                <h4>Dimensions</h4>
                <ul>
                  <li>Width <b>38 cm</b></li>
                  <li>Height <b>42 cm</b></li>
                  <li>Depth <b>14 cm</b></li>
                  <li>Holds <b>16" laptop</b></li>
                </ul>
              </div>
              <div className="ecd-ed-block">
                <h4>Care</h4>
                <p>Wipe clean with a damp cloth. Avoid soaking. Leather darkens with use; brass softens to a warm patina. Both are part of the design.</p>
              </div>
            </div>
            <div className="ecd-pd-related">
              <h4>You may also like</h4>
              <div className="ecd-pd-related-grid">
                <div className="ecd-rcard"><div className="ecd-img"></div><div className="ecd-info"><div className="ecd-nm">Brushed Jumper</div><div className="ecd-pr">$98</div></div></div>
                <div className="ecd-rcard"><div className="ecd-img"></div><div className="ecd-info"><div className="ecd-nm">Heavy Mug Set</div><div className="ecd-pr">$42</div></div></div>
                <div className="ecd-rcard"><div className="ecd-img"></div><div className="ecd-info"><div className="ecd-nm">Wool Throw</div><div className="ecd-pr">$186</div></div></div>
              </div>
            </div>
            <div className="ecd-added-toast" data-ref="toast">Added to bag</div>
          </div>

          {/* CHECKOUT VIEW */}
          <div className="ecd-view ecd-v-checkout" data-ref="vCheckout">
            <div className="ecd-top-strip">
              <div className="ecd-brand">Daily Daydreams</div>
              <div className="ecd-navs"><span>Shop</span><span>Stories</span><span>About</span></div>
              <div className="ecd-cart-btn"><span style={{fontFamily:"'JetBrains Mono', monospace"}}>Checkout</span></div>
            </div>
            <h2 className="ecd-co-title">Your bag &middot; <span data-ref="coCount">2</span> items</h2>
            <div className="ecd-co-body">
              <div className="ecd-co-items">
                <div className="ecd-co-item"><div className="ecd-img ecd-p1"></div><div><div className="ecd-nm">The Everyday Tote</div><div className="ecd-sz">Size · M</div></div><div className="ecd-pr">$84</div></div>
                <div className="ecd-co-item"><div className="ecd-img ecd-p3"></div><div><div className="ecd-nm">Field Notes Set</div><div className="ecd-sz">One size</div></div><div className="ecd-pr">$56</div></div>
              </div>
              <div className="ecd-co-side">
                <div className="ecd-lab">Shipping to</div>
                <div className="ecd-addr"><span data-ref="addrText"></span><span className="ecd-caret"></span></div>
                <div className="ecd-row"><span>Subtotal</span><b className="ecd-num"><span data-ref="subtotal">$0</span></b></div>
                <div className="ecd-row"><span>Shipping</span><b className="ecd-num">$12.00</b></div>
                <div className="ecd-row"><span>VAT (5%)</span><b className="ecd-num"><span data-ref="vat">$0</span></b></div>
                <div className="ecd-total"><span>Total</span><span className="ecd-num" data-ref="total">$0</span></div>
                <div className="ecd-gateways">
                  <span className="ecd-gw" data-gw="card">Card</span>
                  <span className="ecd-gw" data-gw="apple">Apple Pay</span>
                  <span className="ecd-gw" data-gw="google">Google Pay</span>
                </div>
                <div className="ecd-pay" data-ref="pay"><span data-ref="payLbl">Continue to pay</span></div>
              </div>
            </div>
          </div>

          <div className="ecd-cursor" data-ref="cursor"></div>
        </div>
      </div>
      <div className="ecd-stack-caption"><span><b>3 projects</b></span><span className="ecd-live">Live</span></div>
    </div>
  );
}

function ProcessStrip({ steps }) {
  return (
    <div className="svc-process">
      {steps.map((s, i) => (
        <React.Fragment key={s.n}>
          <div className="proc">
            <div className="proc-n">Step {s.n}</div>
            <div className="proc-name">{s.name}</div>
            <div className="proc-meta">{s.meta}</div>
            <div className="proc-desc">{s.desc}</div>
          </div>
          {i < steps.length - 1 && <div className="proc-arrow" aria-hidden="true">→</div>}
        </React.Fragment>
      ))}
    </div>
  );
}

/* Count-up effect: animate from 0 to target on viewport entry, once.
   targetStr is the source string (e.g. "$1,800" or "$45,000").
   Returns a ref to attach to the .num span. Honors prefers-reduced-motion. */
function useCountUp(targetStr) {
  const ref = React.useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const match = String(targetStr).match(/^(\$?)([\d,]+)(.*)$/);
    if (!match) { el.textContent = targetStr; return; }
    const prefix = match[1] || "";
    const target = parseInt(match[2].replace(/,/g, ""), 10);
    const suffix = match[3] || "";
    if (reduced || !Number.isFinite(target)) { el.textContent = targetStr; return; }
    el.textContent = prefix + "0" + suffix;
    let started = false;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (!e.isIntersecting || started) return;
        started = true;
        io.unobserve(e.target);
        const start = performance.now();
        const dur = 700;
        const step = (now) => {
          const t = Math.min(1, (now - start) / dur);
          const eased = 1 - Math.pow(1 - t, 3);
          const v = Math.round(eased * target);
          el.textContent = prefix + v.toLocaleString() + suffix;
          if (t < 1) requestAnimationFrame(step);
        };
        requestAnimationFrame(step);
      });
    }, { threshold: 0.3 });
    io.observe(el);
    return () => io.disconnect();
  }, [targetStr]);
  return ref;
}

function Tier({ tier }) {
  const isHero = tier.role === "hero";
  const priceRef = useCountUp(tier.from);

  return (
    <article className={"tier-new role-" + tier.role + (isHero ? " hero" : "")} id={tier.id}>
      {isHero && <div className="tier-new-badge" style={{position:"absolute",top:"16px",right:"20px",left:"auto"}}>Most common</div>}

      <div className="tier-new-name">{tier.name}</div>

      <div className="tier-new-price">
        <span className="num" ref={priceRef}>{tier.from}</span>
        {tier.toCeiling && <span className="ceiling"> {tier.toCeiling}</span>}
      </div>

      <div className="tier-new-meta">
        <span className="from-lbl">From</span>
        <span className="dot">·</span>
        <span>{tier.timeline}</span>
      </div>

      {tier.step && <p className="tier-step">{tier.step}</p>}

      <p className="tier-new-blurb">{tier.who}</p>

      <div className="tier-new-incl">
        <div className="incl-h">What you get</div>
        <ul>
          {(tier.inclOutcome || tier.incl).map((i) => <li key={i}>{i}</li>)}
        </ul>
        {tier.inclOutcome && (
          <details className="tier-tech">
            <summary>See full technical scope</summary>
            <ul>
              {tier.incl.map((i) => <li key={i}>{i}</li>)}
            </ul>
          </details>
        )}
      </div>

      {tier.notIncl && (
        <div className="tier-new-not">
          <div className="not-h">Not included</div>
          <div className="not-list">{tier.notIncl}</div>
        </div>
      )}
    </article>
  );
}

function TierRail({ tiers }) {
  const heroIdx = tiers.findIndex((t) => t.role === "hero");
  const [sel, setSel] = useState(heroIdx < 0 ? 0 : heroIdx);
  const btnRefs = React.useRef([]);
  const pad2 = (n) => ("0" + n).slice(-2);
  const move = (e, i) => {
    let n = null;
    if (e.key === "ArrowDown" || e.key === "ArrowRight") n = (i + 1) % tiers.length;
    else if (e.key === "ArrowUp" || e.key === "ArrowLeft") n = (i - 1 + tiers.length) % tiers.length;
    else if (e.key === "Home") n = 0;
    else if (e.key === "End") n = tiers.length - 1;
    if (n !== null) {
      e.preventDefault();
      setSel(n);
      const b = btnRefs.current[n];
      if (b) b.focus();
    }
  };
  const t = tiers[sel];
  return (
    <div className="tr-split">
      <div className="tr-rail" role="tablist" aria-label="Product build tiers">
        {tiers.map((tt, i) => (
          <button
            key={tt.id}
            ref={(el) => (btnRefs.current[i] = el)}
            className="tr-item rv"
            style={{transitionDelay: (0.08 + i * 0.1) + "s"}}
            type="button"
            role="tab"
            id={"tab-" + tt.id}
            aria-controls={"pane-" + tt.id}
            aria-selected={i === sel}
            tabIndex={i === sel ? 0 : -1}
            onClick={() => setSel(i)}
            onKeyDown={(e) => move(e, i)}
          >
            <span className="tr-idx">
              {pad2(i + 1)}
              
            </span>
            <span className="tr-name">{tt.name}</span>
            <span className="tr-time">{tt.timeline}</span>
          </button>
        ))}
      </div>
      <div className="tr-panes rv" style={{transitionDelay: "0.34s"}}>
        <section
          key={t.id}
          className="tr-pane"
          role="tabpanel"
          id={"pane-" + t.id}
          aria-labelledby={"tab-" + t.id}
          tabIndex={0}
        >
          <div className="tr-scroll">
            <div className="tr-head">{pad2(sel + 1)} · {t.name}</div>
            {t.step && <p className="tr-step">{t.step}</p>}
            <p className="tr-who">{t.who}</p>
            <div className="tr-incl-h">What you get</div>
            <ul className="tr-incl">
              {(t.inclOutcome || t.incl).map((x) => <li key={x}>{x}</li>)}
            </ul>
            {t.inclOutcome && (
              <details className="tr-tech">
                <summary>Full technical scope</summary>
                <ul>{t.incl.map((x) => <li key={x}>{x}</li>)}</ul>
              </details>
            )}
            {t.notIncl && (
              <p className="tr-not"><span className="tr-not-lbl">Not included</span>{t.notIncl}</p>
            )}
            <a className="tr-cta" href="start-project.html">Start a project</a>
          </div>
        </section>
      </div>
    </div>
  );
}

function Service({ service, glassAnchor }) {
  const display = SERVICE_DISPLAY[service.id] || {};

  // Split the service name into two halves so the second half can be italic-serif.
  // Source: service.name verbatim. Just split on last space or hyphen.
  const splitIdx = Math.max(service.name.lastIndexOf(" "), service.name.lastIndexOf("-"));
  const namePrefix = splitIdx > 0 ? service.name.slice(0, splitIdx + 1) : "";
  const nameNoun = splitIdx > 0 ? service.name.slice(splitIdx + 1) + "." : service.name + ".";

  return (
    <section
      className="svc"
      id={service.id}
      {...(glassAnchor && { "data-glass-anchor": "" })}
    >
      <div className="bignum" aria-hidden="true">{service.n}</div>

      <div className="grid-asym">
        <div className="col-l">
          <div className="svc-eb rv" style={{transitionDelay:"0ms"}}>
            <span className="sq"></span>
            <span>A service <b>// {service.n} of 03</b></span>
          </div>

          <h2 className="svc-name-new rv" style={{transitionDelay:"100ms"}}>
            {namePrefix}<em>{nameNoun}</em>
          </h2>

          <p className="svc-tag rv" style={{transitionDelay:"220ms"}}>
            {firstSentence(service.scope)}
          </p>

          {display.slabText && (
            <span className="svc-slab rv" style={{transitionDelay:"320ms"}}>
              {display.slabText}
            </span>
          )}

          {display.statBand && (
            <div className="svc-stat-row rv" style={{transitionDelay:"420ms"}}>
              <div className="svc-stat">
                <div className="v">{display.statBand}</div>
                <div className="l">Price</div>
              </div>
              <div className="svc-stat">
                <div className="v">{display.statTime}</div>
                <div className="l">Build time</div>
              </div>
              <div className="svc-stat">
                <div className="v">{display.statThird}</div>
                <div className="l">{display.statThirdLbl}</div>
              </div>
            </div>
          )}

        </div>

        <div className="col-r">
          <div className="demo-slot rv" id={service.id + "-demo"} style={{transitionDelay:"300ms"}}>
            {service.id === "business-website" && <BusinessWebsiteDemo />}
            {service.id === "ecommerce" && <EcommerceDemo />}
            {service.id === "product-build" && <ProductBuildDemo />}
          </div>
          <p className="demo-disclaimer rv" style={{transitionDelay:"400ms"}}>
            Mockup only. The actual product I build for you would be designed for your brand, not this.
          </p>
        </div>
      </div>

      {service.process && <ProcessStrip steps={service.process} />}

      <div className="svc-tiers-section">
        <div className="svc-tiers-head rv" style={{transitionDelay:"100ms"}}>Choose your <em>scale</em></div>
        <TierRail tiers={service.tiers} />
      </div>

    </section>
  );
}

function HowItRuns() {
  const steps = ["Scope", "Weekly build on your phone", "AI-written, reviewed line by line", "Launch included", "Yours to run"];
  return (
    <section className="howrun" id="process">
      <div className="addons-inner">
        <div className="howrun-teaser rv">
          <div className="howrun-copy">
            <div className="addons-eyebrow">The process</div>
            <h2 className="addons-h">How a build runs.</h2>
            <p className="addons-sub">Every engagement follows the same cadence, app, website or store. Scope, a working build on your phone every week, AI written and reviewed line by line, launch included, everything handed over in your name.</p>
            <div className="howrun-steps">
              {steps.map((s, i) => (
                <span className="hr-chip" key={s}><b>{String(i + 1).padStart(2, "0")}</b> {s}</span>
              ))}
            </div>
          </div>
          <a className="howrun-link" href="how-i-work.html">See how every build runs <span aria-hidden="true">→</span></a>
        </div>
      </div>
    </section>
  );
}

/* AddOns() + Retainers() components parked 2026-07-22 · see ../removed-from-website/ */

function Closer() {
  return (
    <section className="closer" id="contact">
      <div className="closer-glow" aria-hidden="true">
        <span className="ember" /><span className="ember" /><span className="ember" /><span className="ember" /><span className="ember" />
      </div>
      <div className="closer-inner">
        <div className="closer-eyebrow"><span className="sq" /><span>§ Start a conversation</span></div>
        <h2>
          Send me a paragraph about <span className="accent">your project.</span>
        </h2>
        <p>
          I reply within 24 hours either way. If it's a fit, we book a call. If it isn't, I'll point you to someone who is.
        </p>
        <p className="closer-note">Every build is supported after launch, with ongoing care and feature work available as needed.</p>
        <a className="btn-primary" href="mailto:hello@sanketchitnis.com">
          Book a discovery call <span className="arr">→</span>
        </a>
      </div>
    </section>
  );
}

function Footer() {
  return (
    <footer className="ft">
      <span>Sanket Chitnis</span>
      <span>Building with AI</span>
    </footer>
  );
}

function App() {
  useEffect(() => {
    const els = document.querySelectorAll(".rv");
    if (!els.length) return;
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            e.target.classList.add("in");
            io.unobserve(e.target);
          }
        });
      },
      { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }
    );
    els.forEach((el) => io.observe(el));
    return () => io.disconnect();
  }, []);

  // Scroll engine — scrubs sky-dusk, sky-night, stars, and hero-copy
  // opacity/position as the user scrolls through the .day outer section.
  // Mirrors home-sunset's scroll engine verbatim (ramp thresholds preserved).
  useEffect(() => {
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduced) return;

    const day = document.getElementById("day-hero");
    if (!day) return;
    const dayStage = day.querySelector(".day-stage");
    const svcShell = document.querySelector(".svc-overview-shell");
    const svcOverview = document.getElementById("services-overview");

    let vh, dayTop, dayH;
    function measure() {
      vh = window.innerHeight;
      dayTop = day.offsetTop;
      dayH = day.offsetHeight;
    }
    measure();
    window.addEventListener("resize", measure);

    function clamp(x, a, b) { return Math.max(a, Math.min(b, x)); }
    function ramp(x, a, b) { return clamp((x - a) / (b - a), 0, 1); }

    let ticking = false;
    function onScroll() {
      if (!ticking) {
        ticking = true;
        requestAnimationFrame(update);
      }
    }
    function update() {
      ticking = false;
      const y = window.scrollY;
      const dp = clamp((y - dayTop) / (dayH - vh), 0, 1);

      const duskO  = ramp(dp, .15, .55);
      const nightO = ramp(dp, .50, .92);
      const starsO = ramp(dp, .65, .98);
      const copyO  = 1 - ramp(dp, .08, .30);
      const copyY  = (-ramp(dp, .08, .35) * 7) + "vh";

      dayStage.style.setProperty("--duskO", duskO);
      dayStage.style.setProperty("--nightO", nightO);
      dayStage.style.setProperty("--starsO", starsO);
      dayStage.style.setProperty("--copyO", copyO);
      dayStage.style.setProperty("--copyY", copyY);
      // toggle body.night past 50% scroll so nav text flips light over the dark sections below
      document.body.classList.toggle("night", dp > 0.5);
      // services-overview fades in over the night sky once cinematic is past its peak.
      // Threshold .80 = night nearly bloomed, stars just starting to twinkle.
      const overviewVisible = dp > 0.80;
      if (svcOverview) svcOverview.classList.toggle("in", overviewVisible);
      if (svcShell) svcShell.classList.toggle("in", overviewVisible);
    }

    window.addEventListener("scroll", onScroll, { passive: true });
    update();

    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", measure);
    };
  }, []);

  /* Living hero backdrop: golden-hour dust (locked design, mirrors home) */
  useEffect(() => {
    const stage = document.querySelector(".day-stage");
    const canvas = document.querySelector(".day-canvas");
    if (!stage || !canvas) return;
    const ctx = canvas.getContext("2d");
    let W = 0, H = 0, dpr = 1, t = 0, raf = null, visible = false;
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    let px = -1e4, py = -1e4, cx = -1e4, cy = -1e4, cin = 0, cwant = 0;
    const onMove = (e) => {
      const r = canvas.getBoundingClientRect();
      px = e.clientX - r.left; py = e.clientY - r.top; cwant = 1;
      if (cx < -9e3){ cx = px; cy = py; }
    };
    const onLeave = () => { cwant = 0; };
    window.addEventListener("pointermove", onMove, { passive:true });
    document.addEventListener("pointerleave", onLeave);

    const hash = (x, y) => { const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453; return n - Math.floor(n); };
    const sm = (v) => v * v * (3 - 2 * v);
    const noise = (x, y) => {
      const xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi;
      const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1), d = hash(xi + 1, yi + 1);
      const u = sm(xf), v = sm(yf);
      return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
    };
    const TAU = Math.PI * 2;
    const cream = (a) => "rgba(240,235,221," + a + ")";

    let dust = [];
    function seed(){
      dust = [];
      const n = Math.round(W * H / 2100);
      for (let i = 0; i < n; i++){
        const z = .35 + hash(i, 9) * .65;
        dust.push({ x:hash(i, 1) * W, y:hash(i, 2) * H, z:z,
                    a:(.10 + hash(i, 3) * .22) * z, r:(.9 + hash(i, 4) * 1.8) * z,
                    tw:hash(i, 5) * TAU, vx:0, vy:0 });
      }
    }
    function resize(){
      const r = stage.getBoundingClientRect();
      dpr = Math.min(window.devicePixelRatio || 1, 2);
      W = Math.max(1, Math.round(r.width)); H = Math.max(1, Math.round(r.height));
      canvas.width = W * dpr; canvas.height = H * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      seed();
    }
    function drawParticles(){
      ctx.fillStyle = cream(.38); ctx.fillRect(0, 0, W, H);
      for (let i = 0; i < dust.length; i++){
        const p = dust[i];
        let a = noise(p.x * .0016, p.y * .0016 + t * .05) * TAU * 2;
        p.vx += Math.cos(a) * .012 * p.z; p.vy += Math.sin(a) * .012 * p.z - .0035 * p.z;
        const dx = p.x - cx, dy = p.y - cy, d2 = dx * dx + dy * dy, R = 150;
        if (cin > 0 && d2 < R * R && d2 > .01){
          const f = (1 - Math.sqrt(d2) / R) * .16 * cin;
          p.vx += dx / Math.sqrt(d2) * f; p.vy += dy / Math.sqrt(d2) * f;
        }
        p.vx *= .96; p.vy *= .96;
        p.x += p.vx + .05 * p.z; p.y += p.vy;
        if (p.x > W + 8) p.x = -8; if (p.x < -8) p.x = W + 8;
        if (p.y > H + 8) p.y = -8; if (p.y < -8) p.y = H + 8;
        const tw = .72 + .28 * Math.sin(t * 1.4 + p.tw);
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r, 0, TAU);
        ctx.fillStyle = p.z > .62 ? "rgba(198,98,46," + (p.a * tw) + ")"
                                  : "rgba(106,90,68," + (p.a * tw) + ")";
        ctx.fill();
      }
    }
    function frame(){
      raf = null;
      t += .016;
      cin += (cwant - cin) * .04;
      cx += (px - cx) * .05; cy += (py - cy) * .05;
      if (!document.body.classList.contains("night")) drawParticles();
      if (visible) raf = requestAnimationFrame(frame);
    }
    const start = () => { if (!raf && visible && !reduced) raf = requestAnimationFrame(frame); };
    const stop = () => { if (raf){ cancelAnimationFrame(raf); raf = null; } };
    const onVis = () => { if (document.hidden) stop(); else start(); };

    resize();
    window.addEventListener("resize", resize);
    let io = null;
    if (reduced){
      visible = false;
      drawParticles();
    } else {
      io = new IntersectionObserver((es) => {
        visible = es[0].isIntersecting;
        if (visible) start(); else stop();
      });
      io.observe(stage);
      document.addEventListener("visibilitychange", onVis);
    }
    return () => {
      stop();
      if (io) io.disconnect();
      window.removeEventListener("resize", resize);
      window.removeEventListener("pointermove", onMove);
      document.removeEventListener("pointerleave", onLeave);
      document.removeEventListener("visibilitychange", onVis);
    };
  }, []);

  return (
    <>
      <Nav />
      <Hero />
      <section className="svc-overview-shell">
        <section className="services-overview" id="services-overview">
          <div className="overview-head">
            <div className="overview-eyebrow">The three builds</div>
            <h2 className="overview-h">Three ways to build what you have in mind.</h2>
          </div>
          <ServiceHub />
        </section>
      </section>
      <SectionNav />
      {SERVICES.map((s, i) => <Service service={s} key={s.id} glassAnchor={i === 0} />)}
      <HowItRuns />
      <Closer />
      <Footer />
    </>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
