import React, { useEffect, useState, useMemo, useCallback } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
import { open as openDialog, save as saveDialog } from '@tauri-apps/plugin-dialog'
import UpdateNotifier from './components/UpdateNotifier.jsx'
import Icon from './components/Icon.jsx'
import {
  checkForAppUpdate,
  installAppUpdate,
  skipUpdateVersion,
  getLastUpdateCheck,
  formatLastCheck,
  isTauri,
} from './utils/updater.js'

// Transitive Dependency-Aufloesung: gibt Set aller Module zurueck, die
// aktiviert sein muessen, wenn `selected` aktiviert sind.
function resolveDeps(selected, allModules) {
  const byId = Object.fromEntries(allModules.map(m => [m.id, m]))
  const out = new Set(selected)
  let changed = true
  while (changed) {
    changed = false
    for (const id of [...out]) {
      const m = byId[id]
      if (!m) continue
      for (const dep of (m.dependsOn || [])) {
        if (!out.has(dep)) { out.add(dep); changed = true }
      }
    }
  }
  return out
}

function formatRelative(iso) {
  if (!iso) return ''
  try {
    const d = new Date(iso)
    const now = new Date()
    const diff = (now - d) / 1000
    if (diff < 60)        return 'vor wenigen Sek.'
    if (diff < 3600)      return `vor ${Math.floor(diff/60)} Min.`
    if (diff < 86400)     return `vor ${Math.floor(diff/3600)} h`
    if (diff < 86400 * 7) return `vor ${Math.floor(diff/86400)} Tagen`
    return d.toLocaleDateString('de-CH')
  } catch { return '' }
}

function formatFileSize(bytes) {
  if (!bytes && bytes !== 0) return ''
  const mb = bytes / (1024 * 1024)
  if (mb >= 1) return `${mb.toFixed(1)} MB`
  return `${(bytes / 1024).toFixed(0)} KB`
}

// Sortierung: gepinnte zuerst (intern nach lastOpened), dann der Rest nach
// lastOpened. Stabil — ein Toggle der Pin-Eigenschaft veraendert die Reihenfolge
// vorhersagbar.
function sortProjects(list) {
  return [...list].sort((a, b) => {
    if (!!a.pinned !== !!b.pinned) return a.pinned ? -1 : 1
    return (b.lastOpened || '').localeCompare(a.lastOpened || '')
  })
}

function newTagId() {
  return 'tag_' + Math.random().toString(36).slice(2, 9)
}

// Schwarz/Weiss-Kontrast fuer Tag-Chip-Text
function isDarkColor(hex) {
  if (!hex) return true
  const h = hex.replace('#', '')
  const n = h.length === 3
    ? [0,1,2].map(i => parseInt(h[i] + h[i], 16))
    : [0,2,4].map(i => parseInt(h.slice(i, i+2), 16))
  // Perceived brightness
  return (n[0] * 0.299 + n[1] * 0.587 + n[2] * 0.114) < 140
}

