// data.jsx — shared context, design atoms, icons, mock data
const { createContext, useContext, useState, useRef, useEffect } = React;

/* ─────────────────────────── App context ─────────────────────────── */
const AppCtx = createContext(null);
const useApp = () => useContext(AppCtx);

/* ─────────────────────────── Simple line icons ───────────────────────────
   Minimal geometric strokes for UI chrome only. */
function Icon({ name, size = 24, stroke = 'currentColor', sw = 1.8, fill = 'none', style }) {
  const p = { fill, stroke, strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round' };
  const paths = {
    home:   <><path {...p} d="M4 11.5 12 4l8 7.5" /><path {...p} d="M6 10.5V20h12v-9.5" /></>,
    map:    <><path {...p} d="M9 4 4 6v14l5-2 6 2 5-2V4l-5 2-6-2Z" /><path {...p} d="M9 4v14M15 6v14" /></>,
    heart:  <path {...p} d="M12 20s-7-4.6-9.2-9C1.3 8 2.8 5 6 5c2 0 3.2 1.4 6 4 2.8-2.6 4-4 6-4 3.2 0 4.7 3 3.2 6C19 15.4 12 20 12 20Z" />,
    gear:   <><circle {...p} cx="12" cy="12" r="3.2" /><path {...p} d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5 5l2.1 2.1M16.9 16.9 19 19M19 5l-2.1 2.1M7.1 16.9 5 19" /></>,
    plus:   <path {...p} d="M12 5v14M5 12h14" />,
    chevR:  <path {...p} d="m9 5 7 7-7 7" />,
    chevL:  <path {...p} d="m15 5-7 7 7 7" />,
    chevD:  <path {...p} d="m5 9 7 7 7-7" />,
    back:   <path {...p} d="m15 5-7 7 7 7" />,
    lock:   <><rect {...p} x="5" y="10" width="14" height="10" rx="2.5" /><path {...p} d="M8 10V7a4 4 0 0 1 8 0v3" /></>,
    camera: <><path {...p} d="M4 8h3l1.5-2h7L17 8h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z" /><circle {...p} cx="12" cy="13" r="3.2" /></>,
    note:   <><path {...p} d="M6 3h9l4 4v14H6Z" /><path {...p} d="M15 3v4h4M9 12h6M9 16h6" /></>,
    link:   <><path {...p} d="M9.5 13.5 14 9M8 11 6 13a3 3 0 0 0 4.2 4.2L12 15.5M16 8.5 18 6.5A3 3 0 0 0 13.8 2.3L12 4" /></>,
    mic:    <><rect {...p} x="9.5" y="3" width="5" height="11" rx="2.5" /><path {...p} d="M6 11a6 6 0 0 0 12 0M12 17v3" /></>,
    send:   <path {...p} d="M4 12 20 4l-5 16-3.5-6.5L4 12Z" />,
    battery:<><rect {...p} x="3" y="8" width="15" height="9" rx="2.5" /><path {...p} d="M20 11v3" /></>,
    car:    <><path {...p} d="M5 16v2M19 16v2" /><path {...p} d="M4 16v-3l2-5h12l2 5v3Z" /><circle cx="8" cy="16" r="1.4" fill={stroke} stroke="none"/><circle cx="16" cy="16" r="1.4" fill={stroke} stroke="none"/></>,
    train:  <><rect {...p} x="6" y="4" width="12" height="13" rx="3" /><path {...p} d="M6 11h12M9 21l-2-2M15 21l2-2" /><circle cx="9.5" cy="14" r="0.9" fill={stroke} stroke="none"/><circle cx="14.5" cy="14" r="0.9" fill={stroke} stroke="none"/></>,
    sun:    <><circle {...p} cx="12" cy="12" r="4" /><path {...p} d="M12 3v2M12 19v2M3 12h2M19 12h2M5.5 5.5 7 7M17 17l1.5 1.5M18.5 5.5 17 7M7 17l-1.5 1.5" /></>,
    rain:   <><path {...p} d="M7 14a4 4 0 0 1 .5-8 5 5 0 0 1 9.5 1.5A3.5 3.5 0 0 1 16.5 14Z" /><path {...p} d="M8 17l-1 2M12 17l-1 2M16 17l-1 2" /></>,
    check:  <path {...p} d="m5 12.5 4.5 4.5L19 7" />,
    sparkle:<path {...p} d="M12 4c.6 3.5 1.5 4.4 5 5-3.5.6-4.4 1.5-5 5-.6-3.5-1.5-4.4-5-5 3.5-.6 4.4-1.5 5-5Z" />,
    copy:   <><rect {...p} x="8" y="8" width="12" height="12" rx="2.5" /><path {...p} d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" /></>,
    share:  <><circle {...p} cx="7" cy="12" r="2.5" /><circle {...p} cx="17" cy="6" r="2.5" /><circle {...p} cx="17" cy="18" r="2.5" /><path {...p} d="m9.2 10.8 5.6-3.4M9.2 13.2l5.6 3.4" /></>,
    pin:    <><path {...p} d="M12 21s7-6 7-11a7 7 0 0 0-14 0c0 5 7 11 7 11Z" /><circle {...p} cx="12" cy="10" r="2.4" /></>,
    bag:    <><path {...p} d="M6 8h12l-1 12H7Z" /><path {...p} d="M9 8V6a3 3 0 0 1 6 0v2" /></>,
    google: <g><path d="M21 12.2c0-.7-.1-1.4-.2-2H12v3.9h5.1a4.4 4.4 0 0 1-1.9 2.9v2.4h3.1c1.8-1.7 2.7-4.1 2.7-7.1Z" fill="#5b6470"/><path d="M12 21c2.5 0 4.6-.8 6.1-2.3l-3.1-2.4c-.8.6-2 .9-3 .9-2.3 0-4.3-1.6-5-3.7H3.8v2.4A9 9 0 0 0 12 21Z" fill="#7e8794"/><path d="M7 13.5a5.4 5.4 0 0 1 0-3.4V7.7H3.8a9 9 0 0 0 0 8.1L7 13.5Z" fill="#9aa1ac"/><path d="M12 6.6c1.3 0 2.5.5 3.4 1.4l2.6-2.6A9 9 0 0 0 3.8 7.7L7 10.1C7.7 8.1 9.7 6.6 12 6.6Z" fill="#5b6470"/></g>,
    arrow:  <path {...p} d="M5 12h14M13 6l6 6-6 6" />,
    skip:   <path {...p} d="M5 5v14l9-7zM18 5v14" />,
    refresh:<><path {...p} d="M4 12a8 8 0 0 1 13.7-5.6L20 8" /><path {...p} d="M20 4v4h-4" /><path {...p} d="M20 12a8 8 0 0 1-13.7 5.6L4 16" /><path {...p} d="M4 20v-4h4" /></>,
    edit:   <path {...p} d="M4 20h4L19 9l-4-4L4 16Zm11-13 4 4" />,
    clock:  <><circle {...p} cx="12" cy="12" r="8" /><path {...p} d="M12 7.5V12l3 2" /></>,
    moon:   <path {...p} d="M20 14.2A7.6 7.6 0 0 1 9.4 3.8a6.4 6.4 0 1 0 10.6 10.4Z" />,
    walk:   <><circle cx="13" cy="4.4" r="1.8" fill={stroke} stroke="none" /><path {...p} d="M13 8v5l-2.5 5.5M13 11l3.5 1.8M13 12.5l-3 2" /></>,
    cal:    <><rect {...p} x="4" y="5.5" width="16" height="15" rx="3" /><path {...p} d="M4 10h16M8 3.5v4M16 3.5v4" /></>,
    chat:   <><path {...p} d="M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H9l-4 3.5V6a1 1 0 0 1 1-1Z" /><path {...p} d="M9 10h6M9 13h3.5" /></>,
    route:  <><circle {...p} cx="6" cy="6" r="2.4" /><circle {...p} cx="18" cy="18" r="2.4" /><path {...p} d="M8.4 6H15a3 3 0 0 1 0 6H9a3 3 0 0 0 0 6h6.6" /></>,
    expand: <path {...p} d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" />,
    locate: <><circle {...p} cx="12" cy="12" r="3.4" /><path {...p} d="M12 2.5v3.5M12 18v3.5M2.5 12H6M18 12h3.5" /></>,
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" style={style} aria-hidden="true">
      {paths[name]}
    </svg>
  );
}

/* ─────────────────────────── Buttons & atoms ─────────────────────────── */
function Btn({ children, onClick, variant = 'primary', full, size = 'md', style, icon, disabled }) {
  const base = {
    fontFamily: 'var(--font-ui)', fontWeight: 700, cursor: disabled ? 'default' : 'pointer',
    border: 'none', borderRadius: 999, display: 'inline-flex', alignItems: 'center',
    justifyContent: 'center', gap: 8, width: full ? '100%' : 'auto',
    transition: 'transform .15s ease, box-shadow .2s ease, background .2s ease',
    letterSpacing: '.02em', whiteSpace: 'nowrap', opacity: disabled ? 0.5 : 1,
  };
  const sizes = {
    sm: { padding: '9px 16px', fontSize: 14 },
    md: { padding: '14px 22px', fontSize: 16 },
    lg: { padding: '17px 26px', fontSize: 17 },
  };
  const variants = {
    primary: { background: 'var(--ink)', color: 'var(--paper)', boxShadow: '0 8px 20px -8px rgba(61,54,44,.55)' },
    rose:    { background: 'var(--me)', color: '#fff', boxShadow: '0 10px 22px -10px var(--me)' },
    amethyst:{ background: 'var(--amethyst)', color: '#fff', boxShadow: '0 10px 22px -10px var(--amethyst)' },
    soft:    { background: 'var(--surface)', color: 'var(--ink)', boxShadow: 'inset 0 0 0 1px var(--line-2)' },
    ghost:   { background: 'transparent', color: 'var(--ink-soft)' },
  };
  const ref = useRef(null);
  return (
    <button ref={ref} disabled={disabled} onClick={onClick}
      onPointerDown={e => !disabled && (e.currentTarget.style.transform = 'scale(.97)')}
      onPointerUp={e => (e.currentTarget.style.transform = '')}
      onPointerLeave={e => (e.currentTarget.style.transform = '')}
      style={{ ...base, ...sizes[size], ...variants[variant], ...style }}>
      {icon && <Icon name={icon} size={size === 'sm' ? 16 : 19} />}
      {children}
    </button>
  );
}

function Chip({ children, active, onClick, color = 'var(--ink)', style }) {
  return (
    <button onClick={onClick} style={{
      fontFamily: 'var(--font-ui)', fontWeight: 500, fontSize: 13.5, cursor: 'pointer',
      padding: '8px 14px', borderRadius: 999, whiteSpace: 'nowrap',
      border: active ? `1.5px solid ${color}` : '1.5px solid var(--line-2)',
      background: active ? color : 'var(--surface)',
      color: active ? '#fff' : 'var(--ink-soft)',
      transition: 'all .18s ease', ...style,
    }}>{children}</button>
  );
}

/* card surface */
function Card({ children, style, onClick, pad = 18 }) {
  return (
    <div onClick={onClick} style={{
      background: 'var(--surface)', borderRadius: 22, padding: pad,
      boxShadow: '0 1px 0 rgba(255,255,255,.6) inset, 0 14px 30px -22px rgba(61,54,44,.5)',
      border: '1px solid var(--line)', ...style,
    }}>{children}</div>
  );
}

/* small charm avatar (two interlocking rings = the amulet) */
function Charm({ size = 30, a = 'var(--me)', b = 'var(--partner)', linked = true }) {
  return (
    <svg width={size} height={size} viewBox="0 0 40 32">
      <circle cx="15" cy="16" r="10" fill="none" stroke={a} strokeWidth="3" />
      <circle cx={linked ? 25 : 25} cy="16" r="10" fill="none" stroke={b} strokeWidth="3" />
    </svg>
  );
}

/* AI（相棒の犬）のチャットアイコン。丸く切り抜いて表示する */
function AiAvatar({ size = 28, style }) {
  return (
    <div style={{
      width: size, height: size, borderRadius: '50%', flexShrink: 0,
      background: 'color-mix(in oklch, var(--amethyst) 16%, var(--paper))',
      overflow: 'hidden', ...style,
    }}>
      <img src="/assets/ai-dog.png" alt="Amulet" width={size} height={size}
        style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
    </div>
  );
}

/* avatar disc with initial */
function Avatar({ name = 'A', color = 'var(--me)', size = 38 }) {
  return (
    <div style={{
      width: size, height: size, borderRadius: '50%', flexShrink: 0,
      background: `color-mix(in oklch, ${color} 22%, var(--paper))`,
      color: `color-mix(in oklch, ${color} 75%, var(--ink))`,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      fontWeight: 700, fontSize: size * 0.42, fontFamily: 'var(--font-serif)',
      border: `1.5px solid color-mix(in srgb, ${color} 40%, transparent)`,
    }}>{name}</div>
  );
}

/* striped image placeholder */
// 何も画像が無いときに出す共通フォールバック画像（静的サイト直下にホスト・差し替え可）
const FALLBACK_IMG = '/fallback.webp';
function ImgSlot({ label = 'IMAGE', h = 120, r = 16, style, src }) {
  const [srcFailed, setSrcFailed] = React.useState(false);
  const [fbFailed, setFbFailed] = React.useState(false);
  // ① photoUrl（src）→ ② 失敗/未指定ならフォールバック画像 → ③ それも失敗したら色枠ラベル
  const useSrc = src && !srcFailed;
  if (useSrc || !fbFailed) {
    return (
      <img src={useSrc ? src : FALLBACK_IMG} alt={label}
        onError={() => (useSrc ? setSrcFailed(true) : setFbFailed(true))} style={{
        height: h, width: '100%', borderRadius: r, objectFit: 'cover', display: 'block',
        border: '1px solid var(--line)', ...style,
      }} />
    );
  }
  return (
    <div style={{
      height: h, borderRadius: r, position: 'relative', overflow: 'hidden',
      background: 'repeating-linear-gradient(135deg, var(--surface-2) 0 11px, var(--paper) 11px 22px)',
      border: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'center', ...style,
    }}>
      <span style={{
        fontFamily: 'ui-monospace, monospace', fontSize: 10.5, letterSpacing: '.16em',
        color: 'var(--ink-faint)', textTransform: 'uppercase',
        background: 'var(--paper)', padding: '3px 9px', borderRadius: 6,
      }}>{label}</span>
    </div>
  );
}

/* section eyebrow */
function Eyebrow({ children, color = 'var(--ink-faint)' }) {
  return <div style={{
    fontFamily: 'ui-monospace, monospace', fontSize: 10.5, letterSpacing: '.22em',
    textTransform: 'uppercase', color, fontWeight: 500,
  }}>{children}</div>;
}

/* ── Firestore イベント → 画面用の形に正規化（live: true が実データの印） ── */
function normalizeEvent(d, uid) {
  const stats = d.inputStats || {};
  const mine = (uid && stats[uid] && stats[uid].messageCount) || 0;
  const theirs = Object.entries(stats)
    .filter(([k]) => k !== uid)
    .reduce((a, [, v]) => a + ((v && v.messageCount) || 0), 0);
  return {
    id: d.id, live: true,
    date: d.dateLabel || d.date || '', dow: d.dow || '',
    title: d.title || 'ただの週末', area: d.area || '',
    progress: Math.min(1, (mine + theirs) / 16), mine, theirs,
    weather: 'sun', planStatus: d.planStatus || 'none', raw: d,
  };
}

/* ─────────────────────────── Shells ─────────────────────────── */
const SAFE_TOP = 50;
const NAV_H = 86; // nav incl. home-indicator pad

function TopBar({ title, onBack, right, sub, center }) {
  return (
    <div style={{
      position: 'sticky', top: 0, zIndex: 30,
      paddingTop: SAFE_TOP, paddingBottom: 10,
      background: 'color-mix(in srgb, var(--bg) 82%, transparent)',
      backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '0 14px', minHeight: 40 }}>
        {onBack ? (
          <button onClick={onBack} style={{
            width: 38, height: 38, borderRadius: 999, border: '1px solid var(--line-2)',
            background: 'var(--surface)', display: 'flex', alignItems: 'center', justifyContent: 'center',
            cursor: 'pointer', color: 'var(--ink)', flexShrink: 0,
          }}><Icon name="back" size={20} /></button>
        ) : <div style={{ width: right ? 38 : 0 }} />}
        <div style={{ flex: 1, textAlign: center ? 'center' : 'left' }}>
          {title && <div style={{ fontWeight: 700, fontSize: 18, color: 'var(--ink)', letterSpacing: '.01em' }}>{title}</div>}
          {sub && <div style={{ fontSize: 12, color: 'var(--ink-faint)', marginTop: 1 }}>{sub}</div>}
        </div>
        {right || (onBack ? <div style={{ width: 38 }} /> : null)}
      </div>
    </div>
  );
}

function BottomNav({ active, go }) {
  const items = [
    { id: 'home', icon: 'home', label: 'ホーム' },
    { id: 'amulet-map', icon: 'map', label: 'マップ' },
    { id: 'wish-list', icon: 'heart', label: '小言' },
    { id: 'settings', icon: 'gear', label: '設定' },
  ];
  return (
    <div style={{
      position: 'absolute', left: 0, right: 0, bottom: 0, zIndex: 40,
      paddingBottom: 24, paddingTop: 8,
      background: 'color-mix(in srgb, var(--paper) 88%, transparent)',
      backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
      borderTop: '1px solid var(--line)',
      display: 'flex', justifyContent: 'space-around',
    }}>
      {items.map(it => {
        const on = active === it.id;
        return (
          <button key={it.id} onClick={() => go(it.id)} style={{
            background: 'none', border: 'none', cursor: 'pointer',
            display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
            color: on ? 'var(--amethyst)' : 'var(--ink-faint)', padding: '4px 14px',
            transition: 'color .2s',
          }}>
            <Icon name={it.icon} size={24} sw={on ? 2.2 : 1.8} />
            <span style={{ fontSize: 10.5, fontWeight: on ? 700 : 500 }}>{it.label}</span>
          </button>
        );
      })}
    </div>
  );
}

/* scroll body that respects safe areas */
function Body({ children, nav, pad = 18, style, scrollRef }) {
  return (
    <div className="noscroll" ref={scrollRef} style={{
      flex: 1, overflowY: 'auto', overflowX: 'hidden',
      padding: `0 ${pad}px`, paddingBottom: nav ? NAV_H + 12 : 30,
      ...style,
    }}>{children}</div>
  );
}

function Field({ label, value = '', onChange, placeholder, type = 'text', icon, area, rows = 3, right }) {
  const handle = (e) => onChange && onChange(e.target.value);
  return (
    <label style={{ display: 'block' }}>
      {label && <div style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--ink-soft)', marginBottom: 7, letterSpacing: '.02em' }}>{label}</div>}
      <div style={{
        display: 'flex', alignItems: area ? 'flex-start' : 'center', gap: 10,
        background: 'var(--surface)', border: '1px solid var(--line-2)', borderRadius: 14,
        padding: area ? '13px 14px' : '0 14px',
      }}>
        {icon && <Icon name={icon} size={19} style={{ color: 'var(--ink-faint)', marginTop: area ? 2 : 0, flexShrink: 0 }} />}
        {area ? (
          <textarea rows={rows} placeholder={placeholder} value={value}
            onChange={handle} readOnly={!onChange}
            style={{ flex: 1, border: 'none', outline: 'none', resize: 'none', background: 'transparent',
              fontFamily: 'var(--font-ui)', fontSize: 15, color: 'var(--ink)', lineHeight: 1.5 }} />
        ) : (
          <input type={type} placeholder={placeholder} value={value}
            onChange={handle} readOnly={!onChange}
            style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', height: 50,
              fontFamily: 'var(--font-ui)', fontSize: 15.5, color: 'var(--ink)' }} />
        )}
        {right}
      </div>
    </label>
  );
}

