// detail-v2.jsx — 详情页:整本食谱书风格
// 三页:① 食材页(配料表) ② 工序页(逐步) ③ 技法页(食神手记)
// 左右滑动 = 翻页

const { useState: uD2, useEffect: eD2, useRef: rD2 } = React;

// ── Step Timer (compact circular badge — fits in a corner) ──
// Single tap = start/pause; double tap = reset.
// Display: <600s → "45s", >=600s → "12m"
function StepTimer({ seconds, accent, onComplete, size = 44 }) {
  const [remaining, setRemaining] = uD2(seconds);
  const [running, setRunning] = uD2(false);
  const tickRef = rD2(null);
  const tapTimerRef = rD2(null);
  const completedRef = rD2(false);

  eD2(() => {
    setRemaining(seconds); setRunning(false); completedRef.current = false;
  }, [seconds]);

  eD2(() => {
    if (!running) return;
    tickRef.current = setInterval(() => {
      setRemaining(r => {
        if (r <= 1) {
          setRunning(false);
          if (!completedRef.current) { completedRef.current = true; onComplete?.(); }
          return 0;
        }
        return r - 1;
      });
    }, 1000);
    return () => clearInterval(tickRef.current);
  }, [running]);

  // Single tap = toggle running; double tap = reset
  const handleTap = () => {
    if (tapTimerRef.current) {
      // double tap
      clearTimeout(tapTimerRef.current);
      tapTimerRef.current = null;
      setRunning(false);
      setRemaining(seconds);
      completedRef.current = false;
      return;
    }
    tapTimerRef.current = setTimeout(() => {
      tapTimerRef.current = null;
      if (remaining === 0) {
        // Treat tap on finished as reset+start? No — just reset to seconds.
        setRemaining(seconds);
        completedRef.current = false;
        setRunning(true);
      } else {
        setRunning(r => !r);
      }
    }, 240);
  };

  const pct = seconds > 0 ? remaining / seconds : 0; // 1 → 0
  const finished = remaining === 0;

  // Compact display: <600s → "45s", >=600s → "Xm" (ceil up; only show min when >=10min)
  const display = finished
    ? '0'
    : (seconds < 600
        ? String(remaining)         // pure number, < 600s
        : String(Math.ceil(remaining / 60))); // minutes
  const unit = seconds < 600 ? 's' : 'm';

  // Adapt font size to digit count
  const digitCount = display.length;
  const fontSize = digitCount >= 3 ? size * 0.30 : digitCount === 2 ? size * 0.36 : size * 0.40;

  // SVG ring geometry
  const r = (size / 2) - 3;
  const C = 2 * Math.PI * r;
  const dashOffset = C * (1 - pct);

  return (
    <button
      onClick={handleTap}
      title={running ? '点击暂停 · 双击重置' : (finished ? '点击重新计时' : '点击开始 · 双击重置')}
      style={{
        position: 'relative', width: size, height: size,
        padding: 0, background: 'transparent', border: 'none', cursor: 'pointer',
        outline: 'none', WebkitTapHighlightColor: 'transparent',
        display: 'inline-block', flexShrink: 0,
      }}>
      {/* Clock body */}
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}
        style={{ position: 'absolute', top: 0, left: 0,
          filter: running ? `drop-shadow(0 0 6px ${accent}66)` : 'none',
          transition: 'filter .3s',
        }}>
        {/* face */}
        <circle cx={size/2} cy={size/2} r={r}
          fill={finished ? `${accent}22` : BOOK.paper}
          stroke={BOOK.ink} strokeWidth="1" strokeOpacity="0.25" />
        {/* progress ring */}
        <circle cx={size/2} cy={size/2} r={r}
          fill="none" stroke={accent} strokeWidth="2.5"
          strokeLinecap="round"
          strokeDasharray={C}
          strokeDashoffset={dashOffset}
          transform={`rotate(-90 ${size/2} ${size/2})`}
          style={{ transition: running ? 'stroke-dashoffset 1s linear' : 'stroke-dashoffset .3s' }} />
      </svg>

      {/* Time text — center */}
      <div style={{
        position: 'absolute', inset: 0,
        display: 'flex', alignItems: 'baseline', justifyContent: 'center',
        pointerEvents: 'none', gap: 1,
      }}>
        <span style={{
          fontFamily: '"DM Mono", monospace',
          fontSize: fontSize, fontWeight: 600,
          color: finished ? accent : BOOK.ink,
          letterSpacing: '-0.02em', lineHeight: 1,
          alignSelf: 'center',
        }}>{display}</span>
        <span style={{
          fontFamily: '"DM Mono", monospace',
          fontSize: size * 0.20, fontWeight: 500,
          color: (finished ? accent : BOOK.ink) + '99',
          lineHeight: 1, alignSelf: 'center',
          marginBottom: -size * 0.04,
        }}>{unit}</span>
      </div>

      {/* Running pulse dot */}
      {running && (
        <div style={{
          position: 'absolute', top: 2, right: 2,
          width: 5, height: 5, borderRadius: 3,
          background: accent,
          animation: 'timerPulse 1s ease-in-out infinite',
        }} />
      )}

      <style>{`@keyframes timerPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.2; } }`}</style>
    </button>
  );
}

