// ====== MECHANICS EXPERIMENTS ======

// ---------- 1. Length & Mass Measurement ----------
function MeasurementExp({ category, name, catColor }) {
  const [objectLen, setObjectLen] = useState(4.52); // cm
  const [objectMass, setObjectMass] = useState(48.5); // g
  const [subMode, setSubMode] = useState('length'); // length, balance

  const canvasW = 680, canvasH = 400;

  // Length measurement with ruler
  const scaleX = 60, scaleY = 200;
  const rulerStart = 0; // cm
  const rulerEnd = 10; // cm
  const pixelsPerCm = 50;
  const rulerW = (rulerEnd - rulerStart) * pixelsPerCm;

  // object position
  const objStartCm = 1.2; // starting at 1.20 cm mark
  const objStartX = scaleX + objStartCm * pixelsPerCm;
  const objEndX = objStartX + objectLen * pixelsPerCm;

  // Balance: weights on left, object on right (or vice versa)
  const balanceCx = 340, balanceCy = 200;
  const balanceLen = 180;
  const [weights, setWeights] = useState([50, 20, 10, 5]); // g weights on left
  const totalWeight = weights.reduce((s, w) => s + w, 0);
  const [isBalanced, setIsBalanced] = useState(false);

  // Calculate tilt: positive means left heavier (object side heavier)
  const tilt = Math.max(-15, Math.min(15, (objectMass - totalWeight) * 0.3));

  const addWeight = (w) => {
    if (weights.reduce((s, x) => s + x, 0) + w <= 200) {
      setWeights([...weights, w]);
    }
  };

  const removeWeight = () => {
    if (weights.length > 0) {
      const newW = [...weights];
      newW.pop();
      setWeights(newW);
    }
  };

  useEffect(() => {
    setIsBalanced(Math.abs(totalWeight - objectMass) < 0.3);
  }, [totalWeight, objectMass]);

  const panelTabs = [
    { id: 'controls', label: '操作' },
    { id: 'data', label: '读数/公式' },
    { id: 'principle', label: '原理' },
  ];

  const panelContent = (tab) => {
    if (tab === 'controls') return (
      <>
        <div className="panel-section">
          <div className="panel-section-title">实验</div>
          <div className="btn-row">
            <button className={`btn ${subMode === 'length' ? 'primary' : ''}`} style={{flex:1}}
              onClick={() => setSubMode('length')}>
              刻度尺
            </button>
            <button className={`btn ${subMode === 'balance' ? 'primary' : ''}`} style={{flex:1}}
              onClick={() => setSubMode('balance')}>
              天平
            </button>
          </div>
        </div>

        {subMode === 'length' && (
          <div className="panel-section">
            <div className="panel-section-title">物体长度</div>
            <ControlSlider label="物体长度" value={objectLen} onChange={setObjectLen}
              min={0.5} max={8} step={0.01} unit=" cm" />
            <div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 4 }}>
              起始刻度：{objStartCm.toFixed(2)} cm
            </div>
          </div>
        )}

        {subMode === 'balance' && (
          <div className="panel-section">
            <div className="panel-section-title">物体质量（左盘）</div>
            <ControlSlider label="物体" value={objectMass} onChange={setObjectMass}
              min={1} max={200} step={0.5} unit=" g" />
            <div className="panel-section-title" style={{ marginTop: 14 }}>砝码（右盘）</div>
            <div className="btn-row" style={{ flexWrap: 'wrap', gap: 6 }}>
              {[100, 50, 20, 10, 5].map(w => (
                <button key={w} className="btn sm" onClick={() => addWeight(w)}>
                  +{w}g
                </button>
              ))}
              <button className="btn sm danger" onClick={removeWeight}>-1个</button>
              <button className="btn sm" onClick={() => setWeights([])}>清空</button>
            </div>
            <div style={{ marginTop: 10, fontSize: 13, color: 'var(--text-dim)' }}>
              砝码总质量：<span style={{ color: catColor, fontWeight: 'bold' }}>{totalWeight} g</span>
            </div>
          </div>
        )}
      </>
    );
    if (tab === 'data') return (
      <>
        {subMode === 'length' && (
          <>
            <div className="meter-row">
              <MeterCard label="测量结果" value={(objectLen).toFixed(2)} unit=" cm"
                sub={`${(objectLen*10).toFixed(1)} mm`} full />
            </div>
            <FormulaBox
              expr="L = L末 - L始"
              subs={[
                `末端刻度：<span class="val">${(objStartCm + objectLen).toFixed(2)} cm</span>`,
                `起始刻度：<span class="val">${objStartCm.toFixed(2)} cm</span>`,
                `物体长度：<span class="val">${objectLen.toFixed(2)} cm</span>`,
                `分度值 1mm，估读到 0.1mm（下一位）`,
              ]}
            />
            <div className="explain-box">
              <h4>刻度尺读数要点</h4>
              <p>1. 刻度尺要放正，刻度紧贴被测物体</p>
              <p style={{marginTop:3}}>2. 视线与尺面垂直</p>
              <p style={{marginTop:3}}>3. 读数要估读到分度值的下一位</p>
              <p style={{marginTop:3}}>4. 记录要有数值和单位</p>
            </div>
          </>
        )}
        {subMode === 'balance' && (
          <>
            <div className="meter-row">
              <MeterCard label="物体质量" value={objectMass} unit=" g" sub="待测物" />
              <MeterCard label="砝码总质量" value={totalWeight} unit=" g" sub="右盘" />
            </div>
            <FormulaBox
              expr="m物 = m砝码 + m游码"
              subs={[
                `左盘物体：<span class="val">${objectMass} g</span>`,
                `右盘砝码：<span class="val">${totalWeight} g</span>`,
                isBalanced
                  ? `<span style="color:#111114">● 天平平衡，物体质量 = ${totalWeight} g</span>`
                  : (objectMass > totalWeight
                    ? `<span style="color:#111114">左重右轻，需增加砝码</span>`
                    : `<span style="color:#111114">右重左轻，需减少砝码</span>`),
              ]}
            />
          </>
        )}
      </>
    );
    if (tab === 'principle') return (
      <ExplainBox title={subMode === 'length' ? '长度的测量' : '质量的测量'}>
        {subMode === 'length' ? (
          <>
            <p><strong>长度单位：</strong>米（m），常用还有 km、dm、cm、mm、μm、nm。</p>
            <p style={{marginTop:6}}><strong>测量工具：</strong>刻度尺、卷尺、游标卡尺、螺旋测微器等。</p>
            <p style={{marginTop:6}}><strong>刻度尺使用：</strong></p>
            <p>• 选：根据需要选择合适量程和分度值的刻度尺</p>
            <p>• 放：刻度尺放正，刻度紧贴被测物体</p>
            <p>• 读：视线与尺面垂直，估读到分度值下一位</p>
            <p>• 记：数值 + 单位</p>
            <p style={{marginTop:6}}><strong>误差：</strong>误差不可避免，多次测量取平均值减小误差。</p>
          </>
        ) : (
          <>
            <p><strong>质量：</strong>物体所含物质的多少，是物体的属性，不随形状、状态、位置改变。</p>
            <p style={{marginTop:6}}><strong>单位：</strong>千克（kg），常用还有 t、g、mg。</p>
            <p style={{marginTop:6}}><strong>天平使用：</strong></p>
            <p>• 放：水平台</p>
            <p>• 拨：游码归零</p>
            <p>• 调：调平衡螺母使横梁平衡</p>
            <p>• 称：左物右码，先大后小</p>
            <p>• 记：m物 = m砝码 + m游码</p>
          </>
        )}
      </ExplainBox>
    );
  };

  return (
    <ExperimentShell category={category} name={name} catColor={catColor}
      panelTabs={panelTabs} panelContent={panelContent}>
      <div className="sim-canvas-wrap">
        <svg width={canvasW} height={canvasH} className="sim-canvas paper-bg">

          {subMode === 'length' ? (
            <g>
              {/* ruler */}
              <rect x={scaleX - 2} y={scaleY - 20} width={rulerW + 4} height="40"
                fill="#f0d090" stroke="#8B6914" strokeWidth="1.5" rx="2" />

              {/* cm marks */}
              {Array.from({length: 11}, (_, i) => {
                const x = scaleX + i * pixelsPerCm;
                return (
                  <g key={i}>
                    <line x1={x} y1={scaleY - 20} x2={x} y2={scaleY - 5}
                      stroke="#333" strokeWidth="1.5" />
                    <text x={x} y={scaleY - 23} textAnchor="middle" fontSize="10" fill="#333">
                      {i}
                    </text>
                  </g>
                );
              })}

              {/* mm marks */}
              {Array.from({length: 100}, (_, i) => {
                if (i % 10 === 0) return null;
                const x = scaleX + i * pixelsPerCm / 10;
                const isHalf = i % 5 === 0;
                return (
                  <line key={i} x1={x} y1={scaleY - 20} x2={x} y2={scaleY - (isHalf ? 10 : 13)}
                    stroke="#666" strokeWidth="0.8" />
                );
              })}

              {/* object (block) */}
              <rect x={objStartX} y={scaleY + 10} width={objectLen * pixelsPerCm} height="50"
                fill={catColor + '88'} stroke={catColor} strokeWidth="2" rx="2" />
              <text x={objStartX + objectLen * pixelsPerCm / 2} y={scaleY + 40}
                textAnchor="middle" fill="#fff" fontSize="12" fontWeight="bold">
                待测物体
              </text>

              {/* reading lines */}
              <line x1={objStartX} y1={scaleY - 30} x2={objStartX} y2={scaleY + 70}
                stroke="#FF5C5C" strokeWidth="1" strokeDasharray="4,3" />
              <line x1={objEndX} y1={scaleY - 30} x2={objEndX} y2={scaleY + 70}
                stroke="#FF5C5C" strokeWidth="1" strokeDasharray="4,3" />

              <text x="400" y="50" fill="#5E3A18" fontSize="14" fontWeight="bold">
                刻度尺测量长度
              </text>
              <text x="400" y="72" fill="#888" fontSize="11" textAnchor="middle">
                分度值 1mm，估读到 0.1mm
              </text>
            </g>
          ) : (
            <g>
              {/* balance stand */}
              <rect x={balanceCx - 8} y={balanceCy + 50} width="16" height="80" fill="#8B5A2B" />
              <rect x={balanceCx - 40} y={balanceCy + 125} width="80" height="10" fill="#5E3A18" rx="2" />

              {/* pivot */}
              <polygon points={`${balanceCx-10},${balanceCy+50} ${balanceCx+10},${balanceCy+50} ${balanceCx},${balanceCy+35}`}
                fill="#8B5A2B" />

              {/* beam */}
              <g transform={`rotate(${tilt}, ${balanceCx}, ${balanceCy + 35})`}>
                <rect x={balanceCx - balanceLen} y={balanceCy + 30} width={balanceLen * 2} height="10"
                  fill="#A97442" stroke="#5E3A18" strokeWidth="1.5" rx="2" />

                {/* left pan - object */}
                <line x1={balanceCx - balanceLen + 20} y1={balanceCy + 35}
                  x2={balanceCx - balanceLen + 20} y2={balanceCy + 80}
                  stroke="#8B5A2B" strokeWidth="1.5" />
                <ellipse cx={balanceCx - balanceLen + 20} cy={balanceCy + 82} rx="40" ry="8"
                  fill="#B87333" stroke="#5E3A18" strokeWidth="1.5" />
                <text x={balanceCx - balanceLen + 20} y={balanceCy + 100} textAnchor="middle" fill="#5E3A18" fontSize="11">
                  物体 {objectMass}g
                </text>

                {/* object on left pan */}
                <rect x={balanceCx - balanceLen + 10} y={balanceCy + 62} width="20" height="18"
                  fill={catColor + 'aa'} stroke={catColor} strokeWidth="1.5" rx="2" />

                {/* right pan - weights */}
                <line x1={balanceCx + balanceLen - 20} y1={balanceCy + 35}
                  x2={balanceCx + balanceLen - 20} y2={balanceCy + 80}
                  stroke="#8B5A2B" strokeWidth="1.5" />
                <ellipse cx={balanceCx + balanceLen - 20} cy={balanceCy + 82} rx="40" ry="8"
                  fill="#B87333" stroke="#5E3A18" strokeWidth="1.5" />
                <text x={balanceCx + balanceLen - 20} y={balanceCy + 100} textAnchor="middle" fill="#5E3A18" fontSize="11">
                  砝码 {totalWeight}g
                </text>

                {/* weights on right pan */}
                {weights.slice(0, 5).map((w, i) => (
                  <g key={i} transform={`translate(${balanceCx + balanceLen - 20 + (i-2)*10}, ${balanceCy + 72 - i * 6})`}>
                    <rect x="-6" y="0" width="12" height={Math.max(4, Math.sqrt(w) * 1.5)}
                      fill="#D4A853" stroke="#8B6914" strokeWidth="0.8" />
                  </g>
                ))}
              </g>

              {/* pointer */}
              <g transform={`rotate(${tilt * 0.5}, ${balanceCx}, ${balanceCy + 40})`}>
                <line x1={balanceCx} y1={balanceCy + 30} x2={balanceCx} y2={balanceCy}
                  stroke="#FF3B30" strokeWidth="1.5" />
                <circle cx={balanceCx} cy={balanceCy + 35} r="4" fill="#FF3B30" />
              </g>

              {/* scale marks */}
              {[-3,-2,-1,0,1,2,3].map(i => (
                <line key={i} x1={balanceCx + i * 8} y1={balanceCy - 5}
                  x2={balanceCx + i * 8} y2={balanceCy + 5}
                  stroke="#5E3A18" strokeWidth="1" />
              ))}
              <line x1={balanceCx} y1={balanceCy - 8} x2={balanceCx} y2={balanceCy + 8}
                stroke="#FF3B30" strokeWidth="1.5" />

              {/* label */}
              <text x={balanceCx} y="50" textAnchor="middle" fill="#5E3A18" fontSize="14" fontWeight="bold">
                托盘天平
              </text>
              <text x={balanceCx} y="72" textAnchor="middle" fill="#888" fontSize="11">
                {isBalanced ? '横梁平衡' : (tilt > 0 ? '左重右轻' : '右重左轻')}
              </text>
            </g>
          )}
        </svg>
      </div>
    </ExperimentShell>
  );
}

