/* global React, ReactDOM */
const { useEffect, useRef, useState } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "vibe": "deep",
  "tileScale": 44,
  "showGuides": true,
  "haptics": true,
  "autoCamera": true,
  "tappableGlow": true,
  "showHints": true
}/*EDITMODE-END*/;

// Palettes — thin and thick rhombi get DIFFERENT, distinct colors so the
// two-shape nature of P3 reads at a glance.
const VIBES = {
  // Cool deep-space palette: thick = warm coral, thin = cyan/blue
  deep: {
    bg1: '#070912', bg2: '#0d1226',
    thick: '#e8794a', thin: '#5cc6ff',
    thickEdge: '#fbbf91', thinEdge: '#9be3ff',
    hidden: '#0e1428', hiddenEdge: '#1a2447',
    tappable: '#2d3a6e', tappableEdge: '#7aa2ff',
    ink: '#e8ecff', dim: '#6b7297', glow: '#9bb6ff',
  },
  // Warm paper: muted earthy thick / cool thin
  paper: {
    bg1: '#f4ecd8', bg2: '#e6dcbf',
    thick: '#c2410c', thin: '#1e3a5f',
    thickEdge: '#7c2d12', thinEdge: '#0c2440',
    hidden: '#ddd2b0', hiddenEdge: '#c0b48e',
    tappable: '#f4ecd8', tappableEdge: '#c2410c',
    ink: '#3a2a14', dim: '#937a4f', glow: '#c2410c',
  },
  // Neon dream
  neon: {
    bg1: '#08020c', bg2: '#15082a',
    thick: '#ff2bd6', thin: '#21f0ff',
    thickEdge: '#ff8de8', thinEdge: '#8ff7ff',
    hidden: '#1a0628', hiddenEdge: '#2c1145',
    tappable: '#3a0a5e', tappableEdge: '#ff2bd6',
    ink: '#fdf2ff', dim: '#7a5a8c', glow: '#ff2bd6',
  },
  // Mono ink
  mono: {
    bg1: '#0a0a0a', bg2: '#1c1c1c',
    thick: '#fafafa', thin: '#737373',
    thickEdge: '#ffffff', thinEdge: '#a3a3a3',
    hidden: '#161616', hiddenEdge: '#2a2a2a',
    tappable: '#262626', tappableEdge: '#fafafa',
    ink: '#fafafa', dim: '#737373', glow: '#ffffff',
  },
  // Terminal ASCII green-on-black
  ascii: {
    bg1: '#000000', bg2: '#001a05',
    thick: '#22ff66', thin: '#0a8a3a',
    thickEdge: '#88ffaa', thinEdge: '#46ce6a',
    hidden: '#021a08', hiddenEdge: '#053018',
    tappable: '#062a14', tappableEdge: '#22ff66',
    ink: '#a8ffb8', dim: '#3a7a48', glow: '#22ff66',
  },
};

// ===== LEVELS MODE =====

const LEVELS_DATA = [
  { id: 1, key: 'deuce', stars: 1, status: 'done',    best: 24 },
  { id: 2, key: 'ace',   stars: 3, status: 'done',    best: 88 },
  { id: 3, key: 'star',  stars: 2, status: 'done',    best: 142 },
  { id: 4, key: 'sun',   stars: 0, status: 'current', best: null },
  { id: 5, key: 'jack',  stars: 0, status: 'locked',  best: null },
  { id: 6, key: 'queen', stars: 0, status: 'locked',  best: null },
  { id: 7, key: 'king',  stars: 0, status: 'locked',  best: null },
];

function FigureSVG({ figKey, size = 80, withGlow = true, palette }) {
  const VF = window.VertexFigures;
  if (!VF || !VF.VERTEX_FIGURES) return <div style={{ width: size, height: size }} />;
  const fig = VF.VERTEX_FIGURES[figKey];
  if (!fig) return <div style={{ width: size, height: size }} />;
  const scaleMap = { sun: 0.14, star: 0.22, deuce: 0.36, ace: 0.26, jack: 0.22, queen: 0.22, king: 0.22 };
  const html = VF.vertexFigureToSVG(fig, {
    size, scale: size * (scaleMap[figKey] || 0.26),
    palette, glow: withGlow, id: figKey,
  });
  return <div style={{ display: 'inline-block', lineHeight: 0 }} dangerouslySetInnerHTML={{ __html: html }} />;
}

function LevelStar({ filled, palette }) {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14">
      <path d="M7 1l1.8 3.6 4 .6-2.9 2.8.7 4L7 10.2 3.4 12.1l.7-4L1.2 5.2l4-.6L7 1z"
        fill={filled ? palette.thick : 'none'}
        stroke={filled ? palette.thick : palette.dim}
        strokeWidth="1.2" strokeLinejoin="round" />
    </svg>
  );
}

function MapScreen({ palette, onSelect, onBack }) {
  const VF = window.VertexFigures;
  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 100,
      background: `radial-gradient(ellipse at top, ${palette.bg2}, ${palette.bg1} 70%)`,
      color: palette.ink,
      fontFamily: "'Manrope', system-ui, sans-serif",
      display: 'flex', flexDirection: 'column',
      paddingTop: 52, paddingBottom: 20, boxSizing: 'border-box',
      overflow: 'hidden',
    }}>
      <div style={{ padding: '0 20px 12px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
        <div>
          <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: '0.2em', color: palette.dim }}>CHAPTER I</div>
          <div style={{ fontSize: 24, fontWeight: 700, marginTop: 2 }}>Vertex Figures</div>
          <div style={{ fontSize: 12, color: palette.dim, marginTop: 2 }}>Master the 7 ways tiles can meet</div>
        </div>
        <div style={{ textAlign: 'right' }}>
          <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: '0.2em', color: palette.dim }}>STARS</div>
          <div style={{ fontSize: 20, fontWeight: 700, color: palette.thick }}>6 / 21</div>
        </div>
      </div>

      <div style={{ flex: 1, overflowY: 'auto', padding: '4px 14px', display: 'flex', flexDirection: 'column', gap: 8 }}>
        {LEVELS_DATA.map((lvl) => {
          const isLocked = lvl.status === 'locked';
          const isCurrent = lvl.status === 'current';
          const fig = VF?.VERTEX_FIGURES[lvl.key];
          return (
            <div key={lvl.id} style={{
              display: 'flex', alignItems: 'center', gap: 12,
              padding: 10, borderRadius: 16,
              background: isCurrent ? 'rgba(232,121,74,0.08)' : 'rgba(255,255,255,0.02)',
              border: '1px solid ' + (isCurrent ? palette.thick : 'rgba(255,255,255,0.06)'),
              boxShadow: isCurrent ? '0 0 20px rgba(232,121,74,0.15)' : 'none',
              opacity: isLocked ? 0.42 : 1,
            }}>
              <div style={{
                width: 58, height: 58, flexShrink: 0, borderRadius: 12,
                background: isLocked ? palette.hidden : 'linear-gradient(135deg, rgba(122,162,255,0.08), rgba(232,121,74,0.04))',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}>
                {isLocked
                  ? <svg width="18" height="20" viewBox="0 0 20 22"><path d="M5 10V7a5 5 0 0110 0v3M3 10h14v10H3z" fill="none" stroke={palette.dim} strokeWidth="1.6" strokeLinejoin="round"/></svg>
                  : <FigureSVG figKey={lvl.key} size={44} withGlow={isCurrent} palette={palette} />
                }
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
                  <span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: '0.2em', color: palette.dim }}>{String(lvl.id).padStart(2,'0')}</span>
                  <span style={{ fontSize: 15, fontWeight: 700 }}>{fig?.name || lvl.key}</span>
                </div>
                <div style={{ fontSize: 11, color: palette.dim, marginTop: 2 }}>
                  {isLocked ? 'Locked — clear previous' : (fig?.description || '')}
                </div>
                <div style={{ display: 'flex', gap: 3, marginTop: 5, alignItems: 'center' }}>
                  {[1,2,3].map(n => <LevelStar key={n} filled={n <= lvl.stars} palette={palette} />)}
                  {lvl.best && <span style={{ marginLeft: 6, fontFamily: "'JetBrains Mono', monospace", fontSize: 10, color: palette.dim }}>best · <span style={{ color: palette.ink }}>{lvl.best}</span></span>}
                </div>
              </div>
              {isCurrent && (
                <div onClick={() => onSelect(lvl)} style={{
                  padding: '7px 12px', borderRadius: 999,
                  background: palette.thick, color: palette.bg1,
                  fontFamily: "'JetBrains Mono', monospace", fontSize: 10, fontWeight: 700, letterSpacing: '0.16em',
                  cursor: 'pointer', flexShrink: 0, WebkitTapHighlightColor: 'transparent',
                }}>PLAY</div>
              )}
              {!isCurrent && !isLocked && (
                <div onClick={() => onSelect(lvl)} style={{
                  padding: '7px 12px', borderRadius: 999,
                  border: '1px solid ' + palette.dim, color: palette.dim,
                  fontFamily: "'JetBrains Mono', monospace", fontSize: 10, fontWeight: 700, letterSpacing: '0.16em',
                  cursor: 'pointer', flexShrink: 0, WebkitTapHighlightColor: 'transparent',
                }}>REPLAY</div>
              )}
            </div>
          );
        })}
      </div>

      <div style={{ padding: '10px 20px 0' }}>
        <button onClick={onBack} style={{
          width: '100%', height: 42, borderRadius: 12,
          background: 'transparent', border: `1px solid ${palette.dim}44`,
          color: palette.dim, fontFamily: "'JetBrains Mono', monospace",
          fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', cursor: 'pointer',
        }}>← BACK</button>
      </div>
    </div>
  );
}