export default function App() {
  const [recent, setRecent] = useState([])
  const [modules, setModules] = useState([])
  const [dialog, setDialog] = useState(null)       // null | { mode: 'new'|'edit', project? }
  const [settingsOpen, setSettingsOpen] = useState(false)
  const [settingsTab, setSettingsTab] = useState('rhino')
  const [busy, setBusy] = useState(false)
  const [query, setQuery] = useState('')

  // Window-Layouts + aktuell gewaehltes Default — fuer Quick-Switcher in Topbar
  const [layouts, setLayouts] = useState([])
  const [activeLayout, setActiveLayout] = useState('')

  // Rhino-Status: alle 5s pollen
  const [rhinoRunning, setRhinoRunning] = useState(false)

  // Tags aus den globalen Settings — Settings-Tab editiert sie, Dialog + Card
  // konsumieren sie. Single Source of Truth ist die Tauri-settings.json.
  const [tags, setTags] = useState([])

  // File-Meta pro Pfad (Map). Wird beim Mount + nach OpenProject neu geladen.
  const [fileMeta, setFileMeta] = useState({})

  // Aktiv gefilterte Tag-IDs (Topbar-Filter). Leeres Set = alle anzeigen.
  const [tagFilter, setTagFilter] = useState(new Set())

  // Snapshot-Toast (Pfad oder null)
  const [snapshotToast, setSnapshotToast] = useState(null)

  // View-Tabs: 'recent' = kuerzlich geoeffnete, 'all' = alle mit Sort/Group
  const [view, setView] = useState('recent')
  const [sortBy, setSortBy] = useState('lastOpened')   // lastOpened | name | createdAt
  const [groupByTag, setGroupByTag] = useState(false)

  // Pro-Projekt-Pfad -> bool (.rhl-Lock vorhanden?)
  const [openMap, setOpenMap] = useState({})

  // Pro-Pfad -> dataUrl (PNG-Thumbnail) | null
  const [thumbMap, setThumbMap] = useState({})

  useEffect(() => {
    invoke('list_recent').then(r => setRecent(sortProjects(r || []))).catch(console.error)
    invoke('read_modules_manifest').then(setModules).catch(console.error)
    invoke('list_window_layouts').then(setLayouts).catch(() => {})
    invoke('read_dossier_settings').then(ds => setActiveLayout(ds?.windowLayout || '')).catch(() => {})
    invoke('read_settings').then(s => setTags(s?.tags || [])).catch(() => {})
  }, [])

  // File-Meta laden sobald recent sich aendert
  useEffect(() => {
    let alive = true
    Promise.all((recent || []).map(p =>
      invoke('get_file_meta', { path: p.path })
        .then(m => [p.path, m]).catch(() => [p.path, null])
    )).then(pairs => {
      if (!alive) return
      const map = {}
      for (const [k, v] of pairs) map[k] = v
      setFileMeta(map)
    })
    return () => { alive = false }
  }, [recent])

  // Thumbnails laden — Initial + alle 30s neu (Cache-Refresh nach Saves)
  useEffect(() => {
    let alive = true
    const load = async () => {
      if (recent.length === 0) return
      const pairs = await Promise.all(recent.map(p =>
        invoke('read_thumbnail', { path: p.path })
          .then(r => [p.path, r?.dataUrl || null])
          .catch(() => [p.path, null])
      ))
      if (!alive) return
      const next = {}
      for (const [k, v] of pairs) next[k] = v
      setThumbMap(next)
    }
    load()
    const t = setInterval(load, 30000)
    return () => { alive = false; clearInterval(t) }
  }, [recent])

  // Rhino-Status-Poll + Open-Status pro File (.rhl-Check fuer primary + extras)
  useEffect(() => {
    let alive = true
    const check = async () => {
      try {
        const running = await invoke('is_rhino_running')
        if (!alive) return
        setRhinoRunning(!!running)
        if (running && recent.length > 0) {
          // Alle zu checkenden Pfade sammeln (primary + extras) ohne Duplikate
          const paths = new Set()
          for (const p of recent) {
            paths.add(p.path)
            for (const f of (p.extraFiles || [])) paths.add(f)
          }
          const results = await Promise.all([...paths].map(pth =>
            invoke('is_project_open', { path: pth })
              .then(b => [pth, !!b]).catch(() => [pth, false])
          ))
          if (!alive) return
          const map = {}
          for (const [k, v] of results) map[k] = v
          setOpenMap(map)
        } else {
          if (alive) setOpenMap({})
        }
      } catch {}
    }
    check()
    const t = setInterval(check, 5000)
    return () => { alive = false; clearInterval(t) }
  }, [recent])

  // Tray neu bauen sobald recent sich aendert (Reihenfolge, Inhalt)
  const syncTray = useCallback(() => {
    invoke('refresh_tray_menu').catch(() => {})
  }, [])

  // Gefiltert nach Query + aktiven Tag-Filtern
  const filteredRecent = useMemo(() => {
    const q = query.trim().toLowerCase()
    return recent.filter(p => {
      if (q) {
        const inText =
          (p.name || '').toLowerCase().includes(q) ||
          (p.path || '').toLowerCase().includes(q)
        if (!inText) return false
      }
      if (tagFilter.size > 0) {
        const pTags = new Set(p.tagIds || [])
        for (const t of tagFilter) if (!pTags.has(t)) return false
      }
      return true
    })
  }, [recent, query, tagFilter])

  // 'Recent'-View: nur die letzten 8, ohne Tag-Filter
  const recentSlice = useMemo(() => {
    return [...recent]
      .sort((a, b) => {
        if (!!a.pinned !== !!b.pinned) return a.pinned ? -1 : 1
        return (b.lastOpened || '').localeCompare(a.lastOpened || '')
      })
      .slice(0, 8)
  }, [recent])

  // 'Alle'-View: Sortierung nach gewaehltem Kriterium (Pin immer oben)
  const allSorted = useMemo(() => {
    const cmp = (a, b) => {
      if (!!a.pinned !== !!b.pinned) return a.pinned ? -1 : 1
      switch (sortBy) {
        case 'name':       return (a.name || '').localeCompare(b.name || '')
        case 'createdAt':  return (b.createdAt || b.lastOpened || '').localeCompare(a.createdAt || a.lastOpened || '')
        case 'lastOpened':
        default:           return (b.lastOpened || '').localeCompare(a.lastOpened || '')
      }
    }
    return [...filteredRecent].sort(cmp)
  }, [filteredRecent, sortBy])

  // Wenn group-by-tag aktiv: Map tagId -> Projekte. Untagged kommt zuletzt.
  const allGrouped = useMemo(() => {
    if (!groupByTag) return null
    const groups = new Map()
    for (const t of tags) groups.set(t.id, [])
    groups.set('__untagged__', [])
    for (const p of allSorted) {
      const ids = (p.tagIds || []).filter(id => groups.has(id))
      if (ids.length === 0) {
        groups.get('__untagged__').push(p)
      } else {
        for (const id of ids) groups.get(id).push(p)
      }
    }
    // Leere Gruppen wegwerfen
    return Array.from(groups.entries()).filter(([_, ps]) => ps.length > 0)
  }, [allSorted, groupByTag, tags])

  const togglePin = async (proj) => {
    const next = sortProjects(recent.map(p =>
      p.path === proj.path ? { ...p, pinned: !p.pinned } : p
    ))
    setRecent(next)
    await invoke('save_recent', { projects: next }).catch(() => {})
    syncTray()
  }

  const snapshotProject = async (proj) => {
    try {
      const target = await invoke('create_snapshot', { path: proj.path })
      setSnapshotToast(target)
      setTimeout(() => setSnapshotToast(null), 2400)
    } catch (ex) {
      alert(`Snapshot fehlgeschlagen: ${ex}`)
    }
  }

  const toggleTagFilter = (tagId) => {
    setTagFilter(prev => {
      const next = new Set(prev)
      if (next.has(tagId)) next.delete(tagId); else next.add(tagId)
      return next
    })
  }

  const onPickLayout = async (name) => {
    setActiveLayout(name)
    // Default speichern (fuer Auto-Apply beim naechsten Start)
    const cur = await invoke('read_dossier_settings').catch(() => ({}))
    await invoke('save_dossier_settings', {
      settings: { ...cur, windowLayout: name || null },
    }).catch(() => {})
    if (name) {
      // Bei aktiver Rhino-Session: direkt anwenden via Polling-Flag
      await invoke('apply_layout_now', { name }).catch(() => {})
    }
  }

  // Tray-Menue navigiert ueber das `dossier:navigate` Event.
  useEffect(() => {
    let unlisten = null
    listen('dossier:navigate', (e) => {
      const view = e.payload
      if (view === 'settings') { setSettingsTab('rhino'); setSettingsOpen(true) }
      else if (view === 'new') { setDialog({ mode: 'new' }) }
      else if (view === 'check-update') {
        setSettingsTab('updates')
        setSettingsOpen(true)
        // Modal-Pfad ueber UpdateNotifier wuerde nur bei verfuegbarem Update
        // erscheinen — fuer den manuellen Check zeigen wir den Settings-Tab,
        // der direkt den Check-Button hat.
      }
    }).then(fn => { unlisten = fn })
    return () => { if (unlisten) unlisten() }
  }, [])

  const openProject = async (proj, fileOverride = null) => {
    if (busy) return
    const target = fileOverride || proj.path
    setBusy(true)
    try {
      // Per-Projekt-Layout NUR beim Oeffnen der Primary-Datei anwenden —
      // bei Extra-Files wollen wir vermutlich nicht das Layout switchen.
      if (!fileOverride && proj.windowLayout) {
        try { await invoke('apply_layout_now', { name: proj.windowLayout }) }
        catch (ex) { console.warn('per-project layout:', ex) }
      }
      await invoke('open_rhino', { path3dm: target })
      const next = recent
        .map(p => p.path === proj.path ? { ...p, lastOpened: new Date().toISOString() } : p)
        .sort((a, b) => (b.lastOpened || '').localeCompare(a.lastOpened || ''))
      await invoke('save_recent', { projects: next })
      setRecent(next)
      syncTray()
    } catch (ex) {
      alert(`Rhino-Start fehlgeschlagen: ${ex}`)
    } finally {
      setBusy(false)
    }
  }

  const revealProject = async (proj) => {
    try { await invoke('show_in_finder', { path: proj.path }) }
    catch (ex) { alert(`Im Finder zeigen: ${ex}`) }
  }

  const editProject = (proj) => {
    setDialog({ mode: 'edit', project: proj })
  }

  const saveProject = async ({ name, path, modules: mods, useTemplate, windowLayout,
                                tagIds, extraFiles }) => {
    if (!path) { alert('Bitte eine .3dm-Datei auswaehlen.'); return }
    if (!name.trim()) { alert('Bitte einen Projekt-Namen angeben.'); return }
    try {
      if (useTemplate) {
        try { await invoke('copy_template_to', { targetPath: path }) }
        catch (ex) { console.warn('Template-Kopie:', ex) }
      }
      await invoke('write_project_config', { path3dm: path, name: name.trim(), modules: mods })
      const existing = recent.find(p => p.path === path)
      const others = recent.filter(p => p.path !== path)
      const now = new Date().toISOString()
      const merged = {
        name: name.trim(),
        path,
        modules: mods,
        windowLayout: windowLayout || null,
        tagIds: tagIds || [],
        extraFiles: extraFiles || [],
        pinned: existing?.pinned || false,
        createdAt: existing?.createdAt || now,
        lastOpened: existing?.lastOpened || now,
      }
      const next = sortProjects([merged, ...others])
      await invoke('save_recent', { projects: next })
      setRecent(next)
      syncTray()
      setDialog(null)
    } catch (ex) {
      alert(`Speichern fehlgeschlagen: ${ex}`)
    }
  }

  const removeProject = async (proj) => {
    const next = recent.filter(p => p.path !== proj.path)
    await invoke('save_recent', { projects: next })
    setRecent(next)
    syncTray()
  }

  return (
    <div className="app">
      <div className="topbar">
        <div className="brand-wrap">
          <span className="brand">DOSSIER<span className="brand-dot">.</span></span>
          <span className="version">v0.6.3</span>
        </div>
        <span className="pre-badge">Pre-Release</span>

        <span className={`status-pill ${rhinoRunning ? 'is-on' : ''}`}
              title={rhinoRunning ? 'Rhino läuft' : 'Rhino offline'}>
          <span className="dot" />
          Rhino {rhinoRunning ? 'aktiv' : 'offline'}
        </span>

        <div className="spacer" />

        {layouts.length > 0 && (
          <div className="layout-switcher" title="Window-Layout wechseln">
            <span className="ls-label">LAYOUT</span>
            <select value={activeLayout}
                    onChange={e => onPickLayout(e.target.value)}>
              <option value="">— keines</option>
              {layouts.map(l => <option key={l} value={l}>{l}</option>)}
            </select>
          </div>
        )}

        <button className="primary pill" onClick={() => setDialog({ mode: 'new' })}>
          <Icon name="plus" size={14} /> Neues Projekt
        </button>
        <button className="ghost icon-btn"
                onClick={() => { setSettingsTab('rhino'); setSettingsOpen(true) }}
                title="Einstellungen">
          <Icon name="settings" />
        </button>
      </div>

      <div className="main">
        <div className="main-inner">
          <div className="view-tabs">
            <button className={`view-tab ${view === 'recent' ? 'is-active' : ''}`}
                    onClick={() => setView('recent')}>
              Kürzlich
            </button>
            <button className={`view-tab ${view === 'all' ? 'is-active' : ''}`}
                    onClick={() => setView('all')}>
              Alle Projekte
              {recent.length > 0 && <span className="view-tab-count">{recent.length}</span>}
            </button>
            <div className="view-tabs-spacer" />
            {recent.length > 0 && (
              <div className="search-wrap">
                <input type="text" className="search-input"
                       placeholder="Suchen…"
                       value={query}
                       onChange={e => setQuery(e.target.value)} />
                {query && (
                  <button className="ghost icon-btn search-clear"
                          onClick={() => setQuery('')}
                          title="Suche zurücksetzen">
                    <Icon name="close" size={14} />
                  </button>
                )}
              </div>
            )}
          </div>

          {view === 'all' && (
            <div className="view-controls">
              <span className="view-controls-label">Sortieren</span>
              <select value={sortBy} onChange={e => setSortBy(e.target.value)}>
                <option value="lastOpened">Zuletzt geöffnet</option>
                <option value="name">Name</option>
                <option value="createdAt">Erstellt</option>
              </select>
              {tags.length > 0 && (
                <label className="view-controls-group">
                  <input type="checkbox" checked={groupByTag}
                         onChange={e => setGroupByTag(e.target.checked)} />
                  Nach Tags gruppieren
                </label>
              )}
            </div>
          )}

          {view === 'all' && tags.length > 0 && !groupByTag && (
            <div className="tag-filter-bar">
              <span className="tag-filter-label">Filter:</span>
              {tags.map(t => {
                const active = tagFilter.has(t.id)
                return (
                  <button key={t.id}
                          className={`tag-chip btn-tag ${active ? 'is-active' : ''}`}
                          style={{
                            background: active ? t.color : 'transparent',
                            borderColor: t.color,
                            color: active ? (isDarkColor(t.color) ? '#fff' : '#0a1715') : t.color,
                          }}
                          onClick={() => toggleTagFilter(t.id)}>
                    {t.name}
                  </button>
                )
              })}
              {tagFilter.size > 0 && (
                <button className="ghost" style={{ fontSize: 10 }}
                        onClick={() => setTagFilter(new Set())}>
                  Zurücksetzen
                </button>
              )}
            </div>
          )}

          {recent.length === 0 ? (
            <div className="empty">
              <span className="eyebrow">Noch leer</span>
              Lege dein erstes Dossier-Projekt an —<br />
              wähle eine bestehende <code>.3dm</code> oder erstelle eine neue.
              <div style={{ marginTop: 18 }}>
                <button className="primary pill" onClick={() => setDialog({ mode: 'new' })}>
                  <Icon name="plus" size={14} /> Neues Projekt
                </button>
              </div>
            </div>
          ) : view === 'recent' ? (
            <div className="project-list">
              {recentSlice.map(p =>
                <ProjectCard key={p.path} p={p}
                             tags={tags} fileMeta={fileMeta} openMap={openMap} thumbMap={thumbMap}
                             busy={busy}
                             onOpen={openProject} onEdit={editProject}
                             onReveal={revealProject} onSnapshot={snapshotProject}
                             onTogglePin={togglePin} onRemove={removeProject} />
              )}
            </div>
          ) : groupByTag && allGrouped ? (
            <div className="project-list">
              {allGrouped.map(([tagId, ps]) => {
                const tag = tagId === '__untagged__' ? null : tags.find(t => t.id === tagId)
                return (
                  <div key={tagId} className="project-group">
                    <div className="project-group-header">
                      {tag ? (
                        <span className="tag-chip" style={{
                          background: tag.color,
                          color: isDarkColor(tag.color) ? '#fff' : '#0a1715',
                        }}>{tag.name}</span>
                      ) : (
                        <span className="tag-chip" style={{
                          background: 'transparent', borderColor: 'var(--border)', color: 'var(--text4)',
                        }}>Ohne Tag</span>
                      )}
                      <span className="project-group-count">{ps.length}</span>
                    </div>
                    {ps.map(p =>
                      <ProjectCard key={p.path} p={p}
                                   tags={tags} fileMeta={fileMeta} openMap={openMap} thumbMap={thumbMap}
                                   busy={busy}
                                   onOpen={openProject} onEdit={editProject}
                                   onReveal={revealProject} onSnapshot={snapshotProject}
                                   onTogglePin={togglePin} onRemove={removeProject} />
                    )}
                  </div>
                )
              })}
            </div>
          ) : allSorted.length === 0 ? (
            <div className="empty">
              <span className="eyebrow">Keine Treffer</span>
              Kein Projekt entspricht den Filterkriterien.
            </div>
          ) : (
            <div className="project-list">
              {allSorted.map(p =>
                <ProjectCard key={p.path} p={p}
                             tags={tags} fileMeta={fileMeta} openMap={openMap} thumbMap={thumbMap}
                             busy={busy}
                             onOpen={openProject} onEdit={editProject}
                             onReveal={revealProject} onSnapshot={snapshotProject}
                             onTogglePin={togglePin} onRemove={removeProject} />
              )}
            </div>
          )}
        </div>
      </div>

      {dialog && (
        <ProjectDialog
          mode={dialog.mode}
          project={dialog.project}
          modules={modules}
          onCancel={() => setDialog(null)}
          onSave={saveProject}
        />
      )}
      {settingsOpen && (
        <SettingsDialog
          initialTab={settingsTab}
          onClose={() => { setSettingsOpen(false); invoke('read_settings').then(s => setTags(s?.tags || [])).catch(() => {}) }}
        />
      )}
      <UpdateNotifier />
      {snapshotToast && (
        <div className="toast">
          <Icon name="snapshot" size={14} />
          <span>Snapshot erstellt: <code>{snapshotToast.split('/').pop()}</code></span>
        </div>
      )}
    </div>
  )
}