function Segmented({ options, value, onChange, color = 'var(--amethyst)' }) {
  return (
    <div style={{
      display: 'flex', background: 'var(--surface-2)', borderRadius: 12, padding: 4, gap: 4,
      border: '1px solid var(--line)',
    }}>
      {options.map(o => {
        const val = typeof o === 'string' ? o : o.value;
        const lab = typeof o === 'string' ? o : o.label;
        const on = value === val;
        return (
          <button key={val} onClick={() => onChange(val)} style={{
            flex: 1, padding: '9px 6px', borderRadius: 9, border: 'none', cursor: 'pointer',
            fontFamily: 'var(--font-ui)', fontWeight: on ? 700 : 500, fontSize: 13.5,
            background: on ? 'var(--surface)' : 'transparent',
            color: on ? color : 'var(--ink-faint)',
            boxShadow: on ? '0 2px 8px -4px rgba(61,54,44,.4)' : 'none',
            transition: 'all .18s',
          }}>{lab}</button>
        );
      })}
    </div>
  );
}

function PrivacyBadge({ children = '相手には見えません' }) {
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center', gap: 7, flexShrink: 0, whiteSpace: 'nowrap',
      background: 'color-mix(in oklch, var(--amethyst) 14%, var(--paper))',
      color: 'color-mix(in oklch, var(--amethyst) 72%, var(--ink))',
      border: '1px solid color-mix(in srgb, var(--amethyst) 28%, transparent)',
      borderRadius: 999, padding: '6px 13px', fontSize: 12, fontWeight: 700,
    }}>
      {children}
    </div>
  );
}

