// Main walkie-talkie app — orchestrates state, PTT flow, and cross-tab comms.
// Mobile-first: every control lives ON the radio body. Real-time streaming PCM.

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

const CHANNELS = [
  {n:1,  name:'GUARD-1',  freq:'462.5625'},
  {n:2,  name:'TEAM-A',   freq:'462.5875'},
  {n:3,  name:'TEAM-B',   freq:'462.6125'},
  {n:4,  name:'OPS',      freq:'462.6375'},
  {n:5,  name:'LOGISTIC', freq:'462.6625'},
  {n:6,  name:'EMERG',    freq:'462.6875'},
  {n:7,  name:'OPEN-1',   freq:'462.7125'},
  {n:8,  name:'OPEN-2',   freq:'467.5625'},
  {n:9,  name:'SCOUT',    freq:'467.5875'},
  {n:10, name:'DRIVER',   freq:'467.6125'},
  {n:11, name:'EVENT',    freq:'467.6375'},
  {n:12, name:'SAFETY',   freq:'467.6625'},
  {n:13, name:'WORK',     freq:'467.6875'},
  {n:14, name:'CREW',     freq:'467.7125'},
  {n:15, name:'FAMILY',   freq:'462.5500'},
  {n:16, name:'WEATHER',  freq:'162.5500'},
];

function randomCallsign(){
  const letters = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
  const L = () => letters[Math.floor(Math.random()*letters.length)];
  const D = () => Math.floor(Math.random()*10);
  return `${L()}${D()}${L()}-${D()}${D()}${L()}`;
}

const BODY_PALETTES_LIST = [
  {key:'graphite', swatch:['oklch(22% 0.006 80)','oklch(15% 0.006 80)','oklch(72% 0.16 70)'],   body:'oklch(22% 0.006 80)',  body2:'oklch(15% 0.006 80)'},
  {key:'ranger',   swatch:['oklch(35% 0.04 130)','oklch(22% 0.03 130)','oklch(78% 0.13 90)'],   body:'oklch(35% 0.04 130)',  body2:'oklch(22% 0.03 130)'},
  {key:'rescue',   swatch:['oklch(55% 0.18 50)','oklch(38% 0.14 45)','oklch(96% 0.04 90)'],     body:'oklch(55% 0.18 50)',   body2:'oklch(38% 0.14 45)'},
  {key:'marine',   swatch:['oklch(30% 0.06 240)','oklch(18% 0.05 240)','oklch(85% 0.14 200)'],  body:'oklch(30% 0.06 240)',  body2:'oklch(18% 0.05 240)'},
  {key:'retro',    swatch:['oklch(72% 0.05 80)','oklch(55% 0.06 75)','oklch(40% 0.14 30)'],     body:'oklch(72% 0.05 80)',   body2:'oklch(55% 0.06 75)'},
];

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "bodyColor": ["oklch(22% 0.006 80)","oklch(15% 0.006 80)","oklch(72% 0.16 70)"],
  "aiPartner": false
}/*EDITMODE-END*/;

