// widgets.jsx — shared widgets used across screens
// Each is small and composable. All accept T via reading window.T.

// ──────────────────────────────────────────────────────────────
// 곳간(즐겨찾기) 훅 — 세션 전역 window.__cpSeedFavs 에 읽고 쓴다.
// useLangPrefs 와 동일한 패턴: 화면 간 토글 상태 공유.
// ──────────────────────────────────────────────────────────────
window.__cpSeedFavs = window.__cpSeedFavs || [];
function useSeedFavs() {
  const [favs, _set] = React.useState(window.__cpSeedFavs);
  const setFavs = (f) => { window.__cpSeedFavs = f; _set(f); };
  return {
    favs,
    isFav: (id) => favs.includes(id),
    // returns the NEW state (true = now in 곳간)
    toggle: (id) => {
      const now = !favs.includes(id);
      setFavs(now ? [...favs, id] : favs.filter(x => x !== id));
      return now;
    },
  };
}

// ──────────────────────────────────────────────────────────────
// Hamster mascot (CheekPouch) — 6 reference frames
// ──────────────────────────────────────────────────────────────
const _HAMSTER_BASE = (typeof window !== 'undefined' && window.__CP_ASSETS) || 'assets/';
const _HAMSTER_FRAMES = [
  { upTo: 0.05, src: _HAMSTER_BASE + 'hamster-h0.png',   label: 0 },
  { upTo: 0.20, src: _HAMSTER_BASE + 'hamster-h10.png',  label: 10 },
  { upTo: 0.40, src: _HAMSTER_BASE + 'hamster-h30.png',  label: 30 },
  { upTo: 0.60, src: _HAMSTER_BASE + 'hamster-h50.png',  label: 50 },
  { upTo: 0.85, src: _HAMSTER_BASE + 'hamster-h70.png',  label: 70 },
  { upTo: 1.01, src: _HAMSTER_BASE + 'hamster-h100.png', label: 100 },
];
const _HAMSTER_ASPECT = 230 / 340;

function _pickFrame(fill) {
  for (const f of _HAMSTER_FRAMES) if (fill <= f.upTo) return f;
  return _HAMSTER_FRAMES[_HAMSTER_FRAMES.length - 1];
}

function Hamster({ count = 0, max = 10, size = 140, headOnly = false }) {
  const fill = Math.min(Math.max(count / Math.max(1, max), 0), 1);
  const frame = _pickFrame(fill);
  const W = size;
  const H = Math.round(size * _HAMSTER_ASPECT);
  if (headOnly) {
    const imgW = Math.round(size * 1.45);
    const imgH = Math.round(imgW * _HAMSTER_ASPECT);
    const top = Math.round(size / 2 - 0.30 * imgH);
    return (
      <div style={{
        position: 'relative', display: 'inline-block',
        width: size, height: size,
        overflow: 'hidden', borderRadius: '50%',
        lineHeight: 0,
      }}>
        <img src={frame.src} alt="" draggable={false} style={{
          position: 'absolute', width: imgW, height: imgH,
          left: '50%', top, transform: 'translateX(-50%)',
          userSelect: 'none', pointerEvents: 'none',
        }}/>
      </div>
    );
  }
  return (
    <div style={{ display: 'inline-block', width: W, height: H, lineHeight: 0 }}>
      <img src={frame.src} alt="" draggable={false}
           style={{ width: W, height: H, display: 'block',
                    userSelect: 'none', pointerEvents: 'none' }}/>
    </div>
  );
}

