import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useJourneyStore } from '../store/journeyStore'
import { useAuthStore } from '../store/authStore'
import { useTranslation } from '../i18n'
import { journeyApi, authApi, addonsApi, mapsApi } from '../api/client'
import { addListener, removeListener } from '../api/websocket'
import Navbar from '../components/Layout/Navbar'
import JourneyMap from '../components/Journey/JourneyMap'
import type { JourneyMapHandle } from '../components/Journey/JourneyMap'
import JournalBody from '../components/Journey/JournalBody'
import MarkdownToolbar from '../components/Journey/MarkdownToolbar'
import PhotoLightbox from '../components/Journey/PhotoLightbox'
import { useToast } from '../components/shared/Toast'
import ConfirmDialog from '../components/shared/ConfirmDialog'
import {
  ArrowLeft, RefreshCw, MoreHorizontal, Share2, Download, List, Grid, MapPin, Link, Copy,
  Clock, Package, Image, ChevronRight,
  UserPlus, Plus, Minus, Calendar, Camera, BookOpen, X, Check, ImagePlus, Trash2, Pencil,
  Laugh, Smile, Meh, Annoyed, Frown,
  Sun, CloudSun, Cloud, CloudRain, CloudLightning, Snowflake, ChevronDown, Eye, EyeOff,
} from 'lucide-react'
import MobileMapTimeline from '../components/Journey/MobileMapTimeline'
import MobileEntryView from '../components/Journey/MobileEntryView'
import { useIsMobile } from '../hooks/useIsMobile'
import type { JourneyEntry, JourneyPhoto, JourneyDetail } from '../store/journeyStore'

const GRADIENTS = [
  'linear-gradient(135deg, #0F172A 0%, #6366F1 45%, #EC4899 100%)',
  'linear-gradient(135deg, #1E293B 0%, #7C3AED 50%, #F59E0B 100%)',
  'linear-gradient(135deg, #134E5E 0%, #71B280 100%)',
  'linear-gradient(135deg, #2D1B69 0%, #11998E 100%)',
  'linear-gradient(135deg, #4B134F 0%, #C94B4B 100%)',
  'linear-gradient(135deg, #373B44 0%, #4286F4 100%)',
]

function pickGradient(id: number): string {
  return GRADIENTS[id % GRADIENTS.length]
}

const MOOD_CONFIG: Record<string, { bg: string; text: string; icon: typeof Laugh; label: string }> = {
  amazing: { bg: '#FDF2F8', text: '#BE185D', icon: Laugh, label: 'journey.mood.amazing' },
  good: { bg: '#FFFBEB', text: '#B45309', icon: Smile, label: 'journey.mood.good' },
  neutral: { bg: '#F4F4F5', text: '#3F3F46', icon: Meh, label: 'journey.mood.neutral' },
  rough: { bg: '#F5F3FF', text: '#6D28D9', icon: Frown, label: 'journey.mood.rough' },
}

const WEATHER_CONFIG: Record<string, { icon: typeof Sun; label: string }> = {
  sunny: { icon: Sun, label: 'journey.weather.sunny' },
  partly: { icon: CloudSun, label: 'journey.weather.partly' },
  cloudy: { icon: Cloud, label: 'journey.weather.cloudy' },
  rainy: { icon: CloudRain, label: 'journey.weather.rainy' },
  stormy: { icon: CloudLightning, label: 'journey.weather.stormy' },
  cold: { icon: Snowflake, label: 'journey.weather.cold' },
}

function groupByDate(entries: JourneyEntry[]): Map<string, JourneyEntry[]> {
  const groups = new Map<string, JourneyEntry[]>()
  for (const e of entries) {
    const d = e.entry_date
    if (!groups.has(d)) groups.set(d, [])
    groups.get(d)!.push(e)
  }
  return groups
}

function formatDate(d: string): { weekday: string; month: string; day: number } {
  const date = new Date(d + 'T00:00:00')
  return {
    weekday: date.toLocaleDateString(undefined, { weekday: 'long' }),
    month: date.toLocaleDateString(undefined, { month: 'long' }),
    day: date.getDate(),
  }
}

function photoUrl(p: JourneyPhoto, size: 'thumbnail' | 'original' = 'thumbnail'): string {
  return `/api/photos/${p.photo_id}/${size}`
}