// ---------- 2. Gravity vs Mass ----------
function GravityExp({ category, name, catColor }) {
  const [mass, setMass] = useState(500); // g
  const [g, setG] = useState(9.8); // N/kg
  const gravity = (mass / 1000) * g; // N

  const canvasW = 600, canvasH = 420;

  // spring dynamometer
  const dynX = 300, dynTop = 40;
  const dynW = 50;
  const dynH = 260;
  // spring extends with gravity: each N extends by some amount
  const extension = gravity * 20; // pixels per Newton
  const maxExtension = 120;

  const panelTabs = [
    { id: 'controls', label: '操作' },
    { id: 'data', label: '数据/公式' },
    { id: 'principle', label: '原理' },
  ];

  const panelContent = (tab) => {
    if (tab === 'controls') return (
      <>
        <div className="panel-section">
          <div className="panel-section-title">参数调节</div>
          <ControlSlider label="物体质量 m" value={mass} onChange={setMass} min={100} max={1500} step={10} unit=" g" />
          <ControlSlider label="重力加速度 g" value={g} onChange={setG} min={1} max={20} step={0.1} unit=" N/kg" />
          <div className="btn-row" style={{ marginTop: 8 }}>
            <button className="btn sm" onClick={() => setG(9.8)}>地球 9.8</button>
            <button className="btn sm" onClick={() => setG(1.67)}>月球 1.67</button>
            <button className="btn sm" onClick={() => setG(3.7)}>火星 3.7</button>
          </div>
        </div>
      </>
    );
    if (tab === 'data') return (
      <>
        <div className="meter-row">
          <MeterCard label="质量 m" value={mass} unit=" g" sub={`${(mass/1000).toFixed(2)} kg`} />
          <MeterCard label="重力 G" value={gravity.toFixed(2)} unit=" N" />
        </div>
        <FormulaBox
          expr="G = m · g"
          subs={[
            `质量 <span class="val">m = ${(mass/1000).toFixed(2)} kg</span>`,
            `g = <span class="val">${g} N/kg</span>`,
            `G = ${(mass/1000).toFixed(2)} kg × ${g} N/kg = <span class="val">${gravity.toFixed(2)} N</span>`,
            `重力与质量成<span style="color:${catColor}">正比</span>，G/m = g = 常数`,
          ]}
        />
      </>
    );
    if (tab === 'principle') return (
      <ExplainBox title="重力">
        <p><strong>重力：</strong>由于地球吸引而使物体受到的力，符号 G。</p>
        <p style={{marginTop:6}}><strong>方向：</strong>竖直向下。</p>
        <p style={{marginTop:6}}><strong>大小：</strong>G = m·g</p>
        <p>• g = 9.8 N/kg（地球表面）</p>
        <p>• 物理意义：质量为1kg的物体受到的重力是9.8N</p>
        <p style={{marginTop:6}}><strong>作用点：</strong>重心（形状规则、质量均匀的物体在几何中心）。</p>
        <p style={{marginTop:6}}>物体所受重力与质量成正比。</p>
      </ExplainBox>
    );
  };

  return (
    <ExperimentShell category={category} name={name} catColor={catColor}
      panelTabs={panelTabs} panelContent={panelContent}>
      <div className="sim-canvas-wrap">
        <svg width={canvasW} height={canvasH} className="sim-canvas">
          {/* ceiling */}
          <rect x="0" y="0" width={canvasW} height="20" fill="#A97442" />
          <line x1="0" y1="20" x2={canvasW} y2="20" stroke="#5E3A18" strokeWidth="2" />

          {/* spring dynamometer */}
          <g transform={`translate(${dynX - dynW/2}, ${dynTop})`}>
            {/* hook at top */}
            <path d="M 25 -15 Q 35 -15 35 -5 L 35 0 L 25 0 Q 15 0 15 10 Q 15 20 25 20"
              fill="none" stroke="#666" strokeWidth="2" />

            {/* outer case */}
            <rect x="0" y="20" width={dynW} height={dynH} rx="4"
              fill="#f5f0e0" stroke="#8B6914" strokeWidth="1.5" />

            {/* scale window */}
            <rect x="8" y="30" width={dynW - 16} height={dynH - 40} fill="#fff" stroke="#ccc" strokeWidth="0.5" />

            {/* scale marks (0 to 10N) */}
            {Array.from({length: 11}, (_, i) => {
              const y = 40 + i * (dynH - 60) / 10;
              return (
                <g key={i}>
                  <line x1="14" y1={y} x2="20" y2={y} stroke="#333" strokeWidth="1" />
                  <line x1={dynW - 20} y1={y} x2={dynW - 14} y2={y} stroke="#333" strokeWidth="1" />
                  <text x="24" y={y + 3} fontSize="9" fill="#666">{i}N</text>
                </g>
              );
            })}

            {/* spring inside */}
            <g transform={`translate(${dynW/2}, 35)`}>
              {/* top fixed point */}
              <line x1="0" y1="0" x2="0" y2="10" stroke="#666" strokeWidth="1" />
              {/* spring coils */}
              <path d={generateSpringPath(0, 10, 0, 100 + extension, 6, 12)}
                fill="none" stroke="#D4A853" strokeWidth="1.5" />
              {/* pointer */}
              <g transform={`translate(0, ${100 + extension})`}>
                <line x1="-20" y1="0" x2="20" y2="0" stroke="#FF3B30" strokeWidth="2" />
                <polygon points="-20,0 -16,-4 -16,4" fill="#FF3B30" />
                <polygon points="20,0 16,-4 16,4" fill="#FF3B30" />
              </g>
              {/* lower rod + hook */}
              <line x1="0" y1={100 + extension} x2="0" y2={130 + extension} stroke="#666" strokeWidth="1.5" />
            </g>

            {/* bottom hook */}
            <path d={`M ${dynW/2} ${dynH + 10} Q ${dynW/2 + 8} ${dynH + 10} ${dynW/2 + 8} ${dynH + 18} Q ${dynW/2 + 8} ${dynH + 26} ${dynW/2} ${dynH + 26}`}
              fill="none" stroke="#666" strokeWidth="2" transform={`translate(0, ${extension})`} />
          </g>

          {/* hanging mass */}
          <g transform={`translate(${dynX}, ${dynTop + dynH + 30 + extension})`}>
            <rect x="-20" y="0" width="40" height={Math.min(60, mass/30)}
              fill={catColor} stroke={catColor} strokeWidth="1.5" rx="3" />
            <text x="0" y={Math.min(60, mass/30)/2 + 4} textAnchor="middle" fill="#fff" fontSize="10" fontWeight="bold">
              {mass}g
            </text>
          </g>

          {/* reading label */}
          <g transform={`translate(${dynX + 60}, ${dynTop + 40 + (100 + extension) - 10})`}>
            <rect x="0" y="-15" width="80" height="30" rx="4" fill="#141416" stroke="#333" strokeWidth="1" />
            <text x="40" y="5" textAnchor="middle" fill={catColor} fontSize="14" fontWeight="bold" fontFamily="JetBrains Mono, monospace">
              {gravity.toFixed(2)} N
            </text>
          </g>

          <text x={canvasW / 2} y={canvasH - 20} textAnchor="middle" fill="#5E3A18" fontSize="13">
            G = mg = {gravity.toFixed(2)} N
          </text>
        </svg>
      </div>
    </ExperimentShell>
  );
}