// Big animated breathing mascot (for processing screen)
function HamsterAnimated({ size = 200 }) {
  const [phase, setPhase] = React.useState(0);
  React.useEffect(() => {
    const t = setInterval(() => setPhase(p => (p + 1) % 4), 620);
    return () => clearInterval(t);
  }, []);
  const counts = [3, 6, 9, 6];
  return (
    <div style={{ display: 'inline-block', animation: 'cp-breathe 1.6s ease-in-out infinite' }}>
      <Hamster count={counts[phase]} max={10} size={size}/>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Buttons
// ──────────────────────────────────────────────────────────────
function Button({ kind = 'primary', size = 'md', icon, iconRight, disabled, busy, onClick, children, danger, fullWidth, color, style }) {
  const T = window.T;
  const heights = { sm: 36, md: 48, lg: 52 };
  const pads    = { sm: '0 14px', md: '0 18px', lg: '0 20px' };
  const fontS   = { sm: 13.5,     md: 15,        lg: 15.5 };

  let bg, fg, border;
  const accent = danger ? T.danger : (color || T.accent);
  if (kind === 'primary') {
    bg = accent; fg = '#FFFFFF'; border = 'none';
  } else if (kind === 'secondary') {
    bg = T.surface; fg = T.ink; border = `0.5px solid ${T.hairlineStrong}`;
  } else if (kind === 'ghost') {
    bg = 'transparent'; fg = T.ink; border = 'none';
  } else if (kind === 'soft') {
    bg = danger ? T.dangerSoft : T.accentSoft; fg = accent; border = 'none';
  } else if (kind === 'outline') {
    bg = 'transparent'; fg = accent; border = `1.5px solid ${accent}`;
  }
  return (
    <button onClick={onClick} disabled={disabled || busy} style={{
      height: heights[size], padding: pads[size],
      background: bg, color: fg, border,
      borderRadius: 14,
      fontFamily: 'inherit', fontSize: fontS[size], fontWeight: 600,
      letterSpacing: '-0.01em',
      cursor: (disabled || busy) ? 'default' : 'pointer',
      opacity: disabled ? 0.4 : 1,
      width: fullWidth ? '100%' : undefined,
      display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8,
      transition: 'opacity .18s, transform .12s',
      WebkitTapHighlightColor: 'transparent',
      ...style,
    }}>
      {busy ? <Spinner color={fg}/> : icon}
      {children && <span style={{ whiteSpace: 'nowrap' }}>{children}</span>}
      {!busy && iconRight}
    </button>
  );
}

function IconButton({ icon, onClick, size = 36, disabled, danger, color, bg, style }) {
  const T = window.T;
  return (
    <button onClick={onClick} disabled={disabled} style={{
      width: size, height: size, borderRadius: '50%',
      background: bg || 'transparent', border: 'none',
      color: danger ? T.danger : (color || T.ink),
      cursor: disabled ? 'default' : 'pointer',
      opacity: disabled ? 0.4 : 1,
      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
      WebkitTapHighlightColor: 'transparent',
      ...style,
    }}>{icon}</button>
  );
}

function Spinner({ color = '#FFF', size = 18 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
         style={{ animation: 'cp-spin 0.9s linear infinite', flexShrink: 0 }}>
      <circle cx="12" cy="12" r="9" stroke={color} strokeOpacity="0.25" strokeWidth="2.4" fill="none"/>
      <path d="M21 12a9 9 0 0 0-9-9" stroke={color} strokeWidth="2.4" strokeLinecap="round" fill="none"/>
    </svg>
  );
}

// ──────────────────────────────────────────────────────────────
// Chips, segments, toggles
// ──────────────────────────────────────────────────────────────
function Chip({ active, onClick, children, accent, danger, leadingDot, style }) {
  const T = window.T;
  const ac = danger ? T.danger : (accent || T.accent);
  return (
    <button onClick={onClick} style={{
      height: 32, padding: '0 12px',
      background: active ? ac : T.surface,
      color: active ? '#FFF' : T.ink2,
      border: active ? `1px solid ${ac}` : `0.5px solid ${T.hairlineStrong}`,
      borderRadius: 16,
      fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600,
      letterSpacing: '-0.01em', cursor: onClick ? 'pointer' : 'default',
      display: 'inline-flex', alignItems: 'center', gap: 6,
      WebkitTapHighlightColor: 'transparent',
      whiteSpace: 'nowrap',
      ...style,
    }}>
      {leadingDot && <span style={{ width: 8, height: 8, borderRadius: 4, background: leadingDot }}/>}
      {children}
    </button>
  );
}

function Segment({ options, value, onChange, size = 'md' }) {
  const T = window.T;
  const heights = { sm: 30, md: 34 };
  return (
    <div style={{
      display: 'inline-flex', padding: 3, borderRadius: 9,
      background: T.segmentTrack, height: heights[size],
    }}>
      {options.map(o => {
        const selected = value === o.value;
        return (
          <button key={o.value} onClick={() => onChange(o.value)} style={{
            height: heights[size] - 6, padding: '0 12px',
            background: selected ? T.segmentSelected : 'transparent',
            color: selected ? T.ink : T.ink2,
            border: 'none',
            borderRadius: 7,
            fontFamily: 'inherit', fontSize: 12.5, fontWeight: selected ? 600 : 500,
            cursor: 'pointer', whiteSpace: 'nowrap',
            boxShadow: selected ? T.segmentShadow : 'none',
            transition: 'background .15s, color .15s',
            WebkitTapHighlightColor: 'transparent',
          }}>{o.label}</button>
        );
      })}
    </div>
  );
}

function Toggle({ value, onChange, size = 'md' }) {
  const T = window.T;
  const W = size === 'sm' ? 38 : 46;
  const H = size === 'sm' ? 22 : 28;
  const knob = H - 4;
  return (
    <button onClick={() => onChange(!value)} style={{
      width: W, height: H, borderRadius: H,
      background: value ? T.accent : 'rgba(60, 42, 20, 0.18)',
      border: 'none', position: 'relative',
      cursor: 'pointer', padding: 0,
      transition: 'background .2s',
      WebkitTapHighlightColor: 'transparent',
    }}>
      <span style={{
        position: 'absolute', top: 2, left: value ? W - knob - 2 : 2,
        width: knob, height: knob, borderRadius: knob,
        background: '#FFF',
        boxShadow: '0 1.5px 3px rgba(0,0,0,0.18)',
        transition: 'left .2s ease',
      }}/>
    </button>
  );
}

// ──────────────────────────────────────────────────────────────
// Search bar
// ──────────────────────────────────────────────────────────────
function SearchBar({ value, onChange, onFocus, onBlur, focused, placeholder = '표현, 단어, 주머니 검색', onCancel, autoFocus }) {
  const T = window.T;
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (autoFocus && ref.current) ref.current.focus();
  }, [autoFocus]);
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%' }}>
      <div style={{
        flex: 1, height: 44, borderRadius: 12,
        background: T.surface, border: `0.5px solid ${T.hairlineStrong}`,
        display: 'flex', alignItems: 'center',
        padding: '0 12px', gap: 8,
      }}>
        <Icons.Search size={18} color={T.ink3}/>
        <input ref={ref}
          value={value || ''} onChange={e => onChange && onChange(e.target.value)}
          onFocus={onFocus} onBlur={onBlur}
          placeholder={placeholder}
          style={{
            flex: 1, border: 'none', outline: 'none', background: 'transparent',
            color: T.ink, fontSize: 14.5, fontFamily: 'inherit',
            letterSpacing: '-0.01em',
          }}/>
        {value && value.length > 0 && (
          <button onClick={() => onChange && onChange('')} style={{
            width: 20, height: 20, borderRadius: 10, padding: 0,
            background: 'rgba(60,42,20,0.18)', border: 'none',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            color: '#FFF', cursor: 'pointer',
          }}><Icons.Close size={12} color="#FFF"/></button>
        )}
      </div>
      {focused && onCancel && (
        <button onClick={onCancel} style={{
          background: 'transparent', border: 'none', color: T.accent,
          fontSize: 14.5, fontWeight: 600, padding: '0 4px', cursor: 'pointer',
        }}>취소</button>
      )}
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Seed card (compact list row, both regular + mini-play)
// ──────────────────────────────────────────────────────────────
function SeedCard({ seed, playing, paused, onPlay, onClick, style }) {
  const T = window.T;
  const PlayingWave = window.PlayingWave;
  const highlighted = playing || paused;
  const starred = (window.__cpSeedFavs || []).includes(seed.id);
  return (
    <div onClick={onClick} style={{
      background: T.surface, borderRadius: 14,
      border: `0.5px solid ${T.hairline}`,
      padding: '14px 14px', display: 'flex', alignItems: 'center', gap: 12,
      cursor: onClick ? 'pointer' : 'default',
      transition: 'background .15s, border-color .15s, box-shadow .15s',
      ...style,
    }}>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{
          fontSize: 11.5, fontWeight: 600,
          color: highlighted ? T.accent : T.ink3,
          letterSpacing: '0.04em', textTransform: 'uppercase',
          marginBottom: 4,
          display: 'flex', alignItems: 'center', gap: 5,
        }}>
          {playing && PlayingWave && <PlayingWave color={T.accent} size={10}/>}
          {paused && !playing && <Icons.Pause size={9} color={T.accent}/>}
          <span>{seed.langLabel}</span>
        </div>
        <div style={{
          fontSize: 16, fontWeight: 600, color: T.ink,
          letterSpacing: '-0.01em',
          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          marginBottom: 2,
        }}>{seed.foreign}</div>
        <div style={{
          fontSize: 12.5, color: T.ink2,
          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        }}>{seed.korean}</div>
      </div>
      {starred && (
        <span style={{ display: 'inline-flex', flexShrink: 0 }} aria-label="곳간에 담김">
          <Icons.Star size={15} color={T.accent} filled/>
        </span>
      )}
      <button onClick={(e) => { e.stopPropagation(); onPlay && onPlay(); }} style={{
        width: 36, height: 36, borderRadius: '50%',
        background: playing ? T.accent : T.inkOverlaySoft,
        border: 'none', padding: 0,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        color: playing ? '#FFF' : T.ink, cursor: 'pointer',
        flexShrink: 0,
        animation: playing ? 'cp-mic-pulse 1.6s ease-in-out infinite' : 'none',
        transition: 'box-shadow .2s, background .15s',
      }}>
        {playing
          ? <Icons.Stop size={14} color="#FFF"/>
          : <Icons.Play size={14} color={T.ink} fillTri/>}
      </button>
    </div>
  );
}