function ProjectCard({ p, tags, fileMeta, openMap, thumbMap = {}, busy,
                       onOpen, onEdit, onReveal, onSnapshot, onTogglePin, onRemove }) {
  const [expanded, setExpanded] = useState(false)
  const meta = fileMeta[p.path]
  const extras = p.extraFiles || []
  const thumb = thumbMap[p.path]
  // Open-Status: PRIMARY oder einer der Extras offen
  const isOpen = !!openMap[p.path] || extras.some(f => openMap[f])
  const projTags = (p.tagIds || [])
    .map(id => tags.find(t => t.id === id))
    .filter(Boolean)
  return (
    <div className={`project-card ${p.pinned ? 'is-pinned' : ''} ${isOpen ? 'is-open' : ''}`}
         onDoubleClick={() => onOpen(p)}>
      <div className="card-row">
        {thumb ? (
          <div className="card-thumb">
            <img src={thumb} alt="" />
          </div>
        ) : (
          <div className="card-thumb is-empty"
               title="Beim nächsten Speichern in Rhino wird automatisch ein Thumbnail erzeugt">
            <Icon name="snapshot" size={18} />
          </div>
        )}
        <div className="card-content">
          <div className="name-row">
            <span className="name">{p.name}</span>
            <span className="module-count">{p.modules?.length || 0} Module</span>
            {extras.length > 0 && (
              <button className="module-count expand-chip"
                      onClick={() => setExpanded(x => !x)}
                      title={expanded ? 'Dateien einklappen' : 'Alle Dateien anzeigen'}>
                +{extras.length} Datei{extras.length === 1 ? '' : 'en'}
                <Icon name={expanded ? 'chevron_up' : 'chevron_down'} size={12} />
              </button>
            )}
            {projTags.map(t => (
              <span key={t.id} className="tag-chip"
                    style={{
                      background: t.color,
                      color: isDarkColor(t.color) ? '#fff' : '#0a1715',
                    }}>
                {t.name}
              </span>
            ))}
          </div>
          <div className="path" title={p.path}>{p.path}</div>
        </div>
        <div className="status-col" title={isOpen ? 'In Rhino geöffnet' : ''}>
          {isOpen && <span className="open-dot" />}
        </div>
        <div className="meta">
          <div>{formatRelative(p.lastOpened)}</div>
          {meta?.exists && (
            <div style={{ color: 'var(--text4)', fontSize: 9, marginTop: 2 }}>
              {formatFileSize(meta.size)}
              {meta.modifiedIso && ` · ${formatRelative(meta.modifiedIso)}`}
            </div>
          )}
          {meta && !meta.exists && (
            <div style={{ color: 'var(--danger)', fontSize: 9, marginTop: 2 }}>
              Datei fehlt
            </div>
          )}
        </div>
        <div className="actions">
          <button className={`ghost icon-btn ${p.pinned ? 'is-active' : ''}`}
                  onClick={() => onTogglePin(p)}
                  title={p.pinned ? 'Pin entfernen' : 'Anpinnen'}>
            <Icon name={p.pinned ? 'pin_filled' : 'pin'} />
          </button>
          <button className="ghost icon-btn"
                  onClick={() => onSnapshot(p)}
                  title="Snapshot erstellen (Kopie mit Zeitstempel in _snapshots/)">
            <Icon name="snapshot" />
          </button>
          <button className="ghost icon-btn"
                  onClick={() => onReveal(p)}
                  title="Im Finder zeigen">
            <Icon name="folder" />
          </button>
          <button className="ghost icon-btn"
                  onClick={() => onEdit(p)}
                  title="Bearbeiten">
            <Icon name="edit" />
          </button>
          <button className="primary pill" disabled={busy} onClick={() => onOpen(p)}>
            Öffnen
          </button>
          <button className="ghost danger icon-btn"
                  onClick={() => onRemove(p)}
                  title="Aus Liste entfernen">
            <Icon name="close" size={14} />
          </button>
        </div>
      </div>

      {expanded && extras.length > 0 && (
        <div className="file-list">
          <FileRow path={p.path} isPrimary
                   isOpen={!!openMap[p.path]}
                   busy={busy}
                   onOpen={() => onOpen(p)} />
          {extras.map(f => (
            <FileRow key={f} path={f}
                     isOpen={!!openMap[f]}
                     busy={busy}
                     onOpen={() => onOpen(p, f)} />
          ))}
        </div>
      )}
    </div>
  )
}

function FileRow({ path, isPrimary = false, isOpen, busy, onOpen }) {
  const name = path.split('/').pop()
  return (
    <div className="file-row">
      <div className="file-row-status">
        {isOpen && <span className="open-dot" />}
      </div>
      <div className="file-row-name">
        {isPrimary && <span className="file-row-badge">Hauptdatei</span>}
        <span title={path}>{name}</span>
      </div>
      <div className="file-row-path" title={path}>{path}</div>
      <button className="primary pill" disabled={busy} onClick={onOpen}>
        Öffnen
      </button>
    </div>
  )
}

function SettingsDialog({ initialTab = 'rhino', onClose }) {
  const [tab, setTab] = useState(initialTab)

  return (
    <div className="dialog-bg" onClick={(e) => { if (e.target === e.currentTarget) onClose() }}>
      <div className="dialog" style={{ width: 640 }}>
        <header style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
          <span>Einstellungen</span>
          <div style={{ display: 'flex', gap: 4, marginLeft: 'auto', flexWrap: 'wrap' }}>
            <TabBtn active={tab === 'rhino'}    onClick={() => setTab('rhino')}>Rhino</TabBtn>
            <TabBtn active={tab === 'view'}     onClick={() => setTab('view')}>View</TabBtn>
            <TabBtn active={tab === 'ebenen'}   onClick={() => setTab('ebenen')}>Ebenen</TabBtn>
            <TabBtn active={tab === 'tags'}     onClick={() => setTab('tags')}>Tags</TabBtn>
            <TabBtn active={tab === 'layout'}   onClick={() => setTab('layout')}>Layout</TabBtn>
            <TabBtn active={tab === 'updates'}  onClick={() => setTab('updates')}>Updates</TabBtn>
          </div>
        </header>
        <div className="body">
          {tab === 'rhino'   && <RhinoSettings />}
          {tab === 'view'    && <ViewSettings />}
          {tab === 'ebenen'  && <EbenenSchemaSettings />}
          {tab === 'tags'    && <TagsSettings />}
          {tab === 'layout'  && <LayoutSettings />}
          {tab === 'updates' && <UpdatesSettings />}
        </div>
        <footer>
          <button className="primary pill" onClick={onClose}>Schliessen</button>
        </footer>
      </div>
    </div>
  )
}