export default function JourneyDetailPage() {
  const { id } = useParams()
  const navigate = useNavigate()
  const toast = useToast()
  const { t } = useTranslation()
  const { current, loading, notFound, loadJourney, updateEntry, deleteEntry, uploadPhotos, deletePhoto } = useJourneyStore()
  const mapRef = useRef<JourneyMapHandle>(null)
  const fullMapRef = useRef<JourneyMapHandle>(null)
  const [activeLocationId, setActiveLocationId] = useState<string | null>(null)

  const isMobile = useIsMobile()
  const [view, setView] = useState<'timeline' | 'gallery' | 'map'>('timeline')
  const [viewingEntry, setViewingEntry] = useState<JourneyEntry | null>(null)
  const [editingEntry, setEditingEntry] = useState<JourneyEntry | null>(null)
  const [lightbox, setLightbox] = useState<{ photos: { id: number; src: string; caption?: string | null; provider?: string; asset_id?: string | null; owner_id?: number | null }[]; index: number } | null>(null)
  const [deleteTarget, setDeleteTarget] = useState<JourneyEntry | null>(null)
  const [showInvite, setShowInvite] = useState(false)
  const [showAddTrip, setShowAddTrip] = useState(false)
  const [unlinkTrip, setUnlinkTrip] = useState<{ trip_id: number; title: string } | null>(null)
  const [showSettings, setShowSettings] = useState(false)
  const [hideSkeletons, setHideSkeletons] = useState(false)

  useEffect(() => {
    if (id) loadJourney(Number(id)).catch(() => {})
  }, [id])

  useEffect(() => {
    if (current?.hide_skeletons !== undefined) setHideSkeletons(current.hide_skeletons)
  }, [current?.hide_skeletons])

  useEffect(() => {
    if (notFound) {
      toast.error(t('journey.notFound'))
      navigate('/journey')
    }
  }, [notFound])

  // WebSocket real-time updates
  useEffect(() => {
    if (!id) return
    const journeyId = Number(id)
    const handler = (event: Record<string, unknown>) => {
      const type = event.type as string
      if (!type?.startsWith('journey:')) return
      if (event.journeyId !== journeyId) return
      // reload journey data on any change from other contributors
      loadJourney(journeyId)
    }
    addListener(handler)
    return () => removeListener(handler)
  }, [id])

  // scroll sync with map
  const observerRef = useRef<IntersectionObserver | null>(null)
  const setupObserver = useCallback(() => {
    observerRef.current?.disconnect()
    observerRef.current = new IntersectionObserver((entries) => {
      for (const e of entries) {
        if (e.isIntersecting) {
          const entryId = e.target.getAttribute('data-entry-id')
          if (entryId) mapRef.current?.highlightMarker(entryId)
        }
      }
    }, { threshold: 0.5 })

    document.querySelectorAll('[data-entry-id]').forEach(el => {
      observerRef.current?.observe(el)
    })
  }, [])

  useEffect(() => {
    if (current?.entries?.length) {
      setTimeout(setupObserver, 300)
    }
    return () => observerRef.current?.disconnect()
  }, [current?.entries, setupObserver])

  const handleMarkerClick = useCallback((entryId: string) => {
    const el = document.querySelector(`[data-entry-id="${entryId}"]`)
    if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
  }, [])

  const handleLocationClick = useCallback((id: string) => {
    setActiveLocationId(id)
  }, [])

  const mapEntries = useMemo(
    () => (current?.entries || []).filter(e => e.location_lat && e.location_lng),
    [current?.entries]
  )

  const sidebarMapItems = useMemo(() => mapEntries.map(e => ({
    id: String(e.id),
    lat: e.location_lat!,
    lng: e.location_lng!,
    title: e.title || '',
    mood: e.mood,
    created_at: e.entry_date,
    entry_date: e.entry_date,
  })), [mapEntries])

  const tripDates = useMemo(() => {
    const dates = new Set<string>()
    if (!current?.trips) return dates
    for (const trip of current.trips) {
      if (!trip.start_date || !trip.end_date) continue
      const start = new Date(trip.start_date + 'T00:00:00')
      const end = new Date(trip.end_date + 'T00:00:00')
      for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
        dates.add(d.toISOString().split('T')[0])
      }
    }
    return dates
  }, [current?.trips])

  if (loading || !current) {
    return (
      <div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
        <Navbar />
        <div style={{ paddingTop: 'var(--nav-h, 0px)' }} className="flex justify-center py-20">
          <div className="w-6 h-6 border-2 border-zinc-300 border-t-zinc-900 rounded-full animate-spin" />
        </div>
      </div>
    )
  }

  const timelineEntries = current.entries.filter(e => e.title !== 'Gallery' && e.title !== '[Trip Photos]' && (!hideSkeletons || e.type !== 'skeleton'))
  const dayGroups = groupByDate(timelineEntries)
  const sortedDates = [...dayGroups.keys()].sort()

  const showMobileCombined = isMobile && view === 'timeline'

  return (
    <div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
      <Navbar />

      {/* Mobile combined map+timeline (Polarsteps-style) — renders as fullscreen overlay */}
      {showMobileCombined && (
        <MobileMapTimeline
          entries={timelineEntries}
          mapEntries={sidebarMapItems}
          dark={document.documentElement.classList.contains('dark')}
          onEntryClick={(entry) => setViewingEntry(entry)}
          onAddEntry={() => {
            const today = new Date().toISOString().split('T')[0]
            setEditingEntry({ id: 0, journey_id: current.id, author_id: 0, type: 'entry', entry_date: today, visibility: 'private', sort_order: 0, photos: [], created_at: 0, updated_at: 0 } as JourneyEntry)
          }}
        />
      )}

      {/* Fullscreen entry view (mobile) */}
      {viewingEntry && (
        <MobileEntryView
          entry={viewingEntry}
          onClose={() => setViewingEntry(null)}
          onEdit={() => { setViewingEntry(null); setEditingEntry(viewingEntry); }}
          onDelete={() => { setViewingEntry(null); setDeleteTarget(viewingEntry); }}
          onPhotoClick={(photos, idx) => setLightbox({ photos: photos.map(p => ({ id: p.id, src: photoUrl(p, 'original'), caption: p.caption, provider: p.provider, asset_id: p.asset_id, owner_id: p.owner_id })), index: idx })}
        />
      )}

      {/* Floating tab toggle on mobile combined view */}
      {showMobileCombined && (
        <div className="fixed top-[calc(var(--nav-h,56px)+12px)] left-4 z-30">
          <div className="flex bg-white/90 dark:bg-zinc-800/90 backdrop-blur-lg border border-zinc-200 dark:border-zinc-700 rounded-lg overflow-hidden shadow-lg">
            <button
              onClick={() => setView('timeline')}
              className="flex items-center gap-1.5 px-3 py-[7px] text-[12px] font-medium bg-zinc-900 dark:bg-white text-white dark:text-zinc-900"
            >
              <MapPin size={13} />
              {t('journey.detail.journeyTab') || 'Journey'}
            </button>
            <button
              onClick={() => setView('gallery')}
              className="flex items-center gap-1.5 px-3 py-[7px] text-[12px] font-medium text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
            >
              <Grid size={13} />
              {t('journey.share.gallery')}
            </button>
          </div>
        </div>
      )}

      <div style={{ paddingTop: 'var(--nav-h, 0px)' }} className={showMobileCombined ? 'hidden' : ''}>
        <div className="max-w-[1440px] mx-auto px-0 md:px-8 pt-0 md:py-6">

          {/* Back link — desktop */}
          <button onClick={() => navigate('/journey')} className="hidden md:inline-flex items-center gap-1.5 text-[12px] text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 mb-4 mx-0">
            <ArrowLeft size={14} />
            {t('journey.detail.backToJourney')}
          </button>

          {/* Hero card — full width */}
          <div className="px-4 md:px-0 mb-6">
            <div className="rounded-none md:rounded-2xl -mx-4 md:mx-0 overflow-hidden relative p-5 md:p-7" style={{ background: pickGradient(current.id), color: 'white' }}>
                {current.cover_image && (
                  <div className="absolute inset-0 z-[1]">
                    <img src={`/uploads/${current.cover_image}`} className="w-full h-full object-cover" alt="" />
                    <div className="absolute inset-0" style={{ background: pickGradient(current.id), opacity: 0.55 }} />
                  </div>
                )}
                <div className="absolute inset-0 pointer-events-none z-[2]" style={{ background: 'radial-gradient(circle at 20% 20%, rgba(236,72,153,0.3), transparent 50%), radial-gradient(circle at 80% 80%, rgba(99,102,241,0.3), transparent 50%)' }} />

                <div className="relative z-[3] flex items-center justify-between mb-5">
                  {/* Desktop: badges */}
                  <div className="hidden md:flex items-center gap-2">
                    {current.status === 'active' && (
                      <div className="inline-flex items-center gap-2 px-2.5 py-1 bg-white/15 backdrop-blur rounded-full text-[10px] font-semibold uppercase">
                        <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
                        Live
                      </div>
                    )}
                    <div className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/[0.12] backdrop-blur border border-white/15 rounded-full text-[11px] font-medium">
                      <RefreshCw size={11} />
                      {t('journey.detail.syncedWithTrips')}
                    </div>
                  </div>
                  {/* Mobile: back button on the left */}
                  <button
                    onClick={() => navigate('/journey')}
                    className="md:hidden w-[34px] h-[34px] rounded-lg bg-white/15 backdrop-blur flex items-center justify-center hover:bg-white/25"
                  >
                    <ArrowLeft size={14} />
                  </button>
                  <div className="flex items-center gap-1.5">
                    <button onClick={() => { import('../components/PDF/JourneyBookPDF').then(m => m.downloadJourneyBookPDF(current)) }} className="w-[34px] h-[34px] rounded-lg bg-white/15 backdrop-blur flex items-center justify-center hover:bg-white/25"><Download size={14} /></button>
                    <div className="relative group">
                      <button
                        onClick={async () => {
                          const next = !hideSkeletons
                          setHideSkeletons(next)
                          await journeyApi.updatePreferences(current.id, { hide_skeletons: next })
                        }}
                        className={`w-[34px] h-[34px] rounded-lg backdrop-blur flex items-center justify-center ${hideSkeletons ? 'bg-white/30' : 'bg-white/15 hover:bg-white/25'}`}
                      >
                        {hideSkeletons ? <EyeOff size={14} /> : <Eye size={14} />}
                      </button>
                      <span className="absolute top-full mt-2 left-1/2 -translate-x-1/2 px-2 py-1 rounded-md bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 text-[11px] font-medium whitespace-nowrap opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity">
                        {hideSkeletons ? t('journey.skeletons.show') : t('journey.skeletons.hide')}
                      </span>
                    </div>
                    <button onClick={() => setShowSettings(true)} className="w-[34px] h-[34px] rounded-lg bg-white/15 backdrop-blur flex items-center justify-center hover:bg-white/25"><MoreHorizontal size={14} /></button>
                  </div>
                </div>

                <div className="relative z-[3] mb-5">
                  <h1 className="text-[32px] font-bold tracking-[-0.02em] leading-tight mb-1.5">{current.title}</h1>
                  {current.subtitle && <p className="text-[13px] opacity-85">{current.subtitle}</p>}
                </div>

                <div className="relative z-[3] border-t border-white/15 pt-5 flex items-end justify-between">
                  <div className="flex gap-8">
                    {[
                      { value: sortedDates.length, label: t('journey.stats.days') },
                      { value: current.stats.cities, label: t('journey.stats.cities') },
                      { value: current.stats.entries, label: t('journey.stats.entries') },
                      { value: current.stats.photos, label: t('journey.stats.photos') },
                    ].map(s => (
                      <div key={s.label} className="flex flex-col gap-0.5">
                        <span className="text-[20px] font-bold">{s.value}</span>
                        <span className="text-[10px] uppercase tracking-[0.08em] opacity-70">{s.label}</span>
                      </div>
                    ))}
                  </div>
                </div>
            </div>
          </div>

          {/* Main grid */}
          <div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6 px-4 md:px-0">

            {/* Left column */}
            <div>
              {/* View Controls */}
              <div className="flex items-center justify-between mt-5 mb-5">
                <div className="flex bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg overflow-hidden">
                  {(isMobile
                    ? [
                        { id: 'timeline' as const, icon: MapPin, label: t('journey.detail.journeyTab') || 'Journey' },
                        { id: 'gallery' as const, icon: Grid, label: t('journey.share.gallery') },
                      ]
                    : [
                        { id: 'timeline' as const, icon: List, label: t('journey.share.timeline') },
                        { id: 'gallery' as const, icon: Grid, label: t('journey.share.gallery') },
                        { id: 'map' as const, icon: MapPin, label: t('journey.share.map') },
                      ]
                  ).map(v => (
                    <button
                      key={v.id}
                      onClick={() => setView(v.id)}
                      className={`flex items-center gap-1.5 px-3 py-[7px] text-[12px] font-medium ${
                        view === v.id
                          ? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900'
                          : 'text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
                      }`}
                    >
                      <v.icon size={13} />
                      {v.label}
                    </button>
                  ))}
                </div>
                {(!isMobile ? view === 'timeline' : view !== 'gallery') && (
                  <button
                    onClick={() => {
                      const today = new Date().toISOString().split('T')[0]
                      setEditingEntry({ id: 0, journey_id: current.id, author_id: 0, type: 'entry', entry_date: today, visibility: 'private', sort_order: 0, photos: [], created_at: 0, updated_at: 0 } as JourneyEntry)
                    }}
                    className={`w-8 h-8 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 flex items-center justify-center hover:bg-zinc-800 dark:hover:bg-zinc-100 ${isMobile && view === 'timeline' ? 'hidden' : ''}`}
                  >
                    <Plus size={16} />
                  </button>
                )}
              </div>

              {/* Timeline (desktop only — mobile uses fullscreen combined view above) */}
              {!isMobile && view === 'timeline' && (
                <div className="flex flex-col gap-6 pb-24 md:pb-6">
                  {sortedDates.length === 0 && (
                    <div className="text-center py-16">
                      <div className="w-16 h-16 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center mx-auto mb-4">
                        <BookOpen size={24} className="text-zinc-400" />
                      </div>
                      <p className="text-[15px] font-medium text-zinc-700 dark:text-zinc-300">No entries yet</p>
                      <p className="text-[12px] text-zinc-500 mt-1">Add a trip to get started with skeleton entries</p>
                    </div>
                  )}

                  {sortedDates.map((date, dayIdx) => {
                    const entries = dayGroups.get(date)!
                    const fd = formatDate(date)
                    const locations = [...new Set(entries.map(e => e.location_name).filter(Boolean))]

                    return (
                      <div key={date} className="flex flex-col gap-3">
                        <div className="sticky top-0 md:top-[68px] z-[5] bg-white/95 dark:bg-zinc-900/95 backdrop-blur border-y md:border border-zinc-200 dark:border-zinc-700 rounded-none md:rounded-xl -mx-4 md:mx-0 px-4 py-3.5 flex items-center justify-between">
                          <div className="flex items-center gap-3">
                            <div className="w-8 h-8 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 flex items-center justify-center text-[13px] font-bold">
                              {dayIdx + 1}
                            </div>
                            <div>
                              <h3 className="text-[14px] font-semibold text-zinc-900 dark:text-white">{new Date(date + 'T00:00:00').toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long' })}</h3>
                            </div>
                          </div>
                          <div className="flex items-center gap-3 text-[11px] text-zinc-500">
                            <span className="flex items-center gap-1"><MapPin size={12} /> {entries.length} {t('journey.synced.places')}</span>
                          </div>
                        </div>

                        {entries.map(entry => (
                          <div key={entry.id} data-entry-id={String(entry.id)}>
                            {entry.type === 'skeleton' ? (
                              <SkeletonCard entry={entry} onClick={() => setEditingEntry(entry)} />
                            ) : entry.type === 'checkin' ? (
                              <CheckinCard entry={entry} onClick={() => setEditingEntry(entry)} />
                            ) : (
                              <EntryCard
                                entry={entry}
                                onEdit={() => setEditingEntry(entry)}
                                onDelete={() => setDeleteTarget(entry)}
                                onPhotoClick={(photos, idx) => setLightbox({ photos: photos.map(p => ({ id: p.id, src: photoUrl(p, 'original'), caption: p.caption, provider: p.provider, asset_id: p.asset_id, owner_id: p.owner_id })), index: idx })}
                              />
                            )}
                          </div>
                        ))}
                      </div>
                    )
                  })}
                </div>
              )}

              {/* Gallery View */}
              {view === 'gallery' && (
                <GalleryView
                  entries={current.entries}
                  journeyId={current.id}
                  userId={useAuthStore.getState().user?.id || 0}
                  trips={current.trips}
                  onPhotoClick={(photos, idx) => setLightbox({ photos: photos.map(p => ({ id: p.id, src: photoUrl(p, 'original'), caption: p.caption, provider: p.provider, asset_id: p.asset_id, owner_id: p.owner_id })), index: idx })}
                  onRefresh={() => loadJourney(Number(id))}
                />
              )}

              {/* Full Map View (desktop only — mobile uses combined view) */}
              {!isMobile && view === 'map' && <div className="pb-24 md:pb-6"><MapView
                entries={current.entries}
                mapEntries={mapEntries}
                sortedDates={sortedDates}
                activeLocationId={activeLocationId}
                fullMapRef={fullMapRef}
                onLocationClick={handleLocationClick}
              /></div>}
            </div>

            {/* Right sidebar — hidden on mobile */}
            <div className="hidden lg:flex flex-col gap-4 lg:sticky lg:top-[80px] lg:self-start">
              {/* Map panel */}
              <div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl overflow-hidden">
                <JourneyMap
                  ref={mapRef}
                  checkins={[]}
                  entries={sidebarMapItems as any}
                  height={240}
                  onMarkerClick={handleMarkerClick}
                />
                <div className="px-3.5 py-2.5 border-t border-zinc-200 dark:border-zinc-700 flex items-center justify-between text-[11px] text-zinc-500">
                  <span>{mapEntries.length} {t('journey.stats.places')}</span>
                </div>
              </div>

              {/* Stats panel */}
              <div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl p-4">
                <div className="text-[10px] font-semibold tracking-[0.1em] uppercase text-zinc-500 mb-3">{t('journey.detail.journeyStats')}</div>
                <div className="grid grid-cols-2 gap-2">
                  {[
                    { value: sortedDates.length, label: t('journey.stats.days') },
                    { value: current.stats.entries, label: t('journey.stats.entries') },
                    { value: current.stats.photos, label: t('journey.stats.photos') },
                    { value: current.stats.cities, label: t('journey.stats.cities') },
                  ].map(s => (
                    <div key={s.label} className="rounded-lg bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-zinc-700/50 px-3 py-2.5">
                      <div className="text-[18px] font-bold tracking-[-0.02em] text-zinc-900 dark:text-white leading-none mb-0.5">{s.value}</div>
                      <div className="text-[9px] uppercase tracking-[0.1em] text-zinc-400 dark:text-zinc-500 font-semibold">{s.label}</div>
                    </div>
                  ))}
                </div>
              </div>

              {/* Synced Trips panel */}
              <div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl p-4">
                <div className="flex items-center justify-between mb-3.5">
                  <span className="text-[10px] font-semibold tracking-[0.1em] uppercase text-zinc-500">{t('journey.detail.syncedTrips')}</span>
                  <button onClick={() => setShowAddTrip(true)} className="w-[22px] h-[22px] rounded-md bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-700">
                    <Plus size={12} />
                  </button>
                </div>
                <div className="flex flex-col gap-1">
                  {current.trips.map((trip: any) => (
                    <div
                      key={trip.trip_id}
                      onClick={() => navigate(`/trips/${trip.trip_id}`)}
                      className="group flex items-center gap-2.5 p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer"
                    >
                      <div className="w-9 h-9 rounded-md flex-shrink-0" style={{ background: pickGradient(trip.trip_id) }} />
                      <div className="flex-1 min-w-0">
                        <div className="text-xs font-medium text-zinc-900 dark:text-white truncate">{trip.title}</div>
                        <div className="text-[10px] text-zinc-500 flex items-center gap-1.5">
                          {trip.place_count || 0} {t('journey.detail.places')}
                          <span className="inline-flex items-center gap-0.5 px-1.5 py-px rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-[9px] font-medium"><span className="w-1 h-1 rounded-full bg-emerald-500" />{t('journey.synced.synced')}</span>
                        </div>
                      </div>
                      <button
                        onClick={e => { e.stopPropagation(); setUnlinkTrip({ trip_id: trip.trip_id, title: trip.title }) }}
                        className="w-6 h-6 rounded-md flex items-center justify-center text-zinc-400 opacity-0 group-hover:opacity-100 hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/30 dark:hover:text-red-400 transition-opacity"
                        title="Unlink trip"
                      >
                        <Trash2 size={12} />
                      </button>
                      <ChevronRight size={14} className="text-zinc-400" />
                    </div>
                  ))}
                  {current.trips.length === 0 && (
                    <p className="text-[11px] text-zinc-400 text-center py-3">{t('journey.detail.noTripsLinked')}</p>
                  )}
                </div>
              </div>

              {/* Contributors panel */}
              <div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl p-4">
                <div className="flex items-center justify-between mb-3.5">
                  <span className="text-[10px] font-semibold tracking-[0.1em] uppercase text-zinc-500">{t('journey.detail.contributors')}</span>
                  <button onClick={() => setShowInvite(true)} className="w-[22px] h-[22px] rounded-md bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-700">
                    <UserPlus size={12} />
                  </button>
                </div>
                <div className="flex flex-col gap-2.5">
                  {current.contributors.map((c: any) => (
                    <div key={c.user_id} className="flex items-center gap-2.5">
                      {c.avatar_url ? (
                        <img src={c.avatar_url} className="w-7 h-7 rounded-full object-cover" alt="" />
                      ) : (
                        <div className="w-7 h-7 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 flex items-center justify-center text-[11px] font-semibold">
                          {(c.username || '?')[0].toUpperCase()}
                        </div>
                      )}
                      <div className="flex-1 min-w-0">
                        <div className="text-xs font-medium text-zinc-900 dark:text-white">{c.username}</div>
                      </div>
                      <span className={`text-[9px] font-medium px-1.5 py-0.5 rounded-full ${
                        c.role === 'owner' ? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-500'
                      }`}>
                        {c.role}
                      </span>
                    </div>
                  ))}
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

      {/* Entry Editor */}
      {editingEntry && (
        <EntryEditor
          entry={editingEntry}
          journeyId={current.id}
          tripDates={tripDates}
          galleryPhotos={current.entries.flatMap(e => e.photos || [])}
          onClose={() => setEditingEntry(null)}
          onSave={async (data) => {
            let entryId = editingEntry.id
            if (editingEntry.id === 0) {
              const created = await useJourneyStore.getState().createEntry(current.id, data)
              entryId = created.id
            } else {
              await updateEntry(editingEntry.id, data)
            }
            return entryId
          }}
          onUploadPhotos={async (entryId, formData) => {
            return await uploadPhotos(entryId, formData)
          }}
          onDone={() => {
            setEditingEntry(null)
            loadJourney(Number(id))
          }}
        />
      )}

      {/* Journey Settings */}
      {showSettings && (
        <JourneySettingsDialog
          journey={current}
          onClose={() => setShowSettings(false)}
          onSaved={() => { setShowSettings(false); loadJourney(Number(id)) }}
          onOpenInvite={() => { setShowSettings(false); setShowInvite(true) }}
        />
      )}

      {/* Add Trip Dialog */}
      {showAddTrip && current && (
        <AddTripDialog
          journeyId={current.id}
          existingTripIds={current.trips.map((t: any) => t.trip_id)}
          onClose={() => setShowAddTrip(false)}
          onAdded={() => { setShowAddTrip(false); loadJourney(Number(id)) }}
        />
      )}

      {/* Contributor Invite Dialog */}
      {showInvite && (
        <ContributorInviteDialog
          journeyId={current.id}
          existingUserIds={current.contributors.map((c: any) => c.user_id)}
          onClose={() => setShowInvite(false)}
          onInvited={() => { setShowInvite(false); loadJourney(Number(id)) }}
        />
      )}

      {/* Delete confirm */}
      <ConfirmDialog
        isOpen={!!deleteTarget}
        onClose={() => setDeleteTarget(null)}
        onConfirm={async () => {
          if (!deleteTarget) return
          await deleteEntry(deleteTarget.id)
          setDeleteTarget(null)
          loadJourney(Number(id))
        }}
        title={t('journey.entries.deleteTitle')}
        message={t('journey.deleteConfirmMessage', { title: deleteTarget?.title || 'this entry' })}
        confirmLabel={t('common.delete')}
        danger
      />

      {/* Unlink Trip confirm */}
      <ConfirmDialog
        isOpen={!!unlinkTrip}
        onClose={() => setUnlinkTrip(null)}
        onConfirm={async () => {
          if (!unlinkTrip || !current) return
          try {
            await journeyApi.removeTrip(current.id, unlinkTrip.trip_id)
            toast.success(t('journey.trips.tripUnlinked'))
            setUnlinkTrip(null)
            loadJourney(Number(id))
          } catch {
            toast.error(t('journey.trips.unlinkFailed'))
          }
        }}
        title={t('journey.trips.unlinkTrip')}
        message={t('journey.trips.unlinkMessage', { title: unlinkTrip?.title })}
        confirmLabel={t('journey.trips.unlink')}
        danger
      />

      {/* Lightbox */}
      {lightbox && (
        <PhotoLightbox
          photos={lightbox.photos.map(p => ({ id: p.id.toString(), src: p.src, caption: p.caption, provider: p.provider, asset_id: p.asset_id, owner_id: p.owner_id }))}
          startIndex={lightbox.index}
          onClose={() => setLightbox(null)}
        />
      )}
    </div>
  )
}

// ── Map View ──────────────────────────────────────────────────────────────