function App(){
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const palette = (() => {
    const v = t.bodyColor;
    const first = Array.isArray(v) ? v[0] : null;
    const match = BODY_PALETTES_LIST.find(p => p.swatch[0] === first);
    return match || BODY_PALETTES_LIST[0];
  })();

  const [power, setPower]     = useState(true);
  const [chIdx, setChIdx]     = useState(1);
  const [volume, setVolume]   = useState(7);
  const [squelch, setSquelch] = useState(2);
  const [battery, setBattery] = useState(94);
  const [signal, setSignal]   = useState(4);
  const [scanning, setScanning] = useState(false);
  const [monitor, setMonitor] = useState(false);
  const [muted, setMuted]     = useState(false);
  const [fxOn, setFxOn]       = useState(true);  // 拟真音效开关

  const [callsign] = useState(() => {
    let s = sessionStorage.getItem('rt808-callsign');
    if(!s){ s = randomCallsign(); sessionStorage.setItem('rt808-callsign', s); }
    return s;
  });

  const [pttDown, setPttDown] = useState(false);
  const [txing, setTxing]     = useState(false);
  const [rxing, setRxing]     = useState(false);
  const [rxFrom, setRxFrom]   = useState(null);
  const [vuLevel, setVuLevel] = useState(0);
  const [lastLine, setLastLine] = useState('Standby · channel quiet');
  const [log, setLog] = useState([
    {t: Date.now()-90000, kind:'sys', text:'RT-808 online · firmware v2.4.1'},
  ]);

  const ch = CHANNELS[chIdx-1];
  const fxOnRef = useRef(fxOn); fxOnRef.current = fxOn;
  const chRef = useRef(ch); chRef.current = ch;
  const monitorRef = useRef(monitor); monitorRef.current = monitor;
  const powerRef = useRef(power); powerRef.current = power;
  const callsignRef = useRef(callsign); callsignRef.current = callsign;

  // VU from streaming chunks
  function chunkRMS(pcm){
    let s = 0;
    for(let i=0;i<pcm.length;i++) s += pcm[i]*pcm[i];
    return Math.min(1, Math.sqrt(s/pcm.length)*3);
  }

  // ---- broadcast channel ----
  const bcRef = useRef(null);
  const playersRef = useRef({});      // streamId -> {player, from, channel}
  const activeStreams = useRef(0);

  const appendLog = (entry) => {
    setLog(prev => [...prev.slice(-30), {t: Date.now(), ...entry}]);
  };

  useEffect(() => {
    const bc = new BroadcastChannel('rt808-walkie');
    bcRef.current = bc;
    bc.onmessage = (e) => {
      const m = e.data;
      if(!powerRef.current) return;
      if(m.from === callsignRef.current) return;
      if(m.channel !== chRef.current.n && !monitorRef.current) return;

      if(m.type === 'stream-start'){
        const player = RadioAudio.makeStreamPlayer({sampleRate: m.sampleRate, applyFX: fxOnRef.current});
        playersRef.current[m.streamId] = {player, from: m.from, channel: m.channel};
        activeStreams.current++;
        setRxing(true);
        setRxFrom(m.from);
        setLastLine(`RX ${m.from} · CH${String(m.channel).padStart(2,'0')}`);
        appendLog({kind:'rx', text:`${m.from} calling`, channel: m.channel});
        RadioAudio.rxStartTone();
      } else if(m.type === 'stream-chunk'){
        const rec = playersRef.current[m.streamId];
        if(rec) rec.player.feed(m.pcm);
      } else if(m.type === 'stream-end'){
        const rec = playersRef.current[m.streamId];
        if(!rec) return;
        rec.player.end().then(() => {
          RadioAudio.rxEndTone();
          delete playersRef.current[m.streamId];
          activeStreams.current = Math.max(0, activeStreams.current - 1);
          if(activeStreams.current === 0){
            setRxing(false);
            setLastLine(`RX complete · ${rec.from}`);
          }
        });
      } else if(m.type === 'text-tx'){
        // AI text reply
        setRxing(true); setRxFrom(m.from);
        setLastLine(`RX ${m.from}: ${m.text}`);
        appendLog({kind:'rx', text:`${m.from}: ${m.text}`, channel: m.channel});
        RadioAudio.rxStartTone();
        setTimeout(()=>{ RadioAudio.rxEndTone(); setRxing(false); setLastLine(`RX complete · ${m.from}`); }, Math.max(900, m.text.length*180));
      }
    };
    return () => bc.close();
  }, []);

  useEffect(() => {
    RadioLiveKit.on('connected', ({room}) => {
      setLastLine(`LiveKit · ${room}`);
    });
    RadioLiveKit.on('rxStart', ({from}) => {
      if(!powerRef.current) return;
      activeStreams.current++;
      setRxing(true);
      setRxFrom(from);
      setLastLine(`RX ${from} · CH${String(chRef.current.n).padStart(2,'0')}`);
      appendLog({kind:'rx', text:`${from} calling`, channel: chRef.current.n});
      RadioAudio.rxStartTone();
    });
    RadioLiveKit.on('rxEnd', ({from}) => {
      RadioAudio.rxEndTone();
      activeStreams.current = Math.max(0, activeStreams.current - 1);
      if(activeStreams.current === 0){
        setRxing(false);
        setLastLine(`RX complete · ${from}`);
      }
    });
    RadioLiveKit.on('peerJoin', ({from}) => {
      appendLog({kind:'sys', text:`${from} joined`, channel: chRef.current.n});
    });
    RadioLiveKit.on('peerLeave', ({from}) => {
      appendLog({kind:'sys', text:`${from} left`, channel: chRef.current.n});
    });
    RadioLiveKit.on('disconnected', () => {
      setRxing(false);
      activeStreams.current = 0;
    });
  }, []);

  useEffect(() => {
    let cancelled = false;
    if(!power){
      RadioLiveKit.leave();
      return;
    }
    setLastLine(`Connecting CH${String(ch.n).padStart(2,'0')} · LiveKit`);
    RadioLiveKit.join({channel: ch.n, identity: callsign})
      .then(() => {
        if(!cancelled) setLastLine(`Standby · CH${String(ch.n).padStart(2,'0')}`);
      })
      .catch((e) => {
        console.warn(e);
        if(!cancelled){
          setLastLine('LiveKit connection failed');
          RadioAudio.errorTone();
        }
      });
    return () => { cancelled = true; };
  }, [power, ch.n, callsign]);

  // ---- power / audio ----
  useEffect(() => {
    if(power){
      RadioAudio.resume();
      RadioAudio.startHiss();
      RadioAudio.setVolume(volume/10);
    } else { RadioAudio.stopHiss(); }
  }, [power]);
  useEffect(() => { RadioAudio.setVolume(volume/10); }, [volume]);
  useEffect(() => { RadioAudio.setMuted(muted || !power); }, [muted, power]);
  useEffect(() => { RadioAudio.setSquelch(squelch/9); }, [squelch]);

  // ---- battery & signal ----
  useEffect(() => {
    if(!power) return;
    const id = setInterval(() => {
      setBattery(b => Math.max(0, b - 0.05));
      setSignal(s => Math.max(1, Math.min(5, s + (Math.random()-0.5)*0.6)));
    }, 3000);
    return () => clearInterval(id);
  }, [power]);

  // ---- scanning ----
  useEffect(() => {
    if(!scanning || !power) return;
    const id = setInterval(() => {
      setChIdx(i => (i % CHANNELS.length) + 1);
      RadioAudio.clickTone();
    }, 700);
    return () => clearInterval(id);
  }, [scanning, power]);

  // ---- PTT lifecycle (streaming) ----
  const streamIdRef = useRef(null);
  const startPtt = useCallback(async () => {
    if(!power) return;
    if(scanning) setScanning(false);
    if(rxing){ RadioAudio.errorTone(); return; }
    if(pttDown) return;
    setPttDown(true);
    try{
      RadioAudio.txStartTone();
      const streamId = Math.random().toString(36).slice(2,10);
      streamIdRef.current = streamId;
      const ccount = chRef.current.n;
      await RadioLiveKit.join({channel: ccount, identity: callsignRef.current});
      await RadioLiveKit.startPtt();
      setVuLevel(0.75);
      setTxing(true);
      setLastLine(`TX ON · LiveKit`);
      appendLog({kind:'tx', text:`你正在发射`, channel: ccount});
    } catch(e){
      console.warn(e);
      setLastLine('LiveKit / mic failed');
      RadioAudio.errorTone();
      setPttDown(false);
    }
  }, [power, scanning, rxing, pttDown]);

  const endPtt = useCallback(async () => {
    if(!pttDown) return;
    setPttDown(false);
    if(!txing){ return; }
    const sid = streamIdRef.current;
    await RadioLiveKit.stopPtt();
    setVuLevel(0); setTxing(false);
    RadioAudio.txEndTone();
    if(sid){
      setLastLine('发射结束');
      if(t.aiPartner) triggerAIResponse();
    }
    streamIdRef.current = null;
  }, [pttDown, txing, callsign, ch.n, t.aiPartner]);

  async function triggerAIResponse(){
    setLastLine('···等待回复');
    try{
      const persona = chRef.current.name;
      const text = await window.claude.complete(
        `You are an operator on a handheld radio. Channel is "${persona}". Reply with one VERY short radio-style transmission in Chinese (under 18 chars), using radio brevity (e.g. "收到", "10-4", "稍候"). Just the reply, no quotes.`
      );
      const reply = (text || '收到').trim().replace(/^["']|["']$/g,'').slice(0,30);
      await new Promise(r=>setTimeout(r, 400 + Math.random()*600));
      bcRef.current.postMessage({type:'text-tx', from:'NPC-7B', channel: chRef.current.n, text: reply});
    }catch(e){ setLastLine('伙伴节点离线'); }
  }

  // keyboard PTT
  useEffect(() => {
    const down = (e) => { if(e.code==='Space' && !e.repeat){ e.preventDefault(); startPtt(); } };
    const up = (e) => { if(e.code==='Space'){ e.preventDefault(); endPtt(); } };
    window.addEventListener('keydown', down);
    window.addEventListener('keyup', up);
    return () => { window.removeEventListener('keydown', down); window.removeEventListener('keyup', up); };
  }, [startPtt, endPtt]);

  // scale-to-fit
  const [scale, setScale] = useState(1);
  useEffect(() => {
    const update = () => {
      const W = window.innerWidth, H = window.innerHeight;
      const designW = 400, designH = 820;
      setScale(Math.min(1, W/designW, H/designH));
    };
    update();
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  }, []);

  // -------- LCD screen content --------
  const screenLog = log.slice(-3).reverse();

  return (
    <div style={{
      width:'100%', height:'100%',
      display:'flex', alignItems:'center', justifyContent:'center',
      padding: 12, position:'relative',
    }}>
      <style>{`
        @keyframes led-blink { 0%,100%{opacity:1} 50%{opacity:.25} }
        @keyframes screen-flicker { 0%,100%{opacity:1} 97%{opacity:.92} }
      `}</style>

      <div style={{
        transform:`scale(${scale})`, transformOrigin:'center center',
      }}>
      {/* Radio body */}
      <div style={{
        position:'relative',
        width: 360,
        borderRadius: 26,
        padding: '54px 18px 22px',
        background: `linear-gradient(165deg, ${palette.body} 0%, ${palette.body2} 100%)`,
        boxShadow:`
          inset 0 1px 0 rgba(255,255,255,.08),
          inset 0 -2px 4px rgba(0,0,0,.4),
          0 30px 60px rgba(0,0,0,.55),
          0 10px 20px rgba(0,0,0,.4)
        `,
        border:'1px solid rgba(0,0,0,.6)',
      }}>
        <Antenna />

        {/* texture overlay */}
        <div style={{
          position:'absolute', inset:0, borderRadius:26, pointerEvents:'none',
          backgroundImage:'radial-gradient(circle at 30% 12%, rgba(255,255,255,.04), transparent 40%), repeating-linear-gradient(45deg, rgba(255,255,255,.012) 0 2px, transparent 2px 5px)',
        }}/>

        {/* PTT side rail */}
        <div style={{
          position:'absolute', left:-6, top:80, width:14, height:160,
          background:`linear-gradient(90deg, oklch(8% 0 0), ${palette.body2})`,
          borderRadius:'3px 0 0 3px',
          boxShadow: pttDown ? 'inset 0 0 8px rgba(255,80,40,.7)' : 'inset 0 2px 4px rgba(0,0,0,.6)',
        }}/>

        {/* LED row */}
        <div style={{position:'absolute', top:16, left:78, display:'flex', gap:10, alignItems:'center'}}>
          <div style={{display:'flex', alignItems:'center', gap:5}}>
            <LED color="red" on={txing} blink={txing} size={9}/>
            <span style={{fontSize:8, fontFamily:'JetBrains Mono, monospace', color:'oklch(60% 0.04 30)', fontWeight:700, letterSpacing:1.5}}>TX</span>
          </div>
          <div style={{display:'flex', alignItems:'center', gap:5}}>
            <LED color="green" on={rxing || power} blink={rxing} size={9}/>
            <span style={{fontSize:8, fontFamily:'JetBrains Mono, monospace', color:'oklch(55% 0.05 145)', fontWeight:700, letterSpacing:1.5}}>RX</span>
          </div>
          <div style={{display:'flex', alignItems:'center', gap:5}}>
            <LED color="amber" on={battery < 20 && power} blink={battery < 10} size={9}/>
            <span style={{fontSize:8, fontFamily:'JetBrains Mono, monospace', color:'oklch(60% 0.08 80)', fontWeight:700, letterSpacing:1.5}}>BAT</span>
          </div>
        </div>

        {/* operator ID badge (top right, integrated on body) */}
        <div style={{position:'absolute', top:12, right:18, textAlign:'right'}}>
          <div style={{fontSize:7.5, fontFamily:'Inter, sans-serif', fontWeight:800, letterSpacing:2.5, color:'oklch(55% 0.01 80)'}}>RT-808 · OP</div>
          <div style={{fontSize:13, fontFamily:'JetBrains Mono, monospace', fontWeight:800, color:'oklch(85% 0.10 70)', letterSpacing:0.5, marginTop:1, textShadow:'0 0 10px oklch(72% 0.16 70 / .35)'}}>
            {callsign}
          </div>
        </div>

        {/* Speaker grill (compact) */}
        <div style={{marginBottom:10}}>
          <SpeakerGrill rows={4} cols={16}/>
        </div>

        {/* LCD screen — now includes mini log */}
        <div style={{animation: rxing ? 'screen-flicker 0.18s infinite' : 'none'}}>
          <LCDScreen
            channel={ch.n}
            channelName={ch.name}
            freq={ch.freq}
            mode={scanning ? 'SCAN' : monitor ? 'MON' : 'FM'}
            signal={Math.round(signal)}
            battery={battery}
            txing={txing}
            rxing={rxing}
            fxOn={fxOn}
            vuLevel={vuLevel}
            lastLine={power ? lastLine : '— OFF —'}
            squelch={squelch}
            screenLog={screenLog}
            power={power}
          />
        </div>

        {/* Knobs row */}
        <div style={{display:'flex', justifyContent:'space-around', marginTop:12}}>
          <Knob label="CH" value={chIdx} onChange={(v)=>{ setChIdx(v); RadioAudio.clickTone(); setLastLine(`频道 ${String(v).padStart(2,'0')} · ${CHANNELS[v-1].name}`); }} min={1} max={16} step={1} size={58} marks={['1','8','16']}/>
          <Knob label="VOL" value={volume} onChange={setVolume} min={0} max={10} step={1} size={58} marks={['0','5','10']}/>
          <Knob label="SQL" value={squelch} onChange={(v)=>{ setSquelch(v); RadioAudio.clickTone(); }} min={0} max={9} step={1} size={58} marks={['0','5','9']}/>
        </div>

        {/* Big keypad — 3 cols x 3 rows, bigger touch targets */}
        <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:7, marginTop:14}}>
          <BigKey active={monitor} onClick={() => { setMonitor(m=>!m); RadioAudio.clickTone(); setLastLine(monitor?'监听关闭':'监听全频道'); }}>MON</BigKey>
          <BigKey active={scanning} onClick={() => { setScanning(s=>!s); RadioAudio.clickTone(); }}>{scanning?'STOP':'SCAN'}</BigKey>
          <BigKey active={muted} onClick={() => { setMuted(m=>!m); RadioAudio.clickTone(); }}>{muted?'MUTED':'MUTE'}</BigKey>
          <BigKey onClick={() => { setChIdx(i => Math.max(1, i-1)); RadioAudio.clickTone(); }}>▼ CH</BigKey>
          <BigKey active={fxOn} onClick={() => { setFxOn(f=>!f); RadioAudio.clickTone(); setLastLine(fxOn?'拟真音效关闭':'拟真音效开启'); }}>
            {fxOn?'FX·ON':'FX·OFF'}
          </BigKey>
          <BigKey onClick={() => { setChIdx(i => Math.min(16, i+1)); RadioAudio.clickTone(); }}>▲ CH</BigKey>
          <BigKey onClick={() => {
            if(power){ RadioAudio.txEndTone(); setPower(false); }
            else { setPower(true); setTimeout(()=>RadioAudio.rxStartTone(), 100); setBattery(b=>Math.min(100,b+5)); }
          }} accent="red" wide>{power?'⏻ POWER':'⏻ ON'}</BigKey>
          <BigKey wide accent="orange"
            pressed={pttDown}
            onDown={startPtt} onUp={endPtt}>
            {pttDown ? '◉ TALK' : 'PTT'}
          </BigKey>
        </div>

        {/* Bottom mic dots */}
        <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginTop:12, padding:'0 4px'}}>
          <div style={{fontSize:8, color:'oklch(50% 0.01 80)', fontFamily:'JetBrains Mono, monospace', letterSpacing:1.5}}>
            UHF · {ch.freq} MHz
          </div>
          <div style={{display:'flex', gap:3}}>
            {Array.from({length:6}).map((_,i)=>(
              <div key={i} style={{width:3, height:8, borderRadius:1, background:'oklch(8% 0 0)', boxShadow:'inset 0 1px 1px rgba(0,0,0,.8)'}}/>
            ))}
          </div>
        </div>

        {/* Screws */}
        {[{top:6, left:6}, {top:6, right:6}, {bottom:6, left:6}, {bottom:6, right:6}].map((pos,i)=>(
          <div key={i} style={{
            position:'absolute', ...pos, width:8, height:8, borderRadius:'50%',
            background:'radial-gradient(circle at 35% 30%, oklch(40% 0.005 80), oklch(15% 0.003 80) 75%)',
            boxShadow:'inset 0 1px 1px rgba(255,255,255,.08), inset 0 -1px 1px rgba(0,0,0,.5)',
          }}>
            <div style={{position:'absolute', inset:'25% 10%', background:'oklch(10% 0 0)', borderRadius:1}}/>
          </div>
        ))}
      </div>
      </div>

      <TweaksPanel title="Tweaks">
        <TweakSection title="外观">
          <TweakColor
            label="机身配色"
            value={t.bodyColor}
            onChange={(v)=>setTweak('bodyColor', v)}
            options={BODY_PALETTES_LIST.map(p=>p.swatch)}
            optionLabels={BODY_PALETTES_LIST.map(p=>p.key)}
          />
        </TweakSection>
        <TweakSection title="通讯">
          <TweakToggle label="AI 伙伴节点" value={t.aiPartner} onChange={(v)=>setTweak('aiPartner', v)}/>
          <div style={{fontSize:10, color:'oklch(60% 0.01 80)', lineHeight:1.5}}>
            单人测试：每次松开 PTT 都会收到一个虚拟操作员的简短回复。
          </div>
        </TweakSection>
      </TweaksPanel>
    </div>
  );
}