// Seed card horizontal (for "recently viewed" carousel)
function SeedCardMini({ seed, onClick, playing, paused }) {
  const T = window.T;
  const highlighted = playing || paused;
  const starred = (window.__cpSeedFavs || []).includes(seed.id);
  return (
    <div onClick={onClick} style={{
      width: 200, flexShrink: 0, position: 'relative',
      background: T.surface, borderRadius: 14,
      border: `0.5px solid ${highlighted ? T.accent : T.hairline}`,
      boxShadow: highlighted ? `0 0 0 1px ${T.accent}` : 'none',
      padding: 14, cursor: 'pointer',
      transition: 'border-color .15s, box-shadow .15s',
    }}>
      {starred && (
        <span style={{ position: 'absolute', top: 12, right: 12, display: 'inline-flex' }}
              aria-label="곳간에 담김">
          <Icons.Star size={15} color={T.accent} filled/>
        </span>
      )}      <div style={{
        fontSize: 10.5, fontWeight: 600,
        color: highlighted ? T.accent : T.ink3,
        letterSpacing: '0.06em', textTransform: 'uppercase',
        marginBottom: 6,
        display: 'flex', alignItems: 'center', gap: 4,
      }}>
        {playing && <PlayingWave color={T.accent} size={10}/>}
        {paused && !playing && (
          <Icons.Pause size={9} color={T.accent}/>
        )}
        {seed.langLabel}
      </div>
      <div style={{
        fontSize: 15, fontWeight: 600, color: T.ink,
        letterSpacing: '-0.01em', lineHeight: 1.3,
        marginBottom: 6,
        display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
        overflow: 'hidden',
        minHeight: 36,
      }}>{seed.foreign}</div>
      <div style={{
        fontSize: 12, color: T.ink2,
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
      }}>{seed.korean}</div>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Pouch (collection) card — color + name + count
// ──────────────────────────────────────────────────────────────

// Compact language label for pouches — shows the first language's Korean label
// plus a "+N" overflow count when the pouch contains more than one language.
function LangChips({ codes = [], size = 'sm' }) {
  const T = window.T;
  if (!codes || codes.length === 0) return null;
  const labels = codes.map(c =>
    window.MOCK_LANGS.find(l => l.code === c)?.label
  ).filter(Boolean);
  if (labels.length === 0) return null;
  const dims = size === 'xs'
    ? { font: 11,   ovFont: 10,   gap: 4, ovPadV: 1,   ovPadH: 4 }
    : { font: 12,   ovFont: 10.5, gap: 5, ovPadV: 1.5, ovPadH: 5 };
  const overflow = labels.length - 1;
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: dims.gap,
                   flexShrink: 1, minWidth: 0 }}>
      <span style={{
        fontSize: dims.font, color: T.ink3, fontWeight: 500,
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        minWidth: 0,
      }}>{labels[0]}</span>
      {overflow > 0 && (
        <span style={{
          fontSize: dims.ovFont, color: T.ink2, fontWeight: 600,
          background: T.inkOverlaySoft,
          borderRadius: 4,
          padding: `${dims.ovPadV}px ${dims.ovPadH}px`,
          lineHeight: 1,
          flexShrink: 0,
        }}>+{overflow}</span>
      )}
    </span>
  );
}