function TabBtn({ active, onClick, children }) {
  return (
    <button
      onClick={onClick}
      style={{
        background: active ? 'var(--accent)' : 'transparent',
        borderColor: active ? 'var(--accent)' : 'var(--border)',
        color: active ? 'white' : 'var(--text-muted)',
        fontSize: 11,
        padding: '4px 10px',
      }}
    >
      {children}
    </button>
  )
}

function RhinoSettings() {
  const [rhinoApp, setRhinoApp] = useState('')
  const [templatePath, setTemplatePath] = useState('')
  const [autoLoadPlugin, setAutoLoadPlugin] = useState(false)
  const [pluginStartupPath, setPluginStartupPath] = useState('')
  const [defaultStartup, setDefaultStartup] = useState('')
  const [loaded, setLoaded] = useState(false)
  const [saved, setSaved] = useState(false)
  const [pluginLoading, setPluginLoading] = useState(false)

  useEffect(() => {
    Promise.all([
      invoke('read_settings').catch(() => ({})),
      invoke('get_default_plugin_startup_path').catch(() => ''),
    ]).then(([s, def]) => {
      setRhinoApp(s.rhinoApp || 'Rhinoceros 8')
      setTemplatePath(s.templatePath || '')
      setAutoLoadPlugin(!!s.autoLoadPlugin)
      setPluginStartupPath(s.pluginStartupPath || '')
      setDefaultStartup(def || '')
      setLoaded(true)
    })
  }, [])

  const pickRhino = async () => {
    const sel = await openDialog({
      title: 'Rhino-App waehlen',
      filters: [{ name: 'Anwendung', extensions: ['app'] }],
      defaultPath: '/Applications',
    })
    if (typeof sel === 'string') setRhinoApp(sel)
  }

  const pickTemplate = async () => {
    const sel = await openDialog({
      title: 'Template-3dm waehlen',
      filters: [{ name: 'Rhino', extensions: ['3dm'] }],
    })
    if (typeof sel === 'string') setTemplatePath(sel)
  }

  const pickStartup = async () => {
    const sel = await openDialog({
      title: 'startup.py des Dossier-Plugins',
      filters: [{ name: 'Python', extensions: ['py'] }],
    })
    if (typeof sel === 'string') setPluginStartupPath(sel)
  }

  const clearTemplate = () => setTemplatePath('')
  const useDefault = () => setRhinoApp('Rhinoceros 8')
  const useDefaultStartup = () => setPluginStartupPath('')

  const loadNow = async () => {
    try {
      setPluginLoading(true)
      await invoke('trigger_plugin_load_now')
      setTimeout(() => setPluginLoading(false), 4000)
    } catch (ex) {
      setPluginLoading(false)
      alert(`Plugin-Load: ${ex}`)
    }
  }

  const save = async () => {
    try {
      const cur = await invoke('read_settings').catch(() => ({}))
      await invoke('save_settings', {
        settings: {
          ...cur,
          rhinoApp: rhinoApp.trim() || 'Rhinoceros 8',
          templatePath: templatePath.trim() || null,
          autoLoadPlugin: !!autoLoadPlugin,
          pluginStartupPath: pluginStartupPath.trim() || null,
        },
      })
      setSaved(true)
      setTimeout(() => setSaved(false), 1500)
    } catch (ex) {
      alert(`Speichern fehlgeschlagen: ${ex}`)
    }
  }

  if (!loaded) return null

  return (
    <>
      <div className="row">
        <label>Rhino-Anwendung</label>
        <div className="file-picker">
          <input type="text" value={rhinoApp}
                 onChange={e => setRhinoApp(e.target.value)}
                 placeholder="Rhinoceros 8" />
          <button onClick={pickRhino}>Wählen…</button>
        </div>
        <div className="hint">
          App-Name (in <code className="nowrap">/Applications</code> gesucht)
          oder absoluter Pfad zur <code className="nowrap">.app</code>.
        </div>
        <div className="hint-chips">
          <span className="hint-chips-label">Beispiele</span>
          <code className="nowrap">Rhinoceros 8</code>
          <code className="nowrap">/Applications/Rhino 8.app</code>
          <code className="nowrap">/Applications/Rhino 8 WIP.app</code>
          <button onClick={useDefault} className="hint-action">Standard verwenden</button>
        </div>
      </div>

      <div className="row">
        <label>Projekt-Template (.3dm)</label>
        <div className="file-picker">
          <input type="text" value={templatePath}
                 onChange={e => setTemplatePath(e.target.value)}
                 placeholder="(keine — Rhino-Standard wird verwendet)" />
          <button onClick={pickTemplate}>Wählen…</button>
          {templatePath && <button className="ghost" onClick={clearTemplate}>Entfernen</button>}
        </div>
        <div className="hint">
          Wird bei <em>Neues Projekt</em> als Startdatei kopiert — inkl. aller
          Layer, Schraffuren, Linientypen und Plot-Stile. So bleibt deine
          Ebenen-Hierarchie über alle Projekte konsistent.
        </div>
      </div>

      <div className="row">
        <label>Plugin-Autoload</label>
        <label style={{ display: 'flex', alignItems: 'center', gap: 8,
                        textTransform: 'none', letterSpacing: 0, fontSize: 12,
                        color: 'var(--text)' }}>
          <input type="checkbox" checked={autoLoadPlugin}
                 onChange={e => setAutoLoadPlugin(e.target.checked)}
                 style={{ width: 14, height: 14 }} />
          Dossier-Plugin automatisch nach Rhino-Start laden
        </label>
        <div className="file-picker" style={{ marginTop: 6 }}>
          <input type="text" value={pluginStartupPath}
                 onChange={e => setPluginStartupPath(e.target.value)}
                 placeholder={defaultStartup || 'gebündelte startup.py wird verwendet'} />
          <button onClick={pickStartup}>Wählen…</button>
          <button className="ghost" onClick={useDefaultStartup}>Standard</button>
        </div>
        <div className="hint">
          Aktiv: der Launcher trägt <code className="nowrap">_RunPythonScript &lt;Pfad&gt;</code>
          in Rhinos <em>Run these commands every time a model is opened</em> Liste
          ein (Options → General → Command Lists). Damit lädt das Plugin bei
          jedem Rhino-Start automatisch — egal ob via Launcher oder direkt.
          Idempotent: wird nichts überschrieben, wenn der Eintrag schon da ist.
        </div>
        <div className="hint-chips" style={{ marginTop: 4 }}>
          <button className="ghost" onClick={loadNow} disabled={pluginLoading}>
            <Icon name="refresh" size={14} />
            {pluginLoading ? 'Setze Eintrag…' : 'Autostart-Eintrag jetzt setzen'}
          </button>
          <span style={{ fontSize: 10, color: 'var(--text4)' }}>
            (greift beim nächsten Rhino-Start — Rhino dafür kurz beenden)
          </span>
        </div>
      </div>

      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 4 }}>
        {saved && <span style={{ fontSize: 11, color: 'var(--accent)', alignSelf: 'center' }}>✓ Gespeichert</span>}
        <button className="primary pill" onClick={save}>Speichern</button>
      </div>
    </>
  )
}

function TagsSettings() {
  const [tags, setTags] = useState([])
  const [loaded, setLoaded] = useState(false)
  const [draftName, setDraftName] = useState('')
  const [draftColor, setDraftColor] = useState('#5fa896')

  useEffect(() => {
    invoke('read_settings').then(s => {
      setTags(s?.tags || [])
      setLoaded(true)
    }).catch(() => setLoaded(true))
  }, [])

  const persist = async (next) => {
    setTags(next)
    const cur = await invoke('read_settings').catch(() => ({}))
    await invoke('save_settings', {
      settings: { ...cur, tags: next },
    }).catch(ex => alert(`Tags speichern: ${ex}`))
  }

  const addTag = () => {
    const name = draftName.trim()
    if (!name) return
    if (tags.some(t => t.name.toLowerCase() === name.toLowerCase())) {
      alert('Es gibt schon einen Tag mit diesem Namen.')
      return
    }
    const next = [...tags, { id: newTagId(), name, color: draftColor }]
    persist(next)
    setDraftName('')
  }

  const removeTag = (id) => {
    if (!confirm('Tag löschen? Projekte mit diesem Tag verlieren ihn.')) return
    persist(tags.filter(t => t.id !== id))
  }

  const updateTag = (id, patch) => {
    persist(tags.map(t => t.id === id ? { ...t, ...patch } : t))
  }

  if (!loaded) return null

  return (
    <>
      <div className="row">
        <label>Tag anlegen</label>
        <div className="tag-create">
          <input type="color" value={draftColor}
                 onChange={e => setDraftColor(e.target.value)}
                 className="color-input"
                 title="Farbe" />
          <input type="text" value={draftName}
                 onChange={e => setDraftName(e.target.value)}
                 onKeyDown={e => e.key === 'Enter' && addTag()}
                 placeholder="z.B. Wohnbau, Wettbewerb, Studie…" />
          <button className="primary pill" onClick={addTag}
                  disabled={!draftName.trim()}>
            <Icon name="plus" size={14} /> Hinzufügen
          </button>
        </div>
      </div>

      <div className="row">
        <label>Vorhandene Tags ({tags.length})</label>
        {tags.length === 0 ? (
          <div style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.5 }}>
            Noch keine Tags. Mit dem Formular oben kannst du Kategorien
            anlegen — Name + Farbe. Sie sind dann im Projekt-Dialog wählbar
            und filtern die Projektliste.
          </div>
        ) : (
          <div className="tag-list">
            {tags.map(t => (
              <div key={t.id} className="tag-row">
                <input type="color" value={t.color}
                       onChange={e => updateTag(t.id, { color: e.target.value })}
                       className="color-input"
                       title="Farbe" />
                <input type="text" value={t.name}
                       onChange={e => updateTag(t.id, { name: e.target.value })}
                       style={{ flex: 1 }} />
                <span className="tag-chip"
                      style={{
                        background: t.color,
                        color: isDarkColor(t.color) ? '#fff' : '#0a1715',
                      }}>
                  {t.name || '—'}
                </span>
                <button className="ghost danger icon-btn"
                        onClick={() => removeTag(t.id)}
                        title="Tag löschen">
                  <Icon name="trash" />
                </button>
              </div>
            ))}
          </div>
        )}
      </div>
    </>
  )
}

