/* eslint-disable */
const { useState: useSH3, useEffect: useEH3, useRef: useRH3 } = React;

const HERO_MAP_VIEW_KEYS = ["map", "agenda", "people"];

function getHeroMapViewKeys(vertical) {
  const configured = vertical && vertical.map && Array.isArray(vertical.map.views)
    ? vertical.map.views
    : HERO_MAP_VIEW_KEYS;
  const views = configured.filter((view) => HERO_MAP_VIEW_KEYS.includes(view));
  return views.length ? views : HERO_MAP_VIEW_KEYS;
}

function getHeroPhoneRowViewKeys(vertical, requestedViews) {
  if (Array.isArray(requestedViews) && requestedViews.length) return requestedViews;
  return ["ticket", ...getHeroMapViewKeys(vertical), "scan", "dash"];
}

function getHeroFeatureViewKeys(vertical, requestedViews) {
  const features = vertical && vertical.features ? vertical.features : {};
  const configured = Array.isArray(requestedViews) && requestedViews.length
    ? requestedViews
    : Array.isArray(vertical && vertical.featureViews)
      ? vertical.featureViews
      : Object.keys(features);
  return configured.filter((key) => features[key]);
}

function getHeroPreviewPageMeta(vertical, pageKey) {
  const scenes = (vertical.scenes || []).reduce((acc, scene) => {
    acc[scene.key] = scene;
    return acc;
  }, {});
  const map = vertical.map || {};
  const feature = vertical.features && vertical.features[pageKey];
  if (feature) {
    return {
      key: pageKey,
      eyebrow: feature.kicker,
      title: feature.appTitle,
      label: feature.appTitle,
      description: feature.description || feature.sub,
      bullets: feature.bullets || [],
    };
  }

  if (pageKey === "ticket") {
    const isPma = vertical.key === "philippineMarketingAssociation";
    const isTemplate = vertical.key === "template";
    const isBench = vertical.key === "bench";
    let description;
    let bullets;
    if (isTemplate) {
      description = "[Pass scene description — what does the prospect's main mobile pass do for the attendee?]";
      bullets = [
        "[Pass bullet 1 — one short line, max ~14 words.]",
        "[Pass bullet 2 — one short line, max ~14 words.]",
        "[Pass bullet 3 — one short line, max ~14 words.]",
      ];
    } else if (isPma) {
      description = "The Akademya learning home. Members resume Trannovation, AI for MSMEs, and Agora case-study courses, with CPM credit progress tracked on the member pass.";
      bullets = [
        "Course catalog, paths, and certificates of completion in one player.",
        "Sponsor-funded courses, Agora case studies, and the Trannovation bootcamp series.",
        "CPM credit packet and member-pass-linked certification trail.",
      ];
    } else if (isBench) {
      description = "The BENCH/ Active Pass is the guest's identity, credit ledger, partner consent, and live raffle weight. One QR at the door issues it; every station tap updates it.";
      bullets = [
        "QR registration + Active Pass in 30 seconds, no app install.",
        "Per-partner consent matrix lives on the pass.",
        "Same record survives the pilot, the public weekend, and the Fun Run.",
      ];
    } else {
      description = "The fan's entry and identity layer. It verifies the ticket, shows the branded pass, and becomes the starting point for rewards, queues, live updates, and sponsor actions.";
      bullets = [
        "Links ticket identity, QR entry, and attendee profile in one pass.",
        "Gives fans one saved place to launch the rest of the experience.",
        "Supports branded pass art and post-verification welcome messaging.",
      ];
    }
    return {
      key: pageKey,
      eyebrow: scenes.ticket && scenes.ticket.eyebrow,
      title: vertical.ticket.appHeadPass,
      label: vertical.ticket.appHeadPass,
      description,
      bullets,
    };
  }
  if (pageKey === "map") {
    const isBench = vertical.key === "bench";
    return {
      key: pageKey,
      eyebrow: scenes.map && scenes.map.eyebrow,
      title: map.headings.map || "Floor",
      label: map.tabs.map || "Map",
      description: isBench
        ? "The wellness-floor guide shows guests which stations are open, queue lengths at the Cold Plunge and Skillmill, and which partner booths are issuing credits right now."
        : "The floor guide helps fans find booths, stages, queues, sponsor quests, and exclusive drops without digging through social posts or static maps.",
      bullets: isBench
        ? [
            "Live station openness, queue length, and credit weight.",
            "Partner-booth highlights with current scan rate.",
            "Next content-station capture window per guest.",
          ]
        : [
            "Shows live points of interest and sponsor-highlighted destinations.",
            "Turns popular zones into measurable visits and route decisions.",
            "Can surface queue status, reward eligibility, and next-step actions.",
          ],
    };
  }
  if (pageKey === "agenda") {
    const isBench = vertical.key === "bench";
    return {
      key: pageKey,
      eyebrow: "PROGRAM VIEW",
      title: map.headings.agenda || "Schedule",
      label: map.tabs.agenda || "Schedule",
      description: isBench
        ? "The station schedule lays out doors, headline Cold Plunge session, Skillmill heats, the rower meter challenge, partner booth openings, and the cinematic raffle finale."
        : "The schedule view organizes stage programs, drops, contests, and activity windows so fans can plan their day and react to live changes.",
      bullets: isBench
        ? [
            "Each station has a call-time card on the pass.",
            "Cinematic raffle finale gets the 20-minute call-time push.",
            "Headline partner sessions surface push alerts for capacity.",
          ]
        : [
            "Highlights current and upcoming ToyCon moments by day and time.",
            "Keeps limited windows like drops, finals, and guest blocks visible.",
            "Creates a clear place for push alerts and capacity updates to land.",
          ],
    };
  }
  if (pageKey === "people") {
    const isBench = vertical.key === "bench";
    return {
      key: pageKey,
      eyebrow: "COMMUNITY VIEW",
      title: map.headings.people || "People",
      label: map.tabs.people || "People",
      description: isBench
        ? "The crew view groups every floor segment — guests, recreational athletes, ambassadors, sponsors, station partners, content crew, ops — so each gets the right pass and the right exports."
        : "The people view helps fans opt into fandom groups, find meetups, and connect around interests while keeping organizer-controlled context around the community layer.",
      bullets: isBench
        ? [
            "Eight audience segments, eight pass variants.",
            "Sponsor opt-in matrix per segment, set at registration.",
            "Content-crew has its own console for capture routing.",
          ]
        : [
            "Groups fans by collecting, cosplay, TCG, photography, family routes, and more.",
            "Turns social activity into opt-in profiles and measurable interest signals.",
            "Supports moderated meetup prompts, chat starts, and sponsor-relevant segments.",
          ],
    };
  }
  if (pageKey === "scan") {
    const isBench = vertical.key === "bench";
    return {
      key: pageKey,
      eyebrow: scenes.scan && scenes.scan.eyebrow,
      title: vertical.scan.screenTitle,
      label: vertical.scan.screenTitle,
      description: isBench
        ? "The scanner turns every Cold Plunge timer, Skillmill coach scan, Concept2 rower auto-read, partner-booth QR, and content capture into a tagged credit on the BENCH/ Active Pass."
        : "The scanner turns QR interactions into ToyCon stamps, sponsor visits, contest entries, queue joins, and rewards from one consistent flow.",
      bullets: isBench
        ? [
            "Six validation types, one scanner flow.",
            "Every credit tags the station, partner, and timestamp.",
            "Raffle weight updates the moment the scan validates.",
          ]
        : [
            "Fans scan booth or activity codes instead of learning separate mechanics.",
            "Each scan can record sponsor value, unlock progress, or trigger a reward.",
            "Organizers get a live activity trail instead of waiting for post-event reports.",
          ],
    };
  }
  if (pageKey === "dash") {
    const isBench = vertical.key === "bench";
    return {
      key: pageKey,
      eyebrow: scenes.dash && scenes.dash.eyebrow,
      title: vertical.dash.appTitle,
      label: vertical.dash.appTitle,
      description: isBench
        ? "The ops console gives BENCH/ live control over Active Pass issuance, credits per hour, raffle weight live, station heat, sponsor claim rate, the helpdesk queue, and the raffle-screen takeover."
        : "The ops dashboard gives organizers live control over adoption, scans, queues, heat zones, rewards, sponsor activity, and urgent show-floor decisions.",
      bullets: isBench
        ? [
            "Floor-camera feeds plus live tile per partner.",
            "Operator triggers the next raffle reveal from the console.",
            "Post-event PDF rolls up to BENCH/ + each sponsor automatically.",
          ]
        : [
            "Shows what is happening while ToyCon is still live.",
            "Helps staff react to crowd pressure, hot activations, and capacity issues.",
            "Packages sponsor outcomes into clear metrics for renewal conversations.",
          ],
    };
  }
  return {
    key: pageKey,
    eyebrow: "APP SCREEN",
    title: pageKey,
    label: pageKey,
    description: "A configurable app screen in the ToyCon platform preview.",
    bullets: [],
  };
}

