// demos.jsx — Ask line + Map + Trip builder
// Three interactive moments that show how Plinn writes, sees, and composes.
// No live API dependencies — canned responses keep the demo deterministic.
//
// Note: an Apple Maps (MapKit JS) version of the map is parked under
// parked/apple-maps/ — the live map below is the custom hand-built one.

const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ─────────────────────────────────────────────────────────────
// Shared atoms
// ─────────────────────────────────────────────────────────────

function DemoFrame({ eyebrow, title, lede, children, kicker }) {
  return (
    <section className="section">
      <div className="section-head">
        <div>
          <div className="eyebrow">{eyebrow}</div>
          <h2 dangerouslySetInnerHTML={{ __html: title }}/>
        </div>
        <div className="lede" dangerouslySetInnerHTML={{ __html: lede }}/>
      </div>
      {kicker && <div className="mono" style={{ marginBottom: 24, color: 'var(--mute)' }}>{kicker}</div>}
      {children}
    </section>
  );
}

// ═════════════════════════════════════════════════════════════
// 1. ASK LINE
// ═════════════════════════════════════════════════════════════

const ASK_SUGGESTIONS = [
  'Four days in Lisbon, walking, art-leaning',
  'Coffee my friends liked, walkable from here',
  'One quiet weekend, somewhere with a coastline',
];

// Pre-written replies — in Plinn voice (specific, quiet, italic for warmth).
// Generic fallback used if no match.
const ASK_REPLIES = {
  default: {
    answer:
      'Four days, twelve of your places, mostly on foot. Day one stays in <em>Príncipe Real</em> — the bookshop, the late lunch you saved, the rooftop. Day two crosses to <em>Belém</em>. Day three is quieter, just Alfama and a walk back along the river. Edit anything that doesn\'t feel right.',
    cite: 'Composed from 12 of your saved places in Lisbon · 3 from Mariana',
  },
  coffee: {
    answer:
      '<em>Man vs. Machine</em>, 0.6 km — three friends saved it. <em>Amaze</em>, 0.9 km — Lena loves the morning light. One you haven\'t seen yet: <em>Truth Coffee</em>, 350 m — opened in March, four-six on Google.',
    cite: 'From your friends\' lists · plus one match from the neighbourhood',
  },
  quiet: {
    answer:
      'Two come to mind. <em>Cadaqués</em> — small, the museum is closed Mondays, you\'d like the late ferry. <em>Porto Covo</em> — quieter still, no museum, one bakery worth the walk. Both are two trains from where you live.',
    cite: 'Matched to your saved coastlines · 2 of 14 places',
  },
};

function pickReply(query) {
  const q = query.toLowerCase();
  if (q.includes('coffee') || q.includes('café') || q.includes('cafe')) return ASK_REPLIES.coffee;
  if (q.includes('quiet') || q.includes('weekend') || q.includes('coast')) return ASK_REPLIES.quiet;
  return ASK_REPLIES.default;
}

function useTypewriter(text, speed = 14) {
  const [out, setOut] = useState('');
  useEffect(() => {
    if (!text) { setOut(''); return; }
    setOut('');
    let i = 0;
    const id = setInterval(() => {
      i += 1;
      setOut(text.slice(0, i));
      if (i >= text.length) clearInterval(id);
    }, speed);
    return () => clearInterval(id);
  }, [text, speed]);
  return out;
}