function LevelBriefing({ level, palette, onBack, onBegin }) {
  const VF = window.VertexFigures;
  const fig = VF?.VERTEX_FIGURES[level.key];
  if (!fig) return null;
  const thickN = fig.rhombs.filter(r => r.type === 'thick').length;
  const thinN  = fig.rhombs.filter(r => r.type === 'thin').length;
  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 100,
      background: `radial-gradient(ellipse at center 30%, ${palette.bg2}, ${palette.bg1} 70%)`,
      color: palette.ink,
      fontFamily: "'Manrope', system-ui, sans-serif",
      display: 'flex', flexDirection: 'column',
      paddingTop: 52, paddingBottom: 28, boxSizing: 'border-box',
    }}>
      <div style={{ padding: '0 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <div onClick={onBack} style={{
          width: 34, height: 34, borderRadius: 999,
          border: '1px solid rgba(255,255,255,0.12)',
          display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
          WebkitTapHighlightColor: 'transparent',
        }}>
          <svg width="12" height="12" viewBox="0 0 14 14"><path d="M9 2L4 7l5 5" fill="none" stroke={palette.ink} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
        </div>
        <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: '0.2em', color: palette.dim }}>LEVEL {String(level.id).padStart(2,'0')}</div>
        <div style={{ width: 34 }} />
      </div>

      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16, padding: '0 24px' }}>
        <div style={{
          width: 190, height: 190, borderRadius: 28,
          background: 'radial-gradient(circle at center, rgba(232,121,74,0.14), rgba(122,162,255,0.05) 60%, transparent 80%)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <FigureSVG figKey={level.key} size={170} withGlow palette={palette} />
        </div>

        <div style={{ textAlign: 'center' }}>
          <div style={{ fontSize: 30, fontWeight: 700 }}>{fig.name}</div>
          <div style={{ fontSize: 13, color: palette.dim, marginTop: 5, lineHeight: 1.5, maxWidth: 260 }}>{fig.description}</div>
        </div>

        <div style={{
          display: 'flex', gap: 10, alignItems: 'center',
          padding: '10px 16px', borderRadius: 12,
          background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.06)',
        }}>
          {thickN > 0 && (
            <span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
              <span style={{ fontSize: 18, fontWeight: 700, color: palette.thick, fontFamily: "'JetBrains Mono', monospace" }}>{thickN}</span>
              <svg width="18" height="18" viewBox="-12 -12 24 24"><polygon points="9,0 0,5.3 -9,0 0,-5.3" fill={palette.thick} stroke={palette.thickEdge} strokeWidth="1" /></svg>
              <span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: '0.14em', color: palette.dim }}>THICK</span>
            </span>
          )}
          {thickN > 0 && thinN > 0 && <span style={{ width: 1, height: 18, background: 'rgba(255,255,255,0.1)', margin: '0 4px' }} />}
          {thinN > 0 && (
            <span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
              <span style={{ fontSize: 18, fontWeight: 700, color: palette.thin, fontFamily: "'JetBrains Mono', monospace" }}>{thinN}</span>
              <svg width="12" height="18" viewBox="-7 -12 14 24"><polygon points="6.5,0 0,2.1 -6.5,0 0,-2.1" fill={palette.thin} stroke={palette.thinEdge} strokeWidth="1" /></svg>
              <span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: '0.14em', color: palette.dim }}>THIN</span>
            </span>
          )}
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 6, alignSelf: 'stretch' }}>
          {[{n:1,text:'Build the figure'},{n:2,text:'No misclicks'},{n:3,text:'Under 30 seconds'}].map(({n,text}) => (
            <div key={n} style={{
              display: 'flex', alignItems: 'center', gap: 10,
              padding: '9px 12px', borderRadius: 10,
              background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
            }}>
              <div style={{ display: 'flex', gap: 2 }}>{[1,2,3].map(s => <LevelStar key={s} filled={s<=n} palette={palette} />)}</div>
              <span style={{ fontSize: 12, color: palette.ink }}>{text}</span>
            </div>
          ))}
        </div>
      </div>

      <div style={{ padding: '0 20px' }}>
        <button onClick={onBegin} style={{
          width: '100%', height: 52, borderRadius: 16,
          background: palette.thick, color: palette.bg1, border: 'none',
          fontFamily: "'JetBrains Mono', monospace", fontSize: 13, fontWeight: 700, letterSpacing: '0.22em',
          cursor: 'pointer', boxShadow: `0 8px 28px rgba(232,121,74,0.3)`,
          WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation',
        }}>BEGIN</button>
      </div>
    </div>
  );
}