// Mapping Frontend-Key -> Label. Reihenfolge spielt fuer die UI eine Rolle.
const VIEW_COLOR_FIELDS = [
  { key: 'background', label: 'Viewport-Hintergrund', default: '#f0f0f0' },
  { key: 'gridLine',   label: 'Raster (Minor)',       default: '#bcbcbc' },
  { key: 'gridMajor',  label: 'Raster (Major)',       default: '#9e9e9e' },
  { key: 'gridX',      label: 'Raster X-Achse',       default: '#c84040' },
  { key: 'gridY',      label: 'Raster Y-Achse',       default: '#40a040' },
  { key: 'worldX',     label: 'Welt X-Achse',         default: '#ff0000' },
  { key: 'worldY',     label: 'Welt Y-Achse',         default: '#00b000' },
  { key: 'worldZ',     label: 'Welt Z-Achse',         default: '#0080ff' },
]

// Default-Schema fuer neue Projekte. Muss im Sync mit INITIAL_EBENEN in
// src/App.jsx des Rhino-Panels bleiben — wenn der User keine eigene Schema
// definiert, schickt das Plugin diese hier als FIRST_RUN-Default.
const DEFAULT_LAYER_SCHEMA = [
  { code: '00', name: 'RASTER',         color: '#484850', lw: 0.13 },
  { code: '01', name: 'VERMESSUNG',     color: '#707078', lw: 0.18 },
  { code: '10', name: 'SITUATION',      color: '#909090', lw: 0.18 },
  { code: '11', name: 'STRASSE',        color: '#a89070', lw: 0.18 },
  { code: '12', name: 'GEBAEUDE',       color: '#888888', lw: 0.25 },
  { code: '13', name: 'BAEUME',         color: '#50a050', lw: 0.13 },
  { code: '14', name: 'HOEHENLINIEN',   color: '#909050', lw: 0.18 },
  { code: '20', name: 'WAENDE',         color: '#0a0a0a', lw: 0.50 },
  { code: '21', name: 'TUEREN_FENSTER', color: '#5080c8', lw: 0.25 },
  { code: '22', name: 'MOEBEL',         color: '#909090', lw: 0.13 },
  { code: '25', name: 'STUETZEN',       color: '#c87050', lw: 0.50 },
  { code: '30', name: 'DECKEN',         color: '#605850', lw: 0.35 },
  { code: '31', name: 'DAECHER',        color: '#7a4a3a', lw: 0.35 },
  { code: '35', name: 'TRAEGER',        color: '#a87858', lw: 0.50 },
  { code: '50', name: 'TEXT',           color: '#d0d0d0', lw: 0.13 },
  { code: '60', name: 'PLANGRAFIK',     color: '#c0a040', lw: 0.13 },
  { code: '90', name: 'REFERENZEN',     color: '#585860', lw: 0.13 },
  { code: '99', name: 'KONSTRUKTION',   color: '#404048', lw: 0.13 },
]

// Built-in Presets — nicht in den Settings gespeichert, immer verfuegbar.
const BUILTIN_PRESETS = [
  {
    id: 'builtin:rhino',
    name: 'Rhino-Standard',
    builtin: true,
    colors: {},   // leere Colors = Rhino-Defaults nicht ueberschreiben
  },
  {
    id: 'builtin:dossier',
    name: 'Dossier-Standard',
    builtin: true,
    colors: {
      background: '#1c2421',
      gridLine:   '#2a3330',
      gridMajor:  '#3a4540',
      gridX:      '#7a4a3a',
      gridY:      '#4a7a5a',
      worldX:     '#c84a3a',
      worldY:     '#5fa896',
      worldZ:     '#4a8ac8',
    },
  },
]

function ViewSettings() {
  const [colors, setColors] = useState({})
  const [autoApply, setAutoApply] = useState(false)
  const [loaded, setLoaded] = useState(false)
  const [saved, setSaved] = useState(false)
  const [importBusy, setImportBusy] = useState(false)
  const [customPresets, setCustomPresets] = useState([])

  const allPresets = useMemo(() => [...BUILTIN_PRESETS, ...customPresets], [customPresets])

  useEffect(() => {
    invoke('read_dossier_settings').then(ds => {
      setColors(ds?.viewportColors || {})
      setAutoApply(!!ds?.autoApplyViewColors)
      setLoaded(true)
    }).catch(() => setLoaded(true))
    invoke('read_settings').then(s => setCustomPresets(s?.viewColorPresets || []))
      .catch(() => {})
  }, [])

  // Match: welcher Preset entspricht den aktuellen Colors exakt?
  const currentPresetId = useMemo(() => {
    const keys = VIEW_COLOR_FIELDS.map(f => f.key)
    const norm = (obj) => keys.map(k => (obj?.[k] || '').toLowerCase()).join('|')
    const cur = norm(colors)
    return allPresets.find(p => norm(p.colors) === cur)?.id || ''
  }, [colors, allPresets])

  const applyPreset = (presetId) => {
    const p = allPresets.find(pp => pp.id === presetId)
    if (!p) return
    const next = { ...p.colors }
    setColors(next)
    persist(next, autoApply)
  }

  const persistCustomPresets = async (next) => {
    setCustomPresets(next)
    const cur = await invoke('read_settings').catch(() => ({}))
    await invoke('save_settings', {
      settings: { ...cur, viewColorPresets: next },
    }).catch(ex => console.warn('preset save:', ex))
  }

  const saveAsPreset = async () => {
    const name = prompt('Name für dieses Preset:')
    if (!name || !name.trim()) return
    const id = 'custom_' + Math.random().toString(36).slice(2, 9)
    persistCustomPresets([...customPresets, { id, name: name.trim(), colors }])
  }

  const deletePreset = async (id) => {
    const p = customPresets.find(pp => pp.id === id)
    if (!p) return
    if (!confirm(`Preset „${p.name}" löschen?`)) return
    persistCustomPresets(customPresets.filter(pp => pp.id !== id))
  }

  const exportPreset = async (id) => {
    const p = allPresets.find(pp => pp.id === id) || { name: 'export', colors }
    const payload = JSON.stringify({
      dossier: { kind: 'view-color-preset', version: 1 },
      name: p.name, colors: p.colors,
    }, null, 2)
    const target = await saveDialog({
      title: 'Preset exportieren',
      defaultPath: `dossier-view-${p.name.replace(/\s+/g, '-').toLowerCase()}.json`,
      filters: [{ name: 'JSON', extensions: ['json'] }],
    })
    if (typeof target !== 'string') return
    try {
      await invoke('write_text_file', { path: target, content: payload })
      alert(`Preset exportiert:\n${target}`)
    } catch (ex) {
      alert(`Export: ${ex}`)
    }
  }

  const importPreset = async () => {
    const sel = await openDialog({
      title: 'Preset importieren',
      filters: [{ name: 'JSON', extensions: ['json'] }],
    })
    if (typeof sel !== 'string') return
    try {
      const raw = await invoke('read_text_file', { path: sel })
      const parsed = JSON.parse(raw)
      if (!parsed?.colors || !parsed?.name) {
        alert('Format unbekannt — JSON muss "name" und "colors" enthalten.')
        return
      }
      const id = 'custom_' + Math.random().toString(36).slice(2, 9)
      persistCustomPresets([...customPresets, { id, name: parsed.name, colors: parsed.colors }])
      alert(`Preset „${parsed.name}" importiert.`)
    } catch (ex) {
      alert(`Import: ${ex}`)
    }
  }

  // Lokale Aenderung sofort persistieren, ohne Apply
  const persist = async (nextColors, nextAuto) => {
    const cur = await invoke('read_dossier_settings').catch(() => ({}))
    await invoke('save_dossier_settings', {
      settings: {
        ...cur,
        viewportColors: nextColors,
        autoApplyViewColors: !!nextAuto,
      },
    }).catch(ex => console.warn('view-colors save:', ex))
  }

  const setColor = (key, value) => {
    const next = { ...colors, [key]: value || null }
    setColors(next)
    persist(next, autoApply)
  }

  const setAutoApplyFlag = (v) => {
    setAutoApply(v)
    persist(colors, v)
  }

  const applyNow = async () => {
    try {
      await invoke('apply_view_colors_now')
      setSaved(true)
      setTimeout(() => setSaved(false), 1800)
    } catch (ex) {
      alert(`Anwenden fehlgeschlagen: ${ex}`)
    }
  }

  const resetAll = () => {
    if (!confirm('Alle Farben zurücksetzen?')) return
    setColors({})
    persist({}, autoApply)
  }

  const importDisplayMode = async () => {
    const sel = await openDialog({
      title: 'Display-Mode (.ini) wählen',
      filters: [{ name: 'Rhino Display Mode', extensions: ['ini'] }],
    })
    if (typeof sel !== 'string') return
    try {
      setImportBusy(true)
      await invoke('queue_import_display_mode', { path: sel })
      alert(`Importiert. Rhino-Plugin übernimmt beim nächsten Idle-Tick (~1 s).\n\n${sel}`)
    } catch (ex) {
      alert(`Import fehlgeschlagen: ${ex}`)
    } finally {
      setImportBusy(false)
    }
  }

  if (!loaded) return null

  return (
    <>
      <div className="row">
        <label>Preset</label>
        <div className="preset-bar">
          <select value={currentPresetId}
                  onChange={e => applyPreset(e.target.value)}
                  style={{ flex: 1 }}>
            <option value="">— Eigene Konfiguration</option>
            <optgroup label="Standard">
              {BUILTIN_PRESETS.map(p =>
                <option key={p.id} value={p.id}>{p.name}</option>
              )}
            </optgroup>
            {customPresets.length > 0 && (
              <optgroup label="Eigene">
                {customPresets.map(p =>
                  <option key={p.id} value={p.id}>{p.name}</option>
                )}
              </optgroup>
            )}
          </select>
          <button className="ghost" onClick={saveAsPreset}
                  title="Aktuelle Farben als neues Preset speichern">
            <Icon name="plus" size={14} /> Speichern als…
          </button>
          {currentPresetId && currentPresetId.startsWith('custom_') && (
            <button className="ghost icon-btn danger"
                    onClick={() => deletePreset(currentPresetId)}
                    title="Aktives Preset löschen">
              <Icon name="trash" />
            </button>
          )}
        </div>
        <div className="hint-chips" style={{ marginTop: 4 }}>
          <button className="ghost" style={{ fontSize: 10 }} onClick={importPreset}>
            Preset importieren…
          </button>
          {currentPresetId && (
            <button className="ghost" style={{ fontSize: 10 }}
                    onClick={() => exportPreset(currentPresetId)}>
              Aktives Preset exportieren…
            </button>
          )}
        </div>
      </div>

      <div className="row">
        <label>Viewport-Farben</label>
        <div className="color-grid">
          {VIEW_COLOR_FIELDS.map(f => (
            <div key={f.key} className="color-row">
              <input type="color"
                     value={colors[f.key] || f.default}
                     onChange={e => setColor(f.key, e.target.value)}
                     className="color-input" />
              <div className="color-row-text">
                <div className="color-row-label">{f.label}</div>
                <div className="color-row-hex">{colors[f.key] || '— Rhino-Default'}</div>
              </div>
              {colors[f.key] && (
                <button className="ghost icon-btn"
                        onClick={() => setColor(f.key, null)}
                        title="Auf Rhino-Default zurücksetzen">
                  <Icon name="close" size={14} />
                </button>
              )}
            </div>
          ))}
        </div>
      </div>

      <div className="row">
        <label style={{ display: 'flex', alignItems: 'center', gap: 8,
                        textTransform: 'none', letterSpacing: 0, fontSize: 12,
                        color: 'var(--text)' }}>
          <input type="checkbox" checked={autoApply}
                 onChange={e => setAutoApplyFlag(e.target.checked)}
                 style={{ width: 14, height: 14 }} />
          Beim Rhino-Start automatisch anwenden
        </label>
      </div>

      <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
        {saved && <span style={{ fontSize: 11, color: 'var(--accent)', alignSelf: 'center' }}>✓ Angewendet</span>}
        <button className="ghost" onClick={resetAll}>Alle zurücksetzen</button>
        <button className="primary pill" onClick={applyNow}>Jetzt anwenden</button>
      </div>

      <div className="row" style={{ marginTop: 18, borderTop: '1px solid var(--border)', paddingTop: 18 }}>
        <label>Display-Modes importieren</label>
        <div style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.6 }}>
          Importiert eine Display-Mode <code>.ini</code>-Datei in Rhino.
          Die Stile lassen sich in Rhinos <em>Options → Display Modes →
          Export</em> erzeugen. So baust du dir deine Dossier-Stile
          (Hardline, Soft-Shaded, Plan-1:50, etc.) einmal und kannst sie auf
          jedem Mac in zwei Klicks importieren.
        </div>
        <button className="primary pill" onClick={importDisplayMode}
                disabled={importBusy} style={{ marginTop: 10 }}>
          <Icon name="plus" size={14} />
          {importBusy ? 'Importiere…' : '.ini-Datei wählen'}
        </button>
      </div>
    </>
  )
}