function AskDemo() {
  const [query, setQuery] = useState('');
  const [submitted, setSubmitted] = useState(null);   // the locked-in query
  const [thinking, setThinking] = useState(false);
  const inputRef = useRef(null);

  const reply = useMemo(() => submitted ? pickReply(submitted) : null, [submitted]);
  const answer = useTypewriter(thinking ? '' : (reply?.answer || ''), 12);

  const submit = (val) => {
    const v = (val ?? query).trim();
    if (!v) return;
    setSubmitted(v);
    setQuery(v);
    setThinking(true);
    setTimeout(() => setThinking(false), 800);
  };

  const reset = () => {
    setSubmitted(null);
    setQuery('');
    setThinking(false);
    setTimeout(() => inputRef.current?.focus(), 50);
  };

  return (
    <DemoFrame
      eyebrow="01 · Ask"
      title="A sentence is the <em>fastest</em> way."
      lede="Type what you want. Plinn reads your saved places, your taste, and your friends' lists — and answers in plain English."
    >
      <div className="ask-card">
        <div className="ask-row">
          <span className="ask-diamond">◆</span>
          <input
            ref={inputRef}
            className="ask-input"
            placeholder="Ask Plinn anything…"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            onKeyDown={(e) => e.key === 'Enter' && submit()}
            disabled={!!submitted && thinking}
          />
          {submitted ? (
            <button className="ask-clear" onClick={reset} aria-label="ask again">
              ASK AGAIN
            </button>
          ) : (
            <button className="ask-go" onClick={() => submit()}>
              Ask
            </button>
          )}
        </div>

        {!submitted && (
          <div className="ask-suggestions">
            <div className="mono" style={{ marginBottom: 14 }}>Try one of these</div>
            {ASK_SUGGESTIONS.map((s, i) => (
              <button
                key={i}
                className="ask-suggestion"
                onClick={() => submit(s)}
              >
                <span className="arr">→</span>
                <span>{s}</span>
              </button>
            ))}
          </div>
        )}

        {submitted && (
          <div className="ask-answer">
            <div className="mono" style={{ marginBottom: 18 }}>You asked</div>
            <div className="ask-question">"{submitted}"</div>

            <div className="mono" style={{ marginTop: 36, marginBottom: 18 }}>
              {thinking ? 'Plinn is reading your places…' : 'Plinn says'}
            </div>

            {!thinking && (
              <div
                className="ask-reply"
                dangerouslySetInnerHTML={{ __html: answer + (answer.length < (reply?.answer || '').length ? '<span class="cursor">▌</span>' : '') }}
              />
            )}
            {thinking && <div className="ask-thinking"><span/><span/><span/></div>}

            {!thinking && answer.length >= (reply?.answer || '').length && (
              <div className="ask-cite">{reply.cite}</div>
            )}
          </div>
        )}
      </div>

      <div className="demo-foot mono">
        Honest about its source · {submitted ? '1 of 12 example questions' : 'reads your real saved places · no account demo'}
      </div>
    </DemoFrame>
  );
}

// ═════════════════════════════════════════════════════════════
// 2. MAP — with the real label-priority + collision system
// ═════════════════════════════════════════════════════════════

