// Guestmaker · CDP hero · the Resolution Workbench
// LEFT  intake queue · raw guest records arriving from multiple PMS instances
// CTR   dark workbench · 3-stage pipeline (deterministic ticks · fuzzy bars · AI reasoning)
// RIGHT golden record · canonical profile with per-field hotel provenance
const { useState, useEffect, useRef, useMemo } = React;

// ─── tokens ───
const FUCHSIA      = "#ED4D86";
const FUCHSIA_DEEP = "#C93A6B";
const FUCHSIA_DARK = "#8E2148";
const FUCHSIA_SOFT = "#F8B7CF";
const FUCHSIA_PALE = "#FCE4EE";
const PLUM         = "#211A22";

const INK    = "#1a1a1a";
const DIM    = "#6e6363";
const FAINT  = "#a99fa3";
const RULE   = "#e3ddcf";
const PAPER  = "#fbfaf6";
const CREAM  = "#f6f4ef";
const SURFACE = "#ffffff";

const GREEN  = "#3b8d5a";
const AMBER  = "#b06a2a";
const RED    = "#a23a3a";

const MONO   = "'JetBrains Mono', ui-monospace, monospace";
const SERIF  = "var(--f-display)";

// ─── icons ───
function Icon({ name, size = 14, color, strokeWidth = 1.8, style }) {
  const stroke = color || "currentColor";
  const props = {
    width: size, height: size, viewBox: "0 0 24 24", fill: "none",
    stroke, strokeWidth, strokeLinecap: "round", strokeLinejoin: "round",
    style: { display: "block", flexShrink: 0, ...style },
  };
  switch (name) {
    case "building":   return <svg {...props}><rect x="4" y="3" width="16" height="18" rx="1"/><path d="M9 7h1M14 7h1M9 11h1M14 11h1M9 15h1M14 15h1M10 21v-4h4v4"/></svg>;
    case "building2":  return <svg {...props}><path d="M3 21V8l9-5 9 5v13"/><path d="M9 21V13h6v8M9 9h.01M15 9h.01"/></svg>;
    case "palm":       return <svg {...props}><path d="M12 21V9"/><path d="M12 9C9 6 5 7 4 11"/><path d="M12 9C15 6 19 7 20 11"/><path d="M12 9C10 5 12 2 16 3"/><path d="M12 9C14 5 12 2 8 3"/></svg>;
    case "anchor":     return <svg {...props}><circle cx="12" cy="5" r="2"/><path d="M12 7v14M5 12h2a5 5 0 0 0 10 0h2"/><path d="M3 17l2 2M21 17l-2 2"/></svg>;
    case "mountain":   return <svg {...props}><path d="M3 21l7-13 4 7 3-5 4 11z"/><circle cx="17" cy="6" r="1.2"/></svg>;
    case "globe":      return <svg {...props}><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>;
    case "globe-sdk":  return <svg {...props}><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><ellipse cx="12" cy="12" rx="4" ry="9"/></svg>;
    case "shield":     return <svg {...props}><path d="M12 2 4 5v7c0 5 3.5 9 8 10 4.5-1 8-5 8-10V5l-8-3z"/><path d="M8.5 12.5l2.5 2.5L16 9.5"/></svg>;
    case "merge":      return <svg {...props}><path d="M6 21V8a5 5 0 0 1 5-5h2a5 5 0 0 1 5 5v13"/><path d="M2 12l4-4 4 4M16 12l4-4 4 4"/></svg>;
    case "spark":      return <svg {...props}><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/><circle cx="12" cy="12" r="3"/></svg>;
    case "brain":      return <svg {...props}><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z"/></svg>;
    case "play":       return <svg {...props} fill={stroke}><polygon points="6,4 20,12 6,20"/></svg>;
    case "pause":      return <svg {...props} fill={stroke}><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>;
    case "check":      return <svg {...props}><polyline points="4,12 10,18 20,6"/></svg>;
    case "x":          return <svg {...props}><path d="M18 6L6 18M6 6l12 12"/></svg>;
    case "dash":       return <svg {...props}><line x1="6" y1="12" x2="18" y2="12"/></svg>;
    case "equal":      return <svg {...props}><path d="M5 9h14M5 15h14"/></svg>;
    case "user":       return <svg {...props}><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>;
    case "mail":       return <svg {...props}><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></svg>;
    case "phone":      return <svg {...props}><path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3.1-8.7A2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1 1 .3 1.9.6 2.8a2 2 0 0 1-.5 2.1L8 9.9a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.5 2.8.6a2 2 0 0 1 1.8 2z"/></svg>;
    case "id":         return <svg {...props}><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="12" r="2.5"/><path d="M14 10h5M14 14h3M5 17c1-1.5 2.5-2.2 4-2.2s3 .7 4 2.2"/></svg>;
    case "cal":        return <svg {...props}><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M16 3v4M8 3v4M3 11h18"/></svg>;
    case "fingerprint":return <svg {...props}><path d="M6 18c2-4 2-8 0-12"/><path d="M9 20c3-5 3-11 0-16"/><path d="M12 21c3-6 3-12 0-18"/><path d="M15 20c2-4 2-9 0-14"/><path d="M18 18c1-3 1-7 0-10"/></svg>;
    case "target":     return <svg {...props}><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill={stroke}/></svg>;
    case "alert":      return <svg {...props}><path d="M12 2 2 22h20L12 2zM12 9v5M12 18v.5"/></svg>;
    case "chevron-r":  return <svg {...props}><polyline points="9,6 15,12 9,18"/></svg>;
    case "arrow-r":    return <svg {...props}><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12,5 19,12 12,19"/></svg>;
    case "earth":      return <svg {...props}><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/></svg>;
    case "lang":       return <svg {...props}><path d="M5 8h14M9 5l1 13M19 14l-4 7M15 14l4 7M14 17h6"/></svg>;
    default: return null;
  }
}

// ─── sources (5 hotels + web SDK) ───
const SOURCES = [
  { id: "mvl", code: "MVL", title: "Mountain View Lodge", pms: "Apaleo",     volume: "84/day",   icon: "mountain"  },
  { id: "dch", code: "DCH", title: "Downtown City Hotel", pms: "Oracle Opera", volume: "127/day", icon: "building2" },
  { id: "sbr", code: "SBR", title: "Sunset Beach Resort", pms: "Mews",       volume: "96/day",   icon: "palm"      },
  { id: "msu", code: "MSU", title: "Marina Suites",       pms: "Cloudbeds",  volume: "52/day",   icon: "anchor"    },
  { id: "ogv", code: "OGV", title: "Olive Grove Retreat", pms: "Protel",     volume: "31/day",   icon: "building"  },
  { id: "wsk", code: "WEB", title: "Web SDK · Visitors",  pms: "Anonymous",  volume: "5,830 ses",icon: "globe-sdk" },
];
const SRC_BY_ID = Object.fromEntries(SOURCES.map(s => [s.id, s]));

// ─── golden records ───
const GOLDEN = [
  { id: "GR1", cid: "G-04821", name: "Sergio Wagner",   nat: "DE", dob: "4/25/1961",
    sources: ["mvl","dch","sbr"], stays: 3, spend: 6615, quality: 92, conf: 100 },
  { id: "GR2", cid: "G-04822", name: "Noah Fischer",    nat: "US", dob: "9/01/1993",
    sources: ["ogv","dch","mvl"], stays: 2, spend: 4180, quality: 84, conf: 91, viaAi: true },
  { id: "GR3", cid: "G-04823", name: "Isabelle Flores", nat: "ES", dob: "7/14/1987",
    sources: ["msu","dch","ogv"], stays: 3, spend: 9693, quality: 96, conf: 95 },
  { id: "GR4", cid: "G-04824", name: "Maria Johnson",   nat: "AT", dob: "3/11/1979",
    sources: ["mvl","sbr","ogv"], stays: 3, spend: 7604, quality: 88, conf: 92 },
  { id: "GR5", cid: "G-04825", name: "Hugo Davis",      nat: "CZ", dob: "8/19/1971",
    sources: ["msu","sbr"], stays: 1, spend: 6188, quality: 75, conf: 100 },
];
const GR_BY_ID = Object.fromEntries(GOLDEN.map(g => [g.id, g]));

// ─── stages ───
const STAGES = [
  { id: "det",   label: "DETERMINISTIC", sub: "email · phone · doc · composite",  color: GREEN,   icon: "fingerprint", band: "≥ 99%" },
  { id: "fuzzy", label: "FUZZY MATCH",   sub: "9-feature weighted score",          color: AMBER,   icon: "target",      band: "0.70 – 0.90" },
  { id: "ai",    label: "AI VERIFY",     sub: "Claude · the long tail",            color: FUCHSIA, icon: "brain",       band: "merges > 85%" },
];

// ─── packets · the timeline of resolution decisions ───
// Each packet is one raw candidate flowing through the workbench.
// `stage` = the deepest stage it reached (det / fuzzy / ai).
// `kind`  = the verdict (new / merge / review / skip).
// `det`   = field tick results (true / false / null) for stage 1.
// `feats` = 9-feature scores for stage 2.
// `reason`= AI verdict text for stage 3.
const PACKETS = [
  { t: 0.6, src: "mvl", target: "GR1", raw: { pms: "pms-67",  name: "Sergio Wagner", email: "swagner@orange.fr",    phone: "+49 632 497 142", nat: "DE", dob: "1961-04-25" },
    stage: "det", kind: "new", conf: 100,
    det: { email: null, phone: null, doc: null },
    reason: "First sighting. New canonical profile created." },

  { t: 2.0, src: "dch", target: "GR1", raw: { pms: "pms-284", name: "Sean Wagner",    email: "swagner@orange.fr",   phone: "+353 046 803 901",nat: "IE", dob: "1961-04-25" },
    stage: "ai", kind: "merge", conf: 88,
    det: { email: true, phone: false, doc: null },
    feats: { fam:1.00, given:0.62, dob:1.00, doc:0.0, nat:0.0, gender:1.0, stay:0.4, hotels:0.3, channel:1.0 },
    composite: 0.78,
    reason: "Different given name (Sean vs Sergio) but same email and DOB. Sean is a common Anglicisation. Merge with high confidence." },

  { t: 3.6, src: "sbr", target: "GR1", raw: { pms: "pms-141", name: "Sergio Wagner",  email: "swagner@orange.fr",   phone: "+49 632 497 142", nat: "DE", dob: "1961-04-25" },
    stage: "det", kind: "merge", conf: 100,
    det: { email: true, phone: true, doc: null },
    reason: "Email + phone exact match. Auto-merge." },

  { t: 5.0, src: "msu", target: "GR5", raw: { pms: "pms-22",  name: "Hugo Davis",     email: "hdavis@outlook.com",  phone: "+420 769 846 413", nat: "CZ", dob: "1971-08-19" },
    stage: "det", kind: "new", conf: 100,
    det: { email: null, phone: null, doc: null },
    reason: "First sighting. New canonical profile created." },

  { t: 6.4, src: "ogv", target: "GR2", raw: { pms: "pms-11",  name: "Noah Fischer",   email: "noahfischer98@web.de",phone: "+1 570 722 861",   nat: "US", dob: "1993-09-01" },
    stage: "det", kind: "new", conf: 100,
    det: { email: null, phone: null, doc: null },
    reason: "First sighting. New canonical profile created." },

  { t: 7.8, src: "dch", target: "GR2", raw: { pms: "pms-285", name: "Noah Fischer",   email: "noahfischer45@orange.fr",phone: "+1 570 722 861",nat: "US", dob: "1993-09-01" },
    stage: "ai", kind: "merge", conf: 91,
    det: { email: false, phone: true, doc: null },
    feats: { fam:1.00, given:1.00, dob:1.00, doc:0.0, nat:1.0, gender:1.0, stay:0.7, hotels:0.4, channel:0.6 },
    composite: 0.82,
    reason: "Same phone (+1570722861), identical name and DOB. Different email domains (orange.fr vs web.de) consistent with multiple accounts. Merge." },

  { t: 9.4, src: "mvl", target: "GR4", raw: { pms: "pms-70",  name: "Maria Johnson",  email: "mjohnson@icloud.com", phone: "+43 740 179 039",  nat: "AT", dob: "1979-03-11" },
    stage: "det", kind: "new", conf: 100,
    det: { email: null, phone: null, doc: null },
    reason: "First sighting. New canonical profile created." },

  { t: 10.8, src: "sbr", target: "GR4", raw: { pms: "pms-142", name: "Maria Johnson", email: "mjohnson@icloud.com", phone: "+43 740 179 039", nat: "AT", dob: "1979-03-11" },
    stage: "fuzzy", kind: "merge", conf: 93,
    det: { email: true, phone: true, doc: null },
    feats: { fam:1.00, given:1.00, dob:1.00, doc:0.0, nat:1.0, gender:1.0, stay:0.6, hotels:0.4, channel:0.7 },
    composite: 0.93,
    reason: "Composite 0.93 ≥ 0.90 threshold. Auto-merge without AI review." },

  { t: 12.2, src: "msu", target: "GR3", raw: { pms: "pms-23",  name: "Isabelle Flores", email: "iflores@gmail.com", phone: "+34 338 465 991", nat: "ES", dob: "1987-07-14" },
    stage: "det", kind: "new", conf: 100,
    det: { email: null, phone: null, doc: null },
    reason: "First sighting. New canonical profile created." },

  { t: 13.6, src: "dch", target: "GR3", raw: { pms: "pms-286", name: "Isabelle Flores", email: "iflores@gmail.com", phone: "+34 338 465 991", nat: "ES", dob: "1987-07-14" },
    stage: "det", kind: "merge", conf: 100,
    det: { email: true, phone: true, doc: null },
    reason: "Deterministic email + phone match. Auto-merge." },

  { t: 15.0, src: "ogv", target: "GR3", raw: { pms: "pms-12",  name: "Isabel Flores",  email: "iflores87@gmail.com", phone: "+34 338 465 991",nat: "ES", dob: "1987-07-14" },
    stage: "fuzzy", kind: "merge", conf: 95,
    det: { email: false, phone: true, doc: null },
    feats: { fam:1.00, given:0.84, dob:1.00, doc:0.0, nat:1.0, gender:1.0, stay:0.5, hotels:0.5, channel:0.8 },
    composite: 0.95,
    reason: "Name variation (Isabel vs Isabelle). Composite 0.95 ≥ 0.90. Auto-merge." },

  { t: 16.6, src: "dch", target: "GR3", raw: { pms: "pms-291", name: "Inés Flores",    email: "ines.flores@orange.fr", phone: "+33 156 412 098", nat: "FR", dob: "1990-02-08" },
    stage: "ai", kind: "skip", conf: 64,
    det: { email: false, phone: false, doc: null },
    feats: { fam:1.00, given:0.31, dob:0.0, doc:0.0, nat:0.0, gender:1.0, stay:0.0, hotels:0.0, channel:0.0 },
    composite: 0.64,
    reason: "Composite 0.64 below 0.70 threshold. Different DOB and nationality · likely a different person. Skipped." },

  { t: 18.0, src: "mvl", target: "GR2", raw: { pms: "pms-71",  name: "Noah Fischer",   email: "noah.fischer@web.de", phone: "+1 570 722 861", nat: "US", dob: "1993-09-01" },
    stage: "fuzzy", kind: "merge", conf: 92,
    det: { email: false, phone: true, doc: null },
    feats: { fam:1.00, given:1.00, dob:1.00, doc:0.0, nat:1.0, gender:1.0, stay:0.5, hotels:0.5, channel:0.7 },
    composite: 0.92,
    reason: "Composite 0.92 ≥ 0.90. Auto-merge." },

  { t: 19.4, src: "wsk", target: "GR1", raw: { pms: "anon-83af",name: "anonymous",     email: "swagner@orange.fr",   phone: "·",               nat: "·",  dob: "·" },
    stage: "det", kind: "merge", conf: 100,
    det: { email: true, phone: null, doc: null },
    reason: "Newsletter signup. Email matches existing profile. Visitor → profile linked." },

  { t: 20.8, src: "ogv", target: "GR4", raw: { pms: "pms-15",  name: "Maria Johnson",  email: "m.johnson@icloud.com",phone: "+43 740 179 039", nat: "AT", dob: "1979-03-11" },
    stage: "det", kind: "merge", conf: 100,
    det: { email: false, phone: true, doc: null },
    reason: "Phone E.164 exact match. Auto-merge." },

  { t: 22.2, src: "sbr", target: "GR5", raw: { pms: "pms-148", name: "Hugo Davis",     email: "hdavis@outlook.com",  phone: "+420 769 846 413",nat: "CZ", dob: "1971-08-19" },
    stage: "det", kind: "merge", conf: 100,
    det: { email: true, phone: true, doc: null },
    reason: "Email + phone exact match. Auto-merge." },
];
const TIMELINE_END = 23.6;
const PACKET_HOLD = 1.4; // how long each packet is "current" on the workbench