function EbenenSchemaSettings() {
  const [schema, setSchema] = useState([])
  const [loaded, setLoaded] = useState(false)
  const [saved, setSaved] = useState(false)
  const [importing, setImporting] = useState(false)

  const reload = useCallback(() => {
    return invoke('read_dossier_settings').then(ds => {
      const s = (ds?.layerSchema && ds.layerSchema.length)
        ? ds.layerSchema
        : DEFAULT_LAYER_SCHEMA
      setSchema(s)
      setLoaded(true)
    })
  }, [])

  useEffect(() => {
    reload().catch(() => { setSchema(DEFAULT_LAYER_SCHEMA); setLoaded(true) })
  }, [reload])

  const persist = async (next) => {
    setSchema(next)
    const cur = await invoke('read_dossier_settings').catch(() => ({}))
    await invoke('save_dossier_settings', {
      settings: { ...cur, layerSchema: next },
    }).catch(ex => console.warn('schema save:', ex))
    setSaved(true)
    setTimeout(() => setSaved(false), 1500)
  }

  const updateRow = (i, patch) => {
    persist(schema.map((row, idx) => idx === i ? { ...row, ...patch } : row))
  }

  const addRow = () => {
    // Naechste freie Code aus dem hoechsten + 1
    const max = schema.reduce((m, r) => Math.max(m, parseInt(r.code, 10) || 0), 0)
    const code = String(max + 1).padStart(2, '0')
    persist([...schema, { code, name: 'NEU', color: '#888888', lw: 0.13 }])
  }

  const removeRow = (i) => {
    if (!confirm(`Ebene „${schema[i].code}_${schema[i].name}" entfernen?`)) return
    persist(schema.filter((_, idx) => idx !== i))
  }

  const moveRow = (i, dir) => {
    const j = i + dir
    if (j < 0 || j >= schema.length) return
    const next = [...schema]
    ;[next[i], next[j]] = [next[j], next[i]]
    persist(next)
  }

  const resetToDefault = () => {
    if (!confirm('Schema auf Dossier-Standard zurücksetzen?')) return
    persist(DEFAULT_LAYER_SCHEMA)
  }

  const importFromRhino = async () => {
    if (!confirm('Aktuelles Schema mit den Ebenen aus dem laufenden Rhino-Projekt überschreiben?')) return
    try {
      setImporting(true)
      await invoke('request_ebenen_export')
      // Plugin verarbeitet im naechsten Idle-Tick (~1s). Wir pollen kurz.
      for (let i = 0; i < 8; i++) {
        await new Promise(r => setTimeout(r, 600))
        const ds = await invoke('read_dossier_settings').catch(() => null)
        if (ds && !ds.pendingExportEbenen) {
          await reload()
          setImporting(false)
          return
        }
      }
      setImporting(false)
      alert('Rhino hat nicht innerhalb von 5 s reagiert. Läuft Rhino mit aktivem Dossier-Plugin?')
    } catch (ex) {
      setImporting(false)
      alert(`Import: ${ex}`)
    }
  }

  const exportSchema = async () => {
    const payload = JSON.stringify({
      dossier: { kind: 'layer-schema', version: 1 },
      schema,
    }, null, 2)
    const target = await saveDialog({
      title: 'Schema exportieren',
      defaultPath: 'dossier-ebenen-schema.json',
      filters: [{ name: 'JSON', extensions: ['json'] }],
    })
    if (typeof target !== 'string') return
    try {
      await invoke('write_text_file', { path: target, content: payload })
      alert(`Schema exportiert:\n${target}`)
    } catch (ex) {
      alert(`Export: ${ex}`)
    }
  }

  const importSchema = async () => {
    const sel = await openDialog({
      title: 'Schema importieren',
      filters: [{ name: 'JSON', extensions: ['json'] }],
    })
    if (typeof sel !== 'string') return
    try {
      const raw = await invoke('read_text_file', { path: sel })
      const parsed = JSON.parse(raw)
      const next = parsed?.schema || parsed
      if (!Array.isArray(next)) {
        alert('Format unbekannt — JSON muss ein Array mit Layer-Templates enthalten.')
        return
      }
      // Validierung: jeder Eintrag braucht code, name, color, lw
      const clean = next.filter(r => r.code && r.name && r.color !== undefined && r.lw !== undefined)
      if (clean.length === 0) {
        alert('Keine gültigen Einträge im Import gefunden.')
        return
      }
      persist(clean)
      alert(`${clean.length} Ebenen importiert.`)
    } catch (ex) {
      alert(`Import: ${ex}`)
    }
  }

  if (!loaded) return null

  return (
    <>
      <div className="row">
        <label>Default-Sublayer für neue Projekte</label>
        <div className="hint">
          Diese Liste wird bei <em>Neues Projekt</em> in Rhino als Anfangs-Schema
          verwendet. Pro Sublayer: Code (für die Hierarchie 01_WAND, …), Name,
          Farbe und Stiftdicke. Reihenfolge entspricht der Anzeige in Rhinos
          Layer-Panel.
        </div>
      </div>

      <div className="schema-grid">
        <div className="schema-header">
          <div>Code</div>
          <div>Name</div>
          <div style={{ textAlign: 'center' }}>Farbe</div>
          <div>Stift (mm)</div>
          <div></div>
        </div>
        {schema.map((row, i) => (
          <div key={i} className="schema-row">
            <input type="text" value={row.code}
                   maxLength={3}
                   onChange={e => updateRow(i, { code: e.target.value.toUpperCase() })}
                   style={{ fontFamily: 'var(--font-mono)', textAlign: 'center' }} />
            <input type="text" value={row.name}
                   onChange={e => updateRow(i, { name: e.target.value.toUpperCase() })}
                   style={{ fontFamily: 'var(--font-mono)' }} />
            <input type="color" value={row.color}
                   onChange={e => updateRow(i, { color: e.target.value })}
                   className="color-input"
                   style={{ margin: '0 auto', display: 'block' }} />
            <input type="number" value={row.lw}
                   step={0.01} min={0.05} max={2.0}
                   onChange={e => updateRow(i, { lw: parseFloat(e.target.value) || 0.13 })}
                   style={{ fontFamily: 'var(--font-mono)' }} />
            <div style={{ display: 'flex', gap: 2 }}>
              <button className="ghost icon-btn"
                      onClick={() => moveRow(i, -1)}
                      disabled={i === 0}
                      title="Nach oben">↑</button>
              <button className="ghost icon-btn"
                      onClick={() => moveRow(i, 1)}
                      disabled={i === schema.length - 1}
                      title="Nach unten">↓</button>
              <button className="ghost danger icon-btn"
                      onClick={() => removeRow(i)}
                      title="Entfernen">
                <Icon name="trash" size={14} />
              </button>
            </div>
          </div>
        ))}
      </div>

      <div className="hint-chips" style={{ marginTop: 14 }}>
        <button className="ghost" onClick={addRow}>
          <Icon name="plus" size={14} /> Zeile hinzufügen
        </button>
        <button className="ghost" onClick={importFromRhino} disabled={importing}>
          {importing ? 'Lese aus Rhino …' : 'Aus laufendem Rhino importieren'}
        </button>
        <button className="ghost" onClick={importSchema}>Aus JSON…</button>
        <button className="ghost" onClick={exportSchema}>Exportieren…</button>
        <button className="ghost danger" onClick={resetToDefault}>
          Auf Dossier-Standard zurücksetzen
        </button>
        {saved && <span style={{ fontSize: 11, color: 'var(--accent)' }}>✓ Gespeichert</span>}
      </div>
    </>
  )
}