// World coordinates run 0..1000 in both axes. Pan + zoom transform to screen.
const PINS_DATA = [
  // Loved (red, label always wins)
  { id: 'tantris',   x: 540, y: 320, tier: 'loved',     cat: 'Restaurant', name: 'Tantris',         meta: '0.4 km' },
  { id: 'pinakothek',x: 360, y: 280, tier: 'loved',     cat: 'Museum',     name: 'Alte Pinakothek', meta: '1.2 km' },
  // Saved (ink, labels show by default)
  { id: 'viktualien',x: 500, y: 480, tier: 'saved',     cat: 'Market',     name: 'Viktualienmarkt', meta: '180 m' },
  { id: 'andaz',     x: 280, y: 540, tier: 'saved',     cat: 'Hotel',      name: 'Andaz',           meta: '2.6 km' },
  { id: 'amaze',     x: 680, y: 560, tier: 'saved',     cat: 'Café',       name: 'Amaze',           meta: '0.9 km' },
  { id: 'mvm',       x: 440, y: 660, tier: 'saved',     cat: 'Café',       name: 'Man vs. Machine', meta: '0.6 km' },
  { id: 'tegernsee', x: 760, y: 380, tier: 'saved',     cat: 'Pastry',     name: 'Konditorei',      meta: '1.1 km' },
  { id: 'glock',     x: 220, y: 410, tier: 'saved',     cat: 'Bookshop',   name: 'Glockenbach',     meta: '1.8 km' },
  // Discovery (small ink dots, no labels by default)
  { id: 'd1', x: 420, y: 240, tier: 'discovery', cat: 'Café',   name: 'Truth Coffee', meta: '350 m · ★ 4.6' },
  { id: 'd2', x: 600, y: 360, tier: 'discovery', cat: 'Wine',   name: 'Vinaiolo',     meta: '420 m' },
  { id: 'd3', x: 240, y: 500, tier: 'discovery', cat: 'Pizza',  name: 'Lo Studente',  meta: '1.4 km' },
  { id: 'd4', x: 720, y: 480, tier: 'discovery', cat: 'Café',   name: 'Aroma',        meta: '900 m' },
  { id: 'd5', x: 320, y: 720, tier: 'discovery', cat: 'Bar',    name: 'Roeckl',       meta: '1.6 km' },
  { id: 'd6', x: 580, y: 780, tier: 'discovery', cat: 'Italian',name: 'Trattoria',    meta: '2.1 km' },
  { id: 'd7', x: 760, y: 700, tier: 'discovery', cat: 'Asian',  name: 'Koi',          meta: '2.4 km' },
  { id: 'd8', x: 180, y: 360, tier: 'discovery', cat: 'Bakery', name: 'Hofpfisterei', meta: '1.9 km' },
  { id: 'd9', x: 820, y: 340, tier: 'discovery', cat: 'Bistro', name: 'Käfer',        meta: '2.7 km' },
  { id: 'd10',x: 460, y: 800, tier: 'discovery', cat: 'Coffee', name: 'Standl 20',    meta: '1.2 km' },
];

const TIER_BASE = { loved: 100, saved: 70, discovery: 0 };

function stableHash(id) {
  let h = 0;
  for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) | 0;
  return Math.abs(h % 1000) / 1000;
}

// Estimate label box size in screen px from the pin's content
function labelSize(pin) {
  const nameW = pin.name ? pin.name.length * 7.2 + 16 : 0;
  const catW = (pin.cat?.length || 0) * 5.6 + 28;
  const metaW = (pin.meta?.length || 0) * 5.6 + 8;
  return { w: Math.max(nameW, catW, metaW), h: 44 };
}

function rectsOverlap(a, b) {
  return !(a.x + a.w < b.x || b.x + b.w < a.x || a.y + a.h < b.y || b.y + b.h < a.y);
}