function generateSpringPath(x1, y1, x2, y2, coils, width) {
  const points = [];
  const totalLen = Math.sqrt((x2-x1)**2 + (y2-y1)**2);
  const dx = (x2-x1)/totalLen;
  const dy = (y2-y1)/totalLen;
  const px = -dy, py = dx; // perpendicular
  const segs = coils * 4;
  for (let i = 0; i <= segs; i++) {
    const t = i / segs;
    const cx = x1 + (x2-x1) * t;
    const cy = y1 + (y2-y1) * t;
    const phase = (i % 4) / 4;
    let pw;
    if (i === 0 || i === segs) pw = 0;
    else {
      const q = i % 4;
      if (q === 1) pw = width;
      else if (q === 2) pw = 0;
      else if (q === 3) pw = -width;
      else pw = 0;
    }
    // smooth it
    const ramp = t < 0.05 ? t/0.05 : t > 0.95 ? (1-t)/0.05 : 1;
    pw *= ramp;
    points.push(`${cx + px * pw},${cy + py * pw}`);
  }
  return `M ${points.join(' L ')}`;
}

// ---------- 3. Friction ----------
function FrictionExp({ category, name, catColor }) {
  const canvasW = 650, canvasH = 400;
  const [mass, setMass] = useState(500); // g
  const [surface, setSurface] = useState('wood'); // wood, glass, rough
  const [isPulling, setIsPulling] = useState(false);
  const [pullSpeed, setPullSpeed] = useState(0); // constant speed
  const [friction, setFriction] = useState(0);
  const [pullForce, setPullForce] = useState(0);
  const [position, setPosition] = useState(0);
  const rafRef = useRef();

  const surfaces = {
    wood:  { name: '木板',   μ: 0.3,  color: '#A97442' },
    glass: { name: '玻璃',   μ: 0.15, color: '#c0d8f0' },
    rough: { name: '毛巾',   μ: 0.6,  color: '#D4A853' },
  };
  const surf = surfaces[surface];
  const g = 9.8;
  const normal = (mass / 1000) * g; // N
  const maxStaticFriction = surf.μ * normal * 1.2;
  const kineticFriction = surf.μ * normal;

  useEffect(() => {
    if (!isPulling) {
      // let pull force decrease to 0
      const id = setInterval(() => {
        setPullForce(f => Math.max(0, f - 0.5));
        setPosition(p => p);
      }, 50);
      return () => clearInterval(id);
    }
    let last = performance.now();
    const tick = (now) => {
      const dt = (now - last) / 1000;
      last = now;

      setPullForce(f => {
        // ramp up pulling force
        const target = kineticFriction + 0.5;
        return f + (target - f) * 0.05;
      });

      setFriction(f => {
        if (pullForce < maxStaticFriction) {
          return pullForce; // static
        } else {
          return kineticFriction; // kinetic
        }
      });

      // move if pull > friction
      if (pullForce > maxStaticFriction) {
        const net = pullForce - kineticFriction;
        const a = net / (mass/1000);
        setPullSpeed(v => Math.min(v + a * dt, 0.08));
        setPosition(p => (p + pullSpeed * dt * 50) % 200);
      } else {
        setPullSpeed(v => v * 0.95);
      }

      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [isPulling, surface, mass, pullForce, kineticFriction, maxStaticFriction]);

  const isStatic = pullForce < maxStaticFriction;

  const panelTabs = [
    { id: 'controls', label: '操作' },
    { id: 'data', label: '数据/公式' },
    { id: 'principle', label: '原理' },
  ];

  const panelContent = (tab) => {
    if (tab === 'controls') return (
      <>
        <div className="panel-section">
          <div className="panel-section-title">接触面</div>
          <div className="btn-row">
            {Object.entries(surfaces).map(([k, v]) => (
              <button key={k} className={`btn ${surface === k ? 'primary' : ''}`} style={{flex:1}}
                onClick={() => setSurface(k)}>
                {v.name}
              </button>
            ))}
          </div>
        </div>
        <div className="panel-section">
          <div className="panel-section-title">物体质量</div>
          <ControlSlider label="m" value={mass} onChange={setMass} min={100} max={1500} step={10} unit=" g" />
        </div>
        <div className="panel-section">
          <div className="panel-section-title">操作</div>
          <button className={`btn ${isPulling ? 'primary' : ''}`} style={{ width: '100%', justifyContent: 'center' }}
            onClick={() => { setIsPulling(!isPulling); }}>
            <Icon name={isPulling ? 'Pause' : 'ArrowRight'} size={14} />
            <span>{isPulling ? '停止拉动' : '匀速拉动'}</span>
          </button>
        </div>
      </>
    );
    if (tab === 'data') return (
      <>
        <div className="meter-row">
          <MeterCard label="弹簧测力计" value={pullForce.toFixed(2)} unit=" N" sub={isStatic ? '静摩擦' : '动摩擦'} />
          <MeterCard label="摩擦力 f" value={friction.toFixed(2)} unit=" N" />
        </div>
        <FormulaBox
          expr="f = μ · N"
          subs={[
            `压力 N = G = mg = <span class="val">${normal.toFixed(2)} N</span>`,
            `动摩擦因数 μ = <span class="val">${surf.μ}</span>（${surf.name}）`,
            `最大静摩擦 ≈ <span class="val">${maxStaticFriction.toFixed(2)} N</span>`,
            `动摩擦 f = μN = <span class="val">${kineticFriction.toFixed(2)} N</span>`,
            isStatic
              ? `当前为静摩擦：f = 拉力 = ${pullForce.toFixed(2)} N`
              : `匀速拉动时：拉力 = 滑动摩擦力 = ${friction.toFixed(2)} N`,
          ]}
        />
      </>
    );
    if (tab === 'principle') return (
      <ExplainBox title="摩擦力">
        <p><strong>滑动摩擦力：</strong>两个互相接触的物体，当它们相对滑动时，在接触面上产生的阻碍相对运动的力。</p>
        <p style={{marginTop:6}}><strong>影响因素：</strong></p>
        <p>• 压力：压力越大，摩擦力越大</p>
        <p>• 接触面粗糙程度：越粗糙，摩擦力越大</p>
        <p style={{marginTop:6}}><strong>测量：</strong>用弹簧测力计匀速拉动，f = F<sub>拉</sub>。</p>
        <p style={{marginTop:6}}><strong>增大摩擦：</strong>增大压力、增大粗糙程度、变滚动为滑动。</p>
        <p style={{marginTop:6}}><strong>减小摩擦：</strong>减小压力、减小粗糙程度、变滑动为滚动、加润滑油。</p>
      </ExplainBox>
    );
  };

  return (
    <ExperimentShell category={category} name={name} catColor={catColor}
      panelTabs={panelTabs} panelContent={panelContent}>
      <div className="sim-canvas-wrap">
        <svg width={650} height={400} className="sim-canvas">
          {/* surface */}
          <rect x="40" y="250" width="580" height="100" fill={surf.color} />
          {/* surface texture */}
          {surface === 'rough' && Array.from({length: 60}, (_, i) => (
            <line key={i} x1={50 + i * 10} y1="250" x2={45 + i * 10} y2="260"
              stroke="#8B6914" strokeWidth="1" opacity="0.5" />
          ))}
          {surface === 'wood' && Array.from({length: 8}, (_, i) => (
            <line key={i} x1="40" y1={260 + i * 12} x2="620" y2={260 + i * 12}
              stroke="#8B5A2B" strokeWidth="0.5" opacity="0.4" />
          ))}

          {/* block */}
          <g transform={`translate(${100 + position}, 210)`}>
            <rect x="0" y="0" width="80" height="40" fill={catColor} stroke="#333" strokeWidth="1.5" rx="3" />
            <text x="40" y="25" textAnchor="middle" fill="#fff" fontSize="11" fontWeight="bold">{mass}g</text>
          </g>

          {/* spring dynamometer (horizontal) */}
          <g transform={`translate(${180 + position}, 210)`}>
            {/* body */}
            <rect x="0" y="10" width="100" height="20" fill="#f5f0e0" stroke="#8B6914" strokeWidth="1" rx="3" />
            {/* scale */}
            {[0, 2, 4, 6, 8, 10].map(v => (
              <text key={v} x={8 + v * 9} y="22" fontSize="7" fill="#666">{v}N</text>
            ))}
            {/* pointer */}
            <line x1={8 + pullForce * 9} y1="8" x2={8 + pullForce * 9} y2="32" stroke="#FF3B30" strokeWidth="1.5" />
            {/* hook */}
            <line x1="-8" y1="20" x2="0" y2="20" stroke="#666" strokeWidth="1.5" />
            {/* right side: pulled by string */}
            <line x1="100" y1="20" x2="120" y2="20" stroke="#8B5A2B" strokeWidth="1.5" />
          </g>

          {/* pulling arrow */}
          {isPulling && (
            <g transform={`translate(${320 + position}, 220)`}>
              <g transform="translate(-10, 2)">
                <line x1="0" y1="0" x2="16" y2="0" stroke="#FF6B3D" strokeWidth="1.5" />
                <polygon points="16,0 12,-4 12,4" fill="#FF6B3D" />
              </g>
              <text x="10" y="-6" fill="#FF6B3D" fontSize="12" fontWeight="600">F</text>
            </g>
          )}

          {/* force display */}
          <g transform="translate(450, 60)">
            <rect x="0" y="0" width="140" height="70" rx="6" fill="#141416" stroke="#333" strokeWidth="1" />
            <text x="10" y="22" fill="#8FA3BD" fontSize="10">弹簧测力计示数</text>
            <text x="10" y="50" fill={catColor} fontSize="20" fontWeight="bold" fontFamily="JetBrains Mono, monospace">
              {pullForce.toFixed(2)} N
            </text>
          </g>

          <text x="60" y="60" fill="#5E3A18" fontSize="14" fontWeight="bold">
            探究滑动摩擦力的影响因素
          </text>
          <text x="60" y="82" fill="#888" fontSize="11">
            接触面：{surf.name}（μ = {surf.μ}）
          </text>
        </svg>
      </div>
    </ExperimentShell>
  );
}

// ---------- 4. Inertia / Newton First Law ----------
function InertiaExp({ category, name, catColor }) {
  const [surface, setSurface] = useState('wood'); // towel, cotton, wood, glass (friction decreasing)
  const [isRunning, setIsRunning] = useState(false);
  const [carPos, setCarPos] = useState(0); // 0 at top of ramp, increases to right
  const [carVel, setCarVel] = useState(0);
  const [phase, setPhase] = useState('ramp'); // ramp, flat, stopped
  const rafRef = useRef();

  const surfaces = {
    towel:  { name: '毛巾',   μ: 0.4,  color: '#D4A853', dist: 30 },
    cotton: { name: '棉布',   μ: 0.2,  color: '#E8D4B8', dist: 60 },
    wood:   { name: '木板',   μ: 0.1,  color: '#A97442', dist: 120 },
    glass:  { name: '玻璃',   μ: 0.03, color: '#c0d8f0', dist: 250 },
  };
  const surf = surfaces[surface];

  const canvasW = 680, canvasH = 380;
  const rampAngle = 30; // degrees
  const rampLen = 150;
  const rampTopX = 100, rampTopY = 100;
  const rampBottomX = rampTopX + rampLen * Math.cos(rampAngle * Math.PI/180);
  const rampBottomY = rampTopY + rampLen * Math.sin(rampAngle * Math.PI/180);
  const flatY = rampBottomY;

  const reset = () => {
    setIsRunning(false);
    setCarPos(0);
    setCarVel(0);
    setPhase('ramp');
  };

  useEffect(() => {
    if (!isRunning) return;
    let last = performance.now();
    const tick = (now) => {
      const dt = (now - last) / 1000;
      last = now;
      const dtScaled = dt * 2;

      if (phase === 'ramp') {
        const a = 9.8 * Math.sin(rampAngle * Math.PI/180); // acceleration down ramp
        setCarVel(v => v + a * dtScaled * 10); // scaled
        setCarPos(p => {
          const np = p + carVel * dtScaled;
          if (np >= rampLen) {
            setPhase('flat');
            return rampLen;
          }
          return np;
        });
      } else if (phase === 'flat') {
        // decelerate due to friction
        const decel = surf.μ * 9.8 * 10;
        setCarVel(v => {
          const nv = v - decel * dtScaled;
          if (nv <= 0) {
            setPhase('stopped');
            return 0;
          }
          return nv;
        });
        setCarPos(p => p + carVel * dtScaled);
      }

      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [isRunning, phase, carVel, surf.μ]);

  const distance = phase === 'ramp' ? 0 : carPos - rampLen;

  // car position in screen coords
  let carX, carY, carRot;
  if (phase === 'ramp') {
    const t = carPos / rampLen;
    carX = rampTopX + carPos * Math.cos(rampAngle * Math.PI/180);
    carY = rampTopY + carPos * Math.sin(rampAngle * Math.PI/180) - 12;
    carRot = rampAngle;
  } else {
    carX = rampBottomX + (carPos - rampLen);
    carY = flatY - 12;
    carRot = 0;
  }

  const panelTabs = [
    { id: 'controls', label: '操作' },
    { id: 'data', label: '数据/结论' },
    { id: 'principle', label: '原理' },
  ];

  const panelContent = (tab) => {
    if (tab === 'controls') return (
      <>
        <div className="panel-section">
          <div className="panel-section-title">水平表面</div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
            {Object.entries(surfaces).map(([k, v]) => (
              <button key={k} className={`btn sm ${surface === k ? 'primary' : ''}`}
                onClick={() => { setSurface(k); reset(); }}>
                {v.name}
              </button>
            ))}
          </div>
        </div>
        <div className="panel-section">
          <div className="panel-section-title">实验操作</div>
          <button className="btn primary" style={{ width: '100%', justifyContent: 'center' }}
            onClick={() => { reset(); setTimeout(() => setIsRunning(true), 100); }}>
            ▶ 释放小车
          </button>
          <button className="btn" style={{ width: '100%', justifyContent: 'center', marginTop: 8 }}
            onClick={reset}>
            ↺ 重置
          </button>
        </div>
        <div className="panel-section">
          <div style={{ fontSize: 12, color: 'var(--text-dim)' }}>
            提示：依次从毛巾、棉布到木板，观察小车滑行距离变化
          </div>
        </div>
      </>
    );
    if (tab === 'data') return (
      <>
        <div className="meter-row">
          <MeterCard label="水平面" value={surf.name} unit="" sub={`μ ≈ ${surf.μ}`} />
          <MeterCard label="滑行距离" value={distance.toFixed(0)} unit=" 格" sub={phase === 'stopped' ? '已停止' : '滑行中'} />
        </div>
        <FormulaBox
          expr="阻力越小，滑行距离越远"
          subs={[
            `表面越光滑 → 阻力越小 → 速度减小得越慢`,
            `表面越粗糙 → 阻力越大 → 速度减小得越快`,
            `如果表面绝对光滑（阻力为0），小车将以恒定速度永远运动下去`,
            `<strong style="color:${catColor}">实验方法：控制变量法</strong>`,
            `每次从斜面同一高度释放，保证初速度相同`,
          ]}
        />
        <div className="explain-box">
          <h4>对比实验</h4>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12 }}>
            {Object.entries(surfaces).map(([k, v]) => (
              <div key={k} style={{ display: 'flex', justifyContent: 'space-between', color: k === surface ? catColor : 'var(--text-dim)' }}>
                <span>{v.name}</span>
                <span style={{ fontFamily: 'JetBrains Mono, monospace' }}>
                  {k === surface ? (phase === 'stopped' ? distance.toFixed(0) : '...') : v.dist} 格
                </span>
              </div>
            ))}
          </div>
        </div>
      </>
    );
    if (tab === 'principle') return (
      <ExplainBox title="牛顿第一定律">
        <p><strong>实验推理：</strong></p>
        <p>• 阻力越小 → 小车速度减小越慢 → 滑行越远</p>
        <p>• 如果水平面绝对光滑（阻力为0）→ 小车匀速直线运动</p>
        <p style={{marginTop:6}}><strong>牛顿第一定律：</strong>一切物体在没有受到力的作用时，总保持静止状态或匀速直线运动状态。</p>
        <p style={{marginTop:6}}><strong>惯性：</strong>物体保持运动状态不变的性质。一切物体都有惯性。</p>
        <p style={{marginTop:6}}>牛顿第一定律又叫惯性定律，是在实验基础上推理得出的。</p>
      </ExplainBox>
    );
  };

  return (
    <ExperimentShell category={category} name={name} catColor={catColor}
      panelTabs={panelTabs} panelContent={panelContent}>
      <div className="sim-canvas-wrap">
        <svg width={canvasW} height={canvasH} className="sim-canvas">
          {/* table */}
          <rect x="0" y={flatY + 20} width={canvasW} height={canvasH - flatY - 20} fill="#A97442" />

          {/* incline / ramp */}
          <polygon
            points={`${rampTopX},${rampTopY} ${rampBottomX},${rampBottomY} ${rampTopX},${rampBottomY}`}
            fill="#B87333" stroke="#5E3A18" strokeWidth="2" />
          {/* ramp surface lines */}
          {Array.from({length: 6}, (_, i) => (
            <line key={i}
              x1={rampTopX + i * 20 * Math.cos(rampAngle*Math.PI/180)}
              y1={rampTopY + i * 20 * Math.sin(rampAngle*Math.PI/180)}
              x2={rampTopX + (i*20+10) * Math.cos(rampAngle*Math.PI/180)}
              y2={rampTopY + (i*20+10) * Math.sin(rampAngle*Math.PI/180) + 4}
              stroke="#5E3A18" strokeWidth="0.5" opacity="0.5" />
          ))}

          {/* flat surface */}
          <rect x={rampBottomX - 5} y={flatY} width={canvasW - rampBottomX + 5} height="20"
            fill={surf.color} />
          {/* surface texture */}
          {surface === 'towel' && Array.from({length: 50}, (_, i) => (
            <circle key={i} cx={rampBottomX + i * 10} cy={flatY + 10} r="2" fill="#8B6914" opacity="0.4" />
          ))}

          {/* distance markers */}
          {[0, 50, 100, 150, 200, 250].map(d => (
            <g key={d}>
              <line x1={rampBottomX + d} y1={flatY + 20}
                x2={rampBottomX + d} y2={flatY + 28}
                stroke="#5E3A18" strokeWidth="1" />
              <text x={rampBottomX + d} y={flatY + 40} textAnchor="middle" fontSize="9" fill="#5E3A18">
                {d}
              </text>
            </g>
          ))}

          {/* car */}
          <g transform={`translate(${carX}, ${carY}) rotate(${carRot})`}>
            <rect x="-15" y="-10" width="30" height="14" fill={catColor} stroke="#333" strokeWidth="1" rx="2" />
            <rect x="-10" y="-16" width="20" height="7" fill={catColor} stroke="#333" strokeWidth="1" rx="1" />
            <circle cx="-9" cy="5" r="5" fill="#333" />
            <circle cx="9" cy="5" r="5" fill="#333" />
          </g>

          {/* start line */}
          <line x1={rampTopX} y1={rampTopY - 5} x2={rampTopX} y2={rampTopY + 20}
            stroke="#FF3B30" strokeWidth="2" strokeDasharray="4,2" />
          <text x={rampTopX - 30} y={rampTopY + 40} fill="#FF3B30" fontSize="10">起点</text>

          {/* labels */}
          <text x="30" y="30" fill="#5E3A18" fontSize="14" fontWeight="bold">
            阻力对物体运动的影响
          </text>
          <text x="30" y="52" fill="#888" fontSize="11">
            表面：{surf.name}
          </text>
          <text x={rampBottomX + distance} y={flatY - 5} fill={catColor} fontSize="11" textAnchor="middle" fontWeight="bold">
            {phase === 'stopped' ? `停在 ${distance.toFixed(0)} 格处` : ''}
          </text>
        </svg>
      </div>
    </ExperimentShell>
  );
}

// ---------- 5. Buoyancy / Archimedes ----------
function BuoyancyExp({ category, name, catColor }) {
  const [density, setDensity] = useState(1.0); // g/cm³ of liquid
  const [objectDensity, setObjectDensity] = useState(0.8); // g/cm³
  const [objectVol, setObjectVol] = useState(100); // cm³
  const [submergeRatio, setSubmergeRatio] = useState(0.5); // 0 to 1
  const [isFloating, setIsFloating] = useState(true);

  const g = 9.8;
  const mass = objectDensity * objectVol; // g
  const weight = (mass / 1000) * g; // N
  const vSubmerged = objectVol * submergeRatio; // cm³
  const buoyancy = (density / 1000) * g * (vSubmerged / 1000000) * 1000; // F = ρgV, kluge
  // Actually: ρ in g/cm³, V in cm³, g in m/s²
  // ρ(kg/m³) = ρ(g/cm³) * 1000
  // V(m³) = V(cm³) * 1e-6
  // F = ρ*g*V = d*1000 * 9.8 * v*1e-6 = d*v*9.8*1e-3
  const Fbuoy = density * vSubmerged * g * 0.001; // N

  const tension = weight - Fbuoy; // N (positive = spring pulls up)

  // Auto-float: if object less dense than liquid, floats naturally
  const naturalFloatRatio = objectDensity / density; // how much submerged when floating
  const isFloatingState = objectDensity < density;

  const canvasW = 620, canvasH = 440;
  const waterLevel = 260;
  const beakerTop = 100;
  const beakerX = 180, beakerW = 260;

  // Object position: when fully submerged, top at water level - height
  const objH = objectVol / 20; // approximate height, width=20
  const objW = 20;
  const objX = beakerX + beakerW/2 - objW/2;
  // when submergeRatio = 1, object fully under water
  // when submergeRatio = 0, object just touching surface
  let objY;
  if (isFloating && isFloatingState) {
    const r = Math.min(1, naturalFloatRatio);
    objY = waterLevel - objH * r;
  } else {
    objY = waterLevel - objH * submergeRatio;
  }

  const panelTabs = [
    { id: 'controls', label: '操作' },
    { id: 'data', label: '数据/公式' },
    { id: 'principle', label: '原理' },
  ];

  const panelContent = (tab) => {
    if (tab === 'controls') return (
      <>
        <div className="panel-section">
          <div className="panel-section-title">液体</div>
          <ControlSlider label="液体密度" value={density} onChange={setDensity} min={0.8} max={2} step={0.01} unit=" g/cm³" />
          <div className="btn-row">
            <button className="btn sm" onClick={() => setDensity(1.0)}>水 1.0</button>
            <button className="btn sm" onClick={() => setDensity(0.8)}>酒精 0.8</button>
            <button className="btn sm" onClick={() => setDensity(1.2)}>盐水 1.2</button>
          </div>
        </div>
        <div className="panel-section">
          <div className="panel-section-title">物体</div>
          <ControlSlider label="物体密度" value={objectDensity} onChange={setObjectDensity} min={0.3} max={2.5} step={0.05} unit=" g/cm³" />
          <ControlSlider label="物体体积" value={objectVol} onChange={setObjectVol} min={40} max={200} step={10} unit=" cm³" />
        </div>
        <div className="panel-section">
          <div className="panel-section-title">浸入程度</div>
          <ControlSlider label="浸入比例" value={submergeRatio} onChange={setSubmergeRatio} min={0} max={1} step={0.01} unit="" />
          <ControlToggle label="自由漂浮" checked={isFloating} onChange={setIsFloating} />
        </div>
      </>
    );
    if (tab === 'data') return (
      <>
        <div className="meter-row">
          <MeterCard label="物重 G" value={weight.toFixed(2)} unit=" N" sub={`m=${mass.toFixed(0)}g`} />
          <MeterCard label="浮力 F浮" value={Fbuoy.toFixed(3)} unit=" N" />
        </div>
        <div className="meter-row">
          <MeterCard label="弹簧测力计" value={tension.toFixed(3)} unit=" N"
            sub={tension <= 0.01 ? '漂浮' : '拉力'} />
          <MeterCard label="V排" value={vSubmerged.toFixed(0)} unit=" cm³"
            sub={`${(submergeRatio*100).toFixed(0)}% 浸入`} />
        </div>
        <FormulaBox
          expr="F浮 = ρ液 · g · V排"
          subs={[
            `液体密度 ρ液 = <span class="val">${density} g/cm³</span>`,
            `排开液体体积 V排 = <span class="val">${vSubmerged.toFixed(0)} cm³</span>`,
            `F浮 = ${density} × ${(vSubmerged/1000).toFixed(3)} × ${g} = <span class="val">${Fbuoy.toFixed(3)} N</span>`,
            isFloating && isFloatingState
              ? `<span style="color:#111114">漂浮：F浮 = G = ${weight.toFixed(2)} N，V排/V物 = ρ物/ρ液 = ${(objectDensity/density*100).toFixed(1)}%</span>`
              : (Fbuoy >= weight
                ? `<span style="color:#111114">上浮中（F浮 > G）</span>`
                : `<span style="color:#111114">下沉中（F浮 < G）</span>`),
          ]}
        />
      </>
    );
    if (tab === 'principle') return (
      <ExplainBox title="浮力 · 阿基米德原理">
        <p><strong>浮力：</strong>浸在液体中的物体受到液体向上托的力。</p>
        <p style={{marginTop:6}}><strong>方向：</strong>竖直向上。</p>
        <p style={{marginTop:6}}><strong>阿基米德原理：</strong>浸在液体中的物体所受浮力，大小等于它排开的液体所受的重力。</p>
        <p>F<sub>浮</sub> = G<sub>排</sub> = ρ<sub>液</sub>·g·V<sub>排</sub></p>
        <p style={{marginTop:6}}><strong>浮沉条件：</strong></p>
        <p>• F<sub>浮</sub> {'>'} G → 上浮 → 最终漂浮（F<sub>浮</sub> = G）</p>
        <p>• F<sub>浮</sub> = G → 悬浮</p>
        <p>• F<sub>浮</sub> {'<'} G → 下沉 → 最终沉底</p>
        <p style={{marginTop:6}}>阿基米德原理也适用于气体。</p>
      </ExplainBox>
    );
  };

  const liquidColor = density < 0.9 ? '#E8D4B8' : density < 1.1 ? '#66CCFF' : '#4DB8FF';

  // displaced water level rise
  const waterRise = vSubmerged / (beakerW - 10) * 10; // approximate rise
  const currentWaterLevel = waterLevel - waterRise;

  return (
    <ExperimentShell category={category} name={name} catColor={catColor}
      panelTabs={panelTabs} panelContent={panelContent}>
      <div className="sim-canvas-wrap">
        <svg width={canvasW} height={canvasH} className="sim-canvas">
          {/* table */}
          <rect x="0" y={canvasH - 30} width={canvasW} height="30" fill="#A97442" />

          {/* spring dynamometer */}
          <g transform={`translate(${beakerX + beakerW/2 - 25}, 30)`}>
            <line x1="25" y1="0" x2="25" y2="10" stroke="#666" strokeWidth="2" />
            <rect x="0" y="10" width="50" height="100" rx="4" fill="#f5f0e0" stroke="#8B6914" strokeWidth="1.5" />
            {/* scale */}
            {Array.from({length: 11}, (_, i) => (
              <text key={i} x="28" y={25 + i * 8} fontSize="8" fill="#666">{i * 0.5}</text>
            ))}
            {/* spring and pointer */}
            <g transform="translate(25, 20)">
              <path d={generateSpringPath(0, 0, 0, 50 + tension * 30, 8, 8)}
                fill="none" stroke="#D4A853" strokeWidth="1.5" />
              <line x1="-18" y1={50 + tension * 30} x2="18" y2={50 + tension * 30}
                stroke="#FF3B30" strokeWidth="1.5" />
            </g>
            {/* hook */}
            <path d="M 25 110 Q 31 110 31 116 Q 31 122 25 122" fill="none" stroke="#666" strokeWidth="1.5" />
            <text x="25" y="-5" textAnchor="middle" fill="#666" fontSize="9">弹簧测力计</text>
          </g>

          {/* string from dynamometer to object */}
          {tension > 0.01 && (
            <line x1={beakerX + beakerW/2} y1="152"
              x2={beakerX + beakerW/2} y2={objY - 5}
              stroke="#8B5A2B" strokeWidth="1" />
          )}

          {/* beaker */}
          <g>
            <path d={`M ${beakerX} ${beakerTop} L ${beakerX} 390 Q ${beakerX} 400 ${beakerX + 10} 400 L ${beakerX + beakerW - 10} 400 Q ${beakerX + beakerW} 400 ${beakerX + beakerW} 390 L ${beakerX + beakerW} ${beakerTop} Z`}
              fill="rgba(200,230,255,0.2)" stroke="#666" strokeWidth="2" />

            {/* liquid */}
            <path d={`M ${beakerX + 4} 386 Q ${beakerX + 4} 400 ${beakerX + 10} 400 L ${beakerX + beakerW - 10} 400 Q ${beakerX + beakerW - 4} 400 ${beakerX + beakerW - 4} 386 L ${beakerX + beakerW - 4} ${currentWaterLevel} L ${beakerX + 4} ${currentWaterLevel} Z`}
              fill={liquidColor} opacity="0.6" />

            {/* scale marks */}
            {[0, 50, 100, 150, 200, 250].map(v => {
              const y = 380 - v * 1.2;
              return (
                <g key={v}>
                  <line x1={beakerX + beakerW - 18} y1={y} x2={beakerX + beakerW - 6} y2={y}
                    stroke="#666" strokeWidth="0.8" />
                  <text x={beakerX + beakerW - 22} y={y + 3} fontSize="9" fill="#666" textAnchor="end">{v}</text>
                </g>
              );
            })}
          </g>

          {/* object (cylinder) */}
          <g>
            <rect x={objX} y={objY} width={objW} height={objH}
              fill={catColor} stroke="#333" strokeWidth="1.5" rx="2"
              opacity="0.9" />
            <text x={objX + objW/2} y={objY + objH/2 + 4} textAnchor="middle" fill="#fff" fontSize="9" fontWeight="bold">
              {objectVol}cm³
            </text>
            {/* water line on object */}
            {objY < currentWaterLevel && objY + objH > currentWaterLevel && (
              <line x1={objX} y1={currentWaterLevel} x2={objX + objW} y2={currentWaterLevel}
                stroke="#fff" strokeWidth="1" strokeDasharray="3,2" opacity="0.7" />
            )}
          </g>

          {/* displaced water label */}
          <text x={beakerX + 20} y={currentWaterLevel - 5} fill="#fff" fontSize="10" fontWeight="bold">
            水面↑{waterRise.toFixed(1)}
          </text>

          {/* state label */}
          <g transform="translate(20, 60)">
            <rect x="0" y="0" width="130" height="60" rx="6" fill="#141416" stroke="#333" strokeWidth="1" />
            <text x="10" y="20" fill="#8FA3BD" fontSize="10">浮力 F浮</text>
            <text x="10" y="42" fill={catColor} fontSize="16" fontWeight="bold" fontFamily="JetBrains Mono, monospace">
              {Fbuoy.toFixed(3)} N
            </text>
          </g>
        </svg>
      </div>
    </ExperimentShell>
  );
}

// ---------- 6. Liquid Pressure ----------
function LiquidPressureExp({ category, name, catColor }) {
  const [density, setDensity] = useState(1.0); // g/cm³
  const [depth, setDepth] = useState(20); // cm
  const g = 9.8;

  // p = ρgh
  const pressure = density * 1000 * g * (depth / 100); // Pa
  const pressureKPa = pressure / 1000;

  // U-tube manometer height difference
  // pressure = ρ_mano * g * Δh
  const manoDensity = 13.6; // mercury g/cm³
  const deltaH = pressure / (manoDensity * 1000 * g) * 100; // cm

  const canvasW = 620, canvasH = 440;
  const tankX = 100, tankW = 280, tankTop = 80, tankBottom = 380;

  const panelTabs = [
    { id: 'controls', label: '操作' },
    { id: 'data', label: '数据/公式' },
    { id: 'principle', label: '原理' },
  ];

  const panelContent = (tab) => {
    if (tab === 'controls') return (
      <>
        <div className="panel-section">
          <div className="panel-section-title">参数调节</div>
          <ControlSlider label="液体密度" value={density} onChange={setDensity} min={0.8} max={2} step={0.05} unit=" g/cm³" />
          <ControlSlider label="深度 h" value={depth} onChange={setDepth} min={5} max={28} step={1} unit=" cm" />
        </div>
        <div className="panel-section">
          <div className="panel-section-title">快捷设置</div>
          <div className="btn-row">
            <button className="btn sm" onClick={() => setDensity(1.0)}>水</button>
            <button className="btn sm" onClick={() => setDensity(0.8)}>酒精</button>
            <button className="btn sm" onClick={() => setDensity(1.2)}>盐水</button>
          </div>
        </div>
      </>
    );
    if (tab === 'data') return (
      <>
        <div className="meter-row">
          <MeterCard label="液体压强 p" value={pressureKPa.toFixed(2)} unit=" kPa"
            sub={`${pressure.toFixed(0)} Pa`} />
          <MeterCard label="U形管液面差" value={deltaH.toFixed(2)} unit=" cm" />
        </div>
        <FormulaBox
          expr="p = ρ · g · h"
          subs={[
            `液体密度 ρ = <span class="val">${density} g/cm³</span> = ${density*1000} kg/m³`,
            `深度 h = <span class="val">${depth} cm</span> = ${depth/100} m`,
            `g = 9.8 N/kg`,
            `p = ${density*1000} × 9.8 × ${depth/100} = <span class="val">${pressure.toFixed(0)} Pa</span>`,
            `液体内部压强随深度增加而增大`,
            `同一深度，向各方向压强相等`,
          ]}
        />
      </>
    );
    if (tab === 'principle') return (
      <ExplainBox title="液体的压强">
        <p><strong>产生原因：</strong>液体受重力且具有流动性。</p>
        <p style={{marginTop:6}}><strong>特点：</strong></p>
        <p>• 液体内部向各个方向都有压强</p>
        <p>• 压强随深度增加而增大</p>
        <p>• 同一深度，向各方向压强相等</p>
        <p>• 液体密度越大，压强越大</p>
        <p style={{marginTop:6}}><strong>公式：</strong>p = ρgh</p>
        <p style={{marginTop:6}}><strong>连通器：</strong>上端开口、下端连通的容器，同种液体静止时液面相平。</p>
        <p style={{marginTop:6}}>帕斯卡原理：加在密闭液体上的压强，大小不变地向各方向传递。</p>
      </ExplainBox>
    );
  };

  const probeY = tankTop + depth * 10; // scale: 1cm = 10px
  const probeX = tankX + tankW / 2;

  // U-tube manometer
  const manoX = 460, manoY = 80, manoW = 100, manoH = 300;
  const manoLeftH = 150 - deltaH * 5;
  const manoRightH = 150 + deltaH * 5;

  return (
    <ExperimentShell category={category} name={name} catColor={catColor}
      panelTabs={panelTabs} panelContent={panelContent}>
      <div className="sim-canvas-wrap">
        <svg width={canvasW} height={canvasH} className="sim-canvas">
          {/* liquid tank */}
          <g>
            <rect x={tankX} y={tankTop} width={tankW} height={tankBottom - tankTop}
              fill="rgba(200,230,255,0.3)" stroke="#666" strokeWidth="2" />

            {/* liquid */}
            <rect x={tankX + 2} y={tankTop + 2} width={tankW - 4} height={tankBottom - tankTop - 4}
              fill={density < 0.9 ? '#E8D4B8' : density < 1.1 ? '#66CCFF' : '#4DB8FF'}
              opacity="0.5" />

            {/* depth scale */}
            {[0, 5, 10, 15, 20, 25, 30].map(d => {
              const y = tankTop + 10 + d * 10;
              return (
                <g key={d}>
                  <line x1={tankX + 5} y1={y} x2={tankX + 15} y2={y} stroke="#fff" strokeWidth="1" opacity="0.7" />
                  <text x={tankX + 18} y={y + 3} fontSize="9" fill="#fff" opacity="0.8">{d}cm</text>
                </g>
              );
            })}
          </g>

          {/* pressure probe */}
          <g>
            {/* vertical rod */}
            <line x1={tankX + tankW/2} y1={tankTop - 20} x2={tankX + tankW/2} y2={probeY}
              stroke="#888" strokeWidth="2" />
            {/* probe head (facing right) */}
            <g transform={`translate(${probeX}, ${probeY})`}>
              <circle cx="0" cy="0" r="10" fill="#fff" stroke="#333" strokeWidth="1.5" />
              <circle cx="4" cy="0" r="3" fill="#FF3B30" />
              <text x="0" y="-16" textAnchor="middle" fontSize="9" fill="#333">橡皮膜</text>
            </g>

            {/* arrows showing pressure */}
            {pressure > 0 && (
              <g transform={`translate(${probeX}, ${probeY})`}>
                <line x1="10" y1="0" x2={10 + pressureKPa * 2} y2="0" stroke="#FF5C5C" strokeWidth="2" />
                <polygon points={`${10 + pressureKPa * 2},0 ${10 + pressureKPa * 2 - 5},-3 ${10 + pressureKPa * 2 - 5},3`} fill="#FF5C5C" />
              </g>
            )}
          </g>

          {/* tube connecting probe to manometer */}
          <path d={`M ${probeX + 8} ${probeY} Q ${probeX + 40} ${probeY} ${probeX + 50} ${probeY - 30} L ${probeX + 50} ${manoY + 20} L ${manoX + 10} ${manoY + 20}`}
            fill="none" stroke="#888" strokeWidth="1.5" />

          {/* U-tube manometer */}
          <g transform={`translate(${manoX}, ${manoY})`}>
            <text x="50" y="-5" textAnchor="middle" fill="#5E3A18" fontSize="12" fontWeight="bold">U形管压强计</text>
            {/* U tube */}
            <path d="M 10 20 L 10 280 Q 10 295 25 295 L 75 295 Q 90 295 90 280 L 90 20"
              fill="none" stroke="#666" strokeWidth="2" />
            {/* mercury / liquid left */}
            <rect x="12" y={manoLeftH} width="16" height={280 - manoLeftH} fill="#C0C0C0" />
            {/* mercury / liquid right */}
            <rect x="72" y={manoRightH} width="16" height={280 - manoRightH} fill="#C0C0C0" />

            {/* scale */}
            {[-6, -3, 0, 3, 6].map(d => (
              <text key={d} x="50" y={150 - d * 15} textAnchor="middle" fontSize="9" fill="#666">
                {d > 0 ? `+${d}` : d}cm
              </text>
            ))}
          </g>

          {/* reading */}
          <g transform="translate(20, 60)">
            <rect x="0" y="0" width="70" height="40" rx="4" fill="#141416" stroke="#333" strokeWidth="1" />
            <text x="35" y="16" textAnchor="middle" fill="#8FA3BD" fontSize="9">深度</text>
            <text x="35" y="32" textAnchor="middle" fill={catColor} fontSize="14" fontWeight="bold">{depth}cm</text>
          </g>
        </svg>
      </div>
    </ExperimentShell>
  );
}

// ---------- 7. Lever ----------
function LeverExp({ category, name, catColor }) {
  const [leftCount, setLeftCount] = useState(2);
  const [leftDist, setLeftDist] = useState(3); // unit: 格
  const [rightCount, setRightCount] = useState(3);
  const [rightDist, setRightDist] = useState(2);

  const leftMoment = leftCount * leftDist;
  const rightMoment = rightCount * rightDist;
  const isBalanced = leftMoment === rightMoment;
  const tilt = Math.max(-20, Math.min(20, (rightMoment - leftMoment) * 3));

  const canvasW = 650, canvasH = 400;
  const pivotX = 325, pivotY = 280;
  const leverLen = 260;
  const units = 10; // number of units each side
  const unitPx = leverLen / units;

  const panelTabs = [
    { id: 'controls', label: '操作' },
    { id: 'data', label: '数据/公式' },
    { id: 'principle', label: '原理' },
  ];

  const panelContent = (tab) => {
    if (tab === 'controls') return (
      <>
        <div className="panel-section">
          <div className="panel-section-title">左侧</div>
          <ControlSlider label="钩码数" value={leftCount} onChange={setLeftCount} min={0} max={10} step={1} unit=" 个" />
          <ControlSlider label="力臂（格）" value={leftDist} onChange={setLeftDist} min={1} max={8} step={1} unit=" 格" />
        </div>
        <div className="panel-section">
          <div className="panel-section-title">右侧</div>
          <ControlSlider label="钩码数" value={rightCount} onChange={setRightCount} min={0} max={10} step={1} unit=" 个" />
          <ControlSlider label="力臂（格）" value={rightDist} onChange={setRightDist} min={1} max={8} step={1} unit=" 格" />
        </div>
        <div className="panel-section">
          <div className="panel-section-title">平衡状态</div>
          <div style={{
            padding: '10px',
            borderRadius: '6px',
            textAlign: 'center',
            background: isBalanced ? 'rgba(82,199,149,0.15)' : 'rgba(255,92,92,0.1)',
            color: isBalanced ? 'var(--success)' : 'var(--danger)',
            fontWeight: 'bold',
            fontSize: 14,
          }}>
            {isBalanced ? '杠杆平衡' : (tilt > 0 ? '右侧下沉' : '左侧下沉')}
          </div>
        </div>
      </>
    );
    if (tab === 'data') return (
      <>
        <div className="meter-row">
          <MeterCard label="F₁·L₁（左）" value={leftMoment} unit="" sub={`${leftCount}×${leftDist}`} />
          <MeterCard label="F₂·L₂（右）" value={rightMoment} unit="" sub={`${rightCount}×${rightDist}`} />
        </div>
        <FormulaBox
          expr="F₁ · L₁ = F₂ · L₂"
          subs={[
            `左侧：F₁ = ${leftCount} 个钩码，L₁ = ${leftDist} 格`,
            `右侧：F₂ = ${rightCount} 个钩码，L₂ = ${rightDist} 格`,
            `F₁·L₁ = <span class="val">${leftMoment}</span>`,
            `F₂·L₂ = <span class="val">${rightMoment}</span>`,
            isBalanced
              ? `<span style="color:#111114">F₁L₁ = F₂L₂，杠杆平衡</span>`
              : `<span style="color:#111114">不相等，杠杆不平衡</span>`,
          ]}
        />
      </>
    );
    if (tab === 'principle') return (
      <ExplainBox title="杠杆平衡条件">
        <p><strong>杠杆：</strong>在力的作用下能绕固定点转动的硬棒。</p>
        <p style={{marginTop:6}}><strong>五要素：</strong>支点、动力、阻力、动力臂、阻力臂。</p>
        <p style={{marginTop:6}}><strong>力臂：</strong>支点到力的作用线的距离。</p>
        <p style={{marginTop:6}}><strong>杠杆平衡条件：</strong></p>
        <p style={{textAlign: 'center', fontSize: 16, fontWeight: 'bold', color: 'var(--accent)'}}>
          F₁ · L₁ = F₂ · L₂
        </p>
        <p style={{marginTop:6}}><strong>杠杆分类：</strong></p>
        <p>• 省力杠杆：L₁ {'>'} L₂（撬棍、钳子）</p>
        <p>• 费力杠杆：L₁ {'<'} L₂（镊子、筷子）</p>
        <p>• 等臂杠杆：L₁ = L₂（天平）</p>
      </ExplainBox>
    );
  };

  return (
    <ExperimentShell category={category} name={name} catColor={catColor}
      panelTabs={panelTabs} panelContent={panelContent}>
      <div className="sim-canvas-wrap">
        <svg width={canvasW} height={canvasH} className="sim-canvas">
          {/* base */}
          <rect x={pivotX - 60} y={pivotY + 40} width="120" height="20" fill="#8B5A2B" rx="3" />
          <polygon points={`${pivotX-15},${pivotY+40} ${pivotX+15},${pivotY+40} ${pivotX},${pivotY+10}`}
            fill="#8B5A2B" />

          {/* lever */}
          <g transform={`rotate(${tilt}, ${pivotX}, ${pivotY + 10})`}>
            <rect x={pivotX - leverLen} y={pivotY} width={leverLen * 2} height="12"
              fill="#A97442" stroke="#5E3A18" strokeWidth="1.5" rx="2" />

            {/* unit marks */}
            {Array.from({length: units * 2 + 1}, (_, i) => {
              const idx = i - units;
              const x = pivotX + idx * unitPx;
              const isMajor = Math.abs(idx) % 2 === 0;
              return (
                <g key={i}>
                  <line x1={x} y1={pivotY - (isMajor ? 8 : 4)} x2={x} y2={pivotY}
                    stroke="#5E3A18" strokeWidth="1" />
                  {idx !== 0 && isMajor && (
                    <text x={x} y={pivotY + 24} textAnchor="middle" fontSize="9" fill="#5E3A18">
                      {Math.abs(idx)}
                    </text>
                  )}
                </g>
              );
            })}

            {/* pivot mark */}
            <circle cx={pivotX} cy={pivotY + 6} r="6" fill="#5E3A18" />

            {/* left weights */}
            <g transform={`translate(${pivotX - leftDist * unitPx}, ${pivotY + 12})`}>
              {Array.from({length: leftCount}, (_, i) => (
                <g key={i} transform={`translate(0, ${i * 14})`}>
                  <rect x="-10" y="0" width="20" height="12" fill={catColor} stroke="#333" strokeWidth="1" rx="2" />
                  <rect x="-2" y="-4" width="4" height="4" fill="#333" />
                </g>
              ))}
              {leftCount > 0 && (
                <text x="0" y={leftCount * 14 + 16} textAnchor="middle" fontSize="9" fill="#5E3A18">
                  F₁ = {leftCount}
                </text>
              )}
            </g>

            {/* right weights */}
            <g transform={`translate(${pivotX + rightDist * unitPx}, ${pivotY + 12})`}>
              {Array.from({length: rightCount}, (_, i) => (
                <g key={i} transform={`translate(0, ${i * 14})`}>
                  <rect x="-10" y="0" width="20" height="12" fill={catColor} stroke="#333" strokeWidth="1" rx="2" />
                  <rect x="-2" y="-4" width="4" height="4" fill="#333" />
                </g>
              ))}
              {rightCount > 0 && (
                <text x="0" y={rightCount * 14 + 16} textAnchor="middle" fontSize="9" fill="#5E3A18">
                  F₂ = {rightCount}
                </text>
              )}
            </g>

            {/* left arm label */}
            <text x={pivotX - leftDist * unitPx / 2} y={pivotY - 10} textAnchor="middle"
              fontSize="10" fill="#FF5C5C" fontWeight="bold">
              L₁={leftDist}
            </text>
            <text x={pivotX + rightDist * unitPx / 2} y={pivotY - 10} textAnchor="middle"
              fontSize="10" fill="#52C795" fontWeight="bold">
              L₂={rightDist}
            </text>
          </g>

          <text x={canvasW / 2} y="40" textAnchor="middle" fill="#5E3A18" fontSize="14" fontWeight="bold">
            探究杠杆的平衡条件
          </text>
        </svg>
      </div>
    </ExperimentShell>
  );
}

// ---------- 8. Pulley ----------
function PulleyExp({ category, name, catColor }) {
  const [mode, setMode] = useState('fixed'); // fixed, movable, system
  const [weight, setWeight] = useState(100); // N (simulated weight)
  const [pullForce, setPullForce] = useState(0);
  const [height, setHeight] = useState(0);
  const [isPulling, setIsPulling] = useState(false);
  const rafRef = useRef();

  // Calculate mechanical advantage
  let MA = 1;
  let tension = weight;
  if (mode === 'fixed') { MA = 1; tension = weight; }
  if (mode === 'movable') { MA = 2; tension = weight / 2; }
  if (mode === 'system') { MA = 4; tension = weight / 4; }

  useEffect(() => {
    if (!isPulling) {
      setPullForce(f => Math.max(0, f - 2));
      return;
    }
    const id = setInterval(() => {
      setPullForce(f => Math.min(tension * 1.1, f + 1));
      if (pullForce >= tension * 0.9) {
        setHeight(h => Math.min(100, h + 0.5));
      }
    }, 30);
    return () => clearInterval(id);
  }, [isPulling, tension, pullForce]);

  const reset = () => {
    setIsPulling(false);
    setHeight(0);
    setPullForce(0);
  };

  const canvasW = 580, canvasH = 420;
  const ceilingY = 50;
  const centerX = 280;

  const panelTabs = [
    { id: 'controls', label: '操作' },
    { id: 'data', label: '数据/公式' },
    { id: 'principle', label: '原理' },
  ];

  const panelContent = (tab) => {
    if (tab === 'controls') return (
      <>
        <div className="panel-section">
          <div className="panel-section-title">滑轮类型</div>
          <div className="btn-row">
            <button className={`btn ${mode === 'fixed' ? 'primary' : ''}`} style={{flex:1}}
              onClick={() => { setMode('fixed'); reset(); }}>定滑轮</button>
            <button className={`btn ${mode === 'movable' ? 'primary' : ''}`} style={{flex:1}}
              onClick={() => { setMode('movable'); reset(); }}>动滑轮</button>
          </div>
          <button className={`btn ${mode === 'system' ? 'primary' : ''}`} style={{width: '100%', marginTop: 6}}
            onClick={() => { setMode('system'); reset(); }}>
            滑轮组（n=4）
          </button>
        </div>
        <div className="panel-section">
          <div className="panel-section-title">物重</div>
          <ControlSlider label="G" value={weight} onChange={v => { setWeight(v); reset(); }} min={20} max={200} step={10} unit=" N" />
        </div>
        <div className="panel-section">
          <div className="panel-section-title">操作</div>
          <button className={`btn ${isPulling ? 'primary' : ''}`} style={{ width: '100%', justifyContent: 'center' }}
            onClick={() => setIsPulling(!isPulling)}>
            <Icon name={isPulling ? 'Pause' : 'ArrowRight'} size={14} />
            <span>{isPulling ? '停止拉动' : '向下拉绳'}</span>
          </button>
          <button className="btn" style={{ width: '100%', justifyContent: 'center', marginTop: 8 }}
            onClick={reset}>
            ↺ 重置
          </button>
        </div>
      </>
    );
    if (tab === 'data') return (
      <>
        <div className="meter-row">
          <MeterCard label="物重 G" value={weight} unit=" N" />
          <MeterCard label="拉力 F" value={pullForce.toFixed(1)} unit=" N"
            sub={`理论 F=${tension.toFixed(1)}N`} />
        </div>
        <div className="meter-row">
          <MeterCard label="物体上升" value={height.toFixed(0)} unit=" cm" />
          <MeterCard label="机械效益" value={MA} unit="倍" sub={`绳子段数 n=${MA}`} />
        </div>
        <FormulaBox
          expr={mode === 'fixed' ? 'F = G（不省力，改变方向）' : mode === 'movable' ? 'F = G/2（省一半力）' : 'F = G/n（n段绳子承担）'}
          subs={[
            mode === 'fixed'
              ? `定滑轮：等臂杠杆，F = G = <span class="val">${weight} N</span>`
              : mode === 'movable'
                ? `动滑轮：动力臂是阻力臂2倍，F = G/2 = <span class="val">${tension} N</span>`
                : `滑轮组：n=${MA}段绳子承担物重，F = G/n = <span class="val">${tension} N</span>`,
            `物体上升 h = ${height.toFixed(0)} cm`,
            `绳子自由端移动 s = ${(height * MA).toFixed(0)} cm = n·h`,
            mode === 'fixed'
              ? `特点：不省力，但可以改变力的方向`
              : `特点：省${MA-1 > 0 ? (MA-1) + '/' + MA : ''}力，费距离${MA}倍`,
          ]}
        />
      </>
    );
    if (tab === 'principle') return (
      <ExplainBox title="滑轮">
        <p><strong>定滑轮：</strong>轴固定不动的滑轮。</p>
        <p>• 实质：等臂杠杆</p>
        <p>• 特点：不省力，可改变力的方向</p>
        <p style={{marginTop:6}}><strong>动滑轮：</strong>轴随物体一起运动的滑轮。</p>
        <p>• 实质：动力臂是阻力臂2倍的杠杆</p>
        <p>• 特点：省一半力，不能改变方向，费距离</p>
        <p style={{marginTop:6}}><strong>滑轮组：</strong>定滑轮+动滑轮组合。</p>
        <p>• F = G/n（n为承担物重的绳子段数）</p>
        <p>• s = n·h</p>
        <p>• 既省力又可改变方向</p>
      </ExplainBox>
    );
  };

  return (
    <ExperimentShell category={category} name={name} catColor={catColor}
      panelTabs={panelTabs} panelContent={panelContent}>
      <div className="sim-canvas-wrap">
        <svg width={canvasW} height={canvasH} className="sim-canvas">
          {/* ceiling */}
          <rect x="0" y="0" width={canvasW} height="30" fill="#8B5A2B" />
          <line x1="0" y1="30" x2={canvasW} y2="30" stroke="#5E3A18" strokeWidth="2" />

          {mode === 'fixed' && (
            <g>
              {/* fixed pulley */}
              <line x1={centerX} y1="30" x2={centerX} y2="60" stroke="#666" strokeWidth="3" />
              <circle cx={centerX} cy="75" r="25" fill="none" stroke="#888" strokeWidth="6" />
              <circle cx={centerX} cy="75" r="6" fill="#666" />
              {/* spokes */}
              {[0, 60, 120, 180, 240, 300].map(a => (
                <line key={a} x1={centerX} y1="75"
                  x2={centerX + 20 * Math.cos(a*Math.PI/180)}
                  y2={75 + 20 * Math.sin(a*Math.PI/180)}
                  stroke="#888" strokeWidth="2" />
              ))}

              {/* rope: left side has weight, right side pulling down */}
              <line x1={centerX - 25} y1="75" x2={centerX - 25} y2={200 + height}
                stroke="#8B5A2B" strokeWidth="2" />
              <line x1={centerX + 25} y1="75" x2={centerX + 25} y2={280 - height}
                stroke="#8B5A2B" strokeWidth="2" />

              {/* weight on left */}
              <g transform={`translate(${centerX - 25}, ${200 + height})`}>
                <rect x="-20" y="0" width="40" height="50" fill={catColor} stroke="#333" strokeWidth="1.5" rx="3" />
                <text x="0" y="28" textAnchor="middle" fill="#fff" fontSize="11" fontWeight="bold">
                  {weight}N
                </text>
              </g>

              {/* pulling hand on right */}
              <g transform={`translate(${centerX + 25}, ${280 - height})`}>
                <circle cx="0" cy="0" r="12" fill="#f5d0a0" stroke="#8B5A2B" strokeWidth="1.5" />
                {isPulling && <text x="15" y="5" fill="#FF6B3D" fontSize="12">F</text>}
              </g>
            </g>
          )}

          {mode === 'movable' && (
            <g>
              {/* ceiling anchor */}
              <rect x={centerX - 40} y="30" width="20" height="10" fill="#666" />

              {/* fixed point rope left */}
              <line x1={centerX - 30} y1="40" x2={centerX - 30} y2={250 - height}
                stroke="#8B5A2B" strokeWidth="2" />

              {/* movable pulley */}
              <g transform={`translate(${centerX - 5}, ${250 - height})`}>
                <circle cx="0" cy="25" r="22" fill="none" stroke="#888" strokeWidth="5" />
                <circle cx="0" cy="25" r="5" fill="#666" />
                {/* u-shape hook */}
                <path d="M 0 47 Q 0 55 -8 55 Q -16 55 -16 47" fill="none" stroke="#666" strokeWidth="2" />
              </g>

              {/* rope going over pulley and up to right */}
              <path d={`M ${centerX - 30} 40 L ${centerX - 30} ${250 - height + 10}
                Q ${centerX - 30} ${250 - height + 35} ${centerX - 8} ${250 - height + 40}
                L ${centerX + 18} ${250 - height + 40}`}
                fill="none" stroke="#8B5A2B" strokeWidth="2" />

              {/* rope to pulling side */}
              <line x1={centerX + 18} y1={250 - height + 40} x2={centerX + 40} y2={250 - height + 40}
                stroke="#8B5A2B" strokeWidth="2" />
              <line x1={centerX + 40} y1={250 - height + 40} x2={centerX + 40} y2={330 - height}
                stroke="#8B5A2B" strokeWidth="2" />

              {/* weight */}
              <g transform={`translate(${centerX - 5}, ${295 - height})`}>
                <rect x="-22" y="0" width="44" height="50" fill={catColor} stroke="#333" strokeWidth="1.5" rx="3" />
                <text x="0" y="28" textAnchor="middle" fill="#fff" fontSize="11" fontWeight="bold">
                  G={weight}N
                </text>
              </g>

              {/* pulling hand */}
              <g transform={`translate(${centerX + 40}, ${330 - height})`}>
                <circle cx="0" cy="0" r="12" fill="#f5d0a0" stroke="#8B5A2B" strokeWidth="1.5" />
                {isPulling && <text x="15" y="5" fill="#FF6B3D" fontSize="12">F</text>}
              </g>
            </g>
          )}

          {mode === 'system' && (
            <g>
              {/* top fixed block (2 pulleys) */}
              <line x1={centerX} y1="30" x2={centerX} y2="55" stroke="#666" strokeWidth="3" />
              <rect x={centerX - 30} y="55" width="60" height="10" fill="#666" />
              {/* two fixed pulleys */}
              <circle cx={centerX - 15} cy="85" r="18" fill="none" stroke="#888" strokeWidth="5" />
              <circle cx={centerX + 15} cy="85" r="18" fill="none" stroke="#888" strokeWidth="5" />

              {/* bottom movable block (2 pulleys) */}
              <g transform={`translate(0, ${height * 2})`}>
                <rect x={centerX - 30} y={250} width="60" height="10" fill="#666" />
                <circle cx={centerX - 15} cy="280" r="18" fill="none" stroke="#888" strokeWidth="5" />
                <circle cx={centerX + 15} cy="280" r="18" fill="none" stroke="#888" strokeWidth="5" />

                {/* hook + weight */}
                <path d={`M ${centerX} 300 Q ${centerX} 312 ${centerX - 10} 312 Q ${centerX - 20} 312 ${centerX - 20} 300`}
                  fill="none" stroke="#666" strokeWidth="2" />
                <g transform={`translate(${centerX - 25}, 315)`}>
                  <rect x="0" y="0" width="50" height="50" fill={catColor} stroke="#333" strokeWidth="1.5" rx="3" />
                  <text x="25" y="28" textAnchor="middle" fill="#fff" fontSize="11" fontWeight="bold">
                    G={weight}N
                  </text>
                </g>
              </g>

              {/* 4 ropes (simplified) */}
              {[-25, -5, 5, 25].map((ox, i) => (
                <line key={i}
                  x1={centerX + ox} y1="103"
                  x2={centerX + ox} y2={250 + height * 2}
                  stroke="#8B5A2B" strokeWidth="1.5" />
              ))}

              {/* pull rope coming from right side */}
              <line x1={centerX + 33} y1="85" x2={centerX + 80} y2="85"
                stroke="#8B5A2B" strokeWidth="2" />
              <line x1={centerX + 80} y1="85" x2={centerX + 80} y2="130"
                stroke="#8B5A2B" strokeWidth="2" />
              <circle cx={centerX + 80} cy="140" r="10" fill="#f5d0a0" stroke="#8B5A2B" strokeWidth="1.5" />
              {isPulling && <text x={centerX + 95} y="144" fill="#FF6B3D" fontSize="12">F</text>}
            </g>
          )}

          {/* label */}
          <text x={centerX} y={canvasH - 15} textAnchor="middle" fill="#5E3A18" fontSize="13" fontWeight="bold">
            {mode === 'fixed' ? '定滑轮 F = G' : mode === 'movable' ? '动滑轮 F = G/2' : '滑轮组 F = G/4'}
          </text>
        </svg>
      </div>
    </ExperimentShell>
  );
}

Object.assign(window, {
  MeasurementExp,
  GravityExp,
  FrictionExp,
  InertiaExp,
  BuoyancyExp,
  LiquidPressureExp,
  LeverExp,
  PulleyExp,
});