function WinScreen({ level, gameState, palette, onRetry, onNext }) {
  const VF = window.VertexFigures;
  const fig = VF?.VERTEX_FIGURES[level.key];
  const stars = gameState.bestCombo >= 5 ? 3 : gameState.score >= 100 ? 2 : 1;
  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 100,
      background: palette.bg1,
      color: palette.ink,
      fontFamily: "'Manrope', system-ui, sans-serif",
      display: 'flex', flexDirection: 'column',
      paddingTop: 72, paddingBottom: 32, boxSizing: 'border-box',
      overflow: 'hidden',
    }}>
      {/* Orange glow overlay */}
      <div style={{ position: 'absolute', inset: 0, background: `radial-gradient(ellipse at 50% 38%, rgba(232,121,74,0.22), transparent 68%)`, pointerEvents: 'none' }} />
      <svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0.18, pointerEvents: 'none' }}
           viewBox="0 0 402 874" preserveAspectRatio="xMidYMid slice">
        {Array.from({length: 12}, (_, i) => {
          const a = (i * 30) * Math.PI / 180;
          return <line key={i} x1={201} y1={300} x2={201 + Math.cos(a)*700} y2={300 + Math.sin(a)*700} stroke={palette.thick} strokeWidth="1" />;
        })}
      </svg>

      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 20, padding: '0 24px', position: 'relative' }}>
        <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 10, letterSpacing: '0.32em', color: palette.thick }}>COMPLETE</div>
        <div style={{ fontSize: 40, fontWeight: 700 }}>{fig?.name || level.key}</div>
        <div style={{ filter: `drop-shadow(0 0 28px ${palette.thick})` }}>
          <FigureSVG figKey={level.key} size={190} withGlow palette={palette} />
        </div>
        <div style={{ display: 'flex', gap: 10 }}>
          {[1,2,3].map(n => (
            <div key={n} style={{ transform: n<=stars ? 'scale(1.1)' : 'scale(1)', filter: n<=stars ? `drop-shadow(0 0 10px ${palette.thick})` : 'none' }}>
              <svg width="34" height="34" viewBox="0 0 36 36">
                <path d="M18 2l5 10 11 1.5-8 8 2 11-10-5-10 5 2-11-8-8 11-1.5z"
                  fill={n<=stars ? palette.thick : 'none'}
                  stroke={n<=stars ? palette.thickEdge : palette.dim}
                  strokeWidth="1.4" strokeLinejoin="round" />
              </svg>
            </div>
          ))}
        </div>
        <div style={{ display: 'flex', gap: 28 }}>
          {[{label:'SCORE',value:gameState.score},{label:'COMBO',value:`×${gameState.bestCombo}`}].map(({label,value}) => (
            <div key={label} style={{ textAlign: 'center' }}>
              <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: '0.22em', color: palette.dim }}>{label}</div>
              <div style={{ fontSize: 20, fontWeight: 700, fontFamily: "'JetBrains Mono', monospace", marginTop: 2 }}>{value}</div>
            </div>
          ))}
        </div>
      </div>

      <div style={{ padding: '0 20px', display: 'flex', gap: 10, position: 'relative' }}>
        <button onClick={onRetry} style={{
          flex: 1, height: 50, borderRadius: 14,
          background: 'transparent', border: `1px solid ${palette.dim}`,
          color: palette.ink, fontFamily: "'JetBrains Mono', monospace",
          fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', cursor: 'pointer',
          WebkitTapHighlightColor: 'transparent',
        }}>RETRY</button>
        <button onClick={onNext} style={{
          flex: 2, height: 50, borderRadius: 14,
          background: palette.thick, color: palette.bg1, border: 'none',
          fontFamily: "'JetBrains Mono', monospace",
          fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', cursor: 'pointer',
          boxShadow: `0 6px 24px rgba(232,121,74,0.36)`,
          WebkitTapHighlightColor: 'transparent',
        }}>NEXT LEVEL ▸</button>
      </div>
    </div>
  );
}

// ===== MODE BRIEFING (ZEN / SEEDS) =====

function ModeBriefing({ mode, palette, onBack, onBegin }) {
  const isZen = mode === 'zen';
  const R = 30;
  const sunPetals = [0, 1, 2, 3, 4].map(i => {
    const a0 = (i * 72 - 90) * Math.PI / 180;
    const a1 = ((i + 1) * 72 - 90) * Math.PI / 180;
    const x1 = Math.cos(a0) * R, y1 = Math.sin(a0) * R;
    const x3 = Math.cos(a1) * R, y3 = Math.sin(a1) * R;
    return `0,0 ${x1.toFixed(2)},${y1.toFixed(2)} ${(x1+x3).toFixed(2)},${(y1+y3).toFixed(2)} ${x3.toFixed(2)},${y3.toFixed(2)}`;
  });

  const steps = isZen
    ? ['Tap the pulsing tiles to reveal the mosaic', 'Keep your combo alive — each miss resets it', 'Unique Penrose pattern every session']
    : ['Gold-edged tiles belong to a hidden Sun', 'Reveal all 5 Sun tiles to collect it', 'Collect enough Suns to level up'];

  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 100,
      background: palette.bg1, color: palette.ink,
      fontFamily: "'Manrope', system-ui, sans-serif",
      display: 'flex', flexDirection: 'column',
      paddingTop: 52, paddingBottom: 28, boxSizing: 'border-box',
    }}>
      <div style={{ padding: '0 20px' }}>
        <div onClick={onBack} style={{
          width: 34, height: 34, borderRadius: 999,
          border: '1px solid rgba(255,255,255,0.12)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          cursor: 'pointer', WebkitTapHighlightColor: 'transparent',
        }}>
          <svg width="12" height="12" viewBox="0 0 14 14">
            <path d="M9 2L4 7l5 5" fill="none" stroke={palette.ink} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </div>
      </div>

      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 22, padding: '0 28px' }}>
        <div style={{ animation: 'tessera-float 3s ease-in-out infinite', filter: `drop-shadow(0 0 18px ${palette.glow})` }}>
          <svg viewBox="-52 -52 104 104" width="96" height="96">
            {sunPetals.map((pts, i) => (
              <polygon key={i} points={pts}
                fill={isZen ? palette.thick : palette.thin}
                stroke={isZen ? palette.thickEdge : '#FFD700'}
                strokeWidth={isZen ? '1.1' : '2'}
                opacity="0.92" />
            ))}
          </svg>
        </div>

        <div style={{ textAlign: 'center' }}>
          <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 30, fontWeight: 700, letterSpacing: '0.32em' }}>
            {isZen ? 'ZEN' : 'SEEDS'}
          </div>
          <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 10, letterSpacing: '0.2em', color: palette.dim, marginTop: 5 }}>
            {isZen ? 'FREE REVEAL' : 'HUNT SUNS'}
          </div>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 8, alignSelf: 'stretch' }}>
          {steps.map((text, i) => (
            <div key={i} style={{
              display: 'flex', alignItems: 'center', gap: 12,
              padding: '10px 14px', borderRadius: 12,
              background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
            }}>
              <span style={{
                fontFamily: "'JetBrains Mono', monospace", fontSize: 11, fontWeight: 700,
                color: palette.thick, flexShrink: 0, minWidth: 18,
              }}>{String(i + 1).padStart(2, '0')}</span>
              <span style={{ fontSize: 13, color: palette.ink, lineHeight: 1.4 }}>{text}</span>
            </div>
          ))}
        </div>
      </div>

      <div style={{ padding: '0 20px' }}>
        <button onClick={onBegin} style={{
          width: '100%', height: 52, borderRadius: 16,
          background: palette.thick, color: palette.bg1, border: 'none',
          fontFamily: "'JetBrains Mono', monospace", fontSize: 13, fontWeight: 700, letterSpacing: '0.22em',
          cursor: 'pointer', boxShadow: `0 8px 28px rgba(232,121,74,0.3)`,
          WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation',
        }}>BEGIN</button>
      </div>
    </div>
  );
}

// ===== IN-GAME MENU =====