function MapDemo() {
  // viewport pixel size; map fills the container
  const containerRef = useRef(null);
  const [viewport, setViewport] = useState({ w: 800, h: 520 });
  const [pan, setPan] = useState({ x: 500, y: 530 });
  const [zoom, setZoom] = useState(1.25);
  const [density, setDensity] = useState('SAVED'); // NONE | SAVED | NEW
  const [selectedId, setSelectedId] = useState(null);
  const [search, setSearch] = useState({ active: false, matches: new Set() });
  const dragRef = useRef(null);

  // measure container
  useEffect(() => {
    if (!containerRef.current) return;
    const ro = new ResizeObserver(() => {
      const r = containerRef.current.getBoundingClientRect();
      setViewport({ w: r.width, h: r.height });
    });
    ro.observe(containerRef.current);
    return () => ro.disconnect();
  }, []);

  // World → screen projection
  const project = useCallback((p) => ({
    x: (p.x - pan.x) * zoom + viewport.w / 2,
    y: (p.y - pan.y) * zoom + viewport.h / 2,
  }), [pan, zoom, viewport]);

  // Drag handlers
  const onPointerDown = (e) => {
    dragRef.current = { sx: e.clientX, sy: e.clientY, px: pan.x, py: pan.y };
    e.currentTarget.setPointerCapture(e.pointerId);
  };
  const onPointerMove = (e) => {
    if (!dragRef.current) return;
    const dx = (e.clientX - dragRef.current.sx) / zoom;
    const dy = (e.clientY - dragRef.current.sy) / zoom;
    setPan({ x: dragRef.current.px - dx, y: dragRef.current.py - dy });
  };
  const onPointerUp = () => { dragRef.current = null; };
  const onWheel = (e) => {
    e.preventDefault();
    const next = Math.max(0.9, Math.min(3.5, zoom * (e.deltaY < 0 ? 1.08 : 0.93)));
    setZoom(next);
  };

  // ── The label intelligence system ──
  // Returns { id → { side: 'left'|'right', sx, sy, w, h } | null }
  const labelPlacement = useMemo(() => {
    const isMatch = (p) => search.active && search.matches.has(p.id);

    // 1. Eligibility per zoom level
    const eligible = (p) => {
      if (zoom < 1.0) return p.tier === 'loved' || p.id === selectedId;
      if (zoom < 1.5) return p.tier !== 'discovery' || isMatch(p);
      return true;
    };

    // 2. Density toggle
    const densityAllows = (p) => {
      if (density === 'NONE') return false;
      if (density === 'SAVED') return p.tier === 'loved' || p.tier === 'saved';
      if (density === 'NEW') return p.tier === 'discovery';
      return true;
    };

    // 3. Score
    const score = (p) => {
      const base = TIER_BASE[p.tier];
      const s = isMatch(p) ? 40 : 0;
      const sel = p.id === selectedId ? 1000 : 0;
      return base + s + sel + stableHash(p.id);
    };

    // 4. Project all & filter in-viewport
    const pad = 60;
    const inView = PINS_DATA
      .map((p) => ({ pin: p, screen: project(p) }))
      .filter(({ screen }) =>
        screen.x > -pad && screen.x < viewport.w + pad &&
        screen.y > -pad && screen.y < viewport.h + pad
      );

    // 5. Determine which pins want a label
    const candidates = inView.filter(({ pin }) => {
      if (pin.id === selectedId) return true;          // selected always
      if (!densityAllows(pin)) return false;
      if (!eligible(pin)) return false;
      return true;
    });

    // 6. Sort by score descending
    candidates.sort((a, b) => score(b.pin) - score(a.pin));

    // 7. Greedy placement
    // Seed with chrome-occluder rects so labels avoid the search bar / density toggle / zoom controls
    const placed = [
      { x: 14,                  y: 0,                w: viewport.w - 28,  h: 58  },  // top search row
      { x: viewport.w - 80,     y: viewport.h - 100, w: 80,               h: 100 },  // density toggle
      { x: 14,                  y: viewport.h - 40,  w: 220,              h: 40  },  // zoom controls
      { x: viewport.w - 220,    y: 60,               w: 220,              h: 30  },  // HUD
    ];
    const dots = inView.map(({ pin, screen }) => ({ id: pin.id, x: screen.x - 7, y: screen.y - 7, w: 14, h: 14 }));
    const result = {};

    for (const { pin, screen } of candidates) {
      const { w, h } = labelSize(pin);
      const prefer = screen.x > viewport.w * 0.55 ? 'left' : 'right';
      const sides = [prefer, prefer === 'left' ? 'right' : 'left'];
      for (const side of sides) {
        const sx = side === 'right' ? screen.x + 16 : screen.x - 16 - w;
        const sy = screen.y - h / 2;
        // 4px breathing room on each side, but no padding on the side facing the pin
        const box = {
          x: side === 'right' ? sx : sx - 4,
          y: sy - 4,
          w: w + 4,
          h: h + 8,
        };
        const collides =
          placed.some((b) => rectsOverlap(box, b)) ||
          dots.some((b) => b.id !== pin.id && rectsOverlap(box, b));
        if (!collides) {
          placed.push(box);
          result[pin.id] = { side, sx, sy, w, h };
          break;
        }
      }
    }
    return result;
  }, [pan, zoom, viewport, density, selectedId, search, project]);

  // Search affordance
  const runSearch = (query) => {
    const matches = new Set(['amaze', 'mvm', 'd1', 'd4']);  // canned
    setSearch({ active: true, matches });
  };
  const clearSearch = () => setSearch({ active: false, matches: new Set() });

  const selected = selectedId ? PINS_DATA.find((p) => p.id === selectedId) : null;

  return (
    <DemoFrame
      eyebrow="02 · Map"
      title="Your <em>places</em>, where they are."
      lede="Loved at the top, saved in the middle, the rest as quiet dots. Pan and zoom — labels rearrange themselves so nothing crowds."
    >
      <div
        ref={containerRef}
        className="mapdemo"
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        onPointerCancel={onPointerUp}
        onWheel={onWheel}
        onClick={(e) => { if (e.target === containerRef.current) setSelectedId(null); }}
      >
        {/* Procedural map */}
        <svg className="mapdemo-canvas" width={viewport.w} height={viewport.h}>
          <defs>
            <pattern id="mgrid-site" patternUnits="userSpaceOnUse"
              width={48 * zoom} height={48 * zoom}
              x={-pan.x * zoom + viewport.w / 2}
              y={-pan.y * zoom + viewport.h / 2}>
              <path d={`M${48*zoom} 0 H0 V${48*zoom}`} fill="none" stroke="rgba(15,15,15,0.04)" strokeWidth="0.5"/>
            </pattern>
          </defs>
          <rect width="100%" height="100%" fill="#F2EFE6"/>
          <rect width="100%" height="100%" fill="url(#mgrid-site)"/>
          {/* park */}
          <ellipse
            cx={(360 - pan.x) * zoom + viewport.w / 2}
            cy={(180 - pan.y) * zoom + viewport.h / 2}
            rx={140 * zoom}
            ry={70 * zoom}
            fill="rgba(27,75,255,0.045)"
            stroke="rgba(15,15,15,0.06)"
            strokeWidth="0.5"
          />
          {/* river */}
          <path
            d={`M${-100} ${(700 - pan.y) * zoom + viewport.h / 2}
                Q ${(300 - pan.x) * zoom + viewport.w / 2} ${(640 - pan.y) * zoom + viewport.h / 2},
                  ${(600 - pan.x) * zoom + viewport.w / 2} ${(720 - pan.y) * zoom + viewport.h / 2}
                T ${viewport.w + 100} ${(680 - pan.y) * zoom + viewport.h / 2}`}
            fill="none"
            stroke="rgba(27,75,255,0.22)"
            strokeWidth={2}
          />
          {/* streets */}
          {[...Array(10)].map((_, i) => {
            const wy = 100 + i * 90;
            return (
              <line key={i}
                x1={-100}
                y1={(wy - pan.y) * zoom + viewport.h / 2}
                x2={viewport.w + 100}
                y2={(wy + 20 - pan.y) * zoom + viewport.h / 2}
                stroke="rgba(15,15,15,0.07)"
                strokeWidth="1"
              />
            );
          })}
          {[...Array(8)].map((_, i) => {
            const wx = 80 + i * 120;
            return (
              <line key={'v' + i}
                x1={(wx - pan.x) * zoom + viewport.w / 2}
                y1={-100}
                x2={(wx + 30 - pan.x) * zoom + viewport.w / 2}
                y2={viewport.h + 100}
                stroke="rgba(15,15,15,0.05)"
                strokeWidth="1"
              />
            );
          })}
        </svg>

        {/* You are here */}
        <div
          className="mapdemo-here"
          style={{
            left: project({ x: 500, y: 500 }).x,
            top: project({ x: 500, y: 500 }).y,
          }}
        />

        {/* Pins */}
        {PINS_DATA.map((p) => {
          const s = project(p);
          if (s.x < -40 || s.x > viewport.w + 40 || s.y < -40 || s.y > viewport.h + 40) return null;
          const label = labelPlacement[p.id];
          const isMatch = search.active && search.matches.has(p.id);
          const isDim = search.active && !isMatch && p.id !== selectedId;
          const isSelected = p.id === selectedId;

          const dot = p.tier === 'loved' ? (
            <span className="pin-dot loved"/>
          ) : p.tier === 'saved' ? (
            <span className={`pin-dot saved ${isSelected ? 'selected' : ''}`}/>
          ) : (
            <span className={`pin-dot discovery ${isMatch ? 'matched' : ''}`}/>
          );

          return (
            <div
              key={p.id}
              className="pin"
              style={{ left: s.x, top: s.y, opacity: isDim ? 0.25 : 1 }}
              onClick={(e) => { e.stopPropagation(); setSelectedId(isSelected ? null : p.id); }}
            >
              {label && (
                <div
                  className={`flag ${label.side} ${isSelected ? 'selected' : ''} ${isMatch ? 'match' : ''}`}
                  style={{
                    left: label.sx - s.x,
                    top: label.sy - s.y,
                    width: label.w,
                    height: label.h,
                  }}
                >
                  <div className="flag-tether"/>
                  <div className="flag-body">
                    <div className="flag-cat">
                      {isMatch && p.tier !== 'discovery' && <span className="diamond">◆</span>}
                      {isMatch && p.tier === 'discovery' && <span className="dot"/>}
                      <span>{isMatch && (p.tier === 'loved' || p.tier === 'saved') ? `saved · ${p.cat}` : p.cat}</span>
                    </div>
                    <div className="flag-name">{p.name}</div>
                    {p.meta && <div className="flag-meta">{p.meta}</div>}
                  </div>
                </div>
              )}
              {dot}
            </div>
          );
        })}

        {/* Top filter — NL search */}
        <div className="map-search">
          <span className="diamond">◆</span>
          {search.active ? (
            <>
              <div className="map-search-text active">coffee my friends liked, walkable from here</div>
              <span className="map-search-count">{search.matches.size} found</span>
              <button className="map-search-clear" onClick={clearSearch}>Clear</button>
            </>
          ) : (
            <>
              <div className="map-search-text">Try a search…</div>
              <button className="map-search-go" onClick={() => runSearch()}>Run example</button>
            </>
          )}
        </div>

        {/* Density toggle */}
        <div className="map-density">
          {['NONE', 'SAVED', 'NEW'].map((d) => (
            <button
              key={d}
              className={`density-cell ${density === d ? 'on' : ''}`}
              onClick={() => setDensity(d)}
            >{d}</button>
          ))}
        </div>

        {/* Zoom + recenter */}
        <div className="map-zoom">
          <button onClick={() => setZoom(Math.min(3.5, zoom * 1.15))}>+</button>
          <button onClick={() => setZoom(Math.max(0.9, zoom * 0.87))}>−</button>
          <button onClick={() => { setPan({ x: 500, y: 530 }); setZoom(1.25); }}>RECENTER</button>
        </div>

        {/* Selected sheet */}
        {selected && (
          <div className="map-sheet">
            <div className="map-sheet-top">
              <div className="map-sheet-cat">
                <span className="dot"/>
                {selected.tier.toUpperCase()} · {selected.cat.toUpperCase()}
              </div>
              <div className="mono">{selected.meta} · OPEN</div>
            </div>
            <div className="map-sheet-name">{selected.name}</div>
            <div className="map-sheet-sub">Munich · Saved Mar 12 · 2 friends loved this</div>
            <div className="map-sheet-actions">
              <button className="btn-ink" style={{ flex: 1, justifyContent: 'center', padding: '11px 0', fontSize: 11, letterSpacing: 1.3, textTransform: 'uppercase' }}>Add to today</button>
              <button className="btn-line" style={{ flex: 1, justifyContent: 'center', padding: '11px 0', fontSize: 11, letterSpacing: 1.3, textTransform: 'uppercase' }}>Open place</button>
            </div>
          </div>
        )}

        {/* HUD */}
        <div className="map-hud mono">
          zoom {zoom.toFixed(2)} · drag to pan · scroll to zoom
        </div>
      </div>

      <div className="demo-foot mono">
        <span><span className="dot loved"/> loved · always labelled</span>
        <span><span className="dot ink"/> saved · labelled when room</span>
        <span><span className="dot small"/> new · labelled when you ask</span>
      </div>
    </DemoFrame>
  );
}