// ─── playback hook ───
function usePlayback(speed = 1) {
  const [time, setTime] = useState(0);
  const [playing, setPlaying] = useState(true);
  const lastRef = useRef(performance.now());
  const speedRef = useRef(speed);
  useEffect(() => { speedRef.current = speed; }, [speed]);

  useEffect(() => {
    let raf;
    const loop = (now) => {
      const dt = (now - lastRef.current) / 1000;
      lastRef.current = now;
      if (playing) {
        setTime(t => {
          const nt = t + dt * speedRef.current;
          return nt >= TIMELINE_END ? 0 : nt;
        });
      }
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(n => { lastRef.current = n; loop(n); });
    return () => cancelAnimationFrame(raf);
  }, [playing]);

  return { time, setTime, playing, setPlaying };
}

// ─── atoms ───
function Eyebrow({ children, color = FUCHSIA, style }) {
  return <div style={{
    fontFamily: MONO, fontSize: 10.5, color, fontWeight: 600,
    letterSpacing: "0.18em", textTransform: "uppercase", ...style,
  }}>{children}</div>;
}
function Mono({ children, size = 10.5, color = DIM, style }) {
  return <span style={{
    fontFamily: MONO, fontSize: size, color, letterSpacing: "0.04em", ...style,
  }}>{children}</span>;
}
function Dot({ color, size = 6, pulse = false, style }) {
  return <span style={{
    display: "inline-block", width: size, height: size, borderRadius: "50%",
    background: color, animation: pulse ? "gm-dot-pulse 1.4s ease-in-out infinite" : "none",
    flexShrink: 0, ...style,
  }} />;
}
function fmt(t) {
  const m = Math.floor(t / 60); const s = Math.floor(t % 60);
  return `${m}:${String(s).padStart(2, "0")}`;
}
function fmtNum(n) { return n.toLocaleString("en-US"); }

// Find the packet currently "on the workbench" given a time
function currentPacket(time) {
  // last packet whose t <= time
  for (let i = PACKETS.length - 1; i >= 0; i--) {
    if (PACKETS[i].t <= time) return { p: PACKETS[i], age: time - PACKETS[i].t };
  }
  return null;
}

// Compute golden state at given time
function goldenStateAt(grId, time) {
  const gr = GR_BY_ID[grId];
  const fieldSources = {
    name: new Set(), email: new Set(), phone: new Set(),
    dob: new Set(), nat: new Set(), doc: new Set(),
  };
  let sourceSet = new Set();
  let conf = 0, stays = 0, spend = 0;
  let bornT = null, lastT = -10, lastKind = null, lastDelta = null, hotelsAdded = [];

  for (const p of PACKETS) {
    if (p.target !== grId || time < p.t) continue;
    if (p.kind === "skip") continue;
    const isNew = !sourceSet.has(p.src);
    if (isNew) { hotelsAdded.push({ src: p.src, at: p.t }); }
    if (p.kind === "new") {
      bornT = p.t;
      sourceSet.add(p.src);
      fieldSources.name.add(p.src);
      fieldSources.email.add(p.src);
      fieldSources.phone.add(p.src);
      fieldSources.dob.add(p.src);
      fieldSources.nat.add(p.src);
      conf = p.conf;
      stays = 1; spend = Math.round(1500 + Math.random() * 0); // deterministic-ish
    } else if (p.kind === "merge" || p.kind === "review") {
      sourceSet.add(p.src);
      // attribute the matching fields
      if (p.det) {
        if (p.det.email === true)  fieldSources.email.add(p.src);
        if (p.det.phone === true)  fieldSources.phone.add(p.src);
        if (p.det.doc === true)    fieldSources.doc.add(p.src);
      }
      // name/dob/nat usually also contributed
      fieldSources.name.add(p.src);
      fieldSources.dob.add(p.src);
      fieldSources.nat.add(p.src);
      conf = Math.max(conf, p.conf);
      stays += 1; spend += 1800;
    }
    lastT = p.t; lastKind = p.kind;
    lastDelta = { src: p.src, kind: p.kind, t: p.t };
  }
  if (!sourceSet.size) return null;
  return { gr, sourceSet, fieldSources, conf, stays, spend, bornT, lastT, lastKind, lastDelta, hotelsAdded };
}

// ─── Sources strip (above the workbench) ───
function SourcesStrip({ time }) {
  const cur = currentPacket(time);
  const currentSrc = cur?.p.src;
  const recentSrc = cur && cur.age < 1.0 ? currentSrc : null;
  return (
    <ScaleFrame nativeWidth={1216} style={{ maxWidth: 1216, margin: "0 auto" }}>
    <div style={{
      maxWidth: 1216, margin: "0 auto 14px",
      display: "flex", alignItems: "center", gap: 10,
      padding: "10px 16px",
      background: PAPER, border: `1px solid ${RULE}`, borderRadius: 999,
    }}>
      <Mono size={9.5} color={FAINT} style={{ letterSpacing: "0.18em", fontWeight: 700 }}>
        INTAKE SOURCES · 5 PROPERTIES + WEB SDK
      </Mono>
      <span style={{ flex: 1, height: 1, background: RULE }} />
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        {SOURCES.map(s => {
          const hot = recentSrc === s.id;
          return (
            <div key={s.id} title={`${s.title} · ${s.pms}`} style={{
              display: "inline-flex", alignItems: "center", gap: 5,
              padding: "3px 8px 3px 6px",
              borderRadius: 999,
              background: hot ? FUCHSIA + "18" : "#fff",
              border: `1px solid ${hot ? FUCHSIA + "AA" : RULE}`,
              color: hot ? FUCHSIA_DEEP : DIM,
              transition: "all 0.45s ease",
              boxShadow: hot ? `0 0 0 3px ${FUCHSIA}14` : "none",
            }}>
              <Icon name={s.icon} size={11} strokeWidth={2.1} />
              <Mono size={9.5} color={hot ? FUCHSIA_DEEP : INK} style={{ fontWeight: 700, letterSpacing: "0.1em" }}>{s.code}</Mono>
              <Mono size={9} color={hot ? FUCHSIA_DEEP : FAINT} style={{ letterSpacing: "0.04em" }}>{s.volume}</Mono>
            </div>
          );
        })}
      </div>
    </div>
    </ScaleFrame>
  );
}

// ─── LEFT · raw intake queue ───
function RawCard({ p, role }) {
  // role: active | recent | past | queued
  const s = SRC_BY_ID[p.src];
  const isActive = role === "active";
  const isQueued = role === "queued";
  const opacity = role === "active" ? 1 : role === "recent" ? 0.82 : role === "past" ? 0.55 : 0.40;
  return (
    <div style={{
      position: "relative",
      background: isActive ? SURFACE : PAPER,
      border: `1px solid ${isActive ? FUCHSIA + "AA" : RULE}`,
      borderRadius: 9,
      padding: "9px 11px",
      boxShadow: isActive ? `0 16px 28px -16px ${FUCHSIA}AA, 0 0 0 3px ${FUCHSIA}14` : "none",
      transition: "all 0.4s ease",
      opacity,
      transform: isActive ? "translateY(0)" : `translateY(0)`,
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 4 }}>
        <span style={{
          display: "inline-flex", alignItems: "center", gap: 4,
          padding: "2px 6px", borderRadius: 4,
          background: isActive ? FUCHSIA + "16" : "#f1ebde",
          border: `1px solid ${isActive ? FUCHSIA + "55" : "#ddd6c5"}`,
          color: isActive ? FUCHSIA_DEEP : DIM,
        }}>
          <Icon name={s.icon} size={9} strokeWidth={2.2} />
          <Mono size={8.5} style={{ fontWeight: 700, letterSpacing: "0.12em", color: "inherit" }}>{s.code}</Mono>
        </span>
        <Mono size={8.5} color={FAINT} style={{ letterSpacing: "0.08em" }}>{p.raw.pms}</Mono>
        <span style={{ flex: 1 }} />
        {isActive && (
          <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
            <Dot color={FUCHSIA} size={5} pulse />
            <Mono size={8.5} color={FUCHSIA} style={{ fontWeight: 700, letterSpacing: "0.14em" }}>NOW</Mono>
          </span>
        )}
        {isQueued && (
          <Mono size={8.5} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>NEXT</Mono>
        )}
      </div>
      <div style={{
        fontFamily: SERIF, fontSize: 13.5, color: INK, fontWeight: 600,
        letterSpacing: "-0.005em", marginBottom: 2,
        whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
      }}>{p.raw.name}</div>
      <div style={{
        fontFamily: MONO, fontSize: 9.5, color: DIM, letterSpacing: "0.02em",
        whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
      }}>
        {p.raw.email} · {p.raw.phone}
      </div>
      <div style={{
        fontFamily: MONO, fontSize: 9, color: FAINT, marginTop: 1, letterSpacing: "0.04em",
      }}>
        {p.raw.nat} · DOB {p.raw.dob}
      </div>
    </div>
  );
}

function IntakeQueue({ time }) {
  // 1 current + 3 past + 1 queued
  const past = PACKETS.filter(p => p.t <= time).sort((a,b) => b.t - a.t).slice(0, 4);
  const next = PACKETS.filter(p => p.t > time).sort((a,b) => a.t - b.t).slice(0, 1);
  const display = [
    ...past.map((p, i) => ({ p, role: i === 0 ? "active" : i === 1 ? "recent" : "past" })),
    ...next.map(p => ({ p, role: "queued" })),
  ].slice(0, 4);

  return (
    <div style={{
      width: 280, height: 540,
      background: PAPER,
      border: `1px solid ${RULE}`,
      borderRadius: 12,
      padding: "14px 14px 12px",
      display: "flex", flexDirection: "column",
      boxShadow: "0 1px 0 rgba(0,0,0,0.02)",
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
        <Icon name="arrow-r" size={13} color={FUCHSIA} strokeWidth={2.2} />
        <Eyebrow color={DIM}>RAW INTAKE</Eyebrow>
      </div>
      <Mono size={10} color={FAINT} style={{ letterSpacing: "0.08em", marginBottom: 12 }}>
        post-checkout · per-property push
      </Mono>
      <div style={{ display: "flex", flexDirection: "column", gap: 8, flex: 1 }}>
        {display.map((d, i) => (
          <RawCard key={`${d.p.t}-${i}`} p={d.p} role={d.role} />
        ))}
      </div>
      <div style={{
        marginTop: 10, padding: "8px 11px",
        background: "#fff", border: `1px dashed ${RULE}`, borderRadius: 7,
        display: "flex", justifyContent: "space-between", alignItems: "center",
      }}>
        <Mono size={9} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 600 }}>TODAY · INGESTED</Mono>
        <Mono size={13} color={INK} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>18,420</Mono>
      </div>
    </div>
  );
}

// ─── CENTER · the dark resolution workbench ───
function DetFieldRow({ name, label, result, icon, isHot }) {
  // result: true (match) / false (mismatch) / null (n/a)
  const iconName = result === true ? "check" : result === false ? "x" : "dash";
  const color = result === true ? GREEN : result === false ? "#a86060" : "#5e5660";
  return (
    <div style={{
      display: "flex", alignItems: "center", gap: 8,
      padding: "5px 9px", borderRadius: 5,
      background: result === true ? "rgba(59,141,90,0.08)" : "transparent",
      border: `1px solid ${result === true ? "rgba(59,141,90,0.35)" : "rgba(255,255,255,0.06)"}`,
      transition: "all 0.3s ease",
    }}>
      <Icon name={icon} size={10} color="#9b8f95" strokeWidth={2.1} />
      <Mono size={9.5} color="#cdbcc4" style={{ letterSpacing: "0.04em", fontWeight: 600, width: 50 }}>{label}</Mono>
      <span style={{ flex: 1 }} />
      <div style={{
        width: 16, height: 16, borderRadius: 4,
        background: result === true ? GREEN : result === false ? "#3a2222" : "transparent",
        border: `1px solid ${color}`,
        display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        <Icon name={iconName} size={9} color={result === true ? "#fff" : color} strokeWidth={2.6} />
      </div>
    </div>
  );
}

function StageDet({ packet, active, status }) {
  // status: pending / running / passed / skipped
  const stage = STAGES[0];
  const dim = !active && status !== "passed";
  const showDecide = active && status === "running";
  const dec = packet?.det || {};
  return (
    <StageShell stage={stage} idx={0} active={active} status={status}>
      {packet && (
        <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
          <DetFieldRow label="EMAIL"   result={dec.email}  icon="mail"   isHot={active} />
          <DetFieldRow label="PHONE"   result={dec.phone}  icon="phone"  isHot={active} />
          <DetFieldRow label="DOC"     result={dec.doc}    icon="id"     isHot={active} />
        </div>
      )}
    </StageShell>
  );
}

const FEATURES = [
  { id: "fam",     label: "family_name",   w: 0.25 },
  { id: "given",   label: "given_name",    w: 0.20 },
  { id: "dob",     label: "date_of_birth", w: 0.20 },
  { id: "doc",     label: "document",      w: 0.10 },
  { id: "nat",     label: "nationality",   w: 0.08 },
  { id: "gender",  label: "gender",        w: 0.05 },
  { id: "stay",    label: "stay_dates",    w: 0.05 },
  { id: "channel", label: "booking_chan",  w: 0.05 },
  { id: "hotels",  label: "hotels_overlap",w: 0.02 },
];

function FeatureBar({ feat, value, isHot }) {
  const pct = Math.round((value || 0) * 100);
  const color = value >= 0.85 ? GREEN : value >= 0.5 ? AMBER : "#a86060";
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
      <Mono size={8.5} color="#9b8f95" style={{ letterSpacing: "0.04em", width: 88, fontWeight: 600 }}>
        {feat.label}
      </Mono>
      <div style={{
        flex: 1, height: 4, background: "#2a2226", borderRadius: 2, overflow: "hidden", position: "relative",
      }}>
        <div style={{
          width: `${pct}%`, height: "100%",
          background: color, borderRadius: 2,
          transition: "width 0.55s cubic-bezier(0.4,0,0.2,1)",
        }} />
      </div>
      <Mono size={8.5} color={color} style={{
        width: 30, textAlign: "right", fontWeight: 700, fontVariantNumeric: "tabular-nums", letterSpacing: "0.04em",
      }}>
        {value === undefined ? "·" : value.toFixed(2)}
      </Mono>
      <Mono size={7.5} color="#5e5660" style={{ width: 30, letterSpacing: "0.04em" }}>
        ×{feat.w.toFixed(2)}
      </Mono>
    </div>
  );
}

function StageFuzzy({ packet, active, status }) {
  const stage = STAGES[1];
  const feats = packet?.feats;
  const composite = packet?.composite;
  return (
    <StageShell stage={stage} idx={1} active={active} status={status}>
      {feats ? (
        <>
          <div style={{ display: "flex", flexDirection: "column", gap: 3, marginBottom: 7 }}>
            {FEATURES.map(f => <FeatureBar key={f.id} feat={f} value={feats[f.id]} isHot={active} />)}
          </div>
          <div style={{
            display: "flex", justifyContent: "space-between", alignItems: "center",
            padding: "5px 10px", borderRadius: 5,
            background: composite >= 0.90 ? "rgba(59,141,90,0.10)"
                      : composite >= 0.70 ? "rgba(176,106,42,0.10)"
                      : "rgba(162,58,58,0.10)",
            border: `1px solid ${composite >= 0.90 ? "rgba(59,141,90,0.45)"
                                : composite >= 0.70 ? "rgba(176,106,42,0.45)"
                                : "rgba(162,58,58,0.45)"}`,
          }}>
            <Mono size={9} color="#cdbcc4" style={{ letterSpacing: "0.12em", fontWeight: 700 }}>COMPOSITE</Mono>
            <Mono size={13} color={composite >= 0.90 ? GREEN : composite >= 0.70 ? AMBER : "#a86060"}
              style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>
              {composite?.toFixed(2)}
            </Mono>
            <Mono size={8.5} color="#9b8f95" style={{ letterSpacing: "0.12em", fontWeight: 600 }}>
              {composite >= 0.90 ? "AUTO-MERGE" : composite >= 0.70 ? "→ AI REVIEW" : "→ SKIP"}
            </Mono>
          </div>
        </>
      ) : (
        <div style={{
          fontFamily: SERIF, fontSize: 11, fontStyle: "italic", color: "#5e5660",
          textAlign: "center", padding: "16px 0",
        }}>
          deterministic match was sufficient · fuzzy not run
        </div>
      )}
    </StageShell>
  );
}

function StageAi({ packet, active, status }) {
  const stage = STAGES[2];
  const text = packet?.reason || "";
  // typewriter on active
  const [shown, setShown] = useState(text);
  useEffect(() => { setShown(text); }, [text]);

  return (
    <StageShell stage={stage} idx={2} active={active} status={status}>
      {packet && packet.stage === "ai" ? (
        <div style={{
          fontFamily: SERIF, fontSize: 12, color: "#e3d4dc",
          lineHeight: 1.55, fontStyle: "italic", letterSpacing: "0.005em",
          padding: "2px 0",
        }}>
          “{shown}”
        </div>
      ) : (
        <div style={{
          fontFamily: SERIF, fontSize: 11, fontStyle: "italic", color: "#5e5660",
          textAlign: "center", padding: "8px 0",
        }}>
          {packet?.stage === "fuzzy" && packet.composite >= 0.90
            ? "fuzzy auto-merged · AI not invoked"
            : packet?.stage === "det"
              ? "deterministic · AI not invoked"
              : "awaiting candidate…"}
        </div>
      )}
    </StageShell>
  );
}

function StageShell({ stage, idx, active, status, children }) {
  // status: passed / running / pending / skipped
  const dim = status === "pending" || status === "skipped";
  const lit = status === "running" || status === "passed";
  const c = stage.color;
  return (
    <div style={{
      background: lit ? "#1f1518" : "#161214",
      border: `1px solid ${active ? c + "AA" : lit ? c + "33" : "#2a2226"}`,
      borderRadius: 8,
      padding: "9px 11px",
      transition: "all 0.45s ease",
      position: "relative",
      boxShadow: active ? `0 0 0 3px ${c}1A, 0 16px 32px -20px ${c}99` : "none",
      opacity: dim ? 0.45 : 1,
    }}>
      {active && (
        <div style={{
          position: "absolute", left: 0, right: 0, top: 0, height: 2, borderRadius: "8px 8px 0 0",
          background: `linear-gradient(90deg, transparent, ${c}, transparent)`,
          animation: "gm-bar-scan 1.8s linear infinite",
        }} />
      )}
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 7 }}>
        <Mono size={9} color="#7a747a" style={{ fontWeight: 700, letterSpacing: "0.14em" }}>{String(idx+1).padStart(2,"0")}</Mono>
        <Icon name={stage.icon} size={11} color={lit ? c : "#5e5660"} strokeWidth={2.2} />
        <Mono size={9.5} color={lit ? c : "#7a747a"} style={{ fontWeight: 700, letterSpacing: "0.16em" }}>
          {stage.label}
        </Mono>
        <Mono size={8.5} color="#5e5660" style={{ letterSpacing: "0.04em" }}>· {stage.sub}</Mono>
        <span style={{ flex: 1 }} />
        <Mono size={8.5} color={lit ? c : "#5e5660"} style={{ letterSpacing: "0.12em", fontWeight: 700 }}>
          {status === "passed" ? "✓ PASSED"
            : status === "running" ? "RUNNING"
            : status === "skipped" ? "·"
            : "PENDING"}
        </Mono>
      </div>
      {children}
    </div>
  );
}

function ResolutionWorkbench({ time }) {
  const cur = currentPacket(time);
  const packet = cur?.p;
  const age = cur?.age ?? 0;
  // Determine stage activation:
  // - det stage is always "passed" or "running" if packet present
  // - fuzzy: passed if packet.stage in {fuzzy, ai}; skipped if det
  // - ai:    passed if packet.stage === ai
  const stageOrder = ["det", "fuzzy", "ai"];
  const deepest = packet ? stageOrder.indexOf(packet.stage) : -1;
  const getStatus = (i) => {
    if (!packet) return "pending";
    if (i < deepest) return "passed";
    if (i === deepest) return age < PACKET_HOLD ? "running" : "passed";
    return "skipped";
  };
  const det = getStatus(0);
  const fuz = getStatus(1);
  const ai  = getStatus(2);

  // verdict
  const kind = packet?.kind;
  const conf = packet?.conf ?? 0;
  const verdictColor = kind === "new" ? GREEN
    : kind === "skip" ? RED
    : kind === "review" ? PLUM
    : FUCHSIA;
  const verdictLabel = kind === "new" ? "NEW GOLDEN RECORD"
    : kind === "skip" ? "SKIPPED · BELOW THRESHOLD"
    : kind === "review" ? "ESCALATED · AI REVIEW"
    : "MERGED";
  const verdictIcon = kind === "new" ? "spark"
    : kind === "skip" ? "x"
    : kind === "review" ? "brain"
    : "merge";

  return (
    <div style={{
      width: 480, height: 540,
      background: "#1a1418",
      borderRadius: 14,
      padding: "18px 20px",
      color: "#f6f4ef",
      position: "relative",
      boxShadow: `0 40px 80px -36px ${FUCHSIA}BB, 0 0 0 1px ${FUCHSIA}25`,
      display: "flex", flexDirection: "column",
    }}>
      <div style={{
        position: "absolute", inset: -3, borderRadius: 17,
        background: `linear-gradient(135deg, ${FUCHSIA}, transparent 45%, ${PLUM})`,
        opacity: 0.25, zIndex: -1, pointerEvents: "none",
        filter: "blur(14px)",
      }} />

      {/* header */}
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 9 }}>
          <div style={{
            width: 28, height: 28, borderRadius: 7,
            background: FUCHSIA + "22",
            border: `1px solid ${FUCHSIA}55`,
            display: "flex", alignItems: "center", justifyContent: "center",
          }}>
            <Icon name="shield" size={15} color={FUCHSIA_SOFT} strokeWidth={1.9} />
          </div>
          <div>
            <Eyebrow color={FUCHSIA_SOFT}>CDP · RESOLUTION WORKBENCH</Eyebrow>
            <Mono size={9} color="#7a747a" style={{ letterSpacing: "0.12em", marginTop: 2 }}>
              one decision per candidate · 3 stages · auditable
            </Mono>
          </div>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 5 }}>
          <Dot color={FUCHSIA} size={6} pulse />
          <Mono size={9} color={FUCHSIA_SOFT} style={{ letterSpacing: "0.18em", fontWeight: 700 }}>RESOLVING</Mono>
        </div>
      </div>

      {/* candidate header */}
      <div style={{
        background: "#211a1d",
        border: "1px solid #2a2226",
        borderRadius: 7,
        padding: "8px 11px",
        marginBottom: 9,
        display: "flex", alignItems: "center", gap: 9,
      }}>
        <Mono size={9} color={FUCHSIA_SOFT} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>CANDIDATE</Mono>
        <div style={{
          fontFamily: SERIF, fontSize: 13, color: "#f6f4ef", fontWeight: 500,
          letterSpacing: "-0.005em", flex: 1,
          whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
        }}>
          {packet ? <>{packet.raw.name} <span style={{ color: "#8a808a", fontStyle: "italic" }}>· {packet.raw.pms}</span></> : "·"}
        </div>
        <span style={{
          padding: "2px 7px", borderRadius: 4,
          background: packet ? "rgba(255,255,255,0.05)" : "transparent",
          border: "1px solid #2a2226",
        }}>
          <Mono size={9} color="#cdbcc4" style={{ letterSpacing: "0.1em", fontWeight: 600 }}>
            ↔
          </Mono>
        </span>
        <div style={{
          fontFamily: SERIF, fontSize: 13, color: "#f6f4ef", fontWeight: 500,
          letterSpacing: "-0.005em",
          whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: 140,
        }}>
          {packet ? GR_BY_ID[packet.target].name : "·"}
        </div>
      </div>

      {/* the 3 stages */}
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        <StageDet   packet={packet} active={det === "running"}   status={det} />
        <StageFuzzy packet={packet} active={fuz === "running"}   status={fuz} />
        <StageAi    packet={packet} active={ai  === "running"}   status={ai}  />
      </div>

      <div style={{ flex: 1 }} />

      {/* verdict band */}
      {packet ? (
        <div key={packet.t} style={{
          marginTop: 12,
          padding: "10px 14px",
          borderRadius: 9,
          background: `linear-gradient(90deg, ${verdictColor}24, ${verdictColor}0E)`,
          border: `1px solid ${verdictColor}88`,
          display: "flex", alignItems: "center", gap: 10,
          boxShadow: `0 0 0 3px ${verdictColor}14, inset 0 0 0 1px rgba(255,255,255,0.04)`,
          animation: "gm-fadein 0.4s ease",
        }}>
          <Icon name={verdictIcon} size={14} color={verdictColor} strokeWidth={2.4} />
          <Mono size={11} color="#f6f4ef" style={{ letterSpacing: "0.18em", fontWeight: 700 }}>
            {verdictLabel}
          </Mono>
          <span style={{ flex: 1 }} />
          <Mono size={9} color="#cdbcc4" style={{ letterSpacing: "0.14em" }}>CONFIDENCE</Mono>
          <Mono size={18} color="#f6f4ef" style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums", letterSpacing: "-0.01em" }}>
            {conf}%
          </Mono>
        </div>
      ) : (
        <div style={{
          marginTop: 12,
          padding: "10px 14px",
          borderRadius: 9,
          background: "rgba(255,255,255,0.02)",
          border: "1px dashed #2a2226",
          display: "flex", alignItems: "center", gap: 10,
        }}>
          <Dot color="#5e5660" size={6} pulse />
          <Mono size={11} color="#7a747a" style={{ letterSpacing: "0.18em", fontWeight: 700 }}>
            AWAITING CANDIDATE
          </Mono>
          <span style={{ flex: 1 }} />
          <Mono size={9} color="#5e5660" style={{ letterSpacing: "0.14em" }}>IDLE</Mono>
        </div>
      )}
    </div>
  );
}

// ─── RIGHT · golden record output ───
function ProvenanceDots({ srcs, allHotels, recentSrc }) {
  // Show source-hotel chips for fields. `srcs` = Set of ids that contributed.
  return (
    <span style={{ display: "inline-flex", gap: 3 }}>
      {allHotels.map(id => {
        const has = srcs.has(id);
        const s = SRC_BY_ID[id];
        const flash = has && id === recentSrc;
        return (
          <span key={id} title={s.title} style={{
            display: "inline-flex", alignItems: "center", justifyContent: "center",
            width: 18, height: 18, borderRadius: 4,
            background: has ? (flash ? FUCHSIA + "33" : FUCHSIA + "16") : "transparent",
            border: `1px solid ${has ? (flash ? FUCHSIA : FUCHSIA + "55") : "#e3ddcf"}`,
            color: has ? FUCHSIA_DEEP : "#c0b6ab",
            transition: "all 0.4s ease",
            boxShadow: flash ? `0 0 0 2px ${FUCHSIA}26` : "none",
            animation: flash ? "gm-fadein 0.5s ease" : "none",
          }}>
            <Icon name={s.icon} size={10} strokeWidth={2.2} />
          </span>
        );
      })}
    </span>
  );
}