// LCD with embedded mini log
function LCDScreen({channel, channelName, freq, mode, signal, battery, txing, rxing, fxOn, vuLevel, lastLine, squelch, screenLog, power}){
  const bars = Math.round((signal/5)*5);
  return (
    <div style={{
      position:'relative',
      borderRadius:8,
      padding:'10px 12px 8px',
      background:'linear-gradient(180deg, var(--screen-bg-2), var(--screen-bg))',
      boxShadow:'inset 0 3px 8px rgba(0,0,0,.55), inset 0 -1px 2px rgba(255,255,255,.08), 0 1px 0 rgba(255,255,255,.05)',
      border:'2px solid oklch(8% 0 0)',
      color:'oklch(20% 0.03 70)',
      fontFamily:'JetBrains Mono, monospace',
      minHeight: 196,
      overflow:'hidden',
    }}>
      <div style={{position:'absolute', inset:0, pointerEvents:'none',
        backgroundImage:'repeating-linear-gradient(0deg, rgba(0,0,0,.05) 0 1px, transparent 1px 3px)'}}/>

      {/* status bar */}
      <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', fontSize:9, fontWeight:700, letterSpacing:1.5}}>
        <span>{txing ? '◉ TX' : rxing ? '◉ RX' : 'STBY'}</span>
        <span>{mode}</span>
        <span>{fxOn ? 'FX·ON' : 'FX·OFF'}</span>
        <span style={{display:'flex', alignItems:'center', gap:2}}>
          {[1,2,3,4,5].map(i=>(
            <span key={i} style={{display:'inline-block', width:3, height:3+i*2, background: i<=bars ? 'oklch(20% 0.03 70)' : 'oklch(20% 0.03 70 / .25)'}}/>
          ))}
        </span>
        <span>{Math.round(battery)}%</span>
      </div>

      {/* channel big */}
      <div style={{display:'flex', alignItems:'baseline', justifyContent:'center', marginTop:2, gap:6}}>
        <span style={{
          fontFamily:'"DSEG7 Classic", "JetBrains Mono", monospace',
          fontSize: 42, fontWeight: 800, letterSpacing: 2,
          color:'oklch(22% 0.06 80)',
          textShadow:'0 1px 0 rgba(255,255,255,.18)',
          lineHeight:1,
        }}>{String(channel).padStart(2,'0')}</span>
        <span style={{fontSize:10, fontWeight:700, letterSpacing:1.5, marginBottom:4}}>CH</span>
        <span style={{flex:1, textAlign:'right', fontSize:10, fontWeight:700, letterSpacing:1.5, marginBottom:4}}>{channelName}</span>
      </div>

      {/* VU */}
      <div style={{display:'flex', alignItems:'center', gap:4, marginTop:4, fontSize:9, fontWeight:700}}>
        <span style={{width:14}}>VU</span>
        <div style={{flex:1, height:7, background:'oklch(30% 0.05 85)', borderRadius:2, overflow:'hidden', position:'relative'}}>
          <div style={{position:'absolute', left:0, top:0, bottom:0, width:`${Math.round(vuLevel*100)}%`,
            background:'linear-gradient(90deg, oklch(28% 0.10 145), oklch(40% 0.12 85), oklch(45% 0.18 27))',
            transition:'width 50ms linear'}}/>
        </div>
        <span style={{fontSize:8}}>SQ{squelch}</span>
      </div>

      {/* divider */}
      <div style={{height:1, background:'oklch(25% 0.05 85 / .35)', margin:'6px 0 4px'}}/>

      {/* mini log on screen */}
      <div style={{fontSize:9, fontWeight:700, letterSpacing:0.5, lineHeight:1.45, minHeight:60}}>
        {!power ? null : screenLog.length === 0 ? (
          <div style={{opacity:.6}}>— 无通讯记录 —</div>
        ) : screenLog.map((l,i)=>(
          <div key={i} style={{display:'flex', gap:5, alignItems:'baseline', marginBottom:1, opacity: 1 - i*0.25}}>
            <span style={{width:48, flex:'0 0 auto', fontSize:8.5}}>{new Date(l.t).toTimeString().slice(0,8)}</span>
            <span style={{width:18, flex:'0 0 auto', fontSize:8.5}}>{l.kind==='tx'?'TX':l.kind==='rx'?'RX':'··'}</span>
            <span style={{flex:1, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{l.text}{l.channel?` C${String(l.channel).padStart(2,'0')}`:''}</span>
          </div>
        ))}
      </div>

      {/* last status line */}
      <div style={{fontSize:9, fontWeight:700, letterSpacing:0.5, minHeight:12, opacity:.85, marginTop:2, borderTop:'1px dashed oklch(25% 0.05 85 / .3)', paddingTop:3}}>
        {lastLine}
      </div>
    </div>
  );
}

// Big touch-friendly key — supports click OR press-and-hold (for PTT)
function BigKey({children, onClick, onDown, onUp, active=false, accent=null, wide=false, pressed=false}){
  const [down, setDown] = useState(false);
  const isPress = !!(onDown || onUp);
  const isDown = pressed || down;
  const bg = isDown
    ? (accent==='red' ? 'linear-gradient(180deg, oklch(28% 0.10 27), oklch(20% 0.08 27))'
       : accent==='orange' ? 'linear-gradient(180deg, oklch(48% 0.18 50), oklch(35% 0.16 45))'
       : 'linear-gradient(180deg, oklch(12% 0.004 80), oklch(20% 0.004 80))')
    : active
      ? 'linear-gradient(180deg, oklch(38% 0.12 70), oklch(25% 0.10 70))'
      : 'linear-gradient(180deg, oklch(30% 0.006 80), oklch(16% 0.004 80))';
  const color = isDown && accent==='orange' ? 'oklch(98% 0.04 90)'
              : isDown && accent==='red' ? 'oklch(92% 0.06 30)'
              : active ? 'oklch(95% 0.05 80)'
              : 'oklch(85% 0.01 80)';
  const glow = isDown && accent==='orange' ? '0 0 18px oklch(70% 0.18 50 / .7)'
             : isDown && accent==='red' ? '0 0 14px oklch(60% 0.24 27 / .55)'
             : 'none';
  return (
    <button
      onClick={isPress ? undefined : onClick}
      onMouseDown={isPress ? (e)=>{ setDown(true); onDown && onDown(e); } : ()=>setDown(true)}
      onMouseUp={isPress ? (e)=>{ setDown(false); onUp && onUp(e); } : ()=>setDown(false)}
      onMouseLeave={isPress ? (e)=>{ if(down){ setDown(false); onUp && onUp(e); } } : ()=>setDown(false)}
      onTouchStart={(e)=>{ e.preventDefault(); setDown(true); if(isPress){ onDown && onDown(e); } }}
      onTouchEnd={(e)=>{ e.preventDefault(); setDown(false); if(isPress){ onUp && onUp(e); } else if(onClick){ onClick(e); } }}
      style={{
        gridColumn: wide ? 'span 3' : 'auto',
        height: wide ? 50 : 42,
        borderRadius: 8,
        border:'1px solid oklch(8% 0 0)',
        background: bg,
        boxShadow: isDown
          ? `inset 0 3px 5px rgba(0,0,0,.65), ${glow}`
          : `inset 0 1px 0 rgba(255,255,255,.08), 0 3px 6px rgba(0,0,0,.5)`,
        color,
        fontFamily:'JetBrains Mono, monospace',
        fontSize: wide ? 14 : 11,
        fontWeight:800,
        letterSpacing: wide ? 2.5 : 1.2,
        cursor:'pointer',
        transition:'background 60ms, box-shadow 60ms, color 60ms',
        WebkitTapHighlightColor:'transparent',
        padding:0,
        touchAction:'none',
      }}
    >{children}</button>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