/* ─────────────────────────── Google Maps ─────────────────────────── */
const MAP_STYLE = [
  { elementType: 'labels.icon', stylers: [{ visibility: 'off' }] },
  { featureType: 'poi', stylers: [{ visibility: 'off' }] },
  { featureType: 'transit.station', stylers: [{ visibility: 'simplified' }] },
  { featureType: 'road', elementType: 'labels.icon', stylers: [{ visibility: 'off' }] },
  { elementType: 'geometry', stylers: [{ saturation: -20 }, { lightness: 5 }] },
];

// GoogleMapView — ref-based Google Map with custom SVG markers
// Props: center {lat,lng}, zoom, interactive, markers [{id,pos,label,color,onClick}],
//        onMapClick(latLng), onReady(map), style
function GoogleMapView({ center, zoom = 15, interactive = true, markers = [], onMapClick, onReady, fitSignature, style = {} }) {
  const elRef = React.useRef(null);
  const mapRef = React.useRef(null);
  const markerLayerRef = React.useRef([]);
  const [ready, setReady] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    window.__mapsReady.then((g) => {
      if (cancelled || !elRef.current) return;
      const map = new g.maps.Map(elRef.current, {
        center, zoom,
        disableDefaultUI: true,
        gestureHandling: interactive ? 'greedy' : 'none',
        clickableIcons: false,
        keyboardShortcuts: false,
        styles: MAP_STYLE,
        backgroundColor: '#F0EBE1',
      });
      mapRef.current = map;
      if (onMapClick) map.addListener('click', (e) => onMapClick(e.latLng));
      if (onReady) onReady(map);
      setReady(true);
    });
    return () => { cancelled = true; };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  React.useEffect(() => {
    if (!ready || !mapRef.current) return;
    const g = window.google;
    markerLayerRef.current.forEach(m => m.setMap(null));
    markerLayerRef.current = markers.map((mk) => {
      const fill = mk.color || '#8E7DA6';
      const svg = `data:image/svg+xml;utf8,${encodeURIComponent(
        `<svg xmlns='http://www.w3.org/2000/svg' width='32' height='40' viewBox='0 0 32 40'>
          <path d='M16 1c8.3 0 15 6.7 15 15 0 10.5-15 23-15 23S1 26.5 1 16C1 7.7 7.7 1 16 1z'
            fill='${fill}' stroke='#fff' stroke-width='2'/>
          <circle cx='16' cy='16' r='5' fill='rgba(255,255,255,0.7)'/>
        </svg>`
      )}`;
      const marker = new g.maps.Marker({
        position: mk.pos,
        map: mapRef.current,
        icon: { url: svg, scaledSize: new g.maps.Size(32, 40), anchor: new g.maps.Point(16, 38) },
        title: mk.name || mk.title || '',
        zIndex: mk.active ? 100 : 10,
      });
      if (mk.onClick) marker.addListener('click', mk.onClick);
      return marker;
    });
  }, [markers, ready]);

  React.useEffect(() => {
    if (!ready || !mapRef.current || !center || interactive) return;
    mapRef.current.setCenter(center);
  }, [center, interactive, ready]);

  // fitSignature が変わったら、全マーカーが収まる倍率・中心へ自動フィット。
  // signature は座標のみから作るため、ピン選択（activeの切替）では再フィットしない。
  React.useEffect(() => {
    if (!ready || !mapRef.current || !fitSignature) return;
    const g = window.google;
    const positions = markers.map(m => m.pos).filter(Boolean);
    if (positions.length === 0) return;
    if (positions.length === 1) {
      mapRef.current.setCenter(positions[0]);
      mapRef.current.setZoom(zoom);
      return;
    }
    const b = new g.maps.LatLngBounds();
    positions.forEach(p => b.extend(p));
    mapRef.current.fitBounds(b, { top: 64, right: 56, bottom: 64, left: 56 });
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [fitSignature, ready]);

  return (
    <div ref={elRef} style={{ width: '100%', height: '100%', ...style }} />
  );
}

/* ─────────────────────────── Weather (Open-Meteo) ───────────────────────────
   予定の日付・エリアから実際の天気を取得し 'sun' | 'rain' を返す。
   APIキー不要・CORS対応の Open-Meteo を使用。エリアはジオコーディングで
   座標化（失敗時は東京にフォールバック）。結果はキャッシュして再取得を防ぐ。 */
const WX_TOKYO = { lat: 35.6812, lon: 139.7671 };
const _geoCache = new Map();
const _wxCache = new Map();

// WMO weather code: 51+ は霧雨/雨/雪/雷雨とみなし 'rain'、それ以外は 'sun'
const wxKind = (code) => (typeof code === 'number' && code >= 51 ? 'rain' : 'sun');

async function geocodeArea(area) {
  const q = (area || '').replace(/あたり|周辺|近辺/g, '').trim();
  if (!q) return WX_TOKYO;
  if (_geoCache.has(q)) return _geoCache.get(q);
  const pr = (async () => {
    try {
      const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(q)}&count=1&language=ja&countryCode=JP`;
      const j = await (await fetch(url)).json();
      const g = j && j.results && j.results[0];
      if (g && typeof g.latitude === 'number') return { lat: g.latitude, lon: g.longitude };
    } catch (_) {}
    return WX_TOKYO;
  })();
  _geoCache.set(q, pr);
  return pr;
}

async function getWeather(dateISO, area) {
  const iso = (dateISO || '').slice(0, 10);
  if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return 'sun';
  const key = `${iso}|${area || ''}`;
  if (_wxCache.has(key)) return _wxCache.get(key);
  const pr = (async () => {
    try {
      const { lat, lon } = await geocodeArea(area);
      const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&daily=weather_code&timezone=Asia%2FTokyo&start_date=${iso}&end_date=${iso}`;
      const j = await (await fetch(url)).json();
      const code = j && j.daily && j.daily.weather_code && j.daily.weather_code[0];
      if (code == null) return 'sun';
      return wxKind(code);
    } catch (_) {
      return 'sun';
    }
  })();
  _wxCache.set(key, pr);
  return pr;
}