const HERO_VERTICALS = {
  poaMidyear2025: {
    key: "poaMidyear2025",
    label: "POA Midyear",
    scenes: [
      { key: "ticket", eyebrow: "01 — Delegate pass", title: "Start with one verified pass for every delegate, faculty member, and partner.", body: "A mobile pass can connect registration, check-in, session access, certificate rules, partner permissions, and post-event follow-up without asking doctors to search through posts and forms." },
      { key: "map", eyebrow: "02 — Scientific guide", title: "Turn the trauma program into a live clinical learning guide.", body: "Delegates can plan HOTSPOT sessions, case presentations, sponsor visits, fellowship activities, and hotel logistics from one PWA guide built for the Dusit Thani Davao flow." },
      { key: "scan", eyebrow: "03 — Attendance proof", title: "Every scan can verify attendance, CPD proof, and partner value.", body: "QR scans can check delegates in, verify session attendance, route them to workshops, log sponsor interactions, and prepare clean exports after the convention." },
      { key: "dash", eyebrow: "04 — Convention control", title: "POA South Mindanao gets a live operating console.", body: "Registration pace, ballroom capacity, session scans, partner leads, helpdesk issues, alerts, and post-event reporting update while the convention is still running." },
    ],
    trio: {
      eyebrow: "02 — Scientific guide",
      title: "A live guide for sessions, faculty, partners, and fellowship moments.",
      body: "Delegates see which trauma sessions are next, where to go inside Dusit Thani, which partner booths are open, and what attendance actions still count toward post-event documentation.",
    },
    ticket: {
      appHeadPass: "Delegate Pass",
      appHeadRegister: "Register",
      email: "delegate@poa-midyear.ph",
      eventKicker: "XTAG STUDIO · POA CONCEPT",
      eventName: "33rd POA Mid-Year Convention",
      eventDate: "Apr 30-May 3 · Dusit Davao",
      cta: "Claim delegate pass",
      verifying: "Verifying registration...",
      foot: "Delegate record linked · session access ready",
      img: "/assets/poa-midyear-2025/reference/facebook-photo-122108384294781197.jpg",
      alt: "POA Mid-Year Convention profile reference",
      passLogoSrc: "/assets/poa-midyear-2025/reference/facebook-photo-122108384294781197.jpg",
      passLogoAlt: "POA Mid-Year Convention mark reference",
      registerBanner: "/assets/poa-midyear-2025/reference/orthophil-midyear-registration-poster.jpg",
      registerBannerAlt: "POA Mid-Year Convention registration poster reference",
      toastMark: "P",
      toastTitle: "POA Midyear",
      toastMsg: "Delegate pass ready",
    },
    map: {
      mapSrc: "/assets/poa-midyear-2025/reference/facebook-photo-122108384288781197.jpg",
      thumbsSrc: "/assets/session-thumbs.jpg",
      peopleSrc: "/assets/poa-midyear-2025/reference/facebook-photo-122134032194781197.jpg",
      mapAlt: "",
      views: ["map", "agenda", "people"],
      tabs: { map: "Venue", agenda: "Program", people: "Faculty" },
      headings: { map: "Venue", agenda: "Program", people: "Faculty" },
      mapSubheading: (visited, total) => `${visited}/${total} CHECKED`,
      agendaSubheading: "APR 30-MAY 3 · DAVAO",
      peopleSubheading: "8 GROUPS TO COORDINATE",
      venues: [
        { x: 30, y: 14, lbl: "Grand Ballroom" },
        { x: 12, y: 26, lbl: "Registration" },
        { x: 76, y: 22, lbl: "Faculty lounge" },
        { x: 80, y: 48, lbl: "Industry hall" },
        { x: 24, y: 70, lbl: "Case sessions" },
        { x: 60, y: 74, lbl: "Fellowship" },
      ],
      booths: [
        { x: 44, y: 38, lbl: "Trauma track" },
        { x: 62, y: 42, lbl: "Implant partner" },
        { x: 52, y: 58, lbl: "CPD desk" },
      ],
      youHere: "You · Dusit Thani lobby",
      cardIcon: "P",
      cardIconBg: "#183B63",
      cardTitle: "HOTSPOT trauma plenary",
      cardSub: "Grand Ballroom · session scan opens 8:45 AM",
      cardCta: "View session ->",
      totalVisited: 8,
      progressLabel: "Required scans",
      remainingLabel: "actions for certificate packet",
      claimReady: "Review CPD packet ->",
      claimIcon: "P",
      streamViews: "Delegate guide live",
      streamTitle: "Honing Our Technique in Solving Orthopedic Trauma",
      streamSub: "33rd POA Mid-Year Convention",
      days: [
        { day: "30", label: "APR · ARRIVAL" },
        { day: "01", label: "MAY · SCIENTIFIC" },
        { day: "03", label: "MAY · CLOSING" },
      ],
      sessions: [
        { time: "08:00", title: "Delegate check-in and badge pickup", loc: "Lobby", spk: "POA South Mindanao", tag: "OPEN", thumb: 0 },
        { time: "09:00", title: "HOTSPOT trauma plenary", loc: "Grand Ballroom", spk: "Faculty panel", thumb: 1 },
        { time: "10:45", title: "Complex fracture case discussions", loc: "Case room", spk: "Orthopedic faculty", thumb: 2 },
        { time: "13:30", title: "Industry partner learning block", loc: "Partner hall", spk: "Sponsors", thumb: 3 },
        { time: "15:00", title: "Sports and fellowship activities", loc: "Convention program", spk: "Delegates", thumb: 4 },
        { time: "18:30", title: "Top Gun fellowship night", loc: "Fellowship venue", spk: "Organizing committee", thumb: 5 },
      ],
      availability: "8 coordination groups",
      attending: "Delegates, faculty, fellows, guests, partners",
      people: [
        { name: "Dr. Remo-Tito Aguilar", role: "Overall Chair", company: "POA South Mindanao", pos: 0, mutual: 8, tag: "Chair", bio: "Coordinates the convention experience, faculty flow, delegate communication, and post-event continuity.", msgFrom: "Can we see live attendance?", msgReply: "Yes. Session and gate scans update in the admin view." },
        { name: "Faculty Panel", role: "Speaker group", company: "Trauma program", pos: 1, mutual: 6, tag: "Faculty", bio: "Needs session pages, disclosures, room reminders, and attendance counts that support clinical learning.", msgFrom: "Where is my next session?", msgReply: "The guide shows room, time, and scan window." },
        { name: "POA Delegate", role: "Orthopedic surgeon", company: "Regional chapter", pos: 2, mutual: 5, tag: "Delegate", bio: "Plans the scientific program, partner visits, fellowship activities, and certificate requirements.", msgFrom: "What scans are missing?", msgReply: "Your pass shows the remaining CPD actions." },
        { name: "Resident Fellow", role: "Trainee", company: "PBO pathway", pos: 3, mutual: 4, tag: "Fellow", bio: "Uses the guide to find case presentations, faculty talks, and networking moments around the convention.", msgFrom: "Can fellows get a track?", msgReply: "Yes. Access can be segmented by delegate type." },
        { name: "Industry Partner", role: "Sponsor team", company: "Orthopedic implant partner", pos: 4, mutual: 3, tag: "Partner", bio: "Needs compliant booth scans, qualified follow-up notes, and a report after the event.", msgFrom: "Can we export leads?", msgReply: "Yes. Scans and notes export after approval." },
        { name: "Registration Lead", role: "Ops team", company: "Zegen Management Group", pos: 5, mutual: 7, tag: "Ops", bio: "Runs helpdesk, reprints, walk-ins, badge issues, and attendance reconciliation.", msgFrom: "Can helpdesk update records?", msgReply: "Yes. Staff roles control what can be edited." },
        { name: "Chapter Officer", role: "Association admin", company: "POA", pos: 6, mutual: 4, tag: "Admin", bio: "Needs reliable exports for attendance, partner activity, sponsor proof, and future event planning.", msgFrom: "Can reports be chapter-ready?", msgReply: "Yes. Exports can be segmented." },
        { name: "Guest Participant", role: "Convention guest", company: "Davao program", pos: 7, mutual: 2, tag: "Guest", bio: "Receives the right schedule, access limits, and updates without needing the full member workflow.", msgFrom: "Where is fellowship night?", msgReply: "The guide can send the event card and alert." },
      ],
      socialCard: { icon: "P", title: "HOTSPOT faculty huddle", sub: "Grand Ballroom · 9:00 AM · live guide", cta: "Open" },
    },
    featureViews: ["registrationSystem", "badgeDesk", "sessionCredits", "partnerLeads", "fellowshipGuide", "postEventReports"],
    features: {
      registrationSystem: {
        type: "checkins",
        appTitle: "Register",
        kicker: "DELEGATE REGISTRATION",
        title: "One verified record for every attendee type",
        sub: "Delegates, faculty, fellows, guests, and partners enter through the same controlled registration layer.",
        description: "The platform can keep form submissions, payments or manual approvals, confirmations, mobile passes, badge rules, and staff notes tied to a single attendee record.",
        bullets: ["Delegate, faculty, fellow, guest, partner, and staff profiles.", "Confirmation email, QR pass, consent, and check-in state.", "Imports from existing registration forms or manual lists."],
        primary: "4/5",
        primaryLabel: "steps complete",
        progress: 80,
        tabs: ["DELEGATE", "FACULTY", "PARTNER"],
        rows: [
          { label: "Verify registration", meta: "email and mobile", status: "DONE" },
          { label: "Assign access type", meta: "delegate + CPD", status: "DONE" },
          { label: "Issue mobile pass", meta: "QR and helpdesk notes", status: "OPEN" },
          { label: "Scan onsite", meta: "badge pickup", status: "LOCKED" },
        ],
      },
      badgeDesk: {
        type: "checkins",
        appTitle: "Badges",
        kicker: "ONSITE DESK",
        title: "Check-in, badge pickup, and helpdesk in one queue",
        sub: "Scan a QR, search by name, correct a record, or reprint a credential without slowing the line.",
        description: "For a medical convention, the registration desk needs fast recovery paths for walk-ins, spelling corrections, partner badges, replacement passes, and staff overrides.",
        bullets: ["QR scan and name lookup.", "Badge templates by attendee type.", "Helpdesk notes and reprint logs."],
        primary: "1,248",
        primaryLabel: "checked in",
        progress: 62,
        tabs: ["SCAN", "LOOKUP", "REPRINT"],
        rows: [
          { label: "Delegate QR scan", meta: "verified", status: "DONE" },
          { label: "Partner badge", meta: "industry access", status: "OPEN" },
          { label: "Helpdesk correction", meta: "mobile number update", status: "REVIEW" },
        ],
      },
      sessionCredits: {
        type: "puzzle",
        appTitle: "CPD",
        kicker: "SESSION PROOF",
        title: "Session attendance becomes clean post-event documentation",
        sub: "Each session scan can feed CPD packets, certificates, internal reports, and attendance reconciliation.",
        description: "XTAG can log attendance at the ballroom, case discussions, partner learning blocks, and special sessions while keeping exports ready for the organizing committee.",
        bullets: ["Session scan windows and room rules.", "Attendance exports by delegate type.", "Certificate packet status per attendee."],
        primary: "5/8",
        primaryLabel: "required scans",
        progress: 63,
        code: ["H", "O", "T", "S", "P", "?", "T"],
        rows: ["Plenary scanned", "Case discussion scanned", "Evaluation pending"],
      },
      partnerLeads: {
        type: "market",
        appTitle: "Partners",
        kicker: "INDUSTRY VALUE",
        title: "Compliant partner lead capture",
        sub: "Industry partners can scan delegate badges, add notes, and receive controlled exports after the event.",
        description: "Sponsor value becomes easier to renew when partner interactions are logged with booth visits, session sponsorship, offer claims, and qualified follow-up notes.",
        bullets: ["Badge QR scanning for authorized partners.", "Lead qualification notes and product interest tags.", "Organizer-approved export workflow."],
        primary: "436",
        primaryLabel: "partner scans",
        rows: [
          { label: "Implant partner booth", meta: "182 scans", status: "LIVE" },
          { label: "Trauma workshop sponsor", meta: "94 scans", status: "LIVE" },
          { label: "Follow-up export", meta: "approval required", status: "LOCKED" },
        ],
      },
      fellowshipGuide: {
        type: "gallery",
        appTitle: "Guide",
        kicker: "DAVAO EXPERIENCE",
        title: "Scientific program plus fellowship moments",
        sub: "The app can hold the serious clinical program and still guide delegates through sports, dinners, hospitality, and local reminders.",
        description: "The convention post highlighted both clinical learning and camaraderie. A guide keeps those moments organized without reducing the scientific focus.",
        bullets: ["Program, fellowship, and hospitality cards.", "Push alerts for room changes and transportation notes.", "Photo or recap collection after the event."],
        primary: "12",
        primaryLabel: "guide cards",
        imageSrc: "/assets/poa-midyear-2025/reference/facebook-photo-122134037558781197.jpg",
        tags: ["Program", "Fellowship", "Davao"],
      },
      postEventReports: {
        type: "raffle",
        appTitle: "Reports",
        kicker: "POST-EVENT PACKAGE",
        title: "Reports ready before the event memory fades",
        sub: "Attendance, session heat, partner scans, feedback, and sponsor summaries can be packaged within days.",
        description: "Instead of rebuilding numbers from screenshots, sheets, and chat threads, POA can close the event with structured exports for the chapter, partners, and next organizing team.",
        bullets: ["Attendance and session exports.", "Sponsor proof and booth scan reports.", "Delegate feedback and operational lessons."],
        primary: "92%",
        primaryLabel: "report ready",
        progress: 92,
        progressLabel: "closeout package",
        cta: "Review reports",
        stats: [
          { value: "8", label: "exports" },
          { value: "4.8", label: "rating" },
        ],
        rows: ["Attendance summary", "Partner scan report", "Delegate feedback"],
      },
    },
    scan: {
      total: 8,
      startCount: 4,
      doneCount: 5,
      points: 1,
      pointsLabel: "SCAN",
      rankStart: "#42",
      rankDone: "#38",
      screenTitle: "POA Scanner",
      progressTitle: "Your convention record",
      scanTitle: "Scan session QR",
      scanSub: "Verify attendance, access, or partner visit",
      activityHeader: "Recent activity",
      activityScope: "Today",
      newActivity: { title: "HOTSPOT plenary · attendance verified", tag: "+1 SCAN" },
      activities: [
        { time: "3m ago", title: "Badge pickup · delegate verified", tag: "ENTRY" },
        { time: "14m ago", title: "Industry hall · partner visit logged", tag: "LEAD" },
        { time: "22m ago", title: "Case discussion room opened" },
        { time: "39m ago", title: "Guide alert · ballroom doors open" },
      ],
      cameraSrc: "/assets/poa-midyear-2025/reference/facebook-photo-122134032194781197.jpg",
      bakedCameraOverlay: false,
      frameTop: "55%",
      cameraTitle: "Scan POA code",
      recognized: "Code recognized · HOTSPOT plenary",
      success: "Attendance verified",
      toastIcon: "P",
      toastIconBg: "#183B63",
      toastTitle: "Attendance logged",
      toastSub: "HOTSPOT plenary · CPD packet updated",
      rewardIcon: "C",
      rewardIconBg: "#A33D2B",
      rewardTitle: "Certificate progress",
      rewardSub: "Requirement 5 of 8 completed",
      itemLabel: "actions",
    },
    dash: {
      appTitle: "POA Ops",
      headlineLabel: "Delegate activity · live",
      headlineTarget: 1840,
      headlineBase: 2400,
      headlineDecimals: 0,
      headlinePrefix: "",
      headlineSuffix: "",
      headlineDelta: "+312 scans last hour",
      headlineTargetText: "registration and session actions",
      secondaryTarget: 1840,
      kpis: [
        { label: "Checked in", value: "animated", detail: "delegates and guests" },
        { label: "Session proof", value: "78%", detail: "required scans complete" },
        { label: "Partner leads", value: "436", detail: "approved scans" },
        { label: "Feedback", value: "4.8", detail: "delegate pulse" },
      ],
      rowsTitle: "Top modules · live actions",
      rowsMeta: "ACTIONS",
      rows: [
        { lbl: "Registration desk", val: "1.2K", w: 92 },
        { lbl: "HOTSPOT plenary", val: "920", w: 78 },
        { lbl: "Partner hall", val: "436", w: 54 },
        { lbl: "Case discussions", val: "388", w: 46 },
        { lbl: "Fellowship guide", val: "310", w: 38 },
      ],
      claimsTitle: "Partner and session claims",
      claimsMeta: "COMPLETE",
      claims: [
        { lbl: "Badge pickup", val: "82%", w: 82 },
        { lbl: "CPD scan set", val: "78%", w: 78 },
        { lbl: "Partner booth visits", val: "54%", w: 54 },
        { lbl: "Feedback forms", val: "46%", w: 46 },
        { lbl: "Report exports", val: "92%", w: 92 },
      ],
      foot: "Closeout forecast · 92% report-ready · 4.8 / 5 delegate pulse",
      tablet: {
        lastUpdate: "Updated 8s ago",
        spark: {
          label: "Convention actions · last 90 minutes",
          points: [120,180,240,310,360,430,520,610,720,830,910,980,1040,1120,1190,1270,1360,1450,1530,1610,1690,1760,1810,1840],
          peakLabel: "1,840 actions · live",
        },
        gauge: { value: 28, max: 34, label: "Staff stations", sub: "Registration, ballroom, partner hall, helpdesk" },
        nps: { value: 4.8, max: 5, label: "Delegate pulse", sub: "Live quick ratings" },
        funnel: {
          title: "Delegate journey · today",
          stages: [
            { stage: "Registered", count: "1,520", pct: 100, w: 100 },
            { stage: "Checked in", count: "1,248", pct: 82, w: 82 },
            { stage: "Session scanned", count: "1,184", pct: 78, w: 78 },
            { stage: "Partner interaction", count: "436", pct: 29, w: 29 },
          ],
        },
        heat: {
          title: "Venue heat · Dusit Davao",
          hot: "Grand Ballroom",
          cols: ["REG","GB","CASE","IND","CPD","FEL"],
          rows: ["AM","NO","PM","EV"],
          cells: [
            0.80,0.92,0.54,0.40,0.50,0.26,
            0.46,0.88,0.70,0.64,0.56,0.34,
            0.38,0.72,0.82,0.78,0.62,0.44,
            0.28,0.42,0.36,0.40,0.30,0.86,
          ],
        },
        donut: { used: 1184, total: 1520, label: "Session scans", sub: "78% of registered delegates" },
        feed: {
          title: "Live activity",
          items: [
            { time: "12s", text: "HOTSPOT plenary · attendance verified", tag: "CPD" },
            { time: "24s", text: "Partner hall · badge scanned", tag: "LEAD" },
            { time: "48s", text: "Helpdesk · badge reprint resolved", tag: "OPS" },
            { time: "1m", text: "Case room · capacity at 84%", tag: "ROOM" },
            { time: "2m", text: "Feedback · session rating submitted", tag: "PULSE" },
            { time: "3m", text: "Fellowship alert · schedule card opened", tag: "GUIDE" },
          ],
        },
        alert: {
          title: "Case room · 84% capacity",
          sub: "Send overflow note before the next session block",
          cta: "Send alert",
        },
      },
    },
  },
  corporate: {
    key: "corporate",
    label: "Corporate",
    scenes: [
      { key: "ticket",  eyebrow: "01 — Your digital pass",   title: "It starts with a ticket in their pocket.",        body: "Branded full-bleed art with a scannable QR. Live updates pushed mid-event whenever a gate, time or seat changes." },
      { key: "map",     eyebrow: "02 — Inside the app",      title: "A live map of every booth and stage.",            body: "Sponsor booths, sessions, lounges and meeting rooms — all live. Tap any pin to see what's happening, who's there, and turn-by-turn directions." },
      { key: "scan",    eyebrow: "03 — Tap or scan",         title: "One scan checks them in everywhere.",             body: "Open the camera, scan the booth QR — visit logged, sponsor lead captured, leaderboard updates. The same flow at every gate, lounge and activation." },
      { key: "dash",    eyebrow: "04 — The control room",    title: "A live admin dashboard for the host.",            body: "Booth visits, attendee dwell time, sponsor leads, leaderboard rank — updating second-by-second. The same data your sponsors get post-event, in real time." },
    ],
    trio: {
      eyebrow: "02 — Inside the app",
      title: "A live map of every booth, agenda and attendee.",
      body: "Sponsor booths, sessions, lounges and meeting rooms — all live. Tap any pin to see what's happening, who's there, and turn-by-turn directions.",
    },
    ticket: {
      appHeadPass: "Pass",
      appHeadRegister: "Register",
      email: "alex.reyes@xtag.studio",
      eventKicker: "XTAG STUDIO · DEMO",
      eventName: "DevSummit 2026",
      eventDate: "Mar 14 · North Gate",
      cta: "Claim my pass",
      verifying: "Verifying invite…",
      foot: "Already linked · invite found in our system",
      img: "assets/devsummit-ticket.png",
      alt: "DevSummit 2026 event ticket",
      registerBanner: "assets/hero-verticals/register-banners/corporate-register-banner.webp",
      registerBannerAlt: "DevSummit 2026 conference banner",
      toastMark: "X",
      toastTitle: "DevSummit 2026",
      toastMsg: "Welcome",
    },
    map: {
      mapSrc: "assets/convention-map.jpg",
      thumbsSrc: "assets/session-thumbs.jpg",
      peopleSrc: "assets/people-grid.jpg",
      mapAlt: "",
      views: ["map", "agenda", "people"],
      tabs: { map: "Booths", agenda: "Agenda", people: "People" },
      headings: { map: "Booths", agenda: "Agenda", people: "People" },
      mapSubheading: (visited, total) => `${visited}/${total} VISITED`,
      agendaSubheading: "MAR 14 · DAY 1",
      peopleSubheading: "8 OPEN TO NETWORK",
      venues: [
        { x: 30, y: 12, lbl: "Main Stage"   },
        { x:  8, y: 17, lbl: "Cafe"         },
        { x: 82, y: 12, lbl: "Robotics Lab" },
        { x: 85, y: 30, lbl: "VR / AR"      },
        { x: 85, y: 46, lbl: "Workshops"    },
        { x: 78, y: 76, lbl: "Startup Row"  },
      ],
      booths: [
        { x: 26, y: 38, lbl: "Globe"  },
        { x: 55, y: 30, lbl: "Kumu"   },
        { x: 43, y: 35, lbl: "GCash"  },
      ],
      youHere: "You · Reception",
      cardIcon: "V",
      cardIconBg: "#0A1628",
      cardTitle: "Vercel · Booth A12",
      cardSub: "Live demo · 4 in queue · 28m walk",
      cardCta: "Get directions →",
      totalVisited: 12,
      progressLabel: "Booths visited",
      remainingLabel: "booths to go",
      claimReady: "Claim your prizes →",
      claimIcon: "🏆",
      streamViews: "1.2K watching",
      streamTitle: "Welcome & opening remarks",
      streamSub: "Main Stage · Liza Reyes",
      days: [
        { day: "14", label: "MAR · DAY 1" },
        { day: "15", label: "MAR · DAY 2" },
        { day: "16", label: "MAR · DAY 3" },
      ],
      sessions: [
        { time: "9:00",  title: "Welcome & opening remarks",      loc: "Main Stage",   spk: "Liza Reyes",        tag: "NOW", thumb: 0 },
        { time: "10:00", title: "Shipping faster than you spec",  loc: "Main Stage",   spk: "Marcus Dela Cruz",              thumb: 1 },
        { time: "11:30", title: "Building production AI agents",  loc: "Workshop A",   spk: "Lin Wei",                       thumb: 2 },
        { time: "13:00", title: "Lunch + sponsor expo",           loc: "Hall North",                                          thumb: 3 },
        { time: "14:30", title: "Scaling design systems",         loc: "Workshop B",   spk: "Anna Schmidt",                  thumb: 4 },
        { time: "16:00", title: "Founder fireside",               loc: "Main Stage",   spk: "Mei Tanaka",                    thumb: 5 },
      ],
      availability: "8 open to network",
      attending: "347 attending",
      people: [
        { name: "Carlos Mendoza",  role: "CEO",                 company: "GCash",      pos: 0, mutual: 4, tag: "AI",
          bio: "Building Southeast Asia's largest digital wallet. Previously led mobile payments at HSBC.",
          msgFrom: "Loved your AI-in-fintech keynote. See you at the after-party 🍻",
          msgReply: "Thanks! Catch you at Skyline Lounge tonight." },
        { name: "Maria Santos",    role: "Marketing Director",  company: "Globe",      pos: 1, mutual: 2, tag: "Marketing",
          bio: "Demand-gen lead at Globe Telecom. B2B narrative work + brand campaigns.",
          msgFrom: "Saw your panel — want to swap notes on event ROI?",
          msgReply: "Yes! Coffee tomorrow at 9?" },
        { name: "JP Dela Cruz",    role: "Founder",             company: "Kumu",       pos: 2, mutual: 6, tag: "Product",
          bio: "Building creator tools for the next billion. 0→1 obsessed, second-time founder.",
          msgFrom: "Your founder fireside was 🔥. Coffee tomorrow?",
          msgReply: "Sounds good. 9am at the Cafe?" },
        { name: "Liza Reyes",      role: "Senior Engineer",     company: "Vercel",     pos: 3, mutual: 1, tag: "AI",
          bio: "ML platform engineer at Vercel. Cares more about evals than models.",
          msgFrom: "Loved the welcome talk. Free to chat about AI infra?",
          msgReply: "Always! DM me a time." },
        { name: "Diego Ramos",     role: "Product Manager",     company: "PayMaya",    pos: 4, mutual: 3, tag: "Fintech",
          bio: "Product lead for merchant tools at PayMaya. Big fan of payment rails.",
          msgFrom: "Hi Diego! Want to compare notes on payment UX?",
          msgReply: "Sure — let's grab a beer at the after-party 🍺" },
        { name: "Anna Schmidt",    role: "Designer",            company: "Framer",     pos: 5, mutual: 2, tag: "Design",
          bio: "Senior designer at Framer. Obsessed with motion and small interaction details.",
          msgFrom: "Your design-systems talk was inspiring!",
          msgReply: "Thanks! Coffee tomorrow morning?" },
        { name: "Miguel Aquino",   role: "CTO",                 company: "Mynt",       pos: 6, mutual: 8, tag: "Security",
          bio: "CTO at Mynt. Building the secure payments layer for Southeast Asia.",
          msgFrom: "Curious about your security playbook for fintech.",
          msgReply: "Happy to chat — find me at booth A04 tomorrow." },
        { name: "Jasmine Pascual", role: "VP Marketing",        company: "Sun Life",   pos: 7, mutual: 1, tag: "Marketing",
          bio: "VP Marketing at Sun Life PH. Big believer in creative + data working together.",
          msgFrom: "Loved your panel today — story-driven brand 101.",
          msgReply: "Thanks! See you at the after-party tonight?" },
      ],
      socialCard: {
        icon: "🎉",
        title: "After-party tonight",
        sub: "Skyline Lounge · 8:00 PM · 142 going",
        cta: "RSVP",
      },
    },
    scan: {
      total: 12,
      startCount: 7,
      doneCount: 8,
      points: 50,
      pointsLabel: "XP",
      rankStart: "#18",
      rankDone: "#14",
      screenTitle: "Booth Scanner",
      progressTitle: "Your booth journey",
      scanTitle: "Scan booth QR",
      scanSub: "Tap any booth code to check in",
      activityHeader: "Recent activity",
      activityScope: "Today",
      newActivity: { title: "Booth B07 · Snowflake", tag: "+50 XP" },
      activities: [
        { time: "3m ago", title: "Booth A12 · Vercel · Live demo joined", tag: "+50 XP" },
        { time: "8m ago", title: "Booth A04 · Stripe", tag: "+50 XP" },
        { time: "12m ago", title: "Welcome keynote · joined" },
        { time: "22m ago", title: "Checked in · Reception" },
      ],
      cameraSrc: "assets/scan-pov.jpg",
      frameTop: "64%",
      cameraTitle: "Scan booth code",
      recognized: "Code recognized · Booth B07",
      success: "Booth visit logged",
      toastIcon: "S",
      toastIconBg: "#0A1628",
      toastTitle: "Visit logged · Snowflake B07",
      toastSub: "+1 booth · 8 / 12 visited today",
      rewardIcon: "★",
      rewardIconBg: "#18a957",
      rewardTitle: "+50 XP earned",
      rewardSub: "Leaderboard rank ↑ #18 → #14",
      itemLabel: "booths",
    },
    dash: {
      appTitle: "Admin",
      headlineLabel: "Pipeline value · live",
      headlineTarget: 82.4,
      headlineBase: 82.4,
      headlineDecimals: 1,
      headlinePrefix: "₱",
      headlineSuffix: "M",
      headlineDelta: "+₱10.7M last hour",
      headlineTargetText: "₱87M target",
      secondaryTarget: 4201,
      kpis: [
        { label: "Leads captured", value: "animated", detail: "86% qualified" },
        { label: "Conversion", value: "18%", detail: "impressions → leads" },
        { label: "Cost / lead", value: "₱690", detail: "−24% vs last event" },
        { label: "Engagement", value: "4.2×", detail: "sessions / attendee" },
      ],
      rowsTitle: "Top sponsors · pipeline value",
      rowsMeta: "₱ VALUE",
      rows: [
        { lbl: "Vercel · A12",      val: "₱18.1M", w: 92 },
        { lbl: "Stripe · A04",      val: "₱16.6M", w: 84 },
        { lbl: "Snowflake · B02",   val: "₱13.4M", w: 68 },
        { lbl: "Notion · B11",      val: "₱11.5M", w: 58 },
        { lbl: "Linear · C07",      val: "₱8.2M",  w: 42 },
      ],
      claimsTitle: "Goodie bag claims · live",
      claimsMeta: "CLAIMED",
      claims: [
        { lbl: "Event tee",      val: "78%", w: 78 },
        { lbl: "Sponsor tote",   val: "91%", w: 91 },
        { lbl: "Sticker pack",   val: "64%", w: 64 },
        { lbl: "Tumbler",        val: "52%", w: 52 },
        { lbl: "Notebook",       val: "70%", w: 70 },
      ],
      foot: "Sponsor renewal forecast · 92% intent · 4.6 / 5 NPS",
      tablet: {
        lastUpdate: "Updated 11s ago",
        spark: {
          label: "Pipeline · last 90 minutes",
          points: [42,46,51,49,55,58,54,62,65,68,64,70,73,71,74,78,76,80,82,81,80,82,83,82.4],
          peakLabel: "₱82.4M · 1:42 PM",
        },
        gauge: { value: 42, max: 50, label: "Staff on the floor", sub: "8 scanners idle · all gates green" },
        nps:   { value: 4.6, max: 5, label: "NPS · live", sub: "1,284 responses · +0.3 vs last event" },
        funnel: {
          title: "Attendee funnel · today",
          stages: [
            { stage: "Registered",        count: "5,820", pct: 100, w: 100 },
            { stage: "Checked in",        count: "4,621", pct: 79,  w: 79  },
            { stage: "3+ booth visits",   count: "3,180", pct: 55,  w: 55  },
            { stage: "Sponsor lead",      count: "1,042", pct: 18,  w: 18  },
          ],
        },
        heat: {
          title: "Hall A · zone heat",
          hot: "Booth A12 · Vercel",
          cols: ["A","B","C","D","E","F"],
          rows: ["1","2","3","4"],
          cells: [
            0.30,0.45,0.60,0.55,0.40,0.30,
            0.50,0.65,0.95,0.80,0.55,0.40,
            0.40,0.55,0.75,0.70,0.50,0.35,
            0.25,0.35,0.45,0.40,0.30,0.20,
          ],
        },
        donut: { used: 720, total: 920, label: "Session capacity", sub: "12 sessions full · 4 closing" },
        feed: {
          title: "Live activity",
          items: [
            { time: "11s",  text: "Booth A12 · Vercel — visit logged",     tag: "+1 LEAD" },
            { time: "32s",  text: "Linear · C07 — meeting set",            tag: "+1 MEET" },
            { time: "1m",   text: "Reward claimed — Tumbler",              tag: "REWARD" },
            { time: "1m",   text: "VIP arrived — M. Tan (Stripe)",         tag: "VIP"     },
            { time: "2m",   text: "Workshop B at 92% capacity",            tag: "OPS"     },
            { time: "2m",   text: "Snowflake · B02 — NPS submitted",       tag: "+1 NPS"  },
          ],
        },
        alert: {
          title: "Workshop B · 92% capacity",
          sub: "Open Hall C overflow — 18 seats",
          cta: "Open overflow",
        },
      },
    },
  },
  toycon: {
    key: "toycon",
    label: "ToyCon",
    scenes: [
      { key: "ticket", eyebrow: "01 — Fan passport", title: "It starts with a ToyCon passport in every fan's pocket.", body: "A PWA pass launched from QR, ticket email, lanyard, or social link gives fans one place for entry, quests, drops, schedules, and rewards." },
      { key: "map", eyebrow: "02 — Nexus guide", title: "A live map for every booth, stage, queue, and fandom.", body: "Fans find exclusive drops, TCG tables, cosplay stages, artist alleys, meet-and-greets, and sponsor booths without hunting through posts." },
      { key: "scan", eyebrow: "03 — Stamp and queue", title: "One scan turns booth visits into sponsor value.", body: "Every QR stamp can log a sponsor visit, unlock a reward, join a limited-drop queue, or submit a contest entry from the same phone flow." },
      { key: "dash", eyebrow: "04 — Show control", title: "A live command center for ToyCon organizers.", body: "Queue pressure, sponsor scans, passport completion, contest entries, hot zones, and push alerts update while the floor is still moving." },
    ],
    trio: {
      eyebrow: "02 — Nexus guide",
      title: "A live map for every booth, stage, queue, and fandom.",
      body: "Exclusive drops, TCG battlegrounds, artist alleys, cosplay zones, meet-and-greet lines, and sponsor quests stay current inside one fan-facing guide.",
    },
    ticket: {
      appHeadPass: "Fan Pass",
      appHeadRegister: "Register",
      email: "ari.santos@toyfan.ph",
      eventKicker: "XTAG STUDIO · TOYCON CONCEPT",
      eventName: "TOYCON Nexus 2026",
      eventDate: "Jun 12 · SMX Manila",
      cta: "Claim fan passport",
      verifying: "Verifying ticket…",
      foot: "Ticket linked · passport ready after verification",
      img: "/assets/toycon2026/toycon-pass.svg",
      alt: "TOYCON Nexus 2026 fan passport concept",
      passLogoSrc: "/assets/toycon2026/icons/toycon-x-icon.png",
      passLogoAlt: "TOYCON X logo",
      registerBanner: "/assets/toycon2026/toycon-register-banner.svg",
      registerBannerAlt: "TOYCON Nexus 2026 fan passport registration banner concept",
      toastMark: "N",
      toastTitle: "TOYCON Nexus",
      toastMsg: "Fan passport ready",
    },
    map: {
      mapSrc: "/assets/toycon2026/generated/toycon-convention-map.png",
      thumbsSrc: "/assets/session-thumbs.jpg",
      peopleSrc: "/assets/toycon2026/generated/toycon-fandom-people.png",
      mapAlt: "",
      views: ["map", "agenda", "people"],
      tabs: { map: "Floor", agenda: "Schedule", people: "Fandoms" },
      headings: { map: "Floor", agenda: "Schedule", people: "Fandoms" },
      mapSubheading: (visited, total) => `${visited}/${total} STAMPED`,
      agendaSubheading: "JUN 12 · DAY 1",
      peopleSubheading: "8 GROUPS OPEN",
      venues: [
        { x: 28, y: 13, lbl: "Main Stage" },
        { x: 10, y: 20, lbl: "Artist Alley" },
        { x: 74, y: 18, lbl: "Battlegrounds" },
        { x: 84, y: 36, lbl: "Cosplay Zone" },
        { x: 76, y: 64, lbl: "Meet & Greet" },
        { x: 30, y: 76, lbl: "Blind Boxes" },
      ],
      booths: [
        { x: 30, y: 40, lbl: "PopLife" },
        { x: 52, y: 33, lbl: "Toy Drops" },
        { x: 58, y: 57, lbl: "Sponsor Quest" },
      ],
      youHere: "You · SMX entrance",
      cardIcon: "T",
      cardIconBg: "#151b3f",
      cardTitle: "Blind Box Drop · Hall 2",
      cardSub: "18 slots left · queue opens at 1:30 PM",
      cardCta: "Join queue →",
      totalVisited: 10,
      progressLabel: "Quest stamps",
      remainingLabel: "stamps to unlock prize",
      claimReady: "Claim collector reward →",
      claimIcon: "★",
      streamViews: "4.8K watching",
      streamTitle: "Opening stage welcome",
      streamSub: "Main Stage · TOYCON Nexus",
      days: [
        { day: "12", label: "JUN · DAY 1" },
        { day: "13", label: "JUN · DAY 2" },
        { day: "14", label: "JUN · DAY 3" },
      ],
      sessions: [
        { time: "10:00", title: "Opening stage welcome",      loc: "Main Stage",     spk: "TOYCON Nexus", tag: "NOW", thumb: 0 },
        { time: "11:00", title: "Exclusive drop window",      loc: "Hall 2",         spk: "PopLife",                  thumb: 1 },
        { time: "12:30", title: "Toy photography contest",    loc: "Toy Click Zone",                                thumb: 2 },
        { time: "14:00", title: "Pokemon TCG pocket finals",  loc: "Battlegrounds",  spk: "MAD Events",              thumb: 3 },
        { time: "15:30", title: "Cosplay national finals",    loc: "Main Stage",                                     thumb: 4 },
        { time: "18:00", title: "Live performer block",       loc: "Main Stage",     spk: "Guest performers",        thumb: 5 },
      ],
      availability: "8 fandom groups open",
      attending: "70,000+ annual audience",
      people: [
        { name: "Mika Reyes", role: "Cosplayer", company: "Cosplay Zone", pos: 0, mutual: 4, tag: "Cosplay", bio: "Organizes photo meetups and contest reminders for cosplay groups.", msgFrom: "Photo wall opens after lunch?", msgReply: "Yes. Meet by the Cosplay Zone." },
        { name: "Gio Navarro", role: "Gamer", company: "Arcade Guild", pos: 1, mutual: 5, tag: "Gamer", bio: "Finds casual game stations, rhythm-game runs, and squad meetups between stage blocks.", msgFrom: "Any open gamer meetup?", msgReply: "Arcade Guild has two casual tables open." },
        { name: "Paolo Lim", role: "TCG Player", company: "Battlegrounds", pos: 2, mutual: 6, tag: "TCG", bio: "Competing in pocket tournaments and helping first-time players find learn-to-play tables.", msgFrom: "Any open TCG matches?", msgReply: "Two beginner tables are open now." },
        { name: "Ari Santos", role: "Toy Collector", company: "Art Toys", pos: 3, mutual: 5, tag: "Designer Toys", bio: "Hunting limited drops, local resin releases, and creator booths across the floor.", msgFrom: "Are you joining the blind box queue?", msgReply: "Yes. I reserved the 1:30 slot." },
        { name: "Jessa Cruz", role: "Toy Photographer", company: "Toy Click", pos: 4, mutual: 3, tag: "Photo", bio: "Builds diorama scenes and submits Toy Click contest entries from the show floor.", msgFrom: "Where is the contest QR?", msgReply: "At the Toy Click zone desk." },
        { name: "Lia Ong", role: "Toku Fan", company: "Toku Fans PH", pos: 5, mutual: 4, tag: "Tokusatsu", bio: "Coordinates meetups around stage guests, photo ops, and collector swaps.", msgFrom: "Meetup still at 3?", msgReply: "Yes, near the Main Stage left wing." },
        { name: "Ken Bautista", role: "Artist", company: "Designer Alley", pos: 6, mutual: 2, tag: "Artist", bio: "Drops prints, customs, and creator signing windows for collectors.", msgFrom: "Can I reserve a signing slot?", msgReply: "Scan the booth QR to join." },
        { name: "Rina Mercado", role: "Family Fan", company: "Kids Trail", pos: 7, mutual: 1, tag: "Family", bio: "Uses kid-friendly routes, food stops, and schedule alerts to plan the day.", msgFrom: "Any family-friendly quest?", msgReply: "Yes, start at the entrance kiosk." },
      ],
      socialCard: { icon: "★", title: "Cosplay photo meetup", sub: "Cosplay Zone · 3:00 PM · 246 going", cta: "Join" },
    },
    featureViews: ["registrationSystem", "hourlyRaffle", "dayCheckIns", "puzzleQuests", "photoGallery", "auctionMarketplace", "cosplayMeetups"],
    features: {
      registrationSystem: {
        type: "checkins",
        appTitle: "Tickets",
        kicker: "TICKETS + CHECK-IN",
        title: "Buy and claim your TOYCON pass",
        sub: "Pick a tier, pay, verify, scan in at the gate — one flow.",
        description: "Ticket sales and registration are the first interaction every fan has with ToyCon. The platform sells tiered tickets, applies promo codes, handles payment, verifies identity, and delivers a branded pass — then verifies the same pass at the on-site gate. Refunds, transfers, and revenue reporting all live in one console.",
        bullets: [
          "Multi-tier pricing — GA, VIP, day pass, group bulk.",
          "Promo codes, early-bird windows, and limited drops.",
          "Payment handoff to local processors (PayMongo, GCash, cards).",
          "Email and SMS verification with OTP resend.",
          "Branded ticket and digital pass delivered on success.",
          "Same QR drives on-site gate check-in with audit log.",
          "Live sales, attendance, and revenue dashboards.",
        ],
        primary: "3/4",
        primaryLabel: "steps complete",
        progress: 75,
        tabs: ["GA", "VIP", "GROUP"],
        rows: [
          { label: "Account details", meta: "name, email, mobile",     status: "DONE" },
          { label: "Pick tier",       meta: "GA · ₱650 · code SAVE10", status: "DONE" },
          { label: "Pay + verify",    meta: "OTP sent · 04:38",        status: "OPEN" },
          { label: "Gate check-in",   meta: "QR scanned at SMX",       status: "LOCKED" },
        ],
      },
      hourlyRaffle: {
        type: "raffle",
        appTitle: "Hourly Raffle",
        kicker: "SPONSORED DRAW",
        title: "Blind Box Hour",
        sub: "Next draw at 2:00 PM",
        description: "The raffle screen gives sponsors a timed reward mechanic that can refresh every hour, reward completed actions, and keep fans checking back without adding a separate app flow.",
        bullets: [
          "Converts sponsor QR scans and quest completion into eligible entries.",
          "Shows countdown, prize pool, and winner notification rules in one place.",
          "Creates repeatable engagement peaks throughout the event day.",
        ],
        primary: "12:18",
        primaryLabel: "until draw",
        progress: 72,
        progressLabel: "1,284 eligible entries",
        cta: "Enter draw",
        stats: [
          { value: "3", label: "draws left" },
          { value: "8", label: "prizes" },
        ],
        rows: ["Scan any sponsor QR", "Complete 3 ToyCon stamps", "Winners notified in app"],
      },
      dayCheckIns: {
        type: "checkins",
        appTitle: "Check-ins",
        kicker: "TIME WINDOWS",
        title: "Day 1 streak",
        sub: "Morning, afternoon, and closing-floor rewards keep fans moving.",
        description: "Time-of-day check-ins create scheduled engagement windows across the floor, helping ToyCon shape fan movement instead of only reacting to crowded zones.",
        bullets: [
          "Encourages fans to revisit zones at useful times of day.",
          "Can unlock streak rewards while preserving crowd-control windows.",
          "Gives ops a clean signal for attendance and movement patterns.",
        ],
        primary: "2/3",
        primaryLabel: "windows complete",
        progress: 66,
        tabs: ["AM", "PM", "EVE"],
        rows: [
          { label: "SMX entrance", meta: "10:04 AM", status: "DONE" },
          { label: "Hall 2 drop zone", meta: "1:00-3:00 PM", status: "OPEN" },
          { label: "Main Stage exit", meta: "after 5:30 PM", status: "LOCKED" },
        ],
      },
      puzzleQuests: {
        type: "puzzle",
        appTitle: "Puzzle Quest",
        kicker: "CLUE TRAIL",
        title: "Decode the Nexus",
        sub: "Fans solve clues across booths and unlock the final prize word.",
        description: "Puzzle quests turn booth visits into a coordinated scavenger layer, encouraging exploration while giving sponsors themed participation moments.",
        bullets: [
          "Fans collect clues from specific booths, stages, and activation zones.",
          "Hints and final-code entry keep the mechanic understandable on mobile.",
          "Completion data shows which routes and clues drove the most participation.",
        ],
        primary: "4/6",
        primaryLabel: "clues solved",
        progress: 67,
        code: ["T", "O", "Y", "?", "?", "?"],
        rows: ["Artist Alley clue found", "TCG table stamp active", "Hint unlocks in 08:00"],
      },
      photoGallery: {
        type: "gallery",
        appTitle: "Gallery",
        kicker: "FAN PHOTOS",
        title: "Toy Click wall",
        sub: "Moderated cosplay, toy photo, and booth moments become a live gallery.",
        description: "The gallery turns fan photos into a moderated event wall for cosplay, toy photography, sponsor booth moments, and post-event recap content.",
        bullets: [
          "Collects tagged submissions without sending fans to a separate channel.",
          "Supports moderation before photos are featured publicly.",
          "Creates reusable social proof for ToyCon, sponsors, and exhibitors.",
        ],
        primary: "248",
        primaryLabel: "approved posts",
        imageSrc: "/assets/session-thumbs.jpg",
        tags: ["Cosplay", "Toy Click", "Drops"],
      },
      auctionMarketplace: {
        type: "market",
        appTitle: "Auction Market",
        kicker: "LIVE AUCTIONS",
        title: "Creator auctions",
        sub: "Auction, buy-now, pickup lane, and watchlist all live beside the fan passport.",
        description: "The auction marketplace gives exhibitors and creators a controlled way to run limited auctions, buy-now drops, watchlists, and pickup flows inside the ToyCon platform.",
        bullets: [
          "Surfaces live lots, current bid, time left, and pickup status.",
          "Keeps high-demand creator drops attached to verified fan accounts.",
          "Can support auction, buy-now, or reservation mechanics per exhibitor.",
        ],
        primary: "18",
        primaryLabel: "live lots",
        rows: [
          { label: "Resin kaiju custom", meta: "P8,400 high bid", status: "04:21" },
          { label: "Signed art print", meta: "P2,100 high bid", status: "12:08" },
          { label: "Blind box bundle", meta: "Buy now P1,499", status: "PICKUP" },
        ],
      },
      cosplayMeetups: {
        type: "chat",
        appTitle: "Meet-ups",
        kicker: "GROUP CHAT",
        title: "Cosplay crews",
        sub: "Opt-in meetups with host pins, RSVP caps, and live group messages.",
        description: "Cosplay meetups with group chat let fans coordinate by fandom or crew while ToyCon keeps meetups discoverable, capped, and anchored to official locations.",
        bullets: [
          "Shows meetup time, location, RSVP count, and host guidance.",
          "Adds lightweight group chat for opted-in fans without exposing everyone.",
          "Helps staff guide photo-wall traffic and popular cosplay gatherings.",
        ],
        primary: "246",
        primaryLabel: "going at 3:00 PM",
        rows: [
          { label: "Tokusatsu group", meta: "Main Stage left wing", status: "OPEN" },
          { label: "Anime heroes", meta: "Photo wall B", status: "FULL" },
        ],
        messages: [
          { who: "Host", text: "Line up by series color at 2:45." },
          { who: "Mika", text: "Bringing spare prop tape." },
        ],
      },
    },
    scan: {
      total: 10,
      startCount: 4,
      doneCount: 5,
      points: 100,
      pointsLabel: "PTS",
      rankStart: "#42",
      rankDone: "#31",
      screenTitle: "Quest Scanner",
      progressTitle: "Your Nexus quest",
      scanTitle: "Scan booth QR",
      scanSub: "Collect stamps, queues, and contest entries",
      activityHeader: "Recent quest activity",
      activityScope: "Day 1",
      newActivity: { title: "Blind Box Drop · Hall 2", tag: "+100 PTS" },
      activities: [
        { time: "2m ago", title: "PopLife booth · stamp collected", tag: "+100 PTS" },
        { time: "11m ago", title: "Toy Click contest · entry saved", tag: "ENTRY" },
        { time: "18m ago", title: "Battlegrounds · table found" },
        { time: "36m ago", title: "Checked in · SMX entrance" },
      ],
      cameraSrc: "/assets/toycon2026/generated/toycon-qr-cosplay-scan.png",
      bakedCameraOverlay: true,
      frameTop: "56%",
      cameraTitle: "Scan ToyCon code",
      recognized: "Code recognized · Blind Box Drop",
      success: "Quest stamp logged",
      toastIcon: "T",
      toastIconBg: "#151b3f",
      toastTitle: "Stamp logged · Blind Box Drop",
      toastSub: "+1 quest · 5 / 10 completed today",
      rewardIcon: "★",
      rewardIconBg: "#ff2f7d",
      rewardTitle: "+100 points earned",
      rewardSub: "Collector rank ↑ #42 → #31",
      itemLabel: "stamps",
    },
    dash: {
      appTitle: "Nexus Ops",
      headlineLabel: "Sponsor engagement · live",
      headlineTarget: 28400,
      headlineBase: 70000,
      headlineDecimals: 0,
      headlinePrefix: "",
      headlineSuffix: "",
      headlineDelta: "+4,120 scans last hour",
      headlineTargetText: "70,000+ audience target",
      secondaryTarget: 28400,
      kpis: [
        { label: "Passport scans", value: "animated", detail: "68% verified fans" },
        { label: "Quest completion", value: "34%", detail: "stamps → reward-ready" },
        { label: "Avg queue", value: "7m", detail: "−31% vs unmanaged lines" },
        { label: "Engagement", value: "5.2×", detail: "actions / fan" },
      ],
      rowsTitle: "Top activations · live scans",
      rowsMeta: "SCANS",
      rows: [
        { lbl: "Blind Box Drop", val: "5.4K", w: 92 },
        { lbl: "PopLife Quest", val: "4.8K", w: 82 },
        { lbl: "Toy Click Zone", val: "3.9K", w: 70 },
        { lbl: "Battlegrounds", val: "3.2K", w: 58 },
        { lbl: "Cosplay Stage", val: "2.6K", w: 46 },
      ],
      claimsTitle: "Reward claims · live",
      claimsMeta: "CLAIMED",
      claims: [
        { lbl: "Collector badge", val: "76%", w: 76 },
        { lbl: "Sponsor voucher", val: "69%", w: 69 },
        { lbl: "Fast lane perk", val: "42%", w: 42 },
        { lbl: "Photo print", val: "58%", w: 58 },
        { lbl: "Raffle entry", val: "88%", w: 88 },
      ],
      foot: "Sponsor report forecast · 74% renewal intent · 4.7 / 5 fan rating",
      tablet: {
        lastUpdate: "Updated 9s ago",
        spark: {
          label: "Passport actions · last 90 minutes",
          points: [220,380,620,880,1120,1460,1820,2240,2780,3260,3880,4420,4980,5560,6120,6900,7520,8240,9100,9840,10500,11180,11840,12420],
          peakLabel: "+4,120 scans last hour",
        },
        gauge: { value: 42, max: 52, label: "Crew stations live", sub: "10 queue lanes covered · 3 hot zones" },
        nps:   { value: 4.7, max: 5, label: "Fan rating", sub: "2,840 quick pulses · day 1 live" },
        funnel: {
          title: "Fan journey · today",
          stages: [
            { stage: "Passport opened", count: "18,420", pct: 100, w: 100 },
            { stage: "First stamp",     count: "14,780", pct: 80,  w: 80  },
            { stage: "3+ activations",  count: "8,940",  pct: 49,  w: 49  },
            { stage: "Sponsor lead",    count: "3,860",  pct: 21,  w: 21  },
          ],
        },
        heat: {
          title: "SMX floor · zone heat",
          hot: "Blind Box Drop",
          cols: ["A","B","C","D","E","F"],
          rows: ["1","2","3","4"],
          cells: [
            0.35,0.48,0.72,0.62,0.46,0.30,
            0.58,0.80,0.95,0.88,0.60,0.42,
            0.44,0.68,0.86,0.74,0.55,0.36,
            0.30,0.44,0.58,0.62,0.48,0.32,
          ],
        },
        donut: { used: 18420, total: 70000, label: "Passport adoption", sub: "26.3% of 70k target" },
        feed: {
          title: "Live activity",
          items: [
            { time: "9s",   text: "Blind Box Drop · queue joined",      tag: "QUEUE" },
            { time: "24s",  text: "PopLife Quest · stamp logged",       tag: "+1 STAMP" },
            { time: "38s",  text: "Toy Click · contest entry submitted", tag: "ENTRY" },
            { time: "1m",   text: "Hall 2 queue · 7 minute wait",        tag: "OPS" },
            { time: "1m",   text: "Cosplay Stage · capacity at 88%",     tag: "ALERT" },
            { time: "2m",   text: "Sponsor voucher · claimed",          tag: "REWARD" },
          ],
        },
        alert: {
          title: "Cosplay Stage · 88% capacity",
          sub: "Open overflow view and push route update",
          cta: "Send update",
        },
      },
    },
  },
  sports: {
    key: "sports",
    label: "Sports",
    scenes: [
      { key: "ticket", eyebrow: "01 — Race pass", title: "It starts with a race pass in their pocket.", body: "Bib pickup, corral access, finish-line perks and emergency updates stay inside one branded mobile pass." },
      { key: "map", eyebrow: "02 — Race village", title: "A live map of every corral, station and activity.", body: "Start pens, hydration stops, recovery zones and sponsor tents stay current so runners know exactly where to go next." },
      { key: "scan", eyebrow: "03 — Tap or scan", title: "One scan checks runners in across race day.", body: "Scan bib pickup, medal engraving, recovery stations or partner booths — progress, rewards and ops data update instantly." },
      { key: "dash", eyebrow: "04 — Race control", title: "A live dashboard for race-day operations.", body: "Runner flow, activity scans, reward claims and partner performance update while the race village is still moving." },
    ],
    trio: {
      eyebrow: "02 — Race village",
      title: "A live map of every corral, stop and runner.",
      body: "Start pens, hydration stops, recovery zones and partner tents stay live. Tap any pin to see what is open, how busy it is and where to go next.",
    },
    ticket: {
      appHeadPass: "Race Pass",
      appHeadRegister: "Register",
      email: "mika.santos@runclub.ph",
      eventKicker: "XTAG STUDIO · RACE DEMO",
      eventName: "Run Manila 2026",
      eventDate: "Jun 21 · Start Corral",
      cta: "Claim my race pass",
      verifying: "Verifying entry…",
      foot: "Bib linked · entry found in our system",
      img: "assets/hero-verticals/sports-ticket.png",
      alt: "Run Manila 2026 race pass art",
      registerBanner: "assets/hero-verticals/register-banners/sports-register-banner.webp",
      registerBannerAlt: "Run Manila 2026 race village banner",
      toastMark: "R",
      toastTitle: "Run Manila 2026",
      toastMsg: "Race pass ready",
    },
    map: {
      mapSrc: "assets/hero-verticals/sports-map.jpg",
      thumbsSrc: "assets/hero-verticals/sports-thumbs.jpg",
      peopleSrc: "assets/hero-verticals/sports-people.jpg",
      mapAlt: "",
      views: ["map", "agenda", "people"],
      tabs: { map: "Stops", agenda: "Race Day", people: "Runners" },
      headings: { map: "Stops", agenda: "Race Day", people: "Runners" },
      mapSubheading: (visited, total) => `${visited}/${total} CHECKED`,
      agendaSubheading: "JUN 21 · RACE DAY",
      peopleSubheading: "8 OPEN TO RUN",
      venues: [
        { x: 18, y: 18, lbl: "Start Corral" },
        { x: 42, y: 16, lbl: "Gear Check" },
        { x: 72, y: 26, lbl: "Hydration" },
        { x: 28, y: 58, lbl: "First Aid" },
        { x: 62, y: 64, lbl: "Recovery" },
        { x: 78, y: 82, lbl: "Finish Line" },
      ],
      booths: [
        { x: 34, y: 42, lbl: "Bib Pickup" },
        { x: 56, y: 48, lbl: "Medals" },
        { x: 70, y: 38, lbl: "Merch" },
      ],
      youHere: "You · Start Corral",
      cardIcon: "M",
      cardIconBg: "#0A1628",
      cardTitle: "Medal Tent · Finish Zone",
      cardSub: "Photo queue · 18 runners · 4m walk",
      cardCta: "Get route →",
      totalVisited: 8,
      progressLabel: "Stops checked",
      remainingLabel: "stops to go",
      claimReady: "Claim finisher perks →",
      claimIcon: "✓",
      streamViews: "3.8K watching",
      streamTitle: "Start-line warmup",
      streamSub: "Main Corral · Coach Lia",
      days: [
        { day: "21", label: "JUN · RACE" },
        { day: "22", label: "JUN · RECOVER" },
        { day: "23", label: "JUN · CLUBS" },
      ],
      sessions: [
        { time: "5:30", title: "Start-line warmup",         loc: "Main Corral", spk: "Coach Lia", tag: "NOW", thumb: 0 },
        { time: "6:00", title: "10K wave release",          loc: "Start Arch",  spk: "Race Crew",             thumb: 1 },
        { time: "6:45", title: "Hydration challenge",       loc: "Station 2",                              thumb: 2 },
        { time: "7:30", title: "Finish-line photos",        loc: "Photo Wall",                             thumb: 3 },
        { time: "8:15", title: "Recovery stretch clinic",   loc: "Recovery Tent", spk: "Maya Cruz",        thumb: 4 },
        { time: "9:00", title: "Medal ceremony",            loc: "Finish Stage",  spk: "Run Manila",       thumb: 5 },
      ],
      availability: "8 runners nearby",
      attending: "6,840 registered",
      people: [
        { name: "Mika Santos", role: "10K Runner", company: "Run Club MNL", pos: 0, mutual: 5, tag: "Runner", bio: "Training for her first sub-50 10K and organizing weekend shakeout runs.", msgFrom: "Saw your pace group. Want to warm up together?", msgReply: "Yes, meet by Corral B at 5:20." },
        { name: "Paolo Reyes", role: "Pacer", company: "Makati Striders", pos: 1, mutual: 3, tag: "Coach", bio: "Volunteer pacer for 55-minute 10K groups and community run mentor.", msgFrom: "Can I join your pace group?", msgReply: "Of course. Look for the blue flag." },
        { name: "Ari Kim", role: "Physio", company: "Recovery Lab", pos: 2, mutual: 2, tag: "Wellness", bio: "Sports therapist running the recovery tent and post-race mobility sessions.", msgFrom: "Do you have slots after the race?", msgReply: "Yes, scan at the recovery tent." },
        { name: "Luis Tan", role: "Volunteer Lead", company: "Hydration Crew", pos: 3, mutual: 7, tag: "Crew", bio: "Coordinates hydration station volunteers and route safety radio checks.", msgFrom: "Station 2 needs extra cups?", msgReply: "Covered. Bring ice to Station 3." },
        { name: "Kara Lim", role: "Creator", company: "Miles Ahead", pos: 4, mutual: 4, tag: "Creator", bio: "Documents local races and runner stories across Southeast Asia.", msgFrom: "Loved your route preview.", msgReply: "Thanks! See you near the finish arch." },
        { name: "Nico Cruz", role: "Half Marathoner", company: "Quezon City Crew", pos: 5, mutual: 6, tag: "Runner", bio: "Weekend long-run regular chasing a relaxed race-day negative split.", msgFrom: "Coffee after the medal ceremony?", msgReply: "Yes. Recovery brunch after 9." },
        { name: "Rhea Ong", role: "Brand Lead", company: "Finish Village", pos: 6, mutual: 1, tag: "Partner", bio: "Builds community activations for runners, coaches and recovery partners.", msgFrom: "Your booth has a long line.", msgReply: "We just opened a second scanner." },
        { name: "Jon Dela Vega", role: "Race Marshal", company: "Route Safety", pos: 7, mutual: 2, tag: "Crew", bio: "Race marshal focused on flow, safety, signage and runner support.", msgFrom: "Corral C is ready?", msgReply: "Ready. Send wave two in five." },
      ],
      socialCard: { icon: "✓", title: "Recovery brunch", sub: "Finish Village · 9:30 AM · 326 going", cta: "Join" },
    },
    scan: {
      total: 8,
      startCount: 4,
      doneCount: 5,
      points: 75,
      pointsLabel: "PTS",
      rankStart: "#26",
      rankDone: "#19",
      screenTitle: "Race Scanner",
      progressTitle: "Your race village run",
      scanTitle: "Scan activity QR",
      scanSub: "Check in at any race stop",
      activityHeader: "Recent scans",
      activityScope: "Race day",
      newActivity: { title: "Medal Tent · Finish Zone", tag: "+75 PTS" },
      activities: [
        { time: "4m ago", title: "Recovery Zone · Stretch clinic joined", tag: "+75 PTS" },
        { time: "9m ago", title: "Hydration Station 2", tag: "+75 PTS" },
        { time: "34m ago", title: "10K wave release · joined" },
        { time: "1h ago", title: "Bib pickup · Start Corral" },
      ],
      cameraSrc: "assets/hero-verticals/sports-scan.jpg",
      frameTop: "50%",
      cameraTitle: "Scan race code",
      recognized: "Code recognized · Medal Tent",
      success: "Race stop logged",
      toastIcon: "M",
      toastIconBg: "#0A1628",
      toastTitle: "Visit logged · Medal Tent",
      toastSub: "+1 stop · 5 / 8 completed today",
      rewardIcon: "★",
      rewardIconBg: "#18a957",
      rewardTitle: "+75 points earned",
      rewardSub: "Leaderboard rank ↑ #26 → #19",
      itemLabel: "stops",
    },
    dash: {
      appTitle: "Race Ops",
      headlineLabel: "Race village activity · live",
      headlineTarget: 6840,
      headlineBase: 7000,
      headlineDecimals: 0,
      headlinePrefix: "",
      headlineSuffix: "",
      headlineDelta: "+1,126 scans last hour",
      headlineTargetText: "7,000 race-day target",
      secondaryTarget: 6840,
      kpis: [
        { label: "Runner check-ins", value: "animated", detail: "94% verified" },
        { label: "Bib pickup", value: "96%", detail: "entries → claimed" },
        { label: "Avg queue", value: "4m", detail: "−38% vs last race" },
        { label: "Engagement", value: "3.9×", detail: "stops / runner" },
      ],
      rowsTitle: "Top zones · live scans",
      rowsMeta: "SCANS",
      rows: [
        { lbl: "Bib Pickup", val: "1.8K", w: 92 },
        { lbl: "Medal Tent", val: "1.5K", w: 80 },
        { lbl: "Hydration", val: "1.2K", w: 66 },
        { lbl: "Recovery", val: "914", w: 52 },
        { lbl: "Photo Wall", val: "742", w: 42 },
      ],
      claimsTitle: "Reward claims · live",
      claimsMeta: "CLAIMED",
      claims: [
        { lbl: "Finisher photo", val: "84%", w: 84 },
        { lbl: "Medal engrave", val: "71%", w: 71 },
        { lbl: "Recovery pass", val: "63%", w: 63 },
        { lbl: "Race tee", val: "89%", w: 89 },
        { lbl: "Partner perk", val: "58%", w: 58 },
      ],
      foot: "Volunteer coverage · 97% staffed · 4.7 / 5 runner rating",
      tablet: {
        lastUpdate: "Updated 8s ago",
        spark: {
          label: "Race-village scans · last 60 minutes",
          points: [120,180,260,340,420,560,720,940,1180,1420,1640,1820,1960,2120,2240,2380,2520,2660,2780,2900,3020,3120,3220,3320],
          peakLabel: "+1,126 last hour",
        },
        gauge: { value: 38, max: 48, label: "Marshals on duty", sub: "10 stations covered · 2 on break" },
        nps:   { value: 4.7, max: 5, label: "Runner rating", sub: "2,140 responses · finish line live" },
        funnel: {
          title: "Race-day funnel",
          stages: [
            { stage: "Bib pickup",      count: "6,720", pct: 96, w: 96 },
            { stage: "Corral check-in", count: "6,210", pct: 89, w: 89 },
            { stage: "Finish crossed",  count: "5,408", pct: 77, w: 77 },
            { stage: "Medal engraved",  count: "3,860", pct: 55, w: 55 },
          ],
        },
        heat: {
          title: "Race village · zone heat",
          hot: "Bib Pickup tent",
          cols: ["S1","S2","S3","S4","S5","S6"],
          rows: ["N","C","S","E"],
          cells: [
            0.35,0.55,0.80,0.65,0.45,0.30,
            0.60,0.85,0.95,0.75,0.50,0.40,
            0.55,0.70,0.80,0.65,0.45,0.35,
            0.30,0.45,0.55,0.50,0.35,0.25,
          ],
        },
        donut: { used: 6840, total: 7000, label: "Race capacity", sub: "97.7% bib utilization" },
        feed: {
          title: "Live activity",
          items: [
            { time: "8s",   text: "Snowflake · Recovery — scan",          tag: "+1 SCAN" },
            { time: "26s",  text: "Bib A4032 — finish line crossed",      tag: "FINISH" },
            { time: "44s",  text: "Race tee claimed — size M",            tag: "REWARD" },
            { time: "1m",   text: "Corral C — start gun fired",           tag: "OPS"     },
            { time: "1m",   text: "Hydration · S3 — restock requested",   tag: "ALERT"   },
            { time: "2m",   text: "Photo wall · 712 captures uploaded",   tag: "MEDIA"   },
          ],
        },
        alert: {
          title: "Hydration S3 · restock now",
          sub: "Volunteers en route · 6m ETA",
          cta: "Acknowledge",
        },
      },
    },
  },
  tourism: {
    key: "tourism",
    label: "Tourism",
    scenes: [
      { key: "ticket", eyebrow: "01 — Visitor pass", title: "It starts with a destination pass in their pocket.", body: "Travelers get a city pass with landmark access, itinerary updates and local offers they can unlock as they explore." },
      { key: "map", eyebrow: "02 — City guide", title: "A live map of every stop, guide and itinerary.", body: "Landmarks, markets, ferry stops, food trails and creator meetups stay organized in one guest-facing map." },
      { key: "scan", eyebrow: "03 — Tap or scan", title: "One scan turns each stop into measurable engagement.", body: "Tourist spots, booths and guide markers can log arrivals, unlock perks and update the campaign dashboard instantly." },
      { key: "dash", eyebrow: "04 — Campaign room", title: "A live dashboard for tourism partners.", body: "Pass scans, redemptions, dwell time and partner value update while the campaign is still in market." },
    ],
    trio: {
      eyebrow: "02 — City guide",
      title: "A live map of every stop, guide and itinerary.",
      body: "Landmarks, markets, ferry stops, food trails and creator meetups stay current. Tap any pin to see what is nearby, who is hosting and how to get there.",
    },
    ticket: {
      appHeadPass: "City Pass",
      appHeadRegister: "Register",
      email: "tara.cruz@islandweek.travel",
      eventKicker: "XTAG STUDIO · TOURISM DEMO",
      eventName: "Island City Week",
      eventDate: "Aug 9 · Visitor Hub",
      cta: "Claim my city pass",
      verifying: "Verifying booking…",
      foot: "Booking linked · pass found in our system",
      img: "assets/hero-verticals/tourism-ticket.png",
      alt: "Island City Week visitor pass art",
      registerBanner: "assets/hero-verticals/register-banners/tourism-register-banner.webp",
      registerBannerAlt: "Island City Week visitor hub banner",
      toastMark: "I",
      toastTitle: "Island City Week",
      toastMsg: "City pass ready",
    },
    map: {
      mapSrc: "assets/hero-verticals/tourism-map.jpg",
      thumbsSrc: "assets/hero-verticals/tourism-thumbs.jpg",
      peopleSrc: "assets/hero-verticals/tourism-people.jpg",
      mapAlt: "",
      views: ["map", "agenda", "people"],
      tabs: { map: "Stops", agenda: "Itinerary", people: "Guides" },
      headings: { map: "Stops", agenda: "Itinerary", people: "Guides" },
      mapSubheading: (visited, total) => `${visited}/${total} STAMPED`,
      agendaSubheading: "AUG 9 · DAY 1",
      peopleSubheading: "8 GUIDES ONLINE",
      venues: [
        { x: 26, y: 18, lbl: "Harbor Walk" },
        { x: 58, y: 16, lbl: "Old Town" },
        { x: 78, y: 28, lbl: "Viewpoint" },
        { x: 22, y: 54, lbl: "Food Market" },
        { x: 55, y: 60, lbl: "Museum" },
        { x: 76, y: 78, lbl: "Beach Cove" },
      ],
      booths: [
        { x: 36, y: 38, lbl: "Visitor Hub" },
        { x: 62, y: 42, lbl: "Craft Stop" },
        { x: 46, y: 72, lbl: "Ferry Pier" },
      ],
      youHere: "You · Visitor Hub",
      cardIcon: "H",
      cardIconBg: "#0A1628",
      cardTitle: "Harbor Walk · Stop 03",
      cardSub: "Guide nearby · 12 in group · 6m walk",
      cardCta: "Get route →",
      totalVisited: 10,
      progressLabel: "Stamps collected",
      remainingLabel: "stops to go",
      claimReady: "Unlock local perks →",
      claimIcon: "✓",
      streamViews: "842 watching",
      streamTitle: "Sunrise harbor walk",
      streamSub: "Pier Gate · Ana Mercado",
      days: [
        { day: "09", label: "AUG · DAY 1" },
        { day: "10", label: "AUG · DAY 2" },
        { day: "11", label: "AUG · DAY 3" },
      ],
      sessions: [
        { time: "8:00", title: "Sunrise harbor walk",     loc: "Pier Gate",    spk: "Ana Mercado", tag: "NOW", thumb: 0 },
        { time: "10:00", title: "Food market tasting",    loc: "Market Hall",  spk: "Chef Paolo",              thumb: 1 },
        { time: "12:30", title: "Old-town architecture",  loc: "Plaza Steps",  spk: "Mina Sol",                thumb: 2 },
        { time: "14:00", title: "Island boat ride",       loc: "Ferry Pier",                                  thumb: 3 },
        { time: "16:00", title: "Local craft workshop",   loc: "Makers Lane",  spk: "Rafi Cruz",               thumb: 4 },
        { time: "18:00", title: "Sunset viewpoint meetup", loc: "Hill Deck",   spk: "City Guides",             thumb: 5 },
      ],
      availability: "8 guides online",
      attending: "21,400 pass holders",
      people: [
        { name: "Ana Mercado", role: "Local Guide", company: "Harbor Walk", pos: 0, mutual: 4, tag: "Guide", bio: "Leads waterfront history walks and builds slow-travel itineraries for first-time visitors.", msgFrom: "Can we join your harbor route?", msgReply: "Yes. Meet at the pier gate at 8." },
        { name: "Leo Ramos", role: "Food Host", company: "Market Hall", pos: 1, mutual: 3, tag: "Food", bio: "Curates food market stops and introduces travelers to neighborhood vendors.", msgFrom: "Any vegetarian stops today?", msgReply: "Yes, I marked three on the route." },
        { name: "Mina Sol", role: "Architect", company: "Old Town Studio", pos: 2, mutual: 2, tag: "Culture", bio: "Hosts old-town walks focused on adaptive reuse, craft and city stories.", msgFrom: "Loved your plaza notes.", msgReply: "Thanks. Museum stop is next." },
        { name: "Rafi Cruz", role: "Maker", company: "Craft Lane", pos: 3, mutual: 5, tag: "Local", bio: "Runs craft demos and connects travelers with family-owned workshops.", msgFrom: "Can we scan at your workshop?", msgReply: "Yes, marker is at the front table." },
        { name: "Sofia Tan", role: "Travel Creator", company: "Island Notes", pos: 4, mutual: 7, tag: "Creator", bio: "Creates destination guides and short-form itineraries for island city campaigns.", msgFrom: "Where is the best sunset angle?", msgReply: "Hill Deck, left side of the rail." },
        { name: "Nico Reyes", role: "Boat Captain", company: "Bay Loop", pos: 5, mutual: 1, tag: "Guide", bio: "Runs bay loop tours and weather-aware ferry handoffs for travelers.", msgFrom: "Is the 2 PM boat still open?", msgReply: "Four seats left. Scan at Pier 2." },
        { name: "Lara Lim", role: "Museum Host", company: "City Museum", pos: 6, mutual: 3, tag: "Culture", bio: "Builds quick museum routes for families, creators and food trail guests.", msgFrom: "Can you stamp our city pass?", msgReply: "Yes. North entrance scanner." },
        { name: "Jay Ong", role: "Vendor Lead", company: "Night Market", pos: 7, mutual: 6, tag: "Local", bio: "Coordinates night-market vendors and campaign redemptions.", msgFrom: "The booth line is growing.", msgReply: "Opening a second scanner now." },
      ],
      socialCard: { icon: "✓", title: "Sunset guide meetup", sub: "Hill Deck · 6:00 PM · 214 going", cta: "Join" },
    },
    scan: {
      total: 10,
      startCount: 5,
      doneCount: 6,
      points: 40,
      pointsLabel: "PTS",
      rankStart: "#52",
      rankDone: "#41",
      screenTitle: "City Scanner",
      progressTitle: "Your city trail",
      scanTitle: "Scan stop QR",
      scanSub: "Stamp any landmark or booth",
      activityHeader: "Recent stamps",
      activityScope: "Today",
      newActivity: { title: "Harbor Walk · Stop 03", tag: "+40 PTS" },
      activities: [
        { time: "6m ago", title: "Food Market · tasting joined", tag: "+40 PTS" },
        { time: "18m ago", title: "Visitor Hub · welcome kit claimed", tag: "+40 PTS" },
        { time: "42m ago", title: "Old Town walk · joined" },
        { time: "1h ago", title: "Checked in · Ferry Pier" },
      ],
      cameraSrc: "assets/hero-verticals/tourism-scan.jpg",
      frameTop: "53%",
      cameraTitle: "Scan city marker",
      recognized: "Code recognized · Harbor Walk",
      success: "City stamp logged",
      toastIcon: "H",
      toastIconBg: "#0A1628",
      toastTitle: "Stamp logged · Harbor Walk",
      toastSub: "+1 stop · 6 / 10 stamped today",
      rewardIcon: "★",
      rewardIconBg: "#18a957",
      rewardTitle: "+40 points earned",
      rewardSub: "Explorer rank ↑ #52 → #41",
      itemLabel: "stops",
    },
    dash: {
      appTitle: "Campaign",
      headlineLabel: "Campaign redemptions · live",
      headlineTarget: 21400,
      headlineBase: 24000,
      headlineDecimals: 0,
      headlinePrefix: "",
      headlineSuffix: "",
      headlineDelta: "+3,240 scans last hour",
      headlineTargetText: "24,000 campaign target",
      secondaryTarget: 21400,
      kpis: [
        { label: "Pass scans", value: "animated", detail: "72% visitor verified" },
        { label: "Redemption", value: "32%", detail: "stamps → offers" },
        { label: "Avg spend", value: "₱1.8K", detail: "+19% partner lift" },
        { label: "Engagement", value: "5.8×", detail: "stops / traveler" },
      ],
      rowsTitle: "Top stops · redemptions",
      rowsMeta: "CLAIMS",
      rows: [
        { lbl: "Food Market", val: "5.1K", w: 92 },
        { lbl: "Harbor Walk", val: "4.4K", w: 80 },
        { lbl: "Old Town", val: "3.6K", w: 68 },
        { lbl: "Craft Lane", val: "2.8K", w: 54 },
        { lbl: "Beach Cove", val: "2.1K", w: 42 },
      ],
      claimsTitle: "Partner offers · live",
      claimsMeta: "CLAIMED",
      claims: [
        { lbl: "Food tasting", val: "88%", w: 88 },
        { lbl: "Museum pass", val: "64%", w: 64 },
        { lbl: "Boat discount", val: "52%", w: 52 },
        { lbl: "Craft token", val: "70%", w: 70 },
        { lbl: "Market perk", val: "76%", w: 76 },
      ],
      foot: "Guide partner forecast · 88% rebook intent · 4.8 / 5 visitor rating",
      tablet: {
        lastUpdate: "Updated 14s ago",
        spark: {
          label: "Stamp redemptions · last 6 hours",
          points: [620,840,1100,1320,1540,1820,2080,2360,2680,3020,3380,3760,4180,4640,5100,5560,6040,6520,7020,7540,8080,8640,9220,9820],
          peakLabel: "+3,240 last hour",
        },
        gauge: { value: 18, max: 24, label: "Guides on tour", sub: "6 routes covered · 2 starting" },
        nps:   { value: 4.8, max: 5, label: "Visitor rating", sub: "3,460 responses · multi-day pass" },
        funnel: {
          title: "Visitor journey · today",
          stages: [
            { stage: "Pass activated",  count: "9,820", pct: 100, w: 100 },
            { stage: "First stamp",     count: "8,630", pct: 88,  w: 88  },
            { stage: "5+ stops",        count: "5,420", pct: 55,  w: 55  },
            { stage: "Partner offer",   count: "3,180", pct: 32,  w: 32  },
          ],
        },
        heat: {
          title: "City heat · live stamps",
          hot: "Food Market plaza",
          cols: ["W","E","N","S","C","H"],
          rows: ["AM","MID","PM","EVE"],
          cells: [
            0.30,0.45,0.50,0.40,0.35,0.25,
            0.50,0.70,0.80,0.65,0.55,0.40,
            0.65,0.85,0.95,0.75,0.60,0.45,
            0.40,0.55,0.70,0.55,0.40,0.30,
          ],
        },
        donut: { used: 21400, total: 24000, label: "Campaign target", sub: "89.2% of 24k stamps" },
        feed: {
          title: "Live activity",
          items: [
            { time: "14s",  text: "Harbor Walk · stamp logged",            tag: "+1 STAMP" },
            { time: "38s",  text: "Boat discount redeemed — pier 3",       tag: "REDEEM"   },
            { time: "1m",   text: "Old Town · 5-stop badge unlocked",      tag: "BADGE"    },
            { time: "1m",   text: "Food tasting · pasalubong claimed",     tag: "REWARD"   },
            { time: "2m",   text: "Craft Lane · queue at 6 minutes",       tag: "OPS"      },
            { time: "2m",   text: "Beach Cove · sunset window opens",      tag: "EVENT"    },
          ],
        },
        alert: {
          title: "Craft Lane · 6m queue",
          sub: "Push partner offer to redirect 200 visitors",
          cta: "Send push",
        },
      },
    },
  },
  weddings: {
    key: "weddings",
    label: "Weddings",
    scenes: [
      { key: "ticket", eyebrow: "01 — Guest pass", title: "It starts with a guest pass in their pocket.", body: "RSVP details, table notes, QR check-ins and celebration updates stay inside one elegant mobile pass." },
      { key: "map", eyebrow: "02 — Venue guide", title: "A live map of every station, program and guest.", body: "Ceremony areas, food stations, bars, photo booths and program moments stay organized for guests and planners." },
      { key: "scan", eyebrow: "03 — Tap or scan", title: "One scan powers the guest experience.", body: "Photo booths, food stations, welcome gifts and activity corners can log visits, unlock memories and reduce planner guesswork." },
      { key: "dash", eyebrow: "04 — Planner room", title: "A live dashboard for hosts and vendors.", body: "Guest flow, station demand, RSVP status and activity scans update while the celebration is still happening." },
    ],
    trio: {
      eyebrow: "02 — Venue guide",
      title: "A live map of every station, program and guest.",
      body: "Ceremony areas, food stations, bars, photo booths and program moments stay organized. Tap any pin to see what is open, where guests are gathering and what happens next.",
    },
    ticket: {
      appHeadPass: "Guest Pass",
      appHeadRegister: "RSVP",
      email: "sofia.lim@email.com",
      eventKicker: "XTAG STUDIO · GUEST DEMO",
      eventName: "Mara & Nico",
      eventDate: "Oct 18 · Garden Gate",
      cta: "Claim my invite",
      verifying: "Verifying RSVP…",
      foot: "Guest linked · invite found in our system",
      img: "assets/hero-verticals/weddings-ticket.png",
      alt: "Mara and Nico wedding guest pass art",
      registerBanner: "assets/hero-verticals/register-banners/weddings-register-banner.webp",
      registerBannerAlt: "Mara and Nico wedding venue banner",
      toastMark: "M",
      toastTitle: "Mara & Nico",
      toastMsg: "Guest pass ready",
    },
    map: {
      mapSrc: "assets/hero-verticals/weddings-map.jpg",
      thumbsSrc: "assets/hero-verticals/weddings-thumbs.jpg",
      peopleSrc: "assets/hero-verticals/weddings-people.jpg",
      mapAlt: "",
      views: ["map", "agenda", "people"],
      tabs: { map: "Stations", agenda: "Program", people: "Guests" },
      headings: { map: "Stations", agenda: "Program", people: "Guests" },
      mapSubheading: (visited, total) => `${visited}/${total} VISITED`,
      agendaSubheading: "OCT 18 · PROGRAM",
      peopleSubheading: "8 GUESTS NEARBY",
      venues: [
        { x: 24, y: 18, lbl: "Ceremony Lawn" },
        { x: 62, y: 16, lbl: "Cocktail Bar" },
        { x: 78, y: 36, lbl: "Photo Booth" },
        { x: 28, y: 56, lbl: "Food Stations" },
        { x: 56, y: 62, lbl: "Family Tables" },
        { x: 74, y: 78, lbl: "Dance Floor" },
      ],
      booths: [
        { x: 42, y: 40, lbl: "Guest Book" },
        { x: 64, y: 46, lbl: "Dessert" },
        { x: 36, y: 76, lbl: "Favors" },
      ],
      youHere: "You · Garden Gate",
      cardIcon: "P",
      cardIconBg: "#0A1628",
      cardTitle: "Photo Booth · Garden Hall",
      cardSub: "Open now · 12 guests nearby · 2m walk",
      cardCta: "Get route →",
      totalVisited: 6,
      progressLabel: "Stations visited",
      remainingLabel: "stations to go",
      claimReady: "Unlock guest moments →",
      claimIcon: "✓",
      streamViews: "118 watching",
      streamTitle: "Ceremony aisle",
      streamSub: "Garden Lawn · Planner Cam",
      days: [
        { day: "18", label: "OCT · EVENT" },
        { day: "19", label: "OCT · BRUNCH" },
        { day: "20", label: "OCT · PHOTOS" },
      ],
      sessions: [
        { time: "15:30", title: "Guest arrival",            loc: "Garden Gate",  spk: "Host Team", tag: "NOW", thumb: 0 },
        { time: "16:00", title: "Ceremony begins",          loc: "Ceremony Lawn",                              thumb: 1 },
        { time: "17:00", title: "Cocktail hour",            loc: "Terrace Bar",                                thumb: 2 },
        { time: "18:15", title: "Dinner service",           loc: "Reception Tent",                             thumb: 3 },
        { time: "19:30", title: "Photo booth opens",        loc: "Garden Hall",  spk: "Lens Team",             thumb: 4 },
        { time: "20:30", title: "First dance + party",      loc: "Dance Floor",                                thumb: 5 },
      ],
      availability: "8 guests nearby",
      attending: "418 attending",
      people: [
        { name: "Sofia Lim", role: "Bride's Cousin", company: "Table 8", pos: 0, mutual: 6, tag: "Family", bio: "Helping relatives find tables, photo moments and the dessert station.", msgFrom: "Are you near the garden gate?", msgReply: "Yes. I can walk you to Table 8." },
        { name: "Marco Reyes", role: "Groomsman", company: "Wedding Party", pos: 1, mutual: 4, tag: "Party", bio: "Keeps the wedding party on schedule and guests pointed to the next program moment.", msgFrom: "Photo booth open already?", msgReply: "Opens after dinner. I will ping you." },
        { name: "Lia Cruz", role: "Planner", company: "Linen & Light", pos: 2, mutual: 3, tag: "Planner", bio: "Coordinates guest flow, vendors, timeline and station scan data.", msgFrom: "Dessert line is getting long.", msgReply: "Opening the second station now." },
        { name: "Rafa Ong", role: "Photographer", company: "Lens Team", pos: 3, mutual: 2, tag: "Photo", bio: "Captures candid guest moments and manages the shared photo booth gallery.", msgFrom: "Can we get a family shot?", msgReply: "Yes, garden arch in five." },
        { name: "Maya Santos", role: "Friend", company: "Table 12", pos: 4, mutual: 5, tag: "Guest", bio: "College friend, dance-floor starter and signature drink scout.", msgFrom: "Where are the signature drinks?", msgReply: "Terrace Bar, left side." },
        { name: "Noah Tan", role: "Caterer", company: "Harvest Table", pos: 5, mutual: 1, tag: "Vendor", bio: "Runs dinner, dessert and late-night snacks with station-level demand updates.", msgFrom: "Vegetarian station still open?", msgReply: "Yes, next to the dessert bar." },
        { name: "Ina Mercado", role: "Floral Lead", company: "Bloom House", pos: 6, mutual: 2, tag: "Vendor", bio: "Keeps ceremony, reception and photo booth florals fresh throughout the program.", msgFrom: "Can guests take table flowers?", msgReply: "After 9 PM, yes." },
        { name: "Jules Dela Cruz", role: "Host", company: "Reception Team", pos: 7, mutual: 8, tag: "Party", bio: "Welcomes guests, handles program cues and keeps the energy moving.", msgFrom: "When does the first dance start?", msgReply: "8:30 on the main floor." },
      ],
      socialCard: { icon: "✓", title: "Photo booth open", sub: "Garden Hall · 7:30 PM · 86 queued", cta: "Join" },
    },
    scan: {
      total: 6,
      startCount: 2,
      doneCount: 3,
      points: 20,
      pointsLabel: "PTS",
      rankStart: "#31",
      rankDone: "#18",
      screenTitle: "Guest Scanner",
      progressTitle: "Your celebration trail",
      scanTitle: "Scan station QR",
      scanSub: "Check in at any guest station",
      activityHeader: "Recent moments",
      activityScope: "Tonight",
      newActivity: { title: "Photo Booth · Garden Hall", tag: "+20 PTS" },
      activities: [
        { time: "5m ago", title: "Cocktail Bar · signature drink claimed", tag: "+20 PTS" },
        { time: "28m ago", title: "Guest Book · message added", tag: "+20 PTS" },
        { time: "1h ago", title: "Ceremony Lawn · joined" },
        { time: "2h ago", title: "Checked in · Garden Gate" },
      ],
      cameraSrc: "assets/hero-verticals/weddings-scan.jpg",
      frameTop: "43%",
      cameraTitle: "Scan station code",
      recognized: "Code recognized · Photo Booth",
      success: "Guest moment logged",
      toastIcon: "P",
      toastIconBg: "#0A1628",
      toastTitle: "Moment logged · Photo Booth",
      toastSub: "+1 station · 3 / 6 visited tonight",
      rewardIcon: "★",
      rewardIconBg: "#18a957",
      rewardTitle: "+20 points earned",
      rewardSub: "Guest trail rank ↑ #31 → #18",
      itemLabel: "stations",
    },
    dash: {
      appTitle: "Planner",
      headlineLabel: "Guest experience · live",
      headlineTarget: 418,
      headlineBase: 440,
      headlineDecimals: 0,
      headlinePrefix: "",
      headlineSuffix: "",
      headlineDelta: "+126 scans last hour",
      headlineTargetText: "440 guest target",
      secondaryTarget: 418,
      kpis: [
        { label: "Guest scans", value: "animated", detail: "91% checked in" },
        { label: "RSVPs", value: "98%", detail: "confirmed → arrived" },
        { label: "Avg wait", value: "3m", detail: "photo booth queue" },
        { label: "Moments", value: "4.4×", detail: "stations / guest" },
      ],
      rowsTitle: "Top stations · live visits",
      rowsMeta: "VISITS",
      rows: [
        { lbl: "Photo Booth", val: "126", w: 92 },
        { lbl: "Cocktail Bar", val: "112", w: 82 },
        { lbl: "Dessert", val: "94", w: 70 },
        { lbl: "Guest Book", val: "72", w: 54 },
        { lbl: "Favors", val: "58", w: 44 },
      ],
      claimsTitle: "Guest moments · live",
      claimsMeta: "CLAIMED",
      claims: [
        { lbl: "Photo gallery", val: "76%", w: 76 },
        { lbl: "Dessert pass", val: "68%", w: 68 },
        { lbl: "Signature drink", val: "82%", w: 82 },
        { lbl: "Guest favor", val: "71%", w: 71 },
        { lbl: "After-party", val: "57%", w: 57 },
      ],
      foot: "Vendor coverage · 96% staffed · 4.9 / 5 guest rating",
      tablet: {
        lastUpdate: "Updated 6s ago",
        spark: {
          label: "Guest scans · last 90 minutes",
          points: [12,18,22,28,36,46,58,72,90,112,138,164,192,224,256,290,318,344,368,388,402,410,415,418],
          peakLabel: "418 of 440 guests",
        },
        gauge: { value: 24, max: 28, label: "Vendors on site", sub: "Caterer, florist, AV all green" },
        nps:   { value: 4.9, max: 5, label: "Guest rating · live", sub: "312 responses · happiest moment" },
        funnel: {
          title: "Guest journey · tonight",
          stages: [
            { stage: "RSVP confirmed",   count: "440", pct: 100, w: 100 },
            { stage: "Arrived",          count: "418", pct: 95,  w: 95  },
            { stage: "Photo booth",      count: "326", pct: 74,  w: 74  },
            { stage: "After-party RSVP", count: "248", pct: 56,  w: 56  },
          ],
        },
        heat: {
          title: "Venue heat · live",
          hot: "Cocktail bar · garden",
          cols: ["L1","L2","L3","L4","L5","L6"],
          rows: ["GAR","MAIN","DCK","BAR"],
          cells: [
            0.45,0.55,0.70,0.65,0.50,0.35,
            0.55,0.75,0.85,0.80,0.60,0.45,
            0.40,0.60,0.75,0.70,0.55,0.40,
            0.65,0.85,0.95,0.85,0.65,0.50,
          ],
        },
        donut: { used: 418, total: 440, label: "Guest attendance", sub: "95% arrival · 22 still in transit" },
        feed: {
          title: "Live moments",
          items: [
            { time: "6s",   text: "Photo booth · 4 guests captured",       tag: "MOMENT" },
            { time: "22s",  text: "Cocktail bar — signature drink",        tag: "REWARD" },
            { time: "44s",  text: "Toast scheduled · 8:15 PM",             tag: "OPS"    },
            { time: "1m",   text: "Guest book · 32 entries tonight",       tag: "+1"     },
            { time: "2m",   text: "Dessert station — opening soon",        tag: "EVENT"  },
            { time: "2m",   text: "After-party · 248 RSVPs",               tag: "RSVP"   },
          ],
        },
        alert: {
          title: "Toast in 12 minutes",
          sub: "Cue band, dim main hall lights",
          cta: "Send vendor cue",
        },
      },
    },
  },
  philippineMarketingAssociation: {
    key: "philippineMarketingAssociation",
    label: "PMA",
    scenes: [
      { key: "ticket", eyebrow: "01 — Akademya", title: "Launch a Coursera-style learning home for Filipino marketers.", body: "Akademya hosts Trannovation, AI for MSMEs, brand stewardship, and Agora case studies in one player. Course progress, CPM credits, and certificates ride on the member pass." },
      { key: "map", eyebrow: "02 — Catalog and calendar", title: "One PWA for every course, every event, and every member.", body: "Members open one app to find Akademya courses, the Marketplace, accredited partners, the NMC and StratMark calendar, and PMA's research index." },
      { key: "scan", eyebrow: "03 — Credit capture", title: "Every scan logs a course module, a GMM check-in, or a sponsor visit.", body: "QR scans verify GMM attendance, log Trannovation course completion, capture Agora-night sponsor booth visits, and feed CPM/CDM credentialing exports." },
      { key: "dash", eyebrow: "04 — Akademya admin", title: "PMA Secretariat gets one live learning console.", body: "Course enrollments, completion rates, certificate issuance, learner NPS, Marketplace revenue, and event check-in all update from one Akademya admin dashboard." },
    ],
    trio: {
      eyebrow: "02 — Catalog and calendar",
      title: "Akademya, Marketplace, events, and member identity in one PWA.",
      body: "Members see which Trannovation course is next, which Marketplace listing is new, which Agora deadline is closing, and what PMA event is on this week.",
    },
    ticket: {
      appHeadPass: "Akademya",
      appHeadRegister: "Enroll",
      email: "learner@philippinemarketing.org",
      eventKicker: "XTAG STUDIO · PMA AKADEMYA",
      eventName: "Trannovation Foundations",
      eventDate: "Module 04 of 8 · CPM credit · 18 min left",
      cta: "Resume course",
      verifying: "Loading Akademya...",
      foot: "Member record linked · Trannovation track unlocked",
      img: "/assets/philippine-marketing-association/generated/member-pass-card.png",
      alt: "PMA mobile member pass card mockup",
      passLogoSrc: "/assets/philippine-marketing-association/logos/pma-logo.png",
      passLogoAlt: "PMA membership mark",
      registerBanner: "/assets/philippine-marketing-association/reference/pma-2025-event-brown.jpg",
      registerBannerAlt: "PMA 2025 event reference",
      toastMark: "A",
      toastTitle: "Akademya",
      toastMsg: "Module ready · CPM credit",
    },
    map: {
      mapSrc: "/assets/philippine-marketing-association/generated/member-home-overview.png",
      thumbsSrc: "/assets/philippine-marketing-association/generated/session-thumbnails.png",
      peopleSrc: "/assets/philippine-marketing-association/generated/member-avatars.png",
      mapAlt: "",
      views: ["map", "agenda", "people"],
      tabs: { map: "Home", agenda: "Calendar", people: "Directory" },
      headings: { map: "Member home", agenda: "Calendar", people: "Directory" },
      mapSubheading: (visited, total) => `${visited}/${total} ACTIONS DONE`,
      agendaSubheading: "2026 PMA PROGRAM YEAR",
      peopleSubheading: "6 MEMBER TYPES TO SERVE",
      venues: [
        { x: 28, y: 14, lbl: "Akademya" },
        { x: 12, y: 30, lbl: "Member Pass" },
        { x: 74, y: 22, lbl: "Marketplace" },
        { x: 80, y: 48, lbl: "Agora Hub" },
        { x: 24, y: 70, lbl: "NMC & StratMark" },
        { x: 60, y: 74, lbl: "Research Index" },
      ],
      booths: [
        { x: 44, y: 38, lbl: "Trannovation" },
        { x: 62, y: 42, lbl: "CPM track" },
        { x: 52, y: 58, lbl: "Sponsor lounge" },
      ],
      youHere: "You · PMA member home",
      cardIcon: "A",
      cardIconBg: "#A33D2B",
      cardTitle: "Trannovation · Module 04",
      cardSub: "Akademya · 18 min left · CPM credit eligible",
      cardCta: "Resume course ->",
      totalVisited: 6,
      progressLabel: "Member actions",
      remainingLabel: "left to unlock CPM eligibility",
      claimReady: "Review CPM packet ->",
      claimIcon: "A",
      streamViews: "Member home · live",
      streamTitle: "OneNess: Innovation. Transformation. Inspiration.",
      streamSub: "72nd PMA Presidency · 2026",
      days: [
        { day: "Feb", label: "AGORA AWARDS" },
        { day: "Aug", label: "AGORA YOUTH" },
        { day: "Oct", label: "STRATMARK" },
      ],
      sessions: [
        { time: "Feb 11", title: "44th Agora Awards · Luster", loc: "Winford Resort Manila", spk: "PMA Secretariat", tag: "OPEN", thumb: 0 },
        { time: "Mar", title: "GMM 02 · MSMEs and AI-empowered marketing", loc: "Member meeting", spk: "Board of Governors", thumb: 1 },
        { time: "May", title: "Trannovation Bootcamp · Marketing + Sales", loc: "Akademya", spk: "PMA Faculty", thumb: 2 },
        { time: "Aug", title: "33rd Agora Youth Awards", loc: "Student community", spk: "JMA chapters", thumb: 3 },
        { time: "Oct", title: "24th StratMark Student Conference", loc: "Host university", spk: "Academic council", thumb: 4 },
        { time: "Nov", title: "54th National Marketing Conference", loc: "PMA flagship", spk: "Asia Marketing Federation", thumb: 5 },
      ],
      availability: "6 member types to coordinate",
      attending: "Corporate, individual, fellow, student (JMA), faculty, partner",
      people: [
        { name: "Mitch de Ocampo-Ballesteros", role: "72nd PMA President", company: "Philippine Marketing Association", pos: 0, mutual: 8, tag: "President", bio: "Sets the 2026 OneNess agenda for solidarity, inclusive growth, and doing good beyond business across PMA programs.", msgFrom: "Can the member pass track Akademya?", msgReply: "Yes. Each scan and course feeds the same member record." },
        { name: "PMA Corporate Member", role: "Marketing Director", company: "Member company", pos: 1, mutual: 6, tag: "Corporate", bio: "Wants Akademya seats for the team, a clean Marketplace listing, and verified Agora submission proof.", msgFrom: "How do we manage team seats?", msgReply: "Corporate consoles can issue passes and unlock courses." },
        { name: "PMA Individual Member", role: "Senior Marketer", company: "Brand or agency", pos: 2, mutual: 5, tag: "Individual", bio: "Needs CPM credits, easy event access, a portfolio in the Marketplace, and AI-empowered learning paths.", msgFrom: "Can I track CPM credits?", msgReply: "Akademya scans and assessments build the packet." },
        { name: "JMA Student Officer", role: "Student leader", company: "University chapter", pos: 3, mutual: 4, tag: "JMA", bio: "Coordinates StratMark, Agora Youth, and chapter internships through a single student-tier pass.", msgFrom: "Can chapters export attendance?", msgReply: "Yes. Roles control exports by chapter." },
        { name: "Marketing Educator", role: "Faculty advisor", company: "DLSU / UST / Ateneo", pos: 4, mutual: 5, tag: "Faculty", bio: "Uses PMA case studies, Agora-winning work, and Trannovation modules in classroom curricula.", msgFrom: "Can I license a course pack?", msgReply: "Marketplace handles licensing and revenue share." },
        { name: "Sponsor Partner", role: "AGC PHC / brand sponsor", company: "Industry partner", pos: 5, mutual: 7, tag: "Sponsor", bio: "Needs measurable reach, lead capture at Agora and NMC, and Akademya course sponsorship analytics.", msgFrom: "Can sponsors brand a course?", msgReply: "Yes. Sponsored modules can be tracked end to end." },
        { name: "Accredited Agency", role: "Marketing services firm", company: "Marketplace seller", pos: 6, mutual: 3, tag: "Partner", bio: "Sells research, decks, templates, and consulting via the PMA Marketplace with verified credentials.", msgFrom: "How do I list services?", msgReply: "Marketplace onboarding includes a vetted credentialing step." },
        { name: "PMA Secretariat", role: "Operations lead", company: "PMA HQ Pasig", pos: 7, mutual: 6, tag: "Ops", bio: "Runs member servicing, dues collection, GMM logistics, and post-event reporting for the Board.", msgFrom: "Can helpdesk update records?", msgReply: "Yes. Staff roles control what can be edited." },
      ],
      socialCard: { icon: "P", title: "Akademya · Module 04 ready", sub: "Trannovation · 18 min · CPM credit", cta: "Open" },
    },
    featureViews: ["akademya", "marketplace", "memberStack", "agoraEntries", "sponsorPartners", "secretariat"],
    features: {
      akademya: {
        type: "puzzle",
        appTitle: "Akademya",
        kicker: "PMA AKADEMYA",
        title: "Trannovation courses, learning paths, and CPM-ready certification",
        sub: "A Udemy-like learning home for Filipino marketers, built around Trannovation tracks, AI-empowered marketing, and Asia Marketing Federation CPM requirements.",
        description: "Members enroll in modules at their own pace. Sponsor-backed courses, Agora case studies, StratMark talks, and Trannovation bootcamps live in one catalog with credit tracking.",
        bullets: ["Course catalog, learning paths, certificates of completion.", "Sponsor-funded course slots and Agora case-study series.", "CPM credit packet, attempts, scores, and final assessment."],
        primary: "62%",
        primaryLabel: "course complete",
        progress: 62,
        code: ["T", "R", "A", "N", "N", "O", "V"],
        rows: ["Trannovation MKT", "AI for MSMEs", "Brand stewardship"],
      },
      marketplace: {
        type: "market",
        appTitle: "Marketplace",
        kicker: "PMA MARKETPLACE",
        title: "Research, decks, and accredited services in one verified market",
        sub: "Members buy and license research reports, campaign decks, branding templates, and consulting from verified PMA fellows and accredited agencies.",
        description: "Each listing is tied to a verified PMA member or partner. Reviews and revenue share are managed by PMA, with a Marketplace fee structure to fund Akademya scholarships.",
        bullets: ["Research index, decks, templates, and consulting services.", "Verified seller credentials and Agora-winner badges.", "Stripe and PayMongo checkout with revenue share for PMA."],
        primary: "184",
        primaryLabel: "listings live",
        rows: [
          { label: "Research index", meta: "PMA + AMF studies", status: "LIVE" },
          { label: "Agora case decks", meta: "Marketing Co. of the Year", status: "LIVE" },
          { label: "Accredited services", meta: "Verified members", status: "REVIEW" },
        ],
      },
      memberStack: {
        type: "checkins",
        appTitle: "Member",
        kicker: "MEMBER STACK",
        title: "One member record across registration, dues, and renewal",
        sub: "Corporate, individual, fellow, student, faculty, and partner records live in one CRM with dues, history, and credential trails.",
        description: "Onboarding, dues collection, renewals, badge generation, and PMA pin issuance flow through one record — with corporate consoles that manage team seats and access.",
        bullets: ["Member onboarding, KYC, payments, and renewal reminders.", "Corporate consoles for team seats and access roles.", "Member directory, fellowships, life member, and lifetime tiers."],
        primary: "1,820",
        primaryLabel: "active members",
        progress: 78,
        tabs: ["INDIVIDUAL", "CORPORATE", "JMA"],
        rows: [
          { label: "New application", meta: "individual tier", status: "OPEN" },
          { label: "Dues renewal", meta: "corporate · 24 seats", status: "DONE" },
          { label: "JMA chapter import", meta: "DLSU MARKomm", status: "REVIEW" },
        ],
      },
      agoraEntries: {
        type: "raffle",
        appTitle: "Agora",
        kicker: "AGORA AWARDS",
        title: "Agora and Agora Youth entries become structured submissions",
        sub: "Marketing Company of the Year, Outstanding Achievement, and Agora Youth submissions can be filed, judged, and archived inside one workflow.",
        description: "Each entry connects to the submitting member, their case study, judging scores, and the public archive that the Marketplace can later license.",
        bullets: ["Category templates, submission rules, and deadline gating.", "Judging panel access, scoring, and audit trail.", "Public Agora archive that powers Marketplace case-deck listings."],
        primary: "44",
        primaryLabel: "Agora Awards · 2026",
        progress: 90,
        progressLabel: "submissions in review",
        cta: "Review entries",
        stats: [
          { value: "33", label: "Agora Youth" },
          { value: "Feb 11", label: "ceremony" },
        ],
        rows: ["Marketing Company of the Year", "Outstanding Achievement in Education", "Agora Youth · 33rd edition"],
      },
      sponsorPartners: {
        type: "market",
        appTitle: "Sponsors",
        kicker: "SPONSOR PARTNERS",
        title: "Compliant lead capture for AGC PHC and PMA partners",
        sub: "Sponsors and accredited partners capture leads at GMM, NMC, StratMark, and Agora through scoped scans tied to a single member record.",
        description: "Sponsorship value becomes easier to renew when every Agora-night badge scan, NMC booth visit, and Akademya course sponsorship rolls up into one renewal report.",
        bullets: ["Badge QR scanning for authorized sponsor staff.", "Lead qualification notes and product interest tags.", "Renewal report with Akademya, Marketplace, and event activity."],
        primary: "1,248",
        primaryLabel: "sponsor scans · 2026",
        rows: [
          { label: "NMC booth · sponsor", meta: "482 scans", status: "LIVE" },
          { label: "Agora night · sponsor", meta: "316 scans", status: "LIVE" },
          { label: "Akademya course sponsor", meta: "approval required", status: "LOCKED" },
        ],
      },
      secretariat: {
        type: "raffle",
        appTitle: "Reports",
        kicker: "PMA SECRETARIAT",
        title: "Member and event reports ready for the Board",
        sub: "Membership growth, dues collection, Akademya completions, Marketplace revenue, and event attendance package automatically.",
        description: "Instead of rebuilding numbers from sheets and chat threads, the Secretariat can close every quarter with structured exports for the Board, sponsors, and AMF.",
        bullets: ["Member growth and dues collection exports.", "Akademya and Marketplace performance reports.", "Event attendance, sponsor proof, and AMF reporting."],
        primary: "92%",
        primaryLabel: "report ready",
        progress: 92,
        progressLabel: "quarterly closeout",
        cta: "Review reports",
        stats: [
          { value: "12", label: "exports" },
          { value: "4.8", label: "member NPS" },
        ],
        rows: ["Member growth", "Akademya completions", "Marketplace revenue"],
      },
    },
    scan: {
      total: 6,
      startCount: 3,
      doneCount: 4,
      points: 1,
      pointsLabel: "ACTION",
      rankStart: "#218",
      rankDone: "#214",
      screenTitle: "PMA Scanner",
      progressTitle: "Your member record",
      scanTitle: "Scan PMA code",
      scanSub: "Check in, log course progress, or claim sponsor visit",
      activityHeader: "Recent activity",
      activityScope: "Today",
      newActivity: { title: "GMM 02 · attendance verified", tag: "+1 ACTION" },
      activities: [
        { time: "3m ago", title: "Akademya · Trannovation module saved", tag: "COURSE" },
        { time: "14m ago", title: "Marketplace · research index viewed", tag: "ASSET" },
        { time: "22m ago", title: "Sponsor booth · GMM visit logged" },
        { time: "39m ago", title: "Member pass · dues paid" },
      ],
      cameraSrc: "/assets/philippine-marketing-association/generated/scanner-camera-bg.png",
      bakedCameraOverlay: false,
      frameTop: "74%",
      cameraTitle: "Scan PMA code",
      recognized: "Code recognized · GMM 02",
      success: "Attendance verified",
      toastIcon: "P",
      toastIconBg: "#A33D2B",
      toastTitle: "Action logged",
      toastSub: "GMM 02 · CPM credit progressed",
      rewardIcon: "C",
      rewardIconBg: "#1B1B1B",
      rewardTitle: "CPM credit progress",
      rewardSub: "4 of 6 actions completed",
      itemLabel: "actions",
    },
    dash: {
      appTitle: "Akademya Ops",
      headlineLabel: "Course enrollments · live",
      headlineTarget: 1450,
      headlineBase: 1820,
      headlineDecimals: 0,
      headlinePrefix: "",
      headlineSuffix: "",
      headlineDelta: "+186 enrollments this week",
      headlineTargetText: "active learners across Akademya catalog",
      secondaryTarget: 1450,
      kpis: [
        { label: "Active learners", value: "animated", detail: "across 24 courses" },
        { label: "Course completion", value: "62%", detail: "median across tracks" },
        { label: "Certificates issued", value: "812", detail: "CPM credit eligible" },
        { label: "Course NPS", value: "4.8", detail: "learner pulse · live" },
      ],
      rowsTitle: "Top courses · live enrollments",
      rowsMeta: "LEARNERS",
      rows: [
        { lbl: "Trannovation Foundations", val: "412", w: 92 },
        { lbl: "AI for MSMEs", val: "318", w: 78 },
        { lbl: "Trannovation MKT+Sales", val: "246", w: 62 },
        { lbl: "Agora Case Studies", val: "188", w: 46 },
        { lbl: "Sustainability Credo", val: "146", w: 38 },
      ],
      claimsTitle: "Course completion · cohorts",
      claimsMeta: "COMPLETE",
      claims: [
        { lbl: "Trannovation Foundations", val: "82%", w: 82 },
        { lbl: "AI for MSMEs", val: "74%", w: 74 },
        { lbl: "Trannovation Bootcamp", val: "58%", w: 58 },
        { lbl: "Agora Case Studies", val: "46%", w: 46 },
        { lbl: "Certificates · CPM", val: "92%", w: 92 },
      ],
      foot: "Akademya · 92% certificate-ready · 4.8 / 5 learner NPS",
      tablet: {
        lastUpdate: "Updated 8s ago",
        spark: {
          label: "Course minutes watched · last 90 minutes",
          points: [60,110,170,220,290,360,440,520,600,680,750,820,880,940,1000,1060,1120,1180,1240,1300,1360,1400,1430,1450],
          peakLabel: "1,450 learners · live",
        },
        gauge: { value: 24, max: 34, label: "Courses live", sub: "Foundations, Bootcamps, AI, Sustainability" },
        nps: { value: 4.8, max: 5, label: "Learner NPS", sub: "Live course ratings" },
        funnel: {
          title: "Learner journey · 2026",
          stages: [
            { stage: "Enrolled",          count: "1,820", pct: 100, w: 100 },
            { stage: "Started module",    count: "1,450", pct: 80,  w: 80  },
            { stage: "Passed assessment", count: "1,068", pct: 59,  w: 59  },
            { stage: "Certificate earned",count: "812",   pct: 45,  w: 45  },
          ],
        },
        heat: {
          title: "Course heat · 2026 catalog",
          hot: "Trannovation Foundations",
          cols: ["FND","AI","BTC","AGR","SUS","EDU"],
          rows: ["Q1","Q2","Q3","Q4"],
          cells: [
            0.92,0.78,0.48,0.52,0.40,0.34,
            0.86,0.74,0.66,0.58,0.46,0.42,
            0.78,0.68,0.72,0.62,0.50,0.48,
            0.70,0.64,0.58,0.40,0.36,0.32,
          ],
        },
        donut: { used: 812, total: 1820, label: "Certificates issued", sub: "45% of enrolled learners" },
        feed: {
          title: "Live course activity",
          items: [
            { time: "12s", text: "Trannovation · Module 4 completed",           tag: "COURSE" },
            { time: "24s", text: "AI for MSMEs · assessment passed (92%)",      tag: "PASS"   },
            { time: "48s", text: "Trannovation Bootcamp · cohort 03 enrolled",  tag: "ENROL"  },
            { time: "1m",  text: "Agora Case Studies · new lesson published",    tag: "PUB"    },
            { time: "2m",  text: "Certificate · CPM track issued",                tag: "CERT"   },
            { time: "3m",  text: "Sustainability Credo · learner rated 5/5",     tag: "NPS"    },
          ],
        },
        alert: {
          title: "Trannovation Bootcamp · 18:00 cohort live",
          sub: "146 learners waiting · queue Q&A and recordings",
          cta: "Open cohort",
        },
      },
    },
  },
  bench: {
    key: "bench",
    label: "BENCH/ Charged",
    scenes: [
      { key: "ticket", eyebrow: "01 — BENCH/ Active Pass",   title: "Start with one BENCH/ Active Pass for every guest, athlete, and partner.",                       body: "The pass scans guests in, opens the credit ledger, weights every raffle entry, gates partner consent, and survives Phase 2 public and Phase 3 Fun Run on the same record." },
      { key: "map",    eyebrow: "02 — Wellness floor",        title: "Turn the BENCH/ Charged floor into a live wellness guide.",                                     body: "Guests see which stations are open, queue lengths at the Skillmill and Cold Plunge, the partner booths giving credits, and which content moments are next — all from the pass." },
      { key: "scan",   eyebrow: "03 — Station scan",          title: "Every scan logs a station credit, a partner action, or a content capture.",                     body: "Cold Plunge timer, Skillmill coach scan, Concept2 rower auto-read, partner-booth QR, content-station capture — one scanner, six validation types, one credit feed." },
      { key: "dash",   eyebrow: "04 — Live ops",              title: "BENCH/ Charged gets one live operating console.",                                                body: "Active Pass issuance, credits per hour, raffle weight live, station heat, partner claim rate, helpdesk queue, raffle-screen takeover — all in one console behind the floor." },
    ],
    trio: {
      eyebrow: "02 — Wellness floor",
      title: "One floor view for stations, queues, partners, and the next content moment.",
      body: "Guests see the Cold Plunge wait, the Skillmill leaderboard, the partner booth that's issuing credits right now, and the Content Station capture window — without leaving the BENCH/ Active Pass.",
    },
    ticket: {
      appHeadPass: "Active Pass",
      appHeadRegister: "Register",
      email: "guest@bench-charged.ph",
      eventKicker: "XTAG STUDIO · BENCH/ CHARGED",
      eventName: "BENCH/ Charged · 2026 Pilot",
      eventDate: "TBD · Arena floor · Manila",
      cta: "Claim Active Pass",
      verifying: "Verifying invite…",
      foot: "Active Pass ready · credits unlocked · raffle live",
      img: "/assets/bench/generated/hero-pass-card.png?v=20260521cc",
      alt: "BENCH/ Active Pass — guest credential reference",
      passLogoSrc: "/assets/bench/logos/prospect-logo.png",
      passLogoAlt: "BENCH/ slash logo overlay",
      registerBanner: "/assets/bench/generated/section-banner.png",
      registerBannerAlt: "BENCH/ Charged wellness floor banner",
      toastMark: "B",
      toastTitle: "BENCH/ Charged",
      toastMsg: "Active Pass ready",
    },
    map: {
      mapSrc: "/assets/bench/generated/hero-map-overview.png",
      thumbsSrc: "/assets/bench/generated/hero-calendar-thumbs.png",
      peopleSrc: "/assets/bench/generated/hero-people-avatars.png",
      mapAlt: "BENCH/ Charged wellness floor overhead reference",
      views: ["map", "agenda", "people"],
      tabs: { map: "Floor", agenda: "Stations", people: "Crew" },
      headings: { map: "Floor", agenda: "Stations", people: "Crew" },
      mapSubheading: (visited, total) => `${visited}/${total} STATIONS DONE`,
      agendaSubheading: "PILOT · ARENA FLOOR",
      peopleSubheading: "8 SEGMENTS COORDINATING",
      venues: [
        { x: 28, y: 14, lbl: "Cold Plunge" },
        { x: 12, y: 30, lbl: "Skillmill bay" },
        { x: 74, y: 22, lbl: "Concept2 row" },
        { x: 80, y: 48, lbl: "Twister floor" },
        { x: 24, y: 70, lbl: "Partner booths" },
        { x: 60, y: 74, lbl: "Content station" },
      ],
      booths: [
        { x: 44, y: 38, lbl: "Technogym" },
        { x: 62, y: 42, lbl: "Hidilyn Academy" },
        { x: 52, y: 58, lbl: "Recovery" },
      ],
      youHere: "You · Reception desk",
      cardIcon: "B",
      cardIconBg: "#0E0E0E",
      cardTitle: "Skillmill · 60-sec sprint",
      cardSub: "Coach-validated · live leaderboard · 6 ahead",
      cardCta: "Open station →",
      totalVisited: 6,
      progressLabel: "Stations cleared",
      remainingLabel: "stations until raffle bump",
      claimReady: "Open raffle weight →",
      claimIcon: "B",
      streamViews: "BENCH/ Charged live",
      streamTitle: "Cold Plunge headline session",
      streamSub: "BENCH/ Charged · floor live",
      days: [
        { day: "P1", label: "PHASE 1 · PILOT" },
        { day: "P2", label: "PHASE 2 · WEEKEND" },
        { day: "P3", label: "PHASE 3 · FUN RUN" },
      ],
      sessions: [
        { time: "16:00", title: "Doors + BENCH/ Active Pass pickup",       loc: "Reception",      spk: "Ops desk",      tag: "OPEN", thumb: 0 },
        { time: "17:00", title: "Cold Plunge headline session",             loc: "Recovery zone",  spk: "Ice Bath Manila", thumb: 1 },
        { time: "17:45", title: "Skillmill 60-second sprint heats",         loc: "Speed bay",      spk: "Technogym",     thumb: 2 },
        { time: "18:30", title: "Concept2 rower meter challenge",           loc: "Endurance bay",  spk: "Concept2",      thumb: 3 },
        { time: "19:15", title: "Partner booth + content stations open",    loc: "Sponsor row",    spk: "Premier partners", thumb: 4 },
        { time: "20:30", title: "Cinematic raffle finale + winner reveal",  loc: "Main stage",     spk: "BENCH/ host",   thumb: 5 },
      ],
      availability: "8 audience segments to coordinate",
      attending: "Guests, athletes, ambassadors, partners, content crew, ops",
      people: [
        { name: "BENCH/ Ambassador",   role: "Headline endorser",     company: "BENCH/active",         pos: 0, mutual: 8, tag: "Ambassador", bio: "Drives floor attention, anchors the raffle finale, and gets the post-event social cut.",         msgFrom: "When am I on stage?",        msgReply: "Pass shows the call-time card 20 minutes ahead." },
        { name: "Recreational athlete",role: "Skillmill/Row crowd",   company: "BENCH/ Charged guest", pos: 1, mutual: 6, tag: "Athlete",    bio: "Targets the Skillmill leaderboard and Concept2 meter challenge; tracks credits live.",            msgFrom: "Where do I queue?",          msgReply: "The floor map shows live queue length." },
        { name: "Cold Plunge guest",   role: "Recovery seeker",       company: "BENCH/ Charged guest", pos: 2, mutual: 5, tag: "Recovery",   bio: "Times the 3–7 minute hold, validates with station staff, banks Recovery merit.",                  msgFrom: "How long is the wait?",      msgReply: "Two ahead · 9 minutes." },
        { name: "Social crew",         role: "Group of friends",      company: "Twister + content",    pos: 3, mutual: 4, tag: "Social",     bio: "Plays the Giant Twister floor, hits the content station, posts the moment for raffle weight.",   msgFrom: "Where's the content booth?", msgReply: "Floor pin · 30 seconds north." },
        { name: "Premier sponsor",     role: "Brand activation lead", company: "Title sponsor",        pos: 4, mutual: 7, tag: "Sponsor",    bio: "Watches the dashboard tile for credits issued, scans, and claim rate — renewal proof in real time.", msgFrom: "Can we see scoreboard?",     msgReply: "Console tile is open for your team." },
        { name: "Station partner",     role: "Equipment / wellness",  company: "Technogym / Concept2", pos: 5, mutual: 5, tag: "Partner",    bio: "Validates station completions, streams the leaderboard to their own channels.",                    msgFrom: "Can we stream it?",          msgReply: "One-click broadcast to your channel." },
        { name: "Content crew",        role: "Photo + video",         company: "BENCH/ content",       pos: 6, mutual: 3, tag: "Content",    bio: "Captures station moments, drops to guest passes, feeds the post-event reel.",                       msgFrom: "Which station is hot?",      msgReply: "Skillmill is peaking now." },
        { name: "Ops crew",            role: "Floor + helpdesk",      company: "BENCH/ + XTAG",        pos: 7, mutual: 6, tag: "Ops",        bio: "Runs check-in, reprints, walk-ins, station swaps, and the raffle screen takeover.",                msgFrom: "Helpdesk queue?",            msgReply: "Two in queue · 3-minute average." },
      ],
      socialCard: { icon: "B", title: "Skillmill 60-sec sprint", sub: "Speed bay · coach-validated · live", cta: "Open" },
    },
    featureViews: ["registerPass", "stationCredits", "liveRaffle", "partnerScan", "contentMoment"],
    features: {
      registerPass: {
        type: "checkins",
        appTitle: "Register",
        kicker: "ENTRY + BENCH/ ACTIVE PASS",
        title: "One QR. One BENCH/ Active Pass. No app install.",
        sub: "Guests scan the door QR, claim a pass, and enter the credit + raffle ecosystem before they touch a station.",
        description: "The PWA holds the pass on the phone — Apple Wallet and Google Wallet add-on optional. Consent and partner opt-ins are explicit on the pass.",
        bullets: ["Door-QR registration · 30-second form.", "Consent + partner opt-in matrix.", "Active Pass on the phone or wallet, no install."],
        primary: "4/5",
        primaryLabel: "register steps",
        progress: 80,
        tabs: ["GUEST", "ATHLETE", "PARTNER"],
        rows: [
          { label: "Verify mobile + email", meta: "OTP",                status: "DONE" },
          { label: "Choose segment",        meta: "guest, athlete, partner", status: "DONE" },
          { label: "Issue Active Pass",     meta: "phone or wallet",    status: "OPEN" },
          { label: "Scan in at door",       meta: "kiosk lane",         status: "LOCKED" },
        ],
      },
      stationCredits: {
        type: "puzzle",
        appTitle: "Credits",
        kicker: "STATION LEDGER",
        title: "Every station credit, every validation, on one ledger.",
        sub: "Cold Plunge, Skillmill, Rower, Twister, Partner, Content — credits drop the moment validation passes.",
        description: "Each track has its own validation method (timer, coach scan, equipment auto-read, QR). The ledger tags every credit with the station, partner, and timestamp.",
        bullets: ["Six tracks, six validation types.", "Per-station merit tags (Recovery, Speed, Endurance, Social, Sponsor, Content).", "Audit-ready ledger for sponsor exports."],
        primary: "5/6",
        primaryLabel: "tracks cleared",
        progress: 83,
        code: ["B", "E", "N", "C", "H", "+", "/"],
        rows: ["Cold Plunge · +200", "Skillmill · +180", "Concept2 · +160"],
      },
      liveRaffle: {
        type: "raffle",
        appTitle: "Raffle",
        kicker: "WEIGHTED LIVE RAFFLE",
        title: "Credits scale the raffle weight in real time.",
        sub: "Spin wheel, countdowns, confetti, winner reveal — and a fair, transparent draw that rewards the harder stations.",
        description: "Raffle screen runs on the venue LED. Guest's pocket shows their live draw weight. Operator triggers the next reveal from the console.",
        bullets: ["Weighted by credits, capped by tier.", "Cinematic LED reveal + content capture.", "Audit log for legal compliance."],
        primary: "1.84×",
        primaryLabel: "current weight",
        progress: 92,
        progressLabel: "raffle pool engagement",
        cta: "Open raffle screen",
        stats: [
          { value: "324", label: "active pool" },
          { value: "T1",  label: "top tier" },
        ],
        rows: ["Tier 1 prize · BENCH/active 12-month", "Tier 2 prize · Technogym home kit", "Tier 3 prize · Cold Plunge bundle"],
      },
      partnerScan: {
        type: "market",
        appTitle: "Partners",
        kicker: "SPONSOR SCOREBOARD",
        title: "Every partner sees their own tile.",
        sub: "Premier, Station, Activity, Hospitality — credits issued, scans, claim rate, audience segment, content moments.",
        description: "Partner staff scan guest passes, log notes, and stream the station leaderboard from one tile. Exports route through BENCH/ approval before they leave.",
        bullets: ["Per-partner credit count and claim rate.", "One-click leaderboard broadcast.", "Approval-gated audience export."],
        primary: "412",
        primaryLabel: "partner scans",
        rows: [
          { label: "Technogym booth",    meta: "184 scans",      status: "LIVE" },
          { label: "Cold Plunge sponsor", meta: "96 scans",       status: "LIVE" },
          { label: "Audience export",     meta: "BENCH/ approval", status: "REVIEW" },
        ],
      },
      contentMoment: {
        type: "gallery",
        appTitle: "Content",
        kicker: "CONTENT STATION",
        title: "Every station moment auto-drops into the guest's pass.",
        sub: "Photo or short-video capture from the content station, attached to the pass, ready for the post-event reel.",
        description: "Content crew runs the BENCH/-branded station. Captures drop straight to the guest, the originating partner, and the BENCH/ content reel — opt-in at registration.",
        bullets: ["One-tap auto-drop to the guest pass.", "Per-partner content reel + analytics.", "Post-event social cut, BENCH/-coded."],
        primary: "248",
        primaryLabel: "captures · live",
        imageSrc: "/assets/bench/generated/hero-calendar-thumbs.png",
        tags: ["Photo", "Reel", "BENCH/"],
      },
    },
    scan: {
      total: 6,
      startCount: 3,
      doneCount: 4,
      points: 1,
      pointsLabel: "STATION",
      rankStart: "#42",
      rankDone: "#28",
      screenTitle: "BENCH/ Scanner",
      progressTitle: "Your floor record",
      scanTitle: "Scan BENCH/ Charged code",
      scanSub: "Validate a station, log a partner action, or capture content",
      activityHeader: "Recent activity",
      activityScope: "Tonight",
      newActivity: { title: "Skillmill 60-sec sprint · coach-validated", tag: "+180 CR" },
      activities: [
        { time: "2m ago",  title: "Cold Plunge · 4:18 hold validated",       tag: "+200 CR" },
        { time: "8m ago",  title: "Technogym booth · partner scan",          tag: "+80 CR"  },
        { time: "14m ago", title: "Content station · reel captured" },
        { time: "26m ago", title: "Door check-in · Active Pass issued" },
      ],
      cameraSrc: "/assets/bench/generated/hero-scan-signage.png",
      bakedCameraOverlay: false,
      frameTop: "60%",
      cameraTitle: "Scan BENCH/ Charged code",
      recognized: "Code recognized · Skillmill sprint",
      success: "Station credit logged · +180",
      toastIcon: "B",
      toastIconBg: "#0E0E0E",
      toastTitle: "Station validated",
      toastSub: "Skillmill · +180 credits · raffle weight up",
      rewardIcon: "B",
      rewardIconBg: "#E11D2A",
      rewardTitle: "Raffle weight",
      rewardSub: "1.42× → 1.84× after this credit",
      itemLabel: "credits",
    },
    dash: {
      appTitle: "BENCH/ Ops",
      headlineLabel: "Credits issued · live",
      headlineTarget: 8400,
      headlineBase: 12000,
      headlineDecimals: 0,
      headlinePrefix: "",
      headlineSuffix: "",
      headlineDelta: "+1,420 credits last hour",
      headlineTargetText: "credit issuance toward floor target",
      secondaryTarget: 8400,
      kpis: [
        { label: "Active Passes",   value: "animated", detail: "guests on the floor"        },
        { label: "Station rate",    value: "82%",      detail: "tracks attempted vs target" },
        { label: "Raffle pool",     value: "324",      detail: "weighted entries"           },
        { label: "Sponsor scans",   value: "4.6",      detail: "avg per guest"              },
      ],
      rowsTitle: "Stations · live credits",
      rowsMeta: "CREDITS",
      rows: [
        { lbl: "Cold Plunge",       val: "2.1K", w: 92 },
        { lbl: "Skillmill sprint",  val: "1.8K", w: 78 },
        { lbl: "Concept2 row",      val: "1.4K", w: 60 },
        { lbl: "Partner booths",    val: "980",  w: 46 },
        { lbl: "Content station",   val: "640",  w: 32 },
      ],
      claimsTitle: "Sponsor scoreboard",
      claimsMeta: "CLAIM RATE",
      claims: [
        { lbl: "Premier partner",   val: "84%", w: 84 },
        { lbl: "Technogym",         val: "76%", w: 76 },
        { lbl: "Ice Bath Manila",   val: "62%", w: 62 },
        { lbl: "Hospitality",       val: "48%", w: 48 },
        { lbl: "Audience export",   val: "92%", w: 92 },
      ],
      foot: "Raffle finale · 22 minutes · 324 weighted entries · top tier prize armed",
      tablet: {
        lastUpdate: "Updated 8s ago",
        spark: {
          label: "Credits issued · last 90 minutes",
          points: [140,260,400,560,720,900,1090,1280,1470,1660,1850,2040,2230,2420,2610,2800,2990,3180,3370,3560,3750,3940,4120,4280],
          peakLabel: "8,400 credits · live",
        },
        gauge: { value: 28, max: 34, label: "Staff stations",  sub: "Reception, six tracks, content, raffle, helpdesk" },
        nps:   { value: 4.8, max: 5, label: "Guest pulse",     sub: "Live floor ratings" },
        funnel: {
          title: "Floor journey · tonight",
          stages: [
            { stage: "Registered",       count: "486", pct: 100, w: 100 },
            { stage: "Checked in",       count: "412", pct: 85,  w: 85  },
            { stage: "Station scanned",  count: "388", pct: 80,  w: 80  },
            { stage: "Partner action",   count: "284", pct: 58,  w: 58  },
          ],
        },
        heat: {
          title: "Floor heat · BENCH/ Charged",
          hot: "Skillmill bay",
          cols: ["CP","SM","RO","TW","SP","CN"],
          rows: ["16h","17h","18h","19h"],
          cells: [
            0.40,0.42,0.30,0.28,0.36,0.22,
            0.86,0.74,0.62,0.50,0.58,0.46,
            0.78,0.92,0.84,0.68,0.74,0.62,
            0.66,0.78,0.74,0.86,0.82,0.90,
          ],
        },
        donut: { used: 388, total: 486, label: "Station scans", sub: "80% of checked-in guests" },
        feed: {
          title: "Live activity",
          items: [
            { time: "12s", text: "Skillmill · coach-validated sprint",          tag: "+180" },
            { time: "24s", text: "Cold Plunge · 4:18 hold logged",              tag: "+200" },
            { time: "48s", text: "Technogym booth · partner scan",              tag: "PART" },
            { time: "1m",  text: "Content station · BENCH/ reel captured",     tag: "CAP"  },
            { time: "2m",  text: "Raffle weight · pass 1.84×",                  tag: "RFLW" },
            { time: "3m",  text: "Helpdesk · pass reprint resolved",            tag: "OPS"  },
          ],
        },
        alert: {
          title: "Skillmill bay · 92% capacity",
          sub: "Open second lane or trigger floor announcement",
          cta: "Open second lane",
        },
      },
    },
  },
  template: {
    key: "template",
    label: "Template",
    scenes: [
      { key: "ticket", eyebrow: "01 — [Pass scene eyebrow]", title: "[Pass scene title — what the prospect's mobile pass unlocks.]", body: "[Pass scene body — one sentence on how the pass connects identity, verification, and the rest of the experience.]" },
      { key: "map",    eyebrow: "02 — [Guide scene eyebrow]", title: "[Guide scene title — what the in-app home/map gives the audience.]", body: "[Guide scene body — one sentence on how attendees navigate venue, program, or community.]" },
      { key: "scan",   eyebrow: "03 — [Scan scene eyebrow]", title: "[Scan scene title — what every QR scan logs or unlocks.]", body: "[Scan scene body — one sentence on attendance, sponsor visits, rewards, or check-in.]" },
      { key: "dash",   eyebrow: "04 — [Dash scene eyebrow]", title: "[Dashboard title — what the live ops console controls.]", body: "[Dashboard body — one sentence on KPIs, capacity, alerts, and post-event exports.]" },
    ],
    trio: {
      eyebrow: "02 — [Guide scene eyebrow]",
      title: "[Trio title — one sentence that ties the three middle panels together.]",
      body: "[Trio body — what the middle column communicates when all three sub-views are visible.]",
    },
    ticket: {
      appHeadPass: "[Pass]",
      appHeadRegister: "[Register]",
      email: "[attendee@prospect.example]",
      eventKicker: "XTAG STUDIO · [PROSPECT] PROPOSAL",
      eventName: "[Event Name]",
      eventDate: "[Date range · Venue]",
      cta: "[Pass CTA]",
      verifying: "[Verifying pass…]",
      foot: "[Confirmation footer — what the pass unlocks.]",
      img: "/assets/template/generated/hero-pass-card.png",
      alt: "Placeholder member pass card · slot: hero.passCard",
      passLogoSrc: "/assets/template/logos/prospect-logo.png",
      passLogoAlt: "Placeholder prospect logo · slot: logo.prospect",
      registerBanner: "/assets/template/generated/section-banner.png",
      registerBannerAlt: "Placeholder banner · slot: section.banner",
      toastMark: "T",
      toastTitle: "[Prospect]",
      toastMsg: "[Pass ready · short status]",
    },
    map: {
      mapSrc: "/assets/template/generated/hero-map-overview.png",
      thumbsSrc: "/assets/template/generated/hero-calendar-thumbs.png",
      peopleSrc: "/assets/template/generated/hero-people-avatars.png",
      mapAlt: "Placeholder map · slot: hero.mapOverview",
      views: ["map", "agenda", "people"],
      tabs: { map: "Map", agenda: "Schedule", people: "Community" },
      headings: { map: "[Map heading]", agenda: "[Schedule heading]", people: "[Community heading]" },
      mapSubheading: (visited, total) => `${visited}/${total} CHECKED`,
      agendaSubheading: "[Schedule subheading]",
      peopleSubheading: "[Community subheading]",
      venues: [
        { x: 28, y: 14, lbl: "[Zone 1]" },
        { x: 12, y: 30, lbl: "[Zone 2]" },
        { x: 74, y: 22, lbl: "[Zone 3]" },
        { x: 80, y: 48, lbl: "[Zone 4]" },
        { x: 24, y: 70, lbl: "[Zone 5]" },
        { x: 60, y: 74, lbl: "[Zone 6]" },
      ],
      booths: [
        { x: 44, y: 38, lbl: "[Booth 1]" },
        { x: 62, y: 42, lbl: "[Booth 2]" },
        { x: 52, y: 58, lbl: "[Booth 3]" },
      ],
      youHere: "You · [Audience starting point]",
      cardIcon: "T",
      cardIconBg: "#A33D2B",
      cardTitle: "[Map card title]",
      cardSub: "[Map card subtitle]",
      cardCta: "[Map card CTA] ->",
      totalVisited: 6,
      progressLabel: "[Progress label]",
      remainingLabel: "[Remaining label]",
      claimReady: "[Claim CTA] ->",
      claimIcon: "T",
      streamViews: "[Live stream label]",
      streamTitle: "[Live stream title]",
      streamSub: "[Live stream subtitle]",
      days: [
        { day: "[D1]", label: "[DAY 1 LABEL]" },
        { day: "[D2]", label: "[DAY 2 LABEL]" },
        { day: "[D3]", label: "[DAY 3 LABEL]" },
      ],
      sessions: [
        { time: "[T1]", title: "[Session 1 title]", loc: "[Location 1]", spk: "[Speaker 1]", tag: "OPEN", thumb: 0 },
        { time: "[T2]", title: "[Session 2 title]", loc: "[Location 2]", spk: "[Speaker 2]", thumb: 1 },
        { time: "[T3]", title: "[Session 3 title]", loc: "[Location 3]", spk: "[Speaker 3]", thumb: 2 },
        { time: "[T4]", title: "[Session 4 title]", loc: "[Location 4]", spk: "[Speaker 4]", thumb: 3 },
        { time: "[T5]", title: "[Session 5 title]", loc: "[Location 5]", spk: "[Speaker 5]", thumb: 4 },
        { time: "[T6]", title: "[Session 6 title]", loc: "[Location 6]", spk: "[Speaker 6]", thumb: 5 },
      ],
      availability: "[Audience availability line]",
      attending: "[Audience segments]",
      people: [
        { name: "[Person 1]", role: "[Role 1]", company: "[Company 1]", pos: 0, mutual: 8, tag: "[Tag1]", bio: "[Person 1 bio.]", msgFrom: "[Q1]", msgReply: "[A1]" },
        { name: "[Person 2]", role: "[Role 2]", company: "[Company 2]", pos: 1, mutual: 6, tag: "[Tag2]", bio: "[Person 2 bio.]", msgFrom: "[Q2]", msgReply: "[A2]" },
        { name: "[Person 3]", role: "[Role 3]", company: "[Company 3]", pos: 2, mutual: 5, tag: "[Tag3]", bio: "[Person 3 bio.]", msgFrom: "[Q3]", msgReply: "[A3]" },
        { name: "[Person 4]", role: "[Role 4]", company: "[Company 4]", pos: 3, mutual: 4, tag: "[Tag4]", bio: "[Person 4 bio.]", msgFrom: "[Q4]", msgReply: "[A4]" },
        { name: "[Person 5]", role: "[Role 5]", company: "[Company 5]", pos: 4, mutual: 5, tag: "[Tag5]", bio: "[Person 5 bio.]", msgFrom: "[Q5]", msgReply: "[A5]" },
        { name: "[Person 6]", role: "[Role 6]", company: "[Company 6]", pos: 5, mutual: 7, tag: "[Tag6]", bio: "[Person 6 bio.]", msgFrom: "[Q6]", msgReply: "[A6]" },
        { name: "[Person 7]", role: "[Role 7]", company: "[Company 7]", pos: 6, mutual: 3, tag: "[Tag7]", bio: "[Person 7 bio.]", msgFrom: "[Q7]", msgReply: "[A7]" },
        { name: "[Person 8]", role: "[Role 8]", company: "[Company 8]", pos: 7, mutual: 6, tag: "[Tag8]", bio: "[Person 8 bio.]", msgFrom: "[Q8]", msgReply: "[A8]" },
      ],
      socialCard: { icon: "T", title: "[Social card title]", sub: "[Social card sub]", cta: "[Open]" },
    },
    featureViews: ["screenOne", "screenTwo", "screenThree", "screenFour", "screenFive"],
    features: {
      screenOne: {
        type: "puzzle",
        appTitle: "[Screen 1]",
        kicker: "[SCREEN 1 KICKER]",
        title: "[Screen 1 title — what this surface does.]",
        sub: "[Screen 1 sub — one sentence on audience and outcome.]",
        description: "[Screen 1 description — one sentence on how attendees use it.]",
        bullets: ["[Bullet 1.]", "[Bullet 2.]", "[Bullet 3.]"],
        primary: "62%",
        primaryLabel: "[primary label]",
        progress: 62,
        code: ["T", "E", "M", "P", "L", "A", "T"],
        rows: ["[Row 1]", "[Row 2]", "[Row 3]"],
      },
      screenTwo: {
        type: "market",
        appTitle: "[Screen 2]",
        kicker: "[SCREEN 2 KICKER]",
        title: "[Screen 2 title — what this surface does.]",
        sub: "[Screen 2 sub — one sentence on audience and outcome.]",
        description: "[Screen 2 description.]",
        bullets: ["[Bullet 1.]", "[Bullet 2.]", "[Bullet 3.]"],
        primary: "184",
        primaryLabel: "[primary label]",
        rows: [
          { label: "[Row 1 label]", meta: "[meta]", status: "LIVE" },
          { label: "[Row 2 label]", meta: "[meta]", status: "LIVE" },
          { label: "[Row 3 label]", meta: "[meta]", status: "REVIEW" },
        ],
      },
      screenThree: {
        type: "checkins",
        appTitle: "[Screen 3]",
        kicker: "[SCREEN 3 KICKER]",
        title: "[Screen 3 title.]",
        sub: "[Screen 3 sub.]",
        description: "[Screen 3 description.]",
        bullets: ["[Bullet 1.]", "[Bullet 2.]", "[Bullet 3.]"],
        primary: "1,820",
        primaryLabel: "[primary label]",
        progress: 78,
        tabs: ["[TAB 1]", "[TAB 2]", "[TAB 3]"],
        rows: [
          { label: "[Row 1 label]", meta: "[meta]", status: "OPEN" },
          { label: "[Row 2 label]", meta: "[meta]", status: "DONE" },
          { label: "[Row 3 label]", meta: "[meta]", status: "REVIEW" },
        ],
      },
      screenFour: {
        type: "raffle",
        appTitle: "[Screen 4]",
        kicker: "[SCREEN 4 KICKER]",
        title: "[Screen 4 title.]",
        sub: "[Screen 4 sub.]",
        description: "[Screen 4 description.]",
        bullets: ["[Bullet 1.]", "[Bullet 2.]", "[Bullet 3.]"],
        primary: "44",
        primaryLabel: "[primary label]",
        progress: 90,
        progressLabel: "[progress label]",
        cta: "[Screen 4 CTA]",
        stats: [
          { value: "33", label: "[stat 1]" },
          { value: "[D]", label: "[stat 2]" },
        ],
        rows: ["[Row 1]", "[Row 2]", "[Row 3]"],
      },
      screenFive: {
        type: "market",
        appTitle: "[Screen 5]",
        kicker: "[SCREEN 5 KICKER]",
        title: "[Screen 5 title.]",
        sub: "[Screen 5 sub.]",
        description: "[Screen 5 description.]",
        bullets: ["[Bullet 1.]", "[Bullet 2.]", "[Bullet 3.]"],
        primary: "1,248",
        primaryLabel: "[primary label]",
        rows: [
          { label: "[Row 1 label]", meta: "[meta]", status: "LIVE" },
          { label: "[Row 2 label]", meta: "[meta]", status: "LIVE" },
          { label: "[Row 3 label]", meta: "[meta]", status: "LOCKED" },
        ],
      },
    },
    scan: {
      total: 6,
      startCount: 3,
      doneCount: 4,
      points: 1,
      pointsLabel: "[ACTION]",
      rankStart: "#218",
      rankDone: "#214",
      screenTitle: "[Prospect] Scanner",
      progressTitle: "[Progress block title]",
      scanTitle: "Scan [Prospect] code",
      scanSub: "[Scan subtitle — what the scan does for the attendee.]",
      activityHeader: "Recent activity",
      activityScope: "Today",
      newActivity: { title: "[New activity title]", tag: "+1 ACTION" },
      activities: [
        { time: "3m ago",  title: "[Activity 1 title]", tag: "TAG" },
        { time: "14m ago", title: "[Activity 2 title]", tag: "TAG" },
        { time: "22m ago", title: "[Activity 3 title]" },
        { time: "39m ago", title: "[Activity 4 title]" },
      ],
      cameraSrc: "/assets/template/generated/hero-scan-signage.png",
      bakedCameraOverlay: false,
      frameTop: "74%",
      cameraTitle: "Scan [Prospect] code",
      recognized: "[Code recognized message]",
      success: "[Success message]",
      toastIcon: "T",
      toastIconBg: "#A33D2B",
      toastTitle: "[Action logged]",
      toastSub: "[Action sub-message]",
      rewardIcon: "C",
      rewardIconBg: "#1B1B1B",
      rewardTitle: "[Reward title]",
      rewardSub: "[Reward subtitle]",
      itemLabel: "actions",
    },
    dash: {
      appTitle: "[Prospect] Ops",
      headlineLabel: "[Headline KPI label] · live",
      headlineTarget: 1450,
      headlineBase: 1820,
      headlineDecimals: 0,
      headlinePrefix: "",
      headlineSuffix: "",
      headlineDelta: "+186 [primary unit] this week",
      headlineTargetText: "[headline target descriptor]",
      secondaryTarget: 1450,
      kpis: [
        { label: "[KPI 1]", value: "animated", detail: "[KPI 1 detail]" },
        { label: "[KPI 2]", value: "62%",      detail: "[KPI 2 detail]" },
        { label: "[KPI 3]", value: "812",      detail: "[KPI 3 detail]" },
        { label: "[KPI 4]", value: "4.8",      detail: "[KPI 4 detail]" },
      ],
      rowsTitle: "[Rows title]",
      rowsMeta: "[ROWS META]",
      rows: [
        { lbl: "[Row 1 label]", val: "412", w: 92 },
        { lbl: "[Row 2 label]", val: "318", w: 78 },
        { lbl: "[Row 3 label]", val: "246", w: 62 },
        { lbl: "[Row 4 label]", val: "188", w: 46 },
        { lbl: "[Row 5 label]", val: "146", w: 38 },
      ],
      claimsTitle: "[Claims title]",
      claimsMeta: "[CLAIMS META]",
      claims: [
        { lbl: "[Claim 1 label]", val: "82%", w: 82 },
        { lbl: "[Claim 2 label]", val: "74%", w: 74 },
        { lbl: "[Claim 3 label]", val: "58%", w: 58 },
        { lbl: "[Claim 4 label]", val: "46%", w: 46 },
        { lbl: "[Claim 5 label]", val: "92%", w: 92 },
      ],
      foot: "[Dashboard footer — one summary sentence.]",
      tablet: {
        lastUpdate: "Updated 8s ago",
        spark: {
          label: "[Spark chart label · last 90 minutes]",
          points: [60,110,170,220,290,360,440,520,600,680,750,820,880,940,1000,1060,1120,1180,1240,1300,1360,1400,1430,1450],
          peakLabel: "[Spark peak label]",
        },
        gauge: { value: 24, max: 34, label: "[Gauge label]", sub: "[Gauge sub]" },
        nps:   { value: 4.8, max: 5, label: "[NPS label]", sub: "[NPS sub]" },
        funnel: {
          title: "[Funnel title · year]",
          stages: [
            { stage: "[Stage 1]", count: "1,820", pct: 100, w: 100 },
            { stage: "[Stage 2]", count: "1,450", pct: 80,  w: 80  },
            { stage: "[Stage 3]", count: "1,068", pct: 59,  w: 59  },
            { stage: "[Stage 4]", count: "812",   pct: 45,  w: 45  },
          ],
        },
        heat: {
          title: "[Heatmap title · year]",
          hot: "[Hot cell label]",
          cols: ["A","B","C","D","E","F"],
          rows: ["Q1","Q2","Q3","Q4"],
          cells: [
            0.92,0.78,0.48,0.52,0.40,0.34,
            0.86,0.74,0.66,0.58,0.46,0.42,
            0.78,0.68,0.72,0.62,0.50,0.48,
            0.70,0.64,0.58,0.40,0.36,0.32,
          ],
        },
        donut: { used: 812, total: 1820, label: "[Donut label]", sub: "[Donut sub]" },
        feed: {
          title: "[Live feed title]",
          items: [
            { time: "12s", text: "[Feed item 1]", tag: "TAG" },
            { time: "24s", text: "[Feed item 2]", tag: "TAG" },
            { time: "48s", text: "[Feed item 3]", tag: "TAG" },
            { time: "1m",  text: "[Feed item 4]", tag: "TAG" },
            { time: "2m",  text: "[Feed item 5]", tag: "TAG" },
            { time: "3m",  text: "[Feed item 6]", tag: "TAG" },
          ],
        },
        alert: {
          title: "[Alert title]",
          sub: "[Alert subtitle]",
          cta: "[Alert CTA]",
        },
      },
    },
  },
};