function MenuOverlay({ palette, mode, onResume, onNewGame, onChangeMode }) {
  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 200,
      background: 'rgba(7,9,18,0.92)',
      display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
      gap: 10, padding: '0 32px', boxSizing: 'border-box',
    }}>
      <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 10, letterSpacing: '0.32em', color: palette.dim, marginBottom: 6 }}>PAUSED</div>
      <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 26, fontWeight: 700, letterSpacing: '0.24em', marginBottom: 18 }}>
        {mode === 'zen' ? 'ZEN' : 'SEEDS'}
      </div>

      <button onClick={onResume} style={{
        width: '100%', height: 52, borderRadius: 16,
        background: palette.thick, color: palette.bg1, border: 'none',
        fontFamily: "'JetBrains Mono', monospace", fontSize: 13, fontWeight: 700, letterSpacing: '0.22em',
        cursor: 'pointer', boxShadow: `0 8px 28px ${palette.thick}44`,
        WebkitTapHighlightColor: 'transparent',
      }}>RESUME</button>

      <button onClick={onNewGame} style={{
        width: '100%', height: 46, borderRadius: 14,
        background: 'transparent', border: `1px solid ${palette.dim}`,
        color: palette.ink, fontFamily: "'JetBrains Mono', monospace",
        fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', cursor: 'pointer',
        WebkitTapHighlightColor: 'transparent',
      }}>NEW GAME</button>

      <button onClick={onChangeMode} style={{
        width: '100%', height: 46, borderRadius: 14,
        background: 'transparent', border: `1px solid ${palette.dim}44`,
        color: palette.dim, fontFamily: "'JetBrains Mono', monospace",
        fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', cursor: 'pointer',
        WebkitTapHighlightColor: 'transparent',
      }}>CHANGE MODE</button>
    </div>
  );
}

// ===== END LEVELS MODE =====

function DynamicLogo({ palette }) {
  const canvasRef = React.useRef(null);
  const paletteRef = React.useRef(palette);
  paletteRef.current = palette;

  React.useEffect(() => {
    const SIZE = 120;
    const SCALE = 16;
    const canvas = canvasRef.current;
    if (!canvas || !window.PenroseGeo) return;
    canvas.width = SIZE * 2; canvas.height = SIZE * 2;
    canvas.style.width = SIZE + 'px'; canvas.style.height = SIZE + 'px';
    const ctx = canvas.getContext('2d');
    const { generateTiling, findCenterTile } = window.PenroseGeo;

    function easeOutBack(t) {
      const s = 1.70158;
      return 1 + (s + 1) * Math.pow(t - 1, 3) + s * Math.pow(t - 1, 2);
    }

    let tiles, revealed, revealedAt, queue, queueSet;
    let phase = 'grow', phaseStart = 0, lastReveal = 0;
    let raf;

    function newTiling() {
      const pattern = 0.2 + Math.random() * 0.6;
      tiles = generateTiling({ radius: 12, pattern });
      if (!tiles || tiles.length === 0) return;
      const cIdx = findCenterTile(tiles);
      if (cIdx < 0) return;
      revealed = new Set([cIdx]);
      revealedAt = new Map([[cIdx, performance.now()]]);
      queue = [];
      queueSet = new Set();
      for (const n of tiles[cIdx].neighbors) { queue.push(n); queueSet.add(n); }
      phase = 'grow';
      phaseStart = performance.now();
      lastReveal = performance.now();
    }

    function tick(now) {
      if (!tiles || tiles.length === 0) { raf = requestAnimationFrame(tick); return; }

      const pal = paletteRef.current;
      ctx.clearRect(0, 0, SIZE * 2, SIZE * 2);
      const cx = SIZE, cy = SIZE;

      function toScreen(x, y) { return [cx + x * SCALE, cy + y * SCALE]; }

      function pathTile(t, grow) {
        const tx = t.mean.x, ty = t.mean.y;
        ctx.beginPath();
        t.verts.forEach((v, i) => {
          const lx = tx + (v.x - tx) * grow, ly = ty + (v.y - ty) * grow;
          const [sx, sy] = toScreen(lx, ly);
          i === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
        });
        ctx.closePath();
      }

      if (phase === 'grow') {
        if (now - lastReveal > 140 && queue.length > 0) {
          const id = queue.shift();
          queueSet.delete(id);
          if (!revealed.has(id)) {
            revealed.add(id);
            revealedAt.set(id, now);
            for (const n of tiles[id].neighbors) {
              if (!revealed.has(n) && !queueSet.has(n)) { queue.push(n); queueSet.add(n); }
            }
          }
          lastReveal = now;
        }
        if (revealed.size >= 30 && queue.length === 0) { phase = 'wait'; phaseStart = now; }
      } else if (phase === 'wait') {
        if (now - phaseStart > 800) { phase = 'fade'; phaseStart = now; }
      } else if (phase === 'fade') {
        if (now - phaseStart > 500) { newTiling(); }
      }

      const fadeAlpha = phase === 'fade' ? Math.max(0, 1 - (now - phaseStart) / 500) : 1;

      for (const id of revealed) {
        const t = tiles[id];
        if (!t) continue;
        const age = (now - (revealedAt.get(id) || 0)) / 320;
        const grow = age >= 1 ? 1 : easeOutBack(Math.min(1, Math.max(0, age)));
        pathTile(t, grow);
        ctx.fillStyle = t.type === 'thick' ? pal.thick : pal.thin;
        ctx.globalAlpha = 0.9 * fadeAlpha;
        ctx.fill();
        ctx.lineWidth = 0.8;
        ctx.strokeStyle = t.type === 'thick' ? pal.thickEdge : pal.thinEdge;
        ctx.globalAlpha = 0.55 * fadeAlpha;
        ctx.stroke();
      }
      ctx.globalAlpha = 1;
      raf = requestAnimationFrame(tick);
    }

    newTiling();
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  return (
    <canvas ref={canvasRef} width={240} height={240}
      style={{ width: 120, height: 120, filter: `drop-shadow(0 0 18px ${palette.glow})` }} />
  );
}

// Full-screen ambient Penrose background — always spreading from center
function BackgroundCanvas({ palette }) {
  const canvasRef = useRef(null);
  const palRef = useRef(palette);
  palRef.current = palette;

  useEffect(() => {
    const c = canvasRef.current;
    if (!c || !window.PenroseGeo) return;
    const { generateTiling, findCenterTile } = window.PenroseGeo;
    const SC = 38; // px per unit — matches game default scale
    let W = 402, H = 874, dpr = 2;

    function resize() {
      const p = c.parentElement; if (!p) return;
      W = p.clientWidth; H = p.clientHeight;
      dpr = Math.min(window.devicePixelRatio || 1, 2);
      c.width = W * dpr; c.height = H * dpr;
      c.style.width = W + 'px'; c.style.height = H + 'px';
    }
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(c.parentElement);

    function easeOutBack(t) {
      const s = 1.70158;
      return 1 + (s + 1) * Math.pow(t - 1, 3) + s * Math.pow(t - 1, 2);
    }

    let tiles, revealed, revealedAt, queue, queueSet;
    let phase = 'grow', phaseStart = 0, lastReveal = 0;
    let raf;

    function reset() {
      const pattern = 0.2 + Math.random() * 0.6;
      tiles = generateTiling({ radius: 18, pattern });
      if (!tiles || !tiles.length) return;
      const ci = findCenterTile(tiles);
      if (ci < 0) return;
      revealed = new Set([ci]);
      revealedAt = new Map([[ci, performance.now()]]);
      queue = []; queueSet = new Set();
      tiles[ci].neighbors.forEach(n => { queue.push(n); queueSet.add(n); });
      phase = 'grow'; phaseStart = performance.now(); lastReveal = performance.now();
    }

    function tick(now) {
      if (!tiles || !tiles.length) { raf = requestAnimationFrame(tick); return; }
      const pal = palRef.current;
      const ctx = c.getContext('2d');
      ctx.clearRect(0, 0, c.width, c.height);
      ctx.save();
      ctx.scale(dpr, dpr);
      const cx = W / 2, cy = H / 2;
      function toScr(x, y) { return [cx + x * SC, cy + y * SC]; }
      function pathTile(t, grow) {
        const tx = t.mean.x, ty = t.mean.y;
        ctx.beginPath();
        t.verts.forEach((v, i) => {
          const [sx, sy] = toScr(tx + (v.x - tx) * grow, ty + (v.y - ty) * grow);
          i === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
        });
        ctx.closePath();
      }

      // State machine
      if (phase === 'grow') {
        if (now - lastReveal > 160 && queue.length) {
          for (let k = 0; k < 2 && queue.length; k++) {
            const id = queue.shift(); queueSet.delete(id);
            if (!revealed.has(id)) {
              const t = tiles[id];
              const [sx, sy] = toScr(t.mean.x, t.mean.y);
              // Only reveal tiles visible on screen (+ margin)
              if (sx > -80 && sx < W + 80 && sy > -80 && sy < H + 80) {
                revealed.add(id); revealedAt.set(id, now);
              }
              t.neighbors.forEach(n => {
                if (!revealed.has(n) && !queueSet.has(n)) { queue.push(n); queueSet.add(n); }
              });
            }
          }
          lastReveal = now;
        }
        if (!queue.length) { phase = 'wait'; phaseStart = now; }
      } else if (phase === 'wait') {
        if (now - phaseStart > 2200) { phase = 'fade'; phaseStart = now; }
      } else if (phase === 'fade') {
        if (now - phaseStart > 2600) reset();
      }

      const fa = phase === 'fade' ? Math.max(0, 1 - (now - phaseStart) / 2600) : 1;

      for (const id of revealed) {
        const t = tiles[id]; if (!t) continue;
        const age = (now - (revealedAt.get(id) || 0)) / 550;
        const grow = age >= 1 ? 1 : easeOutBack(Math.min(1, Math.max(0, age)));
        pathTile(t, grow);
        ctx.fillStyle = t.type === 'thick' ? pal.thick : pal.thin;
        ctx.globalAlpha = fa;
        ctx.fill();
        ctx.lineWidth = 1.1;
        ctx.strokeStyle = t.type === 'thick' ? pal.thickEdge : pal.thinEdge;
        ctx.globalAlpha = 0.65 * fa;
        ctx.stroke();
      }
      ctx.globalAlpha = 1;
      ctx.restore();
      raf = requestAnimationFrame(tick);
    }

    reset();
    raf = requestAnimationFrame(tick);
    return () => { cancelAnimationFrame(raf); ro.disconnect(); };
  }, []);

  return (
    <canvas ref={canvasRef} style={{
      position: 'absolute', inset: 0, display: 'block',
      width: '100%', height: '100%', pointerEvents: 'none',
    }} />
  );
}

function Onboarding({ palette, onStart }) {
  const MODES = [
    { id: 'zen',    label: 'ZEN',    sub: 'Free reveal · no targets' },
    { id: 'seeds',  label: 'SEEDS',  sub: 'Hunt hidden Sun patterns' },
    { id: 'levels', label: 'LEVELS', sub: 'Chapter I · Vertex Figures' },
  ];

  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 100,
      display: 'flex', flexDirection: 'column',
      alignItems: 'center', justifyContent: 'center',
      // Radial vignette: dark center for readability, transparent edges → shows BackgroundCanvas
      background: `radial-gradient(ellipse at 50% 48%, ${palette.bg1}f5 0%, ${palette.bg1}d0 35%, ${palette.bg1}88 62%, ${palette.bg1}00 88%)`,
      padding: '0 28px',
      boxSizing: 'border-box',
      WebkitTapHighlightColor: 'transparent',
    }}>
      {/* Dynamic logo */}
      <div style={{ animation: 'tessera-float 3.2s ease-in-out infinite' }}>
        <DynamicLogo palette={palette} />
      </div>

      {/* Tagline above title */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 10, marginTop: 20,
      }}>
        <span style={{ width: 24, height: 1, background: palette.dim, opacity: 0.28, display: 'block' }} />
        <span style={{
          fontFamily: "'JetBrains Mono', monospace",
          fontSize: 9, letterSpacing: '0.44em',
          color: palette.dim, textTransform: 'uppercase',
        }}>APERIODIC · MOSAIC</span>
        <span style={{ width: 24, height: 1, background: palette.dim, opacity: 0.28, display: 'block' }} />
      </div>

      {/* Title */}
      <div style={{
        fontFamily: "'JetBrains Mono', monospace",
        fontSize: 44, fontWeight: 700,
        letterSpacing: '0.22em', color: palette.ink,
        textShadow: `0 0 48px ${palette.glow}66`,
        marginTop: 6,
      }}>
        PENROSE
      </div>

      <div style={{ width: 32, height: 1, background: palette.dim, margin: '28px 0', opacity: 0.2 }} />

      {/* Mode buttons — vertical */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 9, width: '100%', maxWidth: 320 }}>
        {MODES.map(({ id, label, sub }) => (
          <button key={id} onClick={() => onStart(id)} style={{
            width: '100%', height: 54, borderRadius: 14,
            background: 'rgba(255,255,255,0.03)',
            border: `1px solid ${palette.dim}44`,
            cursor: 'pointer',
            display: 'flex', alignItems: 'center',
            padding: '0 16px 0 18px',
            WebkitTapHighlightColor: 'transparent',
            touchAction: 'manipulation',
          }}>
            <span style={{
              fontFamily: "'JetBrains Mono', monospace",
              fontSize: 13, fontWeight: 700, letterSpacing: '0.22em',
              color: palette.ink, minWidth: 68, textAlign: 'left',
            }}>{label}</span>
            <span style={{
              fontFamily: "'Manrope', sans-serif",
              fontSize: 12, color: palette.dim, flex: 1, textAlign: 'left',
            }}>{sub}</span>
            <svg width="7" height="12" viewBox="0 0 7 12" style={{ flexShrink: 0, marginLeft: 8 }}>
              <path d="M1 1l5 5-5 5" fill="none" stroke={palette.dim} strokeWidth="1.5"
                strokeLinecap="round" strokeLinejoin="round" opacity="0.6" />
            </svg>
          </button>
        ))}
      </div>
    </div>
  );
}