function LayoutSettings() {
  const [layouts, setLayouts] = useState([])
  const [selected, setSelected] = useState('')
  const [autoApply, setAutoApply] = useState(false)
  const [loaded, setLoaded] = useState(false)
  const [saved, setSaved] = useState(false)

  useEffect(() => {
    Promise.all([
      invoke('list_window_layouts').catch(() => []),
      invoke('read_dossier_settings').catch(() => ({})),
    ]).then(([list, ds]) => {
      setLayouts(list || [])
      setSelected(ds?.windowLayout || '')
      setAutoApply(!!ds?.autoApplyLayout)
      setLoaded(true)
    })
  }, [])

  const save = async () => {
    try {
      await invoke('save_dossier_settings', {
        settings: { windowLayout: selected || null, autoApplyLayout: autoApply },
      })
      setSaved(true)
      setTimeout(() => setSaved(false), 1500)
    } catch (ex) {
      alert(`Speichern fehlgeschlagen: ${ex}`)
    }
  }

  if (!loaded) return null

  return (
    <div className="row">
      <label>Standard-Layout für Dossier</label>
      {layouts.length === 0 ? (
        <div style={{ padding: 12, background: 'var(--bg-panel)', border: '1px solid var(--border)',
                      borderRadius: 6, fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5 }}>
          Keine Rhino Window-Layouts gefunden. Erstelle erst eines in Rhino via
          <em> Window → Window Layouts → Save</em>, dann hier auswählen.
        </div>
      ) : (
        <select value={selected}
                onChange={e => setSelected(e.target.value)}
                style={{
                  fontFamily: 'inherit', fontSize: 13,
                  background: 'var(--bg-base)', color: 'var(--text)',
                  border: '1px solid var(--border)', borderRadius: 6,
                  padding: '6px 10px',
                }}>
          <option value="">(keines)</option>
          {layouts.map(l => <option key={l} value={l}>{l}</option>)}
        </select>
      )}
      <label style={{ display: 'flex', alignItems: 'center', gap: 8, textTransform: 'none', letterSpacing: 0,
                      fontSize: 12, color: 'var(--text)', marginTop: 8 }}>
        <input type="checkbox" checked={autoApply}
               onChange={e => setAutoApply(e.target.checked)}
               style={{ width: 14, height: 14 }} />
        Beim Rhino-Start automatisch anwenden
      </label>
      <div style={{ fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.5, marginTop: 4 }}>
        Dossier liest diese Einstellung beim Öffnen eines Projekts und ruft Rhinos
        Window-Layout auf. So spart man sich manuelles Wiederherstellen von Panel-Anordnungen.
      </div>
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
        {saved && <span style={{ fontSize: 11, color: 'var(--accent)', alignSelf: 'center' }}>✓ Gespeichert</span>}
        <button className="primary pill" onClick={save}>Speichern</button>
      </div>
    </div>
  )
}

function UpdatesSettings() {
  const [version, setVersion] = useState('')
  const [lastCheck, setLastCheck] = useState(() => getLastUpdateCheck())
  const [state, setState] = useState('idle')
  const [update, setUpdate] = useState(null)
  const [error, setError] = useState(null)
  const [downloaded, setDownloaded] = useState(0)
  const [total, setTotal] = useState(0)

  useEffect(() => {
    if (!isTauri()) return
    import('@tauri-apps/api/app').then(({ getVersion }) => {
      getVersion().then(setVersion).catch(() => {})
    })
  }, [])

  const runCheck = useCallback(async () => {
    setError(null)
    if (!isTauri()) {
      setState('not-tauri')
      return
    }
    setState('checking')
    try {
      const res = await checkForAppUpdate({ respectSkip: false })
      setLastCheck(getLastUpdateCheck())
      if (res.available) {
        setUpdate(res.update)
        setState('available')
      } else {
        setUpdate(null)
        setState('no-update')
      }
    } catch (e) {
      console.error('Update-Check fehlgeschlagen:', e)
      setError(String(e?.message || e))
      setState('idle')
    }
  }, [])

  const install = async () => {
    if (!update) return
    setError(null)
    try {
      setState('downloading')
      setDownloaded(0)
      setTotal(0)
      await installAppUpdate(update, (event) => {
        if (event.event === 'Started') {
          setTotal(event.data.contentLength || 0)
        } else if (event.event === 'Progress') {
          setDownloaded((d) => d + (event.data.chunkLength || 0))
        } else if (event.event === 'Finished') {
          setState('installing')
        }
      })
    } catch (e) {
      setError(String(e?.message || e))
      setState('available')
    }
  }

  const skipVersion = () => {
    skipUpdateVersion(update?.version)
    setUpdate(null)
    setState('idle')
  }

  const isBusy = state === 'downloading' || state === 'installing'
  const pct = total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : null

  return (
    <div className="row">
      <div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0',
                    fontSize: 12, borderBottom: '1px solid var(--border)' }}>
        <span style={{ color: 'var(--text-muted)' }}>Aktuelle Version</span>
        <strong>{version || '—'}</strong>
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0',
                    fontSize: 12, borderBottom: '1px solid var(--border)', marginBottom: 12 }}>
        <span style={{ color: 'var(--text-muted)' }}>Letzte Prüfung</span>
        <span>{formatLastCheck(lastCheck)}</span>
      </div>

      <button className="primary pill" onClick={runCheck} disabled={state === 'checking' || isBusy}>
        {state === 'checking' ? 'Wird geprüft …' : 'Nach Updates suchen'}
      </button>

      {state === 'no-update' && (
        <div style={{ marginTop: 10, padding: '10px 12px', background: 'rgba(90, 158, 90, 0.12)',
                      border: '1px solid var(--accent)', borderRadius: 6, fontSize: 12,
                      color: 'var(--accent)', lineHeight: 1.5 }}>
          ✓ Dossier ist auf dem neuesten Stand.
        </div>
      )}

      {state === 'available' && update && (
        <div style={{ marginTop: 10, padding: '12px 14px', background: 'var(--bg-panel)',
                      border: '1px solid var(--accent)', borderRadius: 8 }}>
          <div style={{ fontSize: 10, letterSpacing: '0.12em', color: 'var(--accent)',
                        fontWeight: 600, marginBottom: 4 }}>UPDATE VERFÜGBAR</div>
          <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 6 }}>Dossier {update.version}</div>
          {update.body && (
            <div style={{ fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5,
                          whiteSpace: 'pre-wrap', marginBottom: 10, maxHeight: 160, overflowY: 'auto' }}>
              {update.body}
            </div>
          )}
          <button className="primary pill" style={{ width: '100%', marginBottom: 6 }} onClick={install} disabled={isBusy}>
            {isBusy ? 'Bitte warten …' : 'Installieren und neu starten'}
          </button>
          <button onClick={skipVersion} disabled={isBusy} style={{ width: '100%', fontSize: 12 }}>
            Diese Version überspringen
          </button>
        </div>
      )}

      {isBusy && (
        <div style={{ marginTop: 12 }}>
          <div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 6, letterSpacing: '0.04em' }}>
            {state === 'downloading'
              ? (pct !== null ? `Wird heruntergeladen … ${pct}%` : 'Wird heruntergeladen …')
              : 'Wird installiert …'}
          </div>
          <div style={{ height: 4, background: 'var(--bg-elev)', borderRadius: 2, overflow: 'hidden' }}>
            <div style={{
              height: '100%',
              width: pct !== null ? `${pct}%` : '100%',
              background: 'var(--accent)',
              transition: 'width 0.2s',
              animation: pct === null ? 'dossier-us-pulse 1.2s ease-in-out infinite' : undefined,
            }} />
          </div>
          <style>{`@keyframes dossier-us-pulse { 0%,100% { opacity: 0.5; } 50% { opacity: 1; } }`}</style>
        </div>
      )}

      {state === 'not-tauri' && (
        <div style={{ marginTop: 10, padding: '10px 12px', background: 'var(--bg-panel)',
                      border: '1px solid var(--border)', borderRadius: 6, fontSize: 12,
                      color: 'var(--text-muted)' }}>
          Updates sind nur in der Desktop-App verfügbar.
        </div>
      )}

      {error && (
        <div style={{ marginTop: 10, padding: '10px 12px', background: 'rgba(200, 112, 80, 0.12)',
                      border: '1px solid var(--danger)', borderRadius: 6, fontSize: 12,
                      color: 'var(--danger)' }}>
          {error}
        </div>
      )}

      <p style={{ fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.6, marginTop: 14 }}>
        Updates werden automatisch beim Start der App geprüft.
      </p>
    </div>
  )
}