function MapView({ entries, mapEntries, sortedDates, activeLocationId, fullMapRef, onLocationClick }: {
  entries: JourneyEntry[]
  mapEntries: JourneyEntry[]
  sortedDates: string[]
  activeLocationId: string | null
  fullMapRef: React.RefObject<JourneyMapHandle | null>
  onLocationClick: (id: string) => void
}) {
  const { t } = useTranslation()
  // group map entries by date
  const byDate = new Map<string, { entry: JourneyEntry; globalIdx: number }[]>()
  mapEntries.forEach((e, i) => {
    const d = e.entry_date
    if (!byDate.has(d)) byDate.set(d, [])
    byDate.get(d)!.push({ entry: e, globalIdx: i })
  })
  const dates = [...byDate.keys()].sort()

  // find first and last entry indices
  const firstId = mapEntries[0]?.id
  const lastId = mapEntries[mapEntries.length - 1]?.id

  const mapItems = useMemo(() => mapEntries.map(e => ({
    id: String(e.id),
    lat: e.location_lat!,
    lng: e.location_lng!,
    title: e.title || '',
    mood: e.mood,
    entry_date: e.entry_date,
  })), [mapEntries])

  return (
    <div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl overflow-hidden">
      <JourneyMap
        ref={fullMapRef}
        checkins={[]}
        entries={mapItems as any}
        height={560}
        activeMarkerId={activeLocationId}
        onMarkerClick={onLocationClick}
      />

      {/* Locations list */}
      <div>
        {/* Stats header */}
        {mapEntries.length > 0 && (
          <div className="mx-5 mt-4 mb-2 grid grid-cols-3 gap-2">
            {[
              { value: mapEntries.length, label: t('journey.stats.places') },
              { value: dates.length, label: t('journey.stats.days') },
              { value: entries.filter(e => e.type === 'entry').length, label: 'Stories' },
            ].map(s => (
              <div key={s.label} className="bg-zinc-50 dark:bg-zinc-800 rounded-xl p-3 text-center">
                <div className="text-[17px] font-bold text-zinc-900 dark:text-white tracking-tight">{s.value}</div>
                <div className="text-[9px] font-medium text-zinc-500 uppercase tracking-[0.06em]">{s.label}</div>
              </div>
            ))}
          </div>
        )}

        {/* Day groups */}
        <div className="px-5 pb-5">
          {dates.map((date, dayIdx) => {
            const items = byDate.get(date)!
            const fd = formatDate(date)

            return (
              <div key={date}>
                {/* Day separator */}
                <div className="flex items-center gap-2.5 py-3">
                  <span className="text-[10px] font-bold text-zinc-500 dark:text-zinc-400 tracking-[0.12em] uppercase">{t('journey.detail.day', { number: dayIdx + 1 })}</span>
                  <span className="text-[10px] text-zinc-400 font-medium">{fd.month} {fd.day}</span>
                  <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
                </div>

                {/* Location items */}
                {items.map(({ entry: e, globalIdx }, itemIdx) => {
                  const isActive = activeLocationId === String(e.id)
                  const isFirst = e.id === firstId
                  const isLast = e.id === lastId
                  const showConnector = itemIdx < items.length - 1

                  return (
                    <div key={e.id}>
                      <div
                        onClick={() => onLocationClick(String(e.id))}
                        className={`flex items-center gap-3 p-3 rounded-[14px] cursor-pointer transition-all ${
                          isActive
                            ? 'bg-zinc-100 dark:bg-zinc-800 border border-zinc-900 dark:border-zinc-100 translate-x-0.5'
                            : 'bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 hover:border-zinc-400 dark:hover:border-zinc-500 hover:translate-x-0.5'
                        }`}
                      >
                        {/* Number badge */}
                        <div className={`w-9 h-9 rounded-full flex items-center justify-center text-[13px] font-bold flex-shrink-0 border-2 border-white dark:border-zinc-900 ${
                          isActive
                            ? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 shadow-[0_0_0_2px_rgba(0,0,0,0.15)]'
                            : 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 shadow-[0_0_0_1px_rgba(0,0,0,0.1)]'
                        }`}>
                          {globalIdx + 1}
                        </div>

                        {/* Info */}
                        <div className="flex-1 min-w-0">
                          <div className="flex items-center gap-1.5 mb-0.5">
                            <span className="text-[14px] font-semibold text-zinc-900 dark:text-white truncate">{e.title || e.location_name}</span>
                          </div>
                          <div className="text-[11px] text-zinc-500 truncate">
                            {e.location_name}{e.entry_time ? ` · ${e.entry_time}` : ''}
                          </div>
                        </div>

                        {/* Chevron */}
                        <ChevronRight size={14} className={`flex-shrink-0 ${isActive ? 'text-zinc-900 dark:text-white' : 'text-zinc-300 dark:text-zinc-600'}`} />
                      </div>

                      {/* Connector line */}
                      {showConnector && (
                        <div className="w-0.5 h-2 bg-zinc-200 dark:bg-zinc-700 ml-[18px] rounded-full" />
                      )}
                    </div>
                  )
                })}
              </div>
            )
          })}

        </div>
      </div>
    </div>
  )
}

// ── Gallery View ──────────────────────────────────────────────────────────