function GoldenField({ icon, label, value, srcs, hotels, recentSrc }) {
  return (
    <div style={{
      display: "grid", gridTemplateColumns: "16px 70px 1fr auto",
      alignItems: "center", gap: 8,
      padding: "6px 0",
      borderBottom: `1px dashed ${RULE}`,
    }}>
      <Icon name={icon} size={11} color={DIM} strokeWidth={2.1} />
      <Mono size={9} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 600 }}>{label}</Mono>
      <span style={{
        fontFamily: MONO, fontSize: 10.5, color: INK, fontWeight: 500,
        whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", letterSpacing: "0.02em",
      }}>{value}</span>
      <ProvenanceDots srcs={srcs} allHotels={hotels} recentSrc={recentSrc} />
    </div>
  );
}

function GoldenOutput({ time }) {
  const cur = currentPacket(time);
  if (!cur) return <div style={{ width: 280, height: 540 }} />;
  const { p: packet, age } = cur;
  const grId = packet.target;
  const state = goldenStateAt(grId, time);
  const gr = GR_BY_ID[grId];

  const hotels = gr.sources; // canonical list of hotels that ever contribute
  const recentSrc = age < 1.2 ? packet.src : null;

  // canonical field values (from gr)
  const fieldVals = {
    name:  gr.name,
    email: ({ GR1: "swagner@orange.fr", GR2: "noahfischer@web.de", GR3: "iflores@gmail.com", GR4: "mjohnson@icloud.com", GR5: "hdavis@outlook.com" })[gr.id],
    phone: ({ GR1: "+49 632 497 142",   GR2: "+1 570 722 861",      GR3: "+34 338 465 991", GR4: "+43 740 179 039",    GR5: "+420 769 846 413" })[gr.id],
    dob:   gr.dob,
    nat:   gr.nat,
    doc:   "·",
  };

  const hotelsLinked = state ? state.sourceSet.size : 0;
  const merging = packet.kind === "merge" && age < 1.2;
  const isNew = packet.kind === "new" && age < 1.0;

  return (
    <div style={{
      width: 280, height: 540,
      background: SURFACE,
      border: `1px solid ${merging ? FUCHSIA + "55" : RULE}`,
      borderRadius: 12,
      padding: "14px 14px 12px",
      display: "flex", flexDirection: "column",
      boxShadow: merging
        ? `0 22px 44px -22px ${FUCHSIA}AA, 0 0 0 3px ${FUCHSIA}14`
        : "0 1px 0 rgba(0,0,0,0.02), 0 16px 32px -22px rgba(20,14,10,0.18)",
      position: "relative",
      transition: "all 0.45s ease",
    }}>
      {/* header */}
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
        <Icon name="shield" size={13} color={FUCHSIA} strokeWidth={2.2} />
        <Eyebrow color={FUCHSIA}>GOLDEN RECORD</Eyebrow>
        <span style={{ flex: 1 }} />
        <Mono size={9} color={FAINT} style={{ letterSpacing: "0.12em", fontWeight: 600 }}>{gr.cid}</Mono>
      </div>
      <Mono size={10} color={FAINT} style={{ letterSpacing: "0.08em", marginBottom: 12 }}>
        canonical · per-field provenance
      </Mono>

      {/* avatar + name */}
      <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
        <div style={{
          width: 38, height: 38, borderRadius: 9, flexShrink: 0,
          background: `linear-gradient(135deg, ${FUCHSIA}, ${FUCHSIA_DEEP})`,
          display: "flex", alignItems: "center", justifyContent: "center", color: "#fff",
          boxShadow: "inset 0 -2px 4px rgba(0,0,0,0.18)",
        }}>
          <span style={{ fontFamily: SERIF, fontSize: 16, fontStyle: "italic", fontWeight: 600 }}>
            {gr.name.split(" ").map(s => s[0]).slice(0,2).join("").toUpperCase()}
          </span>
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{
            fontFamily: SERIF, fontSize: 16, color: INK, fontWeight: 600,
            letterSpacing: "-0.01em",
            whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
          }}>{gr.name}</div>
          <Mono size={9.5} color={DIM} style={{ letterSpacing: "0.06em" }}>
            {hotelsLinked} hotel{hotelsLinked === 1 ? "" : "s"} linked
          </Mono>
        </div>
      </div>

      {/* fields */}
      <div style={{ flex: 1 }}>
        <GoldenField icon="user"  label="NAME"   value={fieldVals.name}  srcs={state?.fieldSources.name  ?? new Set()} hotels={hotels} recentSrc={recentSrc} />
        <GoldenField icon="mail"  label="EMAIL"  value={fieldVals.email} srcs={state?.fieldSources.email ?? new Set()} hotels={hotels} recentSrc={recentSrc} />
        <GoldenField icon="phone" label="PHONE"  value={fieldVals.phone} srcs={state?.fieldSources.phone ?? new Set()} hotels={hotels} recentSrc={recentSrc} />
        <GoldenField icon="cal"   label="DOB"    value={fieldVals.dob}   srcs={state?.fieldSources.dob   ?? new Set()} hotels={hotels} recentSrc={recentSrc} />
        <GoldenField icon="earth" label="NAT"    value={fieldVals.nat}   srcs={state?.fieldSources.nat   ?? new Set()} hotels={hotels} recentSrc={recentSrc} />
        <GoldenField icon="id"    label="DOC"    value={fieldVals.doc}   srcs={state?.fieldSources.doc   ?? new Set()} hotels={hotels} recentSrc={recentSrc} />
      </div>

      {/* footer mini stats */}
      <div style={{
        display: "grid", gridTemplateColumns: "1fr 1fr",
        gap: 8, marginTop: 10,
      }}>
        <div style={{ padding: "6px 9px", background: PAPER, border: `1px solid ${RULE}`, borderRadius: 6 }}>
          <Mono size={8} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 600 }}>QUALITY</Mono>
          <div style={{ fontFamily: MONO, fontSize: 14, fontWeight: 700, color: INK, letterSpacing: "-0.005em" }}>
            {state ? Math.min(gr.quality, 60 + hotelsLinked * 12) : 0}%
          </div>
        </div>
        <div style={{ padding: "6px 9px", background: PAPER, border: `1px solid ${RULE}`, borderRadius: 6 }}>
          <Mono size={8} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 600 }}>CONFIDENCE</Mono>
          <div style={{ fontFamily: MONO, fontSize: 14, fontWeight: 700,
            color: gr.viaAi ? PLUM : (state?.conf ?? 0) >= 95 ? GREEN : AMBER,
            letterSpacing: "-0.005em",
          }}>
            {state?.conf ?? 0}%
          </div>
        </div>
      </div>

      {/* status */}
      <div style={{
        marginTop: 9, padding: "8px 11px", borderRadius: 7,
        background: hotelsLinked >= 2 ? "rgba(59,141,90,0.10)" : "rgba(237,77,134,0.08)",
        border: `1px solid ${hotelsLinked >= 2 ? "rgba(59,141,90,0.45)" : FUCHSIA + "55"}`,
        display: "flex", alignItems: "center", gap: 7,
      }}>
        <Icon name={hotelsLinked >= 2 ? "check" : "spark"} size={11}
          color={hotelsLinked >= 2 ? GREEN : FUCHSIA} strokeWidth={2.4} />
        <Mono size={9.5} color={hotelsLinked >= 2 ? GREEN : FUCHSIA} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>
          {hotelsLinked >= 2 ? "PROMOTED TO CRM" : "AWAITING REACHABILITY"}
        </Mono>
      </div>
    </div>
  );
}

// ─── flow connectors between the three columns ───
function FlowConnectors({ time, playing }) {
  const cur = currentPacket(time);
  const age = cur?.age ?? 99;
  const hot = age < PACKET_HOLD * 1.2;
  // 2 horizontal rails between columns
  return (
    <svg viewBox="0 0 1100 540" preserveAspectRatio="none"
      style={{
        position: "absolute", inset: 0, width: "100%", height: "100%",
        pointerEvents: "none", overflow: "visible",
      }}>
      <defs>
        <radialGradient id="gm-cdp-pk" cx="50%" cy="50%" r="50%">
          <stop offset="0%"  stopColor={FUCHSIA} stopOpacity="1"/>
          <stop offset="55%" stopColor={FUCHSIA} stopOpacity="0.45"/>
          <stop offset="100%" stopColor={FUCHSIA} stopOpacity="0"/>
        </radialGradient>
        <filter id="gm-cdp-blur" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="2" />
        </filter>
      </defs>
      {/* left → center rail at y=270 */}
      <path d="M 280 270 C 295 270, 305 270, 320 270" fill="none" stroke={FUCHSIA} strokeOpacity="0.35" strokeWidth="1.3" strokeDasharray="3 5" />
      {/* center → right rail */}
      <path d="M 800 270 C 815 270, 825 270, 840 270" fill="none" stroke={FUCHSIA} strokeOpacity="0.35" strokeWidth="1.3" strokeDasharray="3 5" />
      {playing && (
        <>
          <circle r="10" fill="url(#gm-cdp-pk)" opacity="0.75">
            <animateMotion dur="1.4s" repeatCount="indefinite" path="M 280 270 L 320 270" />
          </circle>
          <circle r="3.5" fill={FUCHSIA} filter="url(#gm-cdp-blur)">
            <animateMotion dur="1.4s" repeatCount="indefinite" path="M 280 270 L 320 270" />
          </circle>
          <circle r="10" fill="url(#gm-cdp-pk)" opacity="0.75">
            <animateMotion dur="1.4s" repeatCount="indefinite" begin="0.7s" path="M 800 270 L 840 270" />
          </circle>
          <circle r="3.5" fill={FUCHSIA} filter="url(#gm-cdp-blur)">
            <animateMotion dur="1.4s" repeatCount="indefinite" begin="0.7s" path="M 800 270 L 840 270" />
          </circle>
        </>
      )}
    </svg>
  );
}

// ─── full workbench container ───
function Workbench({ time, playing }) {
  return (
    <div style={{
      maxWidth: 1216, margin: "0 auto",
      background: PAPER,
      border: `1px solid ${RULE}`,
      borderRadius: 14,
      padding: "26px 30px 30px",
      position: "relative",
      boxShadow: "0 30px 60px -40px rgba(20,14,10,0.18)",
    }}>
      <div style={{
        display: "flex", alignItems: "center", justifyContent: "space-between",
        marginBottom: 22,
      }}>
        <Eyebrow color={DIM}>RAW PMS RECORD → 3-STAGE RESOLUTION → GOLDEN RECORD</Eyebrow>
        <Mono size={10} color={FAINT} style={{ letterSpacing: "0.14em" }}>
          DET 99% · FUZZY 0.70–0.90 · AI VERIFY
        </Mono>
      </div>

      <ScaleFrame nativeWidth={1100} style={{ maxWidth: 1100, margin: "0 auto" }}>
      <div style={{
        position: "relative",
        display: "grid",
        gridTemplateColumns: "280px 480px 280px",
        columnGap: 20,
        justifyContent: "center",
      }}>
        <FlowConnectors time={time} playing={playing} />
        <div style={{ position: "relative", zIndex: 2 }}>
          <IntakeQueue time={time} />
        </div>
        <div style={{ position: "relative", zIndex: 3 }}>
          <ResolutionWorkbench time={time} />
        </div>
        <div style={{ position: "relative", zIndex: 2 }}>
          <GoldenOutput time={time} />
        </div>
      </div>
      </ScaleFrame>
    </div>
  );
}

// ─── golden record assembly · stacked contribution layers ───
// Each profile becomes a vertical "core sample" of merge contributions.
// Bottom = first sighting, top = most recent merge. Stage color stripes each layer.

function layersForGR(grId, time) {
  return PACKETS
    .filter(p => p.target === grId && p.t <= time && p.kind !== "skip")
    .sort((a, b) => a.t - b.t); // oldest first → stack bottom-up
}

function fieldsFromPacket(p) {
  if (p.kind === "new") return ["user", "mail", "phone", "cal", "earth"];
  const f = [];
  if (p.det?.email === true) f.push("mail");
  if (p.det?.phone === true) f.push("phone");
  if (p.det?.doc === true)   f.push("id");
  if (p.stage === "fuzzy" || p.stage === "ai") {
    if (!f.includes("user")) f.push("user");
    if (!f.includes("cal"))  f.push("cal");
    if (!f.includes("earth"))f.push("earth");
  }
  return f.length ? f : ["user"];
}

function ContributionLayer({ p, isLatest, time }) {
  const src = SRC_BY_ID[p.src];
  const isNew = p.kind === "new";
  const stageObj = STAGES.find(s => s.id === p.stage);
  const stageColor = isNew ? GREEN : stageObj.color;
  const stageLabel = isNew ? "FIRST SIGHTING"
    : p.stage === "det" ? "DETERMINISTIC"
    : p.stage === "fuzzy" ? "FUZZY MATCH"
    : "AI VERIFY";
  const stageShort = isNew ? "NEW"
    : p.stage === "det" ? "DET"
    : p.stage === "fuzzy" ? "FUZZY"
    : "AI";
  const recent = isLatest && (time - p.t) < 1.4;
  const fields = fieldsFromPacket(p);

  return (
    <div style={{
      position: "relative",
      background: SURFACE,
      borderTop: `3px solid ${stageColor}`,
      borderLeft: `1px solid ${RULE}`,
      borderRight: `1px solid ${RULE}`,
      borderBottom: `1px solid ${RULE}`,
      borderRadius: "4px 4px 3px 3px",
      padding: "7px 9px 8px",
      marginLeft: 11, // step away from the spine
      animation: recent ? "gm-fadein 0.55s ease" : "none",
      boxShadow: recent
        ? `0 0 0 2px ${stageColor}44, 0 16px 32px -18px ${stageColor}AA`
        : "0 1px 0 rgba(0,0,0,0.02)",
      transition: "all 0.45s ease",
    }}>
      {/* tick connector to the spine */}
      <span style={{
        position: "absolute", left: -12, top: 14,
        width: 11, height: 1, background: stageColor, opacity: 0.65,
      }} />
      <span style={{
        position: "absolute", left: -14, top: 11,
        width: 7, height: 7, borderRadius: "50%",
        background: stageColor,
        boxShadow: recent ? `0 0 0 3px ${stageColor}33` : `0 0 0 2px ${PAPER}`,
      }} />

      {/* row 1: hotel + stage + conf */}
      <div style={{ display: "flex", alignItems: "center", gap: 5, marginBottom: 4 }}>
        <span style={{
          display: "inline-flex", alignItems: "center", gap: 3,
          padding: "1.5px 5px", borderRadius: 4,
          background: FUCHSIA + "12",
          border: `1px solid ${FUCHSIA}33`,
        }}>
          <Icon name={src.icon} size={9} strokeWidth={2.2} color={FUCHSIA_DEEP} />
          <Mono size={8.5} color={FUCHSIA_DEEP} style={{ fontWeight: 700, letterSpacing: "0.1em" }}>{src.code}</Mono>
        </span>
        <span style={{ flex: 1 }} />
        <Mono size={8} color={stageColor} style={{ fontWeight: 700, letterSpacing: "0.14em" }}>
          {stageShort}
        </Mono>
        {!isNew && (
          <Mono size={9} color={INK} style={{
            fontWeight: 700, fontVariantNumeric: "tabular-nums",
            background: stageColor + "12", padding: "1px 4px", borderRadius: 3,
          }}>
            {p.conf}%
          </Mono>
        )}
      </div>

      {/* row 2: fields contributed */}
      <div style={{ display: "flex", gap: 3, marginBottom: 4, flexWrap: "wrap" }}>
        {fields.map((f, i) => (
          <span key={i} style={{
            display: "inline-flex", alignItems: "center", justifyContent: "center",
            width: 14, height: 14, borderRadius: 3,
            background: stageColor + "14",
            border: `1px solid ${stageColor}55`,
          }}>
            <Icon name={f} size={8} strokeWidth={2.2} color={stageColor} />
          </span>
        ))}
      </div>

      {/* row 3: PMS id / context */}
      <Mono size={8.5} color={FAINT} style={{ letterSpacing: "0.06em" }}>
        {p.raw.pms}{isNew ? " · foundation" : ""}
      </Mono>
    </div>
  );
}