function App() {
  const tweaksDef = window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : [TWEAK_DEFAULTS, () => {}];
  const [tweaks, setTweak] = tweaksDef;
  const palette = VIBES[tweaks.vibe] || VIBES.deep;

  const [screen, setScreen] = useState('onboarding');
  const [briefingMode, setBriefingMode] = useState('zen');
  const [showMenu, setShowMenu] = useState(false);
  const [currentLevel, setCurrentLevel] = useState(null);
  const currentLevelRef = useRef(null);
  currentLevelRef.current = currentLevel;

  const [game, setGame] = useState(() => window.PenroseGame.createGame({ mode: 'zen' }));
  const gameRef = useRef(game);
  gameRef.current = game;

  const wonRef = useRef(false);
  const [, force] = useState(0);
  useEffect(() => {
    wonRef.current = false;
    return game.subscribe(() => {
      force(n => n + 1);
      const gs = gameRef.current.state;
      if (currentLevelRef.current && !wonRef.current && gs.placed >= 1) {
        wonRef.current = true;
        setTimeout(() => setScreen('win'), 800);
      }
    });
  }, [game]);

  const canvasRef = useRef(null);
  const cameraRef = useRef({ x: 0, y: 0, scale: 40, targetScale: 40, targetX: 0, targetY: 0 });
  const dprRef = useRef(2);
  const sizeRef = useRef({ w: 390, h: 700 });
  const tweaksRef = useRef(tweaks);
  tweaksRef.current = tweaks;
  const paletteRef = useRef(palette);
  paletteRef.current = palette;

  // Resize
  useEffect(() => {
    function fit() {
      const c = canvasRef.current; if (!c) return;
      const parent = c.parentElement;
      const w = parent.clientWidth, h = parent.clientHeight;
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      dprRef.current = dpr;
      sizeRef.current = { w, h };
      c.width = w * dpr; c.height = h * dpr;
      c.style.width = w + 'px'; c.style.height = h + 'px';
    }
    fit();
    const ro = new ResizeObserver(fit);
    if (canvasRef.current) ro.observe(canvasRef.current.parentElement);
    return () => ro.disconnect();
  }, []);

  // Render loop with auto-camera
  useEffect(() => {
    window.__penroseEffectFired = (window.__penroseEffectFired || 0) + 1;
    let raf;
    let frames = 0;
    function tick() {
      frames++;
      window.__penroseFrames = frames;
      const game = gameRef.current;
      const cam = cameraRef.current;
      const { w, h } = sizeRef.current;
      const tiles = game.state.tiles;
      // Compute bbox of revealed tile vertices (camera follows revealed area)
      let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
      const ids = game.state.revealOrder;
      for (const id of ids) {
        const t = tiles[id];
        for (const v of t.verts) {
          if (v.x < minX) minX = v.x;
          if (v.y < minY) minY = v.y;
          if (v.x > maxX) maxX = v.x;
          if (v.y > maxY) maxY = v.y;
        }
      }
      if (!isFinite(minX)) { minX = -1; maxX = 1; minY = -1; maxY = 1; }
      const cx = (minX + maxX) / 2;
      const cy = (minY + maxY) / 2;
      // Add padding by including 1.5 ring of neighbors visually
      const spanX = (maxX - minX) || 1;
      const spanY = (maxY - minY) || 1;
      const margin = 2.4; // unit-edges of breathing
      const fitScale = Math.min(w / (spanX + margin * 2), h / (spanY + margin * 2));
      const baseScale = tweaksRef.current.tileScale;
      const targetScale = tweaksRef.current.autoCamera
        ? Math.min(baseScale, Math.max(16, fitScale))
        : baseScale;
      cam.targetScale = targetScale;
      cam.targetX = cx;
      cam.targetY = cy;
      const k = 0.08;
      cam.x += (cam.targetX - cam.x) * k;
      cam.y += (cam.targetY - cam.y) * k;
      cam.scale += (cam.targetScale - cam.scale) * k;

      render();
      raf = requestAnimationFrame(tick);
    }
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  function worldToScreen(x, y) {
    const { w, h } = sizeRef.current;
    const cam = cameraRef.current;
    return { x: w / 2 + (x - cam.x) * cam.scale, y: h / 2 + (y - cam.y) * cam.scale };
  }
  function screenToWorld(sx, sy) {
    const { w, h } = sizeRef.current;
    const cam = cameraRef.current;
    return { x: (sx - w / 2) / cam.scale + cam.x, y: (sy - h / 2) / cam.scale + cam.y };
  }

  function render() {
    const game = gameRef.current;
    const c = canvasRef.current; if (!c) return;
    const ctx = c.getContext('2d');
    const dpr = dprRef.current;
    const { w, h } = sizeRef.current;
    const pal = paletteRef.current;
    const tw = tweaksRef.current;
    const gst = game.state;

    ctx.save();
    ctx.scale(dpr, dpr);

    // Background
    const g = ctx.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, Math.max(w, h) * 0.8);
    g.addColorStop(0, pal.bg2);
    g.addColorStop(1, pal.bg1);
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, w, h);

    if (tw.showGuides) {
      ctx.strokeStyle = pal.dim;
      ctx.globalAlpha = 0.12;
      ctx.lineWidth = 1;
      const cam = cameraRef.current;
      for (let r = 1; r <= 12; r++) {
        ctx.beginPath();
        ctx.arc(w / 2 - cam.x * cam.scale, h / 2 - cam.y * cam.scale, r * cam.scale, 0, Math.PI * 2);
        ctx.stroke();
      }
      ctx.globalAlpha = 1;
    }

    const now = performance.now();
    const cam = cameraRef.current;
    const tiles = game.state.tiles;

    // 1) Hidden tiles that are tappable (have revealed neighbor) — show as ghosts
    // 2) Hidden tiles with at least one tappable-neighbor — show very faintly
    // We compute a per-tile "visibility" level for layering.
    const revealed = game.state.revealed;
    const tappable = new Set();
    for (const id of game.state.revealOrder) {
      const t = tiles[id];
      for (const n of t.neighbors) {
        if (!revealed.has(n)) tappable.add(n);
      }
    }
    // Second-ring: neighbors of tappable tiles (faint outlines for atmosphere)
    const secondRing = new Set();
    for (const id of tappable) {
      const t = tiles[id];
      for (const n of t.neighbors) {
        if (!revealed.has(n) && !tappable.has(n)) secondRing.add(n);
      }
    }

    // --- Draw second ring (faint outlines)
    ctx.lineWidth = 1;
    for (const id of secondRing) {
      const t = tiles[id];
      const sp0 = worldToScreen(t.verts[0].x, t.verts[0].y);
      // Frustum cull
      if (sp0.x < -50 || sp0.x > w + 50 || sp0.y < -50 || sp0.y > h + 50) continue;
      pathTile(ctx, t, 1);
      ctx.globalAlpha = 0.18;
      ctx.strokeStyle = pal.hiddenEdge;
      ctx.stroke();
    }
    ctx.globalAlpha = 1;

    // --- Draw tappable tiles (highlighted candidates)
    const isSeedsMode = gst.mode === 'seeds';
    for (const id of tappable) {
      const t = tiles[id];
      const isSunMember = isSeedsMode && t.sunIndices && t.sunIndices.some(si => !gst.collectedSuns.has(si));

      pathTile(ctx, t, 1);
      ctx.fillStyle = pal.tappable;
      ctx.globalAlpha = 0.55;
      ctx.fill();

      const phase = (now / 800 + (id * 0.137)) % 1;
      const pulse = 0.5 + 0.5 * Math.sin(phase * Math.PI * 2);

      if (isSunMember) {
        // Gold pulsing stroke for sun-member tappable tiles
        ctx.lineWidth = 2 + pulse * 1.2;
        ctx.strokeStyle = '#FFD700';
        ctx.globalAlpha = 0.7 + pulse * 0.3;
        if (tw.tappableGlow) {
          ctx.shadowBlur = 12 + pulse * 16;
          ctx.shadowColor = '#FFD700';
        }
        ctx.stroke();
        ctx.shadowBlur = 0;
      } else {
        ctx.lineWidth = 1.4 + pulse * 0.8;
        ctx.strokeStyle = pal.tappableEdge;
        ctx.globalAlpha = 0.55 + pulse * 0.4;
        if (tw.tappableGlow) {
          ctx.shadowBlur = 8 + pulse * 8;
          ctx.shadowColor = pal.glow;
        }
        ctx.stroke();
        ctx.shadowBlur = 0;
      }
    }
    ctx.globalAlpha = 1;

    // --- Draw revealed tiles (THIS IS THE MAIN ATTRACTION)
    // Sort by reveal time so newer tiles draw on top.
    const order = game.state.revealOrder;
    for (let oi = 0; oi < order.length; oi++) {
      const id = order[oi];
      const t = tiles[id];
      const bornAt = game.state.revealedAt.get(id) || 0;
      const age = (now - bornAt) / 360;
      const grow = age >= 1 ? 1 : easeOutBack(Math.max(0, age));

      pathTile(ctx, t, grow);

      // Fill — distinct colors per type
      ctx.fillStyle = t.type === 'thick' ? pal.thick : pal.thin;
      ctx.globalAlpha = 1;
      ctx.fill();

      // Edge stroke (slight)
      ctx.lineWidth = 1.2;
      ctx.strokeStyle = t.type === 'thick' ? pal.thickEdge : pal.thinEdge;
      ctx.globalAlpha = 0.65;
      ctx.stroke();

      // Fresh glow
      const glowAge = now - bornAt;
      if (glowAge < 800) {
        const tF = 1 - glowAge / 800;
        ctx.globalAlpha = tF * 0.8;
        ctx.lineWidth = 2;
        ctx.shadowBlur = 22 * tF;
        ctx.shadowColor = pal.glow;
        ctx.stroke();
        ctx.shadowBlur = 0;
      }
      ctx.globalAlpha = 1;
    }

    // Gold stroke on revealed sun-member tiles
    if (isSeedsMode && gst.suns.length > 0) {
      for (let si = 0; si < gst.suns.length; si++) {
        const isCollected = gst.collectedSuns.has(si);
        for (const tid of gst.suns[si].tileIds) {
          if (!revealed.has(tid)) continue;
          const tSun = tiles[tid];
          const bornAt = game.state.revealedAt.get(tid) || 0;
          const age = (now - bornAt) / 360;
          const grow = age >= 1 ? 1 : easeOutBack(Math.max(0, age));
          pathTile(ctx, tSun, grow);
          if (isCollected) {
            ctx.lineWidth = 2.5;
            ctx.strokeStyle = '#FFD700';
            ctx.globalAlpha = 0.9;
            ctx.shadowBlur = 6;
            ctx.shadowColor = '#FFD700';
            ctx.stroke();
            ctx.shadowBlur = 0;
          } else {
            const pulse = 0.6 + 0.4 * Math.sin(now / 700 + tid * 0.41);
            ctx.lineWidth = 1.6;
            ctx.strokeStyle = '#FFD700';
            ctx.globalAlpha = 0.35 * pulse;
            ctx.stroke();
          }
        }
      }
      ctx.globalAlpha = 1;
    }

    // Ripples (drawn on top)
    for (let i = game.state.ripples.length - 1; i >= 0; i--) {
      const r = game.state.ripples[i];
      const t = (now - r.start) / r.duration;
      if (t > 1) { game.state.ripples.splice(i, 1); continue; }
      const ease = 1 - Math.pow(1 - t, 2);
      const sp = worldToScreen(r.x, r.y);
      const rippleColor = r.big ? '#FFD700' : pal.glow;
      const rippleRadius = ease * (r.big ? 10 : 5) * cam.scale;
      ctx.beginPath();
      ctx.arc(sp.x, sp.y, rippleRadius, 0, Math.PI * 2);
      ctx.strokeStyle = rippleColor;
      ctx.globalAlpha = (1 - t) * (r.big ? 0.7 : 0.5);
      ctx.lineWidth = r.big ? 3 : 2;
      ctx.stroke();
      if (r.big && t < 0.5) {
        ctx.beginPath();
        ctx.arc(sp.x, sp.y, ease * 4 * cam.scale, 0, Math.PI * 2);
        ctx.globalAlpha = (1 - t * 2) * 0.3;
        ctx.fillStyle = '#FFD700';
        ctx.fill();
      }
    }
    ctx.globalAlpha = 1;

    ctx.restore();
  }

  function pathTile(ctx, tile, grow) {
    const verts = tile.verts;
    const cx = tile.mean.x, cy = tile.mean.y;
    ctx.beginPath();
    for (let i = 0; i < verts.length; i++) {
      const lx = cx + (verts[i].x - cx) * grow;
      const ly = cy + (verts[i].y - cy) * grow;
      const sp = worldToScreen(lx, ly);
      if (i === 0) ctx.moveTo(sp.x, sp.y);
      else ctx.lineTo(sp.x, sp.y);
    }
    ctx.closePath();
  }

  function easeOutBack(t) {
    const s = 1.70158;
    return 1 + (s + 1) * Math.pow(t - 1, 3) + s * Math.pow(t - 1, 2);
  }

  // Pointer
  function handlePointer(e) {
    const rect = canvasRef.current.getBoundingClientRect();
    const sx = (e.clientX || (e.touches && e.touches[0]?.clientX)) - rect.left;
    const sy = (e.clientY || (e.touches && e.touches[0]?.clientY)) - rect.top;
    const wp = screenToWorld(sx, sy);
    // Only consider tappable tiles
    const tiles = game.state.tiles;
    const revealed = game.state.revealed;
    let bestId = -1, bestD = Infinity;
    for (let id = 0; id < tiles.length; id++) {
      if (revealed.has(id)) continue;
      // quick reject by mean distance
      const t = tiles[id];
      const dx = t.mean.x - wp.x, dy = t.mean.y - wp.y;
      const d2 = dx * dx + dy * dy;
      if (d2 > 4) continue;
      if (window.PenroseGeo.pointInPoly(wp.x, wp.y, t.verts)) {
        if (d2 < bestD) { bestD = d2; bestId = id; }
      }
    }
    if (bestId >= 0) {
      const ok = game.reveal(bestId);
      if (ok && tweaks.haptics && navigator.vibrate) navigator.vibrate(8);
      else if (!ok && tweaks.haptics && navigator.vibrate) navigator.vibrate([2, 24, 2]);
    }
  }

  // Hint: pick a random tappable tile, briefly mark it
  function takeHint() {
    const gst = game.state;
    const tiles = gst.tiles;
    const revealed = gst.revealed;
    const tappable = [];
    for (const id of gst.revealOrder) {
      for (const n of tiles[id].neighbors) {
        if (!revealed.has(n)) tappable.push(n);
      }
    }
    if (!tappable.length) return;

    let bestId = tappable[0];
    if (gst.mode === 'seeds') {
      // prefer tappable tiles that belong to an uncollected sun
      const sunMembers = tappable.filter(id => {
        const t = tiles[id];
        return t.sunIndices && t.sunIndices.some(si => !gst.collectedSuns.has(si));
      });
      const pool = sunMembers.length ? sunMembers : tappable;
      // among candidates pick highest sun progress
      let bestProgress = -1;
      for (const id of pool) {
        const t = tiles[id];
        let progress = 0;
        if (t.sunIndices) {
          for (const si of t.sunIndices) {
            if (!gst.collectedSuns.has(si)) progress = Math.max(progress, gst.sunProgress.get(si) || 0);
          }
        }
        if (progress > bestProgress) { bestProgress = progress; bestId = id; }
      }
    } else {
      // pick farthest from camera center for "growth" hint
      const cam = cameraRef.current;
      let bestD = -Infinity;
      for (const id of tappable) {
        const t = tiles[id];
        const dx = t.mean.x - cam.x, dy = t.mean.y - cam.y;
        const d = dx * dx + dy * dy;
        if (d > bestD) { bestD = d; bestId = id; }
      }
    }

    game.reveal(bestId);
    if (tweaks.haptics && navigator.vibrate) navigator.vibrate(8);
  }

  const s = game.state;
  const goalProgress = Math.min(1, s.placed / s.goalTarget);

  return (
    <div className="ph-root" style={{ background: palette.bg1, color: palette.ink }}>

      {/* ── Always-on ambient background ── */}
      <div style={{
        position: 'absolute', inset: 0, zIndex: 0, pointerEvents: 'none',
        opacity: screen === 'onboarding' ? 0.55 : 0.1,
        transition: 'opacity 1.4s ease',
      }}>
        <BackgroundCanvas palette={palette} />
      </div>

      {screen === 'onboarding' && (
        <Onboarding palette={palette} onStart={(mode) => {
          if (mode === 'levels') {
            setScreen('map');
          } else {
            setBriefingMode(mode);
            setScreen('mode-briefing');
          }
        }} />
      )}
      {screen === 'mode-briefing' && (
        <ModeBriefing
          mode={briefingMode}
          palette={palette}
          onBack={() => setScreen('onboarding')}
          onBegin={() => {
            setGame(window.PenroseGame.createGame({ mode: briefingMode }));
            setCurrentLevel(null);
            wonRef.current = false;
            setScreen('game');
          }}
        />
      )}
      {screen === 'map' && (
        <MapScreen palette={palette}
          onBack={() => setScreen('onboarding')}
          onSelect={(lvl) => { setCurrentLevel(lvl); setScreen('briefing'); }} />
      )}
      {screen === 'briefing' && currentLevel && (
        <LevelBriefing level={currentLevel} palette={palette}
          onBack={() => setScreen('map')}
          onBegin={() => {
            setGame(window.PenroseGame.createGame({ mode: 'seeds' }));
            setScreen('game');
          }} />
      )}
      {screen === 'win' && currentLevel && (
        <WinScreen level={currentLevel} gameState={s} palette={palette}
          onRetry={() => {
            wonRef.current = false;
            setGame(window.PenroseGame.createGame({ mode: 'seeds' }));
            setScreen('game');
          }}
          onNext={() => setScreen('map')} />
      )}
      {showMenu && (
        <MenuOverlay
          palette={palette}
          mode={s.mode}
          onResume={() => setShowMenu(false)}
          onNewGame={() => {
            wonRef.current = false;
            setGame(window.PenroseGame.createGame({ mode: s.mode }));
            setShowMenu(false);
          }}
          onChangeMode={() => {
            setShowMenu(false);
            setScreen('onboarding');
          }}
        />
      )}

      <div className="ph-hud-top" style={{ gridTemplateColumns: '1fr 1fr 1fr auto' }}>
        <div className="ph-hud-cell">
          <div className="ph-hud-label" style={{ color: palette.dim }}>SCORE</div>
          <div className="ph-hud-value">{s.score}</div>
        </div>
        <div className="ph-hud-cell ph-hud-center">
          <div className="ph-hud-label" style={{ color: palette.dim }}>LEVEL</div>
          <div className="ph-hud-value">{s.level}</div>
        </div>
        <div className="ph-hud-cell ph-hud-right">
          <div className="ph-hud-label" style={{ color: palette.dim }}>HI</div>
          <div className="ph-hud-value">{s.hi}</div>
        </div>
        <div style={{ display: 'flex', alignItems: 'flex-end', paddingBottom: 12, paddingLeft: 6 }}>
          <button onClick={() => setShowMenu(true)} style={{
            background: 'transparent', border: 'none', cursor: 'pointer', padding: 5,
            WebkitTapHighlightColor: 'transparent', display: 'flex', flexDirection: 'column', gap: 4,
          }}>
            <span style={{ display: 'block', width: 18, height: 2, background: palette.dim, borderRadius: 1 }} />
            <span style={{ display: 'block', width: 12, height: 2, background: palette.dim, borderRadius: 1 }} />
            <span style={{ display: 'block', width: 18, height: 2, background: palette.dim, borderRadius: 1 }} />
          </button>
        </div>
      </div>

      <div className="ph-canvas-wrap" onPointerDown={handlePointer}>
        <canvas ref={canvasRef} className="ph-canvas" />
      </div>

      <div className="ph-hud-bottom">
        {s.mode === 'seeds' ? (
          <div className="ph-tile-counts" style={{ display: 'flex', justifyContent: 'center', gap: 18, fontFamily: "'JetBrains Mono', monospace", fontSize: 11, letterSpacing: '0.14em' }}>
            <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <span style={{ fontSize: 16, lineHeight: 1 }}>☀</span>
              <span style={{ color: palette.dim }}>SUNS</span>
              <span style={{ color: '#FFD700', fontWeight: 700 }}>{s.placed}</span>
            </span>
            <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
              <span style={{ color: palette.dim }}>COMBO ×</span>
              <span style={{ color: s.combo > 3 ? '#FFD700' : palette.ink }}>{s.combo}</span>
            </span>
          </div>
        ) : (
          <div className="ph-tile-counts" style={{ display: 'flex', justifyContent: 'center', gap: 18, fontFamily: "'JetBrains Mono', monospace", fontSize: 11, letterSpacing: '0.14em' }}>
            <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
              <span style={{ color: palette.dim }}>TILES</span>
              <span style={{ color: palette.ink }}>{s.revealed.size}</span>
            </span>
            <span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
              <span style={{ color: palette.dim }}>#</span>
              <span style={{ color: palette.thin, letterSpacing: '0.08em' }}>{s.patternId || '????'}</span>
            </span>
            <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
              <span style={{ color: palette.dim }}>×</span>
              <span style={{ color: s.combo > 3 ? palette.thick : palette.ink }}>{s.combo}</span>
            </span>
          </div>
        )}
        <div className="ph-goal">
          <div className="ph-goal-label" style={{ color: palette.dim, display: 'flex', justifyContent: 'space-between' }}>
            <span>{s.mode === 'seeds' ? `GOAL · COLLECT ${s.goalTarget} SUNS` : `GOAL · REVEAL ${s.goalTarget}`}</span>
            <span style={{ color: palette.ink }}>{s.placed}/{s.goalTarget}</span>
          </div>
          <div className="ph-goal-bar" style={{ background: palette.bg2 }}>
            <div className="ph-goal-fill" style={{
              width: (goalProgress * 100) + '%',
              background: palette.thick,
              boxShadow: `0 0 12px ${palette.glow}`,
            }} />
          </div>
        </div>
        <div className="ph-hud-actions">
          <button className="ph-btn" style={{ borderColor: palette.dim, color: palette.ink }}
            onClick={() => game.reset()}>RESTART</button>
          <button className="ph-btn ph-btn-primary"
            style={{ background: palette.thick, color: palette.bg1, borderColor: palette.thick }}
            onClick={takeHint}>HINT</button>
        </div>
      </div>

      {window.TweaksPanel && (
        <window.TweaksPanel title="Tweaks">
          <window.TweakSection label="Look" />
          <window.TweakSelect label="Vibe" value={tweaks.vibe}
            options={[
              { value: 'deep', label: 'Deep' },
              { value: 'paper', label: 'Paper' },
              { value: 'neon', label: 'Neon' },
              { value: 'mono', label: 'Mono' },
              { value: 'ascii', label: 'ASCII' },
            ]}
            onChange={v => setTweak('vibe', v)} />
          <window.TweakToggle label="Radial guides" value={tweaks.showGuides}
            onChange={v => setTweak('showGuides', v)} />
          <window.TweakToggle label="Tappable glow" value={tweaks.tappableGlow}
            onChange={v => setTweak('tappableGlow', v)} />
          <window.TweakSection label="Feel" />
          <window.TweakToggle label="Auto-camera" value={tweaks.autoCamera}
            onChange={v => setTweak('autoCamera', v)} />
          <window.TweakToggle label="Haptics" value={tweaks.haptics}
            onChange={v => setTweak('haptics', v)} />
          <window.TweakSlider label="Tile scale" min={20} max={70} step={1}
            value={tweaks.tileScale} onChange={v => setTweak('tileScale', v)} />
        </window.TweaksPanel>
      )}
    </div>
  );
}

window.PenroseApp = App;