function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefresh }: {
  entries: JourneyEntry[]
  journeyId: number
  userId: number
  trips: JourneyTrip[]
  onPhotoClick: (photos: JourneyPhoto[], index: number) => void
  onRefresh: () => void
}) {
  const { t } = useTranslation()
  const [showPicker, setShowPicker] = useState(false)
  const [pickerProvider, setPickerProvider] = useState<string | null>(null)
  const [availableProviders, setAvailableProviders] = useState<{ id: string; name: string }[]>([])
  const [galleryUploading, setGalleryUploading] = useState(false)
  const toast = useToast()

  // check which providers are enabled AND connected for the current user
  useEffect(() => {
    (async () => {
      try {
        const addonsData = await addonsApi.enabled()
        const enabledProviders = (addonsData.addons || []).filter(
          (a: any) => a.type === 'photo_provider' && a.enabled
        )
        const connected: { id: string; name: string }[] = []
        for (const p of enabledProviders) {
          try {
            const res = await fetch(`/api/integrations/memories/${p.id}/status`, { credentials: 'include' })
            if (res.ok) {
              const status = await res.json()
              if (status.connected) connected.push({ id: p.id, name: p.name })
            }
          } catch {}
        }
        setAvailableProviders(connected)
      } catch {}
    })()
  }, [])

  const allPhotos: { photo: JourneyPhoto; entry: JourneyEntry }[] = []
  for (const e of entries) {
    for (const p of e.photos) {
      allPhotos.push({ photo: p, entry: e })
    }
  }

  const entriesWithContent = entries.filter(e => e.type !== 'skeleton' || e.title)

  const browseProvider = (provider: string) => {
    setPickerProvider(provider)
    setShowPicker(true)
  }

  const galleryFileRef = useRef<HTMLInputElement>(null)

  const handleGalleryUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files
    if (!files?.length) return
    setGalleryUploading(true)
    try {
      // find existing "Gallery" entry or create one
      let galleryEntry = entries.find(e => e.title === 'Gallery' && e.type === 'entry')
      let entryId = galleryEntry?.id
      if (!entryId) {
        const entry = await journeyApi.createEntry(journeyId, {
          title: t('journey.share.gallery'),
          entry_date: new Date().toISOString().split('T')[0],
          type: 'entry',
        })
        entryId = entry.id
      }
      const formData = new FormData()
      for (const f of files) formData.append('photos', f)
      await journeyApi.uploadPhotos(entryId, formData)
      toast.success(t('journey.photosUploaded', { count: files.length }))
      onRefresh()
    } catch {
      toast.error(t('journey.settings.coverFailed'))
    } finally {
      setGalleryUploading(false)
    }
    e.target.value = ''
  }

  const handleDeletePhoto = async (photoId: number) => {
    // Optimistic update — remove photo from local state immediately
    const store = useJourneyStore.getState()
    if (store.current) {
      const updated = {
        ...store.current,
        entries: store.current.entries.map(e => ({
          ...e,
          photos: e.photos.filter(p => p.id !== photoId),
        })).filter(e => e.type !== 'entry' || e.title !== 'Gallery' || e.photos.length > 0 || e.story),
      }
      useJourneyStore.setState({ current: updated })
    }
    try {
      await journeyApi.deletePhoto(photoId)
    } catch {
      toast.error(t('common.error'))
      onRefresh() // Revert on error
    }
  }

  return (
    <div>
      <input ref={galleryFileRef} type="file" accept="image/*" multiple onChange={handleGalleryUpload} className="hidden" />

      {/* Header */}
      <div className="flex items-center justify-between mb-4 flex-wrap gap-2">
        <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 text-[10px] font-medium text-zinc-500 dark:text-zinc-400">
          <Camera size={10} /> {allPhotos.length} {t('journey.detail.photos')}
        </span>
        <div className="flex items-center gap-2">
          <button
            onClick={() => galleryFileRef.current?.click()}
            disabled={galleryUploading}
            className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[11px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-50"
          >
            {galleryUploading ? (
              <><div className="w-3 h-3 border-2 border-white/30 dark:border-zinc-900/30 border-t-white dark:border-t-zinc-900 rounded-full animate-spin" /> {t('journey.editor.uploading')}</>
            ) : (
              <><Plus size={12} /> {t('common.upload')}</>
            )}
          </button>
          {availableProviders.map(p => (
            <button
              key={p.id}
              onClick={() => browseProvider(p.id)}
              className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border border-zinc-200 dark:border-zinc-700 text-[11px] font-medium text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800"
            >
              <Image size={12} />
              {p.name}
            </button>
          ))}
        </div>
      </div>

      {allPhotos.length === 0 ? (
        <div className="text-center py-16">
          <div className="w-16 h-16 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center mx-auto mb-4">
            <Image size={24} className="text-zinc-400" />
          </div>
          <p className="text-[15px] font-medium text-zinc-700 dark:text-zinc-300">{t('journey.detail.noPhotos')}</p>
          <p className="text-[12px] text-zinc-500 mt-1">{t('journey.detail.noPhotosHint')}</p>
        </div>
      ) : (
        <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-1.5 pb-24 md:pb-6">
          {allPhotos.map(({ photo, entry }, i) => (
            <div
              key={photo.id}
              className="relative aspect-square rounded-lg overflow-hidden cursor-pointer group"
              onClick={() => onPhotoClick(allPhotos.map(a => a.photo), i)}
            >
              <img
                src={photoUrl(photo, 'thumbnail')}
                alt={photo.caption || ''}
                className="w-full h-full object-cover transition-transform group-hover:scale-105"
                loading="lazy"
              />
              <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
              {/* Delete button */}
              <button
                onClick={(e) => { e.stopPropagation(); handleDeletePhoto(photo.id) }}
                className="absolute top-1.5 right-1.5 w-6 h-6 rounded-full bg-black/60 backdrop-blur text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10"
              >
                <X size={12} />
              </button>
              {photo.provider && photo.provider !== 'local' && (
                <div className="absolute top-1.5 left-1.5">
                  <span className="text-[8px] font-medium px-1.5 py-0.5 rounded-full bg-black/70 backdrop-blur text-white flex items-center gap-1">
                    <RefreshCw size={7} />
                    {photo.provider === 'immich' ? 'Immich' : photo.provider === 'synology' ? 'Synology' : photo.provider}
                  </span>
                </div>
              )}
              {photo.caption && (
                <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
                  <p className="text-[10px] text-white truncate">{photo.caption}</p>
                </div>
              )}
              <div className="absolute bottom-1.5 left-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
                <span className="text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-black/50 backdrop-blur text-white">
                  {new Date(entry.entry_date + 'T12:00:00').toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })}
                </span>
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Provider Photo Picker Modal */}
      {showPicker && (
        <ProviderPicker
          provider={pickerProvider!}
          userId={userId}
          entries={entriesWithContent}
          trips={trips}
          existingAssetIds={new Set(entries.flatMap(e => (e.photos || []).filter(p => p.asset_id).map(p => p.asset_id!)))}
          onClose={() => setShowPicker(false)}
          onAdd={async (assetIds, entryId, passphrase) => {
            let targetId = entryId
            if (!targetId) {
              try {
                const entry = await journeyApi.createEntry(journeyId, {
                  title: t('journey.share.gallery'),
                  entry_date: new Date().toISOString().split('T')[0],
                  type: 'entry',
                })
                targetId = entry.id
              } catch { return }
            }
            let added = 0
            try {
              const result = await journeyApi.addProviderPhotos(targetId, pickerProvider!, assetIds, undefined, passphrase)
              added = result.added || 0
            } catch {}
            if (added > 0) {
              toast.success(t('journey.photosAdded', { count: added }))
              onRefresh()
            }
            setShowPicker(false)
          }}
        />
      )}
    </div>
  )
}

// ── Expandable Story ─────────────────────────────────────────────────────

function ExpandableStory({ story }: { story: string }) {
  const { t } = useTranslation()
  const [expanded, setExpanded] = useState(false)
  const [clamped, setClamped] = useState(false)
  const ref = useRef<HTMLDivElement>(null)
  const measuredRef = useRef(false)

  useEffect(() => {
    measuredRef.current = false
  }, [story])

  useEffect(() => {
    if (measuredRef.current) return
    const el = ref.current
    if (el && !expanded) {
      setClamped(el.scrollHeight > el.clientHeight)
      measuredRef.current = true
    }
  })

  return (
    <div>
      <div
        ref={ref}
        onClick={() => { if (clamped || expanded) setExpanded(e => !e) }}
        className={`text-[13px] text-zinc-700 dark:text-zinc-300 leading-relaxed ${
          expanded ? '' : 'line-clamp-3 md:line-clamp-[9]'
        } ${clamped || expanded ? 'cursor-pointer' : ''}`}
      >
        <JournalBody text={story} />
      </div>
      {clamped && !expanded && (
        <button
          onClick={() => setExpanded(true)}
          className="mt-2 inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-zinc-100 dark:bg-zinc-800 text-[10px] font-medium text-zinc-500 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700 active:scale-95 transition-all"
        >
          {t('common.showMore')} <ChevronRight size={10} />
        </button>
      )}
      {expanded && (
        <button
          onClick={() => setExpanded(false)}
          className="mt-2 inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-zinc-100 dark:bg-zinc-800 text-[10px] font-medium text-zinc-500 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700 active:scale-95 transition-all"
        >
          {t('common.showLess')} <ChevronRight size={10} className="rotate-[-90deg]" />
        </button>
      )}
    </div>
  )
}

// ── Verdict Section (Pros & Cons) ────────────────────────────────────────

function VerdictSection({ pros, cons }: { pros: string[]; cons: string[] }) {
  const { t } = useTranslation()
  const [open, setOpen] = useState(false)
  // On desktop always show, on mobile toggle
  return (
    <div className="mt-5">
      {/* Header — clickable on mobile */}
      <button
        onClick={() => setOpen(o => !o)}
        className="md:pointer-events-none w-full flex items-center gap-2.5 mb-3.5 group"
      >
        <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
        <span className="text-[10px] font-bold tracking-[0.14em] uppercase text-zinc-400 flex items-center gap-1.5">
          {t('journey.editor.prosCons')}
          <ChevronDown
            size={12}
            className={`md:hidden text-zinc-400 transition-transform duration-300 ${open ? 'rotate-180' : ''}`}
          />
        </span>
        <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
      </button>

      {/* Collapsed summary on mobile */}
      {!open && (
        <div className="flex items-center justify-center gap-3 md:hidden">
          {pros.length > 0 && (
            <div className="flex items-center gap-1.5">
              <div className="w-5 h-5 rounded-md bg-green-500 flex items-center justify-center">
                <Check size={11} className="text-white" strokeWidth={3} />
              </div>
              <span className="text-[12px] font-semibold text-green-700 dark:text-green-400">{pros.length}</span>
            </div>
          )}
          {cons.length > 0 && (
            <div className="flex items-center gap-1.5">
              <div className="w-5 h-5 rounded-md bg-red-500 flex items-center justify-center">
                <Minus size={11} className="text-white" strokeWidth={3} />
              </div>
              <span className="text-[12px] font-semibold text-red-700 dark:text-red-400">{cons.length}</span>
            </div>
          )}
        </div>
      )}

      {/* Content — always visible on desktop, toggled on mobile */}
      <div
        className={`grid grid-cols-1 md:grid-cols-2 gap-3 overflow-hidden transition-all duration-300 ease-in-out ${
          open ? 'max-h-[800px] opacity-100' : 'max-h-0 md:max-h-none opacity-0 md:opacity-100'
        }`}
      >
        {pros.length > 0 && (
          <div className="rounded-xl border border-green-200 dark:border-green-800/30 p-4 bg-gradient-to-b from-green-50 to-white dark:from-green-950/30 dark:to-zinc-900">
            <div className="flex items-center gap-2 mb-3">
              <div className="w-6 h-6 rounded-lg bg-green-500 flex items-center justify-center">
                <Check size={14} className="text-white" strokeWidth={3} />
              </div>
              <span className="hidden md:inline text-[11px] font-bold tracking-[0.1em] uppercase text-green-700 dark:text-green-400">{t('journey.verdict.lovedIt')}</span>
              <span className="ml-auto text-[11px] font-semibold text-green-600">{pros.length}</span>
            </div>
            <div className="flex flex-col gap-2">
              {pros.map((p, i) => (
                <div key={i} className="flex items-start gap-2">
                  <span className="w-[5px] h-[5px] rounded-full bg-green-500 flex-shrink-0 mt-[7px]" />
                  <span className="text-[13px] text-green-900 dark:text-green-100 leading-snug">{p}</span>
                </div>
              ))}
            </div>
          </div>
        )}
        {cons.length > 0 && (
          <div className="rounded-xl border border-red-200 dark:border-red-800/30 p-4 bg-gradient-to-b from-red-50 to-white dark:from-red-950/30 dark:to-zinc-900">
            <div className="flex items-center gap-2 mb-3">
              <div className="w-6 h-6 rounded-lg bg-red-500 flex items-center justify-center">
                <Minus size={14} className="text-white" strokeWidth={3} />
              </div>
              <span className="hidden md:inline text-[11px] font-bold tracking-[0.1em] uppercase text-red-700 dark:text-red-400">{t('journey.verdict.couldBeBetter')}</span>
              <span className="ml-auto text-[11px] font-semibold text-red-600">{cons.length}</span>
            </div>
            <div className="flex flex-col gap-2">
              {cons.map((c, i) => (
                <div key={i} className="flex items-start gap-2">
                  <span className="w-[5px] h-[5px] rounded-full bg-red-500 flex-shrink-0 mt-[7px]" />
                  <span className="text-[13px] text-red-900 dark:text-red-100 leading-snug">{c}</span>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  )
}

// ── Entry Card ────────────────────────────────────────────────────────────

function EntryCard({ entry, onEdit, onDelete, onPhotoClick }: {
  entry: JourneyEntry
  onEdit: () => void
  onDelete: () => void
  onPhotoClick: (photos: JourneyPhoto[], index: number) => void
}) {
  const { t } = useTranslation()
  const [menuOpen, setMenuOpen] = useState(false)
  const menuBtnRef = useRef<HTMLButtonElement>(null)
  const photos = entry.photos || []
  const mood = entry.mood ? MOOD_CONFIG[entry.mood] : null
  const weather = entry.weather ? WEATHER_CONFIG[entry.weather] : null

  const prosArr = entry.pros_cons?.pros ?? []
  const consArr = entry.pros_cons?.cons ?? []
  const hasProscons = prosArr.length > 0 || consArr.length > 0

  return (
    <div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-2xl overflow-hidden transition-all hover:border-zinc-400 dark:hover:border-zinc-500 hover:shadow-sm">

      {/* Hero area: photos with title overlay */}
      {photos.length > 0 ? (
        <div className="relative">
          <PhotoGrid photos={photos} onClick={(idx) => onPhotoClick(photos, idx)} />
          {/* Gradient overlay for title */}
          <div className="absolute inset-x-0 bottom-0 pointer-events-none" style={{ background: 'linear-gradient(to top, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.2) 50%, transparent 100%)', height: '60%' }} />

          {/* Badges top-left */}
          <div className="absolute top-3 left-4 right-14 flex items-center gap-1.5 z-[2]">
            {entry.location_name && (
              <span className="inline-flex items-center gap-1 px-2.5 py-1 bg-black/40 backdrop-blur-sm rounded-full text-[10px] font-semibold text-white tracking-wide max-w-full overflow-hidden">
                <MapPin size={10} className="flex-shrink-0" />
                <span className="truncate">{entry.location_name}</span>
              </span>
            )}
            {entry.entry_time && (
              <span className="inline-flex items-center gap-1 px-2.5 py-1 bg-black/40 backdrop-blur-sm rounded-full text-[10px] font-semibold text-white tracking-wide">
                <Clock size={10} />
                {entry.entry_time}
              </span>
            )}
          </div>

          {/* Menu top-right */}
          <div className="absolute top-2.5 right-3 z-[2]">
            <button ref={menuBtnRef} onClick={() => setMenuOpen(!menuOpen)} className="w-8 h-8 rounded-[10px] bg-black/40 backdrop-blur-sm flex items-center justify-center text-white hover:bg-black/50">
              <MoreHorizontal size={14} />
            </button>
            {menuOpen && (
              <>
                <div className="fixed inset-0 z-[99]" onClick={() => setMenuOpen(false)} />
                <div className="fixed z-[100] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-lg py-1 min-w-[120px]" style={{ top: (menuBtnRef.current?.getBoundingClientRect().bottom || 0) + 4, right: window.innerWidth - (menuBtnRef.current?.getBoundingClientRect().right || 0) }}>
                  <button onClick={() => { setMenuOpen(false); onEdit() }} className="w-full text-left px-3 py-1.5 text-[12px] text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700 flex items-center gap-2"><Pencil size={12} /> {t('common.edit')}</button>
                  <button onClick={() => { setMenuOpen(false); onDelete() }} className="w-full text-left px-3 py-1.5 text-[12px] text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"><Trash2 size={12} /> {t('common.delete')}</button>
                </div>
              </>
            )}
          </div>

          {/* Title on photo */}
          {entry.title && (
            <div className="absolute bottom-4 left-5 right-5 z-[2] pointer-events-none">
              <h3 className="text-[22px] font-bold text-white tracking-[-0.02em] leading-tight drop-shadow-sm">{entry.title}</h3>
            </div>
          )}
        </div>
      ) : (
        /* No photos: simple header */
        <div className="flex items-center justify-between px-4 pt-3">
          <div className="flex items-center gap-2 min-w-0 flex-1 mr-2">
            {entry.location_name && (
              <span className="inline-flex items-center gap-1 px-2 py-0.5 bg-zinc-100 dark:bg-zinc-800 rounded-full text-[10px] font-semibold text-zinc-500 max-w-full overflow-hidden">
                <MapPin size={10} className="flex-shrink-0" /> <span className="truncate">{entry.location_name}</span>
              </span>
            )}
            {entry.entry_time && (
              <span className="inline-flex items-center gap-1 px-2 py-0.5 bg-zinc-100 dark:bg-zinc-800 rounded-full text-[10px] font-semibold text-zinc-500">
                <Clock size={10} /> {entry.entry_time}
              </span>
            )}
          </div>
          <div className="relative">
            <button ref={menuBtnRef} onClick={() => setMenuOpen(!menuOpen)} className="w-7 h-7 rounded-md flex items-center justify-center text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
              <MoreHorizontal size={14} />
            </button>
            {menuOpen && (
              <>
                <div className="fixed inset-0 z-[99]" onClick={() => setMenuOpen(false)} />
                <div className="fixed z-[100] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-lg py-1 min-w-[120px]" style={{ top: (menuBtnRef.current?.getBoundingClientRect().bottom || 0) + 4, right: window.innerWidth - (menuBtnRef.current?.getBoundingClientRect().right || 0) }}>
                  <button onClick={() => { setMenuOpen(false); onEdit() }} className="w-full text-left px-3 py-1.5 text-[12px] text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700 flex items-center gap-2"><Pencil size={12} /> {t('common.edit')}</button>
                  <button onClick={() => { setMenuOpen(false); onDelete() }} className="w-full text-left px-3 py-1.5 text-[12px] text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"><Trash2 size={12} /> {t('common.delete')}</button>
                </div>
              </>
            )}
          </div>
        </div>
      )}

      <div className="px-5 pt-4 pb-5">
        {/* Title (only if no photos — otherwise shown on image) */}
        {!photos.length && entry.title && (
          <h3 className="text-base font-semibold text-zinc-900 dark:text-white tracking-tight leading-snug mb-1">{entry.title}</h3>
        )}
        {!photos.length && entry.location_name && !entry.title && (
          <div className="mb-2" />
        )}
        {entry.story && (
          <ExpandableStory story={entry.story} />
        )}

        {/* Pros & Cons — "Pros & Cons" style */}
        {hasProscons && (
          <VerdictSection pros={prosArr} cons={consArr} />
        )}

        {(mood || weather || (entry.tags && entry.tags.length > 0)) && (
          <div className="flex items-center justify-between pt-3 mt-3 border-t border-zinc-100 dark:border-zinc-800">
            <div className="flex items-center gap-1.5">
              {mood && <MoodChip mood={entry.mood!} />}
              {weather && <WeatherChip weather={entry.weather!} />}
            </div>
            <div className="flex gap-1">
              {entry.tags?.map((tag, i) => (
                <span key={i} className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400">{tag}</span>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  )
}

function SkeletonCard({ entry, onClick }: { entry: JourneyEntry; onClick: () => void }) {
  const { t } = useTranslation()
  return (
    <div
      onClick={onClick}
      className="bg-white dark:bg-zinc-900 border border-dashed border-zinc-200 dark:border-zinc-700 rounded-xl px-4 py-3.5 flex items-center gap-3 transition-all hover:border-solid hover:border-zinc-400 dark:hover:border-zinc-500 cursor-pointer"
    >
      <div className="w-9 h-9 rounded-lg bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center text-zinc-500 flex-shrink-0">
        <MapPin size={14} />
      </div>
      <div className="flex-1 min-w-0">
        <div className="text-[13px] font-medium text-zinc-900 dark:text-white">
          {entry.title || t('journey.detail.newEntry')}
        </div>
        <div className="text-[11px] text-zinc-500 mt-0.5">
          {entry.location_name || ''}{entry.entry_time ? ` · ${entry.entry_time}` : ''}
        </div>
      </div>
      <div className="text-[11px] text-zinc-500 font-medium flex-shrink-0">
        {t('journey.detail.addEntry')} &rarr;
      </div>
    </div>
  )
}

function CheckinCard({ entry, onClick }: { entry: JourneyEntry; onClick: () => void }) {
  return (
    <div
      onClick={onClick}
      className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl px-3.5 py-2.5 flex items-center gap-2.5 transition-all hover:border-zinc-400 dark:hover:border-zinc-500 cursor-pointer"
    >
      <div className="w-7 h-7 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 flex items-center justify-center flex-shrink-0">
        <MapPin size={13} />
      </div>
      <div className="flex-1 min-w-0">
        <div className="text-[13px] font-medium text-zinc-900 dark:text-white flex items-center gap-1.5">
          {entry.title}
          {entry.location_name && <span className="text-zinc-500 font-normal text-xs">· {entry.location_name}</span>}
        </div>
        {entry.story && <div className="text-[11px] text-zinc-500 mt-0.5">{entry.story}</div>}
      </div>
      <div className="flex items-center gap-2.5 flex-shrink-0">
        {entry.entry_time && <span className="text-[11px] text-zinc-400 tabular-nums">{entry.entry_time}</span>}
      </div>
    </div>
  )
}

function PhotoImg({ photo, className, style, onClick }: { photo: JourneyPhoto; className?: string; style?: React.CSSProperties; onClick?: () => void }) {
  const src = photoUrl(photo, 'thumbnail')
  return (
    <img
      src={src}
      alt=""
      className={className}
      style={style}
      onClick={onClick}
      loading="lazy"
    />
  )
}

function PhotoGrid({ photos, onClick }: { photos: JourneyPhoto[]; onClick: (idx: number) => void }) {
  const count = photos.length
  if (count === 0) return null

  if (count === 1) {
    return (
      <div className="overflow-hidden cursor-pointer" onClick={() => onClick(0)}>
        <PhotoImg photo={photos[0]} className="w-full h-72 object-cover" />
      </div>
    )
  }

  if (count === 2) {
    return (
      <div className="grid grid-cols-2 gap-0.5 overflow-hidden">
        {photos.slice(0, 2).map((p, i) => (
          <PhotoImg key={p.id} photo={p} className="w-full h-52 object-cover cursor-pointer" onClick={() => onClick(i)} />
        ))}
      </div>
    )
  }

  return (
    <div className="overflow-hidden flex" style={{ height: 300, gap: 2 }}>
      <div className="flex-1 min-w-0 cursor-pointer" onClick={() => onClick(0)}>
        <PhotoImg photo={photos[0]} className="w-full h-full object-cover" />
      </div>
      <div className="flex-1 min-w-0 flex flex-col" style={{ gap: 2 }}>
        <div className="flex-1 min-h-0 cursor-pointer" onClick={() => onClick(1)}>
          <PhotoImg photo={photos[1]} className="w-full h-full object-cover" />
        </div>
        <div className="flex-1 min-h-0 relative cursor-pointer" onClick={() => onClick(2)}>
          <PhotoImg photo={photos[2]} className="w-full h-full object-cover" />
          {count > 3 && (
            <div className="absolute bottom-2 right-2 bg-black/60 backdrop-blur text-white rounded-full px-2 py-0.5 text-[10px] font-semibold flex items-center gap-1">
              <Image size={10} />
              +{count - 3}
            </div>
          )}
        </div>
      </div>
    </div>
  )
}

function MoodChip({ mood }: { mood: string }) {
  const { t } = useTranslation()
  const config = MOOD_CONFIG[mood]
  if (!config) return null
  const Icon = config.icon
  return (
    <div className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium" style={{ background: config.bg, color: config.text }}>
      <Icon size={11} />
      {t(config.label)}
    </div>
  )
}

function WeatherChip({ weather }: { weather: string }) {
  const { t } = useTranslation()
  const config = WEATHER_CONFIG[weather]
  if (!config) return null
  const Icon = config.icon
  return (
    <div className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400">
      <Icon size={11} />
      {t(config.label)}
    </div>
  )
}

// ── Scroll Trigger ───────────────────────────────────────────────────────

function ScrollTrigger({ onVisible, loading }: { onVisible: () => void; loading: boolean }) {
  const ref = useRef<HTMLDivElement>(null)
  useEffect(() => {
    const el = ref.current
    if (!el) return
    const obs = new IntersectionObserver(([entry]) => { if (entry.isIntersecting && !loading) onVisible() }, { rootMargin: '200px' })
    obs.observe(el)
    return () => obs.disconnect()
  }, [onVisible, loading])
  return (
    <div ref={ref} className="flex justify-center py-4 mt-2">
      <div className="w-5 h-5 border-2 border-zinc-300 border-t-zinc-900 dark:border-zinc-600 dark:border-t-white rounded-full animate-spin" />
    </div>
  )
}

// ── Photo date grouping ───────────────────────────────────────────────────

function groupPhotosByDate(photos: any[]): { date: string; label: string; assets: any[] }[] {
  const map = new Map<string, any[]>()
  for (const asset of photos) {
    const key = asset.takenAt ? asset.takenAt.slice(0, 10) : '__unknown__'
    if (!map.has(key)) map.set(key, [])
    map.get(key)!.push(asset)
  }
  return [...map.entries()].map(([date, assets]) => ({
    date,
    label: date === '__unknown__'
      ? 'Unknown date'
      : new Date(date + 'T00:00:00').toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }),
    assets,
  }))
}

// ── Provider Picker ───────────────────────────────────────────────────────

function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, onClose, onAdd }: {
  provider: string
  userId: number
  entries: JourneyEntry[]
  trips: JourneyTrip[]
  existingAssetIds: Set<string>
  onClose: () => void
  onAdd: (assetIds: string[], entryId: number | null, passphrase?: string) => Promise<void>
}) {
  const { t } = useTranslation()
  const [filter, setFilter] = useState<'trip' | 'custom' | 'all' | 'album'>('trip')
  const [photos, setPhotos] = useState<any[]>([])
  const [albums, setAlbums] = useState<Array<{ id: string; albumName: string; assetCount: number; passphrase?: string }>>([])
  const [selectedAlbum, setSelectedAlbum] = useState<string | null>(null)
  const [selectedAlbumPassphrase, setSelectedAlbumPassphrase] = useState<string | undefined>(undefined)
  const [loading, setLoading] = useState(false)
  const [loadingMore, setLoadingMore] = useState(false)
  const [hasMore, setHasMore] = useState(false)
  const [searchPage, setSearchPage] = useState(1)
  const [searchFrom, setSearchFrom] = useState('')
  const [searchTo, setSearchTo] = useState('')
  const [selected, setSelected] = useState<Set<string>>(new Set())
  const [customFrom, setCustomFrom] = useState('')
  const [customTo, setCustomTo] = useState('')
  const [targetEntryId, setTargetEntryId] = useState<number | null>(null)
  const [addToOpen, setAddToOpen] = useState(false)
  const abortRef = useRef<AbortController | null>(null)
  const gridRef = useRef<HTMLDivElement>(null)

  // compute trip range
  const tripRange = useMemo(() => {
    let from = '', to = ''
    for (const t of trips) {
      if (t.start_date && (!from || t.start_date < from)) from = t.start_date
      if (t.end_date && (!to || t.end_date > to)) to = t.end_date
    }
    return { from, to }
  }, [trips])

  const cancelPending = () => {
    if (abortRef.current) { abortRef.current.abort() }
    abortRef.current = new AbortController()
    return abortRef.current.signal
  }

  const searchPhotos = async (from: string, to: string, page: number = 1, append: boolean = false) => {
    const signal = cancelPending()
    if (page === 1) { setLoading(true); setPhotos([]) } else { setLoadingMore(true) }
    setSearchFrom(from)
    setSearchTo(to)
    setSearchPage(page)
    try {
      const res = await fetch(`/api/integrations/memories/${provider}/search`, {
        method: 'POST', credentials: 'include', signal,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ from, to, page, size: 50 }),
      })
      if (res.ok) {
        const data = await res.json()
        const assets = data.assets || []
        setPhotos(prev => append ? [...prev, ...assets] : assets)
        setHasMore(!!data.hasMore)
      } else {
        setHasMore(false)
      }
    } catch (e: any) {
      if (e.name !== 'AbortError') setHasMore(false)
    }
    if (!signal.aborted) { setLoading(false); setLoadingMore(false) }
  }

  const loadMorePhotos = () => {
    if (loadingMore || !hasMore) return
    searchPhotos(searchFrom, searchTo, searchPage + 1, true)
  }

  const loadAlbumPhotos = async (album: { id: string; passphrase?: string }) => {
    const signal = cancelPending()
    setLoading(true)
    setPhotos([])
    setHasMore(false)
    try {
      const qs = album.passphrase ? `?passphrase=${encodeURIComponent(album.passphrase)}` : ''
      const res = await fetch(`/api/integrations/memories/${provider}/albums/${album.id}/photos${qs}`, { credentials: 'include', signal })
      if (res.ok) setPhotos((await res.json()).assets || [])
    } catch (e: any) { if (e.name !== 'AbortError') {} }
    if (!signal.aborted) setLoading(false)
  }

  const loadAlbums = async () => {
    try {
      const res = await fetch(`/api/integrations/memories/${provider}/albums`, { credentials: 'include' })
      if (res.ok) setAlbums((await res.json()).albums || [])
    } catch {}
  }

  // load on mount / filter change
  useEffect(() => {
    if (filter === 'trip' && tripRange.from && tripRange.to) {
      searchPhotos(tripRange.from, tripRange.to)
    } else if (filter === 'all') {
      searchPhotos('', '')
    } else if (filter === 'album' && albums.length === 0) {
      loadAlbums()
    }
  }, [filter])

  const handleCustomSearch = () => {
    if (customFrom && customTo) searchPhotos(customFrom, customTo)
  }

  const toggleAsset = (id: string) => {
    setSelected(prev => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id); else next.add(id)
      return next
    })
  }

  const targetLabel = targetEntryId
    ? entries.find(e => e.id === targetEntryId)?.title || entries.find(e => e.id === targetEntryId)?.entry_date || t('journey.stats.entries')
    : t('journey.picker.newGallery')

  return (
    <div className="fixed inset-0 z-[200] flex items-center justify-center p-5" style={{ background: 'rgba(9,9,11,0.75)' }}>
      <div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[720px] md:max-w-[960px] w-full max-h-[85vh] flex flex-col overflow-hidden">

        {/* Header */}
        <div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
          <h2 className="text-[16px] font-bold text-zinc-900 dark:text-white">
            {provider === 'immich' ? 'Immich' : 'Synology Photos'}
          </h2>
          <button onClick={onClose} className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
            <X size={16} />
          </button>
        </div>

        {/* Filter bar */}
        <div className="px-6 py-3 border-b border-zinc-200 dark:border-zinc-700">
          {/* Tabs */}
          <div className="flex gap-1.5 mb-3">
            {[
              { id: 'trip' as const, label: t('journey.picker.tripPeriod') },
              { id: 'custom' as const, label: t('journey.picker.dateRange') },
              { id: 'all' as const, label: t('journey.picker.allPhotos') },
              { id: 'album' as const, label: t('journey.picker.albums') },
            ].map(f => (
              <button
                key={f.id}
                onClick={() => setFilter(f.id)}
                className={`px-3 py-1.5 rounded-lg text-[12px] font-medium transition-colors ${
                  filter === f.id
                    ? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900'
                    : 'text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800'
                }`}
              >
                {f.label}
              </button>
            ))}
          </div>

          {/* Filter content — always visible row */}
          <div className="min-h-[36px] flex items-center">
            {filter === 'trip' && (
              <div className="flex items-center gap-2 text-[12px] text-zinc-500">
                {tripRange.from && tripRange.to ? (
                  <>
                    <Calendar size={13} className="text-zinc-400" />
                    <span className="font-medium text-zinc-900 dark:text-white">
                      {new Date(tripRange.from + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
                    </span>
                    <span className="text-zinc-400">&mdash;</span>
                    <span className="font-medium text-zinc-900 dark:text-white">
                      {new Date(tripRange.to + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
                    </span>
                    <span className="ml-1 text-zinc-400">
                      ({Math.ceil((new Date(tripRange.to).getTime() - new Date(tripRange.from).getTime()) / 86400000) + 1} days)
                    </span>
                  </>
                ) : (
                  <span className="text-zinc-400">{t('journey.trips.noTripsLinkedSettings')}</span>
                )}
              </div>
            )}

            {filter === 'custom' && (
              <div className="flex items-center gap-2 flex-1">
                <div className="flex-1"><DatePicker value={customFrom} onChange={setCustomFrom} /></div>
                <span className="text-zinc-400 text-[12px]">&mdash;</span>
                <div className="flex-1"><DatePicker value={customTo} onChange={setCustomTo} /></div>
                <button onClick={handleCustomSearch}
                  className="px-3 py-1.5 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[12px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 flex-shrink-0">
                  {t('journey.picker.search')}
                </button>
              </div>
            )}

            {filter === 'album' && (
              <div className="flex gap-2 overflow-x-auto flex-1">
                {albums.map((a: any) => (
                  <button
                    key={a.id}
                    onClick={() => { setSelectedAlbum(a.id); setSelectedAlbumPassphrase(a.passphrase); loadAlbumPhotos(a) }}
                    className={`px-2.5 py-1 rounded-lg text-[11px] font-medium whitespace-nowrap flex-shrink-0 border ${
                      selectedAlbum === a.id
                        ? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white'
                        : 'border-zinc-200 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800'
                    }`}
                  >
                    {a.albumName || a.name || 'Album'}{a.assetCount != null ? ` (${a.assetCount})` : ''}
                  </button>
                ))}
                {albums.length === 0 && !loading && <span className="text-[12px] text-zinc-400">{t('journey.picker.noAlbums')}</span>}
              </div>
            )}
          </div>
        </div>

        {/* Add-to entry selector */}
        <div className="px-6 py-2.5 border-b border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
          <div className="relative flex items-center gap-2">
            <span className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500">{t('journey.picker.addTo')}</span>
            <button
              onClick={() => setAddToOpen(!addToOpen)}
              className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-zinc-200 dark:border-zinc-700 text-[12px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800"
            >
              <span className={targetEntryId ? '' : 'font-semibold'}>{targetLabel}</span>
              <ChevronRight size={12} className="rotate-90 text-zinc-400" />
            </button>
            {addToOpen && (
              <>
                <div className="fixed inset-0 z-[9]" onClick={() => setAddToOpen(false)} />
                <div className="absolute left-12 top-full mt-1 z-10 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-lg py-1.5 min-w-[200px] max-h-[240px] overflow-y-auto">
                  <button
                    onClick={() => { setTargetEntryId(null); setAddToOpen(false) }}
                    className={`w-full text-left px-3 py-2 text-[12px] flex items-center gap-2 ${
                      !targetEntryId
                        ? 'bg-zinc-100 dark:bg-zinc-700 font-semibold text-zinc-900 dark:text-white'
                        : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700'
                    }`}
                  >
                    <Camera size={12} />
                    {t('journey.picker.newGallery')}
                  </button>
                  {entries.filter(e => e.type !== 'skeleton' && e.title !== 'Gallery' && e.title !== '[Trip Photos]').length > 0 && (
                    <div className="h-px bg-zinc-200 dark:bg-zinc-700 my-1" />
                  )}
                  {entries.filter(e => e.type !== 'skeleton' && e.title !== 'Gallery' && e.title !== '[Trip Photos]').map(e => (
                    <button
                      key={e.id}
                      onClick={() => { setTargetEntryId(e.id); setAddToOpen(false) }}
                      className={`w-full text-left px-3 py-2 text-[12px] truncate ${
                        targetEntryId === e.id
                          ? 'bg-zinc-100 dark:bg-zinc-700 font-semibold text-zinc-900 dark:text-white'
                          : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700'
                      }`}
                    >
                      {e.title || e.location_name || new Date(e.entry_date + 'T12:00:00').toLocaleDateString(undefined, { day: 'numeric', month: 'short' })}
                    </button>
                  ))}
                </div>
              </>
            )}
          </div>
        </div>

        {/* Select all bar — sticky above grid */}
        {!loading && photos.length > 0 && (() => {
          const selectable = photos.filter((a: any) => !existingAssetIds.has(a.id))
          const allSelected = selectable.length > 0 && selectable.every((a: any) => selected.has(a.id))
          if (selectable.length === 0) return null
          return (
            <div className="px-4 py-2 border-b border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900">
              <button
                onClick={() => {
                  if (allSelected) {
                    setSelected(new Set())
                  } else {
                    setSelected(new Set(selectable.map((a: any) => a.id)))
                  }
                }}
                className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[11px] font-medium border border-zinc-200 dark:border-zinc-700 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800"
              >
                <div className={`w-3.5 h-3.5 rounded border flex items-center justify-center ${
                  allSelected
                    ? 'bg-zinc-900 dark:bg-white border-zinc-900 dark:border-white'
                    : 'border-zinc-300 dark:border-zinc-600'
                }`}>
                  {allSelected && <Check size={9} className="text-white dark:text-zinc-900" strokeWidth={3} />}
                </div>
                {allSelected ? t('journey.picker.deselectAll') : t('journey.picker.selectAll')} ({selectable.length})
              </button>
            </div>
          )
        })()}

        {/* Photo grid */}
        <div className="flex-1 overflow-y-auto p-4">
          {loading ? (
            <div className="flex justify-center py-12">
              <div className="w-6 h-6 border-2 border-zinc-300 border-t-zinc-900 rounded-full animate-spin" />
            </div>
          ) : photos.length === 0 ? (
            <div className="text-center py-12">
              <p className="text-[13px] text-zinc-500">
                {filter === 'trip' && !tripRange.from ? t('journey.trips.noTripsLinkedSettings') : t('journey.detail.noPhotos')}
              </p>
            </div>
          ) : (
            <div>
              {groupPhotosByDate(photos).map(group => (
                <div key={group.date}>
                  <p className="text-[11px] font-medium text-zinc-500 dark:text-zinc-400 mb-2 mt-4 first:mt-0">
                    {group.label}
                  </p>
                  <div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-1.5 mb-1">
                    {group.assets.map((asset: any) => {
                      const isSelected = selected.has(asset.id)
                      const alreadyAdded = existingAssetIds.has(asset.id)
                      return (
                        <div
                          key={asset.id}
                          onClick={() => !alreadyAdded && toggleAsset(asset.id)}
                          className={`relative aspect-square rounded-lg overflow-hidden ${
                            alreadyAdded
                              ? 'opacity-40 cursor-not-allowed'
                              : isSelected
                                ? 'ring-2 ring-zinc-900 dark:ring-white ring-offset-2 dark:ring-offset-zinc-900 cursor-pointer'
                                : 'cursor-pointer'
                          }`}
                        >
                          <img
                            src={`/api/integrations/memories/${provider}/assets/0/${asset.id}/${userId}/thumbnail${selectedAlbumPassphrase ? `?passphrase=${encodeURIComponent(selectedAlbumPassphrase)}` : ''}`}
                            alt=""
                            className="w-full h-full object-cover"
                            loading="lazy"
                            onError={e => {
                              const img = e.currentTarget
                              const original = `/api/integrations/memories/${provider}/assets/0/${asset.id}/${userId}/original${selectedAlbumPassphrase ? `?passphrase=${encodeURIComponent(selectedAlbumPassphrase)}` : ''}`
                              if (!img.src.includes('/original')) img.src = original
                            }}
                          />
                          {alreadyAdded && (
                            <div className="absolute top-1.5 right-1.5 w-5 h-5 rounded-full bg-zinc-500 text-white flex items-center justify-center">
                              <Check size={12} />
                            </div>
                          )}
                          {isSelected && !alreadyAdded && (
                            <div className="absolute top-1.5 right-1.5 w-5 h-5 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 flex items-center justify-center">
                              <Check size={12} />
                            </div>
                          )}
                          {asset.city && (
                            <div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/50 to-transparent">
                              <p className="text-[8px] text-white truncate">{asset.city}</p>
                            </div>
                          )}
                        </div>
                      )
                    })}
                  </div>
                </div>
              ))}
              {/* Infinite scroll trigger */}
              {hasMore && !selectedAlbum && <ScrollTrigger onVisible={loadMorePhotos} loading={loadingMore} />}
            </div>
          )}
        </div>

        {/* Footer */}
        <div className="flex items-center justify-between px-6 py-4 border-t border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
          <span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-zinc-200/60 dark:bg-zinc-700/60 text-[11px] leading-none text-zinc-500 dark:text-zinc-400">
            <span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[10px] leading-none font-bold">{selected.size}</span>
            <span className="leading-[18px]">{t('journey.picker.selected')}</span>
          </span>
          <div className="flex items-center gap-2">
            <button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">
              {t('common.cancel')}
            </button>
            <button
              onClick={() => onAdd([...selected], targetEntryId, selectedAlbumPassphrase)}
              disabled={selected.size === 0}
              className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40 disabled:cursor-not-allowed"
            >
              {t('common.add')} {selected.size > 0 ? `(${selected.size})` : ''}
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

// ── Date Picker ───────────────────────────────────────────────────────────

function DatePicker({ value, onChange, tripDates }: {
  value: string
  onChange: (date: string) => void
  tripDates?: Set<string>
}) {
  const { t } = useTranslation()
  const [open, setOpen] = useState(false)
  const [viewMonth, setViewMonth] = useState(() => {
    const d = value ? new Date(value + 'T00:00:00') : new Date()
    return { year: d.getFullYear(), month: d.getMonth() }
  })

  const daysInMonth = new Date(viewMonth.year, viewMonth.month + 1, 0).getDate()
  const firstDow = new Date(viewMonth.year, viewMonth.month, 1).getDay()
  const monthName = new Date(viewMonth.year, viewMonth.month).toLocaleDateString(undefined, { month: 'long', year: 'numeric' })

  const prevMonth = () => {
    setViewMonth(p => p.month === 0 ? { year: p.year - 1, month: 11 } : { ...p, month: p.month - 1 })
  }
  const nextMonth = () => {
    setViewMonth(p => p.month === 11 ? { year: p.year + 1, month: 0 } : { ...p, month: p.month + 1 })
  }

  const pad = (n: number) => String(n).padStart(2, '0')

  const cells: (number | null)[] = []
  for (let i = 0; i < firstDow; i++) cells.push(null)
  for (let d = 1; d <= daysInMonth; d++) cells.push(d)

  const formatted = value ? new Date(value + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : t('journey.picker.selectDate')

  return (
    <div className="relative">
      <button
        type="button"
        onClick={() => setOpen(!open)}
        className="w-full px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white text-left flex items-center justify-between"
      >
        <span>{formatted}</span>
        <Calendar size={13} className="text-zinc-400" />
      </button>

      {open && (
        <>
          <div className="fixed inset-0 z-[10]" onClick={() => setOpen(false)} />
          <div className="absolute top-full left-0 mt-1 z-[20] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-lg p-3 w-[280px]">
            {/* Month nav */}
            <div className="flex items-center justify-between mb-2">
              <button type="button" onClick={prevMonth} className="w-7 h-7 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center justify-center text-zinc-500">
                <ArrowLeft size={14} />
              </button>
              <span className="text-[13px] font-semibold text-zinc-900 dark:text-white">{monthName}</span>
              <button type="button" onClick={nextMonth} className="w-7 h-7 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center justify-center text-zinc-500">
                <ChevronRight size={14} />
              </button>
            </div>

            {/* Weekday headers */}
            <div className="grid grid-cols-7 mb-1">
              {['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((d, i) => (
                <div key={i} className="text-center text-[10px] font-medium text-zinc-400 py-1">{d}</div>
              ))}
            </div>

            {/* Day grid */}
            <div className="grid grid-cols-7">
              {cells.map((day, i) => {
                if (day === null) return <div key={`e${i}`} />
                const dateStr = `${viewMonth.year}-${pad(viewMonth.month + 1)}-${pad(day)}`
                const isSelected = dateStr === value
                const isTrip = tripDates?.has(dateStr)
                const isToday = dateStr === new Date().toISOString().split('T')[0]

                return (
                  <button
                    key={dateStr}
                    type="button"
                    onClick={() => { onChange(dateStr); setOpen(false) }}
                    className={`w-9 h-9 rounded-lg text-[12px] font-medium flex items-center justify-center relative transition-colors ${
                      isSelected
                        ? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900'
                        : isToday
                          ? 'text-zinc-900 dark:text-white font-bold'
                          : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700'
                    }`}
                  >
                    {day}
                    {isTrip && !isSelected && (
                      <span className="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-indigo-500" />
                    )}
                  </button>
                )
              })}
            </div>
          </div>
        </>
      )}
    </div>
  )
}

function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSave, onUploadPhotos, onDone }: {
  entry: JourneyEntry
  journeyId: number
  tripDates: Set<string>
  galleryPhotos: JourneyPhoto[]
  onClose: () => void
  onSave: (data: Record<string, unknown>) => Promise<number>
  onUploadPhotos: (entryId: number, formData: FormData) => Promise<JourneyPhoto[]>
  onDone: () => void
}) {
  const { t } = useTranslation()
  const [title, setTitle] = useState(entry.title || '')
  const [story, setStory] = useState(entry.story || '')
  const [entryDate, setEntryDate] = useState(entry.entry_date || new Date().toISOString().split('T')[0])
  const [entryTime, setEntryTime] = useState(entry.entry_time || '')
  const [locationName, setLocationName] = useState(entry.location_name || '')
  const [locationLat, setLocationLat] = useState<number | null>(entry.location_lat ?? null)
  const [locationLng, setLocationLng] = useState<number | null>(entry.location_lng ?? null)
  const [locationQuery, setLocationQuery] = useState('')
  const [locationResults, setLocationResults] = useState<{ name: string; address?: string; lat: number; lng: number }[]>([])
  const [locationSearching, setLocationSearching] = useState(false)
  const [showLocationResults, setShowLocationResults] = useState(false)
  const locationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const [mood, setMood] = useState(entry.mood || '')
  const [weather, setWeather] = useState(entry.weather || '')
  const [pros, setPros] = useState<string[]>(entry.pros_cons?.pros?.length ? entry.pros_cons.pros : [''])
  const [cons, setCons] = useState<string[]>(entry.pros_cons?.cons?.length ? entry.pros_cons.cons : [''])
  const [saving, setSaving] = useState(false)
  const [uploading, setUploading] = useState(false)
  const [photos, setPhotos] = useState<JourneyPhoto[]>(entry.photos || [])
  const [pendingFiles, setPendingFiles] = useState<File[]>([])
  const [pendingLinkIds, setPendingLinkIds] = useState<number[]>([])
  const [showGalleryPick, setShowGalleryPick] = useState(false)
  const fileRef = useRef<HTMLInputElement>(null)
  const storyRef = useRef<HTMLTextAreaElement>(null)

  const handleSave = async () => {
    setSaving(true)
    try {
      const entryId = await onSave({
        title: title || null,
        story: story || null,
        entry_date: entryDate,
        entry_time: entryTime || null,
        location_name: locationName || null,
        location_lat: locationLat,
        location_lng: locationLng,
        mood: mood || null,
        weather: weather || null,
        pros_cons: { pros: pros.filter(p => p.trim()), cons: cons.filter(c => c.trim()) },
        type: (entry.type === 'skeleton' && story.trim()) ? 'entry' : undefined,
      })
      // upload queued files after entry is created
      if (pendingFiles.length > 0 && entryId) {
        const formData = new FormData()
        for (const f of pendingFiles) formData.append('photos', f)
        await onUploadPhotos(entryId, formData)
      }
      // link gallery photos that were picked before save
      if (pendingLinkIds.length > 0 && entryId) {
        for (const photoId of pendingLinkIds) {
          try { await journeyApi.linkPhoto(entryId, photoId) } catch {}
        }
      }
      onDone()
    } finally {
      setSaving(false)
    }
  }

  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files
    if (!files?.length) return
    if (entry.id === 0) {
      // queue files for upload after save
      setPendingFiles(prev => [...prev, ...Array.from(files)])
    } else {
      setUploading(true)
      try {
        const formData = new FormData()
        for (const f of files) formData.append('photos', f)
        const newPhotos = await onUploadPhotos(entry.id, formData)
        if (newPhotos?.length) setPhotos(prev => [...prev, ...newPhotos])
      } finally {
        setUploading(false)
      }
    }
  }

  return (
    <div className="fixed inset-0 z-[9999] flex items-end sm:items-center sm:justify-center sm:p-5" style={{ background: 'rgba(9,9,11,0.75)' }}>
      <div className="bg-white dark:bg-zinc-900 sm:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] sm:max-w-[640px] w-full flex flex-col overflow-hidden h-full sm:h-auto sm:max-h-[90vh]" style={{ paddingBottom: 'var(--bottom-nav-h)' }}>


        <div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
          <h2 className="text-[16px] font-bold text-zinc-900 dark:text-white">{entry.id === 0 ? t('journey.detail.newEntry') : t('journey.detail.editEntry')}</h2>
          <button onClick={onClose} className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
            <X size={16} />
          </button>
        </div>

        <div className="flex-1 min-h-0 overflow-y-auto px-6 py-5 flex flex-col gap-4">
          <input
            value={title}
            onChange={e => setTitle(e.target.value)}
            placeholder={t('journey.editor.titlePlaceholder')}
            className="w-full text-[20px] font-medium bg-transparent border-0 border-b border-transparent focus:border-zinc-300 dark:focus:border-zinc-600 outline-none text-zinc-900 dark:text-white placeholder:text-zinc-400 pb-2"
          />

          <div>
            <input ref={fileRef} type="file" accept="image/*" multiple onChange={handleFileChange} onClick={e => { (e.target as HTMLInputElement).value = '' }} className="hidden" />
            <div className="flex gap-2">
              <button
                onClick={() => fileRef.current?.click()}
                disabled={uploading}
                className="flex-1 border border-dashed border-zinc-200 dark:border-zinc-700 rounded-lg py-4 text-[12px] text-zinc-500 hover:border-zinc-400 dark:hover:border-zinc-500 hover:bg-zinc-50 dark:hover:bg-zinc-800 flex items-center justify-center gap-1.5 disabled:opacity-50"
              >
                {uploading ? (
                  <><div className="w-3.5 h-3.5 border-2 border-zinc-300 border-t-zinc-600 rounded-full animate-spin" /> {t('journey.editor.uploading')}</>
                ) : (
                  <><Plus size={13} /> {t('journey.editor.uploadPhotos')}</>
                )}
              </button>
              {galleryPhotos.length > 0 && (
                <button
                  onClick={() => setShowGalleryPick(!showGalleryPick)}
                  className={`flex-1 border rounded-lg py-4 text-[12px] text-zinc-500 flex items-center justify-center gap-1.5 ${
                    showGalleryPick
                      ? 'border-zinc-900 dark:border-white bg-zinc-50 dark:bg-zinc-800'
                      : 'border-dashed border-zinc-200 dark:border-zinc-700 hover:border-zinc-400 dark:hover:border-zinc-500 hover:bg-zinc-50 dark:hover:bg-zinc-800'
                  }`}
                >
                  <Image size={13} /> {t('journey.editor.fromGallery')}
                </button>
              )}
            </div>

            {/* Gallery picker — directly below buttons */}
            {showGalleryPick && (
              <div className="mt-2 border border-zinc-200 dark:border-zinc-700 rounded-xl p-3 bg-zinc-50 dark:bg-zinc-800/50">
                <div className="grid grid-cols-5 sm:grid-cols-6 gap-1.5 max-h-[160px] overflow-y-auto">
                  {galleryPhotos.filter(gp => !photos.some(p => p.id === gp.id)).map(gp => (
                    <div
                      key={gp.id}
                      onClick={async () => {
                        if (entry.id > 0) {
                          try {
                            const linked = await journeyApi.linkPhoto(entry.id, gp.id)
                            if (linked) setPhotos(prev => [...prev, linked])
                          } catch {}
                        } else {
                          setPendingLinkIds(prev => [...prev, gp.id])
                          setPhotos(prev => [...prev, gp])
                        }
                      }}
                      className="aspect-square rounded-lg overflow-hidden cursor-pointer hover:ring-2 hover:ring-zinc-900 dark:hover:ring-white hover:ring-offset-1 dark:hover:ring-offset-zinc-900 transition-all"
                    >
                      <img src={photoUrl(gp)} alt="" className="w-full h-full object-cover" loading="lazy" onError={e => { const img = e.currentTarget; const orig = photoUrl(gp, 'original'); if (!img.src.includes('/original')) img.src = orig }} />
                    </div>
                  ))}
                  {galleryPhotos.filter(gp => !photos.some(p => p.id === gp.id)).length === 0 && (
                    <div className="col-span-full text-center py-3 text-[11px] text-zinc-400">{t('journey.editor.allPhotosAdded')}</div>
                  )}
                </div>
              </div>
            )}
            {(photos.length > 0 || pendingFiles.length > 0) && (
              <div className="mt-3">
                <div className="flex flex-wrap gap-2">
                  {photos.map((p, idx) => (
                    <div key={p.id} className={`w-20 h-20 rounded-lg overflow-hidden relative group ${idx === 0 && photos.length > 1 ? 'ring-2 ring-zinc-900 dark:ring-white ring-offset-1 dark:ring-offset-zinc-900' : ''}`}>
                      <img src={photoUrl(p)} className="w-full h-full object-cover" alt="" onError={e => { const img = e.currentTarget; const orig = photoUrl(p, 'original'); if (!img.src.includes('/original')) img.src = orig }} />
                      {idx === 0 && photos.length > 1 && (
                        <span className="absolute bottom-0.5 left-0.5 px-1 py-px rounded text-[8px] font-bold bg-zinc-900/70 text-white">{t('journey.editor.photoFirst')}</span>
                      )}
                      {idx > 0 && photos.length > 1 && (
                        <button
                          onClick={e => {
                            e.stopPropagation()
                            setPhotos(prev => {
                              const next = [...prev]
                              const [moved] = next.splice(idx, 1)
                              next.unshift(moved)
                              next.forEach((ph, i) => { journeyApi.updatePhoto(ph.id, { sort_order: i }).catch(() => {}) })
                              return next
                            })
                          }}
                          className="absolute bottom-0.5 left-0.5 px-1.5 py-0.5 rounded bg-black/60 text-white text-[8px] font-semibold opacity-0 group-hover:opacity-100 transition-opacity"
                        >
                          {t('journey.editor.makeFirst')}
                        </button>
                      )}
                      <button
                        onClick={async (e) => {
                          e.stopPropagation()
                          await journeyApi.deletePhoto(p.id)
                          setPhotos(prev => prev.filter(x => x.id !== p.id))
                        }}
                        className="absolute top-1 right-1 w-5 h-5 rounded-full bg-black/60 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
                      >
                        <X size={10} />
                      </button>
                    </div>
                  ))}
                  {pendingFiles.map((f, i) => (
                    <div key={`pending-${i}`} className="w-20 h-20 rounded-lg overflow-hidden relative group">
                      <img src={URL.createObjectURL(f)} className="w-full h-full object-cover" alt="" />
                      <button
                        onClick={() => setPendingFiles(prev => prev.filter((_, j) => j !== i))}
                        className="absolute top-1 right-1 w-5 h-5 rounded-full bg-black/60 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
                      >
                        <X size={10} />
                      </button>
                    </div>
                  ))}
                </div>
              </div>
            )}
          </div>

          <div className="shrink-0 border border-zinc-200 dark:border-zinc-700 rounded-lg overflow-hidden focus-within:border-zinc-400 dark:focus-within:border-zinc-500">
            <MarkdownToolbar textareaRef={storyRef} onUpdate={setStory} />
            <textarea
              ref={storyRef}
              value={story}
              onChange={e => setStory(e.target.value)}
              placeholder={t('journey.editor.writeStory')}
              rows={6}
              style={{ minHeight: '144px' }}
              className="w-full px-3 py-2.5 text-[14px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white outline-none resize-none border-0 shrink-0"
            />
          </div>

          {/* Pros & Cons */}
          <div className="bg-zinc-50 dark:bg-zinc-800/50 rounded-2xl p-5">
            <div className="mb-4">
              <span className="text-[11px] font-semibold tracking-[0.12em] uppercase text-zinc-500">{t('journey.editor.prosCons')}</span>
            </div>
            <div className="grid grid-cols-2 gap-4">
              {/* Pros */}
              <div>
                <div className="flex items-center gap-[7px] mb-2.5">
                  <div className="w-4 h-4 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
                    <Check size={9} className="text-green-700 dark:text-green-400" strokeWidth={3.5} />
                  </div>
                  <span className="text-[12px] font-semibold text-green-700 dark:text-green-400">{t('journey.editor.pros')}</span>
                </div>
                <div className="flex flex-col gap-1.5">
                  {pros.map((p, i) => (
                    <div key={i} className="flex items-center gap-2 h-9 px-3 border rounded-[10px] border-zinc-200 dark:border-zinc-700">
                      <span className="w-[5px] h-[5px] rounded-full bg-green-500 flex-shrink-0" />
                      <input
                        value={p}
                        onChange={e => { const next = [...pros]; next[i] = e.target.value; setPros(next) }}
                        placeholder={t('journey.editor.proPlaceholder')}
                        className="flex-1 min-w-0 bg-transparent border-none outline-none text-[13px] text-zinc-900 dark:text-zinc-100 placeholder:text-green-400 dark:placeholder:text-green-600"
                      />
                      {pros.length > 1 && (
                        <button onClick={() => setPros(pros.filter((_, j) => j !== i))} className="p-1 text-green-300 dark:text-green-700 hover:text-green-600 dark:hover:text-green-400 flex-shrink-0">
                          <X size={13} strokeWidth={2.5} />
                        </button>
                      )}
                    </div>
                  ))}
                  <button
                    onClick={() => setPros([...pros, ''])}
                    className="flex items-center justify-center gap-1.5 h-9 w-full border border-dashed border-green-200 dark:border-green-800/40 rounded-[10px] text-[12px] font-medium text-green-700 dark:text-green-400 hover:border-green-300 dark:hover:border-green-700 transition-colors"
                  >
                    <Plus size={13} strokeWidth={2.5} /> {t('journey.editor.addAnother')}
                  </button>
                </div>
              </div>

              {/* Cons */}
              <div>
                <div className="flex items-center gap-[7px] mb-2.5">
                  <div className="w-4 h-4 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
                    <Minus size={9} className="text-red-700 dark:text-red-400" strokeWidth={3.5} />
                  </div>
                  <span className="text-[12px] font-semibold text-red-700 dark:text-red-400">{t('journey.editor.cons')}</span>
                </div>
                <div className="flex flex-col gap-1.5">
                  {cons.map((c, i) => (
                    <div key={i} className="flex items-center gap-2 h-9 px-3 border rounded-[10px] border-zinc-200 dark:border-zinc-700">
                      <span className="w-[5px] h-[5px] rounded-full bg-red-500 flex-shrink-0" />
                      <input
                        value={c}
                        onChange={e => { const next = [...cons]; next[i] = e.target.value; setCons(next) }}
                        placeholder={t('journey.editor.conPlaceholder')}
                        className="flex-1 min-w-0 bg-transparent border-none outline-none text-[13px] text-zinc-900 dark:text-zinc-100 placeholder:text-red-400 dark:placeholder:text-red-600"
                      />
                      {cons.length > 1 && (
                        <button onClick={() => setCons(cons.filter((_, j) => j !== i))} className="p-1 text-red-300 dark:text-red-700 hover:text-red-600 dark:hover:text-red-400 flex-shrink-0">
                          <X size={13} strokeWidth={2.5} />
                        </button>
                      )}
                    </div>
                  ))}
                  <button
                    onClick={() => setCons([...cons, ''])}
                    className="flex items-center justify-center gap-1.5 h-9 w-full border border-dashed border-red-200 dark:border-red-800/40 rounded-[10px] text-[12px] font-medium text-red-700 dark:text-red-400 hover:border-red-300 dark:hover:border-red-700 transition-colors"
                  >
                    <Plus size={13} strokeWidth={2.5} /> {t('journey.editor.addAnother')}
                  </button>
                </div>
              </div>
            </div>
          </div>

          <div className="h-px bg-zinc-200 dark:bg-zinc-700" />

          <div className="grid grid-cols-2 gap-3">
            <div>
              <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-1.5">{t('journey.editor.date')}</label>
              <DatePicker value={entryDate} onChange={setEntryDate} tripDates={tripDates} />
            </div>
            <div className="relative">
              <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-1.5">{t('journey.editor.location')}</label>
              <div className="relative">
                <input
                  value={locationQuery || locationName}
                  onChange={e => {
                    const q = e.target.value
                    setLocationQuery(q)
                    setShowLocationResults(true)
                    if (locationTimerRef.current) clearTimeout(locationTimerRef.current)
                    if (q.trim().length >= 2) {
                      locationTimerRef.current = setTimeout(async () => {
                        setLocationSearching(true)
                        try {
                          const res = await mapsApi.search(q)
                          setLocationResults((res.places || []).slice(0, 6).map((p: any) => ({
                            name: p.name, address: p.address, lat: Number(p.lat), lng: Number(p.lng),
                          })))
                        } catch { setLocationResults([]) }
                        finally { setLocationSearching(false) }
                      }, 400)
                    } else {
                      setLocationResults([])
                    }
                  }}
                  onFocus={() => { if (locationResults.length > 0) setShowLocationResults(true) }}
                  placeholder={t('journey.editor.searchLocation')}
                  className="w-full px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white outline-none focus:border-zinc-400 dark:focus:border-zinc-500"
                />
                {locationLat && (
                  <div className="absolute right-2 top-1/2 -translate-y-1/2">
                    <MapPin size={13} className="text-zinc-500 dark:text-zinc-400" />
                  </div>
                )}
              </div>
              {showLocationResults && locationResults.length > 0 && (
                <>
                  <div className="fixed inset-0 z-[99]" onClick={() => setShowLocationResults(false)} />
                  <div className="absolute left-0 right-0 top-full mt-1 z-[100] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-lg overflow-hidden max-h-[240px] overflow-y-auto">
                    {locationResults.map((r, i) => (
                      <button
                        key={i}
                        onClick={() => {
                          setLocationName(r.name)
                          setLocationLat(r.lat)
                          setLocationLng(r.lng)
                          setLocationQuery('')
                          setShowLocationResults(false)
                          setLocationResults([])
                        }}
                        className="w-full text-left px-3 py-2.5 hover:bg-zinc-50 dark:hover:bg-zinc-700 flex items-start gap-2.5 border-b border-zinc-100 dark:border-zinc-700 last:border-0"
                      >
                        <MapPin size={13} className="text-zinc-400 flex-shrink-0 mt-0.5" />
                        <div className="min-w-0">
                          <div className="text-[13px] font-medium text-zinc-900 dark:text-white truncate">{r.name}</div>
                          {r.address && <div className="text-[11px] text-zinc-500 truncate">{r.address}</div>}
                        </div>
                      </button>
                    ))}
                  </div>
                </>
              )}
              {locationSearching && (
                <div className="absolute left-0 right-0 top-full mt-1 z-[100] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-lg px-3 py-3 text-center text-[12px] text-zinc-400">
                  {t('journey.editor.searching')}
                </div>
              )}
            </div>
          </div>

          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.editor.mood')}</label>
            <div className="flex gap-2">
              {Object.entries(MOOD_CONFIG).map(([key, config]) => {
                const Icon = config.icon
                const active = mood === key
                return (
                  <button key={key} onClick={() => setMood(active ? '' : key)}
                    className={`flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-medium border transition-all ${
                      active ? '' : 'border-zinc-200 dark:border-zinc-700 text-zinc-500'
                    }`}
                    style={active ? { background: config.bg, color: config.text, borderColor: config.text + '30' } : undefined}>
                    <Icon size={12} />
                    {t(config.label)}
                  </button>
                )
              })}
            </div>
          </div>

          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.editor.weather')}</label>
            <div className="flex flex-wrap gap-2">
              {Object.entries(WEATHER_CONFIG).map(([key, config]) => {
                const Icon = config.icon
                const active = weather === key
                return (
                  <button key={key} onClick={() => setWeather(active ? '' : key)}
                    className={`flex items-center gap-1 px-2 py-1 rounded-full text-[11px] font-medium border transition-all ${
                      active ? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white' : 'border-zinc-200 dark:border-zinc-700 text-zinc-500 hover:border-zinc-400'
                    }`}>
                    <Icon size={12} />
                    {t(config.label)}
                  </button>
                )
              })}
            </div>
          </div>
        </div>


        <div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50" style={{ paddingBottom: 'max(16px, env(safe-area-inset-bottom, 16px))' }}>
          <button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
          <button onClick={handleSave} disabled={saving} className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-50">
            {saving ? t('common.saving') : t('common.save')}
          </button>
        </div>
      </div>
    </div>
  )
}

// ── Add Trip Dialog ──────────────────────────────────────────────────────

function AddTripDialog({ journeyId, existingTripIds, onClose, onAdded }: {
  journeyId: number
  existingTripIds: number[]
  onClose: () => void
  onAdded: () => void
}) {
  const { t } = useTranslation()
  const [trips, setTrips] = useState<{ id: number; title: string; destination?: string; start_date?: string; end_date?: string }[]>([])
  const [search, setSearch] = useState('')
  const [adding, setAdding] = useState<number | null>(null)
  const toast = useToast()

  useEffect(() => {
    journeyApi.availableTrips().then(d => setTrips(d.trips || [])).catch(() => {})
  }, [])

  const filtered = trips.filter(trip => {
    if (existingTripIds.includes(trip.id)) return false
    if (!search) return true
    const q = search.toLowerCase()
    return trip.title.toLowerCase().includes(q) || (trip.destination || '').toLowerCase().includes(q)
  })

  const handleAdd = async (tripId: number) => {
    setAdding(tripId)
    try {
      await journeyApi.addTrip(journeyId, tripId)
      toast.success(t('journey.trips.tripLinked'))
      onAdded()
    } catch {
      toast.error(t('journey.trips.linkFailed'))
    } finally {
      setAdding(null)
    }
  }

  return (
    <div className="fixed inset-0 z-[200] flex items-center justify-center p-5" style={{ background: 'rgba(9,9,11,0.75)' }}>
      <div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[420px] w-full flex flex-col overflow-hidden">

        <div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
          <h2 className="text-[16px] font-bold text-zinc-900 dark:text-white">{t('journey.trips.linkTrip')}</h2>
          <button onClick={onClose} className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
            <X size={16} />
          </button>
        </div>

        <div className="px-6 py-5 flex flex-col gap-4">
          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-1.5">{t('journey.trips.searchTrip')}</label>
            <input
              value={search}
              onChange={e => setSearch(e.target.value)}
              placeholder={t('journey.trips.searchPlaceholder')}
              className="w-full px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white outline-none focus:border-zinc-400 dark:focus:border-zinc-500"
            />
          </div>

          <div className="max-h-[280px] overflow-y-auto flex flex-col gap-1">
            {filtered.length === 0 && (
              <p className="text-[12px] text-zinc-400 text-center py-4">{t('journey.trips.noTripsAvailable')}</p>
            )}
            {filtered.map(trip => (
              <div
                key={trip.id}
                className="flex items-center gap-2.5 p-2.5 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 border border-transparent"
              >
                <div className="w-9 h-9 rounded-md flex-shrink-0" style={{ background: pickGradient(trip.id) }} />
                <div className="flex-1 min-w-0">
                  <div className="text-[13px] font-medium text-zinc-900 dark:text-white truncate">{trip.title}</div>
                  {(trip.destination || trip.start_date) && (
                    <div className="text-[11px] text-zinc-500 truncate">
                      {trip.destination}{trip.destination && trip.start_date ? ' · ' : ''}{trip.start_date}
                    </div>
                  )}
                </div>
                <button
                  onClick={() => handleAdd(trip.id)}
                  disabled={adding === trip.id}
                  className="px-3 py-1.5 rounded-lg text-[11px] font-semibold bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-200 disabled:opacity-50"
                >
                  {adding === trip.id ? '...' : t('journey.trips.link')}
                </button>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  )
}

// ── Contributor Invite Dialog ─────────────────────────────────────────────

function ContributorInviteDialog({ journeyId, existingUserIds, onClose, onInvited }: {
  journeyId: number
  existingUserIds: number[]
  onClose: () => void
  onInvited: () => void
}) {
  const { t } = useTranslation()
  const [users, setUsers] = useState<{ id: number; username: string; email: string; avatar?: string | null }[]>([])
  const [search, setSearch] = useState('')
  const [selectedUserId, setSelectedUserId] = useState<number | null>(null)
  const [role, setRole] = useState<'editor' | 'viewer'>('viewer')
  const [sending, setSending] = useState(false)
  const toast = useToast()

  useEffect(() => {
    authApi.listUsers().then(d => setUsers(d.users || [])).catch(() => {})
  }, [])

  const filtered = users.filter(u => {
    if (existingUserIds.includes(u.id)) return false
    if (!search) return true
    const q = search.toLowerCase()
    return u.username.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)
  })

  const handleInvite = async () => {
    if (!selectedUserId) return
    setSending(true)
    try {
      await journeyApi.addContributor(journeyId, selectedUserId, role)
      toast.success(t('journey.contributors.added'))
      onInvited()
    } catch {
      toast.error(t('journey.contributors.addFailed'))
    } finally {
      setSending(false)
    }
  }

  return (
    <div className="fixed inset-0 z-[200] flex items-center justify-center p-5" style={{ background: 'rgba(9,9,11,0.75)' }}>
      <div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[420px] w-full flex flex-col overflow-hidden">

        <div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
          <h2 className="text-[16px] font-bold text-zinc-900 dark:text-white">{t('journey.contributors.invite')}</h2>
          <button onClick={onClose} className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
            <X size={16} />
          </button>
        </div>

        <div className="px-6 py-5 flex flex-col gap-4">
          {/* Search */}
          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-1.5">{t('journey.contributors.searchUser')}</label>
            <input
              value={search}
              onChange={e => setSearch(e.target.value)}
              placeholder={t('journey.contributors.searchPlaceholder')}
              className="w-full px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white outline-none focus:border-zinc-400 dark:focus:border-zinc-500"
            />
          </div>

          {/* User list */}
          <div className="max-h-[200px] overflow-y-auto flex flex-col gap-1">
            {filtered.length === 0 && (
              <p className="text-[12px] text-zinc-400 text-center py-4">{t('journey.contributors.noUsers')}</p>
            )}
            {filtered.map(u => (
              <div
                key={u.id}
                onClick={() => setSelectedUserId(u.id)}
                className={`flex items-center gap-2.5 p-2.5 rounded-lg cursor-pointer transition-all ${
                  selectedUserId === u.id
                    ? 'bg-zinc-100 dark:bg-zinc-800 border border-zinc-900 dark:border-white'
                    : 'hover:bg-zinc-50 dark:hover:bg-zinc-800 border border-transparent'
                }`}
              >
                <div className="w-8 h-8 rounded-full bg-zinc-200 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 flex items-center justify-center text-[12px] font-semibold">
                  {u.username[0].toUpperCase()}
                </div>
                <div className="flex-1 min-w-0">
                  <div className="text-[13px] font-medium text-zinc-900 dark:text-white">{u.username}</div>
                  <div className="text-[11px] text-zinc-500 truncate">{u.email}</div>
                </div>
                {selectedUserId === u.id && (
                  <div className="w-5 h-5 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 flex items-center justify-center">
                    <Check size={12} />
                  </div>
                )}
              </div>
            ))}
          </div>

          {/* Role selector */}
          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.invite.role')}</label>
            <div className="flex gap-2">
              {(['viewer', 'editor'] as const).map(r => (
                <button
                  key={r}
                  onClick={() => setRole(r)}
                  className={`flex-1 py-2 rounded-lg text-[12px] font-medium border transition-all ${
                    role === r
                      ? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white'
                      : 'border-zinc-200 dark:border-zinc-700 text-zinc-500 hover:border-zinc-400'
                  }`}
                >
                  {t(`journey.invite.${r}`)}
                </button>
              ))}
            </div>
          </div>
        </div>

        <div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
          <button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">
            {t('common.cancel')}
          </button>
          <button
            onClick={handleInvite}
            disabled={!selectedUserId || sending}
            className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40 disabled:cursor-not-allowed"
          >
            {sending ? t('journey.invite.inviting') : t('journey.invite.invite')}
          </button>
        </div>
      </div>
    </div>
  )
}

// ── Journey Settings Dialog ───────────────────────────────────────────────

// ── Journey Share Section ─────────────────────────────────────────────────

function JourneyShareSection({ journeyId }: { journeyId: number }) {
  const { t } = useTranslation()
  const [link, setLink] = useState<{ token: string; share_timeline: boolean; share_gallery: boolean; share_map: boolean } | null>(null)
  const [loading, setLoading] = useState(true)
  const [copied, setCopied] = useState(false)
  const toast = useToast()

  useEffect(() => {
    journeyApi.getShareLink(journeyId).then(d => setLink(d.link || null)).catch(() => {}).finally(() => setLoading(false))
  }, [journeyId])

  const createLink = async () => {
    try {
      const res = await journeyApi.createShareLink(journeyId, { share_timeline: true, share_gallery: true, share_map: true })
      setLink({ token: res.token, share_timeline: true, share_gallery: true, share_map: true })
      toast.success(t('journey.share.linkCreated'))
    } catch { toast.error(t('journey.share.createFailed')) }
  }

  const togglePerm = async (key: 'share_timeline' | 'share_gallery' | 'share_map') => {
    if (!link) return
    const updated = { ...link, [key]: !link[key] }
    setLink(updated)
    try {
      await journeyApi.createShareLink(journeyId, { share_timeline: updated.share_timeline, share_gallery: updated.share_gallery, share_map: updated.share_map })
    } catch { setLink(link); toast.error(t('journey.share.updateFailed')) }
  }

  const deleteLink = async () => {
    try {
      await journeyApi.deleteShareLink(journeyId)
      setLink(null)
      toast.success(t('journey.share.linkDeleted'))
    } catch { toast.error(t('journey.share.deleteFailed')) }
  }

  const shareUrl = link ? `${window.location.origin}/public/journey/${link.token}` : ''

  const copyLink = () => {
    navigator.clipboard.writeText(shareUrl)
    setCopied(true)
    setTimeout(() => setCopied(false), 2000)
  }

  if (loading) return null

  return (
    <div>
      <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.share.publicShare')}</label>

      {!link ? (
        <button
          onClick={createLink}
          className="w-full flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
        >
          <Link size={14} /> {t('journey.share.createLink')}
        </button>
      ) : (
        <div className="flex flex-col gap-3">
          {/* URL + Copy */}
          <div className="flex items-center gap-2 p-2.5 rounded-lg bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700">
            <Link size={13} className="text-zinc-400 flex-shrink-0" />
            <span className="flex-1 text-[11px] text-zinc-600 dark:text-zinc-400 truncate">{shareUrl}</span>
            <button
              onClick={copyLink}
              className="flex-shrink-0 px-2.5 py-1 rounded-md bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[11px] font-medium hover:bg-zinc-700 dark:hover:bg-zinc-200"
            >
              {copied ? t('journey.share.copied') : t('journey.share.copy')}
            </button>
          </div>

          {/* Permission toggles */}
          <div className="flex flex-col gap-1.5">
            {[
              { key: 'share_timeline' as const, label: t('journey.share.timeline'), icon: List },
              { key: 'share_gallery' as const, label: t('journey.share.gallery'), icon: Grid },
              { key: 'share_map' as const, label: t('journey.share.map'), icon: MapPin },
            ].map(({ key, label, icon: Icon }) => (
              <button
                key={key}
                onClick={() => togglePerm(key)}
                className={`flex items-center gap-2.5 px-3 py-2 rounded-lg border text-[12px] font-medium transition-all ${
                  link[key]
                    ? 'border-zinc-900 dark:border-white bg-zinc-900 dark:bg-white text-white dark:text-zinc-900'
                    : 'border-zinc-200 dark:border-zinc-700 text-zinc-500 hover:border-zinc-400'
                }`}
              >
                <Icon size={13} />
                {label}
                {link[key] && <Check size={12} className="ml-auto" />}
              </button>
            ))}
          </div>

          {/* Delete link */}
          <button
            onClick={deleteLink}
            className="text-[11px] font-medium text-red-500 hover:text-red-600 self-start"
          >
            Remove share link
          </button>
        </div>
      )}
    </div>
  )
}

function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
  journey: JourneyDetail
  onClose: () => void
  onSaved: () => void
  onOpenInvite: () => void
}) {
  const { t } = useTranslation()
  const [title, setTitle] = useState(journey.title)
  const [subtitle, setSubtitle] = useState(journey.subtitle || '')
  const [saving, setSaving] = useState(false)
  const [showAddTrip, setShowAddTrip] = useState(false)
  const [unlinkTarget, setUnlinkTarget] = useState<{ trip_id: number; title: string } | null>(null)
  const coverRef = useRef<HTMLInputElement>(null)
  const toast = useToast()
  const navigate = useNavigate()
  const { updateJourney, deleteJourney } = useJourneyStore()

  const handleSave = async () => {
    setSaving(true)
    try {
      await updateJourney(journey.id, { title, subtitle: subtitle || null })
      onSaved()
    } catch {
      toast.error(t('journey.settings.saveFailed'))
    } finally {
      setSaving(false)
    }
  }

  const handleCoverUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return
    const formData = new FormData()
    formData.append('cover', file)
    try {
      await journeyApi.uploadCover(journey.id, formData)
      toast.success(t('journey.settings.coverUpdated'))
      onSaved()
    } catch {
      toast.error(t('journey.settings.coverFailed'))
    }
  }

  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)

  const handleDelete = async () => {
    try {
      await deleteJourney(journey.id)
      navigate('/journey')
    } catch {
      toast.error(t('journey.settings.failedToDelete'))
    }
  }

  return (
    <div className="fixed inset-0 z-[200] flex items-end md:items-center justify-center md:p-5 overscroll-none" style={{ background: 'rgba(9,9,11,0.75)' }} onClick={onClose} onTouchMove={e => { if (e.target === e.currentTarget) e.preventDefault() }}>
      <div className="bg-white dark:bg-zinc-900 rounded-t-2xl md:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[480px] w-full max-h-[85vh] md:max-h-[90vh] flex flex-col overflow-hidden" style={{ paddingBottom: 'var(--bottom-nav-h)' }} onClick={e => e.stopPropagation()}>

        <div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
          <h2 className="text-[16px] font-bold text-zinc-900 dark:text-white">{t('journey.settings.title')}</h2>
          <button onClick={onClose} className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
            <X size={16} />
          </button>
        </div>

        <div className="flex-1 overflow-y-auto overscroll-contain px-6 py-5 flex flex-col gap-5">
          {/* Cover Image */}
          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.settings.coverImage')}</label>
            <input ref={coverRef} type="file" accept="image/*" onChange={handleCoverUpload} className="hidden" />
            <button
              onClick={() => coverRef.current?.click()}
              className="w-full h-28 rounded-xl border border-dashed border-zinc-200 dark:border-zinc-700 flex items-center justify-center gap-2 text-[12px] text-zinc-500 hover:border-zinc-400 dark:hover:border-zinc-500 hover:bg-zinc-50 dark:hover:bg-zinc-800 overflow-hidden relative"
            >
              {journey.cover_image ? (
                <>
                  <img src={`/uploads/${journey.cover_image}`} className="absolute inset-0 w-full h-full object-cover opacity-50" alt="" />
                  <span className="relative z-10 flex items-center gap-1.5"><ImagePlus size={14} /> {t('journey.settings.changeCover')}</span>
                </>
              ) : (
                <span className="flex items-center gap-1.5"><ImagePlus size={14} /> {t('journey.settings.addCover')}</span>
              )}
            </button>
          </div>

          {/* Title */}
          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-1.5">{t('journey.settings.name')}</label>
            <input
              value={title}
              onChange={e => setTitle(e.target.value)}
              className="w-full px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[14px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white outline-none focus:border-zinc-400"
            />
          </div>

          {/* Subtitle */}
          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-1.5">{t('journey.settings.subtitle')}</label>
            <input
              value={subtitle}
              onChange={e => setSubtitle(e.target.value)}
              placeholder={t('journey.settings.subtitlePlaceholder')}
              className="w-full px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[14px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white outline-none focus:border-zinc-400"
            />
          </div>

          <div className="h-px bg-zinc-200 dark:bg-zinc-700" />

          {/* Synced Trips */}
          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.detail.syncedTrips')}</label>
            <div className="flex flex-col gap-1.5">
              {journey.trips.map((trip: any) => (
                <div key={trip.trip_id} className="flex items-center gap-2.5 p-2 rounded-lg bg-zinc-50 dark:bg-zinc-800">
                  <div className="w-8 h-8 rounded-md flex-shrink-0" style={{ background: pickGradient(trip.trip_id) }} />
                  <div className="flex-1 min-w-0">
                    <div className="text-[12px] font-medium text-zinc-900 dark:text-white">{trip.title}</div>
                    <div className="text-[10px] text-zinc-500">{trip.place_count || 0} {t('journey.synced.places')}</div>
                  </div>
                  <button
                    onClick={() => setUnlinkTarget({ trip_id: trip.trip_id, title: trip.title })}
                    className="w-8 h-8 rounded-lg flex-shrink-0 flex items-center justify-center bg-red-500/10 text-red-500 hover:bg-red-500/20 dark:bg-red-500/15 dark:hover:bg-red-500/25 transition-colors"
                    title="Unlink trip"
                  >
                    <Trash2 size={14} />
                  </button>
                </div>
              ))}
              {journey.trips.length === 0 && <p className="text-[11px] text-zinc-400">{t('journey.trips.noTripsLinkedSettings')}</p>}
              <button
                onClick={() => setShowAddTrip(true)}
                className="w-full mt-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
              >
                <Plus size={14} /> {t('journey.trips.addTrip')}
              </button>
            </div>
          </div>

          {/* Contributors */}
          <div>
            <label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.detail.contributors')}</label>
            <div className="flex flex-col gap-2">
              {journey.contributors.map((c: any) => (
                <div key={c.user_id} className="flex items-center gap-2.5">
                  <div className="w-7 h-7 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 flex items-center justify-center text-[11px] font-semibold">
                    {(c.username || '?')[0].toUpperCase()}
                  </div>
                  <div className="flex-1 text-[12px] font-medium text-zinc-900 dark:text-white">{c.username}</div>
                  <span className={`text-[9px] font-medium px-1.5 py-0.5 rounded-full ${c.role === 'owner' ? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-500'}`}>{c.role}</span>
                </div>
              ))}
              <button
                onClick={onOpenInvite}
                className="w-full mt-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
              >
                <UserPlus size={14} /> {t('journey.contributors.invite')}
              </button>
            </div>
          </div>

          <div className="h-px bg-zinc-200 dark:bg-zinc-700" />

          {/* Public Share */}
          <JourneyShareSection journeyId={journey.id} />

        </div>

        {/* Footer */}
        <div className="flex flex-wrap items-center gap-2 px-6 py-4 pb-6 md:pb-4 border-t border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
          <button
            onClick={() => setShowDeleteConfirm(true)}
            className="flex items-center gap-1.5 text-[12px] font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg px-2.5 py-2 mr-auto"
          >
            <Trash2 size={13} />
            {t('journey.settings.delete')}
          </button>
          <button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
          <button onClick={handleSave} disabled={saving || !title.trim()} className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40">
            {saving ? t('common.saving') : t('common.save')}
          </button>
        </div>
      </div>

      {/* Unlink Trip confirm */}
      <ConfirmDialog
        isOpen={!!unlinkTarget}
        onClose={() => setUnlinkTarget(null)}
        onConfirm={async () => {
          if (!unlinkTarget) return
          try {
            await journeyApi.removeTrip(journey.id, unlinkTarget.trip_id)
            toast.success(t('journey.trips.tripUnlinked'))
            setUnlinkTarget(null)
            onSaved()
          } catch {
            toast.error(t('journey.trips.unlinkFailed'))
          }
        }}
        title={t('journey.trips.unlinkTrip')}
        message={t('journey.trips.unlinkMessage', { title: unlinkTarget?.title })}
        confirmLabel={t('journey.trips.unlink')}
        danger
      />

      {/* Add Trip */}
      {showAddTrip && (
        <AddTripDialog
          journeyId={journey.id}
          existingTripIds={journey.trips.map((t: any) => t.trip_id)}
          onClose={() => setShowAddTrip(false)}
          onAdded={() => { setShowAddTrip(false); onSaved() }}
        />
      )}

      <ConfirmDialog
        isOpen={showDeleteConfirm}
        onClose={() => setShowDeleteConfirm(false)}
        onConfirm={handleDelete}
        title={t('journey.settings.deleteJourney')}
        message={t('journey.settings.deleteMessage', { title: journey.title })}
        confirmLabel={t('common.delete')}
        danger
      />
    </div>
  )
}