// ── Lightweight Markdown renderer for chat responses ──
// Handles: **bold**, *italic*, `code`, ## heading, - bullet, 1. numbered, > quote, blank lines.
// Intentionally minimal — no link, image, table support to keep things safe + fast.
function MdInline({ text }) {
  const tokens = [];
  let i = 0;
  let key = 0;
  while (i < text.length) {
    if (text.startsWith('**', i)) {
      const end = text.indexOf('**', i + 2);
      if (end !== -1) {
        tokens.push(<strong key={key++}>{text.slice(i + 2, end)}</strong>);
        i = end + 2; continue;
      }
    }
    if (text[i] === '*' && text[i + 1] !== '*') {
      const end = text.indexOf('*', i + 1);
      if (end !== -1 && text[end - 1] !== '*' && text[end + 1] !== '*') {
        tokens.push(<em key={key++}>{text.slice(i + 1, end)}</em>);
        i = end + 1; continue;
      }
    }
    if (text[i] === '`') {
      const end = text.indexOf('`', i + 1);
      if (end !== -1) {
        tokens.push(
          <code key={key++} style={{
            fontFamily: '"DM Mono", monospace', fontSize: '0.9em',
            background: 'rgba(0,0,0,0.06)', padding: '1px 5px', borderRadius: 3,
          }}>{text.slice(i + 1, end)}</code>
        );
        i = end + 1; continue;
      }
    }
    // Take a chunk of plain text up to next special char
    const rest = text.slice(i);
    const next = rest.search(/[*`]/);
    const chunk = next === -1 ? rest : rest.slice(0, Math.max(1, next));
    tokens.push(<span key={key++}>{chunk}</span>);
    i += chunk.length;
  }
  return <>{tokens}</>;
}

function MdBlock({ text }) {
  const lines = (text || '').split('\n');
  return (
    <>
      {lines.map((line, idx) => {
        let m = line.match(/^(#{1,4})\s+(.+)$/);
        if (m) {
          const level = m[1].length;
          const fs = [15.5, 14, 13, 13][level - 1] || 13;
          return (
            <div key={idx} style={{
              fontSize: fs, fontWeight: 600,
              marginTop: idx > 0 ? 6 : 0, marginBottom: 3,
              letterSpacing: '0.02em',
            }}><MdInline text={m[2]} /></div>
          );
        }
        m = line.match(/^\s*[-*]\s+(.+)$/);
        if (m) {
          return (
            <div key={idx} style={{ display: 'flex', gap: 6, paddingLeft: 4, margin: '1px 0' }}>
              <span style={{ flexShrink: 0, opacity: 0.55 }}>·</span>
              <span style={{ flex: 1 }}><MdInline text={m[1]} /></span>
            </div>
          );
        }
        m = line.match(/^\s*(\d+)\.\s+(.+)$/);
        if (m) {
          return (
            <div key={idx} style={{ display: 'flex', gap: 6, paddingLeft: 4, margin: '1px 0' }}>
              <span style={{ flexShrink: 0, opacity: 0.7, minWidth: '1.4em' }}>{m[1]}.</span>
              <span style={{ flex: 1 }}><MdInline text={m[2]} /></span>
            </div>
          );
        }
        m = line.match(/^>\s+(.+)$/);
        if (m) {
          return (
            <div key={idx} style={{
              paddingLeft: 8, borderLeft: '2px solid currentColor',
              opacity: 0.75, fontStyle: 'italic', margin: '2px 0',
            }}><MdInline text={m[1]} /></div>
          );
        }
        if (line.trim() === '') {
          return <div key={idx} style={{ height: 5 }} />;
        }
        return <div key={idx}><MdInline text={line} /></div>;
      })}
    </>
  );
}

// ── Sub-page navigation (used inside tabs that paginate when content overflows) ──
function SubPageNav({ sub, totalSubs, onChange, accent }) {
  return (
    <div style={{
      marginTop: 10, paddingTop: 10,
      borderTop: `1px dashed ${BOOK.rule}`,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      flexShrink: 0,
    }}>
      <button
        onClick={() => sub > 0 && onChange(sub - 1)}
        disabled={sub === 0}
        style={{
          background: 'transparent', border: 'none',
          fontFamily: '"DM Mono", monospace', fontSize: 9,
          letterSpacing: '0.25em', textTransform: 'uppercase',
          color: sub === 0 ? BOOK.ink + '44' : BOOK.ink + 'AA',
          cursor: sub === 0 ? 'default' : 'pointer',
          padding: '4px 0',
        }}>← 上页</button>

      <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
        {Array.from({ length: totalSubs }, (_, i) => (
          <button key={i}
            onClick={() => onChange(i)}
            aria-label={`第 ${i + 1} 页`}
            style={{
              width: i === sub ? 18 : 6, height: 6, borderRadius: 3,
              background: i === sub ? accent : BOOK.ink + '33',
              border: 'none', cursor: 'pointer', padding: 0,
              transition: 'all .25s',
            }} />
        ))}
        <span style={{
          marginLeft: 8,
          fontFamily: '"DM Mono", monospace', fontSize: 9,
          letterSpacing: '0.2em', color: BOOK.ink + '88',
        }}>{sub + 1} / {totalSubs}</span>
      </div>

      <button
        onClick={() => sub < totalSubs - 1 && onChange(sub + 1)}
        disabled={sub === totalSubs - 1}
        style={{
          background: 'transparent', border: 'none',
          fontFamily: '"DM Mono", monospace', fontSize: 9,
          letterSpacing: '0.25em', textTransform: 'uppercase',
          color: sub === totalSubs - 1 ? BOOK.ink + '44' : accent,
          cursor: sub === totalSubs - 1 ? 'default' : 'pointer',
          padding: '4px 0',
        }}>下页 →</button>
    </div>
  );
}

// ── Steps Timeline (vertical timeline + swipeable detail panel) ──
// • Vertical timeline rail on the left: circle nodes connected by line
// • Tap node = focus that step; double-tap node = mark complete
// • Right panel shows current step detail; swipe left/right to change step
// • At the last step + swipe right → onAdvanceTab() to advance to next book tab
// • At the first step + swipe left → onRetreatTab() to go back to previous tab
function StepsTimeline({ steps, stepIdx, setStepIdx, checked, toggleStep, accent, onAdvanceTab, onRetreatTab }) {
  const dragRef = rD2({ x: 0, dx: 0, dragging: false });
  const [dx, setDx] = uD2(0);
  const tapTimers = rD2({});

  const handleNodeTap = (i) => {
    if (tapTimers.current[i]) {
      clearTimeout(tapTimers.current[i]);
      tapTimers.current[i] = null;
      toggleStep(i);
      return;
    }
    tapTimers.current[i] = setTimeout(() => {
      tapTimers.current[i] = null;
      setStepIdx(i);
    }, 240);
  };

  // Detail panel double-tap = mark complete
  const detailTapTimer = rD2(null);
  const handleDetailTap = () => {
    if (detailTapTimer.current) {
      clearTimeout(detailTapTimer.current);
      detailTapTimer.current = null;
      toggleStep(stepIdx);
      return;
    }
    detailTapTimer.current = setTimeout(() => {
      detailTapTimer.current = null;
    }, 260);
  };

  // Swipe handlers on the detail panel — drag to change step
  // stopPropagation so the outer book-page swipe doesn't also fire
  const onDown = (e) => {
    e.stopPropagation();
    const x = e.touches ? e.touches[0].clientX : e.clientX;
    dragRef.current = { x, dx: 0, dragging: true };
  };
  const onMove = (e) => {
    if (!dragRef.current.dragging) return;
    e.stopPropagation();
    const x = e.touches ? e.touches[0].clientX : e.clientX;
    const d = x - dragRef.current.x;
    dragRef.current.dx = d;
    setDx(d);
  };
  const onUp = (e) => {
    if (!dragRef.current.dragging) return;
    e && e.stopPropagation && e.stopPropagation();
    const d = dragRef.current.dx;
    dragRef.current.dragging = false;
    const threshold = 60;
    if (d < -threshold) {
      if (stepIdx < steps.length - 1) setStepIdx(stepIdx + 1);
      else if (onAdvanceTab) onAdvanceTab();   // swipe right past last step → next tab
    } else if (d > threshold) {
      if (stepIdx > 0) setStepIdx(stepIdx - 1);
      else if (onRetreatTab) onRetreatTab();   // swipe left past first step → prev tab
    }
    setDx(0);
  };

  const st = steps[stepIdx];
  const isChecked = checked.has(stepIdx);

  return (
    <div style={{ flex: 1, display: 'flex', gap: 14, overflow: 'hidden', minHeight: 0 }}>
      {/* Vertical Timeline rail.
          We split the rail height into N segments and draw the line as N-1 between-node
          stripes (rendered behind the buttons), so the line never passes through any circle.
          Each between-stripe is colored accent if its left node is past/active, else rule. */}
      <div style={{
        width: 36, flexShrink: 0, position: 'relative',
        paddingTop: 6, paddingBottom: 6,
        display: 'flex', flexDirection: 'column',
      }}>
        {steps.map((s, i) => {
          const done = checked.has(i);
          const active = i === stepIdx;
          const past = i < stepIdx;
          // Spacer (line segment) BEFORE each non-first node, sized by flex
          // The segment spans from previous node center to this node center
          const segmentColor = i <= stepIdx ? accent : BOOK.rule;
          const NODE_HEIGHT = 36;
          return (
            <React.Fragment key={i}>
              {i > 0 && (
                <div style={{
                  flex: 1, display: 'flex', justifyContent: 'center',
                }}>
                  <div style={{
                    width: 2, height: '100%',
                    background: segmentColor,
                    transition: 'background .3s',
                  }} />
                </div>
              )}
              <button
                onClick={() => handleNodeTap(i)}
                title="点击聚焦 · 双击标记完成"
                style={{
                  width: 36, height: NODE_HEIGHT, padding: 0, border: 'none',
                  background: 'transparent', cursor: 'pointer',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  WebkitTapHighlightColor: 'transparent',
                  flexShrink: 0,
                  position: 'relative', zIndex: 1,
                }}>
                <div style={{
                  width: active ? 26 : 18, height: active ? 26 : 18,
                  borderRadius: '50%',
                  background: done ? accent : (active ? '#fff' : BOOK.paper),
                  border: `2px solid ${done || active || past ? accent : BOOK.rule}`,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  color: done ? '#fff' : accent,
                  fontFamily: '"DM Mono", monospace',
                  fontSize: active ? 11 : 9, fontWeight: 700,
                  transition: 'all .2s',
                  boxShadow: active ? `0 0 0 4px ${accent}22` : 'none',
                }}>
                  {done ? <UIIcons.check size={11} /> : (i + 1)}
                </div>
              </button>
            </React.Fragment>
          );
        })}
      </div>

      {/* Detail panel — swipeable, with page-flip animation on step change */}
      <div style={{ flex: 1, minWidth: 0, position: 'relative', overflow: 'hidden',
                    perspective: '900px' }}
        onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onUp}
        onTouchStart={onDown} onTouchMove={onMove} onTouchEnd={onUp}>
        <div
          key={stepIdx}
          onClick={handleDetailTap}
          className="step-flip-in"
          style={{
            transform: `translateX(${dx}px)`,
            transition: dragRef.current.dragging ? 'none' : 'transform .35s cubic-bezier(.2,.8,.2,1)',
            opacity: 1 - Math.min(0.4, Math.abs(dx) / 400),
            height: '100%', display: 'flex', flexDirection: 'column',
            cursor: 'pointer', WebkitTapHighlightColor: 'transparent',
          }}>
          {/* Step header */}
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 8 }}>
            <span style={{
              fontFamily: '"DM Mono", monospace', fontSize: 9,
              letterSpacing: '0.3em', color: accent, textTransform: 'uppercase',
            }}>STEP {String(stepIdx + 1).padStart(2, '0')} / {String(steps.length).padStart(2, '0')}</span>
            {isChecked && (
              <span style={{
                fontFamily: '"DM Mono", monospace', fontSize: 8.5,
                letterSpacing: '0.2em', color: accent,
                display: 'inline-flex', alignItems: 'center', gap: 3,
              }}>
                <UIIcons.check size={10} /> DONE
              </span>
            )}
          </div>
          {/* Title row + tiny timer in the corner */}
          <div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 10 }}>
            <h3 style={{
              margin: 0, fontFamily: '"Noto Serif SC", serif',
              fontSize: 19, fontWeight: 500, color: BOOK.ink,
              letterSpacing: '0.03em', lineHeight: 1.25, flex: 1,
              textDecoration: isChecked ? 'line-through' : 'none',
              textDecorationColor: accent + '88',
              textDecorationThickness: '1.5px',
            }}>{st.title}</h3>
            {st.timer && (
              <div onClick={(e) => e.stopPropagation()} style={{ flexShrink: 0, marginTop: -2 }}>
                <StepTimer
                  key={`${stepIdx}-${st.timer}`}
                  seconds={st.timer}
                  accent={accent}
                  size={42}
                  onComplete={() => !isChecked && toggleStep(stepIdx)}
                />
              </div>
            )}
          </div>

          {/* Description */}
          <p style={{
            margin: 0, fontFamily: '"Noto Serif SC", serif',
            fontSize: 13, lineHeight: 1.85, color: BOOK.ink + 'DD', fontWeight: 400,
            textIndent: '2em', letterSpacing: '0.02em',
            marginBottom: st.tip ? 14 : 0,
            flex: 1, overflow: 'auto',
          }}>{st.body}</p>

          {/* Tip — marginalia */}
          {st.tip && (
            <div style={{
              padding: '10px 12px',
              background: 'rgba(255,255,255,0.5)',
              borderLeft: `2px solid ${accent}`,
              marginTop: 'auto',
            }}>
              <div style={{
                fontFamily: '"DM Mono", monospace', fontSize: 8,
                letterSpacing: '0.3em', color: accent,
                textTransform: 'uppercase', marginBottom: 4,
                display: 'flex', alignItems: 'center', gap: 4,
              }}>
                <UIIcons.chef size={11} /> 食神提示
              </div>
              <div style={{
                fontSize: 11.5, color: BOOK.ink + 'CC', lineHeight: 1.6,
                fontFamily: '"Noto Serif SC", serif', fontStyle: 'italic',
              }}>{st.tip}</div>
            </div>
          )}

          {/* Hint */}
          <div style={{
            marginTop: 10, paddingTop: 8,
            borderTop: `1px dashed ${BOOK.rule}`,
            display: 'flex', justifyContent: 'space-between', alignItems: 'center',
            fontFamily: '"DM Mono", monospace', fontSize: 8,
            color: BOOK.ink + '77', letterSpacing: '0.25em', textTransform: 'uppercase',
          }}>
            <span>← 滑动切换 →</span>
            <span>双击 = 标记完成</span>
          </div>
        </div>
      </div>
    </div>
  );
}

// ── Detail (book-style) ──
function DetailV2({ recipe, recipeIndex, recipes, onSwitchRecipe, onBack, t, density }) {
  const [servings, setServings] = uD2(recipe.baseServings);
  const [checked, setChecked] = uD2(new Set());
  // pageIdx: 0=cover, 1=ingredients, 2=steps, 3=variations, 4=my notes
  const [pageIdx, setPageIdx] = uD2(0);
  const TOTAL = 6;
  const [flipping, setFlipping] = uD2(null);
  const [drag, setDrag] = uD2(0);
  const dragStartX = rD2(0);
  const dragging = rD2(false);
  const [stepIdx, setStepIdx] = uD2(0);
  // Sub-page indices for tabs that paginate when content overflows
  const [principlesSub, setPrinciplesSub] = uD2(0);
  const [variationsSub, setVariationsSub] = uD2(0);
  const PRINCIPLES_PER_PAGE = 2;
  const VARIATIONS_PER_PAGE = 2;
  // User notes (per-recipe, persisted to localStorage)
  const notesKey = `recipe-app:notes:${recipe.id}`;
  const [myNotes, setMyNotes] = uD2(() => {
    try { return localStorage.getItem(notesKey) || ''; } catch { return ''; }
  });
  eD2(() => {
    try { localStorage.setItem(notesKey, myNotes); } catch {}
  }, [myNotes, notesKey]);

  // Chat with chef — per-recipe history, persisted to localStorage
  const chatKey = `recipe-app:chat:${recipe.id}`;
  const [chatOpen, setChatOpen] = uD2(false);
  const [chatInput, setChatInput] = uD2('');
  const [chatSending, setChatSending] = uD2(false);
  const [chatError, setChatError] = uD2('');
  const [chatMessages, setChatMessages] = uD2(() => {
    try { return JSON.parse(localStorage.getItem(chatKey) || '[]'); } catch { return []; }
  });
  eD2(() => {
    try { localStorage.setItem(chatKey, JSON.stringify(chatMessages)); } catch {}
  }, [chatMessages, chatKey]);

  eD2(() => {
    setServings(recipe.baseServings);
    setChecked(new Set());
    setPageIdx(0);
    setStepIdx(0);
    setPrinciplesSub(0);
    setVariationsSub(0);
    try { setMyNotes(localStorage.getItem(`recipe-app:notes:${recipe.id}`) || ''); } catch {}
    try { setChatMessages(JSON.parse(localStorage.getItem(`recipe-app:chat:${recipe.id}`) || '[]')); } catch { setChatMessages([]); }
    setChatOpen(false); setChatError(''); setChatInput('');
  }, [recipe.id]);

  // Reset sub-page when leaving the relevant tab
  eD2(() => { if (pageIdx !== 3) setPrinciplesSub(0); }, [pageIdx]);
  eD2(() => { if (pageIdx !== 4) setVariationsSub(0); }, [pageIdx]);

  // Send a message to /api/chat with full recipe context
  const sendChat = async () => {
    const text = chatInput.trim();
    if (!text || chatSending) return;
    setChatError('');
    const newHistory = [...chatMessages, { role: 'user', content: text }];
    setChatMessages(newHistory);
    setChatInput('');
    setChatSending(true);
    try {
      const PAGE_KEYS = ['cover', 'ingredients', 'method', 'principles', 'variations', 'notes'];
      // Cache-buster + explicit no-cache to defeat iOS Safari's aggressive POST caching
      const res = await fetch('/api/chat?t=' + Date.now(), {
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          'cache-control': 'no-cache',
          'pragma': 'no-cache',
        },
        cache: 'no-store',
        body: JSON.stringify({
          messages: newHistory,
          recipe: {
            name: recipe.name, nameEn: recipe.nameEn, intro: recipe.intro,
            time: recipe.time, difficulty: recipe.difficulty, tags: recipe.tags,
            ingredients: recipe.ingredients, steps: recipe.steps,
            variations: recipe.variations || [],
            principles: recipe.principles || [],
            chefNote: recipe.chefNote,
          },
          currentPage: PAGE_KEYS[pageIdx],
          currentStepIdx: pageIdx === 2 ? stepIdx : undefined,
        }),
      });
      if (!res.ok) {
        const errText = await res.text();
        throw new Error(`HTTP ${res.status}: ${errText.slice(0, 200)}`);
      }
      const data = await res.json();
      if (data.error) throw new Error(data.error);
      setChatMessages(h => [...h, { role: 'assistant', content: data.text || '(empty response)' }]);
    } catch (e) {
      setChatError(e.message || String(e));
    } finally {
      setChatSending(false);
    }
  };

  const clearChat = () => {
    setChatMessages([]);
    setChatError('');
  };

  const accent = recipe.accent;
  const pad = density === 'compact' ? 22 : 26;

  const goTo = (newIdx) => {
    if (newIdx < 0 || newIdx >= TOTAL || newIdx === pageIdx || flipping) return;
    const dir = newIdx > pageIdx ? 'next' : 'prev';
    setFlipping({ from: pageIdx, to: newIdx, dir });
    setTimeout(() => {
      setPageIdx(newIdx);
      setFlipping(null);
      setDrag(0);
    }, 540);
  };

  const onPointerDown = (e) => {
    if (flipping) return;
    dragging.current = true;
    dragStartX.current = e.clientX || e.touches?.[0]?.clientX || 0;
  };
  const onPointerMove = (e) => {
    if (!dragging.current || flipping) return;
    const x = e.clientX || e.touches?.[0]?.clientX || 0;
    const dx = x - dragStartX.current;
    setDrag(Math.max(-1, Math.min(1, dx / 320)));
  };
  const onPointerUp = () => {
    if (!dragging.current) return;
    dragging.current = false;
    if (drag < -0.22 && pageIdx < TOTAL - 1) goTo(pageIdx + 1);
    else if (drag > 0.22 && pageIdx > 0) goTo(pageIdx - 1);
    else setDrag(0);
  };

  const pageProps = (i) => {
    let z = 0, transform = '', display = 'none', opacity = 1;
    if (flipping) {
      if (i === flipping.from) {
        display = 'block';
        if (flipping.dir === 'next') { z = 30; transform = 'rotateY(-180deg)'; }
        else { z = 5; transform = 'rotateY(0deg)'; }
      } else if (i === flipping.to) {
        display = 'block';
        if (flipping.dir === 'next') { z = 5; transform = 'rotateY(0deg)'; }
        else { z = 30; transform = 'rotateY(0deg)'; }
      }
    } else {
      if (i === pageIdx) {
        display = 'block';
        z = 20;
        const rot = drag < 0 ? drag * 180 : 0;
        transform = `rotateY(${rot}deg)`;
      } else if (i === pageIdx + 1) {
        display = 'block'; z = 5;
      } else if (i === pageIdx - 1) {
        display = 'block';
        z = drag > 0 ? 30 : 4;
        const rot = drag > 0 ? -180 + drag * 180 : -180;
        transform = `rotateY(${rot}deg)`;
      }
    }
    return { z, transform, display, opacity };
  };

  const toggleStep = (i) => setChecked(s => {
    const n = new Set(s);
    if (n.has(i)) n.delete(i); else n.add(i);
    return n;
  });
  const completedCount = checked.size;
  const PAGE_LABELS = ['封面', '食材', '工序', '原理', '变体', '备注'];
  const PAGE_LABELS_EN = ['COVER', 'INGREDIENTS', 'METHOD', 'WHY', 'VARIATIONS', 'NOTES'];

  // ─── Page: Recipe Cover (within detail) ───
  function renderCover() {
    return (
      <div style={{ position: 'relative', height: '100%',
        padding: '32px 56px 50px 44px',
        display: 'flex', flexDirection: 'column' }}>
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          marginBottom: 14, paddingBottom: 8,
          borderBottom: `1px solid ${BOOK.rule}`,
        }}>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.3em', color: accent,
            textTransform: 'uppercase', fontWeight: 600,
          }}>● CH. {String(recipeIndex + 1).padStart(2, '0')} · {recipe.category.toUpperCase()}</span>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.2em', color: BOOK.ink + '88',
          }}>NO. {String(recipeIndex + 1).padStart(2, '0')}</span>
        </div>

        {/* Photo plate — real image if heroImage is set, else cookbook placeholder */}
        <div style={{
          position: 'relative', width: '100%', height: 200,
          background: BOOK.paperDeep, overflow: 'hidden',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          border: `1px solid ${BOOK.rule}`, marginBottom: 18,
          flexShrink: 0,
        }}>
          {recipe.heroImage ? (
            <>
              <img src={recipe.heroImage} alt={recipe.name}
                loading="lazy"
                style={{
                  width: '100%', height: '100%', objectFit: 'cover',
                  display: 'block',
                  filter: 'saturate(0.96) contrast(1.02)',
                }} />
              {/* FIG caption overlay in cookbook style */}
              <div style={{
                position: 'absolute', bottom: 8, right: 8,
                fontFamily: '"DM Mono", monospace', fontSize: 8.5,
                letterSpacing: '0.3em', color: '#F5EFE2', textTransform: 'uppercase',
                background: 'rgba(0,0,0,0.5)',
                padding: '3px 8px',
                backdropFilter: 'blur(2px)',
              }}>FIG. {String(recipeIndex + 1).padStart(2, '0')} · PHOTO</div>
            </>
          ) : (
            <>
              <div style={{
                position: 'absolute', inset: 0,
                background: `repeating-linear-gradient(135deg, transparent 0 8px, rgba(0,0,0,0.04) 8px 9px)`,
              }} />
              <div style={{
                position: 'relative', textAlign: 'center', maxWidth: '78%',
                padding: '12px 16px', background: BOOK.paper,
                border: `1px dashed ${BOOK.ink}55`,
              }}>
                <div style={{
                  fontFamily: '"DM Mono", monospace', fontSize: 8.5,
                  letterSpacing: '0.3em', color: BOOK.ink + '88', textTransform: 'uppercase',
                  marginBottom: 4,
                }}>FIG. {String(recipeIndex + 1).padStart(2, '0')} · PHOTO</div>
                <div style={{
                  fontFamily: '"Noto Serif SC", serif', fontSize: 12,
                  color: BOOK.ink + 'BB', lineHeight: 1.5, fontWeight: 300,
                }}>{recipe.heroImageDesc || `${recipe.name} · 成品图`}</div>
              </div>
            </>
          )}
        </div>

        {/* Title */}
        <div style={{
          fontFamily: '"Cormorant Garamond", serif', fontStyle: 'italic',
          fontSize: 14, color: BOOK.ink + 'AA', marginBottom: 4,
        }}>{recipe.nameEn}</div>
        <h1 style={{
          margin: 0, fontFamily: '"Noto Serif SC", serif',
          fontSize: 32, fontWeight: 300, color: BOOK.ink,
          letterSpacing: '0.06em', lineHeight: 1.1,
        }}>{recipe.name}</h1>

        {/* Decorative §§ divider */}
        <div style={{
          display: 'flex', alignItems: 'center', gap: 10,
          margin: '14px 0',
        }}>
          <div style={{ flex: 1, height: 1, background: BOOK.rule }} />
          <span style={{
            fontFamily: '"Cormorant Garamond", serif', fontStyle: 'italic',
            fontSize: 16, color: BOOK.ink + '88',
          }}>§</span>
          <div style={{ flex: 1, height: 1, background: BOOK.rule }} />
        </div>

        {/* Meta */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-around',
          fontFamily: '"DM Mono", monospace', fontSize: 9.5,
          letterSpacing: '0.18em', color: BOOK.ink + 'AA',
          marginBottom: 14,
        }}>
          <span><b style={{ color: BOOK.ink, fontWeight: 600 }}>{recipe.time}</b> MIN</span>
          <span style={{ color: BOOK.ruleSoft }}>·</span>
          <span><b style={{ color: BOOK.ink, fontWeight: 600 }}>{recipe.difficulty}</b></span>
          <span style={{ color: BOOK.ruleSoft }}>·</span>
          <span><b style={{ color: BOOK.ink, fontWeight: 600 }}>{recipe.steps.length}</b> 步</span>
        </div>

        {/* Intro */}
        <p style={{
          margin: 0, fontFamily: '"Noto Serif SC", serif', fontSize: 13,
          lineHeight: 1.85, color: BOOK.ink, fontWeight: 300,
          textIndent: '2em', letterSpacing: '0.02em',
          flex: 1, overflow: 'hidden',
        }}>{recipe.intro}</p>

        {/* Tags */}
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 12 }}>
          {recipe.tags.map(tg => (
            <span key={tg} style={{
              fontFamily: '"Noto Sans SC"', fontSize: 10.5,
              padding: '3px 8px', border: `1px solid ${BOOK.rule}`,
              color: BOOK.ink + 'CC',
              background: 'rgba(255,255,255,0.4)',
            }}>{tg}</span>
          ))}
        </div>
      </div>
    );
  }

  // ─── Page: Ingredients ───
  function renderIngredients() {
    return (
      <div style={{ position: 'relative', height: '100%',
        padding: '32px 56px 50px 44px',
        display: 'flex', flexDirection: 'column' }}>
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          marginBottom: 14, paddingBottom: 8,
          borderBottom: `1px solid ${BOOK.rule}`,
        }}>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.3em', color: accent,
            textTransform: 'uppercase', fontWeight: 600,
          }}>● 食材 · INGREDIENTS</span>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.2em', color: BOOK.ink + '88',
          }}>{String(recipe.ingredients.length).padStart(2, '0')} 项</span>
        </div>

        {/* Servings adjuster — natural-unit recipes don't auto-scale; show multiplier hint */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '12px 14px',
          background: 'rgba(255,255,255,0.4)',
          border: `1px dashed ${accent}66`,
          marginBottom: 14,
        }}>
          <div style={{ minWidth: 0 }}>
            <div style={{
              fontFamily: '"DM Mono", monospace', fontSize: 8.5,
              letterSpacing: '0.3em', color: accent,
              textTransform: 'uppercase', marginBottom: 2,
            }}>份量参考</div>
            <div style={{
              fontFamily: '"Noto Serif SC", serif', fontSize: 12,
              color: BOOK.ink + 'AA',
            }}>
              原方 {recipe.baseServings}
              {servings !== recipe.baseServings && (
                <span style={{
                  fontFamily: '"DM Mono", monospace', fontSize: 11,
                  color: accent, marginLeft: 8, fontWeight: 600,
                }}>× {(servings / recipe.baseServings).toFixed(1).replace(/\.0$/, '')}</span>
              )}
            </div>
          </div>
          <ServingsStepper value={servings} onChange={setServings} t={{
            ink: BOOK.ink, rule: BOOK.rule,
          }} />
        </div>

        {/* Ingredient lines (book bibliography style) */}
        <div style={{ flex: 1, overflow: 'hidden' }}>
          {recipe.ingredients.map((ing, i) => {
            const Ic = FoodIcons[ing.icon] || FoodIcons.salt;
            return (
              <div key={i} style={{
                display: 'flex', alignItems: 'baseline', gap: 8,
                padding: '8px 0',
                borderBottom: `1px dotted ${BOOK.ruleSoft}`,
              }}>
                <span style={{
                  fontFamily: '"DM Mono", monospace', fontSize: 9,
                  color: accent, letterSpacing: '0.15em',
                  width: 22,
                }}>{String(i + 1).padStart(2, '0')}</span>
                <div style={{
                  width: 22, height: 22, flexShrink: 0,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  color: accent, opacity: 0.85,
                }}>
                  <Ic size={16} />
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <span style={{
                    fontFamily: '"Noto Serif SC", serif', fontSize: 13.5,
                    color: BOOK.ink, fontWeight: 400, letterSpacing: '0.03em',
                  }}>{ing.name}</span>
                  {ing.note && (
                    <span style={{
                      fontFamily: '"Cormorant Garamond", serif', fontStyle: 'italic',
                      fontSize: 11, color: BOOK.ink + '88', marginLeft: 6,
                    }}>· {ing.note}</span>
                  )}
                </div>
                <span style={{
                  fontFamily: '"Noto Serif SC", serif', fontSize: 12.5,
                  color: BOOK.ink, fontWeight: 500, letterSpacing: '0.04em',
                  flexShrink: 0, textAlign: 'right',
                }}>{ing.qty}</span>
              </div>
            );
          })}
        </div>

        {/* Page footer */}
        <div style={{
          marginTop: 12, paddingTop: 8,
          borderTop: `1px dashed ${BOOK.rule}`,
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          fontFamily: '"DM Mono", monospace', fontSize: 9,
          color: BOOK.ink + '88', letterSpacing: '0.25em', textTransform: 'uppercase',
        }}>
          <span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
            <UIIcons.plus size={11} /> 加入购物清单
          </span>
          <span>翻至工序 →</span>
        </div>
      </div>
    );
  }

  // ─── Page: Steps (timeline + horizontal swipe + double-tap to complete) ───
  function renderSteps() {
    return (
      <div style={{ position: 'relative', height: '100%',
        padding: '32px 56px 50px 44px',
        display: 'flex', flexDirection: 'column' }}>
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          marginBottom: 14, paddingBottom: 8,
          borderBottom: `1px solid ${BOOK.rule}`,
        }}>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.3em', color: accent,
            textTransform: 'uppercase', fontWeight: 600,
            display: 'inline-flex', alignItems: 'center', gap: 6,
          }}>
            <UIIcons.flame size={11} /> 工序 · METHOD
          </span>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.2em', color: BOOK.ink + '88',
          }}>{completedCount}/{recipe.steps.length} 步</span>
        </div>

        {/* Timeline + swipeable detail */}
        <StepsTimeline
          steps={recipe.steps}
          stepIdx={stepIdx}
          setStepIdx={setStepIdx}
          checked={checked}
          toggleStep={toggleStep}
          accent={accent}
          onAdvanceTab={() => goTo(pageIdx + 1)}
          onRetreatTab={() => goTo(pageIdx - 1)}
        />
      </div>
    );
  }

  // ─── Page: Principles (原理) — the science behind each step ───
  function renderPrinciples() {
    const principles = recipe.principles || [];
    const totalSubs = Math.max(1, Math.ceil(principles.length / PRINCIPLES_PER_PAGE));
    const sub = Math.min(principlesSub, totalSubs - 1);
    const start = sub * PRINCIPLES_PER_PAGE;
    const slice = principles.slice(start, start + PRINCIPLES_PER_PAGE);
    return (
      <div style={{ position: 'relative', height: '100%',
        padding: '32px 56px 50px 44px',
        display: 'flex', flexDirection: 'column' }}>
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          marginBottom: 14, paddingBottom: 8,
          borderBottom: `1px solid ${BOOK.rule}`,
        }}>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.3em', color: accent,
            textTransform: 'uppercase', fontWeight: 600,
            display: 'inline-flex', alignItems: 'center', gap: 6,
          }}>
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none"
                 stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
              <path d="M9 18h6M10 21h4M9 14a4.5 4.5 0 0 1-1.5-3.5A4.5 4.5 0 0 1 12 6a4.5 4.5 0 0 1 4.5 4.5A4.5 4.5 0 0 1 15 14v3H9v-3z" />
            </svg>
            要诀 · 内功心法 · WHY
          </span>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.2em', color: BOOK.ink + '88',
          }}>{String(principles.length).padStart(2, '0')} 条</span>
        </div>

        <div style={{
          fontFamily: '"Cormorant Garamond", serif', fontStyle: 'italic',
          fontSize: 12, color: BOOK.ink + '99', marginBottom: 14,
          lineHeight: 1.6,
        }}>
          下乘者按方,上乘者按理 — 知其所以然,自家锅里也能稳定复刻。
        </div>

        <div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', minHeight: 0 }}>
          {principles.length === 0 && (
            <div style={{
              fontFamily: '"Noto Serif SC", serif', fontSize: 12,
              color: BOOK.ink + '88', fontStyle: 'italic',
              padding: '24px 0', textAlign: 'center',
            }}>暂未整理 — 按经验做就好</div>
          )}
          {slice.map((p, i) => {
            const realIdx = start + i;
            return (
              <div key={realIdx} style={{
                padding: '14px 0',
                borderBottom: i < slice.length - 1 ? `1px dotted ${BOOK.ruleSoft}` : 'none',
                display: 'flex', gap: 12,
              }}>
                <span style={{
                  fontFamily: '"Cormorant Garamond", serif', fontStyle: 'italic',
                  fontSize: 22, color: accent, lineHeight: 1, flexShrink: 0,
                  minWidth: 26,
                }}>{String(realIdx + 1).padStart(2, '0')}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{
                    fontFamily: '"Noto Serif SC", serif', fontSize: 14,
                    color: BOOK.ink, fontWeight: 500,
                    letterSpacing: '0.04em', marginBottom: 5,
                  }}>{p.name}</div>
                  <div style={{
                    fontFamily: '"Noto Serif SC", serif', fontSize: 12,
                    color: BOOK.ink + 'CC', lineHeight: 1.85, fontWeight: 300,
                    letterSpacing: '0.01em',
                  }}>{p.body}</div>
                </div>
              </div>
            );
          })}
        </div>

        {totalSubs > 1 && <SubPageNav
          sub={sub} totalSubs={totalSubs}
          onChange={setPrinciplesSub}
          accent={accent}
        />}
      </div>
    );
  }

  // ─── Page: Variations (was 技法) — different ways to riff on this recipe ───
  function renderVariations() {
    const variations = recipe.variations || [];
    const totalSubs = Math.max(1, Math.ceil(variations.length / VARIATIONS_PER_PAGE));
    const sub = Math.min(variationsSub, totalSubs - 1);
    const start = sub * VARIATIONS_PER_PAGE;
    const slice = variations.slice(start, start + VARIATIONS_PER_PAGE);
    return (
      <div style={{ position: 'relative', height: '100%',
        padding: '32px 56px 50px 44px',
        display: 'flex', flexDirection: 'column' }}>
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          marginBottom: 14, paddingBottom: 8,
          borderBottom: `1px solid ${BOOK.rule}`,
        }}>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.3em', color: accent,
            textTransform: 'uppercase', fontWeight: 600,
            display: 'inline-flex', alignItems: 'center', gap: 6,
          }}>
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none"
                 stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
              <path d="M5 5l4 4M19 19l-4-4M5 19l4-4M19 5l-4 4M12 12l0 0" />
              <circle cx="12" cy="12" r="2.5" />
            </svg>
            变招 · VARIATIONS
          </span>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.2em', color: BOOK.ink + '88',
          }}>{String(variations.length).padStart(2, '0')} 种</span>
        </div>

        {/* Chef hand-written quote (top, italic) */}
        <div style={{
          padding: '12px 14px',
          background: 'rgba(255,255,255,0.45)',
          border: `1px dashed ${accent}55`,
          marginBottom: 14,
          position: 'relative',
        }}>
          <span style={{
            position: 'absolute', top: -10, left: 12,
            background: BOOK.paper, padding: '0 6px',
            fontFamily: '"DM Mono", monospace', fontSize: 8.5,
            letterSpacing: '0.3em', color: accent,
            textTransform: 'uppercase',
          }}>食神手记 · CHEF</span>
          <div style={{
            fontFamily: '"Cormorant Garamond", serif', fontStyle: 'italic',
            fontSize: 13, lineHeight: 1.7, color: BOOK.ink + 'DD',
          }}>"{recipe.chefNote}"</div>
        </div>

        {/* Variations list */}
        <div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', minHeight: 0 }}>
          {variations.length === 0 && (
            <div style={{
              fontFamily: '"Noto Serif SC", serif', fontSize: 12,
              color: BOOK.ink + '88', fontStyle: 'italic',
              padding: '24px 0', textAlign: 'center',
            }}>暂未收录变体 · 自己去试试看</div>
          )}
          {slice.map((v, i) => {
            const realIdx = start + i;
            return (
              <div key={realIdx} style={{
                padding: '14px 0',
                borderBottom: i < slice.length - 1 ? `1px dotted ${BOOK.ruleSoft}` : 'none',
                display: 'flex', gap: 12,
              }}>
                <span style={{
                  fontFamily: '"Cormorant Garamond", serif', fontStyle: 'italic',
                  fontSize: 22, color: accent, lineHeight: 1, flexShrink: 0,
                  minWidth: 24,
                }}>{String(realIdx + 1).padStart(2, '0')}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{
                    display: 'flex', alignItems: 'baseline', gap: 8,
                    marginBottom: 4, flexWrap: 'wrap',
                  }}>
                    <span style={{
                      fontFamily: '"Noto Serif SC", serif', fontSize: 14,
                      color: BOOK.ink, fontWeight: 500,
                      letterSpacing: '0.04em',
                    }}>{v.name}</span>
                    {v.region && (
                      <span style={{
                        fontFamily: '"DM Mono", monospace', fontSize: 8.5,
                        letterSpacing: '0.2em', color: accent,
                        padding: '1px 6px', border: `1px solid ${accent}66`,
                        textTransform: 'uppercase',
                      }}>{v.region}</span>
                    )}
                  </div>
                  <div style={{
                    fontFamily: '"Noto Serif SC", serif', fontSize: 12,
                    color: BOOK.ink + 'CC', lineHeight: 1.75, fontWeight: 300,
                    marginBottom: v.tip ? 6 : 0,
                  }}>{v.body}</div>
                  {v.tip && (
                    <div style={{
                      paddingLeft: 10,
                      borderLeft: `2px solid ${accent}55`,
                      fontFamily: '"Noto Serif SC", serif', fontSize: 11,
                      color: BOOK.ink + 'AA', lineHeight: 1.65,
                      fontStyle: 'italic',
                    }}>{v.tip}</div>
                  )}
                </div>
              </div>
            );
          })}
        </div>

        {totalSubs > 1 && <SubPageNav
          sub={sub} totalSubs={totalSubs}
          onChange={setVariationsSub}
          accent={accent}
        />}

        {/* Completed banner — only on last sub-page */}
        {completedCount === recipe.steps.length && sub === totalSubs - 1 && (
          <div style={{
            marginTop: 12, padding: '10px',
            background: `${accent}15`,
            border: `1px solid ${accent}44`,
            textAlign: 'center', flexShrink: 0,
          }}>
            <UIIcons.heart size={14} filled />
            <span style={{
              fontFamily: '"Noto Serif SC", serif', fontSize: 12,
              color: accent, marginLeft: 6,
            }}>全部完成 · 可以开吃啦</span>
          </div>
        )}
      </div>
    );
  }

  // ─── Page: My Notes (备注) — user-editable notes per recipe ───
  function renderMyNotes() {
    return (
      <div style={{ position: 'relative', height: '100%',
        padding: '32px 56px 50px 44px',
        display: 'flex', flexDirection: 'column' }}>
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          marginBottom: 14, paddingBottom: 8,
          borderBottom: `1px solid ${BOOK.rule}`,
        }}>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.3em', color: accent,
            textTransform: 'uppercase', fontWeight: 600,
          }}>● 备注 · MY NOTES</span>
          <span style={{
            fontFamily: '"DM Mono", monospace', fontSize: 9,
            letterSpacing: '0.2em', color: BOOK.ink + '88',
          }}>{myNotes.length > 0 ? `${myNotes.length} 字` : '空白'}</span>
        </div>

        <div style={{
          fontFamily: '"Cormorant Garamond", serif', fontStyle: 'italic',
          fontSize: 12, color: BOOK.ink + '99', marginBottom: 10,
          lineHeight: 1.6,
        }}>
          做完之后写下你自己的心得 — 火候、配菜、口味偏好。下次做的时候会派上用场。
        </div>

        <textarea
          value={myNotes}
          onChange={(e) => setMyNotes(e.target.value)}
          placeholder="例如：&#10;• 第二次做时油加多了，下次少 1 勺&#10;• 加了一点白胡椒粉,味道更立体&#10;• 我家小孩喜欢放糖多一点&#10;..."
          spellCheck={false}
          style={{
            flex: 1, minHeight: 0, width: '100%',
            background: 'rgba(255,255,255,0.55)',
            border: `1px dashed ${BOOK.rule}`,
            outline: 'none',
            padding: '12px 14px',
            fontFamily: '"Noto Serif SC", serif',
            fontSize: 13, lineHeight: 1.75, color: BOOK.ink,
            resize: 'none',
            backgroundImage: `repeating-linear-gradient(0deg, transparent 0 22px, rgba(140,100,40,0.06) 22px 23px)`,
          }}
        />

        <div style={{
          marginTop: 8,
          fontFamily: '"DM Mono", monospace', fontSize: 8.5,
          letterSpacing: '0.25em', color: BOOK.ink + '77',
          textTransform: 'uppercase', textAlign: 'right',
        }}>
          {myNotes.length > 0 ? '已自动保存到本机' : '自动保存到本机'}
        </div>
      </div>
    );
  }

  function renderPage(i) {
    if (i === 0) return renderCover();
    if (i === 1) return renderIngredients();
    if (i === 2) return renderSteps();
    if (i === 3) return renderPrinciples();
    if (i === 4) return renderVariations();
    return renderMyNotes();
  }

  function renderBack() {
    return (
      <div style={{
        position: 'absolute', inset: 0,
        background: BOOK.paperDeep,
        backgroundImage: `repeating-linear-gradient(0deg, transparent 0 24px, rgba(140,100,40,0.05) 24px 25px)`,
        border: `1px solid ${BOOK.rule}`,
        borderRight: `2px solid ${BOOK.ink}25`,
        backfaceVisibility: 'hidden',
        transform: 'rotateY(180deg)',
        padding: '40px',
        display: 'flex', flexDirection: 'column',
        alignItems: 'center', justifyContent: 'center',
        boxShadow: 'inset -10px 0 14px -12px rgba(0,0,0,0.18)',
      }}>
        <div style={{
          fontFamily: '"DM Mono", monospace', fontSize: 9,
          letterSpacing: '0.4em', color: BOOK.ink + '88',
          textTransform: 'uppercase', marginBottom: 14,
        }}>PAGE TURN</div>
        <div style={{
          fontFamily: '"Cormorant Garamond", serif', fontStyle: 'italic',
          fontSize: 28, color: BOOK.ink + '88',
        }}>flip</div>
      </div>
    );
  }

  return (
    <div style={{
      background: '#E8E1D4',
      backgroundImage: `
        repeating-linear-gradient(45deg, transparent 0 4px, rgba(0,0,0,0.015) 4px 5px)
      `,
      color: BOOK.ink, minHeight: '100%',
      fontFamily: '"Inter", "Noto Sans SC", system-ui, sans-serif',
      paddingBottom: 32,
    }}>
      {/* Top bar */}
      <div style={{
        padding: `52px ${pad}px 14px`,
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        color: BOOK.ink,
      }}>
        <button onClick={onBack} style={{ ...iconBtn(t), color: BOOK.ink }}>
          <UIIcons.back size={18} />
        </button>
        <div style={{ textAlign: 'center' }}>
          <div style={{
            fontFamily: '"Noto Serif SC", serif', fontSize: 12,
            color: BOOK.ink, letterSpacing: '0.08em',
          }}>{recipe.name}</div>
          <div style={{
            fontFamily: '"DM Mono", monospace', fontSize: 8,
            color: BOOK.ink + '88', letterSpacing: '0.3em', marginTop: 2,
            textTransform: 'uppercase',
          }}>NO. {String(recipeIndex + 1).padStart(2, '0')} · {PAGE_LABELS_EN[pageIdx]}</div>
        </div>
        <div style={{ display: 'flex', gap: 4 }}>
          <button
            onClick={() => setChatOpen(true)}
            title="问问食神"
            style={{
              ...iconBtn(t), color: BOOK.ink, position: 'relative',
            }}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
                 stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
              <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
            </svg>
            {chatMessages.length > 0 && (
              <span style={{
                position: 'absolute', top: 6, right: 6,
                width: 6, height: 6, borderRadius: '50%',
                background: accent,
              }} />
            )}
          </button>
          <button style={{ ...iconBtn(t), color: BOOK.ink }}><UIIcons.share size={16} /></button>
          <button style={{ ...iconBtn(t), color: BOOK.ink }}><UIIcons.bookmark size={18} /></button>
        </div>
      </div>

      {/* BOOK STAGE */}
      <div
        onMouseDown={onPointerDown}
        onMouseMove={onPointerMove}
        onMouseUp={onPointerUp}
        onMouseLeave={onPointerUp}
        onTouchStart={onPointerDown}
        onTouchMove={onPointerMove}
        onTouchEnd={onPointerUp}
        style={{
          position: 'relative',
          margin: `8px 16px 0`,
          height: 596,
          perspective: '1800px',
          perspectiveOrigin: '0% 50%',
          touchAction: 'pan-y',
          userSelect: 'none',
        }}
      >
        {/* Book base shadow */}
        <div style={{
          position: 'absolute', inset: '-6px -8px -10px -2px',
          background: 'rgba(0,0,0,0.45)',
          filter: 'blur(14px)', zIndex: 0, borderRadius: 4,
        }} />

        <BookSpine pos="right" />

        {/* Spine binding */}
        <div style={{
          position: 'absolute', top: 0, bottom: 0, left: -3, width: 8,
          background: 'linear-gradient(90deg, #2A1E10, #5A4528)',
          borderRight: `1px solid #1A0F05`,
          boxShadow: 'inset -1px 0 0 rgba(0,0,0,0.6)',
          zIndex: 35, pointerEvents: 'none',
        }} />

        {Array.from({ length: TOTAL }, (_, i) => {
          const { z, transform, display, opacity } = pageProps(i);
          const useTransition = !dragging.current;
          return (
            <div key={i} style={{
              position: 'absolute', inset: 0,
              display, zIndex: z, opacity,
              transformStyle: 'preserve-3d',
              transformOrigin: 'left center',
              transform,
              transition: useTransition
                ? 'transform 0.54s cubic-bezier(0.45, 0.05, 0.25, 1), opacity 0.3s'
                : 'none',
            }}>
              <div style={{
                position: 'absolute', inset: 0,
                background: BOOK.paper,
                backgroundImage: `repeating-linear-gradient(0deg, transparent 0 26px, rgba(140,100,40,0.04) 26px 27px)`,
                border: `1px solid ${BOOK.rule}`,
                borderLeft: `2px solid ${BOOK.ink}30`,
                backfaceVisibility: 'hidden',
                boxShadow: i === pageIdx
                  ? '0 18px 36px -22px rgba(0,0,0,0.7), inset 8px 0 12px -10px rgba(0,0,0,0.18)'
                  : 'inset 8px 0 12px -10px rgba(0,0,0,0.18)',
                overflow: 'hidden',
              }}>
                {/* Binding holes */}
                <div style={{
                  position: 'absolute', left: 14, top: 0, bottom: 0,
                  display: 'flex', flexDirection: 'column',
                  justifyContent: 'space-around', padding: '70px 0',
                  pointerEvents: 'none', zIndex: 1,
                }}>
                  {[0,1,2,3,4].map(k => (
                    <span key={k} style={{
                      width: 5, height: 5, borderRadius: 3,
                      background: BOOK.hole,
                    }} />
                  ))}
                </div>

                {/* Tab indicators (right edge) — thumb index */}
                <div style={{
                  position: 'absolute', right: -1, top: 80,
                  display: 'flex', flexDirection: 'column', gap: 6,
                  pointerEvents: 'auto', zIndex: 2,
                }}>
                  {PAGE_LABELS.map((lbl, k) => {
                    const isActive = k === pageIdx;
                    return (
                      <button key={k} onClick={() => goTo(k)} style={{
                        writingMode: 'vertical-rl',
                        width: 22, height: 70,
                        padding: '8px 0', border: 'none',
                        background: isActive ? accent : (k === i ? BOOK.paper : BOOK.paperDeep),
                        color: isActive ? '#fff' : BOOK.ink + 'AA',
                        fontFamily: '"Noto Serif SC", serif', fontSize: 10.5,
                        letterSpacing: '0.25em', cursor: 'pointer',
                        borderTop: `1px solid ${BOOK.rule}`,
                        borderBottom: `1px solid ${BOOK.rule}`,
                        borderLeft: `1px solid ${BOOK.rule}`,
                        boxShadow: isActive ? `inset 2px 0 0 ${accent}` : 'none',
                        transition: 'all .15s',
                      }}>{lbl}</button>
                    );
                  })}
                </div>

                {renderPage(i)}

                {/* Page footer */}
                <div style={{
                  position: 'absolute', bottom: 12, left: 44, right: 50,
                  display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                  fontFamily: '"DM Mono", monospace', fontSize: 9,
                  color: BOOK.ink + '77', letterSpacing: '0.25em', textTransform: 'uppercase',
                  pointerEvents: 'none',
                }}>
                  <span>{recipe.name} · CH.{String(recipeIndex + 1).padStart(2, '0')}</span>
                  <span>p. {String((recipeIndex + 1) * 12 + i).padStart(3, '0')}</span>
                </div>
              </div>
              {renderBack()}
            </div>
          );
        })}
      </div>

      {/* Bottom controls */}
      <div style={{
        margin: `18px ${pad}px 0`,
        padding: '12px 14px',
        background: 'rgba(0, 0, 0, 0.04)',
        border: `1px solid rgba(0, 0, 0, 0.08)`,
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        gap: 10,
      }}>
        <button onClick={() => goTo(pageIdx - 1)}
          disabled={pageIdx === 0}
          style={{
            background: 'transparent', border: 'none', color: BOOK.ink,
            opacity: pageIdx === 0 ? 0.3 : 1,
            cursor: pageIdx === 0 ? 'default' : 'pointer',
            display: 'flex', alignItems: 'center', gap: 6,
            fontFamily: '"DM Mono", monospace', fontSize: 10,
            letterSpacing: '0.2em', textTransform: 'uppercase',
            padding: '4px 8px',
          }}>
          ← {PAGE_LABELS[Math.max(0, pageIdx - 1)]}
        </button>

        <div style={{
          fontFamily: '"DM Mono", monospace', fontSize: 11,
          color: BOOK.ink, letterSpacing: '0.15em',
        }}>
          {String(pageIdx + 1).padStart(2, '0')}
          <span style={{ color: BOOK.ink + '88' }}> / {String(TOTAL).padStart(2, '0')}</span>
        </div>

        <button onClick={() => goTo(pageIdx + 1)}
          disabled={pageIdx === TOTAL - 1}
          style={{
            background: 'transparent', border: 'none', color: BOOK.ink,
            opacity: pageIdx === TOTAL - 1 ? 0.3 : 1,
            cursor: pageIdx === TOTAL - 1 ? 'default' : 'pointer',
            display: 'flex', alignItems: 'center', gap: 6,
            fontFamily: '"DM Mono", monospace', fontSize: 10,
            letterSpacing: '0.2em', textTransform: 'uppercase',
            padding: '4px 8px',
          }}>
          {PAGE_LABELS[Math.min(TOTAL - 1, pageIdx + 1)]} →
        </button>
      </div>

      {/* ───── Chat drawer (问问食神) ───── */}
      {chatOpen && (
        <ChatDrawer
          recipe={recipe}
          accent={accent}
          messages={chatMessages}
          input={chatInput}
          setInput={setChatInput}
          sending={chatSending}
          error={chatError}
          onSend={sendChat}
          onClose={() => setChatOpen(false)}
          onClear={clearChat}
          pageLabel={PAGE_LABELS[pageIdx]}
          stepLabel={pageIdx === 2 && recipe.steps[stepIdx] ? `STEP ${stepIdx + 1} · ${recipe.steps[stepIdx].title}` : null}
        />
      )}
    </div>
  );
}