function ProjectDialog({ mode, project, modules, onCancel, onSave }) {
  const [name, setName] = useState(project?.name || '')
  const [path, setPath] = useState(project?.path || '')
  const [picked, setPicked] = useState(new Set(project?.modules || ['ebenen', 'oberleiste']))
  const [useTemplate, setUseTemplate] = useState(true)  // nur bei mode='new' wirksam
  const [hasTemplate, setHasTemplate] = useState(false)
  const [windowLayout, setWindowLayout] = useState(project?.windowLayout || '')
  const [layouts, setLayouts] = useState([])
  const [tagIds, setTagIds] = useState(new Set(project?.tagIds || []))
  const [allTags, setAllTags] = useState([])
  const [extraFiles, setExtraFiles] = useState(project?.extraFiles || [])

  useEffect(() => {
    invoke('read_settings').then(s => {
      if (mode === 'new') setHasTemplate(!!s?.templatePath)
      setAllTags(s?.tags || [])
    }).catch(() => {})
    invoke('list_window_layouts').then(setLayouts).catch(() => {})
  }, [mode])

  const toggleTag = (id) => {
    setTagIds(prev => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id); else next.add(id)
      return next
    })
  }

  const addExtraFile = async () => {
    const sel = await openDialog({
      title: 'Weitere Projektdatei (.3dm) wählen',
      filters: [{ name: 'Rhino', extensions: ['3dm'] }],
    })
    if (typeof sel !== 'string') return
    if (sel === path) { alert('Das ist bereits die Hauptdatei.'); return }
    if (extraFiles.includes(sel)) return
    setExtraFiles([...extraFiles, sel])
  }

  const removeExtraFile = (p) => {
    setExtraFiles(extraFiles.filter(x => x !== p))
  }

  const effective = useMemo(() => resolveDeps(picked, modules), [picked, modules])
  const locked    = useMemo(() => {
    const out = new Set()
    for (const id of effective) {
      if (!picked.has(id)) out.add(id)
    }
    return out
  }, [picked, effective, modules])

  const toggle = (id) => {
    if (locked.has(id)) return
    setPicked(prev => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id); else next.add(id)
      return next
    })
  }

  const pickFile = async () => {
    // Bei "neu" wollen wir wirklich einen NEUEN Pfad — saveDialog zeigt das
    // dem User klar an ("Save As") und akzeptiert auch noch nicht existierende
    // Dateien. Bei "edit" zeigen wir openDialog (Datei muss existieren).
    const sel = mode === 'new'
      ? await saveDialog({
          title: '3dm-Datei anlegen',
          defaultPath: name.trim() ? `${name.trim()}.3dm` : 'untitled.3dm',
          filters: [{ name: 'Rhino', extensions: ['3dm'] }],
        })
      : await openDialog({
          title: '3dm-Datei waehlen',
          filters: [{ name: 'Rhino', extensions: ['3dm'] }],
        })
    if (typeof sel === 'string') setPath(sel)
  }

  return (
    <div className="dialog-bg" onClick={(e) => { if (e.target === e.currentTarget) onCancel() }}>
      <div className="dialog">
        <header>{mode === 'new' ? 'Neues Projekt' : 'Projekt bearbeiten'}</header>
        <div className="body">
          <div className="row">
            <label>Projekt-Name</label>
            <input type="text" value={name} onChange={e => setName(e.target.value)}
                   placeholder="z.B. Wohnhaus Brunner" autoFocus />
          </div>
          <div className="row">
            <label>3dm-Datei</label>
            <div className="file-picker">
              <input type="text" value={path} readOnly
                     placeholder={mode === 'new' ? 'Speicherort wählen…' : 'Pfad zur 3dm-Datei'} />
              <button onClick={pickFile}>{mode === 'new' ? 'Anlegen…' : 'Wählen…'}</button>
            </div>
            {mode === 'new' && hasTemplate && (
              <label style={{ display: 'flex', alignItems: 'center', gap: 8,
                              textTransform: 'none', letterSpacing: 0, fontSize: 12,
                              color: 'var(--text)', marginTop: 6 }}>
                <input type="checkbox" checked={useTemplate}
                       onChange={e => setUseTemplate(e.target.checked)}
                       style={{ width: 14, height: 14 }} />
                Vorlage aus Einstellungen verwenden
              </label>
            )}
            {mode === 'new' && !hasTemplate && (
              <div style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.5, marginTop: 4 }}>
                Keine Vorlage gesetzt — die Datei wird mit Rhinos Standard angelegt.
                In den Einstellungen kannst du eine Template-3dm definieren.
              </div>
            )}
          </div>
          <div className="row">
            <label>Zusätzliche Projektdateien (.3dm)</label>
            {extraFiles.length === 0 ? (
              <div className="hint">
                Keine. Klick „Datei hinzufügen", wenn dein Projekt aus mehreren
                <code className="nowrap">.3dm</code>-Dateien besteht (z.B. Plan + Details + Präsentation).
              </div>
            ) : (
              <div className="extra-files">
                {extraFiles.map(f => (
                  <div key={f} className="extra-file-row">
                    <Icon name="folder" size={14} />
                    <span className="extra-file-path" title={f}>{f.split('/').pop()}</span>
                    <button className="ghost danger icon-btn"
                            onClick={() => removeExtraFile(f)}
                            title="Entfernen">
                      <Icon name="close" size={12} />
                    </button>
                  </div>
                ))}
              </div>
            )}
            <button className="ghost" onClick={addExtraFile} style={{ marginTop: 6 }}>
              <Icon name="plus" size={14} /> Datei hinzufügen
            </button>
          </div>

          <div className="row">
            <label>Tags</label>
            {allTags.length === 0 ? (
              <div style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.5 }}>
                Noch keine Tags definiert. In den Einstellungen → Tags
                kannst du eigene Kategorien (Name + Farbe) anlegen.
              </div>
            ) : (
              <div className="tag-picker">
                {allTags.map(t => {
                  const active = tagIds.has(t.id)
                  return (
                    <button key={t.id}
                            className={`tag-chip btn-tag ${active ? 'is-active' : ''}`}
                            style={{
                              background: active ? t.color : 'transparent',
                              borderColor: t.color,
                              color: active ? (isDarkColor(t.color) ? '#fff' : '#0a1715') : t.color,
                            }}
                            onClick={() => toggleTag(t.id)}>
                      {t.name}
                    </button>
                  )
                })}
              </div>
            )}
          </div>

          <div className="row">
            <label>Window-Layout für dieses Projekt</label>
            {layouts.length === 0 ? (
              <div style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.5 }}>
                Keine Rhino-Window-Layouts gefunden. In Rhino:
                <em> Window → Window Layouts → Save…</em>
              </div>
            ) : (
              <select value={windowLayout}
                      onChange={e => setWindowLayout(e.target.value)}
                      style={{ width: '100%' }}>
                <option value="">— Standard aus Einstellungen verwenden</option>
                {layouts.map(l => <option key={l} value={l}>{l}</option>)}
              </select>
            )}
            <div style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.5, marginTop: 4 }}>
              Wird beim Öffnen dieses Projekts in Rhino angewendet.
              Überschreibt das globale Default-Layout.
            </div>
          </div>

          <div className="row">
            <label>Module ({[...effective].length} aktiv)</label>
            <div className="module-grid">
              {modules.map(m => {
                const isActive = effective.has(m.id)
                const isLocked = locked.has(m.id)
                return (
                  <div
                    key={m.id}
                    className={`module-row ${isActive ? 'active' : ''} ${isLocked ? 'locked' : ''}`}
                    onClick={() => toggle(m.id)}
                    title={isLocked ? 'Wird von einem anderen Modul gebraucht' : ''}
                  >
                    <div className="check">{isActive ? (isLocked ? '🔒' : '✓') : ''}</div>
                    <div className="info">
                      <div className="name">{m.name}</div>
                      <div className="desc">{m.description}</div>
                      {(m.dependsOn || []).length > 0 && (
                        <div className="dep">braucht: {m.dependsOn.join(', ')}</div>
                      )}
                    </div>
                  </div>
                )
              })}
            </div>
          </div>
        </div>
        <footer>
          <button onClick={onCancel}>Abbrechen</button>
          <button className="primary pill"
                  onClick={() => onSave({
                    name, path,
                    modules: [...effective],
                    useTemplate: mode === 'new' && useTemplate && hasTemplate,
                    windowLayout: windowLayout || null,
                    tagIds: [...tagIds],
                    extraFiles,
                  })}>
            {mode === 'new' ? 'Anlegen' : 'Speichern'}
          </button>
        </footer>
      </div>
    </div>
  )
}