function AssemblyColumn({ g, time }) {
  const layers = layersForGR(g.id, time);
  const layerCount = layers.length;
  const isAwaiting = layerCount === 0;
  const latest = layers[layers.length - 1];
  const recentBump = latest ? (time - latest.t) < 1.0 : false;
  const maxSlots = g.sources.length; // total expected hotels
  const emptySlots = Math.max(0, maxSlots - layerCount);

  const conf = latest?.conf ?? 0;
  const confColor = g.viaAi ? PLUM : conf >= 95 ? GREEN : conf >= 70 ? AMBER : DIM;
  const promoted = layerCount >= 2;

  return (
    <div style={{
      flex: 1, minWidth: 0,
      background: PAPER,
      border: `1px solid ${recentBump ? FUCHSIA + "55" : RULE}`,
      borderRadius: 12,
      padding: "13px 11px 12px",
      display: "flex", flexDirection: "column",
      position: "relative",
      height: 520,
      boxShadow: recentBump
        ? `0 22px 44px -22px ${FUCHSIA}AA, 0 0 0 3px ${FUCHSIA}14`
        : "0 1px 0 rgba(0,0,0,0.02), 0 14px 28px -22px rgba(20,14,10,0.14)",
      transition: "all 0.45s ease",
    }}>
      {/* header: avatar + name */}
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
        <div style={{
          width: 30, height: 30, borderRadius: 7, flexShrink: 0,
          background: isAwaiting ? "#e3ddcf" : `linear-gradient(135deg, ${FUCHSIA}, ${FUCHSIA_DEEP})`,
          display: "flex", alignItems: "center", justifyContent: "center", color: "#fff",
          boxShadow: isAwaiting ? "none" : "inset 0 -2px 4px rgba(0,0,0,0.18)",
        }}>
          {isAwaiting ? (
            <Icon name="user" size={13} color="#a89aa0" strokeWidth={1.9} />
          ) : (
            <span style={{ fontFamily: SERIF, fontSize: 12, fontStyle: "italic", fontWeight: 600 }}>
              {g.name.split(" ").map(s => s[0]).slice(0,2).join("").toUpperCase()}
            </span>
          )}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{
            fontFamily: SERIF, fontSize: 13, color: isAwaiting ? FAINT : INK, fontWeight: 600,
            letterSpacing: "-0.005em",
            whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
          }}>{g.name}</div>
          <Mono size={9} color={FAINT} style={{ letterSpacing: "0.06em" }}>
            {g.cid} · {g.nat}
          </Mono>
        </div>
      </div>

      {/* the stack · column-reverse so we render oldest first but they appear at bottom */}
      <div style={{
        position: "relative",
        flex: 1,
        display: "flex", flexDirection: "column-reverse",
        gap: 5,
        minHeight: 0,
      }}>
        {/* vertical spine */}
        <span style={{
          position: "absolute", left: 3, top: 0, bottom: 0, width: 1,
          background: `linear-gradient(180deg, ${FUCHSIA_PALE} 0%, ${FUCHSIA_PALE}66 50%, ${RULE} 100%)`,
          pointerEvents: "none",
        }} />

        {/* layers (oldest first in array → appears at bottom due to column-reverse) */}
        {layers.map((p, i) => (
          <ContributionLayer key={p.t} p={p} isLatest={i === layers.length - 1} time={time} />
        ))}

        {/* empty slots above the most recent layer */}
        {Array.from({ length: emptySlots }).map((_, k) => (
          <div key={`e${k}`} style={{
            marginLeft: 11,
            border: `1px dashed ${RULE}`,
            borderRadius: 4,
            padding: "11px 9px",
            background: "rgba(255,255,255,0.35)",
            display: "flex", alignItems: "center", justifyContent: "space-between",
            opacity: 0.55,
          }}>
            <Mono size={8.5} color={FAINT} style={{ letterSpacing: "0.14em" }}>OPEN</Mono>
            <Mono size={8} color={FAINT} style={{ letterSpacing: "0.18em" }}>·  ·  ·</Mono>
          </div>
        ))}

        {/* awaiting empty state */}
        {isAwaiting && (
          <div style={{
            flex: 1, marginLeft: 11,
            border: `1px dashed ${RULE}`, borderRadius: 6,
            background: "rgba(255,255,255,0.4)",
            display: "flex", alignItems: "center", justifyContent: "center",
            textAlign: "center", padding: 12,
          }}>
            <Mono size={9} color={FAINT} style={{ letterSpacing: "0.16em", lineHeight: 1.7 }}>
              AWAITING<br/>FIRST<br/>SIGHTING
            </Mono>
          </div>
        )}
      </div>

      {/* footer: progress + status */}
      <div style={{ marginTop: 11 }}>
        {/* layer progress bar */}
        <div style={{
          display: "flex", alignItems: "center", gap: 5, marginBottom: 7,
        }}>
          <Mono size={8.5} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 600 }}>
            {layerCount}/{maxSlots}
          </Mono>
          <div style={{
            flex: 1, height: 3, background: "#ece5d4", borderRadius: 2, overflow: "hidden",
          }}>
            <div style={{
              width: `${(layerCount / maxSlots) * 100}%`, height: "100%",
              background: layerCount >= 2 ? GREEN : FUCHSIA,
              transition: "width 0.5s ease",
            }} />
          </div>
          <Mono size={9.5} color={confColor} style={{
            fontWeight: 700, fontVariantNumeric: "tabular-nums",
          }}>
            {conf}%
          </Mono>
        </div>

        {/* status pill */}
        <div style={{
          padding: "5px 8px", borderRadius: 6,
          background: isAwaiting ? "transparent"
            : promoted ? "rgba(59,141,90,0.10)"
            : FUCHSIA + "10",
          border: `1px solid ${isAwaiting ? RULE
            : promoted ? "rgba(59,141,90,0.45)"
            : FUCHSIA + "55"}`,
          display: "flex", alignItems: "center", gap: 6,
        }}>
          {isAwaiting ? (
            <>
              <Dot color={FAINT} size={5} />
              <Mono size={8.5} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>
                PENDING
              </Mono>
            </>
          ) : promoted ? (
            <>
              <Icon name="check" size={9} color={GREEN} strokeWidth={2.4} />
              <Mono size={8.5} color={GREEN} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>
                PROMOTED TO CRM
              </Mono>
            </>
          ) : (
            <>
              <Icon name="spark" size={9} color={FUCHSIA} strokeWidth={2.2} />
              <Mono size={8.5} color={FUCHSIA} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>
                UNIFIED · 1 SOURCE
              </Mono>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

function GoldenAssemblyStrip({ time }) {
  const totalLayers = PACKETS.filter(p => p.t <= time && p.kind !== "skip").length;
  return (
    <div style={{ maxWidth: 1216, margin: "26px auto 0" }}>
      <div style={{
        display: "flex", alignItems: "center", justifyContent: "space-between",
        padding: "0 8px", marginBottom: 14,
      }}>
        <div style={{ display: "flex", alignItems: "center", gap: 9 }}>
          <Icon name="shield" size={13} color={FUCHSIA} strokeWidth={2.2} />
          <Eyebrow color={FUCHSIA}>Identity assembly</Eyebrow>
          <Mono size={10} color={DIM} style={{ letterSpacing: "0.06em" }}>
            · how each profile was built, contribution by contribution
          </Mono>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
            <span style={{ width: 8, height: 2, background: GREEN, borderRadius: 1 }} />
            <Mono size={9} color={DIM} style={{ letterSpacing: "0.12em", fontWeight: 600 }}>NEW</Mono>
          </span>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
            <span style={{ width: 8, height: 2, background: GREEN, borderRadius: 1 }} />
            <Mono size={9} color={DIM} style={{ letterSpacing: "0.12em", fontWeight: 600 }}>DET</Mono>
          </span>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
            <span style={{ width: 8, height: 2, background: AMBER, borderRadius: 1 }} />
            <Mono size={9} color={DIM} style={{ letterSpacing: "0.12em", fontWeight: 600 }}>FUZZY</Mono>
          </span>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
            <span style={{ width: 8, height: 2, background: FUCHSIA, borderRadius: 1 }} />
            <Mono size={9} color={DIM} style={{ letterSpacing: "0.12em", fontWeight: 600 }}>AI</Mono>
          </span>
          <Mono size={10} color={FAINT} style={{ letterSpacing: "0.14em", marginLeft: 10 }}>
            {totalLayers} CONTRIBUTIONS · 5 OF 5,383
          </Mono>
        </div>
      </div>
      <div style={{ display: "flex", gap: 12 }}>
        {GOLDEN.map(g => <AssemblyColumn key={g.id} g={g} time={time} />)}
      </div>
    </div>
  );
}

// ───────────────────── CDP PRIMITIVES · 12-card section ─────────────────────

// each primitive ships with its own bespoke micro-viz
function VizIngestion() {
  const rows = [
    { lbl: "SINGLE", cap: "1",     w: 6,   mode: "SYNC",  color: GREEN },
    { lbl: "BATCH",  cap: "100",   w: 28,  mode: "SYNC",  color: GREEN },
    { lbl: "BULK",   cap: "5,000", w: 100, mode: "ASYNC", color: PLUM  },
  ];
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
      {rows.map((r, i) => (
        <div key={i} style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <Mono size={9} color={DIM} style={{ width: 48, letterSpacing: "0.14em", fontWeight: 700 }}>{r.lbl}</Mono>
          <div style={{ flex: 1, height: 10, background: "#ece5d4", borderRadius: 2 }}>
            <div style={{ width: `${r.w}%`, height: "100%", background: FUCHSIA, borderRadius: 2 }} />
          </div>
          <Mono size={10} color={INK} style={{ width: 48, textAlign: "right", fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{r.cap}</Mono>
          <Mono size={8.5} color={r.color} style={{ width: 42, textAlign: "right", letterSpacing: "0.14em", fontWeight: 700 }}>{r.mode}</Mono>
        </div>
      ))}
    </div>
  );
}

function VizDeterministic() {
  const lines = [
    { k: "EMAIL", match: true },
    { k: "PHONE", match: true },
    { k: "DOC",   match: null },
  ];
  return (
    <div style={{ display: "grid", gridTemplateColumns: "1fr auto 1fr", gap: 8, alignItems: "center" }}>
      <div style={{ background: SURFACE, border: `1px solid ${RULE}`, borderRadius: 6, padding: "7px 9px" }}>
        <Mono size={8} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 700, marginBottom: 4, display: "block" }}>MVL · pms-67</Mono>
        {lines.map((l, i) => (
          <div key={i} style={{ display: "flex", justifyContent: "space-between", fontFamily: MONO, fontSize: 9, color: INK, lineHeight: 1.55 }}>
            <span style={{ color: FAINT }}>{l.k}</span>
            <span style={{ color: l.match === true ? GREEN : "#c0b6ab", fontWeight: 700 }}>{l.match === true ? "✓" : "·"}</span>
          </div>
        ))}
      </div>
      <div style={{ textAlign: "center" }}>
        <div style={{ fontFamily: MONO, fontSize: 8, color: GREEN, letterSpacing: "0.16em", fontWeight: 700 }}>MATCH</div>
        <div style={{ fontFamily: MONO, fontSize: 18, fontWeight: 700, color: GREEN, fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>99%</div>
      </div>
      <div style={{ background: SURFACE, border: `1px solid ${GREEN}55`, borderRadius: 6, padding: "7px 9px", boxShadow: `0 0 0 2px ${GREEN}0E` }}>
        <Mono size={8} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 700, marginBottom: 4, display: "block" }}>SBR · pms-141</Mono>
        {lines.map((l, i) => (
          <div key={i} style={{ display: "flex", justifyContent: "space-between", fontFamily: MONO, fontSize: 9, color: INK, lineHeight: 1.55 }}>
            <span style={{ color: FAINT }}>{l.k}</span>
            <span style={{ color: l.match === true ? GREEN : "#c0b6ab", fontWeight: 700 }}>{l.match === true ? "✓" : "·"}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

function VizFuzzy() {
  const feats = [
    { lbl: "family_name", w: 25, v: 100 },
    { lbl: "given_name",  w: 20, v: 84  },
    { lbl: "date_of_birth", w: 20, v: 100 },
    { lbl: "nationality", w: 8,  v: 100 },
    { lbl: "stay_dates",  w: 5,  v: 50  },
  ];
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
      {feats.map((f, i) => (
        <div key={i} style={{ display: "flex", alignItems: "center", gap: 6 }}>
          <Mono size={8.5} color={DIM} style={{ width: 84, letterSpacing: "0.04em" }}>{f.lbl}</Mono>
          <div style={{ flex: 1, height: 4, background: "#ece5d4", borderRadius: 2 }}>
            <div style={{ width: `${f.v}%`, height: "100%", background: f.v >= 80 ? GREEN : AMBER, borderRadius: 2 }} />
          </div>
          <Mono size={8} color={FAINT} style={{ width: 30, textAlign: "right" }}>×{(f.w/100).toFixed(2)}</Mono>
        </div>
      ))}
      <div style={{
        display: "flex", justifyContent: "space-between",
        padding: "4px 8px", marginTop: 4, borderRadius: 4,
        background: AMBER + "14", border: `1px solid ${AMBER}55`,
      }}>
        <Mono size={9} color={DIM} style={{ letterSpacing: "0.12em", fontWeight: 700 }}>COMPOSITE</Mono>
        <Mono size={10} color={AMBER} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>0.82 → AI</Mono>
      </div>
    </div>
  );
}

function VizAiVerify() {
  return (
    <div style={{
      background: SURFACE, border: `1px solid ${FUCHSIA}33`, borderRadius: 6,
      padding: "9px 11px", boxShadow: `0 14px 28px -22px ${FUCHSIA}55`,
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 5 }}>
        <Icon name="brain" size={11} color={FUCHSIA} strokeWidth={2.2} />
        <Mono size={8.5} color={FUCHSIA} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>CLAUDE-HAIKU · VERIFY</Mono>
        <span style={{ flex: 1 }} />
        <Mono size={10} color={FUCHSIA} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>88%</Mono>
      </div>
      <div style={{
        fontFamily: SERIF, fontSize: 11.5, fontStyle: "italic", color: INK,
        lineHeight: 1.5, letterSpacing: "0.005em",
      }}>
        “Same phone (+1570722861), identical DOB. Different email domains consistent with multiple accounts. Merge.”
      </div>
    </div>
  );
}

function VizSmartTriage() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
      <Mono size={8.5} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>SHARED · garcia@…</Mono>
      {[
        { name: "María García",  jw: 0.41 },
        { name: "Pedro García",  jw: 0.41 },
      ].map((p, i) => (
        <div key={i} style={{
          display: "flex", alignItems: "center", gap: 8,
          background: SURFACE, border: `1px solid ${RED}33`, borderRadius: 5, padding: "5px 8px",
        }}>
          <Icon name="user" size={11} color={DIM} strokeWidth={2} />
          <span style={{ fontFamily: SERIF, fontSize: 12, color: INK, fontWeight: 500, flex: 1 }}>{p.name}</span>
          <Mono size={9} color={RED} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>J-W {p.jw}</Mono>
        </div>
      ))}
      <div style={{
        display: "flex", alignItems: "center", gap: 6,
        padding: "4px 8px", borderRadius: 4,
        background: RED + "0E", border: `1px solid ${RED}55`,
      }}>
        <Icon name="x" size={10} color={RED} strokeWidth={2.4} />
        <Mono size={9} color={RED} style={{ letterSpacing: "0.12em", fontWeight: 700 }}>BELOW 0.85 · NOT MERGED</Mono>
      </div>
    </div>
  );
}

function VizGoldenRecord() {
  const rows = [
    { k: "NAME",  hotels: ["MVL"] },
    { k: "EMAIL", hotels: ["MVL", "DCH"] },
    { k: "PHONE", hotels: ["MVL", "SBR"] },
    { k: "DOB",   hotels: ["MVL"] },
    { k: "NAT",   hotels: ["MVL", "DCH"] },
  ];
  return (
    <div style={{ background: SURFACE, border: `1px solid ${RULE}`, borderRadius: 6, padding: "7px 10px" }}>
      {rows.map((r, i) => (
        <div key={i} style={{
          display: "grid", gridTemplateColumns: "44px 1fr auto", alignItems: "center", gap: 6,
          padding: "3px 0", borderBottom: i < rows.length - 1 ? `1px dashed ${RULE}` : "none",
        }}>
          <Mono size={8.5} color={FAINT} style={{ letterSpacing: "0.12em", fontWeight: 600 }}>{r.k}</Mono>
          <span style={{ height: 1, background: RULE }} />
          <span style={{ display: "flex", gap: 2 }}>
            {r.hotels.map((h, j) => (
              <span key={j} style={{
                fontFamily: MONO, fontSize: 7.5,
                padding: "1px 4px", borderRadius: 2,
                background: FUCHSIA + "14", border: `1px solid ${FUCHSIA}55`,
                color: FUCHSIA_DEEP, fontWeight: 700, letterSpacing: "0.1em",
              }}>{h}</span>
            ))}
          </span>
        </div>
      ))}
    </div>
  );
}

function VizDataQuality() {
  const rows = [
    { lbl: "email",         w: 30, v: 100 },
    { lbl: "phone",         w: 30, v: 100 },
    { lbl: "date_of_birth", w: 15, v: 100 },
    { lbl: "nationality",   w: 15, v: 100 },
    { lbl: "document",      w: 10, v: 0   },
  ];
  return (
    <div>
      {rows.map((r, i) => (
        <div key={i} style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }}>
          <Mono size={8.5} color={DIM} style={{ width: 84 }}>{r.lbl}</Mono>
          <div style={{ flex: 1, height: 4, background: "#ece5d4", borderRadius: 2 }}>
            <div style={{ width: `${r.v}%`, height: "100%", background: r.v >= 50 ? GREEN : "#c0b6ab", borderRadius: 2 }} />
          </div>
          <Mono size={8} color={FAINT} style={{ width: 28, textAlign: "right" }}>×{(r.w/100).toFixed(2)}</Mono>
        </div>
      ))}
      <div style={{
        marginTop: 6, padding: "4px 8px", borderRadius: 4,
        background: GREEN + "10", border: `1px solid ${GREEN}55`,
        display: "flex", justifyContent: "space-between",
      }}>
        <Mono size={9} color={DIM} style={{ letterSpacing: "0.12em", fontWeight: 700 }}>WEIGHTED</Mono>
        <Mono size={11} color={GREEN} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>90%</Mono>
      </div>
    </div>
  );
}

function VizAutoPromotion() {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
      <div style={{ background: SURFACE, border: `1px solid ${FUCHSIA}33`, borderRadius: 6, padding: "8px 9px", flex: 1 }}>
        <Mono size={8} color={FUCHSIA} style={{ letterSpacing: "0.14em", fontWeight: 700, marginBottom: 3, display: "block" }}>CDP · GOLDEN</Mono>
        <div style={{ fontFamily: SERIF, fontSize: 12, color: INK, fontWeight: 600 }}>Sergio Wagner</div>
        <Mono size={8.5} color={FAINT} style={{ display: "block", marginTop: 1 }}>9 fields ready</Mono>
      </div>
      <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 1 }}>
        <Mono size={7.5} color={GREEN} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>FILL-ONLY-NULL</Mono>
        <Icon name="arrow-r" size={18} color={GREEN} strokeWidth={2.4} />
        <Mono size={7.5} color={FAINT} style={{ letterSpacing: "0.14em" }}>UPSERT</Mono>
      </div>
      <div style={{ background: GREEN + "0E", border: `1px solid ${GREEN}55`, borderRadius: 6, padding: "8px 9px", flex: 1 }}>
        <Mono size={8} color={GREEN} style={{ letterSpacing: "0.14em", fontWeight: 700, marginBottom: 3, display: "block" }}>CRM · CONTACT</Mono>
        <div style={{ fontFamily: SERIF, fontSize: 12, color: INK, fontWeight: 600 }}>Sergio Wagner</div>
        <Mono size={8.5} color={GREEN} style={{ display: "block", marginTop: 1, fontWeight: 700 }}>✓ promoted</Mono>
      </div>
    </div>
  );
}

function VizConsent() {
  const tiers = [
    { lbl: "PMS CONSENT",      sub: "passport-side · wins",       color: GREEN, op: 1 },
    { lbl: "CDP AUTO-CONSENT", sub: "email ON · whatsapp OFF",    color: AMBER, op: 0.86 },
    { lbl: "TENANT DEFAULT",   sub: "fallback",                   color: "#a99fa3", op: 0.66 },
  ];
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
      {tiers.map((t, i) => (
        <div key={i} style={{
          background: SURFACE,
          border: `1px solid ${i === 0 ? t.color + "AA" : RULE}`,
          borderLeft: `3px solid ${t.color}`,
          borderRadius: 5, padding: "5px 9px",
          display: "flex", alignItems: "center", gap: 8,
          opacity: t.op,
        }}>
          <Mono size={9} color={t.color} style={{ letterSpacing: "0.14em", fontWeight: 700, width: 130 }}>{t.lbl}</Mono>
          <Mono size={8.5} color={FAINT} style={{ letterSpacing: "0.04em", flex: 1 }}>{t.sub}</Mono>
        </div>
      ))}
    </div>
  );
}

function VizWebSdk() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <span style={{
          padding: "3px 7px", borderRadius: 4, background: SURFACE, border: `1px solid ${RULE}`,
          fontFamily: MONO, fontSize: 9, color: DIM, letterSpacing: "0.04em", fontWeight: 600,
        }}>__gm_visitor_id</span>
        <Mono size={8.5} color={GREEN} style={{ fontWeight: 700, letterSpacing: "0.06em" }}>13-month TTL</Mono>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <span style={{
          padding: "3px 7px", borderRadius: 4, background: SURFACE, border: `1px solid ${RULE}`,
          fontFamily: MONO, fontSize: 9, color: DIM, letterSpacing: "0.04em", fontWeight: 600,
        }}>__gm_session_id</span>
        <Mono size={8.5} color={AMBER} style={{ fontWeight: 700, letterSpacing: "0.06em" }}>30-min idle rotate</Mono>
      </div>
      <div style={{
        display: "flex", alignItems: "center", gap: 6,
        padding: "5px 9px", borderRadius: 4,
        background: FUCHSIA + "0E", border: `1px solid ${FUCHSIA}55`,
      }}>
        <Icon name="check" size={10} color={FUCHSIA} strokeWidth={2.4} />
        <Mono size={9} color={FUCHSIA} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>GPC HONORED · PII STRIPPED</Mono>
      </div>
    </div>
  );
}