// ── Chat drawer (bottom sheet) ──
function ChatDrawer({ recipe, accent, messages, input, setInput, sending, error, onSend, onClose, onClear, pageLabel, stepLabel }) {
  const listRef = rD2(null);
  const inputRef = rD2(null);

  eD2(() => {
    if (listRef.current) {
      listRef.current.scrollTop = listRef.current.scrollHeight;
    }
  }, [messages, sending]);

  eD2(() => {
    setTimeout(() => inputRef.current?.focus(), 220);
  }, []);

  const onKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey && !sending) {
      e.preventDefault();
      onSend();
    }
  };

  const QUICK_PROMPTS = [
    '这步有什么常见错误？',
    '没有 X，能用 Y 替代吗？',
    '怎么判断火候到了？',
    '糊了/咸了/老了怎么救？',
  ];

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 200,
      display: 'flex', flexDirection: 'column', justifyContent: 'flex-end',
      pointerEvents: 'auto',
    }}>
      {/* Backdrop */}
      <div
        onClick={onClose}
        style={{
          position: 'absolute', inset: 0,
          background: 'rgba(28, 22, 12, 0.45)',
          animation: 'chatFadeIn 0.25s ease-out',
        }}
      />
      {/* Sheet */}
      <div style={{
        position: 'relative', zIndex: 1,
        background: BOOK.paper,
        backgroundImage: `repeating-linear-gradient(0deg, transparent 0 24px, rgba(140,100,40,0.04) 24px 25px)`,
        borderTop: `1px solid ${BOOK.rule}`,
        borderTopLeftRadius: 16, borderTopRightRadius: 16,
        boxShadow: '0 -10px 30px rgba(0,0,0,0.25)',
        height: '78vh', maxHeight: 720,
        display: 'flex', flexDirection: 'column',
        animation: 'chatSlideUp 0.32s cubic-bezier(.2,.9,.3,1)',
      }}>
        {/* Pull handle */}
        <div style={{
          width: 40, height: 4, borderRadius: 2, background: BOOK.rule,
          margin: '8px auto 4px', flexShrink: 0,
        }} />

        {/* Header */}
        <div style={{
          padding: '10px 18px 12px', borderBottom: `1px solid ${BOOK.rule}`,
          display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0,
        }}>
          <div style={{ minWidth: 0 }}>
            <div style={{
              fontFamily: '"Noto Serif SC", serif', fontSize: 15, fontWeight: 500,
              color: BOOK.ink, letterSpacing: '0.04em',
            }}>问问食神</div>
            <div style={{
              fontFamily: '"DM Mono", monospace', fontSize: 8.5,
              letterSpacing: '0.25em', color: BOOK.ink + '88',
              textTransform: 'uppercase', marginTop: 2,
              whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
            }}>
              {recipe.name} · {pageLabel}{stepLabel ? ` · ${stepLabel}` : ''}
            </div>
          </div>
          <div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
            {messages.length > 0 && (
              <button onClick={onClear} style={{
                background: 'transparent', border: 'none', cursor: 'pointer',
                fontFamily: '"DM Mono", monospace', fontSize: 9,
                letterSpacing: '0.2em', color: BOOK.ink + '88',
                padding: '6px 8px', textTransform: 'uppercase',
              }}>清空</button>
            )}
            <button onClick={onClose} style={{
              background: 'transparent', border: 'none', cursor: 'pointer',
              padding: 6, color: BOOK.ink, lineHeight: 1,
            }} title="关闭">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
                   stroke="currentColor" strokeWidth="2" strokeLinecap="round">
                <path d="M6 6l12 12M18 6l-12 12" />
              </svg>
            </button>
          </div>
        </div>

        {/* Message list */}
        <div ref={listRef} style={{
          flex: 1, minHeight: 0, overflowY: 'auto',
          padding: '14px 16px',
        }}>
          {messages.length === 0 && (
            <div>
              <div style={{
                fontFamily: '"Noto Serif SC", serif', fontSize: 13,
                color: BOOK.ink + 'BB', lineHeight: 1.7,
                padding: '8px 4px 18px',
                fontStyle: 'italic',
              }}>
                做菜途中遇到任何疑问都可向我请教 — 我会按这道菜的食材、心法和你当前所在的步骤回答。
              </div>
              <div style={{
                fontFamily: '"DM Mono", monospace', fontSize: 9,
                letterSpacing: '0.25em', color: BOOK.ink + '88',
                textTransform: 'uppercase', marginBottom: 8,
              }}>建议提问</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                {QUICK_PROMPTS.map((p, k) => (
                  <button key={k}
                    onClick={() => setInput(p)}
                    style={{
                      fontFamily: '"Noto Serif SC", serif', fontSize: 12,
                      padding: '6px 10px', cursor: 'pointer',
                      background: 'rgba(255,255,255,0.6)',
                      border: `1px solid ${BOOK.rule}`,
                      color: BOOK.ink + 'CC', letterSpacing: '0.02em',
                    }}>{p}</button>
                ))}
              </div>
            </div>
          )}
          {messages.map((m, i) => (
            <div key={i} style={{
              display: 'flex', justifyContent: m.role === 'user' ? 'flex-end' : 'flex-start',
              marginBottom: 10,
            }}>
              <div style={{
                maxWidth: '82%',
                padding: '9px 12px',
                background: m.role === 'user' ? accent : 'rgba(255,255,255,0.55)',
                color: m.role === 'user' ? '#fff' : BOOK.ink,
                border: m.role === 'user' ? 'none' : `1px solid ${BOOK.rule}`,
                fontFamily: '"Noto Serif SC", serif', fontSize: 13,
                lineHeight: 1.7, letterSpacing: '0.02em',
                wordBreak: 'break-word',
                borderRadius: 4,
                whiteSpace: m.role === 'user' ? 'pre-wrap' : 'normal',
              }}>
                {m.role === 'user' ? m.content : <MdBlock text={m.content} />}
              </div>
            </div>
          ))}
          {sending && (
            <div style={{
              display: 'flex', justifyContent: 'flex-start', marginBottom: 10,
            }}>
              <div style={{
                padding: '9px 14px',
                background: 'rgba(255,255,255,0.55)',
                border: `1px solid ${BOOK.rule}`,
                fontFamily: '"DM Mono", monospace', fontSize: 11,
                color: BOOK.ink + 'AA', letterSpacing: '0.15em',
                borderRadius: 4,
              }}>食神参悟中…</div>
            </div>
          )}
          {error && (
            <div style={{
              padding: '10px 12px', marginTop: 8,
              background: '#FBE9E7', border: '1px solid #D45A3D44',
              fontFamily: '"DM Mono", monospace', fontSize: 11,
              color: '#A8341B', lineHeight: 1.5, wordBreak: 'break-word',
            }}>
              发送失败：{error}
              <div style={{ marginTop: 4, opacity: 0.7, fontSize: 10 }}>
                若本地测试出现 404，需要部署到 Cloudflare Pages 后才能用，或用 `npx wrangler pages dev .` 在本地起函数。
              </div>
            </div>
          )}
        </div>

        {/* Input */}
        <div style={{
          padding: '10px 12px 14px',
          borderTop: `1px solid ${BOOK.rule}`,
          display: 'flex', alignItems: 'flex-end', gap: 8, flexShrink: 0,
          paddingBottom: 'max(14px, env(safe-area-inset-bottom))',
        }}>
          <textarea
            ref={inputRef}
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={onKeyDown}
            disabled={sending}
            placeholder="向食神请教…"
            rows={1}
            style={{
              flex: 1, minHeight: 38, maxHeight: 120,
              padding: '9px 12px',
              background: 'rgba(255,255,255,0.7)',
              border: `1px solid ${BOOK.rule}`, outline: 'none',
              fontFamily: '"Noto Serif SC", serif', fontSize: 14,
              lineHeight: 1.5, color: BOOK.ink, resize: 'none',
            }}
          />
          <button
            onClick={onSend}
            disabled={!input.trim() || sending}
            style={{
              padding: '9px 16px',
              background: !input.trim() || sending ? `${accent}88` : accent,
              color: '#fff', border: 'none',
              cursor: !input.trim() || sending ? 'default' : 'pointer',
              fontFamily: '"DM Mono", monospace', fontSize: 11,
              letterSpacing: '0.2em', textTransform: 'uppercase',
              fontWeight: 600, flexShrink: 0,
              transition: 'all .15s',
            }}>发送</button>
        </div>
      </div>
    </div>
  );
}

window.DetailV2 = DetailV2;
window.StepTimer = StepTimer;