/* ─────────── 地図のスムーズズーム ───────────
   ラスタ地図の setZoom() は即座に切り替わってアニメーションしない。
   ただし±1段のズームは短いクロスフェードが入るため、1段ずつ刻んで
   連続実行すると「寄っていく／引いていく」連続ズームに見える。
   panTo は近距離ならネイティブでアニメーションするので、先にパンしてからズームを刻む。 */
function mapFlyTo(map, pos, targetZoom) {
  if (!map) return;
  if (map.__flyTimer) clearTimeout(map.__flyTimer);
  const target = Math.round(targetZoom);
  const stepZoom = () => {
    const z = Math.round(map.getZoom());
    if (z === target) return;
    map.setZoom(z < target ? z + 1 : z - 1);
    map.__flyTimer = setTimeout(stepZoom, 140); // 1段ごとのクロスフェードを待つ
  };
  if (pos) map.panTo(pos);
  map.__flyTimer = setTimeout(stepZoom, 200); // パンのアニメーションに少し譲る
}

// bounds が収まるズーム段数を（fitBounds を使わずに）計算する。mapFlyTo と組み合わせて
// 「全体表示」もアニメーションさせるため。pad はビューポート両端の余白(px)。
function zoomForBounds(map, bounds, pad = 110) {
  const div = map.getDiv();
  const w = Math.max(1, div.clientWidth - pad), h = Math.max(1, div.clientHeight - pad);
  const ne = bounds.getNorthEast(), sw = bounds.getSouthWest();
  const latFrac = lat => {
    const s = Math.sin(lat * Math.PI / 180);
    return Math.log((1 + s) / (1 - s)) / 2;
  };
  const latFraction = Math.abs(latFrac(ne.lat()) - latFrac(sw.lat())) / (2 * Math.PI);
  let lngDiff = ne.lng() - sw.lng();
  if (lngDiff < 0) lngDiff += 360;
  const lngFraction = lngDiff / 360;
  const zLat = latFraction > 0 ? Math.log2(h / 256 / latFraction) : Infinity;
  const zLng = lngFraction > 0 ? Math.log2(w / 256 / lngFraction) : Infinity;
  const z = Math.floor(Math.min(zLat, zLng));
  return Number.isFinite(z) ? Math.max(3, Math.min(18, z)) : 15;
}