function VizIntentScore() {
  const score = 78;
  const r = 28;
  const C = 2 * Math.PI * r;
  const dash = (score / 100) * C;
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
      <div style={{ position: "relative", width: 76, height: 76, flexShrink: 0 }}>
        <svg viewBox="0 0 76 76" width="76" height="76">
          <circle cx="38" cy="38" r={r} fill="none" stroke="#ece5d4" strokeWidth="5" />
          <circle cx="38" cy="38" r={r} fill="none"
            stroke={FUCHSIA} strokeWidth="5"
            strokeDasharray={`${dash} ${C - dash}`}
            strokeLinecap="round"
            transform="rotate(-90 38 38)" />
        </svg>
        <div style={{
          position: "absolute", inset: 0,
          display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
        }}>
          <div style={{ fontFamily: MONO, fontSize: 17, fontWeight: 700, color: INK, fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>{score}</div>
          <Mono size={7} color={FUCHSIA} style={{ letterSpacing: "0.18em", fontWeight: 700, marginTop: 2 }}>HIGH</Mono>
        </div>
      </div>
      <div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 3 }}>
        {[
          { lbl: "booking views", v: 24 },
          { lbl: "return visits", v: 20 },
          { lbl: "sessions",      v: 16 },
          { lbl: "pricing views", v: 12 },
        ].map((row, i) => (
          <div key={i} style={{ display: "flex", justifyContent: "space-between", fontFamily: MONO, fontSize: 9, color: DIM }}>
            <span>{row.lbl}</span>
            <span style={{ color: INK, fontWeight: 700 }}>+{row.v}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

function VizAudit() {
  const rows = [
    { t: "0:08", stage: "AI",    conf: "88%",  note: "Sean ↔ Sergio · name variation",   color: PLUM  },
    { t: "0:06", stage: "DET",   conf: "100%", note: "email + phone · auto-merge",       color: GREEN },
    { t: "0:04", stage: "FUZZY", conf: "93%",  note: "composite ≥ 0.90 · auto-merge",    color: AMBER },
    { t: "0:02", stage: "SKIP",  conf: "64%",  note: "below threshold · different DOB",  color: RED   },
  ];
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
      {rows.map((r, i) => (
        <div key={i} style={{
          display: "grid", gridTemplateColumns: "30px 42px 1fr 30px", gap: 6, alignItems: "center",
          padding: "3px 7px", background: SURFACE, border: `1px solid ${RULE}`, borderRadius: 4,
        }}>
          <Mono size={8} color={FAINT} style={{ letterSpacing: "0.04em" }}>{r.t}</Mono>
          <span style={{
            padding: "1px 4px", borderRadius: 3,
            background: r.color + "16", border: `1px solid ${r.color}55`,
            fontFamily: MONO, fontSize: 8, fontWeight: 700, letterSpacing: "0.12em", color: r.color,
            textAlign: "center",
          }}>{r.stage}</span>
          <span style={{ fontFamily: MONO, fontSize: 9, color: INK, letterSpacing: "0.02em",
            whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{r.note}</span>
          <Mono size={9} color={r.color} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums", textAlign: "right" }}>{r.conf}</Mono>
        </div>
      ))}
    </div>
  );
}

const PRIMITIVES = [
  {
    idx: "01", name: "INGESTION",
    title: "Three modes, one schema",
    body: "Single push, nightly batch or historical bulk. E.164 phone normalization, OTA-temp-email detection, and upsert dedup on (tenant, hotel, ext_reservation_id) · every payload lands as a guest, a stay and the extras, atomically.",
    bullets: [
      "Single · 1 reservation · real-time PMS push",
      "Batch · up to 100 · nightly delta sync",
      "Bulk · up to 5,000 · async with batch_id + replay",
    ],
    quote: "Your PMS push lands as a guest, a stay and the extras · in the same call.",
    viz: VizIngestion,
  },
  {
    idx: "02", name: "DETERMINISTIC",
    title: "The obvious 99%",
    body: "Four exact-match strategies cover the easy hits. Case-insensitive email, E.164-normalized phones, document type+number pairs, and a 4-field composite for guests without contact info.",
    bullets: [
      "Email · case-insensitive exact",
      "Phone · E.164 · handles bare nationals",
      "Composite · family_name + DOB + nationality + gender",
    ],
    quote: "The merge that obviously should have happened. We just do it.",
    viz: VizDeterministic,
  },
  {
    idx: "03", name: "FUZZY MATCH",
    title: "Variant-aware scoring",
    body: "A 9-feature weighted composite over name, DOB, document, nationality, gender, stay proximity, hotel overlap and channel. Five blocking strategies pre-filter the candidates so we never run O(N²).",
    bullets: [
      "9 features · family 0.25 · given 0.20 · DOB 0.20 …",
      "5 blocking strategies · prefix, domain, phone-prefix",
      ">0.90 auto-merge · 0.70–0.90 → AI · <0.70 skip",
    ],
    quote: "Isabel and Isabelle on the same DOB? That's one person.",
    viz: VizFuzzy,
  },
  {
    idx: "04", name: "AI VERIFY",
    title: "Claude on the long tail",
    body: "A custom system prompt with hotel-industry false-positive patterns. Handles transliterations, nicknames, cultural naming conventions. Only the 0.70–0.90 band · about 5% of candidates · and every reasoning text is logged.",
    bullets: [
      "Evaluates the 0.70–0.90 confidence band only",
      "Merges on >85% AI confidence",
      "Full reasoning text logged for every decision",
    ],
    quote: "Sean is a common Anglicisation of Sergio.",
    viz: VizAiVerify,
  },
  {
    idx: "05", name: "SMART TRIAGE",
    title: "Safeguards against false merges",
    body: "Two guests on the same email aren't always the same person. Jaro-Winkler similarity must hold on the given name. Family members on a single booking stay separate via the same-reservation guard.",
    bullets: [
      "Jaro-Winkler ≥ 0.85 · given-name divergence",
      "Same-reservation guard · families stay split",
      "Batched 1,000 unmatched guests per pass",
    ],
    quote: "María García and Pedro García are not the same person.",
    viz: VizSmartTriage,
  },
  {
    idx: "06", name: "GOLDEN RECORD",
    title: "Provenance per field",
    body: "Every canonical field carries provenance metadata. See exactly which PMS contributed which name, email or document. Audit the merge chain all the way back to the contributing reservation.",
    bullets: [
      "field_sources JSONB · per-field hotel attribution",
      "Aggregates · total_stays · spend · hotels_visited",
      "Reversible merges via merged_into_id pointer",
    ],
    quote: "You never lose where a fact came from.",
    viz: VizGoldenRecord,
  },
  {
    idx: "07", name: "DATA QUALITY",
    title: "A score you can act on",
    body: "A weighted completeness score per profile, per hotel and per tenant. Email and phone weigh heaviest because they unlock reachability and segmentation. Recomputed on every merge.",
    bullets: [
      "Email 30% · Phone 30% · DOB 15% · Nat 15% · Doc 10%",
      "Per-profile, per-property and per-tenant rollups",
      "Live recompute on every merge or enrichment",
    ],
    quote: "A number your CRM team can chase, not a chart they ignore.",
    viz: VizDataQuality,
  },
  {
    idx: "08", name: "AUTO-PROMOTION",
    title: "Zero-touch CRM bridge",
    body: "Reachable profiles flow into the CRM automatically. Nine contact fields are filled · only if null · so manual CRM edits are never overwritten. Reservations bridge to the CRM in the same pass.",
    bullets: [
      "Phone-first dedup, email fallback",
      "9 fields enriched · fill-only-null strategy",
      "Reservations upserted into the CRM in parallel",
    ],
    quote: "No more 'import the latest CSV from booking.com'.",
    viz: VizAutoPromotion,
  },
  {
    idx: "09", name: "CONSENT RESOLUTION",
    title: "Three-tier consent stack",
    body: "Consent resolved with explicit priority: PMS-collected consent wins, then CDP auto-consent toggles, then your tenant default. Flip a setting and backfill retroactively across every promoted profile.",
    bullets: [
      "Tier 1 · PMS consent (passport-side)",
      "Tier 2 · CDP auto-consent · email + whatsapp",
      "Tier 3 · tenant default · retroactive backfill",
    ],
    quote: "GDPR-shaped, not GDPR-flavored.",
    viz: VizConsent,
  },
  {
    idx: "10", name: "WEB SDK",
    title: "Anonymous to identified",
    body: "A zero-dependency snippet drops a 13-month visitor_id and a 30-min session_id. Auto-tracks pageviews, scroll, clicks, forms. PII is stripped before transmission. GPC honored. 10s batched flush with beacon fallback.",
    bullets: [
      "Persistent visitor_id · 13-month max-age cookie",
      "Event batching · 10s or 50 events · beacon fallback",
      "GPC honored · __gm_disable_tracking override",
    ],
    quote: "A pixel that respects the browser, not a tag that ignores it.",
    viz: VizWebSdk,
  },
  {
    idx: "11", name: "INTENT SCORE",
    title: "Behavior, not surveys",
    body: "Sessions × 8, page views × 1.5, return visits × 10, booking-engine views × 12, pricing views × 6, days active × 3. Capped per dimension. Deterministic · explainable to legal, not learned by a black box.",
    bullets: [
      "0–100 · High ≥ 70 · Medium 35–69 · Low 1–34",
      "Booking-engine views weighed heaviest",
      "Recomputed nightly · exposed to segment builder",
    ],
    quote: "An intent score you can explain to legal.",
    viz: VizIntentScore,
  },
  {
    idx: "12", name: "AUDIT TRAIL",
    title: "Every decision, replayable",
    body: "Every match decision logged with confidence, matched fields and AI reasoning text. Reverse a merge with merged_into_id, replay an entire batch, debug any single decision, and cascade GDPR deletes with proof of erasure.",
    bullets: [
      "Per-decision log · stage · confidence · reasoning",
      "Reverse a merge via merged_into_id pointer",
      "GDPR delete cascade with proof-of-erasure",
    ],
    quote: "Auditors stop calling. The record always knows.",
    viz: VizAudit,
  },
];

function PrimitiveCard({ p }) {
  return (
    <div style={{
      background: PAPER,
      border: `1px solid ${RULE}`,
      borderRadius: 8,
      padding: "20px 22px 22px",
      display: "flex", flexDirection: "column",
    }}>
      <div style={{ marginBottom: 18, minHeight: 100 }}>
        <p.viz />
      </div>
      <Mono size={10} color={FUCHSIA} style={{ letterSpacing: "0.2em", fontWeight: 700, marginBottom: 7 }}>
        {p.idx} · {p.name}
      </Mono>
      <h3 style={{
        fontFamily: SERIF, fontSize: 19, fontWeight: 500, color: INK,
        letterSpacing: "-0.015em", lineHeight: 1.18, margin: "0 0 9px",
      }}>{p.title}</h3>
      <p style={{ fontSize: 12.5, color: DIM, lineHeight: 1.5, margin: "0 0 11px" }}>{p.body}</p>
      <ul style={{ listStyle: "none", padding: 0, margin: "0 0 14px" }}>
        {p.bullets.map((b, i) => (
          <li key={i} style={{
            display: "flex", alignItems: "flex-start", gap: 7,
            fontSize: 12, color: INK, fontWeight: 500, marginBottom: 5,
            lineHeight: 1.45,
          }}>
            <span style={{
              display: "inline-block", width: 4, height: 4, borderRadius: "50%",
              background: FUCHSIA, marginTop: 7, flexShrink: 0,
            }} />
            <span>{b}</span>
          </li>
        ))}
      </ul>
      <div style={{ flex: 1 }} />
      <div style={{
        fontFamily: SERIF, fontSize: 12, fontStyle: "italic", color: DIM,
        lineHeight: 1.5, paddingTop: 12, borderTop: `1px dashed ${RULE}`,
        letterSpacing: "0.005em",
      }}>"{p.quote}"</div>
    </div>
  );
}

// ───────────────────── CDP VISITOR JOURNEY · 11-day artifact ─────────────────────
// One anonymous browser becomes Sophie Martin. Horizontal multi-lane timeline,
// AI page classification, intent score curve, and the dramatic identity reveal.

const SCENARIO_DURATION = 28; // seconds per scenario before rotation

// ── three real visitor journeys on rotation ──

// Sophie · 11.5 days · long contemplator · newsletter → booked suite
const SOPHIE_PVS = [
  { t: 0.42, ses: 1, url: "/",                          cat: "homepage",       dev: "desktop", utm: "google / organic" },
  { t: 0.45, ses: 1, url: "/rooms",                     cat: "rooms",          dev: "desktop", utm: "google / organic" },
  { t: 0.49, ses: 1, url: "/rooms/sea-view-suite",      cat: "rooms",          dev: "desktop", utm: "google / organic" },
  { t: 0.54, ses: 1, url: "/gallery",                   cat: "gallery",        dev: "desktop", utm: "google / organic" },
  { t: 2.34, ses: 2, url: "/",                          cat: "homepage",       dev: "mobile",  utm: "instagram / social" },
  { t: 2.37, ses: 2, url: "/spa",                       cat: "spa",            dev: "mobile",  utm: "instagram / social" },
  { t: 2.41, ses: 2, url: "/spa/treatments",            cat: "spa",            dev: "mobile",  utm: "instagram / social" },
  { t: 2.46, ses: 2, url: "/spa/aphrodite-ritual",      cat: "spa",            dev: "mobile",  utm: "instagram / social" },
  { t: 4.22, ses: 3, url: "/dining",                    cat: "dining",         dev: "desktop", utm: "direct / ·" },
  { t: 4.25, ses: 3, url: "/dining/verde",              cat: "dining",         dev: "desktop", utm: "direct / ·" },
  { t: 4.29, ses: 3, url: "/rooms/deluxe",              cat: "rooms",          dev: "desktop", utm: "direct / ·" },
  { t: 4.33, ses: 3, url: "/events",                    cat: "events",         dev: "desktop", utm: "direct / ·" },
  { t: 6.12, ses: 4, url: "/rooms",                     cat: "rooms",          dev: "desktop", utm: "direct / ·" },
  { t: 6.16, ses: 4, url: "/pricing",                   cat: "pricing",        dev: "desktop", utm: "direct / ·" },
  { t: 6.19, ses: 4, url: "/pricing/seasonal",          cat: "pricing",        dev: "desktop", utm: "direct / ·" },
  { t: 6.23, ses: 4, url: "/offers/early-bird",         cat: "offers",         dev: "desktop", utm: "direct / ·" },
  { t: 6.28, ses: 4, url: "/booking",                   cat: "booking_engine", dev: "desktop", utm: "direct / ·" },
  { t: 7.42, ses: 5, url: "/booking",                   cat: "booking_engine", dev: "desktop", utm: "direct / ·" },
  { t: 7.45, ses: 5, url: "/booking?dates=2025-12-15",  cat: "booking_engine", dev: "desktop", utm: "direct / ·" },
  { t: 7.49, ses: 5, url: "/rooms/deluxe",              cat: "rooms",          dev: "desktop", utm: "direct / ·" },
  { t: 7.54, ses: 5, url: "/loyalty",                   cat: "loyalty",        dev: "desktop", utm: "direct / ·" },
  { t: 8.85, ses: 6, url: "/newsletter",                cat: "loyalty",        dev: "desktop", utm: "direct / ·",
    reveal: { kind: "newsletter_signup" } },
  { t: 8.89, ses: 6, url: "/loyalty/signup",            cat: "loyalty",        dev: "desktop", utm: "direct / ·" },
  { t: 8.94, ses: 6, url: "/booking",                   cat: "booking_engine", dev: "desktop", utm: "direct / ·" },
  { t: 10.18, ses: 7, url: "/booking?dates=2025-12-15", cat: "booking_engine", dev: "desktop", utm: "email / newsletter" },
  { t: 10.22, ses: 7, url: "/booking/payment",          cat: "booking_engine", dev: "desktop", utm: "email / newsletter" },
  { t: 10.30, ses: 7, url: "/booking/confirmation",     cat: "booking_engine", dev: "desktop", utm: "email / newsletter", booked: true },
];

// Tomás · 5.5 days · wedding planner · form_submit · lead, no booking
const TOMAS_PVS = [
  { t: 0.30, ses: 1, url: "/",                          cat: "homepage",     dev: "desktop", utm: "google / organic" },
  { t: 0.34, ses: 1, url: "/events",                    cat: "events",       dev: "desktop", utm: "google / organic" },
  { t: 0.38, ses: 1, url: "/events/weddings",           cat: "events",       dev: "desktop", utm: "google / organic" },
  { t: 0.43, ses: 1, url: "/gallery",                   cat: "gallery",      dev: "desktop", utm: "google / organic" },
  { t: 0.46, ses: 1, url: "/experiences",               cat: "experiences",  dev: "desktop", utm: "google / organic" },
  { t: 1.20, ses: 2, url: "/events/weddings",           cat: "events",       dev: "mobile",  utm: "facebook / social" },
  { t: 1.24, ses: 2, url: "/events/weddings/spaces",    cat: "events",       dev: "mobile",  utm: "facebook / social" },
  { t: 1.28, ses: 2, url: "/location",                  cat: "location",     dev: "mobile",  utm: "facebook / social" },
  { t: 2.10, ses: 3, url: "/events/weddings",           cat: "events",       dev: "desktop", utm: "direct / ·" },
  { t: 2.14, ses: 3, url: "/events/weddings/photos",    cat: "events",       dev: "desktop", utm: "direct / ·" },
  { t: 2.18, ses: 3, url: "/experiences/private-dining",cat: "experiences",  dev: "desktop", utm: "direct / ·" },
  { t: 3.20, ses: 4, url: "/contact?form=wedding",      cat: "events",       dev: "desktop", utm: "direct / ·",
    reveal: { kind: "form_submit" } },
  { t: 3.24, ses: 4, url: "/events/weddings/quote",     cat: "events",       dev: "desktop", utm: "direct / ·" },
  { t: 4.30, ses: 5, url: "/experiences/private-dining",cat: "experiences",  dev: "desktop", utm: "email / wedding-quote" },
  { t: 4.34, ses: 5, url: "/gallery",                   cat: "gallery",      dev: "desktop", utm: "email / wedding-quote" },
  { t: 5.10, ses: 6, url: "/events/weddings/spaces",    cat: "events",       dev: "mobile",  utm: "email / wedding-quote" },
];

// Karim · 7.5 days · returning member · loyalty enrollment → penthouse booking
const KARIM_PVS = [
  { t: 0.30, ses: 1, url: "/",                          cat: "homepage",       dev: "mobile",  utm: "email / loyalty-tier" },
  { t: 0.33, ses: 1, url: "/loyalty",                   cat: "loyalty",        dev: "mobile",  utm: "email / loyalty-tier" },
  { t: 0.37, ses: 1, url: "/offers",                    cat: "offers",         dev: "mobile",  utm: "email / loyalty-tier" },
  { t: 0.40, ses: 1, url: "/offers/repeat-guests",      cat: "offers",         dev: "mobile",  utm: "email / loyalty-tier" },
  { t: 1.30, ses: 2, url: "/loyalty/benefits",          cat: "loyalty",        dev: "desktop", utm: "direct / ·" },
  { t: 1.34, ses: 2, url: "/rooms",                     cat: "rooms",          dev: "desktop", utm: "direct / ·" },
  { t: 1.38, ses: 2, url: "/rooms/deluxe-suite",        cat: "rooms",          dev: "desktop", utm: "direct / ·" },
  { t: 2.20, ses: 3, url: "/loyalty",                   cat: "loyalty",        dev: "desktop", utm: "direct / ·" },
  { t: 2.24, ses: 3, url: "/spa",                       cat: "spa",            dev: "desktop", utm: "direct / ·" },
  { t: 2.28, ses: 3, url: "/rooms/penthouse",           cat: "rooms",          dev: "desktop", utm: "direct / ·" },
  { t: 4.10, ses: 4, url: "/loyalty/signup",            cat: "loyalty",        dev: "desktop", utm: "direct / ·",
    reveal: { kind: "loyalty_enrollment" } },
  { t: 4.14, ses: 4, url: "/offers/repeat-guests",      cat: "offers",         dev: "desktop", utm: "direct / ·" },
  { t: 4.18, ses: 4, url: "/rooms/penthouse",           cat: "rooms",          dev: "desktop", utm: "direct / ·" },
  { t: 5.30, ses: 5, url: "/pricing",                   cat: "pricing",        dev: "desktop", utm: "email / member-rate" },
  { t: 5.34, ses: 5, url: "/rooms/penthouse",           cat: "rooms",          dev: "desktop", utm: "email / member-rate" },
  { t: 5.38, ses: 5, url: "/booking",                   cat: "booking_engine", dev: "desktop", utm: "email / member-rate" },
  { t: 5.42, ses: 5, url: "/booking?dates=2026-01-08",  cat: "booking_engine", dev: "desktop", utm: "email / member-rate" },
  { t: 6.50, ses: 6, url: "/booking",                   cat: "booking_engine", dev: "desktop", utm: "email / member-rate" },
  { t: 6.54, ses: 6, url: "/booking/payment",           cat: "booking_engine", dev: "desktop", utm: "email / member-rate" },
  { t: 6.90, ses: 6, url: "/booking/confirmation",      cat: "booking_engine", dev: "desktop", utm: "email / member-rate", booked: true },
  { t: 7.40, ses: 7, url: "/loyalty",                   cat: "loyalty",        dev: "mobile",  utm: "direct / ·" },
];

const SCENARIOS = [
  {
    id: "sophie", initial: "SM",
    name: "Sophie Martin", anonId: "anon-83af-2c91", cdpId: "G-04826",
    email: "s.martin@orange.fr",
    tagline: "The contemplator",
    sub: "newsletter signup · books a suite",
    revealKind: "newsletter_signup",
    revealLabel: "NEWSLETTER_SIGNUP · 9-STEP IDEMPOTENT FLOW",
    days: 11.5, revealDay: 8.85, bookDay: 10.30,
    pageviews: SOPHIE_PVS,
  },
  {
    id: "tomas", initial: "TR",
    name: "Tomás Ribeiro", anonId: "anon-91c2-7b3f", cdpId: "G-04827",
    email: "t.ribeiro@gmail.com",
    tagline: "The planner",
    sub: "wedding inquiry · form submit",
    revealKind: "form_submit",
    revealLabel: "FORM_SUBMIT · WEDDING INQUIRY · 9-STEP IDEMPOTENT FLOW",
    days: 5.5, revealDay: 3.20, bookDay: null,
    pageviews: TOMAS_PVS,
  },
  {
    id: "karim", initial: "KK",
    name: "Karim Kowalski", anonId: "anon-4f5a-d29c", cdpId: "G-04828",
    email: "k.kowalski@protonmail.com",
    tagline: "The repeat guest",
    sub: "loyalty enrollment · books the penthouse",
    revealKind: "loyalty_enrollment",
    revealLabel: "LOYALTY_ENROLLMENT · 9-STEP IDEMPOTENT FLOW",
    days: 7.5, revealDay: 4.10, bookDay: 6.90,
    pageviews: KARIM_PVS,
  },
];

const PAGE_CATS = [
  { id: "rooms",          label: "rooms",          color: FUCHSIA       },
  { id: "spa",            label: "spa",            color: PLUM          },
  { id: "dining",         label: "dining",         color: AMBER         },
  { id: "experiences",    label: "experiences",    color: "#7B5A8C"     },
  { id: "events",         label: "events",         color: "#9C7BAE"     },
  { id: "offers",         label: "offers",         color: GREEN         },
  { id: "pricing",        label: "pricing",        color: "#D9742F"     },
  { id: "booking_engine", label: "booking engine", color: FUCHSIA_DEEP  },
  { id: "loyalty",        label: "loyalty",        color: FUCHSIA_SOFT  },
  { id: "gallery",        label: "gallery",        color: "#8E7C6B"     },
  { id: "location",       label: "location",       color: "#7A8A6E"     },
  { id: "homepage",       label: "homepage",       color: "#a99fa3"     },
];
const CAT_BY_ID = Object.fromEntries(PAGE_CATS.map(c => [c.id, c]));

const PAGEVIEWS_LEGACY_REMOVED = true; // pageviews now live inside SCENARIOS

function computeIntent(pvs) {
  const sessions     = new Set(pvs.map(p => p.ses)).size;
  const pageViews    = pvs.length;
  const returnVisits = Math.max(0, sessions - 1);
  const bookingViews = pvs.filter(p => p.cat === "booking_engine").length;
  const pricingViews = pvs.filter(p => p.cat === "pricing").length;
  const daysActive   = new Set(pvs.map(p => Math.floor(p.t))).size;
  return Math.round(
    Math.min(20, sessions     * 8)   +
    Math.min(15, pageViews    * 1.5) +
    Math.min(20, returnVisits * 10)  +
    Math.min(25, bookingViews * 12)  +
    Math.min(12, pricingViews * 6)   +
    Math.min(8,  daysActive   * 3)
  );
}

function useScenarioPlayback() {
  const [time, setTime] = useState(0);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(true);
  const lastRef = useRef(performance.now());

  useEffect(() => {
    let raf;
    const loop = (now) => {
      const dt = (now - lastRef.current) / 1000;
      lastRef.current = now;
      if (playing) {
        setTime(t => {
          const nt = t + dt;
          if (nt >= SCENARIO_DURATION) {
            setIdx(i => (i + 1) % SCENARIOS.length);
            return 0;
          }
          return nt;
        });
      }
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(n => { lastRef.current = n; loop(n); });
    return () => cancelAnimationFrame(raf);
  }, [playing]);

  const scenario = SCENARIOS[idx];
  const progress = time / SCENARIO_DURATION;
  const day = progress * scenario.days;
  return { day, time, idx, scenario, progress, setIdx, setTime, playing, setPlaying };
}

// ─── scenario tabs · the rotation controller ───
function ScenarioTabs({ idx, progress, onJump }) {
  return (
    <div style={{
      maxWidth: 1216, margin: "0 auto 14px",
      display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 8,
    }}>
      {SCENARIOS.map((s, i) => {
        const active = i === idx;
        return (
          <button key={s.id} onClick={() => onJump(i)} style={{
            position: "relative",
            background: active ? SURFACE : PAPER,
            border: `1px solid ${active ? FUCHSIA + "AA" : RULE}`,
            borderRadius: 10,
            padding: "10px 14px 11px",
            cursor: "pointer", textAlign: "left",
            font: "inherit", color: "inherit",
            boxShadow: active ? `0 18px 36px -24px ${FUCHSIA}AA, 0 0 0 3px ${FUCHSIA}12` : "none",
            transition: "all 0.45s ease",
            opacity: active ? 1 : 0.62,
            overflow: "hidden",
          }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
              <span style={{
                width: 22, height: 22, borderRadius: 5,
                background: active ? `linear-gradient(135deg, ${FUCHSIA}, ${FUCHSIA_DEEP})` : "#e3ddcf",
                color: active ? "#fff" : "#a89aa0",
                display: "flex", alignItems: "center", justifyContent: "center",
                fontFamily: SERIF, fontSize: 10, fontStyle: "italic", fontWeight: 600,
                transition: "all 0.45s ease", flexShrink: 0,
              }}>
                {s.initial}
              </span>
              <Mono size={8.5} color={active ? FUCHSIA : DIM} style={{ letterSpacing: "0.2em", fontWeight: 700 }}>
                {String(i + 1).padStart(2, "0")} · SCENARIO
              </Mono>
              <span style={{ flex: 1 }} />
              {active && (
                <Mono size={8} color={FUCHSIA} style={{ letterSpacing: "0.18em", fontWeight: 700 }}>
                  ● NOW
                </Mono>
              )}
            </div>
            <div style={{ display: "flex", alignItems: "baseline", gap: 7, marginBottom: 3 }}>
              <span style={{
                fontFamily: SERIF, fontSize: 16, fontWeight: 600,
                color: active ? INK : DIM, letterSpacing: "-0.005em",
                whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
              }}>{s.name}</span>
              <Mono size={9} color={FAINT} style={{ letterSpacing: "0.04em", fontStyle: "italic" }}>
                · {s.tagline.toLowerCase()}
              </Mono>
            </div>
            <Mono size={9.5} color={DIM} style={{
              letterSpacing: "0.02em", lineHeight: 1.4, display: "block", marginBottom: 7,
              whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
            }}>
              {s.sub}
            </Mono>
            {/* progress bar */}
            <div style={{ height: 2.5, background: "#ece5d4", borderRadius: 2, overflow: "hidden" }}>
              <div style={{
                width: active ? `${progress * 100}%` : "0%",
                height: "100%",
                background: `linear-gradient(90deg, ${FUCHSIA}, ${PLUM})`,
                borderRadius: 2,
                transition: active ? "width 0.1s linear" : "width 0.4s ease",
              }} />
            </div>
          </button>
        );
      })}
    </div>
  );
}

// ─── header card ───
function VisitorHeader({ day, scenario }) {
  const pvs = scenario.pageviews.filter(p => p.t <= day);
  const sessions = new Set(pvs.map(p => p.ses)).size;
  const intent = computeIntent(pvs);
  const isIdentified = day >= scenario.revealDay;
  const isBooked = scenario.bookDay != null && day >= scenario.bookDay;
  const intentLevel = intent >= 70 ? { lbl: "HIGH",   color: FUCHSIA }
                    : intent >= 35 ? { lbl: "MEDIUM", color: AMBER }
                    : intent >  0  ? { lbl: "LOW",    color: DIM }
                    :                { lbl: "NONE",   color: FAINT };
  const bookingWindowVal = isBooked
    ? "0d"
    : scenario.bookDay == null
      ? "LEAD"
      : `${Math.max(0, Math.ceil(scenario.days - day))}d`;

  return (
    <div style={{
      maxWidth: 1216, margin: "0 auto 16px",
      display: "flex", alignItems: "center", gap: 16,
      padding: "13px 18px",
      background: PAPER, border: `1px solid ${RULE}`, borderRadius: 12,
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 11, minWidth: 0 }}>
        <div style={{
          width: 38, height: 38, borderRadius: 8,
          background: isIdentified ? `linear-gradient(135deg, ${FUCHSIA}, ${FUCHSIA_DEEP})` : "#e3ddcf",
          display: "flex", alignItems: "center", justifyContent: "center",
          boxShadow: isIdentified ? "inset 0 -2px 4px rgba(0,0,0,0.18)" : "none",
          transition: "all 0.6s ease",
        }}>
          {isIdentified
            ? <span style={{ fontFamily: SERIF, fontSize: 14, fontStyle: "italic", fontWeight: 600, color: "#fff" }}>{scenario.initial}</span>
            : <Icon name="user" size={17} color="#a89aa0" strokeWidth={1.8} />}
        </div>
        <div>
          <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
            <span style={{
              fontFamily: SERIF, fontSize: 17, fontWeight: 600, color: INK, letterSpacing: "-0.005em",
              transition: "color 0.6s ease",
            }}>
              {isIdentified ? scenario.name : scenario.anonId}
            </span>
            <span style={{
              padding: "2px 7px", borderRadius: 4,
              background: isIdentified ? GREEN + "14" : FAINT + "14",
              border: `1px solid ${isIdentified ? GREEN + "55" : RULE}`,
              fontFamily: MONO, fontSize: 8.5, color: isIdentified ? GREEN : DIM,
              fontWeight: 700, letterSpacing: "0.14em",
            }}>
              {isIdentified ? "IDENTIFIED" : "ANONYMOUS"}
            </span>
            {isBooked && (
              <span style={{
                padding: "2px 7px", borderRadius: 4,
                background: GREEN + "16", border: `1px solid ${GREEN}AA`,
                fontFamily: MONO, fontSize: 8.5, color: GREEN, fontWeight: 700, letterSpacing: "0.14em",
              }}>BOOKED</span>
            )}
          </div>
          <Mono size={9} color={FAINT} style={{ letterSpacing: "0.06em", marginTop: 2, display: "block" }}>
            {isIdentified
              ? `linked → CDP profile ${scenario.cdpId} · via ${scenario.revealKind} · ${pvs.length} pageviews attached`
              : "__gm_visitor_id cookie · 13-month TTL · no contact info yet"}
          </Mono>
        </div>
      </div>

      <span style={{ flex: 1 }} />

      <div style={{ display: "flex", gap: 22 }}>
        <Stat lbl="SESSIONS"  val={sessions} />
        <Stat lbl="PAGEVIEWS" val={pvs.length} />
        <Stat lbl="INTENT"    val={intent} sub={intentLevel.lbl} accent />
        <Stat lbl="BOOKING WINDOW" val={bookingWindowVal} />
      </div>
    </div>
  );
}

function Stat({ lbl, val, sub, accent }) {
  return (
    <div style={{ textAlign: "right" }}>
      <Mono size={8} color={FAINT} style={{ letterSpacing: "0.18em", fontWeight: 700 }}>{lbl}</Mono>
      <div style={{
        fontFamily: MONO, fontSize: 18, fontWeight: 700,
        color: accent ? FUCHSIA : INK, lineHeight: 1.1,
        fontVariantNumeric: "tabular-nums",
      }}>{val}</div>
      {sub && (
        <Mono size={8} color={accent ? FUCHSIA : DIM} style={{ letterSpacing: "0.16em", fontWeight: 700 }}>
          {sub}
        </Mono>
      )}
    </div>
  );
}

// ─── the main timeline SVG ───
function VisitorChart({ day, scenario }) {
  const W = 1036;
  const padL = 124;
  const padR = 16;
  const padTop = 28;
  const innerW = W - padL - padR;
  const laneH = 17;
  const lanesH = laneH * PAGE_CATS.length;
  const gap = 22;
  const intentH = 96;
  const intentY = padTop + lanesH + gap;
  const totalH = intentY + intentH + 22;

  const pageviews = scenario.pageviews;
  const totalDays = scenario.days;
  const revealDay = scenario.revealDay;
  const bookDay = scenario.bookDay;
  const dayTicks = Math.ceil(totalDays) + 1;

  const x = (d) => padL + (Math.max(0, Math.min(totalDays, d)) / totalDays) * innerW;
  const laneY = (i) => padTop + i * laneH;

  const dayClamped = Math.min(day, totalDays);
  const activePvs = pageviews.filter(p => p.t <= dayClamped);

  // intent curve sampled
  const samples = [];
  const step = Math.max(0.06, totalDays / 95);
  for (let d = 0; d <= dayClamped + step / 2; d += step) {
    const dd = Math.min(d, dayClamped);
    const score = computeIntent(pageviews.filter(p => p.t <= dd));
    samples.push({ d: dd, score });
    if (dd === dayClamped) break;
  }
  const yOf = (s) => intentY + intentH * (1 - s / 100);
  const linePath = samples.map((s, i) => `${i === 0 ? "M" : "L"}${x(s.d)},${yOf(s.score)}`).join(" ");
  const areaPath = samples.length > 1
    ? linePath + ` L${x(samples[samples.length-1].d)},${intentY + intentH} L${x(0)},${intentY + intentH} Z`
    : "";

  return (
    <svg viewBox={`0 0 ${W} ${totalH}`} style={{ width: "100%", height: "auto", display: "block" }}>
      <defs>
        <linearGradient id="gm-vis-intent" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"  stopColor={FUCHSIA} stopOpacity="0.42" />
          <stop offset="100%" stopColor={FUCHSIA} stopOpacity="0.04" />
        </linearGradient>
      </defs>

      {/* day grid + axis labels */}
      {Array.from({ length: dayTicks }).map((_, d) => (
        <g key={d}>
          <line x1={x(d)} y1={padTop} x2={x(d)} y2={padTop + lanesH}
            stroke={RULE} strokeOpacity={d === 0 ? 0.9 : 0.45}
            strokeWidth="1" strokeDasharray={d === 0 ? "0" : "1 4"} />
          <text x={x(d)} y={padTop - 10} fontFamily={MONO} fontSize="9" fill={FAINT}
            textAnchor="middle" letterSpacing="0.08em" fontWeight="600">
            Day {d}
          </text>
        </g>
      ))}

      {/* lane labels + backgrounds */}
      {PAGE_CATS.map((c, i) => (
        <g key={c.id}>
          <rect x={padL} y={laneY(i)} width={innerW} height={laneH - 0.5}
            fill={i % 2 === 0 ? "rgba(20,14,10,0.018)" : "transparent"} />
          <line x1={padL} y1={laneY(i) + laneH} x2={padL + innerW} y2={laneY(i) + laneH}
            stroke={RULE} strokeOpacity="0.4" />
          <rect x={padL - 110} y={laneY(i) + laneH/2 - 4} width="8" height="8" rx="2" fill={c.color} />
          <text x={padL - 96} y={laneY(i) + laneH/2 + 3.5} fontFamily={MONO} fontSize="9.5"
            fill={DIM} fontWeight="500" letterSpacing="0.04em">
            {c.label}
          </text>
        </g>
      ))}

      {/* pageview dots */}
      {activePvs.map((p, j) => {
        const ci = PAGE_CATS.findIndex(c => c.id === p.cat);
        if (ci < 0) return null;
        const cat = PAGE_CATS[ci];
        const cx = x(p.t);
        const cy = laneY(ci) + laneH/2;
        const recent = (dayClamped - p.t) < 0.22;
        const isReveal = !!p.reveal;
        const isBookedDot = !!p.booked;
        return (
          <g key={`${scenario.id}-${j}`}>
            {recent && (
              <circle cx={cx} cy={cy} fill={cat.color} opacity="0.32" r="9"
                style={{ animation: "gm-fadein 0.6s ease" }} />
            )}
            <circle cx={cx} cy={cy} r={isReveal || isBookedDot ? 5.5 : 3.8}
              fill={cat.color}
              stroke={isBookedDot ? GREEN : isReveal ? FUCHSIA : "#fff"}
              strokeWidth={isBookedDot || isReveal ? 2 : 0.8} />
            {isReveal && (
              <circle cx={cx} cy={cy} r="9" fill="none" stroke={FUCHSIA} strokeWidth="1" opacity="0.5">
                <animate attributeName="r" from="6" to="14" dur="1.4s" repeatCount="indefinite" />
                <animate attributeName="opacity" from="0.6" to="0" dur="1.4s" repeatCount="indefinite" />
              </circle>
            )}
          </g>
        );
      })}

      {/* reveal marker */}
      {day >= revealDay - 0.02 && (
        <g>
          <line x1={x(revealDay)} y1={padTop} x2={x(revealDay)} y2={intentY + intentH}
            stroke={FUCHSIA} strokeWidth="1.5" strokeDasharray="3 3" opacity="0.65" />
          <g transform={`translate(${x(revealDay)}, ${padTop + lanesH + 4})`}>
            <rect x="-37" y="0" width="74" height="14" rx="3" fill={FUCHSIA} />
            <text x="0" y="10" fontFamily={MONO} fontSize="8.5" fill="#fff"
              textAnchor="middle" fontWeight="700" letterSpacing="0.16em">
              IDENTIFIED
            </text>
          </g>
        </g>
      )}

      {/* booked marker */}
      {bookDay != null && day >= bookDay - 0.02 && (
        <g>
          <line x1={x(bookDay)} y1={padTop} x2={x(bookDay)} y2={intentY + intentH}
            stroke={GREEN} strokeWidth="1.5" strokeDasharray="3 3" opacity="0.65" />
          <g transform={`translate(${x(bookDay)}, ${padTop + lanesH + 4})`}>
            <rect x="-26" y="0" width="52" height="14" rx="3" fill={GREEN} />
            <text x="0" y="10" fontFamily={MONO} fontSize="8.5" fill="#fff"
              textAnchor="middle" fontWeight="700" letterSpacing="0.16em">
              BOOKED
            </text>
          </g>
        </g>
      )}

      {/* intent area + line */}
      {areaPath && <path d={areaPath} fill="url(#gm-vis-intent)" />}
      {linePath && <path d={linePath} fill="none" stroke={FUCHSIA} strokeWidth="1.6" />}

      {/* intent thresholds */}
      {[{ v: 70, label: "HIGH" }, { v: 35, label: "MED" }].map(t => (
        <g key={t.v}>
          <line x1={padL} y1={yOf(t.v)} x2={padL + innerW} y2={yOf(t.v)}
            stroke={FAINT} strokeOpacity="0.5" strokeDasharray="2 5" />
          <text x={padL + innerW - 4} y={yOf(t.v) - 4}
            fontFamily={MONO} fontSize="8" fill={FAINT}
            textAnchor="end" letterSpacing="0.14em" fontWeight="600">
            {t.label} {t.v}
          </text>
        </g>
      ))}

      {/* intent axis label */}
      <rect x={padL - 110} y={intentY + 4} width="8" height="8" rx="2" fill={FUCHSIA} />
      <text x={padL - 96} y={intentY + 12} fontFamily={MONO} fontSize="9.5"
        fill={FUCHSIA} fontWeight="700" letterSpacing="0.14em">
        INTENT
      </text>
      <text x={padL - 96} y={intentY + 24} fontFamily={MONO} fontSize="8.5"
        fill={FAINT} letterSpacing="0.06em">
        score 0–100
      </text>

      {/* current intent value annotation */}
      {samples.length > 0 && (() => {
        const last = samples[samples.length - 1];
        const cx = x(last.d), cy = yOf(last.score);
        return (
          <g>
            <line x1={cx} y1={cy} x2={cx} y2={intentY + intentH} stroke={FUCHSIA} strokeOpacity="0.35" strokeDasharray="1 3" />
            <circle cx={cx} cy={cy} r="4" fill={FUCHSIA} stroke="#fff" strokeWidth="1.5" />
            <g transform={`translate(${cx + 10}, ${cy - 8})`}>
              <rect x="-2" y="-11" width="34" height="16" rx="3" fill="#fff" stroke={FUCHSIA} strokeWidth="1" />
              <text x="14" y="1" fontFamily={MONO} fontSize="11" fill={INK}
                textAnchor="middle" fontWeight="700" style={{ fontVariantNumeric: "tabular-nums" }}>
                {last.score}
              </text>
            </g>
          </g>
        );
      })()}

      {/* playhead */}
      <line x1={x(day)} y1={padTop - 18} x2={x(day)} y2={intentY + intentH}
        stroke={INK} strokeWidth="1" strokeOpacity="0.4" />
      <polygon points={`${x(day) - 5},${padTop - 22} ${x(day) + 5},${padTop - 22} ${x(day)},${padTop - 14}`}
        fill={INK} opacity="0.55" />
    </svg>
  );
}

// ─── bottom panels: AI classifier + identity reveal ───
function VisitorBottom({ day, scenario }) {
  const pvs = scenario.pageviews.filter(p => p.t <= day);
  const current = pvs[pvs.length - 1];
  const isIdentified = day >= scenario.revealDay;
  const isBooked = scenario.bookDay != null && day >= scenario.bookDay;

  // top categories from active pageviews
  const counts = {};
  pvs.forEach(p => { counts[p.cat] = (counts[p.cat] || 0) + 1; });
  const topCats = Object.entries(counts)
    .map(([id, n]) => ({ ...CAT_BY_ID[id], n }))
    .sort((a, b) => b.n - a.n)
    .slice(0, 5);
  const totalN = pvs.length || 1;

  const finalIntent = computeIntent(pvs);
  const bookingWindowFinal = isBooked
    ? `${Math.ceil(scenario.bookDay)} days`
    : scenario.bookDay == null
      ? "open lead"
      : `${Math.ceil(scenario.days)} days`;

  return (
    <div style={{
      maxWidth: 1216, margin: "20px auto 0",
      display: "grid", gridTemplateColumns: "1.15fr 1fr",
      gap: 16,
    }}>
      {/* AI page classifier */}
      <div style={{
        background: PAPER, border: `1px solid ${RULE}`, borderRadius: 12,
        padding: "14px 16px",
      }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
          <Icon name="brain" size={13} color={FUCHSIA} strokeWidth={2.2} />
          <Eyebrow color={FUCHSIA}>AI page classifier</Eyebrow>
          <span style={{ flex: 1 }} />
          <Mono size={9} color={FAINT} style={{ letterSpacing: "0.14em" }}>14 CATEGORIES · GLOB PATTERNS</Mono>
        </div>

        {current && (
          <div key={`${scenario.id}-${current.url}-${current.t}`} style={{
            display: "flex", alignItems: "center", gap: 10,
            padding: "9px 11px", borderRadius: 7,
            background: SURFACE, border: `1px solid ${FUCHSIA}55`,
            marginBottom: 12,
            animation: "gm-fadein 0.4s ease",
            boxShadow: `0 0 0 3px ${FUCHSIA}10`,
          }}>
            <Mono size={9.5} color={INK} style={{ letterSpacing: "0.04em", flex: 1, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {current.url}
            </Mono>
            <Icon name="arrow-r" size={13} color={FUCHSIA} strokeWidth={2.2} />
            <span style={{
              padding: "3px 8px", borderRadius: 4,
              background: CAT_BY_ID[current.cat].color + "18",
              border: `1px solid ${CAT_BY_ID[current.cat].color}88`,
              color: CAT_BY_ID[current.cat].color,
              fontFamily: MONO, fontSize: 9.5, fontWeight: 700, letterSpacing: "0.12em",
            }}>{current.cat}</span>
          </div>
        )}

        <Mono size={9} color={FAINT} style={{ letterSpacing: "0.16em", fontWeight: 700, display: "block", marginBottom: 8 }}>
          TOP INTERESTS · BY PAGEVIEW SHARE
        </Mono>
        <div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
          {topCats.length === 0 && (
            <Mono size={9.5} color={FAINT} style={{ fontStyle: "italic", padding: "4px 0" }}>
              awaiting first pageview…
            </Mono>
          )}
          {topCats.map((c, i) => {
            const pct = Math.round((c.n / totalN) * 100);
            return (
              <div key={i} style={{ display: "flex", alignItems: "center", gap: 8 }}>
                <div style={{ width: 10, height: 10, borderRadius: 2, background: c.color, flexShrink: 0 }} />
                <Mono size={10} color={INK} style={{ width: 96, fontWeight: 600 }}>{c.label}</Mono>
                <div style={{ flex: 1, height: 4, background: "#ece5d4", borderRadius: 2 }}>
                  <div style={{ width: `${pct}%`, height: "100%", background: c.color, borderRadius: 2, transition: "width 0.4s ease" }} />
                </div>
                <Mono size={9.5} color={DIM} style={{ width: 36, textAlign: "right", fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>
                  {pct}%
                </Mono>
                <Mono size={9} color={FAINT} style={{ width: 26, textAlign: "right" }}>
                  · {c.n}
                </Mono>
              </div>
            );
          })}
        </div>
      </div>

      {/* identity reveal */}
      <div style={{
        background: isIdentified
          ? `linear-gradient(135deg, ${SURFACE}, ${FUCHSIA_PALE}33)`
          : PAPER,
        border: `1px solid ${isIdentified ? FUCHSIA + "AA" : RULE}`,
        borderRadius: 12,
        padding: "14px 16px",
        position: "relative",
        boxShadow: isIdentified ? `0 22px 44px -22px ${FUCHSIA}66, 0 0 0 3px ${FUCHSIA}14` : "none",
        transition: "all 0.6s ease",
      }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
          <Icon name="fingerprint" size={13} color={isIdentified ? FUCHSIA : DIM} strokeWidth={2.2} />
          <Eyebrow color={isIdentified ? FUCHSIA : DIM}>
            {isIdentified ? "Identity reveal · linked" : "Awaiting identification"}
          </Eyebrow>
        </div>

        {isIdentified ? (
          <>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
              <div style={{
                width: 36, height: 36, borderRadius: 7,
                background: `linear-gradient(135deg, ${FUCHSIA}, ${FUCHSIA_DEEP})`,
                display: "flex", alignItems: "center", justifyContent: "center", color: "#fff",
                boxShadow: "inset 0 -2px 4px rgba(0,0,0,0.18)",
              }}>
                <span style={{ fontFamily: SERIF, fontSize: 14, fontStyle: "italic", fontWeight: 600 }}>{scenario.initial}</span>
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontFamily: SERIF, fontSize: 15, fontWeight: 600, color: INK, letterSpacing: "-0.005em" }}>
                  {scenario.name}
                </div>
                <Mono size={9.5} color={DIM} style={{ letterSpacing: "0.04em" }}>
                  {scenario.email} · {scenario.cdpId}
                </Mono>
              </div>
              <span style={{
                padding: "3px 7px", borderRadius: 4,
                background: GREEN + "16", border: `1px solid ${GREEN}AA`,
                fontFamily: MONO, fontSize: 8.5, color: GREEN,
                fontWeight: 700, letterSpacing: "0.14em",
              }}>PROMOTED</span>
            </div>

            <div style={{
              padding: "7px 10px", borderRadius: 6,
              background: GREEN + "0E", border: `1px solid ${GREEN}55`,
              marginBottom: 9, display: "flex", alignItems: "center", gap: 6,
            }}>
              <Icon name="check" size={11} color={GREEN} strokeWidth={2.4} />
              <Mono size={9} color={GREEN} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>
                {scenario.revealLabel}
              </Mono>
            </div>

            <div style={{ fontSize: 11.5, color: INK, lineHeight: 1.55, marginBottom: 8 }}>
              <strong style={{ fontWeight: 700 }}>{pvs.length} prior pageviews</strong> retroactively attached.
              Full session history bridged to the CRM. Intent <strong>{finalIntent}</strong>,
              booking window <strong>{bookingWindowFinal}</strong>, top interests written to <span style={{ fontFamily: MONO, fontSize: 10.5 }}>contacts.web_interests</span>.
            </div>

            <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
              {topCats.slice(0, 5).map(c => (
                <span key={c.id} style={{
                  padding: "2px 6px", borderRadius: 3,
                  background: c.color + "16",
                  border: `1px solid ${c.color}55`,
                  color: c.color,
                  fontFamily: MONO, fontSize: 8.5, fontWeight: 700, letterSpacing: "0.1em",
                }}>{c.id}</span>
              ))}
            </div>
          </>
        ) : (
          <>
            <Mono size={10} color={DIM} style={{ display: "block", marginBottom: 10, lineHeight: 1.6, letterSpacing: "0.02em" }}>
              No contact captured yet · every session attaches to the same 13-month visitor_id cookie.
              The moment this visitor reveals themselves (newsletter signup, loyalty enrollment, booking,
              or any form), every prior pageview folds into the CDP profile and intent + interests sync
              to the CRM in nine idempotent steps.
            </Mono>
            <Mono size={9} color={FAINT} style={{ letterSpacing: "0.14em", fontWeight: 700, display: "block", marginBottom: 6 }}>
              REVEAL HOOKS · IDEMPOTENT BY VISITOR_ID
            </Mono>
            <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
              {["newsletter_signup", "loyalty_enrollment", "booking", "form_submit", "manual"].map((s) => (
                <span key={s} style={{
                  padding: "3px 7px", borderRadius: 4,
                  background: SURFACE,
                  border: `1px ${s === scenario.revealKind ? "solid " + FUCHSIA + "AA" : "dashed " + RULE}`,
                  fontFamily: MONO, fontSize: 9,
                  color: s === scenario.revealKind ? FUCHSIA_DEEP : DIM,
                  letterSpacing: "0.04em", fontWeight: s === scenario.revealKind ? 700 : 600,
                }}>{s}</span>
              ))}
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ─── main visitor journey section ───
function VisitorJourney() {
  const { day, idx, scenario, progress, setIdx, setTime } = useScenarioPlayback();
  const jump = (i) => { setIdx(i); setTime(0); };

  return (
    <section data-screen-label="CDP Visitor Journey" style={{
      padding: "70px 32px 30px", background: CREAM, position: "relative",
    }}>
      <div style={{ maxWidth: 1216, margin: "0 auto 24px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
          <span style={{ width: 14, height: 1.5, background: FUCHSIA }} />
          <Mono size={11} color={FUCHSIA} style={{ letterSpacing: "0.22em", textTransform: "uppercase", fontWeight: 700 }}>
            Anonymous → identified · the visitor journey
          </Mono>
        </div>
        <h2 style={{
          fontFamily: SERIF, fontWeight: 400, fontSize: 40, letterSpacing: "-0.025em",
          lineHeight: 1.1, margin: 0, maxWidth: 940,
        }}>
          From anonymous cookie to enriched CRM contact.{" "}
          <span style={{ fontStyle: "italic", color: FUCHSIA }}>
            Three real visitor journeys, on rotation.
          </span>
        </h2>
        <p style={{
          fontSize: 14, color: DIM, lineHeight: 1.6,
          maxWidth: 820, margin: "18px 0 0",
        }}>
          Every visit drops a 13-month cookie. Every page is auto-classified into one of fourteen
          categories. Sessions accumulate, intent climbs with every booking-engine view, and the
          interest profile fills out · all anonymously. The moment the visitor reveals themselves
          (newsletter, loyalty, form, or booking), every prior pageview retroactively attaches to
          the CDP profile and the enriched contact lands in the CRM. Below: three different real
          journeys play out on rotation · a long contemplator, a wedding planner, and a returning
          loyalty member.
        </p>
      </div>

      <ScenarioTabs idx={idx} progress={progress} onJump={jump} />

      <VisitorHeader day={day} scenario={scenario} />

      <div style={{
        maxWidth: 1216, margin: "0 auto",
        background: PAPER, border: `1px solid ${RULE}`,
        borderRadius: 14, padding: "16px 22px 20px",
        boxShadow: "0 30px 60px -40px rgba(20,14,10,0.18)",
      }}>
        <VisitorChart day={day} scenario={scenario} />
      </div>

      <VisitorBottom day={day} scenario={scenario} />
    </section>
  );
}

// ═══════════════════════════════════════════════════════════════════════════
// PROMOTION GATE · what stays in the CDP vs what is sent to the CRM
// + the never-ending ingestion loop (daily PMS delta + historical big bang)
// ═══════════════════════════════════════════════════════════════════════════

// 30 sample consolidated golden records spanning the reachability spectrum.
// reach: "both" (e+p) · "email" · "phone" · "none"
// stays: number of stays consolidated into this golden record
const PG_POPULATION = [
  ["Sergio Wagner",      "SW",  "both",  4],
  ["Maria Johnson",      "MJ",  "both",  3],
  ["Isabelle Flores",    "IF",  "both",  3],
  ["Noah Fischer",       "NF",  "both",  2],
  ["Hugo Davis",         "HD",  "both",  2],
  ["Olivia Brown",       "OB",  "both",  3],
  ["Mei Chen",           "MC",  "both",  2],
  ["Felix Becker",       "FB",  "both",  3],
  ["Erik Lindqvist",     "EL",  "both",  2],
  ["Hannah Wolff",       "HW",  "both",  4],
  ["Mira Reddy",         "MR",  "both",  2],
  ["Aisha Bakr",         "AB",  "both",  3],
  ["Sophie Martin",      "SM",  "email", 1],
  ["Karim Kowalski",     "KK",  "email", 2],
  ["Zoe Park",           "ZP",  "email", 1],
  ["Jonas Kraft",        "JK",  "email", 2],
  ["Lukas Vogel",        "LV",  "email", 1],
  ["Tomás Ribeiro",      "TR",  "phone", 1],
  ["Diego Alvarez",      "DA",  "phone", 2],
  ["Anya Petrov",        "AP",  "phone", 1],
  ["Nina Costa",         "NC",  "phone", 2],
  ["Oscar Bergman",      "OB",  "phone", 1],
  ["Liam Andersson",     "LA",  "none",  1],
  ["Yara Khalil",        "YK",  "none",  1],
  ["Ravi Mehta",         "RM",  "none",  2],
  ["Lara Ferri",         "LF",  "none",  1],
  ["Petra Kovac",        "PK",  "none",  1],
  ["Bao Tran",           "BT",  "none",  1],
  ["Gabriel Souza",      "GS",  "none",  2],
  ["Iris Sundgren",      "IS",  "none",  1],
];

function pgIsReachable(r) { return r === "both" || r === "email" || r === "phone"; }
function pgTone(r) {
  if (r === "both")  return GREEN;
  if (r === "email" || r === "phone") return FUCHSIA;
  return FAINT;
}

// ── small contact chip ──
function PGContactChip({ c, pop }) {
  const r = c[2];
  const reachable = pgIsReachable(r);
  const init = c[1];
  const tone = pgTone(r);
  const eOn = r === "both" || r === "email";
  const pOn = r === "both" || r === "phone";
  return (
    <div style={{
      display: "flex", alignItems: "center", gap: 6,
      padding: "4px 7px 4px 4px",
      background: reachable ? "#fff" : PAPER,
      border: `1px solid ${reachable ? tone + "55" : RULE}`,
      borderRadius: 6,
      opacity: reachable ? 1 : 0.78,
      animation: pop ? "gm-fadein 0.4s ease" : "none",
    }}>
      <div style={{
        width: 18, height: 18, borderRadius: 4,
        background: reachable
          ? `linear-gradient(135deg, ${tone}, ${tone}CC)`
          : "#e3ddcf",
        color: reachable ? "#fff" : "#a89aa0",
        display: "flex", alignItems: "center", justifyContent: "center",
        fontFamily: SERIF, fontSize: 9, fontStyle: "italic", fontWeight: 600,
        flexShrink: 0,
        boxShadow: reachable ? "inset 0 -1px 2px rgba(0,0,0,0.18)" : "none",
      }}>{init}</div>
      <span style={{
        fontFamily: MONO, fontSize: 9.5, color: INK, fontWeight: 500,
        flex: 1, letterSpacing: "0.01em",
        whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
      }}>{c[0]}</span>
      <span style={{ display: "inline-flex", gap: 2 }}>
        <Icon name="mail"  size={9} color={eOn ? tone : "#d6cdc0"} strokeWidth={2.4} />
        <Icon name="phone" size={9} color={pOn ? tone : "#d6cdc0"} strokeWidth={2.4} />
      </span>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════════════════
// CLEAN SANKEY FLOW · pool → gate → CRM | held
// ═══════════════════════════════════════════════════════════════════════════

function PGFlow() {
  // Measure actual container width so SVG bands align exactly with HTML cards.
  const wrapRef = React.useRef(null);
  const [W, setW] = React.useState(1024);
  React.useEffect(() => {
    if (!wrapRef.current) return;
    const m = () => { if (wrapRef.current) setW(wrapRef.current.offsetWidth); };
    m();
    const ro = new ResizeObserver(m);
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  const totalH = 440;
  // Responsive column widths · all derived from measured W.
  const inputW   = Math.max(280, Math.min(380, W * 0.34));
  const outputW  = Math.max(380, Math.min(540, W * 0.50));
  const middleW  = Math.max(120, W - inputW - outputW);
  const inputEnd = inputW;
  const outputX  = inputW + middleW;

  const inputCenterY = 220;
  const crmHalfH    = 50;
  const heldHalfH   = 16;
  const crmCenterY  = 100;
  const heldCenterY = 340;

  const band = (yIn, halfIn, yOut, halfOut) => {
    const x1 = inputEnd;
    const x2 = outputX;
    const cx1 = x1 + (x2 - x1) * 0.5;
    const cx2 = x2 - (x2 - x1) * 0.5;
    return [
      `M ${x1} ${yIn - halfIn}`,
      `C ${cx1} ${yIn - halfIn}, ${cx2} ${yOut - halfOut}, ${x2} ${yOut - halfOut}`,
      `L ${W} ${yOut - halfOut}`,
      `L ${W} ${yOut + halfOut}`,
      `L ${x2} ${yOut + halfOut}`,
      `C ${cx2} ${yOut + halfOut}, ${cx1} ${yIn + halfIn}, ${x1} ${yIn + halfIn}`,
      'Z',
    ].join(' ');
  };

  const center = (yIn, yOut) => {
    const x1 = inputEnd;
    const x2 = outputX;
    const cx1 = x1 + (x2 - x1) * 0.5;
    const cx2 = x2 - (x2 - x1) * 0.5;
    return `M ${x1} ${yIn} C ${cx1} ${yIn}, ${cx2} ${yOut}, ${x2} ${yOut}`;
  };

  return (
    <ScaleFrame nativeWidth={1216} style={{ maxWidth: 1216, margin: "0 auto" }}>
    <div ref={wrapRef} style={{
      maxWidth: 1216, margin: "0 auto",
      position: "relative", height: totalH,
    }}>
      {/* Sankey bands · actual SVG coordinate space matches rendered pixels */}
      <svg width={W} height={totalH} viewBox={`0 0 ${W} ${totalH}`}
        style={{
          position: "absolute", inset: 0,
          zIndex: 0, pointerEvents: "none",
          overflow: "visible",
        }}>
        <defs>
          <linearGradient id="pg-band-crm" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0%"  stopColor={FUCHSIA} stopOpacity="0.35"/>
            <stop offset="60%" stopColor={FUCHSIA} stopOpacity="0.22"/>
            <stop offset="100%" stopColor={FUCHSIA} stopOpacity="0.10"/>
          </linearGradient>
          <linearGradient id="pg-band-held" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0%"  stopColor={DIM} stopOpacity="0.22"/>
            <stop offset="100%" stopColor={DIM} stopOpacity="0.06"/>
          </linearGradient>
        </defs>

        {/* fat CRM band */}
        <path d={band(inputCenterY - heldHalfH - 1, crmHalfH, crmCenterY, crmHalfH)}
          fill="url(#pg-band-crm)" />
        {/* thin Held band */}
        <path d={band(inputCenterY + crmHalfH - 1, heldHalfH, heldCenterY, heldHalfH)}
          fill="url(#pg-band-held)" />

        {/* crisp band edges */}
        <path d={center(inputCenterY - crmHalfH - heldHalfH - 1, crmCenterY - crmHalfH)}
          fill="none" stroke={FUCHSIA} strokeOpacity="0.35" strokeWidth="1" />
        <path d={center(inputCenterY + crmHalfH + heldHalfH - 1, heldCenterY + heldHalfH)}
          fill="none" stroke={DIM} strokeOpacity="0.30" strokeWidth="1" strokeDasharray="3 4" />

        {/* particles flowing through bands */}
        {[0, 0.6, 1.2, 1.8, 2.4].map((delay, i) => (
          <circle key={'c' + i} r="3" fill={FUCHSIA} opacity="0.85">
            <animateMotion dur="3s" begin={`${delay}s`} repeatCount="indefinite"
              path={center(inputCenterY - crmHalfH * 0.4, crmCenterY)} />
          </circle>
        ))}
        {[0.4, 1.6].map((delay, i) => (
          <circle key={'h' + i} r="2.5" fill={DIM} opacity="0.6">
            <animateMotion dur="4.5s" begin={`${delay}s`} repeatCount="indefinite"
              path={center(inputCenterY + crmHalfH * 0.4, heldCenterY)} />
          </circle>
        ))}

        {/* split percentages · anchored just past the gate column */}
        <text x={outputX - 12} y={crmCenterY - 4} fontFamily={MONO} fontSize="10"
          fill={FUCHSIA_DEEP} fontWeight="700" letterSpacing="0.14em" textAnchor="end">77.6%</text>
        <text x={outputX - 12} y={heldCenterY + 4} fontFamily={MONO} fontSize="10"
          fill={DIM} fontWeight="700" letterSpacing="0.14em" textAnchor="end">22.4%</text>
      </svg>

      {/* Foreground grid · fr-based so columns adapt to any container width */}
      <div style={{
        position: "relative", zIndex: 1, height: "100%",
        display: "grid",
        gridTemplateColumns: `${inputW}px ${middleW}px ${outputW}px`,
        gridTemplateRows: "1fr",
      }}>
        {/* LEFT · input pool card */}
        <div style={{
          background: PAPER,
          border: `1px solid ${RULE}`,
          borderRadius: 12,
          padding: "20px 22px 22px",
          display: "flex", flexDirection: "column",
        }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
            <Icon name="shield" size={12} color={DIM} strokeWidth={2.2} />
            <Eyebrow color={DIM}>CDP · ALL CONSOLIDATED</Eyebrow>
          </div>
          <Mono size={9.5} color={FAINT} style={{ letterSpacing: "0.06em", marginBottom: 20 }}>
            every guest, post-resolution
          </Mono>

          <div style={{ position: "relative", height: 124, marginBottom: 18 }}>
            <svg viewBox="0 0 280 124" preserveAspectRatio="none"
              style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}>
              {[12, 32, 52, 72, 92, 112].map((y, i) => (
                <g key={i}>
                  <rect x="0" y={y - 4} width="60" height="8" rx="2"
                    fill="#fff" stroke={RULE} strokeWidth="1" />
                  <text x="6" y={y + 2.5} fontFamily={MONO} fontSize="6" fill={FAINT}
                    letterSpacing="0.08em">BKG·{1000 + i * 113}</text>
                  <path d={`M 60 ${y} C 130 ${y}, 160 62, 220 62`}
                    fill="none" stroke={FUCHSIA} strokeOpacity="0.35" strokeWidth="1" />
                </g>
              ))}
              <rect x="220" y="50" width="60" height="24" rx="4"
                fill={FUCHSIA} stroke={FUCHSIA_DEEP} strokeWidth="1" />
              <text x="250" y="65.5" fontFamily={MONO} fontSize="7" fill="#fff"
                fontWeight="700" letterSpacing="0.14em" textAnchor="middle">GOLDEN</text>
              {[0, 0.6, 1.2].map((d, i) => (
                <circle key={'p' + i} r="2.5" fill={FUCHSIA}>
                  <animateMotion dur="2s" begin={`${d}s`} repeatCount="indefinite"
                    path={`M 60 ${12 + (i * 40) % 100} C 130 ${12 + (i * 40) % 100}, 160 62, 220 62`} />
                </circle>
              ))}
            </svg>
          </div>

          <div style={{
            borderTop: `1px solid ${RULE}`, paddingTop: 14,
            display: "flex", alignItems: "baseline", gap: 10, flexWrap: "wrap",
          }}>
            <span style={{
              fontFamily: MONO, fontSize: 36, fontWeight: 700, color: INK,
              fontVariantNumeric: "tabular-nums", letterSpacing: "-0.015em", lineHeight: 1,
            }}>18,420</span>
            <Mono size={10} color={DIM} style={{ letterSpacing: "0.04em" }}>unique guests</Mono>
          </div>
          <Mono size={9.5} color={FAINT} style={{ letterSpacing: "0.04em", marginTop: 6 }}>
            collapsed from <strong style={{ color: INK }}>47,830</strong> raw bookings
            <span style={{ color: FUCHSIA_DEEP, marginLeft: 6, fontWeight: 700 }}>−61.5%</span>
          </Mono>
        </div>

        {/* MIDDLE · gate hub as REAL HTML, vertically centered */}
        <div style={{
          position: "relative",
          display: "flex", alignItems: "center", justifyContent: "center",
        }}>
          <div style={{
            position: "relative",
            width: 92, height: 92,
            display: "flex", flexDirection: "column",
            alignItems: "center", justifyContent: "center",
            background: "#fff",
            border: `1.5px solid ${FUCHSIA}`,
            borderRadius: "50%",
            boxShadow: `0 0 0 6px ${FUCHSIA}1A, 0 0 40px ${FUCHSIA}55`,
            zIndex: 2,
          }}>
            <Icon name="target" size={20} color={FUCHSIA} strokeWidth={2.4} />
            <Mono size={8.5} color={FUCHSIA_DEEP}
              style={{ letterSpacing: "0.20em", fontWeight: 700, marginTop: 4 }}>
              REACH
            </Mono>
            <Mono size={8.5} color={FUCHSIA_DEEP}
              style={{ letterSpacing: "0.20em", fontWeight: 700 }}>
              GATE
            </Mono>
          </div>
          <Mono size={8.5} color={FUCHSIA_DEEP}
            style={{
              position: "absolute",
              top: "calc(50% + 64px)", left: "50%", transform: "translateX(-50%)",
              letterSpacing: "0.14em", fontWeight: 700, whiteSpace: "nowrap",
            }}>
            has email ∨ phone?
          </Mono>
        </div>

        {/* RIGHT · two stacked outcome cards */}
        <div style={{ display: "flex", flexDirection: "column", gap: 14, height: "100%", minWidth: 0 }}>
          {/* CRM card */}
          <div style={{
            flex: `0 0 ${crmCenterY + crmHalfH + 40}px`,
            position: "relative",
            background: `linear-gradient(135deg, #fff, ${FUCHSIA_PALE}55)`,
            border: `1.5px solid ${FUCHSIA}AA`,
            borderRadius: 12,
            padding: "16px 18px 14px",
            boxShadow: `0 22px 44px -24px ${FUCHSIA}77, 0 0 0 3px ${FUCHSIA}12`,
            overflow: "hidden",
          }}>
            <div style={{
              position: "absolute", top: 12, right: 14,
              padding: "3px 8px", borderRadius: 4,
              background: FUCHSIA, color: "#fff",
              fontFamily: MONO, fontSize: 8.5, fontWeight: 700, letterSpacing: "0.18em",
            }}>PROMOTED → CRM</div>

            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
              <Icon name="check" size={13} color={FUCHSIA} strokeWidth={2.6} />
              <Eyebrow color={FUCHSIA}>MARKETABLE GUESTS</Eyebrow>
            </div>
            <Mono size={9.5} color={FUCHSIA_DEEP} style={{ letterSpacing: "0.06em", marginBottom: 10 }}>
              ready for WhatsApp · email · segmentation
            </Mono>

            <div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: 12 }}>
              <span style={{
                fontFamily: MONO, fontSize: 30, fontWeight: 700, color: FUCHSIA_DEEP,
                fontVariantNumeric: "tabular-nums", letterSpacing: "-0.015em", lineHeight: 1,
              }}>14,290</span>
              <Mono size={10} color={DIM} style={{ letterSpacing: "0.04em" }}>contacts · auto-synced</Mono>
            </div>

            <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
              {[
                { ic: "mail",  lbl: "EMAIL",    n: "14,290" },
                { ic: "phone", lbl: "WHATSAPP", n: "11,840" },
                { ic: "spark", lbl: "SEGMENTS", n: "32" },
              ].map(x => (
                <div key={x.lbl} style={{
                  display: "inline-flex", alignItems: "center", gap: 6,
                  padding: "5px 9px", borderRadius: 6,
                  background: "#fff", border: `1px solid ${FUCHSIA}55`,
                }}>
                  <Icon name={x.ic} size={10} color={FUCHSIA} strokeWidth={2.4} />
                  <Mono size={9} color={FUCHSIA_DEEP} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>{x.lbl}</Mono>
                  <Mono size={11} color={INK} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{x.n}</Mono>
                </div>
              ))}
            </div>
          </div>

          {/* Held card */}
          <div style={{
            flex: 1,
            position: "relative",
            background: PAPER,
            border: `1px dashed ${RULE}`,
            borderRadius: 12,
            padding: "16px 18px 14px",
            overflow: "hidden",
          }}>
            <div style={{
              position: "absolute", top: 12, right: 14,
              padding: "3px 8px", borderRadius: 4,
              background: "#fff", border: `1px solid ${RULE}`, color: DIM,
              fontFamily: MONO, fontSize: 8.5, fontWeight: 700, letterSpacing: "0.18em",
            }}>HELD · WAITING</div>

            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
              <Icon name="dash" size={13} color={DIM} strokeWidth={2.4} />
              <Eyebrow color={DIM}>NOT YET REACHABLE</Eyebrow>
            </div>
            <Mono size={9.5} color={FAINT} style={{ letterSpacing: "0.06em", marginBottom: 10 }}>
              consolidated · still in CDP
            </Mono>

            <div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: 10, flexWrap: "wrap" }}>
              <span style={{
                fontFamily: MONO, fontSize: 30, fontWeight: 700, color: INK,
                fontVariantNumeric: "tabular-nums", letterSpacing: "-0.015em", lineHeight: 1,
              }}>4,130</span>
              <Mono size={10} color={DIM} style={{ letterSpacing: "0.04em" }}>awaiting enrichment</Mono>
              <span style={{ flex: 1 }} />
              <Mono size={9} color={FUCHSIA} style={{ letterSpacing: "0.14em", fontWeight: 700 }}>
                ↑ +38 / DAY → CRM
              </Mono>
            </div>

            <div style={{
              padding: "9px 12px", borderRadius: 7,
              background: "#fff", border: `1px solid ${RULE}`,
              display: "flex", alignItems: "flex-start", gap: 9,
            }}>
              <Icon name="cal" size={11} color={FUCHSIA} strokeWidth={2.2}
                style={{ marginTop: 1, flexShrink: 0 }} />
              <Mono size={9.5} color={INK} style={{ letterSpacing: "0.02em", lineHeight: 1.5 }}>
                <span style={{ color: FUCHSIA_DEEP, fontWeight: 700 }}>Enriched on the next booking.</span>{" "}
                The next checkout with the same name + DOB folds in · if it carries an email or phone,
                the record is auto-promoted to the CRM.
              </Mono>
            </div>
          </div>
        </div>
      </div>
    </div>
    </ScaleFrame>
  );
}

// ─── BOTTOM strip · the never-ending loop ───
function PGLoopRibbon() {
  const W = 1100, H = 200;
  const stops = [
    { x: 90,  label: "5 PMS",          sub: "Apaleo · Opera · Mews · Cloudbeds · Protel", icon: "building2", color: DIM },
    { x: 310, label: "INGESTION",      sub: "confirmed checkouts only", icon: "arrow-r",   color: DIM },
    { x: 520, label: "CDP",            sub: "all guests + all bookings", icon: "shield",   color: FUCHSIA },
    { x: 740, label: "RESOLUTION",     sub: "det → fuzzy → AI",          icon: "merge",    color: FUCHSIA },
    { x: 970, label: "REACH GATE",     sub: "→ CRM ∣ ↺ CDP",             icon: "target",   color: FUCHSIA },
  ];

  return (
    <div style={{
      maxWidth: 1216, margin: "32px auto 0",
      background: "#fff", border: `1px solid ${RULE}`, borderRadius: 14,
      padding: "20px 22px 18px", position: "relative", overflow: "hidden",
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 14 }}>
        <Icon name="globe" size={13} color={FUCHSIA} strokeWidth={2.2} />
        <Eyebrow color={FUCHSIA}>The never-ending ingestion loop</Eyebrow>
        <span style={{ flex: 1 }} />
        <Mono size={9} color={FAINT} style={{ letterSpacing: "0.16em", fontWeight: 700 }}>
          DAILY @ 04:00 LOCAL · PER PROPERTY
        </Mono>
      </div>

      <svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%", height: H, display: "block" }}>
        <defs>
          <radialGradient id="pg-loop-glow" cx="50%" cy="50%" r="50%">
            <stop offset="0%"  stopColor={FUCHSIA} stopOpacity="1"/>
            <stop offset="60%" stopColor={FUCHSIA} stopOpacity="0.4"/>
            <stop offset="100%" stopColor={FUCHSIA} stopOpacity="0"/>
          </radialGradient>
          <marker id="pg-arrow" viewBox="0 0 10 10" refX="9" refY="5"
            markerWidth="7" markerHeight="7" orient="auto">
            <path d="M0,0 L10,5 L0,10 z" fill={FUCHSIA} opacity="0.7" />
          </marker>
          <marker id="pg-arrow-dim" viewBox="0 0 10 10" refX="9" refY="5"
            markerWidth="7" markerHeight="7" orient="auto">
            <path d="M0,0 L10,5 L0,10 z" fill={DIM} opacity="0.55" />
          </marker>
        </defs>

        <path d={`M ${stops[0].x + 30} 90 L ${stops[stops.length - 1].x - 30} 90`}
          fill="none" stroke={FUCHSIA} strokeWidth="1.4" strokeOpacity="0.7"
          markerEnd="url(#pg-arrow)" />

        {[0, 1.2, 2.4].map((d, i) => (
          <circle key={i} r="3.2" fill={FUCHSIA}>
            <animateMotion dur="3.6s" begin={`${d}s`} repeatCount="indefinite"
              path={`M ${stops[0].x + 30} 90 L ${stops[stops.length - 1].x - 30} 90`} />
          </circle>
        ))}

        <path d={`M ${stops[stops.length - 1].x} 110
                  C ${stops[stops.length - 1].x + 40} 170,
                    ${stops[0].x - 40} 170,
                    ${stops[0].x} 110`}
          fill="none" stroke={DIM} strokeOpacity="0.45" strokeWidth="1.3"
          strokeDasharray="4 5" markerEnd="url(#pg-arrow-dim)" />
        <text x={W / 2} y="178" fontFamily={MONO} fontSize="9.5" fill={DIM}
          textAnchor="middle" fontWeight="600" letterSpacing="0.16em">
          ↺ NEXT DAY · NEW CHECKOUTS · NEW MERGES · NEW PROMOTIONS
        </text>

        {stops.map((s, i) => (
          <g key={i} transform={`translate(${s.x}, 90)`}>
            <circle r="26" fill={"#fff"} stroke={s.color} strokeWidth="1.3" />
            {s.color === FUCHSIA && (
              <circle r="34" fill="url(#pg-loop-glow)" opacity="0.35" />
            )}
            <foreignObject x="-13" y="-13" width="26" height="26">
              <div xmlns="http://www.w3.org/1999/xhtml" style={{
                width: 26, height: 26,
                display: "flex", alignItems: "center", justifyContent: "center",
                color: s.color,
              }}>
                <Icon name={s.icon} size={13} color={s.color} strokeWidth={2.2} />
              </div>
            </foreignObject>
            <text x="0" y="-38" fontFamily={MONO} fontSize="10" fill={INK}
              textAnchor="middle" fontWeight="700" letterSpacing="0.18em">
              {String(i + 1).padStart(2, "0")} · {s.label}
            </text>
            <text x="0" y="48" fontFamily={MONO} fontSize="9" fill={DIM}
              textAnchor="middle" fontStyle="italic" letterSpacing="0.04em">
              {s.sub}
            </text>
          </g>
        ))}

        <g transform="translate(20, 6)">
          <rect x="0" y="0" width="220" height="34" rx="6"
            fill={PAPER} stroke={FUCHSIA} strokeWidth="1.2" />
          <rect x="0" y="0" width="3" height="34" fill={FUCHSIA} />
          <text x="12" y="14" fontFamily={MONO} fontSize="8.5" fill={FUCHSIA_DEEP}
            fontWeight="700" letterSpacing="0.18em">DAY 0 · BIG BANG</text>
          <text x="12" y="27" fontFamily={MONO} fontSize="9" fill={INK}
            letterSpacing="0.02em">
            <tspan fontWeight="700">147,420</tspan> historical checkouts · single run
          </text>
        </g>

        <g transform="translate(860, 6)">
          <rect x="0" y="0" width="220" height="34" rx="6"
            fill={PAPER} stroke={GREEN} strokeWidth="1.2" />
          <rect x="0" y="0" width="3" height="34" fill={GREEN} />
          <text x="12" y="14" fontFamily={MONO} fontSize="8.5" fill={GREEN}
            fontWeight="700" letterSpacing="0.18em">DAILY DELTA</text>
          <text x="12" y="27" fontFamily={MONO} fontSize="9" fill={INK}
            letterSpacing="0.02em">
            ~<tspan fontWeight="700">480</tspan> new checkouts/day · ~<tspan fontWeight="700">340</tspan> merges
          </text>
        </g>
      </svg>
    </div>
  );
}

function PromotionGate() {
  return (
    <section data-screen-label="CDP Promotion Gate" style={{
      padding: "70px 32px 16px", background: CREAM, position: "relative",
    }}>
      <div style={{ maxWidth: 1216, margin: "0 auto 28px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
          <span style={{ width: 14, height: 1.5, background: FUCHSIA }} />
          <Mono size={11} color={FUCHSIA} style={{ letterSpacing: "0.22em", textTransform: "uppercase", fontWeight: 700 }}>
            After consolidation · the reachability gate
          </Mono>
        </div>
        <h2 style={{
          fontFamily: SERIF, fontWeight: 400, fontSize: 40, letterSpacing: "-0.025em",
          lineHeight: 1.1, margin: 0, maxWidth: 940,
        }}>
          Many bookings collapse into one record.{" "}
          <span style={{ fontStyle: "italic", color: FUCHSIA }}>
            Then a single rule decides where it lives.
          </span>
        </h2>
        <p style={{
          fontSize: 14, color: DIM, lineHeight: 1.65,
          maxWidth: 880, margin: "18px 0 0",
        }}>
          Resolution leaves every guest as exactly one golden record. The CDP then asks one
          question of each record: <em style={{ color: INK }}>does it have a verified email or
          phone?</em> If yes, it is auto-promoted to the CRM and immediately becomes marketable on
          WhatsApp, email and inside any segment. If no, it stays in the CDP · fully consolidated,
          waiting for the next booking to enrich it. Either way,{" "}
          <strong style={{ color: INK }}>every guest and every booking sits in the CDP</strong>.
          The CRM only sees the reachable subset.
        </p>
      </div>

      {/* top stats ribbon */}
      <div style={{
        maxWidth: 1216, margin: "0 auto 22px",
        display: "grid", gridTemplateColumns: "repeat(5, 1fr)",
        gap: 0,
        background: PAPER, border: `1px solid ${RULE}`, borderRadius: 999,
        padding: "10px 24px",
      }}>
        {[
          { lbl: "RAW BOOKINGS",  v: "47,830", tone: DIM },
          { lbl: "AFTER MERGE",   v: "18,420", tone: INK },
          { lbl: "REDUCTION",     v: "−61.5%", tone: FUCHSIA },
          { lbl: "CRM-READY",     v: "14,290", tone: GREEN },
          { lbl: "HELD IN CDP",   v: "4,130",  tone: DIM },
        ].map((s, i) => (
          <div key={i} style={{
            display: "flex", flexDirection: "column", alignItems: "center",
            borderLeft: i === 0 ? "none" : `1px solid ${RULE}`,
            padding: "0 12px",
          }}>
            <Mono size={9} color={FAINT} style={{ letterSpacing: "0.18em", fontWeight: 700 }}>
              {s.lbl}
            </Mono>
            <span style={{
              fontFamily: MONO, fontSize: 18, fontWeight: 700, color: s.tone,
              fontVariantNumeric: "tabular-nums", letterSpacing: "-0.005em", marginTop: 2,
            }}>{s.v}</span>
          </div>
        ))}
      </div>

      <PGFlow />
      <PGLoopRibbon />
    </section>
  );
}
function CDPPrimitives() {
  return (
    <section style={{ padding: "60px 32px 16px", background: CREAM }}>
      <div style={{ maxWidth: 1216, margin: "0 auto 32px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
          <span style={{ width: 14, height: 1.5, background: FUCHSIA }} />
          <Mono size={11} color={FUCHSIA} style={{ letterSpacing: "0.22em", textTransform: "uppercase", fontWeight: 700 }}>
            12 primitives · one guest, every hotel
          </Mono>
        </div>
        <h2 style={{
          fontFamily: SERIF, fontWeight: 400, fontSize: 40, letterSpacing: "-0.025em",
          lineHeight: 1.1, margin: 0, maxWidth: 940,
        }}>
          Identity resolution that earns every match it claims.{" "}
          <span style={{ fontStyle: "italic", color: FUCHSIA }}>
            Three stages, one decision, full provenance · across every property in the group.
          </span>
        </h2>
        <p style={{
          fontSize: 14, color: DIM, lineHeight: 1.6,
          maxWidth: 780, margin: "18px 0 0",
        }}>
          Cross-PMS ingestion, deterministic-first matching, fuzzy 9-feature scoring on the variants,
          Claude AI on the long tail. Every canonical field attributed to its source hotel, every
          decision logged with confidence and reasoning, every reachable profile promoted into the
          CRM automatically. The CDP is not a side-effect of your warehouse. It is the source of
          truth your campaigns, journeys, segments and agentic replies all sit on top of.
        </p>
      </div>
      <div style={{
        maxWidth: 1216, margin: "0 auto",
        display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12,
      }}>
        {PRIMITIVES.map((p) => <PrimitiveCard key={p.idx} p={p} />)}
      </div>
    </section>
  );
}

// ─── header (breadcrumbs · eyebrow · H1 · H2) ───
//
// Was its own local design system — Source Serif 4, a muted #CE5188 fuchsia,
// an all-caps mono chevron breadcrumb — none of it the site's actual tokens
// (Instrument Serif via .h-display, --accent/--c-instagram, the Home / Platform
// / X breadcrumb every other product page uses). It read as a different,
// disconnected product next to CRM B2C. Rebuilt on the shared classes so it
// can't drift from them again; only the breadcrumb needs inline styling
// since this file has no <style> block of its own to hold a .cdp-crumb rule.
function Header() {
  const crumbLinkStyle = { color: "var(--ink-3)", textDecoration: "none" };
  return (
    <div style={{
      position: "relative", maxWidth: 1216, margin: "0 auto 40px", textAlign: "center",
    }}>
      <div style={{
        display: "flex", justifyContent: "center", alignItems: "center", gap: 10,
        fontFamily: "var(--f-mono)", fontSize: 11, color: "var(--ink-3)",
        letterSpacing: "0.06em", marginBottom: 22,
      }}>
        <a href="../" style={crumbLinkStyle}>Home</a>
        <span style={{ opacity: 0.5 }}>/</span>
        <a href="../#platform" style={crumbLinkStyle}>Platform</a>
        <span style={{ opacity: 0.5 }}>/</span>
        <span style={{ color: "var(--c-instagram)" }}>CDP</span>
      </div>

      <div className="eyebrow" style={{ justifyContent: "center" }}>Customer Data Platform</div>

      <h1 className="h-display" style={{ marginTop: 24, maxWidth: 940, marginInline: "auto" }}>
        It wins the matches other systems abandon.{" "}
        <em className="italic-accent">Then proves every one.</em>
      </h1>

      <p className="lead" style={{ maxWidth: 720, marginTop: 24, marginInline: "auto" }}>
        A guest who stayed at four of your hotels under three slightly different names
        is still the same person. GuestMaker CDP resolves them in real time: deterministic
        for the easy match, fuzzy scoring for variants, and Claude AI for the long tail.
        Every decision auditable, every field attributed to its source hotel,
        and only reachable profiles promoted into the CRM.
      </p>

      <div className="flex gap-3 wrap center" style={{ marginTop: 32, justifyContent: "center" }}>
        <a href={DEMO_URL} className="btn btn-primary">
          Book a demo <IconArrow size={14} className="arrow" />
        </a>
        <a href="#analytics" className="btn btn-ghost">See the data layer</a>
      </div>
    </div>
  );
}

// ─── playback bar ───
function PlayBar({ time, playing, onToggle, onSeek }) {
  const merged = PACKETS.filter(p => time >= p.t && p.kind === "merge").length;
  const created = PACKETS.filter(p => time >= p.t && p.kind === "new").length;
  const aiReview = PACKETS.filter(p => time >= p.t && p.stage === "ai").length;
  const skipped = PACKETS.filter(p => time >= p.t && p.kind === "skip").length;

  return (
    <div style={{
      maxWidth: 1216, margin: "26px auto 0",
      background: "rgba(255,255,255,0.7)",
      border: `1px solid ${RULE}`,
      borderRadius: 14, padding: "12px 18px",
      backdropFilter: "blur(8px)",
      WebkitBackdropFilter: "blur(8px)",
      display: "grid",
      gridTemplateColumns: "auto 1fr auto",
      gap: 22, alignItems: "center",
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
        <button onClick={onToggle} style={{
          display: "inline-flex", alignItems: "center", gap: 9,
          height: 38, padding: "0 16px 0 13px", borderRadius: 999,
          background: playing ? INK : FUCHSIA, border: "none", cursor: "pointer",
          color: "#fff", fontSize: 13, fontWeight: 600, letterSpacing: "-0.005em",
          fontFamily: "'Inter', system-ui, sans-serif",
          boxShadow: playing ? "0 8px 18px -10px rgba(0,0,0,0.5)" : `0 10px 22px -10px ${FUCHSIA}AA`,
        }}>
          <Icon name={playing ? "pause" : "play"} size={11} color="#fff" />
          <span>{playing ? "Pause" : "Play"}</span>
        </button>
        <Mono size={12} color={INK} style={{ fontVariantNumeric: "tabular-nums" }}>
          {fmt(time)} <span style={{ color: FAINT }}>/ {fmt(TIMELINE_END)}</span>
        </Mono>
      </div>

      <div style={{ position: "relative", height: 28, cursor: "pointer" }}
        onClick={(e) => {
          const rect = e.currentTarget.getBoundingClientRect();
          const pct = (e.clientX - rect.left) / rect.width;
          onSeek(Math.max(0, Math.min(TIMELINE_END - 0.05, pct * TIMELINE_END)));
        }}
      >
        <div style={{
          position: "absolute", top: 13, left: 0, right: 0, height: 3,
          background: `${FUCHSIA}15`, borderRadius: 2,
        }} />
        <div style={{
          position: "absolute", top: 13, left: 0,
          width: `${(time / TIMELINE_END) * 100}%`, height: 3,
          background: `linear-gradient(90deg, ${FUCHSIA}, ${PLUM})`,
          borderRadius: 2, pointerEvents: "none",
        }} />
        {PACKETS.map((p, i) => {
          const left = (p.t / TIMELINE_END) * 100;
          const tone = p.kind === "new" ? GREEN
            : p.kind === "skip" ? RED
            : p.stage === "ai" ? PLUM
            : p.stage === "fuzzy" ? AMBER : FUCHSIA;
          return (
            <span key={i} style={{
              position: "absolute", top: 10, left: `${left}%`,
              width: 2, height: 9, background: tone, opacity: time >= p.t ? 1 : 0.35,
              transform: "translateX(-1px)", borderRadius: 1, pointerEvents: "none",
            }} />
          );
        })}
      </div>

      <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
        <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
          <Icon name="spark" size={12} color={GREEN} strokeWidth={2.2} />
          <Mono size={9} color={FAINT} style={{ letterSpacing: "0.18em", fontWeight: 600 }}>NEW</Mono>
          <Mono size={13} color={GREEN} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{created}</Mono>
        </span>
        <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
          <Icon name="merge" size={12} color={FUCHSIA} strokeWidth={2.2} />
          <Mono size={9} color={FAINT} style={{ letterSpacing: "0.18em", fontWeight: 600 }}>MERGED</Mono>
          <Mono size={13} color={FUCHSIA} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{merged}</Mono>
        </span>
        <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
          <Icon name="brain" size={12} color={PLUM} strokeWidth={2.2} />
          <Mono size={9} color={FAINT} style={{ letterSpacing: "0.18em", fontWeight: 600 }}>AI</Mono>
          <Mono size={13} color={PLUM} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{aiReview}</Mono>
        </span>
        <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
          <Icon name="x" size={12} color={RED} strokeWidth={2.4} />
          <Mono size={9} color={FAINT} style={{ letterSpacing: "0.18em", fontWeight: 600 }}>SKIP</Mono>
          <Mono size={13} color={RED} style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{skipped}</Mono>
        </span>
      </div>
    </div>
  );
}

// ─── main hero ───
function HotelCDPHero({ tweaks }) {
  const speed = tweaks?.speed ?? 1;
  const { time, setTime, playing, setPlaying } = usePlayback(speed);

  return (
    <section data-screen-label="Hotel CDP" style={{
      position: "relative",
      minHeight: "100vh",
      background: CREAM,
      color: INK,
      fontFamily: "'Inter', system-ui, sans-serif",
      overflow: "hidden",
      padding: "56px 32px 48px",
    }}>
      <div style={{
        position: "absolute", top: -200, left: "50%", width: 900, height: 700,
        transform: "translateX(-50%)",
        background: `radial-gradient(ellipse, rgba(225, 48, 108, 0.10) 0%, transparent 60%)`,
        filter: "blur(40px)", pointerEvents: "none",
      }} />

      <Header />
      <SourcesStrip time={time} />
      <Workbench time={time} playing={playing} />
      <GoldenAssemblyStrip time={time} />
      <CDPPrimitives />
      <VisitorJourney />
      <PromotionGate />
    </section>
  );
}

window.HotelCDPHero = HotelCDPHero;