function PouchCard({ pouch, onClick, layout = 'grid', playing, playIdx, onPlay, onStop }) {
  const T = window.T;
  const colorKey = pouch.color || 'pouchA';
  const col = T[colorKey];
  const total = pouch.seedIds.length;
  const coverIcon = pouch.coverIcon;   // optional Icons.* key e.g. 'Plane'
  const coverPhoto = pouch.coverPhoto; // optional data URL
  const handlePlay = (e) => {
    e.stopPropagation();
    if (playing) onStop && onStop();
    else         onPlay && onPlay();
  };
  if (layout === 'list') {
    return (
      <div onClick={onClick} style={{
        background: T.surface, borderRadius: 14,
        border: `0.5px solid ${playing ? T.accent : T.hairline}`,
        boxShadow: playing ? `0 0 0 1px ${T.accent}` : 'none',
        padding: '12px 14px',
        display: 'flex', alignItems: 'center', gap: 12,
        cursor: 'pointer',
      }}>
        <div style={{
          width: 40, height: 40, borderRadius: 10,
          background: coverPhoto
            ? `center/cover no-repeat url(${coverPhoto}), ${col}`
            : `linear-gradient(135deg, ${col} 0%, ${col}CC 100%)`,
          flexShrink: 0,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          overflow: 'hidden',
        }}>
          {coverIcon && !coverPhoto && Icons[coverIcon] &&
            React.createElement(Icons[coverIcon], { size: 22, color: '#FFF' })}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 15, fontWeight: 600, color: T.ink, marginBottom: 2 }}>{pouch.name}</div>
          <div style={{
            display: 'flex', alignItems: 'center', gap: 6,
            fontSize: 12.5, color: T.ink3,
            whiteSpace: 'nowrap', overflow: 'hidden',
          }}>
            <span style={{ flexShrink: 0 }}>씨앗 {pouch.seedIds.length}개</span>
            {pouch.langCodes && pouch.langCodes.length > 0 && (
              <React.Fragment>
                <span style={{ opacity: 0.6, flexShrink: 0 }}>·</span>
                <LangChips codes={pouch.langCodes} max={3} size="sm"/>
              </React.Fragment>
            )}
          </div>
        </div>
        {(onPlay || onStop) && (
          <button onClick={handlePlay} aria-label={playing ? '정지' : '재생'} style={{
            width: 32, height: 32, borderRadius: '50%',
            background: playing ? T.accent : T.inkOverlaySoft,
            border: 'none', padding: 0, cursor: 'pointer', flexShrink: 0,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            {playing ? <Icons.Stop size={11} color="#FFF"/>
                     : <Icons.Play size={11} color={T.ink} fillTri/>}
          </button>
        )}
      </div>
    );
  }
  return (
    <div onClick={onClick} style={{
      background: T.surface, borderRadius: 14,
      border: `0.5px solid ${playing ? T.accent : T.hairline}`,
      boxShadow: playing ? `0 0 0 1px ${T.accent}` : 'none',
      padding: 14, cursor: 'pointer',
      display: 'flex', flexDirection: 'column', gap: 12,
      minHeight: 140,
      transition: 'border-color .15s, box-shadow .15s',
    }}>
      <div style={{
        width: '100%', height: 56, borderRadius: 10,
        background: coverPhoto
          ? `center/cover no-repeat url(${coverPhoto}), ${col}`
          : `linear-gradient(135deg, ${col} 0%, ${col}AA 100%)`,
        position: 'relative', overflow: 'hidden',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>
        {!coverIcon && !coverPhoto && (
          <div style={{
            position: 'absolute', right: -8, bottom: -10,
            width: 60, height: 60, borderRadius: 30,
            background: 'rgba(255,255,255,0.12)',
          }}/>
        )}
        {coverIcon && !coverPhoto && Icons[coverIcon] &&
          React.createElement(Icons[coverIcon], { size: 28, color: '#FFF' })}
        {playing && total > 0 && (
          <div style={{
            position: 'absolute', left: 0, bottom: 0, right: 0,
            height: 3, background: 'rgba(0,0,0,0.18)',
          }}>
            <div style={{
              width: `${((playIdx + 1) / total) * 100}%`,
              height: '100%', background: '#FFF',
              transition: 'width .3s ease',
            }}/>
          </div>
        )}
        {(onPlay || onStop) && (
          <button onClick={handlePlay} aria-label={playing ? '정지' : '재생'} style={{
            position: 'absolute', right: 8, bottom: 8,
            width: 32, height: 32, borderRadius: '50%',
            background: playing ? T.accent : 'rgba(255,255,255,0.96)',
            border: 'none', padding: 0, cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: '0 2px 6px rgba(0,0,0,0.20)',
            fontFamily: 'inherit',
          }}>
            {playing ? <Icons.Stop size={11} color="#FFF"/>
                     : <Icons.Play size={11} color={T.ink} fillTri/>}
          </button>
        )}
      </div>
      <div>
        <div style={{ fontSize: 14.5, fontWeight: 600, color: T.ink,
                      letterSpacing: '-0.01em',
                      whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                      marginBottom: 2 }}>{pouch.name}</div>
        {playing ? (
          <div style={{ fontSize: 12, color: T.accent, fontWeight: 700,
                        display: 'flex', alignItems: 'center', gap: 5 }}>
            <PlayingWave color={T.accent} size={9}/>
            <span>{playIdx + 1}/{total} 재생 중</span>
          </div>
        ) : (
          <div style={{
            display: 'flex', alignItems: 'center', gap: 6,
            fontSize: 12, color: T.ink3,
            whiteSpace: 'nowrap', overflow: 'hidden',
          }}>
            <span style={{ flexShrink: 0 }}>씨앗 {total}개</span>
            {pouch.langCodes && pouch.langCodes.length > 0 && (
              <React.Fragment>
                <span style={{ opacity: 0.6, flexShrink: 0 }}>·</span>
                <LangChips codes={pouch.langCodes} max={3} size="sm"/>
              </React.Fragment>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

// Pouch card mini (compact carousel variant — matches SeedCardMini rhythm)
function PouchCardMini({ pouch, onClick, playing, playIdx, onPlay, onStop }) {
  const T = window.T;
  const colorKey = pouch.color || 'pouchA';
  const col = T[colorKey];
  const total = pouch.seedIds.length;
  return (
    <div onClick={onClick} style={{
      width: 148, flexShrink: 0,
      background: T.surface, borderRadius: 14,
      border: `0.5px solid ${playing ? T.accent : T.hairline}`,
      boxShadow: playing ? `0 0 0 1px ${T.accent}` : 'none',
      padding: 12, cursor: 'pointer',
      display: 'flex', flexDirection: 'column', gap: 10,
      transition: 'border-color .15s, box-shadow .15s',
    }}>
      {/* Color chip block — keeps visual identity, hosts inline play */}
      <div style={{
        width: '100%', height: 48, borderRadius: 10,
        background: `linear-gradient(135deg, ${col} 0%, ${col}AA 100%)`,
        position: 'relative', overflow: 'hidden',
      }}>
        <div style={{
          position: 'absolute', right: -6, bottom: -8,
          width: 44, height: 44, borderRadius: 22,
          background: 'rgba(255,255,255,0.14)',
        }}/>
        {/* Progress bar overlay when playing */}
        {playing && total > 0 && (
          <div style={{
            position: 'absolute', left: 0, bottom: 0, right: 0,
            height: 3, background: 'rgba(0,0,0,0.18)',
          }}>
            <div style={{
              width: `${((playIdx + 1) / total) * 100}%`,
              height: '100%', background: '#FFF',
              transition: 'width .3s ease',
            }}/>
          </div>
        )}
        {/* Inline play button — stops propagation so card click still navigates */}
        <button onClick={(e) => {
                  e.stopPropagation();
                  if (playing) onStop && onStop();
                  else         onPlay && onPlay();
                }}
                aria-label={playing ? '정지' : '재생'}
                style={{
          position: 'absolute', right: 6, bottom: 6,
          width: 30, height: 30, borderRadius: '50%',
          background: playing ? T.accent : 'rgba(255,255,255,0.96)',
          border: 'none', padding: 0, cursor: 'pointer',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          boxShadow: '0 2px 6px rgba(0,0,0,0.20)',
          fontFamily: 'inherit',
        }}>
          {playing
            ? <Icons.Stop size={10} color="#FFF"/>
            : <Icons.Play size={10} color={T.ink} fillTri/>}
        </button>
      </div>
      <div style={{ minWidth: 0 }}>
        <div style={{
          fontSize: 13.5, fontWeight: 600, color: T.ink,
          letterSpacing: '-0.01em',
          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          marginBottom: 2,
        }}>{pouch.name}</div>
        {playing ? (
          <div style={{
            fontSize: 11, color: T.accent, fontWeight: 700,
            letterSpacing: '-0.01em',
            display: 'flex', alignItems: 'center', gap: 5,
            whiteSpace: 'nowrap', overflow: 'hidden',
          }}>
            <PlayingWave color={T.accent} size={9}/>
            <span>{playIdx + 1}/{total} 재생 중</span>
          </div>
        ) : (
          <div style={{
            display: 'flex', alignItems: 'center', gap: 6,
            fontSize: 11, color: T.ink3, fontWeight: 500,
            whiteSpace: 'nowrap', overflow: 'hidden',
          }}>
            <span style={{ flexShrink: 0 }}>씨앗 {total}개</span>
            {pouch.langCodes && pouch.langCodes.length > 0 && (
              <React.Fragment>
                <span style={{ color: T.ink3, opacity: 0.6, flexShrink: 0 }}>·</span>
                <LangChips codes={pouch.langCodes} max={3} size="xs"/>
              </React.Fragment>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Bottom sheet (slide-up modal)
// ──────────────────────────────────────────────────────────────
function BottomSheet({ open, onClose, children, height, title }) {
  const T = window.T;
  if (!open) return null;
  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 50,
      display: 'flex', flexDirection: 'column', justifyContent: 'flex-end',
    }}>
      <div onClick={onClose} style={{
        position: 'absolute', inset: 0,
        background: 'rgba(0,0,0,0.4)',
        animation: 'cp-fade-bg 0.2s ease',
      }}/>
      <div style={{
        position: 'relative',
        background: T.bg, borderRadius: '20px 20px 0 0',
        animation: 'cp-sheet-up 0.28s cubic-bezier(0.32, 0.72, 0, 1)',
        maxHeight: '80%',
        display: 'flex', flexDirection: 'column',
        height: height || 'auto',
      }}>
        <div style={{
          width: 36, height: 4, borderRadius: 2,
          background: T.hairlineStrong,
          margin: '8px auto 0',
        }}/>
        {title && (
          <div style={{
            fontSize: 16, fontWeight: 700, color: T.ink,
            letterSpacing: '-0.02em',
            padding: '14px 20px 8px', textAlign: 'center',
          }}>{title}</div>
        )}
        <div style={{ flex: 1, overflowY: 'auto' }}>{children}</div>
      </div>
    </div>
  );
}

// Center modal (system alert style)
function AlertModal({ open, title, message, confirmLabel = '확인', cancelLabel = '취소', onConfirm, onCancel, danger }) {
  const T = window.T;
  if (!open) return null;
  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 60,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      background: 'rgba(0,0,0,0.5)', animation: 'cp-fade-bg 0.2s ease',
      padding: 30,
    }}>
      <div style={{
        background: T.bg, borderRadius: 18,
        width: '100%', maxWidth: 300,
        animation: 'cp-modal-in 0.25s cubic-bezier(0.32, 0.72, 0, 1)',
        overflow: 'hidden',
      }}>
        <div style={{ padding: '22px 20px 16px', textAlign: 'center' }}>
          <div style={{ fontSize: 16, fontWeight: 700, color: T.ink, marginBottom: 6,
                        letterSpacing: '-0.02em' }}>{title}</div>
          {message && <div style={{ fontSize: 13.5, color: T.ink2, lineHeight: 1.45 }}>{message}</div>}
        </div>
        <div style={{ display: 'flex', borderTop: `0.5px solid ${T.hairline}` }}>
          {onCancel && (
            <button onClick={onCancel} style={{
              flex: 1, height: 48, background: 'transparent', border: 'none',
              fontFamily: 'inherit', fontSize: 15, fontWeight: 500, color: T.ink,
              borderRight: `0.5px solid ${T.hairline}`, cursor: 'pointer',
            }}>{cancelLabel}</button>
          )}
          <button onClick={onConfirm} style={{
            flex: 1, height: 48, background: 'transparent', border: 'none',
            fontFamily: 'inherit', fontSize: 15, fontWeight: 600,
            color: danger ? T.danger : T.accent, cursor: 'pointer',
          }}>{confirmLabel}</button>
        </div>
      </div>
    </div>
  );
}

// Toast (auto-dismisses)
function Toast({ message, action, danger, onAction, style }) {
  const T = window.T;
  if (!message) return null;
  return (
    <div style={{
      position: 'absolute', left: 16, right: 16, bottom: 96, zIndex: 70,
      background: T.toastBg, color: T.toastInk,
      borderRadius: 14, padding: '12px 16px',
      display: 'flex', alignItems: 'center', gap: 12,
      animation: 'cp-toast-in 0.22s ease',
      boxShadow: '0 8px 24px rgba(0,0,0,0.18)',
      fontSize: 13.5, fontWeight: 500, letterSpacing: '-0.01em',
      ...style,
    }}>
      <div style={{ flex: 1 }}>{message}</div>
      {action && <button onClick={onAction} style={{
        background: 'transparent', border: 'none',
        color: danger ? T.danger : '#E5BC6E',
        fontSize: 13.5, fontWeight: 700, padding: 0, cursor: 'pointer',
      }}>{action}</button>}
    </div>
  );
}

// Offline badge
function OfflineBadge() {
  const T = window.T;
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      background: T.accentSoft, color: T.accent,
      padding: '4px 10px', borderRadius: 12,
      fontSize: 11.5, fontWeight: 700, letterSpacing: '-0.01em',
    }}>
      <span>✈</span> 오프라인
    </div>
  );
}

// Top app bar
function TopBar({ leading, title, trailing, transparent, scrollProgress, offline }) {
  const T = window.T;
  return (
    <div style={{
      flexShrink: 0,
      padding: '4px 8px',
      minHeight: 48,
      display: 'flex', alignItems: 'center', gap: 4,
      background: transparent ? 'transparent' : T.bg,
      position: 'relative',
    }}>
      <div style={{ width: 40, display: 'flex', justifyContent: 'flex-start' }}>{leading}</div>
      <div style={{
        flex: 1, textAlign: 'center',
        fontSize: 16, fontWeight: 700, color: T.ink,
        letterSpacing: '-0.02em',
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
      }}>{title}</div>
      <div style={{ minWidth: 40, flexShrink: 0, whiteSpace: 'nowrap', display: 'flex', justifyContent: 'flex-end' }}>{trailing}</div>
      {offline && (
        <div style={{ position: 'absolute', right: 50, top: 12 }}>
          <OfflineBadge/>
        </div>
      )}
    </div>
  );
}

// Section header
function SectionHeader({ title, count, action, onTitleClick }) {
  const T = window.T;
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 8,
      margin: '6px 0 12px',
      minWidth: 0,
    }}>
      <div onClick={onTitleClick} style={{
        flex: 1, display: 'flex', alignItems: 'baseline', gap: 6,
        cursor: onTitleClick ? 'pointer' : 'default',
        minWidth: 0, overflow: 'hidden',
      }}>
        <span style={{
          fontSize: 17, fontWeight: 700, color: T.ink,
          letterSpacing: '-0.02em',
          whiteSpace: 'nowrap',
        }}>{title}</span>
        {count != null && (
          <span style={{ fontSize: 13, color: T.ink3, fontWeight: 500, whiteSpace: 'nowrap' }}>{count}</span>
        )}
      </div>
      <div style={{ flexShrink: 0 }}>{action}</div>
    </div>
  );
}

// Tab bar (bottom navigation)
function TabBar({ active, onChange }) {
  const T = window.T;
  const tabs = [
    { id: 'home', label: '홈', icon: <Icons.Home/> },
    { id: 'me',   label: '나', icon: <Icons.User/> },
  ];
  return (
    <div style={{
      height: 64, paddingBottom: 8,
      background: T.tabBg,
      backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)',
      borderTop: `0.5px solid ${T.hairline}`,
      display: 'flex',
    }}>
      {tabs.map(t => {
        const on = active === t.id;
        return (
          <button key={t.id} onClick={() => onChange(t.id)} style={{
            flex: 1, background: 'transparent', border: 'none',
            display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
            paddingTop: 8,
            color: on ? T.accent : T.ink3,
            fontSize: 10.5, fontWeight: 600,
            cursor: 'pointer', WebkitTapHighlightColor: 'transparent',
          }}>
            {React.cloneElement(t.icon, { size: 22, color: on ? T.accent : T.ink3 })}
            <span>{t.label}</span>
          </button>
        );
      })}
    </div>
  );
}

// Scrollable list row (settings list)
function ListRow({ label, sub, leading, trailing, onClick, danger, last, disabled }) {
  const T = window.T;
  const handle = disabled ? undefined : onClick;
  return (
    <div onClick={handle} style={{
      display: 'flex', alignItems: 'center', gap: 12,
      padding: '12px 16px', minHeight: 52,
      cursor: handle ? 'pointer' : 'default',
      borderBottom: last ? 'none' : `0.5px solid ${T.hairline}`,
      opacity: disabled ? 0.4 : 1,
    }}>
      {leading && <div style={{ width: 24, display: 'flex' }}>{leading}</div>}
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{
          fontSize: 14.5, fontWeight: 500,
          color: danger ? T.danger : T.ink,
          letterSpacing: '-0.01em',
        }}>{label}</div>
        {sub && <div style={{ fontSize: 12, color: T.ink3, marginTop: 2 }}>{sub}</div>}
      </div>
      {trailing}
    </div>
  );
}