function mapFlyToBounds(map, positions, pad) {
  if (!map || !window.google || !positions.length) return;
  const b = new window.google.maps.LatLngBounds();
  positions.forEach(p => b.extend(p));
  mapFlyTo(map, b.getCenter(), zoomForBounds(map, b, pad));
}

const Weather = { get: getWeather };

/* 予定日の天気アイコン（取得できるまでは fallback を表示）。
   色はデフォルトで sun=gold / rain=partner。style で上書き可。 */
function WeatherIcon({ dateISO, area, size = 13, fallback = 'sun', style }) {
  const [kind, setKind] = useState(fallback);
  useEffect(() => {
    let dead = false;
    getWeather(dateISO, area).then(k => { if (!dead) setKind(k); });
    return () => { dead = true; };
  }, [dateISO, area]);
  const color = kind === 'rain' ? 'var(--partner)' : 'var(--gold)';
  return <Icon name={kind} size={size} style={{ color, ...style }} />;
}

/* プラン詳細トップのバッジ（アイコン＋ラベル）。取得前は非表示。 */
function WeatherBadge({ dateISO, area }) {
  const [kind, setKind] = useState(null);
  useEffect(() => {
    let dead = false;
    getWeather(dateISO, area).then(k => { if (!dead) setKind(k); });
    return () => { dead = true; };
  }, [dateISO, area]);
  if (!kind) return null;
  const rain = kind === 'rain';
  return (
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 999, padding: '5px 10px', fontSize: 11.5, fontWeight: 700, color: 'var(--ink-soft)', whiteSpace: 'nowrap' }}>
      <Icon name={kind} size={14} style={{ color: rain ? 'var(--partner)' : 'var(--gold)' }} />
      {rain ? '雨' : '晴れ'}
    </div>
  );
}