// ═════════════════════════════════════════════════════════════
// 3. TRIP BUILDER
// ═════════════════════════════════════════════════════════════

const TRIP_PROMPT = 'four days in Lisbon, walking, art-leaning';
const TRIP_DAYS = [
  {
    date: 'Day 1 · Wed',
    head: 'Príncipe Real, slowly.',
    items: [
      { t: '10:00', n: 'Manteigaria', s: 'Pastéis · before the queue · 8 min walk' },
      { t: '13:00', n: 'Tartine Lisboa', s: 'Late lunch · you saved this in 2024' },
      { t: '16:30', n: 'Ler Devagar', s: 'Bookshop · LX Factory · 12 min taxi' },
      { t: '21:00', n: 'Park Bar', s: 'Rooftop · sunset · Mariana saved this' },
    ],
  },
  {
    date: 'Day 2 · Thu',
    head: 'Belém, on foot.',
    items: [
      { t: '09:30', n: 'MAAT', s: 'Architecture + art · 2 h' },
      { t: '12:30', n: 'Pastéis de Belém', s: 'The original · cash only' },
      { t: '15:00', n: 'Mosteiro dos Jerónimos', s: 'Cloister · ticket in wallet' },
      { t: '19:30', n: 'Cervejaria Ramiro', s: 'Seafood · your favourite' },
    ],
  },
  {
    date: 'Day 3 · Fri',
    head: 'A walk through Alfama.',
    items: [
      { t: '10:00', n: 'Miradouro de Santa Luzia', s: 'View · 20 min walk' },
      { t: '13:00', n: 'Prado', s: 'Lunch · Lena saved this' },
      { t: '17:00', n: 'Fundação Saramago', s: 'Quiet hour · close at 6' },
      { t: '21:30', n: 'Mesa de Frades', s: 'Fado · reservation needed' },
    ],
  },
  {
    date: 'Day 4 · Sat',
    head: 'Coast, then home.',
    items: [
      { t: '09:00', n: 'Cascais train', s: 'From Cais do Sodré · 40 min' },
      { t: '11:00', n: 'Boca do Inferno', s: 'Walk · 30 min along the cliff' },
      { t: '14:00', n: 'Mar do Inferno', s: 'Lunch by the water' },
      { t: '18:30', n: 'Train back · LIS 21:40', s: 'Carry-on already at hotel' },
    ],
  },
];