// Card group container (iOS-style settings)
function Group({ title, footer, children, style }) {
  const T = window.T;
  return (
    <div style={{ marginBottom: 20, ...style }}>
      {title && (
        <div style={{
          fontSize: 12, color: T.ink3, fontWeight: 600,
          padding: '0 16px 6px', letterSpacing: '0.02em',
          textTransform: 'uppercase',
        }}>{title}</div>
      )}
      <div style={{
        background: T.surface, borderRadius: 14,
        border: `0.5px solid ${T.hairline}`,
        overflow: 'hidden',
      }}>{children}</div>
      {footer && (
        <div style={{ fontSize: 12, color: T.ink3,
                      padding: '6px 16px 0', lineHeight: 1.5 }}>{footer}</div>
      )}
    </div>
  );
}

// Empty-state container
function EmptyState({ icon, title, message, action }) {
  const T = window.T;
  return (
    <div style={{
      display: 'flex', flexDirection: 'column', alignItems: 'center',
      gap: 12, padding: '48px 24px', textAlign: 'center',
    }}>
      {icon}
      <div style={{ fontSize: 15.5, fontWeight: 600, color: T.ink, letterSpacing: '-0.01em' }}>{title}</div>
      {message && <div style={{ fontSize: 13.5, color: T.ink2, lineHeight: 1.5, maxWidth: 240 }}>{message}</div>}
      {action && <div style={{ marginTop: 8 }}>{action}</div>}
    </div>
  );
}

Object.assign(window, {
  Hamster, HamsterAnimated, Spinner,
  Button, IconButton,
  Chip, Segment, Toggle,
  SearchBar,
  SeedCard, SeedCardMini, PouchCard, PouchCardMini,
  BottomSheet, AlertModal, Toast, OfflineBadge,
  TopBar, SectionHeader, TabBar, ListRow, Group, EmptyState,
  useSeedFavs,
});