const DEFAULT_HERO_VERTICAL_KEYS = ["corporate", "sports", "tourism", "weddings"];
const HERO_VERTICAL_OPTIONS = DEFAULT_HERO_VERTICAL_KEYS
  .map((key) => ({ key, label: HERO_VERTICALS[key].label }));

function heroTagClass(tag) {
  return "tag-" + String(tag).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}

function formatHeroNumber(value, decimals) {
  return Number(value).toLocaleString(undefined, {
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals,
  });
}

function HeroTrioOverlay({ active, vertical, className = "" }) {
  const wrapRef = useRH3(null);
  const mapViews = getHeroMapViewKeys(vertical);
  const mapViewSig = mapViews.join("|");

  useEH3(() => {
    if (!active || !className.includes("mobile")) return;

    const wrap = wrapRef.current;
    const centerPhone = wrap && wrap.children[Math.floor(mapViews.length / 2)];
    if (!wrap || !centerPhone) return;

    const centerScroll =
      centerPhone.offsetLeft - (wrap.clientWidth - centerPhone.clientWidth) / 2;
    requestAnimationFrame(() => {
      wrap.scrollTo({ left: centerScroll, behavior: "auto" });
    });
  }, [active, vertical.key, className, mapViewSig]);

  return (
    <div className={"hero3d-trio-overlay " + className + (active ? " in" : "")} aria-hidden={!active}>
      <div className="trio-header">
        <span className="eyebrow" style={{color:"var(--ember-400)"}}>{vertical.trio.eyebrow}</span>
        <h2>{vertical.trio.title}</h2>
        <p className="trio-lede">{vertical.trio.body}</p>
      </div>
      <div className="phone-trio-wrap" ref={wrapRef}>
        {mapViews.map((view) => (
          <div key={view} className={"trio-phone trio-phone-" + view}>
            <div className="trio-phone-inner">
              <div className="hero3d-phone-shell">
                <div className="hero3d-phone-screen">
                  <div className="dp-notch"></div>
                  <div className="dp-bar">
                    <span>9:41</span>
                    <span style={{display:"flex",gap:5,alignItems:"center"}}>
                      <svg width="14" height="10" viewBox="0 0 14 10" fill="currentColor"><rect x="0" y="6" width="2" height="4" rx="1"/><rect x="3" y="4" width="2" height="6" rx="1"/><rect x="6" y="2" width="2" height="8" rx="1"/><rect x="9" y="0" width="2" height="10" rx="1"/></svg>
                      <span style={{fontSize:11,fontWeight:600}}>100</span>
                    </span>
                  </div>
                  <Scene2Map active={active} forceView={view} vertical={vertical}/>
                </div>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function HeroPhone3D({
  initialVertical = "corporate",
  verticalKeys,
  showVerticalSelectors = true,
  navLinks = [
    { lbl: "Approach", href: "#problem", active: true },
    { lbl: "Case studies", href: "#proof" },
    { lbl: "Contact", href: "#cta" },
  ],
  ctaLabel = "Start a conversation",
  ctaHref = "#cta",
  heroHeading,
  heroLede,
} = {}) {
  const verticalOptionKeys = (Array.isArray(verticalKeys) && verticalKeys.length ? verticalKeys : DEFAULT_HERO_VERTICAL_KEYS)
    .filter((key) => HERO_VERTICALS[key]);
  const verticalOptions = verticalOptionKeys.map((key) => ({ key, label: HERO_VERTICALS[key].label }));
  const optionKeySig = verticalOptionKeys.join("|");
  const initialKey = HERO_VERTICALS[initialVertical]
    ? initialVertical
    : verticalOptionKeys[0] || "corporate";
  const [active, setActive] = useSH3(0);
  const [selectedVertical, setSelectedVertical] = useSH3(initialKey);
  const [verticalLocked, setVerticalLocked] = useSH3(false);
  const [heroVisible, setHeroVisible] = useSH3(true);
  const sectionRef = useRH3(null);
  const phoneRef = useRH3(null);
  const panelRefs = useRH3([]);
  const vertical = HERO_VERTICALS[selectedVertical]
    || HERO_VERTICALS[verticalOptionKeys[0]]
    || HERO_VERTICALS.corporate;
  const defaultHeroHeading = (
    <>
      Integrate digital <span className="hl">passports
        <svg className="squiggle" viewBox="0 0 200 12" preserveAspectRatio="none"><path d="M2 8 Q60 -2 120 4 T 198 6" stroke="#EE5A24" strokeWidth="3" strokeLinecap="round" fill="none"/></svg>
      </span> to your events.
    </>
  );
  const defaultHeroLede = "From frictionless check-in to gamified engagement and real-time sponsor analytics — a digital passport gives every attendee an interactive experience and gives your team the live controls to run a smoother, more measurable event.";

  // Auto-cycle the vertical on the hero panel: while the user is still on
  // scene 0 and hasn't manually picked a vertical, advance to the next
  // vertical (corporate → sports → tourism → weddings → corporate …).
  //
  // Cycle timing is locked to one full Scene-1 ticket animation pass so
  // every vertical gets its own register → verifying → pass run-through
  // before the next swap:
  //   • Scene-1 ticket animation duration = 3.0s  (typing → verifying → pass)
  //   • Settle / read time after the pass renders = 2.5s
  //   • → cycle interval = 5500ms (and same delay for the first cycle so
  //     the corporate ticket also gets its full play before sports lands).
  //
  // Stops the moment the user scrolls away or clicks one of the vertical pills.
  const HERO_CYCLE_MS = 5500;
  useEH3(() => {
    if (active !== 0 || verticalLocked || !showVerticalSelectors || verticalOptionKeys.length <= 1) return;
    let intervalId = null;
    const startId = setTimeout(() => {
      intervalId = setInterval(() => {
        setSelectedVertical((prev) => {
          const i = verticalOptionKeys.findIndex((key) => key === prev);
          const next = (i + 1) % verticalOptionKeys.length;
          return verticalOptionKeys[next];
        });
      }, HERO_CYCLE_MS);
    }, HERO_CYCLE_MS);
    return () => {
      clearTimeout(startId);
      if (intervalId) clearInterval(intervalId);
    };
  }, [active, verticalLocked, showVerticalSelectors, optionKeySig]);

  useEH3(() => {
    function onScroll() {
      // Determine active scene by which panel is closest to viewport center
      const compact = window.matchMedia("(max-width: 980px)").matches;
      const center = window.innerHeight * (compact ? 0.80 : 0.5);
      let best = 0; let bestDist = Infinity;
      panelRefs.current.forEach((p, i) => {
        if (!p) return;
        const r = p.getBoundingClientRect();
        const dist = Math.abs((r.top + r.height / 2) - center);
        if (dist < bestDist) { bestDist = dist; best = i; }
      });
      setActive(best);

      // Parallax: phone drifts downward as you scroll, clamped so it
      // never escapes the sticky frame.
      const sec = sectionRef.current;
      const ph = phoneRef.current;
      if (sec) {
        const rect = sec.getBoundingClientRect();
        if (ph) {
          if (compact) {
            ph.style.transform = "";
          } else {
            const total = Math.max(1, rect.height - window.innerHeight);
            const past = Math.min(Math.max(-rect.top, 0), total);
            const progress = past / total;
            const drift = Math.min(80, progress * 90);
            ph.style.transform = `translateY(${drift}px)`;
          }
        }

        // Vertical-rail visibility: only show while a meaningful chunk of
        // the hero is on screen (≥120px overlap with the viewport).
        const inView = rect.bottom > 120 && rect.top < window.innerHeight - 120;
        setHeroVisible(inView);
      }
    }
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  return (
    <section className={"hero3d hero3d-theme-" + vertical.key} ref={sectionRef} id="top" data-screen-label="01 Hero">
      <div className="hero3d-bg">
        <div className="h3-wedge"></div>
        <div className="h3-wedge h3-wedge-2"></div>
        <div className="h3-grid"></div>
      </div>

      <nav className="hero3d-nav">
        <a href="#top" className="brand" style={{textDecoration:"none"}}>
          <svg width="22" height="22" viewBox="0 0 32 32">
            <path d="M6 6 L18 18" stroke="#EE5A24" strokeWidth="3" strokeLinecap="round"/>
            <path d="M22 6 L26 10" stroke="#EE5A24" strokeWidth="3" strokeLinecap="round"/>
            <path d="M6 22 L10 26" stroke="#fff" strokeWidth="3" strokeLinecap="round"/>
            <path d="M22 22 L14 14" stroke="#fff" strokeWidth="3" strokeLinecap="round"/>
          </svg>
          <span>XTAG Studio</span>
        </a>
        <div className="links">
          {navLinks.map((link, i) => (
            <a key={link.lbl + link.href} href={link.href} className={link.active || i === 0 ? "active" : ""}>{link.lbl}</a>
          ))}
        </div>
        <a href={ctaHref} className="btn btn-primary" style={{textDecoration:"none"}}>{ctaLabel}</a>
      </nav>

      {/* Persistent demo-vertical selector — vertical pill rail anchored
          to the right edge of the viewport, visible (and clickable) across
          every hero scene including scene 2 (z:25 sits above the trio
          overlay z:20). Hides when the hero has scrolled out of view. */}
      {showVerticalSelectors && verticalOptions.length > 1 && <div className={"hero3d-vertical-bar" + (heroVisible ? "" : " is-hidden")}>
        <span className="hvb-label">Demo</span>
        <div className="hero3d-vertical-selector" role="group" aria-label="Choose demo vertical">
          {verticalOptions.map((option) => (
            <button
              key={option.key}
              type="button"
              className={selectedVertical === option.key ? "active" : ""}
              aria-pressed={selectedVertical === option.key}
              onClick={() => { setSelectedVertical(option.key); setVerticalLocked(true); }}
            >
              {option.label}
            </button>
          ))}
        </div>
      </div>}

      {/* Full-width Scene 2 overlay — fixed to viewport, covers the entire
          hero area when active. Holds the trio header + 3 mini phones so they
          read across the full viewport width, not constrained to the phone
          column. */}
      <HeroTrioOverlay active={active === 1} vertical={vertical} className="hero3d-trio-overlay-desktop" />
      {/* Full-width Scene 4 overlay — iPad admin dashboard centerpiece +
          companion phone tucked alongside. Same fixed-viewport pattern as
          the trio overlay above. Gated on heroVisible so it doesn't stay
          glued to the viewport once the user has scrolled past the hero
          (active stays at 3 — the last panel — once you exit downward). */}
      <HeroDashOverlay active={active === 3 && heroVisible} vertical={vertical} className="hero3d-dash-overlay-desktop" />

      <div className="hero3d-flow">
        {/* Phone column — single phone (scenes 0/2); flips out for scenes
            1 and 3 because the trio + dash overlays above take over the
            full viewport. */}
        <div className="hero3d-phone-col">
          <div className="hero3d-phone-pos">
            <HeroTrioOverlay active={active === 1} vertical={vertical} className="hero3d-trio-overlay-mobile" />
            <HeroDashOverlay active={active === 3 && heroVisible} vertical={vertical} className="hero3d-dash-overlay-mobile" />
            <div className={"phone-single-wrap"
              + (active === 1 ? " is-out is-out-trio" : "")
              + (active === 3 ? " is-out is-out-dash" : "")}>
              <div className="hero3d-phone" ref={phoneRef}>
                <div className="hero3d-phone-shell">
                  <div className="hero3d-phone-screen">
                    <div className="dp-notch"></div>
                    <div className="dp-bar">
                      <span>9:41</span>
                      <span style={{display:"flex",gap:5,alignItems:"center"}}>
                        <svg width="14" height="10" viewBox="0 0 14 10" fill="currentColor"><rect x="0" y="6" width="2" height="4" rx="1"/><rect x="3" y="4" width="2" height="6" rx="1"/><rect x="6" y="2" width="2" height="8" rx="1"/><rect x="9" y="0" width="2" height="10" rx="1"/></svg>
                        <span style={{fontSize:11,fontWeight:600}}>100</span>
                      </span>
                    </div>
                    <Scene1Ticket  active={active === 0} vertical={vertical}/>
                    <Scene3Scan    active={active === 2} vertical={vertical}/>
                    <Scene4Dash    active={active === 3} vertical={vertical}/>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>

        {/* Copy column — first panel is hero copy, rest are scene narrations */}
        <div className="hero3d-copy-col">
          <div className={"h3-panel h3-panel-hero" + (active !== 0 ? " is-faded" : "")} ref={el => panelRefs.current[0] = el}>
            <div className="h3-panel-copy">
              <h1>{heroHeading || defaultHeroHeading}</h1>
              <p className="lede">{heroLede || defaultHeroLede}</p>
              {showVerticalSelectors && verticalOptions.length > 1 && <div className="hero3d-vertical-selector hero3d-inline-selector" role="group" aria-label="Choose demo vertical">
                {verticalOptions.map((option) => (
                  <button
                    key={option.key}
                    type="button"
                    className={selectedVertical === option.key ? "active" : ""}
                    aria-pressed={selectedVertical === option.key}
                    onClick={() => { setSelectedVertical(option.key); setVerticalLocked(true); }}
                  >
                    {option.label}
                  </button>
                ))}
              </div>}
            </div>
          </div>

          {vertical.scenes.slice(1).map((s, i) => (
            <div key={s.key} ref={el => panelRefs.current[i + 1] = el}
                 className={"h3-panel h3-panel-scene"
                   + (active === i + 1 ? " is-active" : "")
                   + (s.key === "map" ? " is-trio-panel" : "")
                   + (s.key === "map" && active === 1 ? " is-trio-mode" : "")
                   + (s.key === "dash" ? " is-dash-panel" : "")
                   + (s.key === "dash" && active === 3 ? " is-dash-mode" : "")}>
              <div className="h3-panel-copy">
                <span className="eyebrow" style={{color:"var(--ember-400)"}}>{s.eyebrow}</span>
                <h2>{s.title}</h2>
                <p className="lede">{s.body}</p>
              </div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

function Scene1Ticket({ active, vertical, staticPhase }) {
  // phase: "register" → "verifying" → "pass"
  const ticket = vertical.ticket;
  const fullEmail = ticket.email;
  const [phase, setPhase] = useSH3(staticPhase || "register");
  const [typed, setTyped] = useSH3(staticPhase ? fullEmail : "");

  useEH3(() => {
    if (staticPhase) {
      setPhase(staticPhase);
      setTyped(fullEmail);
      return;
    }
    if (!active) { setPhase("register"); setTyped(""); return; }
    // Always rewind to the register screen at the start of each run
    // (when active flips on, or when the vertical changes), so every
    // cycle plays the full register → verifying → pass animation —
    // not just lands on the pass card.
    setPhase("register");
    setTyped("");
    let i = 0;
    const typer = setInterval(() => {
      i += 1;
      setTyped(fullEmail.slice(0, i));
      if (i >= fullEmail.length) clearInterval(typer);
    }, 55);
    const t1 = setTimeout(() => setPhase("verifying"), 2100);
    const t2 = setTimeout(() => setPhase("pass"), 3000);
    return () => { clearInterval(typer); clearTimeout(t1); clearTimeout(t2); };
  }, [active, fullEmail, vertical.key, staticPhase]);

  const visiblePhase = staticPhase || phase;

  return (
    <div className={"sc sc-ticket" + (active ? " in" : "")}>
      <div className="sc-app-head">
        <span className="h-l">{visiblePhase === "pass" ? ticket.appHeadPass : ticket.appHeadRegister}</span>
        <span className="h-r">+</span>
      </div>

      <div className={"sc-register" + (visiblePhase !== "pass" ? " in" : "")}>
        <div className="reg-banner">
          <img src={ticket.registerBanner} alt={ticket.registerBannerAlt} />
          {ticket.passLogoSrc && (
            <img
              className="reg-banner-logo"
              src={ticket.passLogoSrc}
              alt={ticket.passLogoAlt || ""}
            />
          )}
        </div>
        <div className="reg-event">
          <div className="reg-eb">{ticket.eventKicker}</div>
          <div className="reg-name">{ticket.eventName}</div>
          <div className="reg-dt">{ticket.eventDate}</div>
        </div>
        <div className="reg-card">
          <div className="reg-lbl">Email</div>
          <div className="reg-input">
            <span className="reg-typed">{typed}</span>
            <span className="reg-caret"></span>
          </div>
          <button className={"reg-btn" + (visiblePhase === "verifying" ? " loading" : "")}>
            {visiblePhase === "verifying" ? <><span className="reg-spin"></span> {ticket.verifying}</> : ticket.cta}
          </button>
          <div className="reg-foot"><span className="reg-dot"></span>{ticket.foot}</div>
        </div>
      </div>

      <div className={"sc-passview" + (visiblePhase === "pass" ? " in" : "")}>
        <div className="sc-pass">
          <img src={ticket.img} alt={ticket.alt} />
          {ticket.passLogoSrc && (
            <img
              className="sc-pass-logo"
              src={ticket.passLogoSrc}
              alt={ticket.passLogoAlt || ""}
            />
          )}
        </div>
        <div className="welcome-toast">
          <span className="wt-mark">{ticket.toastMark}</span>
          <div className="wt-body">
            <div className="wt-title">{ticket.toastTitle}</div>
            <div className="wt-msg">{ticket.toastMsg}</div>
          </div>
        </div>
      </div>
    </div>
  );
}

function Scene2Map({ active, forceView, vertical }) {
  const map = vertical.map;
  const venues = map.venues;
  const booths = map.booths;
  const sessions = map.sessions;
  const people = map.people;
  const sessionThumb = (pos) => {
    const col = pos % 3;
    const row = Math.floor(pos / 3);
    return {
      backgroundImage: `url(${map.thumbsSrc})`,
      backgroundSize: "300% 200%",
      backgroundPosition: `${(col / 2) * 100}% ${row * 100}%`,
    };
  };
  // Each portrait sits in a 4-col × 2-row grid sprite
  // (assets/people-grid.jpg). pos = 0..7, row-major from top-left.
  // Each person also has an interest badge + bio + sample chat
  // (used by the click-to-expand bio→chat flow).
  const avatarStyle = (pos) => {
    const col = pos % 4;
    const row = Math.floor(pos / 4);
    return {
      backgroundImage: `url(${map.peopleSrc})`,
      backgroundSize: "400% 200%",
      backgroundPosition: `${(col / 3) * 100}% ${row * 100}%`,
    };
  };

  const TABS = getHeroMapViewKeys(vertical);
  const defaultTab = TABS[0] || "map";
  const mapViewSig = TABS.join("|");
  const tabLabel = (t) => map.tabs[t] || t.charAt(0).toUpperCase() + t.slice(1);
  const [tabState, setTab] = useSH3(defaultTab);
  const tab = forceView || (TABS.includes(tabState) ? tabState : defaultTab);

  useEH3(() => {
    if (forceView) return;
    if (!active) { setTab(defaultTab); return; }
    let i = 0;
    const id = setInterval(() => {
      i = (i + 1) % TABS.length;
      setTab(TABS[i]);
    }, 3500);
    return () => clearInterval(id);
  }, [active, forceView, vertical.key, defaultTab, mapViewSig]);

  // Booths-visited counter — animates up to TOTAL_BOOTHS when the scene
  // becomes visible, then the "Claim prizes" button enables.
  const TOTAL_BOOTH_VISITS = map.totalVisited;
  const [visited, setVisited] = useSH3(0);
  useEH3(() => {
    setVisited(0);
    if (!active && tab !== "map") return;
    let intervalId = null;
    const startId = setTimeout(() => {
      let n = 0;
      intervalId = setInterval(() => {
        n += 1;
        setVisited(n);
        if (n >= TOTAL_BOOTH_VISITS) {
          clearInterval(intervalId);
          intervalId = null;
        }
      }, 520);
    }, 600);
    return () => {
      clearTimeout(startId);
      if (intervalId) clearInterval(intervalId);
    };
  }, [active, tab, TOTAL_BOOTH_VISITS, vertical.key]);
  // Guard the displayed count against a stale value from the previous
  // vertical briefly exceeding the new TOTAL (e.g. corp 12/12 → sports
  // 12/8) on the render between vertical-change and effect-reset.
  const visitedShown = Math.min(visited, TOTAL_BOOTH_VISITS);

  // Click → bio → chat flow for People view
  const [selPerson, setSelPerson] = useSH3(null);
  const [pPhase, setPPhase] = useSH3("list");          // "list" | "bio" | "chat"
  const openPerson = (i) => { setSelPerson(i); setPPhase("bio"); };
  const openChat   = ()  => setPPhase("chat");
  const closePerson = () => { setPPhase("list"); setSelPerson(null); };
  // Reset overlay when scene/tab changes
  useEH3(() => { setPPhase("list"); setSelPerson(null); }, [active, tab, vertical.key]);

  const heading = map.headings[tab] || tabLabel(tab);
  const subHeading = tab === "map"
    ? map.mapSubheading(visitedShown, TOTAL_BOOTH_VISITS)
    : tab === "agenda" ? map.agendaSubheading : map.peopleSubheading;

  return (
    <div className={"sc sc-map sc-map-" + tab + (active ? " in" : "")}>
      <div className="sc-app-head">
        <span className="h-l">{heading}</span>
        <span className="h-r" style={{fontSize:11,color:"rgba(255,255,255,.55)",fontFamily:"var(--mono)",letterSpacing:".18em"}}>{subHeading}</span>
      </div>
      <div className="sc-tabs">
        {TABS.map(t => (
          <button key={t} className={tab === t ? "active" : ""}>
            {tabLabel(t)}
          </button>
        ))}
      </div>

      <div className="sc-views">
        {/* MAP view */}
        <div className={"sc-view sc-view-map" + (tab === "map" ? " in" : "")}>
          <div className="sc-floormap">
            <img src={map.mapSrc} alt={map.mapAlt} className="floor-img"/>
            <div className="floor-overlay"></div>
            {venues.map((v,i) => (
              <div key={"v"+i} className="venue-marker" style={{left: v.x + "%", top: v.y + "%"}}>
                <span className="vm-dot"></span>
                <span className="vm-lbl">{v.lbl}</span>
              </div>
            ))}
            {booths.map((b,i) => (
              <div key={"b"+i} className={"booth-pin" + (i === 2 ? " hot" : "")} style={{left: b.x + "%", top: b.y + "%"}}>
                <span className="bp-dot"></span>
                <span className="bp-lbl">{b.lbl}</span>
              </div>
            ))}
            <div className="sc-youhere">
              <div className="yh-pulse"></div>
              <span>{map.youHere}</span>
            </div>
          </div>
          <div className="sc-mapcard">
            <div className="mc-head">
              <div className="mc-ico" style={{background:map.cardIconBg}}>{map.cardIcon}</div>
              <div>
                <div className="mc-t">{map.cardTitle}</div>
                <div className="mc-s">{map.cardSub}</div>
              </div>
            </div>
            <button className="mc-cta">{map.cardCta}</button>
          </div>
          <div className="bv-progress">
            <div className="bv-row">
              <span className="bv-l">{map.progressLabel}</span>
              <span className="bv-r">{visitedShown}<span className="bv-tot">/{TOTAL_BOOTH_VISITS}</span></span>
            </div>
            <div className="bv-bar">
              <span style={{width: (visitedShown / TOTAL_BOOTH_VISITS * 100) + "%"}}></span>
            </div>
            <button className={"bv-claim" + (visitedShown === TOTAL_BOOTH_VISITS ? " ready" : "")}>
              {visitedShown === TOTAL_BOOTH_VISITS
                ? <><span className="bv-prize">{map.claimIcon}</span> {map.claimReady}</>
                : <>{TOTAL_BOOTH_VISITS - visitedShown} {map.remainingLabel}</>}
            </button>
          </div>
        </div>

        {/* AGENDA view */}
        <div className={"sc-view sc-view-agenda" + (tab === "agenda" ? " in" : "")}>
          <div className="ag-stream" style={sessionThumb(0)}>
            <div className="ag-stream-grad"></div>
            <div className="ag-live-badge"><span className="ag-live-dot"></span>LIVE</div>
            <div className="ag-stream-views">{map.streamViews}</div>
            <button className="ag-stream-play" aria-label="Play live stream">
              <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
            </button>
            <div className="ag-stream-meta">
              <div className="ag-stream-title">{map.streamTitle}</div>
              <div className="ag-stream-sub">{map.streamSub}</div>
            </div>
          </div>
          <div className="ag-days">
            {map.days.map((d, i) => (
              <button key={d.day + d.label} className={i === 0 ? "active" : ""}>
                <span className="ag-d-d">{d.day}</span><span className="ag-d-m">{d.label}</span>
              </button>
            ))}
          </div>
          <div className="ag-list">
            {sessions.map((s,i) => (
              <div key={i} className={"ag-item" + (s.tag ? " ag-now" : "")}>
                <span className="ag-thumb" style={sessionThumb(s.thumb)}></span>
                <span className="ag-time">{s.time}</span>
                <div className="ag-body">
                  <div className="ag-t">{s.title}</div>
                  <div className="ag-loc">{s.loc}{s.spk ? <> · <span className="ag-spk">{s.spk}</span></> : null}</div>
                </div>
                {s.tag && <span className="ag-tag">{s.tag}</span>}
              </div>
            ))}
          </div>
        </div>

        {/* PEOPLE view */}
        <div className={"sc-view sc-view-people" + (tab === "people" ? " in" : "")}>
          <div className="pp-head">
            <span className="pp-c"><span className="pp-d"></span>{map.availability}</span>
            <span className="pp-c-mute">{map.attending}</span>
          </div>
          <div className="pp-list">
            {people.map((p, i) => (
              <div key={i} className="pp-card" style={{ "--d": (i * 60) + "ms" }} onClick={() => openPerson(i)}>
                <span className="pp-avatar" style={avatarStyle(p.pos)}></span>
                <div className="pp-body">
                  <div className="pp-n">{p.name}</div>
                  <div className="pp-r">{p.role} · {p.company}<span className="pp-mu"> · {p.mutual} mutual</span></div>
                </div>
                <span className={"pp-tag " + heroTagClass(p.tag)}>{p.tag}</span>
              </div>
            ))}
          </div>

          <div className="pp-afterparty">
            <span className="pa-ico" aria-hidden>{map.socialCard.icon}</span>
            <div className="pa-body">
              <div className="pa-t">{map.socialCard.title}</div>
              <div className="pa-s">{map.socialCard.sub}</div>
            </div>
            <button className="pa-btn">{map.socialCard.cta}</button>
          </div>

          {/* Click → bio → chat overlay */}
          {selPerson !== null && (
            <div className={"pp-detail pp-detail-" + pPhase}>
              <button className="pp-detail-close" onClick={closePerson} aria-label="Close">×</button>
              <div className="pp-detail-hd">
                <span className="pp-detail-avatar" style={avatarStyle(people[selPerson].pos)}></span>
                <div className="pp-detail-id">
                  <div className="pp-detail-n">{people[selPerson].name}</div>
                  <div className="pp-detail-r">{people[selPerson].role} · {people[selPerson].company}</div>
                  <span className={"pp-tag " + heroTagClass(people[selPerson].tag)}>{people[selPerson].tag}</span>
                </div>
              </div>

              <div className="pp-detail-body">
                {pPhase === "bio" && (
                  <div className="pp-bio">
                    <p className="pp-bio-txt">{people[selPerson].bio}</p>
                    <div className="pp-linkedin">
                      <span className="pp-li-mark">in</span>
                      <span className="pp-li-handle">linkedin.com/in/{people[selPerson].name.toLowerCase().replace(/ /g, "-")}</span>
                    </div>
                    <button className="pp-msg-btn" onClick={openChat}>
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
                      Send a message
                    </button>
                  </div>
                )}

                {pPhase === "chat" && (
                  <div className="pp-chat">
                    <div className="pp-chat-bubble pp-chat-in">{people[selPerson].msgFrom}</div>
                    <div className="pp-chat-bubble pp-chat-out">{people[selPerson].msgReply}</div>
                    <div className="pp-chat-typing">
                      <span></span><span></span><span></span>
                    </div>
                    <div className="pp-chat-input">
                      <span className="pp-chat-placeholder">Type a message…</span>
                      <button className="pp-chat-send" aria-label="Send">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
                      </button>
                    </div>
                  </div>
                )}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function Scene3Scan({ active, vertical }) {
  // home → tap → camera → recognized → success → home-done
  // The flow: app opens to a "Booth Scanner" home with progress + activity.
  // After ~1.5s a tap pulse plays on the scan button, the camera POV opens,
  // the QR is scanned + recognized + confirmed, then we return to home with
  // the booth count incremented and a fresh activity item at the top.
  const scan = vertical.scan;
  const TOTAL = scan.total;
  const [phase, setPhase] = useSH3("home");
  const [count, setCount] = useSH3(scan.startCount);
  const [xp, setXp] = useSH3(0);

  useEH3(() => {
    if (!active) {
      setPhase("home"); setCount(scan.startCount); setXp(0);
      return;
    }
    const timers = [];
    timers.push(setTimeout(() => setPhase("tap"),         1700));
    timers.push(setTimeout(() => setPhase("scanning"),    2400));
    timers.push(setTimeout(() => setPhase("recognized"),  3700));
    timers.push(setTimeout(() => setPhase("success"),     4300));
    timers.push(setTimeout(() => {                         // count up XP
      let n = 0;
      const id = setInterval(() => {
        n += 5; setXp(n);
        if (n >= scan.points) { setXp(scan.points); clearInterval(id); }
      }, 25);
    }, 4350));
    timers.push(setTimeout(() => {                         // back to home
      setPhase("home-done"); setCount(scan.doneCount);
    }, 5500));
    return () => timers.forEach(clearTimeout);
  }, [active, vertical.key]);

  const inHome   = phase === "home" || phase === "tap" || phase === "home-done";
  const inCamera = phase === "scanning" || phase === "recognized" || phase === "success";

  return (
    <div
      className={"sc sc-scan sc-scan-" + phase + (active ? " in" : "")}
      style={{ "--cam-frame-top": scan.frameTop || "64%" }}
    >
      {/* —— HOME : Booth Scanner UI —— */}
      <div className={"scan-home" + (inHome ? " in" : "")}>
        <div className="sc-app-head">
          <span className="h-l">{scan.screenTitle}</span>
          <span className="h-r sh-live">
            <span className="sh-live-dot"></span>LIVE
          </span>
        </div>

        <div className="sh-progress-card">
          <div className="sh-pcard-row">
            <span className="sh-l">{scan.progressTitle}</span>
            <span className="sh-pct">{Math.round(count / TOTAL * 100)}%</span>
          </div>
          <div className="sh-bar">
            <span style={{ width: (count / TOTAL * 100) + "%" }}></span>
          </div>
          <div className="sh-meta">
            <span className="sh-meta-i"><strong>{count}</strong><span className="sh-meta-tot">/{TOTAL}</span> {scan.itemLabel}</span>
            <span className="sh-meta-divider"></span>
            <span className="sh-meta-i">Rank <strong>{phase === "home-done" ? scan.rankDone : scan.rankStart}</strong></span>
            <span className="sh-meta-divider"></span>
            <span className="sh-meta-i"><strong>{count * scan.points}</strong> {scan.pointsLabel}</span>
          </div>
        </div>

        <button className={"sh-scan-btn" + (phase === "tap" ? " is-tapped" : "")}>
          <span className="sh-scan-icon">
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M3 8V5a2 2 0 0 1 2-2h3"/>
              <path d="M21 8V5a2 2 0 0 0-2-2h-3"/>
              <path d="M3 16v3a2 2 0 0 0 2 2h3"/>
              <path d="M21 16v3a2 2 0 0 1-2 2h-3"/>
              <rect x="7" y="7" width="10" height="10" rx="1"/>
              <path d="M11 11h2v2h-2z" fill="currentColor"/>
            </svg>
          </span>
          <div className="sh-scan-text">
            <div className="sh-scan-t">{scan.scanTitle}</div>
            <div className="sh-scan-s">{scan.scanSub}</div>
          </div>
          <span className="sh-scan-arrow">→</span>
          <span className="sh-tap-ring"></span>
        </button>

        <div className="sh-activity">
          <div className="sh-activity-h">
            <span>{scan.activityHeader}</span>
            <span className="sh-activity-l">{scan.activityScope}</span>
          </div>
          <div className="sh-act-list">
            <div className={"sh-act-item sh-act-new" + (phase === "home-done" ? " in" : "")}>
              <span className="sh-act-dot ember"></span>
              <span className="sh-act-time">Just now</span>
              <div className="sh-act-body">
                <div className="sh-act-t">{scan.newActivity.title}</div>
                <span className="sh-act-tag">{scan.newActivity.tag}</span>
              </div>
            </div>
            {scan.activities.map((item, i) => (
              <div className="sh-act-item" key={item.time + item.title}>
                <span className="sh-act-dot"></span>
                <span className="sh-act-time">{item.time}</span>
                <div className="sh-act-body">
                  <div className="sh-act-t">{item.title}</div>
                  {item.tag && <span className="sh-act-tag">{item.tag}</span>}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* —— CAMERA : POV image + scan animation —— */}
      <div className={"sc-camera" + (scan.bakedCameraOverlay ? " sc-camera-baked" : "") + (inCamera ? " in" : "")}>
        <img src={scan.cameraSrc} alt="" className="cam-pov"/>
        <div className="cam-pov-shade"></div>

        <div className="cam-head">
          <span className="cam-back">‹</span>
          <span className="cam-title">{scan.cameraTitle}</span>
          <span className="cam-flash">⚡</span>
        </div>

        <div className="cam-frame">
          <span className="corner tl"></span>
          <span className="corner tr"></span>
          <span className="corner bl"></span>
          <span className="corner br"></span>
          <span className="cam-pulse"></span>
          <span className="cam-pulse cam-pulse-2"></span>
          <span className="cam-pulse cam-pulse-3"></span>
          {phase === "scanning" && <div className="cam-laser"></div>}
          <span className="cam-burst"><span></span><span></span><span></span><span></span><span></span><span></span></span>
          <span className="cam-checkmark">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
          </span>
        </div>

        <div className="cam-status">
          <span className="cs-scanning">Scanning code<span className="cs-dots"><i></i><i></i><i></i></span></span>
          <span className="cs-recognized"><span className="cs-tick">✓</span>{scan.recognized}</span>
          <span className="cs-success"><span className="cs-tick">✓</span>{scan.success}</span>
        </div>

        <div className="cam-xp"><span className="xp-num">+{xp}</span><span className="xp-l">{scan.pointsLabel}</span></div>

        <div className="cam-toaststack">
          <div className="cam-toast t1">
            <div className="ct-ico" style={{background:scan.toastIconBg}}>{scan.toastIcon}</div>
            <div>
              <div className="ct-t">{scan.toastTitle}</div>
              <div className="ct-s">{scan.toastSub}</div>
            </div>
          </div>
          <div className="cam-toast t2">
            <div className="ct-ico" style={{background:scan.rewardIconBg}}>{scan.rewardIcon}</div>
            <div>
              <div className="ct-t">{scan.rewardTitle}</div>
              <div className="ct-s">{scan.rewardSub}</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// useDashCounters — shared headline + KPI count-up animation used by both
// the slim phone scene and the iPad tablet scene.
function useDashCounters(active, vertical) {
  const dash = vertical.dash;
  const [pipeline, setPipeline] = useSH3(0);
  const [leads, setLeads] = useSH3(0);
  useEH3(() => {
    if (!active) { setPipeline(0); setLeads(0); return; }
    const start = performance.now();
    const dur = 1100;
    let raf;
    const tick = (t) => {
      const k = Math.min(1, (t - start) / dur);
      const ease = 1 - Math.pow(1 - k, 3);
      setPipeline(dash.headlineTarget * ease);
      setLeads(Math.round(dash.secondaryTarget * ease));
      if (k < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [active, vertical.key]);
  const headlineValue = `${dash.headlinePrefix}${formatHeroNumber(pipeline, dash.headlineDecimals)}${dash.headlineSuffix}`;
  const headlinePct = Math.round(((pipeline / dash.headlineBase) * 100) || 0);
  return { pipeline, leads, headlineValue, headlinePct };
}

// Scene 4 — phone (compact "ops alert" view). The iPad next to it carries
// the full dashboard; the phone keeps a single alert + mini activity feed
// so the hand-held companion screen has a focused glanceable role.
function Scene4Dash({ active, vertical }) {
  const dash = vertical.dash;
  const tablet = dash.tablet;
  const { leads, headlineValue, headlinePct } = useDashCounters(active, vertical);

  // Two of the four KPIs feel right on the small screen — the rest live on
  // the iPad. Pick the qualitatively most useful pair (capture + ratio).
  const phoneKpis = dash.kpis.slice(0, 2);
  const feedItems = tablet.feed.items.slice(0, 3);

  return (
    <div className={"sc sc-dash sc-dash-phone" + (active ? " in" : "")}>
      <div className="sc-app-head">
        <span className="h-l">{dash.appTitle}</span>
        <span className="h-r" style={{fontSize:11,color:"#4ADE80",fontFamily:"var(--mono)",letterSpacing:".18em",display:"flex",alignItems:"center",gap:6}}>
          <span style={{width:6,height:6,borderRadius:"50%",background:"#4ADE80",boxShadow:"0 0 8px #4ADE80"}}></span>LIVE
        </span>
      </div>

      <div className="dash-headline">
        <div className="dh-l">{dash.headlineLabel}</div>
        <div className="dh-v">{headlineValue}</div>
        <div className="dh-d">{dash.headlineDelta} · {headlinePct}% of {dash.headlineTargetText}</div>
        <div className="dh-bar"><span style={{width: (active ? 95 : 0) + "%"}}></span></div>
      </div>

      <div className="dash-kpi dash-kpi-2">
        {phoneKpis.map((kpi) => (
          <div className="kpi" key={kpi.label}>
            <div className="kpi-l">{kpi.label}</div>
            <div className="kpi-v">{kpi.value === "animated" ? leads.toLocaleString() : kpi.value}</div>
            <div className="kpi-d">{kpi.detail}</div>
          </div>
        ))}
      </div>

      <div className="dash-alert">
        <div className="da-ico">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
            <path d="M12 3v10M12 18v.5" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"/>
            <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="1.6"/>
          </svg>
        </div>
        <div className="da-body">
          <div className="da-t">{tablet.alert.title}</div>
          <div className="da-s">{tablet.alert.sub}</div>
        </div>
        <button className="da-cta">{tablet.alert.cta}</button>
      </div>

      <div className="dash-card dash-card-feed">
        <div className="dc-head">
          <span>{tablet.feed.title}</span>
          <span style={{fontFamily:"var(--mono)",fontSize:9,letterSpacing:".18em",color:"rgba(255,255,255,.5)"}}>LIVE</span>
        </div>
        {feedItems.map((it, i) => (
          <div key={i} className="dc-feed-row" style={{transitionDelay: (i*120 + 320) + "ms"}}>
            <span className="dcf-dot"></span>
            <span className="dcf-text">{it.text}</span>
            <span className="dcf-tag">{it.tag}</span>
          </div>
        ))}
      </div>

      <div className="dash-foot">{dash.foot}</div>
    </div>
  );
}

// —— iPad tablet helpers ——

function Sparkline({ points, active, delay = 0 }) {
  if (!points || !points.length) return null;
  const w = 520, h = 110, pad = 6;
  const max = Math.max(...points);
  const min = Math.min(...points);
  const range = max - min || 1;
  const path = points.map((p, i) => {
    const x = pad + (i / (points.length - 1)) * (w - pad * 2);
    const y = h - pad - ((p - min) / range) * (h - pad * 2 - 4);
    return `${i === 0 ? "M" : "L"}${x.toFixed(1)} ${y.toFixed(1)}`;
  }).join(" ");
  const area = `${path} L${w - pad} ${h - pad} L${pad} ${h - pad} Z`;
  return (
    <svg className={"td-spark" + (active ? " in" : "")} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{transitionDelay: delay + "ms"}}>
      <defs>
        <linearGradient id="td-spark-g" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="rgba(238,90,36,.45)"/>
          <stop offset="100%" stopColor="rgba(238,90,36,0)"/>
        </linearGradient>
      </defs>
      <path className="td-spark-area" d={area} fill="url(#td-spark-g)"/>
      <path className="td-spark-line" d={path} fill="none" stroke="var(--ember-500)" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
}

function Radial({ pct, active, value, max, color = "var(--ember-500)", delay = 0 }) {
  const r = 30;
  const c = 2 * Math.PI * r;
  const offset = active ? c * (1 - pct) : c;
  return (
    <div className="td-radial">
      <svg viewBox="0 0 80 80">
        <circle cx="40" cy="40" r={r} stroke="rgba(255,255,255,.08)" strokeWidth="7" fill="none"/>
        <circle
          cx="40" cy="40" r={r} stroke={color} strokeWidth="7" fill="none"
          strokeLinecap="round"
          strokeDasharray={c}
          strokeDashoffset={offset}
          transform="rotate(-90 40 40)"
          style={{transition: `stroke-dashoffset 1.4s ease ${delay}ms`}}
        />
      </svg>
      <div className="td-radial-c">
        <span className="td-radial-v">{value}</span>
        <span className="td-radial-m">/ {max}</span>
      </div>
    </div>
  );
}

function SemiGauge({ pct, active, value, max, color = "#4ADE80", delay = 0 }) {
  // Half-circle arc from (10,52) to (90,52) with radius 40
  const len = Math.PI * 40;
  const offset = active ? len * (1 - pct) : len;
  return (
    <div className="td-semi">
      <svg viewBox="0 0 100 60">
        <path d="M 10 52 A 40 40 0 0 1 90 52" stroke="rgba(255,255,255,.08)" strokeWidth="9" fill="none" strokeLinecap="round"/>
        <path
          d="M 10 52 A 40 40 0 0 1 90 52"
          stroke={color} strokeWidth="9" fill="none" strokeLinecap="round"
          strokeDasharray={len}
          strokeDashoffset={offset}
          style={{transition: `stroke-dashoffset 1.4s ease ${delay}ms`}}
        />
      </svg>
      <div className="td-semi-c">
        <span className="td-semi-v">{value}</span>
        <span className="td-semi-m">/ {max}</span>
      </div>
    </div>
  );
}

function Donut({ pct, active, value, total, color = "#FFB37A", delay = 0 }) {
  const r = 28;
  const c = 2 * Math.PI * r;
  const offset = active ? c * (1 - pct) : c;
  return (
    <div className="td-donut">
      <svg viewBox="0 0 80 80">
        <circle cx="40" cy="40" r={r} stroke="rgba(255,255,255,.08)" strokeWidth="9" fill="none"/>
        <circle
          cx="40" cy="40" r={r} stroke={color} strokeWidth="9" fill="none"
          strokeLinecap="round"
          strokeDasharray={c}
          strokeDashoffset={offset}
          transform="rotate(-90 40 40)"
          style={{transition: `stroke-dashoffset 1.4s ease ${delay}ms`}}
        />
      </svg>
      <div className="td-donut-c">
        <span className="td-donut-v">{typeof value === "number" ? value.toLocaleString() : value}</span>
        <span className="td-donut-t">/ {typeof total === "number" ? total.toLocaleString() : total}</span>
      </div>
    </div>
  );
}

// Scene 4 — iPad admin dashboard. Eight widget cells in a 4-column grid:
// 1) headline + sparkline (col-span 2)  2) ops radial gauge   3) NPS semi gauge
// 4) attendee funnel (col-span 2)       5) zone heatmap        6) capacity donut
// 7) sponsor leaderboard (col-span 2)   8) live activity feed (col-span 2)
function Scene4DashTablet({ active, vertical }) {
  const dash = vertical.dash;
  const tablet = dash.tablet;
  const { leads, headlineValue, headlinePct } = useDashCounters(active, vertical);

  return (
    <div className={"sc-tab-dash" + (active ? " in" : "")}>
      <div className="td-bar">
        <div className="td-bar-l">
          <span className="td-app">{dash.appTitle}</span>
          <span className="td-vert">· {vertical.label}</span>
        </div>
        <div className="td-bar-c">
          <span className="td-live"><span className="td-live-dot"></span>LIVE</span>
          <span className="td-time">{tablet.lastUpdate}</span>
        </div>
        <div className="td-bar-r">
          <span className="td-pill">Today</span>
          <span className="td-pill">All zones</span>
          <span className="td-pill td-pill-active">Real-time</span>
        </div>
      </div>

      <div className="td-grid">
        <div className="td-w td-w-headline">
          <div className="td-w-eb">{tablet.spark.label}</div>
          <div className="td-w-headrow">
            <div className="td-w-num">{headlineValue}</div>
            <div className="td-w-delta">
              <span className="td-w-d-up">▲</span> {dash.headlineDelta}
              <span className="td-w-d-pct">{headlinePct}% of {dash.headlineTargetText}</span>
            </div>
          </div>
          <Sparkline points={tablet.spark.points} active={active} delay={120}/>
          <div className="td-w-foot">{tablet.spark.peakLabel}</div>
        </div>

        <div className="td-w td-w-gauge">
          <Radial pct={tablet.gauge.value / tablet.gauge.max} active={active} value={tablet.gauge.value} max={tablet.gauge.max} delay={200}/>
          <div className="td-w-eb">{tablet.gauge.label}</div>
          <div className="td-w-sub">{tablet.gauge.sub}</div>
        </div>

        <div className="td-w td-w-nps">
          <SemiGauge pct={tablet.nps.value / tablet.nps.max} active={active} value={tablet.nps.value} max={tablet.nps.max} delay={280}/>
          <div className="td-w-eb">{tablet.nps.label}</div>
          <div className="td-w-sub">{tablet.nps.sub}</div>
        </div>

        <div className="td-w td-w-venue">
          <div className="td-w-title">Venue map</div>
          <div className="td-venue-floor">
            <img src={vertical.map.mapSrc} alt={vertical.map.mapAlt}/>
            <div className="td-venue-overlay"></div>
            {vertical.map.venues.map((v, i) => (
              <div key={"v"+i} className="td-vm-marker" style={{left: v.x + "%", top: v.y + "%"}}>
                <span className="td-vm-dot"></span>
                <span className="td-vm-lbl">{v.lbl}</span>
              </div>
            ))}
            {vertical.map.booths.map((b, i) => (
              <div key={"b"+i} className={"td-bp-pin" + (i === 2 ? " hot" : "")} style={{left: b.x + "%", top: b.y + "%"}}>
                <span className="td-bp-dot"></span>
              </div>
            ))}
          </div>
        </div>

        <div className="td-w td-w-heat">
          <div className="td-w-title">{tablet.heat.title}</div>
          <div className="td-heat-grid" style={{gridTemplateColumns: `repeat(${tablet.heat.cols.length}, 1fr)`}}>
            {tablet.heat.cells.map((v, i) => (
              <span
                key={i}
                className="td-heat-cell"
                style={{
                  background: `rgba(238,90,36,${active ? (v * 0.95 + 0.05) : 0.04})`,
                  transitionDelay: (i * 18 + 420) + "ms",
                }}
              ></span>
            ))}
          </div>
          <div className="td-w-sub">Hottest: <strong>{tablet.heat.hot}</strong></div>
        </div>

        <div className="td-w td-w-donut">
          <Donut pct={tablet.donut.used / tablet.donut.total} active={active} value={tablet.donut.used} total={tablet.donut.total} delay={360}/>
          <div className="td-w-eb">{tablet.donut.label}</div>
          <div className="td-w-sub">{tablet.donut.sub}</div>
        </div>

        <div className="td-w td-w-attendees">
          <div className="td-w-title">{vertical.map.headings.people || "Top attendees"}</div>
          <div className="td-people-list">
            {vertical.map.people.slice(0, 3).map((p, i) => (
              <div key={i} className="td-person" style={{transitionDelay: (i * 90 + 480) + "ms"}}>
                <span className="td-person-avatar" style={{
                  backgroundImage: `url(${vertical.map.peopleSrc})`,
                  backgroundSize: "400% 200%",
                  backgroundPosition: `${((p.pos % 4) / 3) * 100}% ${Math.floor(p.pos / 4) * 100}%`,
                }}></span>
                <div className="td-person-body">
                  <div className="td-person-n">{p.name}</div>
                  <div className="td-person-r">{p.role} · {p.company}</div>
                </div>
                <span className={"pp-tag " + heroTagClass(p.tag)}>{p.tag}</span>
              </div>
            ))}
          </div>
        </div>

        <div className="td-w td-w-feed">
          <div className="td-w-title">{tablet.feed.title}</div>
          <div className="td-feed-list">
            {tablet.feed.items.slice(0, 4).map((it, i) => (
              <div key={i} className="td-feed-row" style={{transitionDelay: (i * 110 + 540) + "ms"}}>
                <span className="td-feed-dot"></span>
                <span className="td-feed-text">{it.text}</span>
                <span className="td-feed-tag">{it.tag}</span>
                <span className="td-feed-time">{it.time}</span>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

// HeroDashOverlay — full-viewport scene-4 stage. Mirrors HeroTrioOverlay
// but holds an iPad (centerpiece) plus a smaller phone tucked alongside.
function HeroDashOverlay({ active, vertical, className = "" }) {
  const lastScene = vertical.scenes[vertical.scenes.length - 1];
  return (
    <div className={"hero3d-dash-overlay " + className + (active ? " in" : "")} aria-hidden={!active}>
      <div className="dash-header">
        <span className="eyebrow" style={{color:"var(--ember-400)"}}>{lastScene.eyebrow}</span>
        <h2>{lastScene.title}</h2>
        <p className="dash-lede">{lastScene.body}</p>
      </div>
      <div className="dash-stage">
        <div className="dash-tablet">
          <div className="dash-tablet-inner">
            <div className="hero3d-tablet">
              <div className="hero3d-tablet-shell">
                <div className="hero3d-tablet-screen">
                  <div className="td-cam"></div>
                  <Scene4DashTablet active={active} vertical={vertical}/>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div className="dash-phone">
          <div className="dash-phone-inner">
            <div className="hero3d-phone">
              <div className="hero3d-phone-shell">
                <div className="hero3d-phone-screen">
                  <div className="dp-notch"></div>
                  <div className="dp-bar">
                    <span>9:41</span>
                    <span style={{display:"flex",gap:5,alignItems:"center"}}>
                      <svg width="14" height="10" viewBox="0 0 14 10" fill="currentColor"><rect x="0" y="6" width="2" height="4" rx="1"/><rect x="3" y="4" width="2" height="6" rx="1"/><rect x="6" y="2" width="2" height="8" rx="1"/><rect x="9" y="0" width="2" height="10" rx="1"/></svg>
                      <span style={{fontSize:11,fontWeight:600}}>100</span>
                    </span>
                  </div>
                  <Scene4Dash active={active} vertical={vertical}/>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function HeroPhoneSceneRow({
  verticalKey = "corporate",
  sceneKeys,
  className = "",
  selectedKey,
  allPageKeys,
  onSelectPage,
} = {}) {
  const vertical = HERO_VERTICALS[verticalKey] || HERO_VERTICALS.corporate;
  const rowViewKeys = getHeroPhoneRowViewKeys(vertical, sceneKeys);
  const pageKeys = Array.isArray(allPageKeys) && allPageKeys.length ? allPageKeys : rowViewKeys;
  const selectedIndex = pageKeys.indexOf(selectedKey);
  const sceneMeta = vertical.scenes.reduce((acc, scene) => {
    acc[scene.key] = scene;
    return acc;
  }, {});
  const renderScene = (sceneKey) => {
    if (sceneKey === "ticket") return <Scene1Ticket active={true} vertical={vertical} staticPhase="pass" />;
    if (HERO_MAP_VIEW_KEYS.includes(sceneKey)) return <Scene2Map active={true} forceView={sceneKey} vertical={vertical} />;
    if (sceneKey === "schedule") return <Scene2Map active={true} forceView="agenda" vertical={vertical} />;
    if (sceneKey === "scan") return <Scene3Scan active={true} vertical={vertical} />;
    if (sceneKey === "dash") return <Scene4Dash active={true} vertical={vertical} />;
    return null;
  };
  const sceneLabel = (sceneKey) => {
    if (sceneMeta[sceneKey]) return sceneMeta[sceneKey].title;
    if (HERO_MAP_VIEW_KEYS.includes(sceneKey)) return (vertical.map.tabs[sceneKey] || sceneKey) + " app screen";
    if (sceneKey === "schedule") return (vertical.map.tabs.agenda || "Schedule") + " app screen";
    return sceneKey;
  };
  const selectByKey = (event, sceneKey) => {
    if (!onSelectPage) return;
    if (event.type === "keydown" && event.key !== "Enter" && event.key !== " ") return;
    if (event.type === "keydown") event.preventDefault();
    onSelectPage(sceneKey, event);
  };

  return (
    <div className={"hero3d hero-phone-scene-row hero3d-theme-" + vertical.key + (className ? " " + className : "")}>
      {rowViewKeys.map((sceneKey) => {
        const isSelected = selectedKey === sceneKey;
        const sceneIndex = pageKeys.indexOf(sceneKey);
        const neighborClass = selectedIndex >= 0 && sceneIndex === selectedIndex - 1
          ? " is-before-selected"
          : selectedIndex >= 0 && sceneIndex === selectedIndex + 1
            ? " is-after-selected"
            : "";
        return (
          <div
            role="button"
            tabIndex={0}
            className={"hero-phone-row-frame hero-phone-row-frame-" + sceneKey + (isSelected ? " is-selected" : "") + neighborClass}
            data-page-key={sceneKey}
            aria-label={"Show details for " + sceneLabel(sceneKey)}
            aria-pressed={isSelected}
            onPointerEnter={(event) => selectByKey(event, sceneKey)}
            onPointerMove={(event) => selectByKey(event, sceneKey)}
            onMouseEnter={(event) => selectByKey(event, sceneKey)}
            onMouseMove={(event) => selectByKey(event, sceneKey)}
            onFocus={(event) => selectByKey(event, sceneKey)}
            onClick={(event) => selectByKey(event, sceneKey)}
            onKeyDown={(event) => selectByKey(event, sceneKey)}
            key={sceneKey}
          >
            <div className="hero3d-phone">
              <div className="hero3d-phone-shell">
                <div className="hero3d-phone-screen" inert="true">
                  <div className="dp-notch"></div>
                  <div className="dp-bar">
                    <span>9:41</span>
                    <span style={{display:"flex",gap:5,alignItems:"center"}}>
                      <svg width="14" height="10" viewBox="0 0 14 10" fill="currentColor"><rect x="0" y="6" width="2" height="4" rx="1"/><rect x="3" y="4" width="2" height="6" rx="1"/><rect x="6" y="2" width="2" height="8" rx="1"/><rect x="9" y="0" width="2" height="10" rx="1"/></svg>
                      <span style={{fontSize:11,fontWeight:600}}>100</span>
                    </span>
                  </div>
                  {renderScene(sceneKey)}
                </div>
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

function HeroFeatureStats({ stats = [] }) {
  if (!stats.length) return null;
  return (
    <div className="hf-stats">
      {stats.map((stat) => (
        <div className="hf-stat" key={stat.label}>
          <strong>{stat.value}</strong>
          <span>{stat.label}</span>
        </div>
      ))}
    </div>
  );
}

function HeroFeatureProgress({ value, label }) {
  if (typeof value !== "number") return null;
  return (
    <div className="hf-progress">
      <div className="hf-progress-top">
        <span>{label}</span>
        <strong>{value}%</strong>
      </div>
      <div className="hf-progress-bar"><span style={{width: value + "%"}}></span></div>
    </div>
  );
}

function HeroFeatureRows({ rows = [], compact = false }) {
  if (!rows.length) return null;
  return (
    <div className={"hf-list" + (compact ? " hf-list-compact" : "")}>
      {rows.map((row, i) => {
        const item = typeof row === "string" ? { label: row } : row;
        return (
          <div className="hf-row" key={(item.label || row) + i}>
            <span className="hf-row-dot"></span>
            <div>
              <strong>{item.label}</strong>
              {item.meta && <em>{item.meta}</em>}
            </div>
            {item.status && <b>{item.status}</b>}
          </div>
        );
      })}
    </div>
  );
}

function HeroFeatureScreen({ feature }) {
  const type = feature.type || "feature";
  return (
    <div className={"sc sc-feature sc-feature-" + type + " in"}>
      <div className="sc-app-head">
        <span className="h-l">{feature.appTitle}</span>
        <span className="h-r" style={{fontSize:11,color:"var(--screen-accent-2)",fontFamily:"var(--mono)",letterSpacing:".18em"}}>LIVE</span>
      </div>

      <div className="hf-body">
        <div className="hf-hero">
          <span className="hf-kicker">{feature.kicker}</span>
          <strong>{feature.title}</strong>
          <p>{feature.sub}</p>
          <div className="hf-primary">
            <span>{feature.primary}</span>
            <em>{feature.primaryLabel}</em>
          </div>
        </div>

        {type === "raffle" && (
          <>
            <HeroFeatureProgress value={feature.progress} label={feature.progressLabel} />
            <HeroFeatureStats stats={feature.stats} />
            <HeroFeatureRows rows={feature.rows} compact={true} />
            <button className="hf-cta" type="button">{feature.cta}</button>
          </>
        )}

        {type === "checkins" && (
          <>
            <div className="hf-tabs">
              {feature.tabs.map((tab, i) => <span className={i === 1 ? "active" : ""} key={tab}>{tab}</span>)}
            </div>
            <HeroFeatureProgress value={feature.progress} label="streak progress" />
            <HeroFeatureRows rows={feature.rows} />
          </>
        )}

        {type === "puzzle" && (
          <>
            <div className="hf-code">
              {feature.code.map((letter, i) => <span className={letter === "?" ? "empty" : ""} key={letter + i}>{letter}</span>)}
            </div>
            <HeroFeatureProgress value={feature.progress} label="quest progress" />
            <HeroFeatureRows rows={feature.rows} compact={true} />
          </>
        )}

        {type === "gallery" && (
          <>
            <div className="hf-gallery-grid">
              {[0, 1, 2, 3, 4, 5].map((pos) => {
                const col = pos % 3;
                const row = Math.floor(pos / 3);
                return (
                  <span
                    key={pos}
                    style={{
                      backgroundImage: `url(${feature.imageSrc})`,
                      backgroundSize: "300% 200%",
                      backgroundPosition: `${(col / 2) * 100}% ${row * 100}%`,
                    }}
                  ></span>
                );
              })}
            </div>
            <div className="hf-tags">{feature.tags.map((tag) => <span key={tag}>{tag}</span>)}</div>
          </>
        )}

        {type === "market" && (
          <>
            <HeroFeatureRows rows={feature.rows} />
            <div className="hf-watch">
              <span>Watchlist</span>
              <strong>32 fans tracking</strong>
            </div>
          </>
        )}

        {type === "chat" && (
          <>
            <HeroFeatureRows rows={feature.rows} compact={true} />
            <div className="hf-chat">
              {feature.messages.map((msg) => (
                <div className="hf-msg" key={msg.who + msg.text}>
                  <span>{msg.who}</span>
                  <p>{msg.text}</p>
                </div>
              ))}
            </div>
          </>
        )}
      </div>
    </div>
  );
}

function HeroPhoneFeatureRow({
  verticalKey = "corporate",
  featureKeys,
  className = "",
  selectedKey,
  allPageKeys,
  onSelectPage,
} = {}) {
  const vertical = HERO_VERTICALS[verticalKey] || HERO_VERTICALS.corporate;
  const rowFeatureKeys = getHeroFeatureViewKeys(vertical, featureKeys);
  if (!rowFeatureKeys.length) return null;
  const pageKeys = Array.isArray(allPageKeys) && allPageKeys.length ? allPageKeys : rowFeatureKeys;
  const selectedIndex = pageKeys.indexOf(selectedKey);
  const selectByKey = (event, featureKey) => {
    if (!onSelectPage) return;
    if (event.type === "keydown" && event.key !== "Enter" && event.key !== " ") return;
    if (event.type === "keydown") event.preventDefault();
    onSelectPage(featureKey, event);
  };

  return (
    <div className={"hero3d hero-phone-scene-row hero-phone-feature-row hero3d-theme-" + vertical.key + (className ? " " + className : "")}>
      {rowFeatureKeys.map((featureKey) => {
        const feature = vertical.features[featureKey];
        const isSelected = selectedKey === featureKey;
        const featureIndex = pageKeys.indexOf(featureKey);
        const neighborClass = selectedIndex >= 0 && featureIndex === selectedIndex - 1
          ? " is-before-selected"
          : selectedIndex >= 0 && featureIndex === selectedIndex + 1
            ? " is-after-selected"
            : "";
        return (
          <div
            role="button"
            tabIndex={0}
            className={"hero-phone-row-frame hero-phone-row-frame-" + featureKey + (isSelected ? " is-selected" : "") + neighborClass}
            data-page-key={featureKey}
            aria-label={"Show details for " + feature.title}
            aria-pressed={isSelected}
            onPointerEnter={(event) => selectByKey(event, featureKey)}
            onPointerMove={(event) => selectByKey(event, featureKey)}
            onMouseEnter={(event) => selectByKey(event, featureKey)}
            onMouseMove={(event) => selectByKey(event, featureKey)}
            onFocus={(event) => selectByKey(event, featureKey)}
            onClick={(event) => selectByKey(event, featureKey)}
            onKeyDown={(event) => selectByKey(event, featureKey)}
            key={featureKey}
          >
            <div className="hero3d-phone">
              <div className="hero3d-phone-shell">
                <div className="hero3d-phone-screen" inert="true">
                  <div className="dp-notch"></div>
                  <div className="dp-bar">
                    <span>9:41</span>
                    <span style={{display:"flex",gap:5,alignItems:"center"}}>
                      <svg width="14" height="10" viewBox="0 0 14 10" fill="currentColor"><rect x="0" y="6" width="2" height="4" rx="1"/><rect x="3" y="4" width="2" height="6" rx="1"/><rect x="6" y="2" width="2" height="8" rx="1"/><rect x="9" y="0" width="2" height="10" rx="1"/></svg>
                      <span style={{fontSize:11,fontWeight:600}}>100</span>
                    </span>
                  </div>
                  <HeroFeatureScreen feature={feature} />
                </div>
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

function HeroPhonePreviewStack({
  verticalKey = "corporate",
  className = "",
} = {}) {
  const vertical = HERO_VERTICALS[verticalKey] || HERO_VERTICALS.corporate;
  const coreKeys = getHeroPhoneRowViewKeys(vertical);
  const featureKeys = getHeroFeatureViewKeys(vertical);
  const pageKeys = [...coreKeys, ...featureKeys];
  const stackRef = useRH3(null);
  const carouselRef = useRH3(null);
  const edgeScrollTimerRef = useRH3(null);
  const edgeScrollDirRef = useRH3(0);
  const edgeScrollSpeedRef = useRH3(0);
  const edgeScrollTargetRef = useRH3(0);
  const edgeScrollActiveRef = useRH3(false);
  const edgeScrollLastTickRef = useRH3(0);
  const [selectedKey, setSelectedKey] = useSH3(pageKeys[0] || null);
  const activeKey = pageKeys.includes(selectedKey) ? selectedKey : pageKeys[0];
  const selectedPage = activeKey ? getHeroPreviewPageMeta(vertical, activeKey) : null;
  const setEdgeScrollActive = (active) => {
    if (edgeScrollActiveRef.current === active) return;
    edgeScrollActiveRef.current = active;
    if (stackRef.current) stackRef.current.classList.toggle("is-edge-scrolling", active);
  };
  const getEdgeIntent = (event) => {
    const carousel = carouselRef.current;
    if (!carousel || !event) return { dir: 0, strength: 0, inEdge: false };
    const rect = carousel.getBoundingClientRect();
    const edgeStart = Math.min(54, Math.max(34, rect.width * 0.052));
    const edgeHold = edgeStart + 22;
    const leftDistance = event.clientX - rect.left;
    const rightDistance = rect.right - event.clientX;
    const activeDir = edgeScrollDirRef.current;
    if (leftDistance <= edgeStart || (activeDir < 0 && leftDistance <= edgeHold)) {
      const strength = Math.max(0, Math.min(1, (edgeHold - leftDistance) / edgeHold));
      return { dir: -1, strength, inEdge: true };
    }
    if (rightDistance <= edgeStart || (activeDir > 0 && rightDistance <= edgeHold)) {
      const strength = Math.max(0, Math.min(1, (edgeHold - rightDistance) / edgeHold));
      return { dir: 1, strength, inEdge: true };
    }
    return { dir: 0, strength: 0, inEdge: false };
  };
  const isEdgeScrollMoving = () => (
    !!edgeScrollDirRef.current ||
    Math.abs(edgeScrollTargetRef.current) > .01 ||
    Math.abs(edgeScrollSpeedRef.current) > .18
  );
  const stopEdgeScroll = () => {
    edgeScrollDirRef.current = 0;
    edgeScrollTargetRef.current = 0;
  };
  const clearEdgeScroll = () => {
    edgeScrollDirRef.current = 0;
    edgeScrollTargetRef.current = 0;
    edgeScrollSpeedRef.current = 0;
    edgeScrollLastTickRef.current = 0;
    setEdgeScrollActive(false);
    if (carouselRef.current) {
      carouselRef.current.style.scrollSnapType = "";
      carouselRef.current.style.scrollBehavior = "";
    }
    if (edgeScrollTimerRef.current) {
      clearInterval(edgeScrollTimerRef.current);
      edgeScrollTimerRef.current = null;
    }
  };
  const startEdgeScroll = (dir, strength = 1) => {
    edgeScrollDirRef.current = dir;
    edgeScrollTargetRef.current = dir * (.45 + (Math.max(0, Math.min(1, strength)) * 2.05));
    setEdgeScrollActive(true);
    if (edgeScrollTimerRef.current) return;
    const tick = () => {
      const carousel = carouselRef.current;
      if (!carousel) {
        clearEdgeScroll();
        return;
      }
      const now = performance.now();
      const delta = edgeScrollLastTickRef.current ? Math.min(34, now - edgeScrollLastTickRef.current) : 16;
      edgeScrollLastTickRef.current = now;
      const frameScale = delta / 16;
      carousel.style.scrollSnapType = "none";
      carousel.style.scrollBehavior = "auto";
      edgeScrollSpeedRef.current += (edgeScrollTargetRef.current - edgeScrollSpeedRef.current) * Math.min(.18, .08 * frameScale);
      if (!edgeScrollTargetRef.current && Math.abs(edgeScrollSpeedRef.current) < .05) {
        clearEdgeScroll();
        return;
      }
      carousel.scrollLeft += edgeScrollSpeedRef.current * frameScale;
    };
    tick();
    edgeScrollTimerRef.current = setInterval(tick, 16);
  };
  const handleCarouselPointerMove = (event) => {
    const intent = getEdgeIntent(event);
    if (intent.dir) {
      startEdgeScroll(intent.dir, intent.strength);
    } else {
      stopEdgeScroll();
    }
  };
  const handleSelectPage = (pageKey, event) => {
    const eventType = event && event.type;
    const isHoverEvent = eventType === "pointerenter" ||
      eventType === "pointermove" ||
      eventType === "mouseenter" ||
      eventType === "mousemove";
    if (isHoverEvent && (getEdgeIntent(event).inEdge || isEdgeScrollMoving())) {
      return;
    }
    if (eventType === "click" || eventType === "keydown" || eventType === "focus") {
      clearEdgeScroll();
    }
    if (pageKey !== selectedKey) setSelectedKey(pageKey);
  };

  useEH3(() => {
    if (!activeKey || !carouselRef.current) return;
    if (isEdgeScrollMoving()) return;
    const carousel = carouselRef.current;
    const target = carousel.querySelector(`[data-page-key="${activeKey}"]`);
    if (!target) return;
    const left = target.offsetLeft - ((carousel.clientWidth - target.offsetWidth) / 2);
    carousel.scrollTo({ left: Math.max(0, left), behavior: "smooth" });
  }, [activeKey]);

  useEH3(() => () => clearEdgeScroll(), []);

  return (
    <div className="hero-phone-preview-stack is-carousel" ref={stackRef}>
      <div className="hero-phone-zoom-head">
        <div>
          <span className="hfp-eyebrow">Screen carousel</span>
          <strong>{selectedPage ? selectedPage.title : "App screens"}</strong>
        </div>
      </div>
      <div
        className="hero-phone-carousel-shell"
        onPointerMove={handleCarouselPointerMove}
        onPointerLeave={stopEdgeScroll}
        onMouseMove={handleCarouselPointerMove}
        onMouseLeave={stopEdgeScroll}
      >
        <div className={"hero-phone-preview-carousel " + className} ref={carouselRef} aria-label="Expanded ToyCon app screen carousel">
          <HeroPhoneSceneRow
            verticalKey={verticalKey}
            className={className + " hero-phone-carousel-row"}
            selectedKey={activeKey}
            allPageKeys={pageKeys}
            onSelectPage={handleSelectPage}
          />
          <HeroPhoneFeatureRow
            verticalKey={verticalKey}
            className={className + " hero-phone-carousel-row"}
            selectedKey={activeKey}
            allPageKeys={pageKeys}
            onSelectPage={handleSelectPage}
          />
        </div>
        <div
          className="hero-phone-scroll-zone hero-phone-scroll-zone-left"
          aria-hidden="true"
        ></div>
        <div
          className="hero-phone-scroll-zone hero-phone-scroll-zone-right"
          aria-hidden="true"
        ></div>
      </div>
      {selectedPage && (
        <div className="hero-phone-detail-panel" role="region" aria-live="polite" aria-label={selectedPage.title + " details"}>
          <div className="hfp-index">
            <span>{String(pageKeys.indexOf(activeKey) + 1).padStart(2, "0")}</span>
            <em>{pageKeys.length} screens</em>
          </div>
          <div className="hfp-copy">
            <span className="hfp-eyebrow">{selectedPage.eyebrow}</span>
            <h3>{selectedPage.title}</h3>
            <p>{selectedPage.description}</p>
            {!!selectedPage.bullets.length && (
              <ul>
                {selectedPage.bullets.map((bullet) => <li key={bullet}>{bullet}</li>)}
              </ul>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

window.HeroPhone3D = HeroPhone3D;
window.HeroPhoneSceneRow = HeroPhoneSceneRow;
window.HeroPhoneFeatureRow = HeroPhoneFeatureRow;
window.HeroPhonePreviewStack = HeroPhonePreviewStack;