function TripDemo() {
  const [stage, setStage] = useState('idle');  // idle | thinking | building | done
  const [shownDays, setShownDays] = useState(0);
  const [shownItems, setShownItems] = useState(0);

  const start = () => {
    setStage('thinking');
    setShownDays(0);
    setShownItems(0);
    setTimeout(() => setStage('building'), 1000);
  };

  const reset = () => {
    setStage('idle');
    setShownDays(0);
    setShownItems(0);
  };

  // Sequentially reveal items
  useEffect(() => {
    if (stage !== 'building') return;
    const totalItems = TRIP_DAYS.reduce((n, d) => n + d.items.length, 0);
    let idx = 0;
    const revealNext = () => {
      idx += 1;
      // walk through days/items to find current position
      let cum = 0;
      let dayI = 0;
      let itemI = 0;
      for (let i = 0; i < TRIP_DAYS.length; i++) {
        if (idx <= cum + TRIP_DAYS[i].items.length) {
          dayI = i;
          itemI = idx - cum;
          break;
        }
        cum += TRIP_DAYS[i].items.length;
      }
      setShownDays(dayI + 1);
      setShownItems(idx);
      if (idx >= totalItems) {
        setStage('done');
        return;
      }
    };
    const id = setInterval(revealNext, 240);
    return () => clearInterval(id);
  }, [stage]);

  const flatIndex = (di, ii) => {
    let n = 0;
    for (let i = 0; i < di; i++) n += TRIP_DAYS[i].items.length;
    return n + ii + 1;
  };

  return (
    <DemoFrame
      eyebrow="03 · Compose"
      title="From a <em>list</em> to a real trip."
      lede="Plinn reads your saved places. You give it one sentence. It hands back a draft you can edit. No magic — just your places, in an order."
    >
      <div className="trip-wrap">
        {/* Left: the prompt + control */}
        <div className="trip-left">
          <div className="mono" style={{ marginBottom: 14 }}>The sentence</div>
          <div className="trip-prompt">"{TRIP_PROMPT}"</div>

          <div className="mono" style={{ marginTop: 36, marginBottom: 14 }}>What Plinn reads</div>
          <ul className="trip-readlist">
            <li><span className="dot loved"/> <span><strong>12 places</strong> you saved in Lisbon</span></li>
            <li><span className="dot ink"/> <span><strong>3 places</strong> Mariana shared</span></li>
            <li><span className="dot small"/> <span>Your taste · art-leaning, walking, late dinners</span></li>
          </ul>

          <div className="trip-controls">
            {stage === 'idle' && (
              <button className="btn-ink" onClick={start}>
                Compose the trip <span className="arr">→</span>
              </button>
            )}
            {(stage === 'thinking' || stage === 'building') && (
              <div className="trip-status">
                <div className="trip-spinner"/>
                <span className="mono ink">
                  {stage === 'thinking' ? 'Reading your places…' : `Composing · ${shownItems} of ${TRIP_DAYS.reduce((n,d)=>n+d.items.length,0)}`}
                </span>
              </div>
            )}
            {stage === 'done' && (
              <button className="btn-line" onClick={reset}>
                Compose again
              </button>
            )}
          </div>
        </div>

        {/* Right: the unfolding itinerary */}
        <div className="trip-right">
          {stage === 'idle' && (
            <div className="trip-empty">
              <div className="serif" style={{ fontSize: 36, lineHeight: 1.1, letterSpacing: '-0.6px' }}>
                <em>Four days</em>, twelve places,<br/>mostly on foot.
              </div>
              <div className="mono" style={{ marginTop: 24 }}>Press compose to see it land</div>
            </div>
          )}

          {stage !== 'idle' && (
            <div className="trip-itinerary">
              {TRIP_DAYS.map((day, di) => {
                if (di + 1 > shownDays) return null;
                return (
                  <div key={di} className="trip-day">
                    <div className="trip-day-head">
                      <span className="mono">{day.date}</span>
                      <span className="serif" style={{ fontSize: 22 }}><em>{day.head}</em></span>
                    </div>
                    <div className="trip-day-items">
                      {day.items.map((it, ii) => {
                        if (flatIndex(di, ii) > shownItems) return null;
                        return (
                          <div key={ii} className="trip-item">
                            <span className="t mono">{it.t}</span>
                            <div>
                              <div className="n">{it.n}</div>
                              <div className="s">{it.s}</div>
                            </div>
                          </div>
                        );
                      })}
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </div>
      </div>

      <div className="demo-foot mono">
        Edits stay yours · Plinn never overwrites the places themselves
      </div>
    </DemoFrame>
  );
}

Object.assign(window, { AskDemo, MapDemo, TripDemo });