/* ─────────── Lottie ───────────
   vendor/lottie_light.min.js（svgレンダラのみ）を使う。
   viewBox は "x y w h" 形式。素材のキャンバスに余白が多いとき、
   描画したい範囲だけを切り出して等倍に見せるために使う。 */
function LottieBox({ src, viewBox, loop = true, speed = 1, paused = false, style }) {
  const host = useRef(null);
  const animRef = useRef(null);
  useEffect(() => {
    const lottie = window.lottie;
    if (!lottie || !host.current) return;
    const anim = lottie.loadAnimation({
      container: host.current, renderer: 'svg', loop, autoplay: true, path: src,
      rendererSettings: viewBox ? { viewBoxSize: viewBox } : undefined,
    });
    anim.setSpeed(speed);
    animRef.current = anim;
    return () => { animRef.current = null; anim.destroy(); };
  }, [src, viewBox]);
  /* paused: 生成に失敗したときなど「動きを止めて見せたい」場面で使う（描画は残したまま停止） */
  useEffect(() => {
    const anim = animRef.current;
    if (!anim) return;
    paused ? anim.pause() : anim.play();
  }, [paused]);
  return <div ref={host} style={style} aria-hidden="true" />;
}

Object.assign(window, {
  AppCtx, useApp, Icon, Btn, Chip, Card, Charm, Avatar, AiAvatar, ImgSlot, Eyebrow,
  TopBar, BottomNav, Body, Field, Segmented, PrivacyBadge, SAFE_TOP, NAV_H,
  GoogleMapView, MAP_STYLE, normalizeEvent, LottieBox,
  mapFlyTo, mapFlyToBounds,
  Weather, WeatherIcon, WeatherBadge,
});
